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