DEV Community

Cover image for Single Responsibility for Beginners (2/2)
Angus Bower-Brown
Angus Bower-Brown

Posted on • Edited on

Single Responsibility for Beginners (2/2)

In our last blog, we explored some beginner 'hows' of the Single Responsibility Principle (how to build classes that follow it, how to check that they are following it at any time). This blog is focused on why we follow the SRP and what it brings to the table for us as developers.

Blindly following principles of design can be helpful when we're starting out, but they stand to benefit us (or at least me!) most when we understand exactly what kind of headaches they're trying to avoid!

What's the point of SRP?

Sandi Metz said that designing classes with a Single Responsibility encourages us to "code for the future" ๐Ÿ”ฎ โœจ ๐Ÿค” We'll exploring what she might mean by that throughout this blog but, in general, classes with a single responsibility are:

  • More reusable
  • More responsive to change

That's pretty much it! That may not seem like much but, unless we can visualise every aspect of our program from very start of our process, these qualities reveal themselves to be incredibly useful over time ๐Ÿ˜

Let's show this in action with some examples from last time.

Example: Back to the Bookshop

To refresh, as a Bookshop owner, we wanted a program that could keep track of all the books on a particular shelf.

So, we needed a program that could:

  • Represent individual books as data objects
  • Represent a bookshelf in some way that can store and remove those objects

We crafted both a program that achieved those goals with separate classes:

class Book
  attr_accessor :title, :author

  def initialize(title, author)
    @title = title
    @author = author
  end
end

class Bookshelf
  attr_accessor :books

  def initialize
    @books = []
  end

  def add_book(book)
    books << book
  end

  def remove_book(book)
    books.delete(book)
  end
end

Enter fullscreen mode Exit fullscreen mode

...and a counter-example of a big Book class that handled everything ๐Ÿคนโ€โ™€๏ธ:

class Book
  attr_accessor :title, :author

  @@bookshelf = []

  def self.remove_book(book)
    @@bookshelf.delete(book)
  end

  def initialize(title, author)
    @title = title
    @author = author
    @@bookshelf << self
  end
end

Enter fullscreen mode Exit fullscreen mode

Both of these examples demonstrate the behaviour we need, but what happens when we need to add a new feature?

A New Feature: A Catalogue

As the bookshop owner, we've now decided to digitise our entire stock of books. As well as keeping track of our bookshelf, we want to represent every book in store as data in some way and have it accessible.

This new idea of a "catalogue" could be useful for a whole bunch of reasons (searching and sorting our stock, keeping track of what new books need to be ordered etc) and our program will now need to:

  • Represent individual books as data objects
  • Represent a bookshelf in some way that can store and remove those objects
  • Represent a catalogue of all books in a way that can also store and remove those objects

Both of our earlier examples take care of two of these concerns, so we'll just have to adapt them to address the third one.

Let's start by looking at the second extract- the Book class that handles everything ๐Ÿคนโ€โ™€๏ธ- and see how it accommodates this new idea.

A Class Divided

Our new Catalogue class will need to store data objects that represent books. At the moment, our Book class is responsible for generating those objects.

We'll need to make them work together!

If we look at our Book class's #initialize method though, there's a snag:

def initialize(title, author)
    @title = title
    @author = author
    @@bookshelf << self
  end
Enter fullscreen mode Exit fullscreen mode

At the moment, whenever a new book object is created, it's automatically pushed to the @@bookshop class variable.

Our Book class's responsibility of representing books as data objects is currently entangled with its responsibility of adding to the bookshelf ๐Ÿชข

This won't do. Our coming Catalogue will have every book in the store; there need be some books in our Catalogue that aren't on our @@bookshelf.

With that in mind, let's "detangle" these methods:

  def self.add_book(book)
    @@bookshelf << book
  end

  def initialize(title, author)
    @title = title
    @author = author
  end

Enter fullscreen mode Exit fullscreen mode

Alright! We can now create new book objects without having to add them to our @@bookshelf!

That being said, with our @@bookshelf and our coming "catalogue", we now have two places where we can "add" books.

It might be better to clarify exactly what data object our Book class's .add_book and .remove_book methods are acting on:

class Book
  attr_accessor :title, :author

  @@bookshelf = []

  def self.add_book_to_bookshelf(book)
    @@bookshelf << book
  end

  def self.remove_book_from_bookshelf(book)
    @@bookshelf.delete(book)
  end

  def initialize(title, author)
    @title = title
    @author = author
  end
end
Enter fullscreen mode Exit fullscreen mode

With this in place our final program might look something like this ๐Ÿซฃ:

class Book
  attr_accessor :title, :author

  @@bookshelf = []

  def self.add_book_to_bookshelf(book)
    @@bookshelf << book
  end

  def self.remove_book_from_bookshelf(book)
    @@bookshelf.delete(book)
  end

  def initialize(title, author)
    @title = title
    @author = author
  end
end

class Catalogue
  attr_reader :books

  def initialize
    @books = []
  end

  def add_book(book)
    @books << book
  end

  def remove_book(book)
    @books.delete(book)
  end
end
Enter fullscreen mode Exit fullscreen mode

Ok! That's one way of doing things!

Another Way

Let's see how we would go about achieving the same thing with our first example, where the classes had separated responsibilities:

class Book
  attr_accessor :title, :author

  def initialize(title, author)
    @title = title
    @author = author
  end
end

class Bookshelf
  attr_accessor :books

  def initialize
    @books = []
  end

  def add_book(book)
    books << book
  end

  def remove_book(book)
    books.delete(book)
  end
end

class Catalogue
  attr_reader :books

  def initialize
    @books = []
  end

  def add_book(book)
    @books << book
  end

  def remove_book(book)
    @books.delete(book)
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice about the above:

  • We haven't had to change our Book class at all for it to be adapted to our new needs
  • We actually haven't had to modify any of our code, (only add new stuff) to create our Catalogue

Programmers often call classes that follow the SRP "cohesive" or say that they have "high cohesion". What they mean by this is that classes that are focused on just one thing work better with other classes.

We didn't design our Book class with the Catalogue class in mind. However, because it's only responsible for one behaviour (representing books as data) and because that behaviour is useful in many situations, it's more than happy to do it in a new context

This partly what we mean by saying coding classes in line with the SRP is 'coding for the future'. The more "cohesive" our classes are, the easier they are to use in ways we didn't even expect!

Cohesive classes can act acting as building blocks for our new ideas, instead of obstacles!

OOD Mindset

Look again at our two finished examples and consider:

  • In which is it clearer what each part of the program does?

  • If we were to add a new feature to this program (a way of making sure each object goes to the right place, maybe a sorting method to alphabetise a collection of books), in which do you feel more sure of where you might start?

In many ways, what we're doing right now- considering how refactoring our code can help our own development process- is what's ultimately important about the SRP.

The SRP isn't essential for code to work (our counter examples were perfectly functional); we don't get any prizes for having 100% SRP purity in our code ๐Ÿ˜ข We also don't always have to follow the SRP; we shouldn't feel pressure to completely redesign our app if we have just one dodgy method with no clear home!

What it does offer us is a guiding principle to help us with our moment-to-moment decision making process as developers. It helps us write code that's readable and changeable, for ourselves and our collaborators.

Wrapping up

  • SRP makes our classes more reusable and responsive to change

  • Classes with a single responsibility are often referred to as being "cohesive" or having "high cohesion"

  • Classes with high cohesion work better with other classes

  • This makes classes that follow the SRP useful to us down the development pipeline in ways that we can't even anticipate

More OOD Posts?

Thanks for reading this little 2 parter!

By now, as beginners, we hopefully have an idea of how to enact the SRP in our programs and what doing so offers us as developers ๐ŸŽ‰

These have been really informative and fun to put together; I might explore the other SOLID principles in future posts. Let me know if that's something you would like!

Top comments (0)