18

A typical implementation of a DDD repository doesn't look very OO, for example a save() method:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Infrastructure part:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Such an interface expects a Product to be an anemic model, at least with getters.

On the other hand, OOP says a Product object should know how to save itself.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

The thing is, when the Product knows how to save itself, it means the infstrastructure code is not separated from domain code.

Maybe we can delegate the saving to another object:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Infrastructure part:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

What is the best approach to achieve this? Is it possible to implement an object-oriented repository?

17
  • 6
    OOP says a Product object should know how to save itself - I'm not sure that's correct really... OOP in itself doesn't really dictate that, it's more of a design/pattern problem (which is where DDD/whatever-you-use comes in)
    – jleach
    Commented Feb 9, 2019 at 10:15
  • 1
    Remember that in the context of OOP, it's talking about objects. Just objects, not data persistence. Your statement indicates that a object's state should not be managed outside itself, which I agree with. A repository is responsible for loading/saving from some persistence layer (which is outside of the realm of OOP). The class properties and methods should maintain their own integrity, yes, but this doesn't mean another object cannot be responsible for persisting the state. And, getters and setters are to ensure integrity of incoming/outgoing data of the object.
    – jleach
    Commented Feb 9, 2019 at 10:39
  • 1
    "this doesn't mean another object cannot be responsible for persisting the state." - I didn't say that. The important statement is, that an object should be active. It means the object (and no one else) can delegate this operation to another object, but not the other way around: no object should just collect information from a passive object to process its own selfish operation (as a repo would do with getters). I tried to implement this approach in snippets above.
    – ttulka
    Commented Feb 9, 2019 at 10:52
  • 1
    @jleach You're right, our undestanding of OOP is different, for me getters+setters are no OOP at all, otherwise my question had no sense. Thank you anyway! :-)
    – ttulka
    Commented Feb 9, 2019 at 11:00
  • 2
    @ttulka I just wanted to reaffirm, that there are at least a couple of us thinking the same way. "An object is a bunch of behavior", and not data. Be aware,that this is different from how most people code, even in oo languages. Following this idea to the fullest often results in radically different designs from what we are used to seeing. However, as anecdotal evidence I can bring my last 5 years, where I was "allowed" to introduce my own designs, that this thinking does indeed result in much more maintainable software. Commented Feb 9, 2019 at 19:29

6 Answers 6

8

Very good observations, I completely agree with you on them. Here is a talk of mine (correction: slides only) on exactly this subject: Object-Oriented Domain-Driven Design.

Short answer: no. There should not be an object in your application that is purely technical and has no domain-relevance. That's like implementing the logging framework in an accounting application.

Your Storage interface example is an excellent one, assuming the Storage is then considered some external framework, even if you write it.

Also, save() in an object should only be allowed if that is part of the domain (the "language"). For example I should not be required to explicitly "save" an Account after I call transfer(amount). I should rightly expect that the business function transfer() would persist my transfer.

All in all, I think the ideas of DDD are good ones. Using ubiquitous language, exercising the domain with conversation, bounded contexts, etc. The building blocks however do need a serious overhaul if to be compatible with object-orientation. See the linked deck for details.

6
  • Is your talk somewhere to watch? (I see are only slides under the link). Thanks!
    – ttulka
    Commented Feb 9, 2019 at 21:26
  • I only have a German recording of the talk, here: javadevguy.wordpress.com/2018/11/26/… Commented Feb 9, 2019 at 22:16
  • Great talk! (Fortunately I speak German). I think your whole blog is worth reading... Thank you for your work!
    – ttulka
    Commented Feb 10, 2019 at 9:47
  • Very insightful slider Robert. I found it very illustrative but I got the feeling that at the end, many of the solutions addressed to not breaking encapsulation and LoD are based on giving a lot of responsabilities to the domain object: printing, serialization, UI formatting, etc. Doesn't that increase coupling between the domain and the technical (implementation details)? For example, the AccountNumber coupled with Apache Wicket API. Or Account with whatever Json object is? Do you think it's a coupling worth having?
    – Laiv
    Commented Feb 10, 2019 at 14:58
  • @Laiv The grammar of your question suggests that there is something wrong with using technology to implement business functions? Let's put it this way: It's not the coupling between domain and technology that's the problem, it's coupling between different abstraction levels. For example AccountNumber should know that it can be represented as a TextField. If others (like a "View") would know this, that is coupling that should not exist, because that component would need to know what AccountNumber consists of, i.e. the internals. Commented Feb 10, 2019 at 16:44
7

You wrote

On the other hand, OOP says a Product object should know how to save itself

and in a comment.

... should be responsible for all the operation done with it

This is a common misunderstanding. Product is a domain object, so it should be responsible for the domain operations which involve a single product object, no less, no more - so definitely not for all operations. Usually persistence is not seen as a domain operation. Quite the opposite, in enterprise applications, it is not uncommon trying to achieve persistence ignorance in the domain model (at least to a certain degree), and keeping persistence mechanics in a separate repository class is a popular solution for this. "DDD" is a technique which aims at this kind of applications.

So what could be a sensible domain operation for a Product? This depends actually on the domain context of the application system. If the system is a small one and only supports CRUD operations exclusively, then indeed, a Product may stay quite "anemic" as in your example. For such kind of applications, it may be debatable if putting the database operations into a separate repo class, or using DDD at all, is worth the hassle.

However, as soon as your application supports real business operations, like buying or selling products, keeping them in stock and managing them, or calculating taxes for them, it is quite common you start to discover operations which can be placed sensibly in a Product class. For example, there might be an operation CalcTotalPrice(int noOfItems) which calculates the price for `n items of a certain product when taking volume discounts into account.

So in short, when you design classes, you need to think about your context, in which of Joel Spolsky's five worlds you are, and if the system contains enough domain logic so DDD will be benefitial. If the answer is yes, it is quite unlikely you end up with an anemic model just because you keep the persistence mechanics out of the domain classes.

7
  • Your point sounds very sensible to me. So, the product becomes an anemic data structure when crossing a border of a context of anemic data structures (database) and the repository is a gateway. But this still means I have to provide access to object's internal structure via getter and setters, which then become a part of its API and could be easily misused by other code, that has nothing to do with persistence. Is there a good practice how to avoid this? Thank you!
    – ttulka
    Commented Feb 9, 2019 at 12:21
  • "But this still means I have to provide access to object's internal structure via getter and setters" - unlikely. The internal state of a persistance-ignorant domain object is usually given exclusively by a set of domain-related attributes. For these attributes, getters and setters (or a constructor-initialization) must exist, otherwise no "interesting" domain operation would be possible. In several frameworks, there are also persistence features available which allow to persist private attributes by reflection, so the encapsulation is only broken for this mechanism, not for "other code".
    – Doc Brown
    Commented Feb 9, 2019 at 17:19
  • 1
    I agree that persistence is usually not part of domain operations, however it should be part of the "real" domain operations inside the object that needs it. For example Account.transfer(amount) should persist the transfer. How it does that is the object's responsibility, not some external entity's. Displaying the object on the other hand is usually a domain operation! The requirements usually describe in great detail how stuff should look. It is part of the language among project members, business or otherwise. Commented Feb 9, 2019 at 19:08
  • @RobertBräutigam: the classic Account.transfer for usually involves two account objects, and a unit of work object. The transactional persisting operation could then be part of the latter (togther with calls to related repos), so it stays out of the "transfer" method. That way, Account can stay persistence-ignorant. I am not saying this is necessarily better than your supposed solution, but yours is also just one of several possible approaches.
    – Doc Brown
    Commented Feb 10, 2019 at 14:16
  • 1
    @RobertBräutigam Pretty sure you’re thinking too much about the relation between the object and the table. Think about the object as having a state for itself, all in memory. After doing the transfers in your account objects, you’d be left with objects with new state. That’s what you’d like to persist, and fortunately the account objects provide a way to let you know about their state. That doesn’t mean their state has to be equal to the tables in the database - i.e the amount transferred could be a money object containing the raw amount and the currency. Commented Feb 14, 2019 at 19:56
7

Practice trumps theory.

Experience teaches us that Product.Save() leads to lots of problems. To get around those problems we invented the repository pattern.

Sure it breaks the OOP rule of hiding the product data. But it works well.

Its much harder to make a set of consistent rules which cover everything than it is to make some general good rules that have exceptions.

4

DDD meets OOP

It helps to keep in mind that there is not intended to be tension between these two ideas -- value objects, aggregates, repositories are an array of patterns used is what some consider to be OOP done right.

On the other hand, OOP says a Product object should know how to save itself.

Not so. Objects encapsulate their own data structures. Your in memory representation of a Product is responsible for exhibiting product behaviors (whatever they are); but the persistent storage is over there (behind the repository) and has its own work to do.

There does need to be some way to copy data between the in memory representation of the database, and its persisted memento. At the boundary, things tend to get pretty primative.

Fundamentally, write only databases aren't particularly useful, and their in memory equivalents are no more useful than the "persisted" sort. There's no point in putting information into a Product object if you are never going to take that information out. You won't necessarily use "getters" -- you aren't trying to share the product data structure, and you certainly shouldn't be sharing mutable access to the Product's internal representation.

Maybe we can delegate the saving to another object:

That certainly works -- your persistent storage effectively becomes a callback. I would probably make the interface simpler:

interface ProductStorage {
    onProduct(String name, double price);
}

There is going to be coupling between the in memory representation and the storage mechanism, because the information needs to get from here to there (and back again). Changing the information to be shared is going to impact both ends of the conversation. So we might as well make that explicit where we can.

This approach -- passing data via callbacks, played an important role in the development of mocks in TDD.

Note that passing the information to the call back has all of the same restrictions as returning the information from a query -- you shouldn't be passing around mutable copies of your data structures.

This approach is a little bit contrary to what Evans described in the Blue Book, where returning data via a query was the normal way to go about things, and domain objects were specifically designed to avoid mixing in "persistence concerns".

I do understand DDD as an OOP technique and so I want to fully understand that seeming contradiction.

One thing to keep in mind -- The Blue Book was written fifteen years ago, when Java 1.4 roamed the earth. In particular, the book predates Java generics -- we have a lot more techniques available to us now then when Evans was developing his ideas.

5
  • 2
    Also worth to mention: "save itself" would always require interaction with other objects (either a file system object, or a database, or a remote web service, some of these might in addition require a session to be established for access control). So such an object would not be self-standing and independent. OOP can therefore not require this, since its intent is to encapsulate object and reduce coupling.
    – Christophe
    Commented Feb 9, 2019 at 14:06
  • Thank you for a great answer. First, I designed the Storage interface in the same way you did, then I considered high coupling and changed it. But you're right, there is an unavoidable coupling anyway, so why not to make it more explicit.
    – ttulka
    Commented Feb 9, 2019 at 14:31
  • 1
    "This approach is a little bit contrary to what Evans described in the Blue Book" - so there is some tension after all :-) That was actually the point of my question, I do understand DDD as an OOP technique and so I want to fully understand that seeming contradiction.
    – ttulka
    Commented Feb 9, 2019 at 14:39
  • 1
    In my experience, each of these things (OOP in general, DDD, TDD, pick-your-acronym) all sound nice and fine in and of themselves, but whenever it comes to "real-world" implementation, there's always some tradeoff or less-than-idealism that must be for it to work.
    – jleach
    Commented Feb 9, 2019 at 15:14
  • I disagree with the notion the persistence (and presentation) are somehow "special". They are not. They should be part of the modeling to the extend the requirements demand. There does not need to be an artificial (data-based) boundary inside the application, unless there are actual requirements to the contrary. Commented Feb 9, 2019 at 19:02
2

Maybe we can delegate the saving to another object

Avoid spreading knowledge of fields unnecessarily. The more things that know about an individual field the harder it becomes to add or remove a field:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Here the product has no idea if you're saving to a log file or a database or both. Here the save method has no idea if you have 4 or 40 fields. That's loosely coupled. That's a good thing.

Of course this is only one example of how you can achieve this goal. If you don't like building and parsing a string to use as your DTO you can also use a collection. LinkedHashMap is a old favorite of mine since it preserves order and it's toString() looks good in a log file.

However you do it, please don't spread knowledge of fields around. This is a form of coupling that people often ignore until it's to late. I want as few things to statically know how many fields my object has as possible. That way adding a field doesn't involve many edits in many places.

9
  • This is in fact the code I posted in my question, right? I used a Map, you propose a String or a List. But, as @VoiceOfUnreason mentioned in his answer, the coupling is still there, just not explicit. It's still unnecessary to know the product's data structure to save it both in a database or a log file, at least when read back as an object.
    – ttulka
    Commented Feb 9, 2019 at 14:46
  • I changed the save method but otherwise yes it's much the same. The difference is the coupling is no longer static allowing new fields to be added without forcing a code change to the storage system. That makes the storage system reusable on many different products. It just forces you to do things that feal a little unnatural like turning a double into a string and back into a double. But that can be worked around as well if it's really an issue. Commented Feb 9, 2019 at 14:48
  • See Josh Bloch's hetrogenous collection Commented Feb 9, 2019 at 14:54
  • But as I said, I see the coupling still there (by parsing), only as not being static (explicit) brings the disadvantage being unable to be checked by a compiler and so more error-prone. The Storage is a part of the domain (as well as the repository interface is) and makes such a persistence API. When changed, it's better to inform clients in compilation time, because they have to react anyway not to get broken in runtime.
    – ttulka
    Commented Feb 9, 2019 at 14:55
  • That's a misconception. The compiler can't check a log file or a DB. All it's checking is if one code file is consistent with another code file that also isn't guaranteed to be consistent with the log file or DB. Commented Feb 9, 2019 at 15:01
0

There is an alternative to already mentioned patterns. The Memento pattern is great for encapsulating internal state of a domain object. The memento object represents a snapshot of the domain object public state. The domain object knows how to create this public state from its internal state and vice versa. A repository then works only with the public representation of the state. With that the internal implementation is decoupled from any persistence specifics and it just has to maintain the public contract. Also your domain object has not to expose any getters that indeed would make it a bit anemic.

For more on this topic I recommend the great book: "Patterns, Principles and and Practices of Domain-Driven Design" by Scott Millett and Nick Tune

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