3

So for DDD folks there, Aggregate Roots are supposed to contain business logics only and exposed what is needed only.

In DDD Red Book by Vaugh Vernon, he used LevelDB and Hibernate as examples. LevelDB is key-value storage and Hibernate I think uses reflection.

However, if I don't want to use any of those, how am I going to save Aggregates?

3 of the easiest solutions I can think of are listed in the title:

  • Exposed public getters
  • Reflection
  • Inject repository to aggregate and have a method called save (Memento pattern?)

Let's imagine Payments:

  • CardPayment with cardNumber, expiration, cardHolderName
  • CashPayment with cashAmount
  • PaypalPayment with paypalAccountId

Each of those has their own unique properties but adheres to an abstract class/interface (won't go deeper for simplicity).

In my whole life, there are cases like this that can't be avoided especially when doing Repository where you really need to know what are you going to save.

Going with public getters, you might need to do an instanceOf checks in repository so you can cast and access the unique properties.

Going with reflection, it may not be a problem but feels like a hack...

Injecting repositoryObj to a save method seems to be the next best option, at least the Aggregate knows what properties to save but this violates DDD I think. It knows about persistence too much and save is not part of ubiquitous language.

I can be pragmatic and eat a cake but I want to know how it is done the pure OOP and/or DDD way.

EDIT:

Found an article from Vaughn Vernon on how to model Aggregates with Entity Framework. The article can be applied to anything else too, it's not really specific to EF. I'll just link to it to prevent longer O.P: https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/

  • Have you considered others ORMs, more jdbc-like as myBatis or have you considered implementing just jdbc templating? Or no-Sql datastores? Have you considered decoupling domain data model from persistence data model? – Laiv Feb 9 at 17:32
  • 1
    It might interest.. – Laiv Feb 9 at 17:59
  • 1
  • 1
    Use reflection. I get it. You want a contract, but adding additional infrastructure/methods to/around your entities to enable persistence only serves to obfuscate the model. We don't want to do that. Reflection is straightforward and can be done transparently. I promise you that the 3rd or 4th time you begin writing some sort of "memento" method/object for an entity you will realize that you might as well just be using reflection. Just start there. – king-side-slide Feb 11 at 16:23
  • 1
    There is no decoupled solution to this problem. The internal state, that is the state that needs persisting, is permanently (and intentionally) bound to and encapsulated by an entity. Utilizing getters means a larger public surface and a coupling to that surface by your repository (and possibly whatever else might be interested). Utilizing some sort of DTO (memento) suffers from the exact same problem with the additional annoyance of needing to write a mapping for every entity. Reflection cuts to exactly the minimum level of coupling necessary to achieve your goal without forcing you... – king-side-slide Feb 11 at 18:52
4

However, if I don't want to use any of those, how am I going to save Aggregates?

To my mind, the most important thing to recognize is that persistence is a form of messaging -- you are storing a representation of your state in the past so that it is available to you running in the future. You future in-memory representation of state may not match the past.

Your business logic isn't responsible for messaging, so either you are going to query the in memory representation to get stuff out, or you are going to pass a capability to the object and allow it to forward data to that capability.

From the point of view of the object being saved, these are fundamentally the same operation -- send a copy of my information to "somebody else", either the caller, or to some other object that the caller has recommended to me.

If you are already using queries on your objects, then there isn't any additional work to do when the data is going to be returned to the caller -- you just use the same API you were going to use all along.

class MyDomainObject {
    // Send the memento along to the caller.
    MyDomainObject.Memento memento () { ... }

    // Send the memento to the target provided.
    save(Target<MyDomainObect.Memento> target) {
        target.onMemento( this.memento() );
    }
}

If you look at these approaches from the outside, they aren't all that different

// This
domainObject.save(target);

// is equivalent to
target.onMemento(domainObject.memento());

I'd recommend reviewing Mark Seemann's essay At the Boundaries Applications Are Not Object-Oriented to get a further sense of what is going on.

  • This is the best candidate solution so far. I will mark this as the answer if there's no more replies coming soon. Your explanation is a good reference to future readers. – doesnotmatter Feb 10 at 7:19
  • The way you explained it in your analogy justifies your solution and seems like the best way to do it. It's the solution #3 in the OP btw. Leaning on #3 solution now. Also the linked question might interest you too – doesnotmatter Feb 10 at 7:24
3

Instead of updating the O.P, I will just post this as an answer so I can mark it as one.

I found the book Patterns, Principles, and Practices of Domain-Driven Design by Nick Tune, Scott Millett.

To quote from Chapter 21: Repositories:

If you are using a persistence framework that does not allow your domain model to be persistence ignorant, you need to take a different approach to the way you persist and retrieve your domain objects so they remain free of infrastructural concerns. There are a number of ways that you can achieve this, but all affect the domain model and the shape of your aggregates. This is, of course, the compromise you need to make your application work.

  • Public Getters and Setters

A simple method to enable the persistence of domain objects is to ensure that all properties have public getters and setters. The problem is that this leaves your domain objects open to being left in an invalid state because clients can bypass your state-altering methods. You can avoid this through code reviews and governance. However, exposing your properties does make it easy to persist your domain model because your repository has easy access to the domain object’s state.

  • Using the Memento Pattern

If you don’t want to expose your domain model’s properties and want them to be totally encapsulated, you can utilize the memento pattern. The Gang of Four pattern lets you restore an object to a previous state. The previous state in this instance can be stored in a database. The way in which it works is that an aggregate produces a snapshot of itself that can be persisted. The aggregate would know how to hydrate itself from the same snapshot. It’s important to understand that the snapshot is not the data model; it’s merely the state of the aggregates, which again is free from any persistence framework. The repository would still have to map the snapshot to the data model.

  • Event Streams

Another way to persist your domain model is to use an event stream. Event streams are similar to the memento pattern, but instead of taking a snapshot in time of your aggregate, your repository persists all the events that have occurred to the aggregate in the form of domain events. Listen for events from the domain, and map them to a data model. Again, you need a factory method to reconstruct and replay these events to rebuild the aggregate.

And in the end

BE PRAGMATIC

In all the strategies of domain-model persistence, it pays to be pragmatic. A pure domain model should be persistence ignorant in that it should be immune to changes required by the needs of any underlying persistence framework. Purity is good in theory, but in practice it can be difficult to achieve, and sometimes you must choose a pragmatic approach.

2

The object-oriented way is virtually any solution that doesn't involve getting data out of the object, either directly through getters, or through reflection. Object integrity, privacy, self-determination, autonomy, encapsulation, cohesion, whatever you want to call it is the highest rule of the land.

So you might think that a save() method might do it, and to be fair it does comply with the above rule. However, objects are supposed to be created out of the decomposition of the problem domain. In other words, objects are supposed to solve at least some part of the problem. The problem with the save() method, is that it is technical. To use a term from DDD, it is not part of the Ubiquitous Language.

So the actual solution is to roll the "persistence", which is a technical aspect usually, into business operations. So login() saves the audit records, register() inserts the user, etc. Persistence is a natural part of the technical implementation of domain operations.

So some say the Domain should be Persistence agnostic. I don't exactly know where this started, I've read the blue book (maybe I missed it there?), haven't yet read Vaughn Vernon's books. But anyway, this doesn't seem to make any sense. Persistence is very clearly needed inside domain operations in a software. How can you really implement a cash transfer with the persistence aspect removed? And more importantly, why would you do that?

0

how it is done the pure OOP

in 'pure OOP' the method is on the object. So

PaymentMethodAbstractClass.Save()

or DDD way

DDD doesn't really say much on the details, other than you have to save the aggregate root as one, not its children individually

and DDD way

Having ChildObject.Save(), or exposing getters presents a problem to OOP if you have to enforce the saving through Parent.Save() perhaps you could put the classes in their own assembly and use internal

This kinda breaks the Open Closed principle though. As I would be unable to make an inherited child class without editing the assembly

On "reflection" I think the only OOP + DDD way of doing it would be to have the abstract child class implement an override-able private serialise method and then inject a save function from the parent.

public class Parent
{
    private void save(string data) {...}
    public void AddChild(type,string data)
    {
        this.child = ChildFactory.New(type, this.save, data);
    }
    public void Save()
    {
        this.child.Save()
    }
}

public class Child
{
    public Child(Func<string> save, string data) {...}

    private virtual string Serialise() {..}

    public void Save() 
    {
        this.save(this.Serialise());
    }
}

Now I can inherit from Child and make a new Serialise which knows about my data but control the saving of child through Parent via the injected save function.

The annoying thing is that you have to control the creation of Child to prevent rogue save methods being added. So I still need to edit ChildFactory every time I subclass Child

0

I usually use a Service when I need interaction between an Aggregate Root and a Repository.

I instantiate the service and DI the Aggregate Root Repository in it.

Here's an example which assumes a Purchase entity:

class PurchaseService() {
  constructor(purchaseRepo, customerRepo, productRepo) {
    this.purchaseRepo = purchaseRepo
    this.customerRepo = customerRepo
    this.productRepo = productRepo
  }

  create(idCustomer, idProduct) {
    const customer = await this.customerRepo.findById(idCustomer)
    const product = await this.productRepo.findById(idProduct)
    const purchase = new Purchase(customer, product)

    await this.purchaseRepo.save(purchase)

    return purchase
  }
}

The reasoning behind this, as you already mentioned, is the fact that I want to avoid classes having any notation of persistence in them; just the domain logic.

  • Maybe because you use js and so something like looping over Object.keys might work but I think that's an equivalent to Reflection – doesnotmatter Feb 10 at 7:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.