継承の使いどころ

業務のほうでやっていたプログラミングが久しぶりに設計を4回もやる苦戦となった。

苦戦というよりは模索したというほうが近いが、「作るより変更するほうが難しい」の一例となった。

作業としては、出力機能にバリエーションが増えることだった。

入力データは変わらないのだが、出力データに関してはその違いが様々な面で出てくる(文字エンコーディングの違いなのだが、JIS XかUnicodeかという違いでもある)。 そのため入力処理あるいはデータアッセンブルの段階で処理してもいいし、出力時に処理してもいいし…という状態だった。

既に600行を越えているプログラムであるため、あまり変更すると変更量が増えてしんどいしバグにもなりかねない。 かといって単純に分岐してしまうとメンテナンスが困難になり、リファクタリングしておくほうがいい。

大部分は同じだが、変数で済むほどには同じじゃない、という加減が問題だった。

データ用クラスを作る

最初に考えたのがこれだった。

現状、データ自体は複数のインスタンス変数に分かれたHashで保持している。

これをクラスとして独立させ、これに出力機能を与える、というものだった。

この際、データをストアするメソッド(現状ではHash#[]->Array#push)にデータの加工を含めるという方法を考えた。

だが、これは現在とは異なるフローである。現状では入力処理はデータのアッセンブルと出力データのための加工を並行して行っている。 これを実現するためにはこの点も変更しなければならず、一見スマートに見える方法だったが、これは呼び出し側から見ればカプセル化によってスマートに見えるだけで、実際はあまりキレイな実装にはならなそうであった。

一見ゴリ押しに見える現在の設計だが、再考してみると案外合理的なのである。 大幅な変更がリファクタリングとして有効に働くかは結構疑問であった。

また、データオブジェクトだけではなくフロー制御で使われるデータもあり、 データオブジェクトとフロークラスでいくつものインスタンス変数を共有する必要があった。 これはあまり美しくない。

変更点が大きくバグを生じる可能性が高かったため、この方法は断念した。

異なる部分をメソッド分けする

「避けたい」と思いつつもどれくらい複雑になるか考えてみた。

これまでひとつのメソッドに組み込まれていた一部をパーツとして分離し、メソッドとして切り出す。 もちろん、これは悪いことではない。

そして、異なる処理を行うメソッドを追加する。 ここまではまだいい。

だが、そのどちらのメソッドを起動するかということについては従来のメソッド上で条件分岐しなくてはいけない。

別に今後増えることも減ることもないのならばそれでもいい。 だが、今回追加されたJIS機能は「将来的に削除する予定の」機能である。 実際仕様書にも「容易にバイパスできるように」と書いてある。

フラグだけで条件分岐を回避するというだけのコードは明らかに汚くなってしまう。

違いをモジュールで吸収する

「違いが生じる部分をMix-inすることにして、Mix-inするモジュールの選択によって挙動を変えよう」というアイディアが生じた。

これは、出力するたびに入力をやり直すことになるが、フィルタとして機能するようなプログラムではないのであまり問題はない。 入力処理もそこまで極端に重いわけでもない。 入力データのSanity checkや分類にも両者に違いがあるため、実行時は分離したほうが良い考えに思えたのだ。

もちろん、両方生成する場合は非効率的だが、その程度は受容できるだろう。

だが、このアイディアはすぐ適切でないと分かった。 出力系統によって一部だけ変更/追加したい、ということがあるのだが、共通処理から分岐するたびにメソッドの有無をチェックしてあるなら呼び出す…ということになってしまう。 これは明らかにsuperしたい状況であるにも関わらず、だ。

だが、Mix-inされたモジュールのインスタンスメソッドよりクラスのインスタンスメソッドのほうが優先度が高いため、これはうまくいかない。 比較的新しい機能である(といってもRuby 2.0だけれど)Module#prependしても良いのだけれど、これはちょっと違うように思われる。 そもそも正しい継承関係と逆で、設計が歪なのだ。

継承する

「出力の固有特性を持っているほうがサブクラス」であることが自然だと思うなら、サブクラスにすれば良いじゃない!というわけで継承することにした。

従来のクラス名を維持すれば互換性を保つこともできるが、従来型(Unicode)と新機能(JIS)は並列の存在であるため、名称をABCABC::DEFのような関係にはし難い。 そこで、ベースクラスはベースクラスで名称をつけ、UnicodeクラスとJISクラスを用意することにした。

この構造は、共通部分も多いがそれぞれ部分的に違い、また全面的にオーバーライドできるものではないため、 共通処理をベースクラスに記述し、サブクラスからsuperを呼んだり、メソッド中で部分的に異なる点はサブクラスで実装されているメソッドで異なる処理をする。

リファクタリングするならば差異のある部分のメソッド構造を変更し、容易にオーバーライドできるようにする、また処理の一部はコール時のブロックにする、といったことが考えられる。

共通部分があって一部違う挙動を示すものがあるとき、継承を使う、ということ自体はごく当たり前のことだ。

しかし、「従来一本道だった処理で複数の異なる挙動を持つプログラムにする」というときに新たに「共通のスーパークラスを作って」継承を利用する、という発想はなかなか出てこなかった。

しかし結果的には、これぞ継承の使い方という見本のようなものになったな、と思う。