Java Design – Class Design with Bi-Directional Relationships

designjava

This is a purely design question.

I want to port a nice educational "game" Bug Brain to Java. In this game you design neural networks which consist of three elements: neurons, nodes and synapses.
Neuron is used to perform some kind of processing on its input signals to produce the desired output; it can be connected with other neurons/nodes (connections are uni-directional and weighted). We can think about nodes can as of junctions – they can also be connected with other neurons/nodes (and these connections are also uni-directional and weighted), but the weight of we cannot change weights of input connections. Synapse is a connection between these two elements.
To sum up: neurons and nodes have both inputs and outputs (to other neurons/nodes) and they do some inputs->outputs processing.

Q: Now, how would you design this class structure?
This is a sketch I "produced" so far:
screenshot of the design

Because the operation "connect two joins (i.e. neuron/node)" has three responsibilities (add synapse, add output to join A, add input to join B) I've decided to put it as a static methid in a controller class.
Of course I could avoid such two-directional relationships between elements: I don't have to keep track of outputs, only inputs but there are some other limitations eg. there can be only 8 I/Os per join so then I'd have to iterate over all synapses every time I add a connection to see if there is still some place to add it. In this case (this game) you won't have too many connections so it would not hit performance, but I'd like to know how to deal with such situations in general.

As it's the foundation of this whole logic of this project I'd like to have it properly made. All suggestions are warmly welcome 😉

Best Answer

Bi-directional associations in a class design are possible, but they always come at a hefty price, which you should know and must be willing to pay.

Generally, I like to avoid bi-directional associations with the only exception being performance. As you have mentioned, without knowing the other directions, you may have to loop through all the other objects to find the right one (or count them in your case). This may be costly, but it can also be worked around in other ways than creating a bi-directional relationship (f.ex. association classes with caches).

As you may well get hundreds of synapses, it certainly isn't good to loop over them to be able to check if something is connectable, so I think your bi-directional association may be a valid option here. It depends on what exactly you mean with this game

won't have too many connections.

If you can afford the looping, then I suggest you do so. If you do keep the bi-directional relationship, you will have to be extremely careful to keep your model consistent. Each time you touch one of these associations, you must ensure that you atomically update the whole model to become consistent again.

It's hard enough to ensure that each and every modification is correctly mirrored for all involved objects / associations, but once you add concurrency to that it gets even more ugly, because you do not ever want another thread to see the model in an intermediate state, where just one direction has been added/removed yet.

Even if you use tools like Hibernate, which support the bi-directional relationships out of the box, you get other trade-offs. For example, if you combine Hibernate with eager fetching you may well end up reading your whole neural network from the database, whenever you just call for one join. That's all part of the price to be payed.

Also note that omitting the bi-directional relationship and adding some helper class which keeps track of the number of inputs/outputs instead is not a good idea. For one, you split up the responsibility for consistent model updates to something outside of your main model, and second, the involved logic to keep things consistent needs to be added either way. You may accept the poor looping performance, or you may reasonably improve it by some cached lookup that is easier to update and need not necessarily be complete.

As a summary: Bi-directional relationships are possible, but should only be considered, when you cannot work around their absence in a feasible way.

Related Topic