views:

517

answers:

5

I'm currently refactoring some code on a project that is wrapping up, and I ended up putting a lot of business logic into service classes rather than in the domain objects. At this point most of the domain objects are data containers only. I had decided to write most of the business logic in service objects, and refactor everything afterwards into better, more reuseable, and more readable shapes. That way I could decide what code should be placed into domain objects, and which code should be spun off into new objects of their own, and what code should be left in a service class. So I have some code:

public decimal CaculateBatchTotal(VendorApplicationBatch batch)
{
     IList<VendorApplication> applications = AppRepo.GetByBatchId(batch.Id);

     if (applications == null || applications.Count == 0)
          throw new ArgumentException("There were no applications for this batch, that shouldn't be possible");
     decimal total = 0m;
     foreach (VendorApplication app in applications)
          total += app.Amount;
     return total;
}

This code seems like it would make a good addition to a domain object, because it's only input parameter is the domain object itself. Seems like a perfect candidate for some refactoring. But the only problem is that this object calls another object's repository. Which makes me want to leave it in the service class.

My questions are thus:

  1. Where would you put this code?
  2. Would you break this function up?
  3. Where would someone who's following strict Domain-Driven design put it?
  4. Why?

Thanks for your time.

Edit Note: Can't use an ORM on this one, so I can't use a lazy loading solution.

Edit Note2: I can't alter the constructor to take in parameters, because of how the would-be data layer instantiates the domain objects using reflection (not my idea).

Edit Note3: I don't believe that a batch object should be able to total just any list of applications, it seems like it should only be able to total applications that are in that particular batch. Otherwise, it makes more sense to me to leave the function in the service class.

+2  A: 

Why not pass in an IList<VendorApplication> as the parameter instead of a VendorApplicationBatch? The calling code for this presumably would come from a service which would have access to the AppRepo. That way your repository access will be up where it belongs while your domain function can remain blissfully ignorant of where that data came from.

Kevin Pang
If I put the code that totals a batch in the repository, but not the getbybatchid, there would be no guarentee that the passed in applications would be from that batch. I kind of feel like batch should only be able to total the applications that it contains.
Mark Rogers
totals a batch in the repository -> totals a batch in the batch domain object
Mark Rogers
I'm not sure I understand. What I'm suggesting is you keep your code the way it is, except instead of passing in a VendorApplicationBatch you pass in the list of applications. GetByBatchID is still in the repository, the code that totals is still in the domain.
Kevin Pang
public decimal CalculateBatchTotal(IList<VendorApplication> applications)
Kevin Pang
Well I'm trying to decide if I should put this function in the VendorBatchApplication object. It seems like a batch should only be able to total applications that are in it.
Mark Rogers
Ah, ok I understand now. I've run into similar situations before. Unfortunately there isn't really a hard-and-fast rule to go by here. Services tend to be what I go with when I need repository access, but I agree it's much more intuitive to write something like vendorBatchApplication.Calculate()
Kevin Pang
It really depends on who you're coding for. If you're exposing a public API, it may be preferable to go with putting the function in the VendorBatchApplication class. If you're not, I'd say it's "cleaner" to keep all repository access in services.
Kevin Pang
Yeah, I continue to torn between leaving it in the service class, and moving it to the domain class. It seems like there is no clear answer.
Mark Rogers
+4  A: 

I'm no expert on DDD but I remember an article from the great Jeremy Miller that answered this very question for me. You would typically want logic related to your domain objects - inside those objects, but your service class would exec the methods that contain this logic. This helped me push domain specific logic into the entity classes, and keep my service classes less bulky (as I found myself putting to much logic inside the service classes like you mentioned)

Edit: Example

I use the enterprise library for simple validation, so in the entity class I will set an attribute like so:

 [StringLengthValidator(1, 100)]
 public string Username {
     get { return mUsername; }
     set { mUsername = value; }
 }

The entity inherits from a base class that has the following "IsValid" method that will ensure each object meets the validation criteria

     public bool IsValid()
     {
         mResults = new ValidationResults();
         Validate(mResults);

         return mResults.IsValid();
     }

     [SelfValidation()]
     public virtual void Validate(ValidationResults results)
     {
         if (!object.ReferenceEquals(this.GetType(), typeof(BusinessBase<T>))) {
             Validator validator = ValidationFactory.CreateValidator(this.GetType());
             results.AddAllResults(validator.Validate(this));
         }
         //before we return the bool value, if we have any validation results map them into the
         //broken rules property so the parent class can display them to the end user
         if (!results.IsValid()) {
             mBrokenRules = new List<BrokenRule>();
             foreach (Microsoft.Practices.EnterpriseLibrary.Validation.ValidationResult result in results) {
                 mRule = new BrokenRule();
                 mRule.Message = result.Message;
                 mRule.PropertyName = result.Key.ToString();
                 mBrokenRules.Add(mRule);
             }
         }
     }

Next we need to execute this "IsValid" method in the service class save method, like so:

 public void SaveUser(User UserObject)
 {
     if (UserObject.IsValid()) {
         mRepository.SaveUser(UserObject);
     }
 }

A more complex example might be a bank account. The deposit logic will live inside the account object, but the service class will call this method.

Toran Billups
+1  A: 

As I understand it (not enough info to know if this is the right design) VendorApplicationBatch should contain a lazy loaded IList inside the domain object, and the logic should stay in the domain.

For Example (air code):

public class VendorApplicationBatch  {

    private IList<VendorApplication> Applications {get; set;};   

    public decimal CaculateBatchTotal()
    {
        if (Applications == null || Applications.Count == 0)
            throw new ArgumentException("There were no applications for this batch, that shouldn't be possible");

        decimal Total = 0m;
        foreach (VendorApplication App in Applications)
            Total += App.Amount;
       return Total;
    }
}

This is easily done with an ORM like NHibernate and I think it would be the best solution.

gcores
I agree with you that this is the best solution. Unfortunatly I was not around when the decision on how to design the datalayer (or lack there of) was made, so I cannot use an ORM, and I made a kick-ass repository system in it's place.
Mark Rogers
Then the answer is to have the service get the VendorApplication list and the Batch object should do the logic. Ex: VendorApplicationBatch.CaculateBatchTotal(IList<VendorApplication> applications)
gcores
I don't think it's correct to allow a batch to total applications that might belong to another batch, there must be some way to guarentee that a batch can only total it's own vendor applications. Thus I can't in good faith, put this function in the batch.
Mark Rogers
Does VendorApplication know which batch it belongs to? You could check for that. Or you could load the Batch with all VendorApplications from the beggining. Otherwise just keep it in the service, it's hard to do DDD when you can't lazy load.
gcores
Yes the vendor applications have a batch id, I guess I could check for that. Maybe your right about that, it just seems a touch ackward, but of course many problems have no perfect solution.
Mark Rogers
+2  A: 

You shouldn't even have access to the repositories from the domain object.

What you can do is either let the service give the domain object the appropriate info or have a delegate in the domain object which is set by a service or in the constructor.

public DomainObject(delegate getApplicationsByBatchID)
{
    ...
}
GoodEnough
Good idea, I like it.
Mark Rogers
Agreed, so for this instance I would do the work that requires repository calls in the service class, then pass a collection or single object to the entity if a calculation is needed
Toran Billups
Yes, the domain object would use it's delegate to get whatever data it needs, the delegate would be set by whatever creates the domain object. The delegate can be a service method or a repository method directly.
GoodEnough
This answer is good, but I'm wondering what you think about the coupling implications. Certainly I can pass anytype of delegate function in there, but it does couple the domain object to data retrieval in general. Is that a bad idea?
Mark Rogers
Also, I can't use the constructor to pass in a delegate, because of how this current system relies on reflection to instantiate objects. Not my design, but part of being a pro is dealing with other people's f'd up conventions
Mark Rogers
Yes it's ok to couple the domain object with data retrieval if that makes sense for the object. Either way it would have needed to retrieve data right?
GoodEnough
You could use an Initialize method or just a setter on the delegate and in the method, check if the delegate is NULL before using it. Then either throw an exception or do as if you received an empty collection, depending on your situation.
GoodEnough
nice thanks for the advice
Mark Rogers
A: 

It seems to me that your CalculateTotal is a service for collections of VendorApplication's, and that returning the collection of VendorApplication's for a Batch fits naturally as a property of the Batch class. So some other service/controller/whatever would retrieve the appropriate collection of VendorApplication's from a batch and pass them to the VendorApplicationTotalCalculator service (or something similar). But that may break some DDD aggregate root service rules or some such thing I'm ignorant of (DDD novice).

qstarin