DEV Community

M Bellucci
M Bellucci

Posted on

A simple SOLID object-oriented solution example.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)