Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In domain-driven design, would it be a violation of DDD to put calls to other objects' repostiories in a domain object?

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.

like image 555
Mark Rogers Avatar asked Feb 27 '09 19:02

Mark Rogers


3 Answers

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)
{
    ...
}
like image 166
mbillard Avatar answered Sep 28 '22 03:09

mbillard


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.

like image 25
Toran Billups Avatar answered Sep 28 '22 03:09

Toran Billups


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.

like image 37
Kevin Pang Avatar answered Sep 28 '22 04:09

Kevin Pang