5

I often write front end apps with a generic MVC pattern. I use javascript but this questions is language independent and relates to OOP as a whole. I struggle to understand the SRP principle and I read many inconsistent content about it.

I am looking for a definitive answer by providing a written example of a problem I so commonly face. Hopefully after months of digging I can try and move forward :-).

I am trying to separate my classes as best as I can for code readability, reusability. I try so dearly to adhere to SOLID principles even when they can bar overkill my practice projects for the sake of learning. I often have a class that has an array which instantiates objects of another class, which in turn will instantiate objects of another class an so on and so on. But I always get confused as to the Single Responsibility Principle with the outermost class. How can this class remain adhering to SRP when it has a multitude of other classes inside it, all with different functionality ?

Let's look at a game example. Each part of the game is separated into separate classes. One aspect of the game means I need a Board Class. This Board class becomes for this purpose by outer most class. It simply holds an array of squares, these squares are instantiated new objects and each square can do different things, these new objects have a property in them which instantiates another object and so on or receives a class as a dependancy via Dependancy injection before instantiation. I am trying to make my classes as small as possible for code readability and to adhere to SRP however I refactor a lot when my dependancies get too large and I start to think it makes more sense to put things in the same class that often refer to each others methods more than a few times and on a regular basis.

But then the outer most class which originally housed just an array of squares which I called the Board class of and who's responsibility it is to find out what square the player landed on (based on the players rolling square count) now houses various other objects and functionality. Not directly as a property but within its single array.

Now the outer Most class ( the Board class, sorry if I am getting repetetive ) has various methods. In each method it firstly calls its original private method which finds the current square we want to target based on the players rolling square count) and then calls a polymorphic method on the targeted square. Polymorphism because each square does different things depending on what square is landed on but can all be targeted using the same method name e.g. activateLogic(). The responsibility of the Board class now becomes not only to find the square the player has landed on but also to call different polymorphic methods on the square depending on what method from the Board class is called from the controller ( which will depend on user input).

So which of these sentences is true

  1. The Board class does not violate SRP , it has an array and its job is to manage that array , even tho it calls other methods form the square the player has landed on, its responsibility can be defined as "calling polymorphic methods depending on the square landed on."

OR

  1. The Board class violates SRP as it is calling various different polymorphic methods from various different square objects meaning its doing two much. If this is the case then how would I break up this class adhering to SRP ? Im not sure it's possible as the copy of an array belongs in this class and all the polymorphic methods are contained in this class.

I feel like 1 is correct , but what does this mean for the board class if the square has 20-30 methods which need to be called from the Board class?

Many thanks

1
  • 1
    The Board class violates SRP as it is calling various different polymorphic methods if it didn't and instead it would have been "doing" what these methods do and know, then it would violate SRP
    – Laiv
    Dec 5, 2022 at 7:32

4 Answers 4

3

How can this class remain adhering to SRP when it has a multitude of other classes inside it all with different functionality ?

A class’s responsibility has nothing to do with how it does its job. It's about what its job is.

So long as that job is one coherent idea you're following SRP.

In his book Clean Architecture Uncle Bob also gave us the idea that to follow SRP the class must only be responsible to one actor. An actor being a person or department that might demand that the class change. You are not following SRP if more than one actor can demand the class change.

In no explanation of SRP has it ever been about contents. It's about responsibly. Responsible for what and responsible to who.

I see no violation here.

6
  • In this specific case the outer class has one responsibility: Play a board game. Tell the CEO of your company (single responsibility: run the company) that he is not allowed to delegate. Tell the chef that he can make steaks but no boiled eggs because that’s a second responsibility.
    – gnasher729
    Dec 5, 2022 at 8:35
  • 1
    @gnasher729 if the class is called Breakfast steak and egges are fine together. Taking a shower isn't. Unless you change the responsibility to MorningRoutine. The point is to do this intentionally and with care. Not just toss whatever into the class thoughtlessly. Dec 5, 2022 at 17:25
  • @gnasher729 thanks for a reply. your analogy confuses me. From what your saying a single Responsibility can be a CEO running a company that consists of multiple responsibilities, but a Chef can’t have more than one? Or is what your trying to say is that the Ceo doesn’t do any of the responsibilities themselves, he just directs other employees to carry out their responsibilities (and that itself is the single responsibility of the CEO) ? If that’s the case The SRP seems like it is a very broad term in how we can implement it. Thanks Dec 6, 2022 at 1:37
  • I think you have the responsibility going backwards. It’s about the class being responsible to the CEO or the chef. But never both. A class that finds itself responsible to two agents should be broken up. Dec 6, 2022 at 1:48
  • 2
    @KevinGreetham I think both of gnasher729's statements are sarcastic. The point is that it would be ridiculous to tell a CEO that they're not allowed to delegate any responsibilities, and it would also be ridiculous to tell a chef that they're only allowed to make one kind of food. Dec 12, 2022 at 13:53
1

I try so dearly to adhere to SOLID principles even when they can bar overkill my practice projects for the sake of learning.

Doing this is violating those very same principles that you're trying to adhere to. Adherence implies treating SOLID principles as a goal, which is precisely the opposite premise set out by SOLID in the first place.

Remember the real key word key word in SOLID Principles is not any of the 5 letters in the acronym but the term Principle; in other words, they're simply a starting point for a chain of reasoning - i.e. the basis for a thought process leading to a goal; to be seen as hints or guidelines to nudge you in the right direction to reach that goal.

What's really missing here, and the reason why you might be struggling with SOLID is the absence of a real goal; if you decide that SOLID is your goal then you've already violated it.

The prominent thinkers around SOLID principles (e.g. Uncle Bob) focus on a more general concept of Code Craftmanship; this is a far more useful starting point because it begins by setting out clearer objectives; most importantly it focuses the objectives on people who write and maintain code rather than the code itself.

One of the most common and easily measurable goals tends to be around coverage of automated behavioural tests. Such a goal is not only objectively measurable, but also frequently associated with software that can keep changing hands between different teams of developers over many years, while preserving most of the knowledge of how a system is supposed to work, why it exists, what its for, and allowing the inheritors of old/existing code to have confidence that they'll be able to maintain it.

So instead of caring about SOLID principles, switch your frame of mind and focus your attention on how to create automated tests for all of the functionality and behaviour in your system; where behaviours are typically anything which you (or your users/stakeholders, as well as the people who need to support those users in production) can observe in your system which relates back to functional requirements.

Or to put it another way; if you can achieve something which allows each individual behaviour in your system to be isolated and covered by automated tests, then you have an objective way to measure your code and a clear goal to work towards, where SOLID principles can play a meaningful part in figuring out how to make those tests as easy to write as possible.

4
  • Thanks for your reply. I agree completely with what your saying, but in reality when learning, I can’t just stumble across a complex project which I believe requires A solid chain of reasoning and then implement it, without previously shoe horning Solid into a simplistic app just simply to practice the fundamentals behind it. E.g I make a game that’s never going to change. I could implement a simple game with a load of squares and use if else to go through each type of square or I could use open/closed principle to make each square an object consisting of polymorphic methods. 1/2 Dec 6, 2022 at 1:54
  • Both work fine, after all it’s a simple game, but improving your knowledge of Solid on simple apps, even if it means over complicating then is an evil necessary if you want to learn how this thing works!! Dec 6, 2022 at 1:59
  • 1
    @KevinGreetham Provided the decisions/conditions inside that long if-else chain are all part of the same behaviour and requirement, then it does not violate any of the SOLID principles. Splitting fragments of behaviour out into separate classes/objects isn't SOLID, it's just over-engineering because any meaningful testing would still need the whole behaviour. This is really the point behind SOLID; it's not about the complexity or structure of your code, it's about the complexity of your requirements, which is mostly based on the people who are driving those requirements (users/stakeholders) Dec 6, 2022 at 9:21
  • My suggestion as far as getting comfortable with applying SOLID would be to focus on moving your mindset to automated tests around your code, since that's typically the starting point for most larger and more complex projects, since that's usually the motivation to start isolating code from its real dependencies (e.g. writing tests typically requires you to isolating the program's real behaviour and logic away from things that make testing hard and interfere with the tests -- UI/web frameworks, databases, network connections, file handling, etc.) Dec 6, 2022 at 9:25
1

First, SRP itself is extremely vague. It is basically a white canvas where you can project your own meaning into it. There is simply no way to tell whether anything violates or supports it, there is always good arguments to be found on both sides. It is completely up to you.

If you want to improve you design, let's go back to the original object-oriented principles. An object is an autonomous agent that solves some part of the requirements. That's it. There are two very important consequences:

  1. An object "solves" something if it completely encapsulates (hides) the details of a certain behavior. This basically means data must not escape from an object. To be overly simplistic: there can not be any getters. If there are, that means the object did not successfully abstracted / solved something. I.e. you can't be "responsible" for something you don't control.
  2. Methods of an object must be requirements-relevant. Technical methods indicate that the object is working on something that is basically irrelevant to the problem at hand. Usually indicates bad design.

So, if your cells really have 20-30 methods (which is a red flag btw), the problem probably is with the cells, not the board. Are those methods really requirements-relevant? Do they return internal data?

2
  • Hi Robert. Ok I get what you are trying to say regarding encapsulation but in reality if we want to create objects through classes and get those objects interacting with each other then surely we will need getters and other public methods (even if it’s an interface/high level method) from one class to be used in another. Either via D.I or otherwise. If we didn’t have objects methods at some level be called from other objects then surely we would have either one big god class or a bit of software that doesn’t do anything apart from share the same scripts? Dec 6, 2022 at 1:50
  • Of those two things, getters and "other" public methods, only getters are problematic. Obviously objects need to cooperate. They just should not, if at all possible, return their internal state. They can return objects that are newly created, or pass things along. Here is an article describing what objects are, and are not allowed to do. About its practicality: I don't use any getters in closed applications. It is not just practical, it is the only way I know to create maintainable software. Dec 6, 2022 at 9:17
1

One aspect of the game means I need a Board Class. This Board class becomes for this purpose by outer most class. It simply holds an array of squares, these squares are instantiated new objects and each square can do different things, these new objects have a property in them which instantiates another object and so on or receives a class as a dependancy via Dependancy injection before instantiation

So the reason you are struggling is because of the existence of this "Board" class. This class has turned into a 'god' class, that knows everything and through which you interact with everything. This can also be known as a 'system' class, where people write a OO design and put everything inside a massive 'System' class with a thousand methods that just proxy to the object that actually does the work.

This is a very familiar trap that people fall into when they first start exploring object orientated design, particularly if you come from procedural design and are used to functions as the primary way of structuring code.

In reality the Board class doesn't need to exist (at least not in this fashion) because it itself doesn't do anything. It just calls lots of other things to do things. And because of this it doesn't have a single responsibility, it appears to have the responsibility of everything because it is a way to call into, well, everything else.

Ultimately OO design is about behaviour, not data. And the Board class primarily exists to store data that it itself doesn't use. Any time you find something with data that it doesn't itself need its time to try and move that data to the object with the behaviour.

So go back and look at your design and pull out the classes that actually do things.

The Player moves. The Player thus needs to know where it is and where can it move.

So why does the Player itself not track where it is. What is the Player ignorant of its position and has to be moved by the Board. Why can't the Player simply move itself, and then notify any other objects that it interacts with that something has happened.

If you think the Player shouldn't be aware of all the rules of the checker board you can pass in an instance of a simplied Board class to the Player which it can use to figure out what is or isn't a valid move. For example

class Player
   def move_forward(board)
     current_square = (board.next_square(current_square))
   end
end

or something like that. The Board object is responsible for the rules of the board, if you say I'm at square a8 it will tell you the next square is square b1, but it is the Player that actually moves.

If you think _ok but how does anything else know the Player moved if there isn't this higher level Board keeping track of everything? Well this is the beauty of message passing. Swap from a top down this objects knows everything and tells everyone everything to a simple pub/sub system where the Player broadcasts to anything that cares that it did something.

Explaining how a pub/sub system works is a little outside the scope of this answer, but basically other objects subscribe to events from the Player and then when the Player does something (remember most of the behaviour of the game comes from the Player) they get notified of this event by the Player 'publishing' the event and can themselves can update themselves based on any rules they have.

For example

class Player
   def subscribe_to_move(some_object)
     move_subscribers.add(some_object)
   end

   def move_forward(board)
     current_square = (board.next_square(current_square))
     move_subscribers.each do |subscriber|
        subscriber.player_event('move', current_square)
     end
   end
end

This removes the need for some over all object to co-ordinate everything. The best analogy of this is traffic lights. At a traffic stop there is no overall conductor saying "This car move forward, this car stop, this car turn left". Instead the traffic lights broadcast a message (I am green) to every car that cares about the traffic lights and then each car follows a set of simple rules based on the traffic light event. No over all master intelligence telling everyone what to do.

9
  • great great insight there!! However on the simplest of terms I will at some point have to find out what the current players count is. E.g a game of monopoly, the current player may be on square 9. I have an array of squares! I want to get square 9 and it has a polymorphic method of activate(). (Each squares activate does something different). Now each square is stored in an array! This array has to be in a class( I dont want it global) and then I want to call square 9’s activate method according to the players current count!? Surely I would need a method of the board 1/2 Dec 13, 2022 at 0:35
  • 2/2 to do this. My players class is seperate. My chance class is seperate. But the board class simple holds one array which is the squares of the board and when I pass the current players count into a method of the board class then yes I am left with the exact problem you have said. But surely this array of squares needs to be in a class which manages it, how else would I store this array and call methods of the current square that I am on? Dec 13, 2022 at 0:38
  • Ok. I see. This is the observer pattern? Dec 14, 2022 at 1:18
  • So in your example ('active' square 9) ask yourself the question 'Does the square do something?'. Forget about the code for a second and just think about what it is like to play Monopoly in real life. The squares don't do anything, they simply hold data and maybe a rule about the game (eg community chest, or jail). It is the players that carry out behaviour, the player moves to a square and then carries out what ever action is possible on the square. The board/squares are just a piece of data the Player holds... Dec 14, 2022 at 12:57
  • 1
    Sure, with a game like Monoply the rules will get a bit complex. The over all point is to have the Players drive the behaviour, rather than the squares. You can certainly have a Board object with an array of squares that implement different rules depending on the square. But the Player is what should cause state to change, even if all the Player is doing is calling into a square and saying "resolve property" or "pay rent for square" or something. Again when it doubt think "how would this work in real life" and try and model that as close as possible. So think what is a real player doing Dec 16, 2022 at 16:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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