Should a Mover and Tracker be separate classes to maintain SRP?
https://softwareengineering.stackexchange.com/questions/402318
-
05-03-2021 - |
Pergunta
I'm doing some Java code practice to better understand the Single Responsibility Principle. Currently, I'm working with a network of data structures, where each structure implements an interface called Location.
public interface Location {
void removeObject(Object o);
void addObject(Object o);
}
Different Locations store and retrieve Objects differently, so they each need their own method to add/remove them. The goal is to be able to freely move the objects between Locations and look up where Objects currently are. I have two classes that are called to move the Objects around, one for doing the actual moving of the Objects (the Mover) and one for keeping track of where the Objects are so they can be fetched for the Mover class (the Tracker).
public class Database {
private Mover mover;
private Tracker tracker;
//Contains methods for moving specific Objects to specific Locations.
}
public class Mover {
void moveObject(Object o, Location a, Location b)
{
a.removeObject(o);
b.addObject(o);
}
}
public class Tracker {
Map<Object, Location> trackerMap;
void updateTracker(Object o, Location a, Location b)
{
trackerMap.remove(a);
trackerMap.put(b, o);
}
Object lookupObject(Object o)
{
return trackerMap.get(o);
}
}
However, I have some concerns about this design. Because the Mover and Tracker have been divided into separate classes, the data structures are essentially being stored twice. Once by the Tracker's map and once by the actual data structures the Mover is modifying. It seems error-prone to me. If the two were to get out of sync for whatever reason, the Mover could end up trying to move things that don't exist, and the Tracker could be unable to find them. I keep getting the sense that the Mover should be directly modifying the Tracker's map when it moves Objects, to guarantee they stay in sync. However, if the Mover starts modifying the Tracker, then the Tracker doesn't need to be its own class. And combining the two, as far as I understand, would violate the SRP.
How can I improve this database design while maintaining the SRP?
Solução
You need to think about how the system interacts with its environment. That is you need to think about your scope boundary API. How is the developer supposed to use this?
First of all, is the code using Database
responsible of tracking where objects are? No, right, it isn't. Thus Database
must have an API that allows to put an object in a Location
regardless of where it was.
Then Database
needs to find the object and move it as needed. For that, the Database
needs a Tracker
.
Wait, the lookupObject
API seems wrong. I do not know the Location
, I want to find it. It should take an object and give me a Location
.
Alright, What is the purpose of Tracker
? It is to know on what Location
an object is. That is not the same thing. It is navigability in the opposite direction. Tracker
would work like Map<Object, Location>
(in fact, that is what I would wrap in Tracker
).
OK, let us go back to Database
. Database
needs to find the object and move it as needed. For that, the Database
needs a Tracker
. The Tracker
tells Database
where the object is, if anywhere.
The object could be:
- Nowhere (it is a new object, we just need to call
addObject
). - In a different
Location
(we need to move it, I mean, callremoveObject
andaddObject
). - In the same Location (nothing to do).
So, the Mover
interface is wrong. Sometimes Database needs to call addObject
alone, and Mover
does not allow that.
I think, you do not need Mover
. Database
can deal with Location
directly.
And of course, Database
would have to update Tracker
.
So we have:
Location
, you can add objects, remove objects. (it possibly knows what objects it has). It is aSet
.Tracker
, it know in whatLocation
an object is. It isMap<Object, Location>
.Database
, It allows to set the location of objects. It uses aTracker
internally, however you do not need to know that to use it.
A Tracker
has a very clear responsibility. So that is good too. No doubt there.
The Location
implementations are potentially external to the system. If they implementations change, the code that uses it (Database
) do not have to.
Location
will not be a reason for Database
to change. If, for whatever reason, the implementation of Location
change, at worst you will need an adapter to the ´Location´ interace. Yet, Database
would not have changed. In fact, the Location
interface might change if Database
needs something else from it.
You need to worry about failing conditions. As designed, the Location
interface, pretends it can never fail.
Finally, the whole problem is: Database
has to do two things:
- Talk to
Location
- Talk to
Tracker
If you read above, you understand that ´Location´ is not a reason to change for Database
. Thus, this is not breaking SRP.
Oh, wait, there is a twist. How does Database
talk to Tracker
. It basically has to do the same thing it does with ´Location´. We have to mirror the operations somehow. Having a common interface would make it easier.
Let us say, we recover the idea of Mover
. Let us say that if you want to add an object, the old Location
is null. So you need to add a null check in your code.
In fact, same API could be used to remove. Which is something we did not talk about. In which case, the new Location
could be null.
Well, exactly the same API would be useful for Tracker
. In fact, please notice that your updateTracker
and moveObject
have the same signature. You can make an interface that exposes a method with that signature...
Now, Database
is talking to a list of objects that implement that interface.
Except, of course, the Tracker
is kind of special, because - for this toy system - it is the single source of truth about where objects are.
By the way, if you have an implementation of Location
that persist objects to a file, in such way that at the start of the execution the Location
already has objects. We would need to populate Tracker
accordingly. Which means that it is a concern of Location
to know what objects it has. Which would imply that Location
gives a list of objects. Which would make Location
responsible of creating objects... That throws a wrench in the design. And another one comes from multiple processes accessing file. Let us not go there because that is beyond the question. Yet, it is a great illustration of the fact that you need to think about about how the system interacts with its environment, that is you need to think about the interface boundary scope of the system.