2

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?

  • A car containing a GPS is still a car. It doesn't suddenly become something else if you add a GPS to it. – Robert Harvey Dec 10 '19 at 20:18
  • These toy systems created as attempts to understand SRP have a fatal flaw: they do not have real requirements. You need to know what are the functions of the system, and how it interacts with its environment. It is paramount. However, alright, we can pretend… – Theraot Dec 10 '19 at 21:30
3

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.

  • Is it acceptable to have the redundancy of both the Database and Location storing the objects for each Location? I've run into that design conundrum several times when planning out my API, and it keeps bringing me back to the issue I posed above. It seems like a more vulnerable system since it provides two data sets that have to be kept in-sync instead of only one that has to be maintained. It also seems like it would make the two classes more tightly coupled. – MagerBlutooth Dec 10 '19 at 21:51
  • @MagerBlutooth design it to be encapsulated and thread-safe. Then it would be resolved. With that said, if you are trying to do a persistent database, instead of only a volatile (on ram) databaase... have keys. For example, you can use a primary key to identify the objects, have an index that maps primary keys to the locations, then you can cache the index or portions of it (yes, the index would be persisted too). I do not know if there would be multiple datatables, datafiles, repositories... however, keys would make sense. – Theraot Dec 10 '19 at 22:07
  • The end goal for this project would be to have a single collection of Locations utilized by multiple Users. Each User would have their own Tracker to keep track of their own Objects, as well as the ability to move Objects from one Location to another. But only one User could access the system at a time. Right now I'm just focused on getting the framework set for a single User. Thank you for your assistance. Since I started researching good code practices, it's been difficult to trust my intuition. – MagerBlutooth Dec 11 '19 at 0:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.