Architecture – What kind of logic can Domain Objects realistically contain

Architecturedesign-patternsdomain-driven-designdomain-modelrepository

I have been struggling with this concept in the context of web applications ever since I first read about it. The theory states that the domain objects should encapsulate their behaviour and business logic. A model which contains only data and has its logic somewhere outside is called an "anemic domain model", which is a bad thing. Also, the domain should not perform data access.

If for instance I had a social app which had a bunch of objects of type User, and users should be able to add other users as their friends, the User class should contain a method named Befriend(User user) so that I could do something like userA.Befriend(userB).

class User {

    Friends[] friends;

    void Befriend(User user) { ... }
}

However, the act of befriending might contain some restrictions and so I would have to do some validation in my Befriend method. Here are some purely theoretical restrictions:

  1. The user must not already be your friend
  2. You and the other user must not have common friends
  3. In Bucharest it must be raining

Now let's imagine that the friends lists might be huge, userA might have 50.000 friends, userB might have 100.000 friends.
So, for validating 1 and 2 it wouldn't be efficient to eagerly pull the entire friends lists from the database when constructing the user object and then doing those checks in my Befriend method iterating the friends list. In the database I have indexes and checks like these would be trivial (and fast). So naturally I would prefer to put these queries somewhere in my Data Access Layer and use them whenever needed.

class FriendsRepository: IFriendsRepository {

    bool HasFriend(User user, User friend);
    bool HasCommonFriends(User userA, User userB);

}

But how am I supposed to use this object inside my Befriend method from my User object? People say domain objects must not use repositories (even through abstractions such as interfaces), though there seems to be some disagreement here. Say I violated this rule. Domain objects don't benefit from Dependency Injection so I would have to change my Befriend method to:

void Befriend(User user, IFriendsRepository friendsRepository) { ... }

Alright. Now what about the weather? That's something completely unrelated to our entity and that information comes from an IWeatherService. Again, I need it in my Befriend method.

void Befriend(User user, IFriendsRepository friendsRepository, IWeatherService weatherService) { ... }

This already makes me feel like this method does not belong inside the User class. I have a lot of external dependencies and I don't get Dependency Injection which sucks. But pulling this out from the User to a service (or whatever) inside my Application Layer makes my domain model anemic. I very rarely encountered methods which could either be executed without validation or contain only extremely simple validation rules, only depending on the immediately available properties on the said entity (like primitive fields for instance, such as Username string, ActiveUntil date etc.).

So I'm left asking: what kind of methods could naturally fit in the domain objects? Let's be honest, real apps often deal with huge amounts of data, many object relations and very complex validation logic. Rarely you only have to do trivial checks like "is this user over 12 years old?".

P.S.: I used that example purely for demonstration purposes. Please don't cling on it.

Best Answer

Arguably, the smallest method of encapsulation is a function.

float harmonic(int n) 
{ 
    float h = 1.0; 

    for (int i = 2; i <= n; i++) { 
        h += 1.0 / i; 
    } 

    return h; 
}

This function contains both code and data. When the function completes, it returns the data that it contains.

Classes encapsulate code and data in a similar manner. The only real difference is that you can have multiple functions (called "methods" in a class) operating on the same data, and multiple instances of that data.

Consider this partial code listing of a Complex Number class, obtained from here:

public class Complex {
    private final double re;   // the real part
    private final double im;   // the imaginary part

    // create a new object with the given real and imaginary parts
    public Complex(double real, double imag) {
        re = real;
        im = imag;
    }

    // return a new Complex object whose value is (this + b)
    public Complex plus(Complex b) {
        Complex a = this;             // invoking object
        double real = a.re + b.re;
        double imag = a.im + b.im;
        return new Complex(real, imag);
    }

    // return a new Complex object whose value is (this * b)
    public Complex times(Complex b) {
        Complex a = this;
        double real = a.re * b.re - a.im * b.im;
        double imag = a.re * b.im + a.im * b.re;
        return new Complex(real, imag);
    }
}

Both of these examples of encapsulation are, shall we say, "self-contained." They don't rely on any external dependencies to function.

The problem of encapsulating code and data gets a bit more thorny when you start designing business applications. The reason this is true is because business applications concern themselves primarily with collections of entities and the relationships between those entities. While there can and are operations that can be performed atomically on individual entities, this is rare. It is more common to perform operations that affect the relationships between entities or the state or number of entities within a collection. Consequently, most of the business logic is more likely to be found in object aggregates.

To illustrate, consider an ordinary business like Amazon. There's no particular reason to pick Amazon, other than it is unremarkably similar to other businesses in many ways: it has customers, inventory, orders, invoices, payments, credits: the usual suspects.

What can you encapsulate within a Customer entity that can be atomically executed, divorced from other entities? Well, maybe you can change their last name. That's a data change in the database that can happen automatically in a repository somewhere, using an anemic data model. Perhaps you can change their password hash. That requires some logic, but it's unlikely to live in the Customer entity. It's more likely to exist in some security module.

All of the interesting business logic lives outside of the fundamental entities. Consider an Invoice, which is not an individual entity, but rather an aggregate of several entities. What can you do inside an Invoice class, divorced from the rest of the system? Well, you can change the shipping address. That's simply a change to a foreign key in the Invoice entity. You can calculate a Total (the sum of the line item quantities and costs), and finally we get to some non-trivial logic that can be encapsulated in the entity itself. Maybe the line items have a line-item total property on them, so there's a bit of logic there.

But what if you want to calculate a balance? Now you have to go somewhere else besides the Invoice to make that calculation, because the Invoice doesn't know anything about all of the other invoices (by design). That could happen in the Customer entity, but it's just as likely to occur in some Accounting module elsewhere.

And then you have linking entities, entities whose sole purpose is to provide connections between entities at the data level. There's generally no logic in those whatsoever.

So at the bottom of your data hierarchy are simple data transfer objects. When combined into aggregate objects, they become useful from a logic standpoint, and any or all of them are subject to processing by any number of software modules, treated as simply data. When you think about it, it doesn't really make much sense to bake a lot of business logic into something like a Customer object, because now you're tightly binding that object to your specific way of doing business.

Should classes encapsulate data and logic? Of course, when it is appropriate and useful to do so. The core idea in software design is suitability. There are no absolute principles; software design techniques must always be evaluated in the context of your specific system to determine if they are appropriate for your specific functional and non-functional requirements.

Related Topic