Domain Objects and Services

2019-01-21 02:06发布

问题:

In this question someone replies "You never let the domain object implementations call services by themselves!". Is this statement a hard fast rule of DDD or does it depend on your own application and architecture?

Contrived example:

As an example lets suppose we have a UserImage object in our model that gets populated from an uploaded image by a user. And then lets suppose that we can submit this image to a 3rd party service that can identify thumb prints and return a Guid if a match is found.

public IThumbPrintService {
    Guid FindMatch(Bitmap image);
}

public class UserImage {
    public Bitmap Image {get; set;} 
    public Guid ThumbPrintId {get; set;}
    public bool FindThumbPrintMatch() {
       // Would you call the service from here?
       ThumbPrintId = _thumbPrintService.FindMatch(this.Image);
       return ! ThumbPrintId.CompareTo(Guid.Empty);
    }
}

public class RoboCopUserImageService : IUserImageService {
     // Or move the call to a service method 
     // since it depends on calling a separate service interface
     public bool FindThumbPrintMatch(UserImage userImage) {
        userImage.ThumbPrintId = _thumbPrintService.FindMatch(userImage.Image);
        return !userImage.ThumbPrintId.CompareTo(Guid.Empty);            
     }
}

What is avoided or gained by not letting domain objects call services themselves?

EDIT: Are there any good online articles that discuss this specific topic?

回答1:

This is the Spreadsheet Conundrum: does the phone dial the phone number, or does the phone number dial itself on the phone?

You might find Double Dispatch to be interesting reading, though overkill in your situation, I reckon.

The Single Responsibility Principle is often at odds with the OO tenet of Tell, Don't Ask. My feeling on the subject has oscillated, and I have settled on the following conditions when logic should go into a domain object:

  • The logic should be domain logic, i.e. nothing like image.load() or image.save()
  • The logic shouldn't give the object more than "one reason" to change in the domain. This is a re-statement of the SRP.
  • The logic shouldn't be forced into the object. In other words, if you end up doing something like trying to calculate the sum of some property in a list of domain objects without using a getter, think twice.

In your situation, I'd opt against putting the call to the service inside the entity object, mainly because the service doesn't seem like it is related to your domain, but more related to persistence. Domain objects should be coupled to domain concepts, and I don't think the service you gave qualifies.

An example where I think calling a service in an entity might be acceptable would be if your application used a third-party workflow server to manage parts of its state. Essentially, this is the State Pattern with the states defined at run-time.

I think it is acceptable to have domainObject.moveToNextState() (assuming this code "makes sense" in your ubiquitous language) call the service that talks to your server because the workflow server manages a part of the domain model.

I'll add that DDD is very interested with following the language of the domain. Do you hear domain experts saying "A user image finds if its thumb print matches those in the XYZ vendor service"? Or do they say "The XYZ vendor service, given a thumb print, indicates whether that thumb print exists"? Go with the one that makes the most sense in your domain.

Some more thoughts (I've thought about this issue a lot because it is central to design):

  • In the Evans DDD book, an Account entity has methods like credit(Amount), debit(Amount), transferTo(Account, Amount) and accrue(), but a FundsTransferService has a transfer(Account, Account, Amount) method. The transferTo method doesn't call any service, but merely handles the logic that involves Accounts, like crediting and debiting the right amounts.

    The FundsTransferService, in addition to co-ordination, has its own rules to check, rules that don't fit into Accounts. The exact amount to credit or debit might involve outside parties. This makes it awkward for transferTo to call the service.

  • For simple objects, like the UserImage, significant domain logic that can fit in the object itself might be scarce because it isn't, as far as I can tell, an Aggregate. Aggregates, I think, present more of an opportunity to house domain logic. The Account example is likely an Aggregate.


回答2:

One disadvantage I see is that allowing your domain object to call services may make it harder to serialize, or at least cause some issues after serializing it when someone on the other side calls its service method(s).



回答3:

If you allow an Entity Object to call a service it is performing two roles Data Object and Service Object. Generally, each object should have on responsibility, not only in implementation but also in usage.

In your case, the lowly UserImage seems to be both an Image and a ThumbPrint Recognizer.



回答4:

I think it's better not to call repositories or services from entities or value objects, but sometimes it's necessary, for example, if an entity has to return other entity which should be loaded from database but it cannot navigate to it using object graph. Then dependency inversion principle comes to help, which means that entities and value objects depend on interfaces of services and repositories and not on implementations.