M Bellucci
Posted on January 20, 2020
First of all, I write this because I want to improve my knowledge of SOLID principles, I don't completely understand them so I encourage you to make questions or have an opinion.
Let's start solving a problem.
Requirement
A text editor allows the user to select a layout for the document.
The default layout is one column, but he can choose two-columns.
Analysis
Under certain conditions, the format needs to be different.
This implies that the format is implemented in many different ways.
Design
How do we model this with objects?
We could create a Format
class with #one-column
and #two-columns
methods.
If we do that, the Page
object would need to have two versions.
class TwoColsPage
def render
...
@text = Format.one-column(@text)
...
end
end
class SinglePage
def render
...
@text = Format.two-columns(@text)
...
end
end
The main problem with this is that once you create a single page document you cannot change it to a two-column document.
Also if we need another format we're forced to create a new Page object for that format.
Another option is to rely on polymorphism(multiple classes implementing the same method)
- OneColumnFormat#format
- TwoColumnFormat#format
From the consumer perspective:
class Page
def render
...
@text = FormatterFactory.create(with: :two_columns).format(@text)
...
end
end
class FormatterFactory
# Returns objects that implement the implicit Formatter interface
# That is, objects that respond_to :format method
def self.create(with:)
case with
when :one_column
OneColumnFormatter.new
when :two_column
TwoColumnFormatter.new
end
end
end
Open-Close
It is open to support new formats by adding new formatters without modifying code.
Single Responsibility
It does respect SRP, every object has a single responsibility.
Page --> returns the resulting text after by applying a set of transformations
FormatterFactory --> creates the requested formatters
OneColumnFormatter --> applies the format strategy for one-column text
TwoColumnFormatter --> applies the format strategy for two-column text
Interface Segregation
ISP would mean that the consumer object doesn't get useless methods. The Page (consumer) receives only one method format
which he uses.
Liskov Substitution
In this case LSP means that objects returned by FactoryFormatter all met the same interface.
If we call FactoryFormatter.create(:unknown_format)
it returns nil
.
does nil
meet the formatter interface?
does nil
respond to format
?
NO!
how can we fix that?
class FormatterFactory
def self.create(with:)
case with
when :one_column
OneColumnFormatter.new
when :two_column
TwoColumnFormatter.new
else
NullFormatter.new
end
end
class NullFormatter
def format(text)
text
end
end
end
Why doing that?
If we return nil for some cases, then the consumer would need to ask for the answer.
formatter = FormatterFactory.create(with: format)
if formatter.nil?
# we're not able to format the text
Logger.error("Formatter failed creating formatter with format #{format}")
else
@text = formatter.format(@text)
end
Let's suppose that the execution returns a NullFormatter.
formatter = FormatterFactory.create(with: format)
@text = formatter.format(@text) # returns the same text it was before
In that case instead of handling a failure or getting a No method :format for nil class
The failure is avoided and the user gets the text with no format.
Dependency Inversion
DIP Depends on abstractions, not concretions. Well, I think that Page depends on objects that implement #format
so I think that "something that implements format
" is the abstraction.
Something that confuses me is that this principle is also called Dependency Injection, in this scenario we could adapt the code to inject the formatter to the page.
Page.new(format: FormattersFactory.create(:two-columns))
Doing that Page
wouldn't depend anymore on FormattersFactory
, but we are moving the dependency to the consumer of Page
(a previous step in the call stack).
I suppose that this is trying to organize the code in a way were all the creations are at the top level in the call-stack.
why doing that?
If you reached this point, thanks for reading hope your comments!
Posted on January 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.