I'm working on a project right now that has a rich domain model, which we plan to use in different environments. One group of consumers are mobile clients, the others are highly concurrent servers. We try to cater to both environments in terms of parallelization, as well as keeping the domain model free of technical details.
When I design a domain model, I tend to keep it as technology independent as possible, that's probably the reason why I'm not the biggest fan of
Annotations. You kind of lock yourself into a certain type of technology and you probably introduce dependencies on 3rd party code. Overall I think you are polluting your core classes with details that don't belong there.
One of those technical aspects I'm referring to is concurrency. You can either choose to prepare your domain model for a concurrent environment or leave it to the consumer of the core classes. Take a look at the following class as an example:
public class Foo {
private Map<Long,Bar> bars;
public Foo() {
bars = new HashMap<Long,Bar>;
}
public void addBar(Long argId, Bar argBar) {
bars.put(argId, argBar);
}
public Map<Long,Bar> getBars() {
return bars;
}
}Obviously, this class is not meant to be used in multithreaded code. There are a couple of issues, which could lead to an unpredictable state of the
Collection bar. Here's another version:
public class Foo {
private final Map<Long,Bar> bars;
public Foo() {
bars = new HashMap<Long,Bar>;
}
public void addBar(Long argId, Bar argBar) {
bars.put(argId, argBar);
}
/**
* @return immutable Map implementation
*/
public Map<Long,Bar> getBars() {
return Collections.immutableMap(bars);
}
}As you can see there are a few easy things, you can do to prepare this class for usage in a parallelized environment. In fact,
I think you should always do them, even if your code won't run in multiple threads. However, this code is not thread-safe either and you need to jump through a couple of more hoops until it is:
public class Foo {
private final Map<Long,Bar> bars;
public Foo() {
bars = new HashMap<Long,Bar>;
}
public synchronized void addBar(Long argId, Bar argBar) {
bars.put(argId, argBar);
}
/**
* @return immutable Map implementation
*/
public synchronized Map<Long,Bar> getBars() {
return Collections.unmodifiableMap(bars);
}
}I think this version gets pretty close and you could call it thread-safe. I say close, because a) I'm not
Brian Goetz and b) we don't know more about
Bar. However, we can be sure, that there shouldn't be any problems with the
Collection bars. You can add new
Bar instances and retrieve them concurrently without affecting the
Collection itself in any way.
The price we paid for this is clarity. The code gets a bit more complicated. I know there are other ways to make the
Collection bar safely concurrent modifiable, e.g. using a
ConcurrentHashMap. The point is, that if you have a complex and fairly large domain class, making it thread-safe would mean littering it with all those technical details,
mixing responsibilities and making it way more complicated to maintain.
So where is the right place to handle concurrency?While researching this topic, I found a great
discussion on stackoverflow, where
Alex Miller suggests the
following:
Start from the data. Decide which data is explicitly shared and protect it. If at all possible, encapsulate the locking with the data. Use pre-existing thread-safe concurrent collections.
Whenever possible, use immutable objects. Make attributes final, set their values in the constructors. If you need to "change" the data consider returning a new instance. Immutable objects don't need locking.
For objects that are not shared or thread-confined, do not spend time making them thread-safe.
This doesn't answer my question directly, but it suggest to handle concurrency on the level of data.
That means using the java.util.concurrent classes in your domain model and only for data which is shared between threads. So what I take away from all of this, is that, it isn't really a question of where to handle concurrency, but rather when. I think if you can minimize the technical pollution of your domain model to the data that needs it, you can contain the damage of clarity. With the
java.util.concurrent Collections you have an abstraction that hides the concurrency aspect quite nicely. I guess the combination of both is the way to go:
Only optimize data that needs it, with a good abstraction, that hides the technical details.