SOLID is a popular group of design principles and if you are a developer, for sure you had contact with it.
It is must if you want to get a nice job, but still many programmers have a problem to deal with this in 100%.
This is my explanation what is SOLID about in Ruby ecosystem.
S - SINGLE RESPONSIBILITY PRINCIPLE
A class should have one, and only one reason to change - Robert C Martin
This rule is one of the simpliest to understand. Let's say, we have a payment flow inside our application, and we want
to create a class which will help to deal with some things inside the process. Let's check out this example:
def PaymentFlow
def initialize(payment)
@payment = payment
end
def update_counters
SaleCounter.count_up(@payment.items.count)
end
def send_email
SomeMailer.send_email(payment: @payment)
end
end
Now, if we want to use methods, we can use it like this:
def some_method
flow = PaymentFlow.new(payment)
flow.update_counters
flow.send_email
end
In this example you can obviously see, that the class is responsible here for more than one thing. It should not be like that.
Single Responsibility rule says, that the class should be responsible only for one thing. In our case above, we should create
two seperated classes, and use them seperately.
def UpdateCountersService
def initialize(payment)
@payment = payment
end
def call
SaleCounter.count_up(@payment.items.count)
end
end
def SendSuccessEmailService
def initialize(payment)
@payment = payment
end
def call
SomeMailer.send_email(payment: @payment)
end
end
and our calls should look like that:
def some_method
UpdateCountersService.new(payment).call
SendSuccessEmailService.new(payment).call
end
What advatage this rule gives to us?
- decreases the coupling inside our code - if something changes - it is easier to control it - we need to change only one thing instead of looking inside many connected methods and trying to resolve, what happened here
O - OPEN / CLOSED PRINCIPLE
In the contrast - it was one of the hardest rules to understand for me and it took quite some time for me to get it 100%.
Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.
Let's say, we have some class like below:
class SendSuccessEmailNotificationService
def initialize(payment)
@payment = payment
end
def call
# some logic for send success email
end
end
As you can see this class will send a success email to the payer.
What if we want to send another email depending on the payment type? The easiest way would be:
class SendSuccessEmailNotificationService
def initialize(payment)
@payment = payment
end
def call
if @payment.type == "vip"
# some logic for send success email to the vips
else
# some logic for send success email to the other people
end
end
end
But this is not the good pattern. As you can see we are changing completely the logic of this class and now we need
to take care of two different paths here. The correct way with using Open/Closed Principle here would be:
class SendSuccessEmailNotificationService
MAILERS = {
"vip" => SendVipSuccessEmailService,
"normal" => SendSuccessEmailService
}.freeze
def initialize(payment)
@payment = payment
end
def call
MAILERS[@payment.type].new(payment: @payment).call
end
end
As you can see, we did not modify the logic of our class - we just have extended its functionality.
L - LISKOV SUBSTITUTION PRINCIPLE
If S is a subtype of T, then objects of type T may be replaced with objects of type S - Barbara Liskov
This is quite straight-forward. Let's say we have class like this:
class ClientInvoices
def initialize(user)
@client = client
end
def invoices
@client.invoices
end
end
Now we want to create some subclass named VipInvoices which will interhit from ClientInvoices and will return some documents too:
class VipInvoices < ClientInvoices
def invoices
invoices = super
invoices.pluck(:name)
end
end
The example above will break the rule of Liskov Substitution Principle, because it will return different type of the data.
To keep invoices method corresponding to LSP rule - three things should match:
- name of the methods should be the same
- arguments passed to those methods should be consistent
- the data, method will return should have the same type
The really nice example, how we can use this principle in practice is the code below:
class File
def regenerate
raise "NotImplemented"
end
end
class Picture < File
def regenerate
# some code
end
end
class Audio < File
end
This code will return raise "Not Implemented" error to us if we call it. It is an easy way to keep the code clean and simple.
I - INTERFACE SEGREGATION PRINCIPLE
Clients should not be forced to depend upon interfaces that they don't use. - Robert C. Martin
This principle is strictly connected with static languages and there is no concept of interfaces in dynamic
languages like Ruby.
But we can somehow move it into Ruby reality. In simple words we can say, that this principle is all about
dividing big class into smaller classes and big method into smaller methods.
D - DEPENDENCY INVERSION PRINCIPLE
A high-level module should not depend on a low-level module; both should depend on abstraction.
The last rule od SOLID is about dependentions. Let's look at this example:
class UploadImageService
def initialize(image)
@image = image
end
def call
if @image.url.end_with?(".jpg")
FirstUploader.new(@image).call
else
SecondUploader.new(@image).call
end
end
end
As you can see, we have 2 uploader classes and depending on the extension of our file, we use different class to upload a file.
This is a violation of Dependency Inversion Principle. The simple resolution of our example is just passing a class name into our name.
class UploadImageService
def initialize(image, uploader_class)
@image = image
@uploader_class = uploader_class
end
def call
uploader_class.new(@image).call
end
end
my_image = Image.new(params)
UploadImageService.new(my_image, FirstUploader).call
The situation, you can see above we are used to call a Dependency Injection.
In the end, i need to quote a really nice SOLID rules summary, wrote by Paweł Dąbrowski on his blog - longliveruby.com.
He took all the rule names and wrote one important sentence about each of them to make them easy to understand:
- Single responsibility principle - one class is responsible for only one thing
- Open/closed principle - class should be open for extension and closed for modification
- Liskov substitution principle - it should be possible to replace the parent class with its child class, and it would work the same
- Interface segregation principle - it’s better to have multiple simple methods instead of one bigger
- Dependency inversion principle - let the person who calls method define the dependency by passing them as arguments
The source of this really nice summary is here.