2019年2月11日月曜日

オブジェクト指向エクササイズの陳腐化

オブジェクト指向エクササイズというのが一頃流行った。自分も Java プログラマだったころは、1,000〜2,000行ほどのプログラムに適用して試したり、同僚にも勧めたりしたことがある。

ただ今の時代、パラダイム面でオブジェクト指向一強だった昔とは違うし、自分でもフルタイムで関数型プログラミングをやるようになったりして、改めて見直してみるとオブジェクト指向エクササイズも色々と怪しいところが多い。

この記事では関数型の視点も取り入れつつ再評価してみたい。

■ 評価の観点
オブジェクト指向エクササイズは『ThoughtWorks アンソロジー』所収のエッセイで紹介されたもので、著者によればオブジェクト指向の考え方を磨くのに役立つという9つのルールから成る。

原語だと Object Calisthenics と言うが、Calisthenics とは「懸垂、腕立て伏せ、ストレッチ、ジャンピングジャックのような、健康状態や体型を維持・改善するための器具を使わない運動(Vocaburary.com)」の事を指す。カタカナでエクササイズというと練習問題のような語感もあるが、もともとは体育・スポーツ・健康管理に関連する言葉らしい。

メタファーの背景にこうした語義があることを踏まえつつ、次の4段階で9つのルールを見直してみる。
  • 有益: 練習効果だけではなく、実プロジェクトでもルールやガイドラインとして取り入れてみる価値のあるもの。野球の素振りや格闘技の技の練習など、本番で確実に再現するための反復練習にも似たもの。
  • 練習としては有効: 上述の Calithenics の語義にまさに近く、著者が意図している通りのもの。実戦でそのまま使う所作でははないが、基礎体力育成のための筋トレやストレッチのような効果が期待できるエクササイズ。
  • ほぼ無意味: あえて実施してみたところで、せいぜい、やはり意味はなかったと納得するしかなさそうなルール。時間の無駄なので、なにか他の勉強でもしていた方が吉なもの。
  • 有害: むかーしの運動部では、ウサギ跳びや練習中の水飲み禁止など、今では医学的に否定されている練習や慣行が奨励されていた。それらと同じように効果が疑わしいどころか、むしろ悪い癖がついて不健康にすらなりそうなプラクティス。
以下、それぞれ見てみる。


■ 1. Only One Level Of Indentation Per Method
〜 一つのメソッドにつきインデントは1段階までにすること 〜

これは有益で、実プロジェクトでもある程度取り入れる価値がある。

エッセイでは SRP(単一責任原則)や短いメソッド(3行のメソッド)に関連して解説されているが、実は SLAP(Single Level of Abstraction Principles)にも関係が深い。

Kent Beck が 『Smalltalk Best Practice Patterns』で紹介していたComposed Methodにも関連するが、抽象度を適切にそろえて構成したメソッドは、5行前後の1段階のコードブロックで表現できるというもの。

これはオブジェクト指向ではなくても言えることで、Scala だと特に、for comprehension のネストは、抽象レベルが混在している可能性が高いので普段から避けたい。


■ 2. Don’t Use The ELSE Keyword
〜 ELSE 句を使用しないこと 〜
これはほぼ無意味

たとえば、下の式は連続一様分布の関数を普通の数式として表したものだけど、これを自然にコーディングすると当然 ELSE 句が生じる。これを ELSE句を使わずに書いてもかえって不自然すぎというか間違ってるので練習としての意味もない。
\[y = \begin{cases} \frac{1}{b - a}, & \text{for } x \in [a, b] \\ 0, & \text{otherwise} \end{cases} \]
また、エッセイでは IF 〜 ELSE を三項演算子で置き換えているサンプルもあるが、三項演算子は式だから OKということなら Scala で IF 文を式として扱えば、ELSE 句を禁止できないことになり、そもそもの基準があやふやになる。

あと、いつものオブジェクト指向のアレで、やたらとポリモーフィズムを連呼して Strategy パターンや NullObject パターンを推奨しているが、せっかく代数的データ型とパターンマッチで書いたシンプルなコードを、ムダな間接層を伴うパターンで置き換えてもまともな知恵は何一つ得られない。やってみてがっかりしても害にはならないが時間の無駄。


■ 3. Wrap All Primitives And Strings
〜 すべてのプリミティブ型とストリング型をラップすること 〜

これは有益。エクササイズに留めず、実戦のコーディングやレビュー観点に取り入れてみる価値があると思う。

もっと言えば、昔の Java ではラップするだけで精一杯だったかもしれないが、現代の Scala プログラミングではジェネリクスや型レベルプログラミングや依存型など、のパワーを追求する道具立てが昔より豊富なので、どんどん活用すると良いと思う。


■ 4. First Class Collections
〜 ファーストクラスコレクションを使用すること 〜

これはほぼ無意味。オブジェクト指向では List は準プリミティブなコンテナオブジェクトに過ぎないのだろうけど、関数型ではあえてドメイン固有の要素から切り離した普遍的な代数的性質なのだから(内部実装はともかく)、せっかく導出した抽象構造をラップして元に戻すのは色々逆行することになり不毛。

また「0または1以上」の多重度をファーストクラスコレクションにするなら、「0 または 1」でも同じことが成り立つはずで、だとすると Option もダメってことになるが、Option[Name] を MaybeName にラップする不毛さはすぐ分かると思う。さらに関数の定義域や値域に使うのもダメとなれば、自然なパターンマッチすらもできなくなる(無理して unapply を書けなくもないが無意味)し、言語が提供する利点を無駄に捨てているだけで、練習としての効果もない。

普通に Option、List、NonEmptyList(Catsなどの)、Sized(Shapelessの) などで表現した方が端的に表現できるデータの性質を、オブジェクト指向ではどうだからといった理由だけでむやみにラップしても何も良いことがない。

カプセル化の観点で見ても、参照透過でイミュータブルなコードを書いていれば、コレクション内部の値を外から勝手に変更することはできず「隠蔽」の価値は半減するので、無駄なラッパー層のコストに釣り合うメリットはやはり無い。


■ 5. One Dot Per Line
〜 1行につきドットは1つまでにすること 〜

ドットの連鎖で生じるいくつかのオブジェクトの責務を、「仲介者」が持ってしまうことになる状況を、単一責任原則デメテルの法則に則って防ぎたい、あるいはその発想を身にけたいって主旨だが、、、ほぼ無意味

ドットの連鎖禁止でできなくなることには、以下のようなものがあるが、、、
  • List や Stream を、map して filter して reduce するような計算
  • DDD でいう Closure of Operations (例えば数値計算の演算子もメソッド呼び出しだと考えれば、a + b はOKでも a + b + c は a.plus(b).plus(c) だからアウト。三角形の面積すら計算できない。)
  • 関数や計算の合成
  • fluent interface な EDSL
「オモチャのオモチャ」で遊ぶのをやめさせるための縛りとしてドットの連鎖に着目するのは、普及期ごろの Java ではそれで良かったかもしれないが、昨今のプログラミング技法を鑑みると時代遅れだし、ましてや関数型プログラミングでは的ハズレすぎてなんの学びも得られない。

※ドットで改行しても同じことだから、「一行あたりのドット」というよりドットの連鎖の禁止として考えている。


■ 6. Don’t Abbreviate
〜 名前を省略しないこと 〜

クラス名、メソッド名、フィールド名を省略しないことには別に異存はない。というか別にあえて禁止するまでもなく普通そうなのだから、練習としては毒にも薬にもならない。ただし逆にローカル変数の命名では、一文字に省略しても可読性が落ちないようにスコープを工夫したほうが良いので、むしろ有害の可能性もある。

つまり省略により可読性が落ちるしたら、関数が大きすぎて今読んでいる行とローカル変数の宣言行が離れすぎているために型が視界の外に出てしまっているとか、あるいは変数が多すぎて紛らわしくなっているとかなので、スコープをコンパクトにすれば済む。むしろそうする方が、名前をフルスペルにする事なんかより遥かにコード改善効果がある。

そもそも本当に命名の省略が常に可読性を劣化させるなら、数学や物理の公式もフルスペルの変数や記号で書かれてるはずだろうが、そうはなってないし、それどころか全部フルスペルにしたら逆に可読性が落ちて読みにくいことくらい少し想像すればわかる。

さらに関数型だと抽象度が高いコードも増えて、具体的な名前をつけようがない f、g、h としか呼べない関数を操作することが頻繁にある。ジェネリクスの型パラメータの名前だって、フルスペルにしたら逆に読みにくい。

大昔は「1文字変数名はループの i と j のみしか許さない」みたいなルールがあったものだけど、本当はクソ長いコードでループや条件ブロックを入れ子にしてるから可読性が落ちてただけで、直す場所が違う。


■ 7. Keep All Entities Small
〜 全てのエンティティを小さくすること 〜

これは有益。プログラムの構成要素について「いくら何でもこれ以上大きくなったら異常」という上限は、「Rule of 30」あたりを目安にすれば良いだろうけど、ちょうどよい小ささの発見は対象とスキル次第になる。

ただし Scala で上手く関数型プログラミングすれば、従来の Java よりは小さくなることはほぼ確実なので、『FP in Scala』などを参考にしながら、オブジェクト指向エクササイズというより関数型エクササイズとして練習すれば良いと思う。


■ 8. No Classes With More Than Two Instance Variables
〜 一つのクラスにつきインスタンス変数は2つまでにすること 〜

実プロジェクトのルールには使えないが練習としては有効かもしれない。

このルールを適用すると全ての複合型を2分木状の型構成で表現することになるけど、3要素以上で構成するのが自然な型も普通に多い。たとえば、AWS EC2 の `describe instances` コマンドの Output を2分木で表現することを想像すると、ひどく不自然になるのはすぐ分かる。だから 2という縛りは飽くまでトレーニング用の人工的な制約でしかない。

とはいえ、スポーツなどでも実戦にはない負荷をかけたり人工的に作った状況下での練習に効果があるように、このエクササイズも一度やってみる価値はあるかもしれない。ちなみに関数型な Scala で練習するとしたら、case class 等の Product Type だけではなく、sealed trait と case object で構成した Sum Type にもこのルールを適用すると、良いモデリング練習になると思う。


■ 9. No Getters/Setters/Properties
〜 Getter/Setter/プロパティを使用しないこと 〜

ほぼ無意味。代数的データ型の Product Type に「Tell, Don't Ask」を適用するのはナンセンス。むしろ無駄な責務の肥大化を招くだけ。

例えば、エッセイに Name クラスがサンプルとして載っているが、これを文字列に書式化したり Json に変換したりする場合、メソッドを Name に持たせるのは責務の置き場所が違う。`Visitor` パターンなどを使えばできるかもしれないが、コスト以上のメリットも得られず無駄な複雑さを招くだけ。

また例えば、数値と単位をそれぞれフィールドとしてもつ体重クラスと身長クラスから BMI を求めるコードを考えると、フィールドにアクセスできない以上クラスの外では計算できないし、かといってBMIの計算は体重の責務でも身長の責務でもないし、すぐに詰んでしまう。

「Tell, Don't Ask」を適用できるある種の文脈もないことはないが(特にOOPでは)、これを一般化してプログラム全体に適用するには無理がある。ましてや関数型プログラミングの場面では、よほど暇ならばあえて実験してみてやはり意味無しという結果を得るのも一興かもしれないけど、不自然なプログラムを書く練習にしかならないから不毛。


■ 結論
関数型からみた9つのルールの再評価結果
  • 有益: 1, 3, 7 の 3個
  • 練習としては有効: 8 の 1個
  • ほぼ無意味: 2, 4, 5, 9 の 4個
  • 有害: 6 の 1個
「無意味」と「有害」で過半数。残った「有益」と「有効」も、別にオブジェクト指向に限った話ではなく、「プログラミングエクササイズ」でも「関数型エクササイズ」でも成り立つし、なんなら「ベターな手続き型プログラムを書くための練習ルール」としても採用できる普通の話だった。まあ Java とか、"Better Java" 程度の Scalaプログラミングで精一杯の感じのあれなら、やってみても良いかもしれない。

■ 補足
オブジェクト指向について
オブジェクト指向について語りだすと、真の起源はどうだったとか、いや実は分派があってとか面倒だが、ここでは wikipedia に載ってるシンプルなパラダイムの分類で考えている。

┌ 命令型┬─ 手続き型
│       └─ オブジェクト指向
└ 宣言型┬─ 関数型
         └─ 論理型
一般にオブジェクト指向と関数型を対立概念と据えて語られることが多いが、こうして見てみると、COBOL や C や Java や Smalltalk みたいな命令型の一群と、Haskell や Prolog のような宣言型の一群で、大きく別れていることになる。昔から繰り返されてきたオブジェクト指向勢による手続き型叩きが(正直、自分も心当たりあるが)、一段上の観点で見直すと、命令型の枠内での内紛でしかなかったことが分かる。

0 件のコメント:

コメントを投稿