[S]OLID - Single Responsibility Principle by example

">

[S]OLID - Single Responsibility Principle by example

">

[S]OLID - Single Responsibility Principle by example

">
<div class="post">
  <div class="row">
    <div class="col-md-12">
      <h1>[S]OLID - Single Responsibility Principle by example</h1>
    </div>
  </div>
  <div class="row info">
    <div class="col-md-12">
      <ul class="list-inline">
        <li class="list-inline-item published_at">
          <i class="fa fa-clock-o"></i> 20 May 2017
        </li>
        <li class="list-inline-item categories">
          <i class="fa fa-folder-o"></i>
              <a class="category" href="/patterns">Patterns</a>
              <a class="category" href="/ruby">Ruby</a>
              <a class="category" href="/design">Design</a>
        </li>
        <li class="list-inline-item">
          <i class="fa fa-comment-o"></i>
          <a data-disqus-identifier="/2017/05/solid-single-responsibility-principle-by-example" href="/2017/05/solid-single-responsibility-principle-by-example#disqus_thread">[S]OLID - Single Responsibility Principle by example</a>
        </li>
      </ul>
    </div>
  </div>

  <div class="row subscription hidden-xs-down">
  <div class="col-md-12">
    <p class="cta">Subscribe to receive new articles. No spam. Quality content.</p>
    <form class="new_subscriber" id="new_subscriber" action="/subscribers" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" /><input type="hidden" name="authenticity_token" value="zAaooWCyMncG6QXbp6U61ki0HMJc2MWPibb3/KffqkGoVbNgFu5epB7K/es7u98AZqncHpB7yuKanTMk9cqz3g==" />
      <div class="form-inline row">
        <div class="col-md-12">
          <input placeholder="[email protected]" required="required" autofocus="autofocus" class="form-control subscribe-email" type="email" name="subscriber[email]" id="subscriber_email" />
          <input type="submit" name="commit" value="Subscribe" class="form-control subscribe" />
        </div>
      </div>
      
</form>  </div>
</div>
<p style="margin: 10px 0 5px 0;">
<a class="twitter-follow-button" data-show-count="false" data-size="large" href="<https://twitter.com/makagon>">Follow @makagon</a>

</p>

  <p>I write a lot about patterns and Object-Oriented Design, so I couldn&#39;t miss opportunity to write about SOLID principles of Object-Oriented Design.
I&#39;m going to show by example how we can use these principles in Ruby.</p>

<p>SOLID stands for:</p>

<ul>
<li><a href="/2017/05/solid-single-responsibility-principle-by-example" target="_blank">Single Responsibility Principle</a></li>
<li><a href="/2017/05/solid-open-closed-principle-by-example" target="_blank">Open/Closed Principle</a></li>
<li><a href="/2017/06/solid-liskov-substitution-principle" target="_blank">Liskov Substitution Principle</a></li>
<li><a href="/2017/07/solid-interface-segregation-principle" target="_blank">Interface Segregation Principle</a></li>
<li><a href="/2017/07/solid-dependency-inversion-principle" target="_blank">Dependency Inversion Principle</a></li>
</ul>

<p>In this article I&#39;m going to cover first one.</p>

<h2>Single Responsibility Principle</h2>

<p>Definition from Wikipedia:</p>

<blockquote>
<p>A class should have only a single responsibility.</p>
</blockquote>

<p>Robert C. Martin describes it as:</p>

<blockquote>
<p>A class should have only one reason to change.</p>
</blockquote>

<p>Quite simple idea, right? Class should have just one responsibility and one reason to change.</p>

<p>Sometimes developers struggling with definition of &#39;responsibility&#39;. It&#39;s much easier to just add another line to method, then another one and you have huge class which is responsible for too many things.</p>

<p>It&#39;s a skill which programmers should develop: to notice the moment when class is getting too big and should be refactored into couple smaller ones.</p>

<p>I feel like the best way to show this principle in action is to go through example.
My idea is to start from code which you might have in your app and refactor that following Single Responsibility Principle.</p>

<h3>Example</h3>

<p>APIs are quite popular these days, so I&#39;m sure in more or less big project you do call third-party APIs. </p>

<p>Let&#39;s say we need to call BlogService to get list of blog posts. We have corresponding class: <code>BlogService</code> and class <code>Post</code> as a way to treat posts as an objects.</p>

<p>Without proper Object-Oriented approach code of <code>BlogService</code> could look like this:</p>

<pre><code>require &#39;net/http&#39;
require &#39;json&#39;

class BlogService
  def initialize(environment = &#39;development&#39;)
    @env = environment
  end

  def posts
    url = &#39;<https://jsonplaceholder.typicode.com/posts&#39>;
    url = &#39;<https://prod.myserver.com>&#39; if env == &#39;production&#39;

    puts &quot;[BlogService] GET #{url}&quot;
    response = Net::HTTP.get_response(URI(url))

    return [] if response.code != &#39;200&#39;

    posts = JSON.parse(response.body)
    posts.map do |params|
      Post.new(
        id: params[&#39;id&#39;],
        user_id: params[&#39;userId&#39;],
        body: params[&#39;body&#39;],
        title: params[&#39;title&#39;]
      )
    end
  end

  private

  attr_reader :env
end

class Post
  attr_reader :id, :user_id, :body, :title

  def initialize(id:, user_id:, body:, title:)
    @id = id
    @user_id = user_id
    @body = body
    @title = title
  end
end

blog_service = BlogService.new
puts blog_service.posts
</code></pre>

<p><strong>I highly encourage you to go through that code example thoroughly and try to figure out responsibilities of <code>BlogService</code> class</strong>. It will help you to improve skill of identifying code smells.</p>

<p>Ok, let&#39;s identify responsibilities of <code>BlogService#posts</code>:</p>

<h3>Configuration</h3>

<p>Presented by these lines:</p>

<pre><code>    url = &#39;<https://jsonplaceholder.typicode.com/posts&#39>;
    url = &#39;<https://prod.myserver.com>&#39; if env == &#39;production&#39;
</code></pre>

<p>Depending on environment we might change base url of API.</p>

<h3>Logging</h3>

<pre><code>puts &quot;[BlogService] GET #{url}&quot;
</code></pre>

<h3>HTTP layer</h3>

<pre><code>response = Net::HTTP.get_response(URI(url))
return [] if response.code != &#39;200&#39;
</code></pre>

<p>We should do something different from parsing response if response is not 200.</p>

<h3>Response processing</h3>

<p>We&#39;re receiving something like this:</p>

<pre style="font-size:0.725em;">
[{
  "userId": 10,
  "id": 95,
  "title": "id minus libero illum nam ad officiis",
  "body": "earum voluptatem facere..."
},
{
  "userId": 10,
  "id": 96,
  "title": "quaerat velit veniam amet cupiditate aut numquam ut sequi",
  "body": "in non odio excepturi sint eum..."
}, ...]
</pre>

<p>And we need to parse JSON into array of hashes and return array of <code>Post</code> objects:</p>

<pre><code>posts = JSON.parse(response.body)
posts.map do |params|
  Post.new(
    id: params[&#39;id&#39;],
    user_id: params[&#39;userId&#39;],
    body: params[&#39;body&#39;],
    title: params[&#39;title&#39;]
  )
end
</code></pre>

<p>At least 4 responsibilities for one class!</p>

<p>Ok, we know that each class should have one reason to change. Let&#39;s try to follow this rule and extract configuration part into separate class:</p>

<pre><code>class BlogServiceConfig
  def initialize(env:)
    @env = env
  end

  def base_url
    return &#39;<https://prod.myserver.com>&#39; if @env == &#39;production&#39;

    &#39;<https://jsonplaceholder.typicode.com>&#39;
  end
end
</code></pre>

<p>Simple class with single responsibility: return configuration for blog service. For now it&#39;s just a <code>base_url</code>, but it could be extended.</p>

<p>Now we can use this class in <code>BlogService</code>:</p>

<pre><code>class BlogService
  # ...
  def posts
    url = &quot;#{config.base_url}/posts&quot;

    puts &quot;[BlogService] GET #{url}&quot;
    response = Net::HTTP.get_response(URI(url))
    # ...
  end

  private
  # ...
  def config
    @config ||= BlogServiceConfig.new(env: @env)
  end
end
</code></pre>

<p>Let&#39;s move forward and extract logging. Probably we will need to log other requests as well, so it makes sense to extract logging functionality into module:</p>

<pre><code>module RequestLogger
  def log_request(service, url, method = &#39;GET&#39;)
    puts &quot;[#{service}] #{method} #{url}&quot;
  end
end
</code></pre>

<p>In last Rails Conf somebody said: &quot;I like to read boring code&quot;. That&#39;s just awesome idea. This code is boring but that&#39;s what we want. We want to read code and understand how it works. With this module we can change basic <code>puts</code> to <code>Rails.logger</code> and our app will work, because now we&#39;re going to use this module inside <code>BlogService</code> class:</p>

<pre><code>class BlogService
  include RequestLogger
  # ...
  def posts
    url = &quot;#{config.base_url}/posts&quot;

    log_request(&#39;BlogService&#39;, url)
    response = Net::HTTP.get_response(URI(url))
    # ...
  end
end
</code></pre>

<p>Ok, we&#39;ve extracted 2 classes so far. Now we need to figure out how to handle HTTP layer and response processing.</p>

<p>This might be tricky, but I&#39;ll try to implement it in general way with re-usable pieces:</p>

<pre><code>class RequestHandler
  ResponseError = Class.new(StandardError)

  def send_request(url, method = :get)
    response = Net::HTTP.get_response(URI(url))
    raise ResponseError if response.code != &#39;200&#39;

    JSON.parse(response.body)
  end
end
</code></pre>

<p><code>RequestHandler</code> makes http call to url and parses JSON response. It&#39;s a 2 responsibilities. But since we don&#39;t want to deal with JSON string, it makes sense to parse JSON into array or hash right in this method. Also one of the benefits of this approach that if we decide to switch from <code>Net::HTTP</code> to <code>Faraday</code> - it will be easy to do so because all HTTP-related code lives in one class.</p>

<p>Let&#39;s send API call using <code>RequestHandler</code>:</p>

<pre><code>class BlogService
  # ...
  def posts
    url = &quot;#{config.base_url}/posts&quot;

    log_request(&#39;BlogService&#39;, url)
    posts = request_handler.send_request(url)
    # ...
  end

  private
  # ...
  def request_handler
    @request_handler ||= RequestHandler.new
  end
end
</code></pre>

<p>Nice! Now we can create <code>ResponseProcessor</code> to process response.</p>

<p>I&#39;ll try to make things reusable, so <code>ResponseProcessor</code> could be implemented this way:</p>

<pre><code>class ResponseProcessor
  def process(response, entity)
    return entity.new(response) if response.is_a?(Hash)

    if response.is_a?(Array)
      response.map { |h| entity.new(h) if h.is_a?(Hash) }
    end
  end
end
</code></pre>

<p>It will work with both: Array and Hash response. If it&#39;s a hash it will instantiate one entity, if it&#39;s an array it will return an array of entities.</p>

<p>Now we can use this processor in our main class:</p>

<pre><code>class BlogService
  # ...    
  def posts
    url = &quot;#{config.base_url}/posts&quot;

    log_request(&#39;BlogService&#39;, url)
    posts = request_handler.send_request(url)
    response_processor.process(posts, Post)
  end

  private
  # ...
  def response_processor
    @response_processor ||= ResponseProcessor.new
  end
end
</code></pre>

<p>Method <code>posts</code> looks much smaller now and we can read some boring code there:</p>

<ol>
<li>Construct url</li>
<li>Log request</li>
<li>Send request</li>
<li>Process response</li>
</ol>

<p>If you&#39;re interested in one of those steps, you can go to class with simple implementation and figure out how that works.
Also all those classes are reusable. So if we need to make call to any other service - we already have a good boilerplate.</p>

<p>But we have one small problem with this implementation. That happens really often. Response we receive doesn&#39;t match fields for <code>Post</code>.
Remember, in initial implementation we had sort of mapping between fields:</p>

<pre><code>  Post.new(
    id: params[&#39;id&#39;],
    user_id: params[&#39;userId&#39;],
    body: params[&#39;body&#39;],
    title: params[&#39;title&#39;]
  )
</code></pre>

<p>That&#39;s the last part we need to implement for <code>ResponseProcessor</code>. It will make code of that class a little bit more complex, but that&#39;s how we will keep flexibility:</p>

<pre><code>class ResponseProcessor
  def process(response, entity, mapping = {})
    return entity.new(map(response, mapping)) if response.is_a?(Hash)

    if response.is_a?(Array)
      response.map { |h| entity.new(map(h, mapping)) if h.is_a?(Hash) }
    end
  end

  def map(params, mapping = {})
    return params if mapping.empty?

    params.each_with_object({}) do |(k, v), hash|
      hash[mapping[k] ? mapping[k] : k] = v
    end
  end

end
</code></pre>

<p>Ok, having that we can pass any mapping we want to have, for our example it&#39;s going to be:</p>

<pre><code>{ &#39;id&#39; =&gt; :id, &#39;userId&#39; =&gt; :user_id, &#39;body&#39; =&gt; :body, &#39;title&#39; =&gt; :title }
</code></pre>

<p>Let&#39;s check final result.</p>

<h1>Result</h1>

<pre><code>require &#39;net/http&#39;
require &#39;json&#39;

module RequestLogger
  def log_request(service, url, method = :get)
    puts &quot;[#{service}] #{method.upcase} #{url}&quot;
  end
end

class RequestHandler
  ResponseError = Class.new(StandardError)

  def send_request(url, method = :get)
    response = Net::HTTP.get_response(URI(url))
    raise ResponseError if response.code != &#39;200&#39;

    JSON.parse(response.body)
  end
end

class ResponseProcessor
  def process(response, entity, mapping = {})
    return entity.new(map(response, mapping)) if response.is_a?(Hash)

    if response.is_a?(Array)
      response.map { |h| entity.new(map(h, mapping)) if h.is_a?(Hash) }
    end
  end

  def map(params, mapping = {})
    return params if mapping.empty?

    params.each_with_object({}) do |(k, v), hash|
      hash[mapping[k] ? mapping[k] : k] = v
    end
  end
end

class BlogServiceConfig
  def initialize(env:)
    @env = env
  end

  def base_url
    return &#39;<https://prod.myserver.com>&#39; if @env == &#39;production&#39;

    &#39;<https://jsonplaceholder.typicode.com>&#39;
  end
end

class BlogService
  include RequestLogger

  def initialize(environment = &#39;development&#39;)
    @env = environment
  end

  def posts
    url = &quot;#{config.base_url}/posts&quot;

    log_request(&#39;BlogService&#39;, url)
    posts = request_handler.send_request(url)
    response_processor.process(posts, Post, mapping)
  end

  private

  attr_reader :env

  def config
    @config ||= BlogServiceConfig.new(env: @env)
  end

  def request_handler
    @request_handler ||= RequestHandler.new
  end

  def response_processor
    @response_processor ||= ResponseProcessor.new
  end

  def mapping
    { &#39;id&#39; =&gt; :id, &#39;userId&#39; =&gt; :user_id, &#39;body&#39; =&gt; :body, &#39;title&#39; =&gt; :title }
  end
end

class Post
  attr_reader :id, :user_id, :body, :title

  def initialize(id:, user_id:, body:, title:)
    @id = id
    @user_id = user_id
    @body = body
    @title = title
  end
end

blog_service = BlogService.new
puts blog_service.posts.inspect
</code></pre>

<p>All classes we&#39;ve created have more or less single responsibility. What&#39;s more important they are reusable and code is pretty simple. One tricky part we have there is mapping between fields, but that allows us to extend functionality.</p>

<p>Let&#39;s say we want to add method which would get one post by id. No problem, we will write the same boring code here:</p>

<pre><code>class BlogService
  # ...
  def post(id)
    url = &quot;#{config.base_url}/posts/#{id}&quot;

    log_request(&#39;BlogService&#39;, url)
    posts = request_handler.send_request(url)
    response_processor.process(posts, Post, mapping)
  end
  # ...
end
</code></pre>

<p>It works :)</p>

<p>We could even extract logging logic into <code>RequestHandler</code>. But for now I think we did a really good job with refactoring initial class with 4 responsibilities. Probably naming could be improved as well, but the general idea here is to have 3 reusable classes instead of 1 class with 4 responsibilities.</p>