Help us understand the problem. What is going on with this article?

「文章のように読めるメソッドを作る」 Composed Method パターン

More than 5 years have passed since last update.

「文章のように読めるメソッドを作る」 Composed Method パターン

概要

Kent Beckによって提唱されたデザインパターン。

Composed Method パターン は、適切な命名とともにメソッドを
小さく分割することによって可読性を上げる手法です。
適切な命名によるメソッド分割= 単一責務

メソッド内の 抽象度を揃え 、低いレイヤーの処理を隠蔽することで
メソッド内を英文のように、 人間が読みやすい形 にします。
これにより、低レイヤーの処理の詳細を気にすることなく、
そのメソッドが何をしているのか読み解くことが容易になります。

メソッドの呼び出しを読むだけでコードを理解できるようになるため、
SI'erのプログラムなどでよく見かけるような、処理内容をなぞっただけの
コメント群は不要になります。

処理を適切な命名とサイズに分割することで冗長なコード・構造などの
コードの臭いを発見するきっかけとなり、さらなるリファクタリングの土台となる、
という効果もあります。

現実の障壁

過去の栄光に囚われている、古いタイプの開発者には嫌われる手法だったりします。

そういったタイプの方は、処理の詳細が一箇所にまとまっていないと不安に駆られるようで、
「お前は可読性の意味をはきちがえている!」
などと暴言を浴びせられることもあるでしょう。

そして、相手の権力が強い場合はせっかく分割したメソッドを全てインライン化される
ところまでワンセットです。

Composed Method パターン サンプル

仕様

パイプ区切りで、人々の名前と年齢を保持しているファイルの内容を
読み込んで、Personモデルのリストにして返却するメソッドを実装します。

年齢に数値以外が合った場合は、 'unknown' に変更します。
言語はRubyを利用します。

適用前サンプル

require 'pp'

Person = Struct.new(:name, :age)

class PeopleReader
  def read_people(people_file)
    people_src = people_file.read
    people_src.each_line.each_with_object([]) do |person_src, people|
      person_columns = person_src.chomp.split('|')
      age = person_columns.last =~ /^\d+$/ ? person_columns.last : 'unknown'
      people << Person.new(person_columns.first, age)
    end
  end
end

people_reader = PeopleReader.new
pp people_reader.read_people(DATA)

__END__
tanaka|25
suzuki|30
sato|125
samuragochi|hoge
  • 出力
[#<struct Person name="tanaka", age="25">,
 #<struct Person name="suzuki", age="30">,
 #<struct Person name="sato", age="125">,
 #<struct Person name="samuragochi", age="unknown">]
  • 注1: Rubyでは __END__ 以降をコメントとして扱いますが DATA 変数によってファイルとして扱うことができる
  • 注2: chomp は文字列の末尾改行を削除した文字列を返却します
  • 注3: pp は人間が見やすいフォーマットで標準出力を行うメソッドです。 Pretty Print。

適用後サンプル

require 'pp'

Person = Struct.new(:name, :age)

class PeopleReader
  SEPARATOR = '|'
  INVALID_AGE_TEXT = 'unknown'

  def read_people(people_file)
    people_src = people_file.read
    people_src.each_line.each_with_object([]) do |person_src, people|
      person_columns = read_person_attributes(person_src)
      name = read_name(person_columns)
      age = read_age(person_columns)
      people << Person.new(name, age)
    end
  end

  private

  def read_person_attributes(person_src)
    person_src.chomp.split(SEPARATOR)
  end

  def read_name(person_columns)
    person_columns.first
  end

  def read_age(person_columns)
    age = person_columns.last
    convert_age_if_invalid(age)
  end

  def convert_age_if_invalid(age)
    integer?(age) ? age : INVALID_AGE_TEXT
  end

  def integer?(age)
    age =~ /^\d+$/
  end
end

people_reader = PeopleReader.new
pp people_reader.read_people(DATA)

__END__
tanaka|25
suzuki|30
sato|125
samuragochi|hoge

※出力は適用前と同じ

まとめ

Composed Method パターン 適用前のコードは、 read_people 内の処理の抽象度がバラバラで読みにくく感じます。
People, Personといったモデルレベルの概念と、 split('|') などの処理の詳細レベルのロジックが
混在しているためです。

Composed Method パターン 適用後のコードは、全体としては長いコードになりましたが、
各処理の抽象度が揃って読みやすくなりました。
read_people メソッドは他のメソッドの呼び出しの集合になっていますが、
メソッド名を見れば処理の詳細をみなくても何をしているのか把握できます。

また、命名に気を遣い単一責務にしたことによってバグ対応や仕様変更の修正範囲が
分割したメソッド内で閉じることが多くなります。

tbpgr
Rubyを扱う人事(研修開発、エンジニア採用) Learning Designer。 tbpgr の読み方は(てぃーびー) 個人ブログでも色々と情報を垂れ流してます。 http://tbpgr.hatenablog.com/
http://tbpgr.hatenablog.com/
studist
「伝えることを、もっと簡単に」をミッションにビジュアルSOPマネジメントプラットフォームのBtoB SaaS「Teachme Biz」を開発・運営するスタートアップ
https://medium.com/studist-dev
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away