Object-oriented – DDD meets OOP: How to implement an object-oriented repository

design-patternsdomain-driven-designobject-oriented

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?

Best Answer

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.

Related Topic