Domain Collections
Collection, List and Set are terms very familiar to developers but hardly used by business experts. Therefore, they should not be part of the domain (API).
Standard Collections Don’t Speak Language of the Domain
Domain (API) must speak the domain language. Listen to the business experts. They’re probably talking just about products rather than a collection or a list of products. Even when a “list” is used, its meaning differs from the List class which appears as standard in programming languages like Java (java.util.List
) or .NET (System.Collections.Generic.List
).
Consider a typical incorrect API (in Java):
interface FindProducts { List<Product> cheaperThan(Money money); }
What is the corresponding requirement?
- As a user I want to find products cheaper than X amount of money.
Well, the requirement talks about products, not a list of products. It means we have a mismatch between the domain and the code that models it. The flaw is not huge, but there are other problems connected.
Standard Collections Have Meaningless Operations
Let’s inspect the interface once again. What can a client do with it:
for (Product product : findProducts .cheaperThan(fiveDollars)) System.out.println(product); findProducts.cheaperThan(fiveDollars) .stream() .mapToDouble(Product::price) .sum();
That is okay. And this?:
findProducts.cheaperThan( fiveDollars).size(); findProducts.cheaperThan( fiveDollars).get(1).price();
Does it make sense? Yeah, it could, depends on the use-case.
What about these?:
findProducts.cheaperThan( fiveDollars).remove(1); findProducts.cheaperThan( fiveDollars).clear();
Nah, those are very likely nonsense. The point is, even when those operations make no sense in the context of the use-case, they are still offered by the use-case API and nothing prevents the client from trying them out.
Of course, the internal collections are probably not mutable and an exception will be thrown in runtime when the client does so, but this is just too late. Better would be not to provide such methods at all. It would make the client code safe right in compilation time.
Not only are many methods meaningless, but even when they make sense, for example when we do want to provide the remove operation upon products, calling the method List.remove()
will not bring the expected result - a product is maybe removed from the list, but it will still be found in the system.
Domain Collections to Rescue
We can fix this by introducing a new domain object Products:
interface FindProducts { Products cheaperThan(Money money); }
What methods are in the Products signature? Everything which makes sense in the context of the domain. For example, we can sort the products:
interface Products { Products sorted(SortBy by); }
Sure, in the end we probably have to provide a way to receive the Product entities from the collection. We have several options:
interface Products extends Iterable<Product> { }
interface Products { List<Product> asList(); }
The meaningless methods like clear()
and remove()
are still included on the standard List interface, but now the client knows he works with a list of products and not with the products themselves. The operations are called on the list and not on the original Domain Collection. It means, even when the client erases the list, the found products remain the same.
We can go a bit further and use java.util.stream.Stream<T>
which is immutable and more natural to what the result actually represents:
interface Products { Stream<Product> asStream(); }
Similarly, in reactive systems we can use reactive types:
interface Products { Flux<Product> asPublisher(); }
Better Performance
Domain Collections help us not only to improve the API, but can have several technical benefits, too.
For example, lazy loading can be applied as the data is not required until the collection “collapses”. This can save a lot of throughput in case the collection is loaded from a database or some external resource. Such optimization would be not possible with Standard Collections. Consider a usage:
findProducts .cheaperThan(fiveDollars) // not queried yet .sorted(Products.SortBy.PRICE) // not queried yet .asStream() // function “collapse” -> data queried
A sample JDBC implementation follows:
class ProductsCheaperThan implements Products { private final Money cheaperThan; private final SortBy sortBy; private final JdbcTemplate jdbcTemplate; // constructor ... @Override public Products sorted(SortBy by) { return new ProductsCheaperThan(cheaperThan, by, jdbcTemplate); } @Override public Stream<Product> asStream() { return jdbcTemplate.queryForList(String.format( "SELECT code, title, price FROM products " + "WHERE price < ? ORDER BY %s", sortBy), cheaperThan.amount()) .stream() .map(this::toProduct); } // private methods ... }
The full source code can be found on my Github.
Summary
Domain Collections encapsulate domain entities in the domain API and provide domain-meaningful operations upon them.
Using Domain Collections over Standard Collections brings several benefits:
- Model speaks domain language,
- safety of operations can be ensured via static typing,
- potential of performance optimization.
Happy collecting!