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?

Foi útil?

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, 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.

Licenciado em: CC-BY-SA com atribuição
scroll top