「文章のように読めるメソッドを作る」 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 メソッドは他のメソッドの呼び出しの集合になっていますが、
メソッド名を見れば処理の詳細をみなくても何をしているのか把握できます。
また、命名に気を遣い単一責務にしたことによってバグ対応や仕様変更の修正範囲が
分割したメソッド内で閉じることが多くなります。