Template method pattern in real case
Tue 11 Jun 2019

Have you ever heard Template Method pattern? We thought we know it but we might be wrong. I just found a secret, a meaning of this pattern. Template method supported by inheritance. So what is template method? 

We wrote code to solve thing perfectly. The trouble is that we need to vary the middle of code (method or code block in general). Sometimes we need to to this, sometimes we need to do that at middle and even adding new code in future. so why don't we separate this function to many smaller functions? The problem is that. No matters, we can do that but we need to have a main function to gather those functions and involving them continuously. However. For example.

def main_function
    func1
    func2
    func3
end

Sometimes we want to involve func4 instead of func2. How? Furthermore, we would want to extend this function in future by making some changes at func2 (middle) position. Something worse we can do at that time:

  • modifying main_function (1)
  • adding if statements at position of func2. (2)
  • adding yield (callback) at position of func2. (3)

(1) this is worst case we need to avoid. Modification means taking risk.

(2) this would make this function gross, not clean.

(3) this is hard to manage code. Because we might make duplicated code or even more messy. Callback code block is like the parameter. Thus, we have to provide this block every we call the function. In other hand, we might do same things at many places where we involve main_function - give the same block. So that we have template method.

Let's firstly have a look on this code.

module HackerNewsServices
  ATag = Struct.new(:title, :href)

  class Crawler < Base
    PATH = 'table.itemlist tr.athing td.title a.storylink'.freeze
    META = {
      image: 'meta[property="og:image"]',
      excerpt: 'meta[property="og:description"]'
    }.freeze

    def latest_news(opts = {})
      collect_a_tags(limit: opts[:limit]).each_with_object([]) do |a, res|
        data = get(a.href)
        res << HackerNew.new(title: a.title, image: image(data), excerpt: excerpt(data))
      end
    end

    private

    def image(data)
      data.at(META[:image]).try(:[], 'content')
    end

    def excerpt(data)
      data.at(META[:excerpt]).try(:[], 'content')
    end

    def collect_a_tags(opts = {})
      data = get.css(PATH)
      limit = opts[:limit] || data.size
      data.take(limit).collect do |a|
        ATag.new(a.text, a['href'])
      end
    end

    def get(href = nil)
      Nokogiri::HTML(open(href || url))
    end

    def get_src_link_preview(html)
      data = Nokogiri::HTML(html)
      return nil if data.css('img').empty?
      data.css('img')[0]['src']
    end

    def get_external_content(data)
      img = data.at('meta[property="og:image"]')
      desc = data.at('meta[property="og:description"]')
    end
  end
end

We can see that there are a ton of code (private methods). By the way, we think about leaving a room for future. The code need to be easy for extending, DRY and readable. If we want to make changes, we make only smallest changes to meet requirements. This is code after refactor.

module HackerNewsServices::Crawlers
  class NewsList < Base

    def data(opts = {})
      atags = collect_tags(limit: opts[:limit]).collect do |tag|
        create_element tag
      end
      block_given? ? yield(atags) : atags
    end

    private

    def create_element(tag)
      raise NotImplementedError
    end

    def collect_tags(opts = {})
      raise NotImplementedError
    end

    def get_html(href = nil)
      Nokogiri::HTML(open(href || url))
    end
  end
end

Source: https://github.com/nctruong/news-reader/blob/master/app/services/hacker_news_services/crawlers/news_list.rb

These are template methods:

  • create_element
  • collect_tags

We are planing to do things. We put these methods into a context and allowing to implement these ones later - template, by using inheritance we have to implement these methods. If not, it would raise Exception.

Using abstract class to define template methods (abstract). Using subclasses for implementations.