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, call removeObject and addObject). - 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 a Set. Tracker, it know in what Location an object is. It is Map<Object, Location>. Database, It allows to set the location of objects. It uses a Tracker 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.