技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

関数型との融合の先に、デザパタと設計が根本的に変わるべきかも

ふと、最近自分のやってる設計をクラス図に落とせるかと言う事を考えて思った事をつらつら。

関数型と融合すること

関数型の概念として以下のようなものがある。

  • 関数(Javaでメソッドと思っていいかもしれない)を引数や返値に指定できる(関数が第一級オブジェクトである)

これは何を意味するかというと、内部で手続き的に変化していく状態(変数と言っていいかもしれない)を、変化していく引数と返値(引数を部分的に適用した関数かもしれない)に置き換えて、関数スタック構造を作っていくという概念に至る。
だから関数型言語では多くの場合で、変数を嫌うし、void 型みたいなものの存在を否定する。

これだけ聞くと慣れない人にはそれで何が作れるんだ?みたいに思うかもしれない。
というか私も始めそう思った。

やってみなければ分からないのだけど、やってみると意外な発見がある。
何より、「状態を排除する」というのが、どれだけ大事かわかる。

状態を排除するとは?

オブジェクト指向で、大抵バグの原因になるのはどういう状況だろう?

  • オブジェクトが想定した動きをしなかったとき

となるのだが、その時中で何が起きているか?と深堀りしよう。

  • オブジェクト自体の状態(クラス変数、依存するクラスの状態等)が変わっていて、呼び出し元の意図した状況での動作をしていない。

という事ではないだろうか?
もちろんオブジェクト指向の設計として、これはオブジェクトの状態設計が不十分だし、もう少し深堀すれば、その処理は本来そのクラスにあるべきではないとか、色々反論できる。

だが、ここで関数型と融合していたらどうだろう?
関数型の思考はともかく状態を排除する方向で進む。

すると何が起きえるのか?
状態(変数等の値)と処理(メソッド)の分離が進む

突き進むと、クラスが大きく二つに分離されていく。

  • データと、データを操作するメソッドが一つになったオブジェクト
  • クラス変数を持たず、前述のデータオブジェクトを引数に計算、応答する関数

(後者をあえて関数と書いたのは、Java 的に言うなら static なメソッドだけで完結すると言う事を言いたかったからだ(当たり前だが、意味ある単位でクラス分けは必要)。)

すると、データとその管理に付随するオブジェクトは、中身がすっからかん(処理が無いとは言わない、IOとかやるべきことはある)になり、処理関数は引数と応答というシンプルな構造になっていく。
必然的にテスタビリティが上がり、単純ゆえにバグを抑えられる。

私は、ソフトウェア設計には 二つの方法があるという結論に達した。 一つは、欠陥がないことが明らかなほど単純にする方法である。 もう一つは、明らかな欠陥がないほど複雑にする方法である。 C.A.R.Hoare

KISS 原則に従うのなら、ステートマシン図とかいろいろな手法で状態管理をさせるより、単純化できるこの様式が正義となる。

因みに、さらに関数原理主義に近づく場合、データクラスも setter は持たす、「新しい状態のクローンクラスを応答する」みたいにするのが正しい。
(もっともそこまでやると面倒なだけなので、必要が無い限りやるべきではない:YAGNI 原則参照)

抽象化アプローチの衝突

オブジェクト指向も、関数型指向も、目指すところが何かというと、抽象化である。
物理的なマシンスペック向上に伴い表現能力が拡大した今のハードウェアで、いつまでも小さな単位で物事を考えていては、完全に追いつかない。

オブジェクト指向は、データと関連する手続きを一つに梱包(Simula言語)し、名前を与えることでトップダウン的にそれを行おうとし、
関数型は数学的に、数学の数式に名前を与えるが如く、小さな関数に名前を与え、関数の組み合わせに名前を与えるというボトムアップ戦略をとった。

Scala 等の関数とオブジェクトのパラダイムがぶつかったのを皮切りに、Java も 8 で関数的アプローチを拾い始めた。
ここでトップダウンボトムアップが衝突を開始したわけだ。

既存のデザインパターンは、オブジェクト指向に沿って開発を行い、可用性を求めた時良く表れる構造を一般化、名前を付けたものである。

だが、これは現状ではオブジェクト指向が前提となっている。
そこに、「メソッドを生成して返す」とか、「メソッドを引数に処理を行う」といった関数的な状況を想定していない。

ファイルのI/O を例に考えてみる。

例えば try-release を行うために、Java は AutoClosable インターフェースを用意し、try 文の言語仕様を拡張した。多分中身はローンパターン。デザパタの一種だ。これは、try - finally までの間で動的に書き込む内容を生成→書き込みを行い、任意(っても finally)で処理を終了する仕組みだ。
では関数ならどうするか?と考えると、In に対して Out ありきで考えるので、「In 書き出すデータすべて Out ファイル」みたいな関数がありそれを使うだけ。状態を持ちまわらないのが前提なので、書き込む内容は事前にすべて準備し、その1関数内だけでOpen/Close も完結してるべきと考える。
(ここではあくまでそれぞれの文化的に善悪を言っているだけなので、現実的にメモリ消費量とかそうした話は考えない。後者は割と原理主義なので私もまずしない)

だがここで考えてみる。
リソース状態の管理としてオブジェクト指向にのっとり、オブジェクト化するのは、オブジェクト指向的に正しいが、try-finally 間だけというのはどういうことか?
もっと自由に動的に書き込む内容を変動させなければ、1スコープごときで閉じるリソース管理に何の意味があるのだろう?
リソースリークを避けるという点で、try-finally で制御しているのだが、そんなスポット的に使用するだけなら、関数型のやり方でもいいのではないだろうか?

上記は非常に極端な思想の違い部分だが、状態排除の為にこうした事を行っていくと、オブジェクト指向部分で求められるものと、関数型部分で求められる事の質が、根本的に変わるのではないだろうかと考えてる。つまり

  • オブジェクト指向
    1. ロジックの切り替え等、設計の動的拡張性の確保(ファクトリとか)
    2. データとその操作処理の梱包
  • 関数型指向:ロジック実装(個々の問題処理実装)

みたいな使い分けになっていかないだろうか…。

ではなぜこれでデザパタや既存設計手法の一部崩壊を?

こうなってくると、デザパタの適用箇所は実業務では少なくなり、必然的に新しいパターンが必要になる。
UML も然りで、関数型入り混じったパラダイムには表現力が足りない気がする(Java なら Function 型みたいな Generic 型で無理やり頑張ってる感があるが、図の表記としては冗長で、もう少しシンプルでないと利用者が敬遠する可能性もある)。

新しい表現方法を考えてみるのも一つの手かもしれない。

(尚、Excel方眼紙に手続きをダラダラ書くような設計は、手続き型以外では害悪でしかないと断じるので、もはやその発展を考えるべきではない)