give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

マトリョーシカ人形のようなメソッド設計を避ける

フィヨルドブートキャンプのコードレビューでよく指摘してるシリーズです。

次のようなパンを焼くRubyプログラムがあります。
このプログラムはどういう工程を経てパンが焼かれるのか、ぱっと把握できますか?

def main
  パンを焼く(粉, 水)
end

def パンを焼く(粉, 水)
  焼く(パンを発酵させる(粉, 水))
end

def パンを発酵させる(粉, 水)
  発酵させる(パンを整形する(粉, 水))
end

def パンを整形する(粉, 水)
  整形する(パンをこねる(粉, 水))
end

def パンをこねる(粉, 水)
  こねる(粉, 水)
end

main

上のプログラムは次のように書いても同じように処理されますが、工程の全体像がつかみやすいのはどちらでしょうか?

def main
  生地 = パンをこねる(粉, 水)
  整形された生地 = パンを整形する(生地)
  発酵した生地 = パンを発酵させる(整形された生地)
  パンを焼く(発酵した生地)
end

def パンをこねる(粉, 水)
  こねる(粉, 水)
end

def パンを整形する(生地)
  整形する(生地)
end

def パンを発酵させる(整形された生地)
  発酵させる(整形された生地)
end

def パンを焼く(発酵した生地)
  焼く(発酵した生地)
end

おそらく後者の方がぱっと全体像が理解しやすいと思います。
(こねる→整形する→発酵させる→焼く、という流れがmainメソッドを見ればわかるため)

前者と後者の違いは何か?

前者の場合、メソッド呼び出しと戻り値の関係を図式化すると以下のようになります。

実行すべき順序は「こねる→整形→発酵→焼く」ですが、メソッド呼び出しの順序は「焼く→発酵→整形→こねる」のように逆順になるため、処理の流れが直感的に理解しにくくなります。

また、コードの読み手は

「焼く、の前に発酵するの?」
「発酵、の前に整形するの?」
「整形、の前にこねるの?(で、どこまで続くの?)」

と、一番最初の処理に到達するまで手前のメソッドを確認し続けなければなりません。
これはまるで一番小さな人形に到達するまで、マトリョーシカ人形の蓋を開けていくような作業です。

Image: マトリョーシカ人形 - Wikipedia

一方、後者であれば以下のようになります。

一般的に、シーケンシャル(逐次的)な処理があり、前の処理の結果を利用して次の処理に進むような場合は、このように起点となるメソッド(この場合はmainメソッド)において、

  • 処理1を呼びだしてその戻り値1を受け取る
  • 処理2に戻り値1を渡して、その戻り値2を受け取る
  • 処理3に戻り値2を渡して・・・

と呼び出した方が、起点となるメソッドを見るだけで処理の全体像が把握しやすくなります。

前者のもう一つの問題点

前者のようなメソッド設計は「パンを焼く」メソッドが再利用しづらくなっています。
なぜなら、「パンを焼く」メソッドが受け取る引数は粉と水だからです。
つまり、常に処理のスタート地点からでないと呼び出せないメソッドになっています。
(この問題は「パンを焼く」以外のメソッドについても同様)

# 一番最後の工程のために、一番最初の工程で必要な引数を渡さなければならない
def パンを焼く(粉, 水)
  焼く(パンを発酵させる(粉, 水))
end

本来であれば、パンを焼くために必要なインプット(引数)は発酵した生地だけなので、以下のように発酵した生地を引数として受け取る方が再利用性が高くなります。

def パンを焼く(発酵した生地)
  焼く(発酵した生地)
end

たとえば、以下は冷凍して保存しておいた発酵生地を解凍し、それを「パンを焼く」メソッドに渡す例です。

解凍した生地 = 解凍する(冷凍された発酵生地)
パンを焼く(解凍した生地)

これであれば、「パンを焼く」メソッドがいろんなユースケースで再利用できますね。

参考:意図的にマトリョーシカにする場合もある

この記事では「マトリョーシカ人形のようなメソッド設計を避けよう」という話を書いていますが、意図的にマトリョーシカ人形っぽい設計を採用する場合もあります。

詳しくは触れませんが、Rack(もしくはPythonのWSGI)のアーキテクチャはマトリョーシカ人形的です(タマネギによくたとえられます)。

Image: "RBS generation framework using Rack architecture"の予習記事 - READYFOR Tech Blog

デザインパターンのDecoratorパターンもマトリョーシカっぽい設計になることがあります。
参考 JavaでDecoratorパターン - Qiita

ただし、どちらもケースもインターフェースを完全に揃えているのがポイントです。
すなわち、内側の処理も外側の処理も「同じメソッド名・同じ型の引数・同じ型の戻り値」になっており、各処理を自由に差し替えられる利点を持っています。

このへんの話を詳しく説明し始めると長くなるのでここでは割愛します(気になる人は調べてみましょう!)。
とりあえず、上級テクニックではあるものの、きちんとルールを決めて使えば「マトリョーシカ人形」も有効に働くケースがある、ということだけ覚えておいてください。

おまけ:Elixirのパイプライン演算子を使うとこうなる

マトリョーシカではないRubyコードはこんなふうに書きましたが、

def main
  生地 = パンをこねる(粉, 水)
  整形された生地 = パンを整形する(生地)
  発酵した生地 = パンを発酵させる(整形された生地)
  パンを焼く(発酵した生地)
end

Elixirのパイプライン演算子を使うと、上と同じ処理がこんなふうに書けます。

def main do
  パンをこねる(粉, 水)
  |> パンを整形する
  |> パンを発酵させる
  |> パンを焼く
end

こういう処理だとElixir(というか関数型言語?)の方がよりシンプルで美しく書けますね。

【PR】フィヨルドブートキャンプでメンターやってます

僕はフィヨルドブートキャンプというプログラミングスクールでメンターをやっています。
完全未経験の人でもプログラミングを学べるので、興味がある人はぜひ覗いてみてください〜。

bootcamp.fjord.jp