オブジェクト指向の呪いと、その避け方

このテーマで書く前に、まず、最初に自分に多少の偏りがあることを認めておかなくてはなりません。

階層化されたツリー構造(GUI/リレーショナルな参照構造)に埋め込まれる状態はコード品質を悪化させるので、できるだけ出現するべきではない。 ただし、状態は確実に存在する。だからこそ慎重に扱うべきだ、という派閥です

アンチパターン: 特に理由もないクラスメソッドへの所属

何かのバリデータを実装したいとします。

その関数がどこに所属するかについて、よく見るこれらの実装は全部アンチパターンといっていいと思います

export class Validator {
  static validate() {...}
}

export class Validator {
  validate() {...}
}

export default ({
  validate: () => ...
})

正解

export function validate() {
  ...
}

状態は割れ窓です。最終的な出力に関与される余地は、できるだけ減らしたほうがいいです。(詳しくは後述します)

また、名前空間がほしいだけのクラスも不要です。そもそもvalidateにもっと厳密な名前をつけるか、import 時にエイリアスを付ける、といった解決策はいくらでもあります。参照スコープが限定されているモジュールシステムの中ではあまり厳密ではありません。

new でヒープに積むから効率がどうこう、実際あまり問題じゃありません。ほとんどの場合、それを気にするのは早すぎる最適化です。パフォーマンスチューニングは良いコードだったらいくらでもやる余地はあって、今回は忘れるべきです。

古いJavaのような、クラスにしかメソッドが所属できないモジュールシステムばかりの時代じゃありません。 クラスは基本的に不要だと思います

状態は割れ窓

思いつき限り最悪のコードを書きます。

const validator = new Validator({defaultWithXXX: true})
validator.ignoreMethodOptions = true
const result = validator.validate({ withXXX: false })

オプションがたくさんあるから手続き的に組み立てたいんだ!という主張があるとしたら、オプションを組み立てる部分を別関数にするべきだと思います。 僕だったら高階関数でこうしますが…

const buildValidator = (options) => (input) => ...
const validate = buildValidator({...})

これは関数スコープにoptionsを保持するので不変であることは保証されます。とはいえ、カリー化のないJSで高階関数をやるのは型の支援がないと難しいので、 僕も Flow/TypeScript で型が保証されてる時にしかやりません。

アンチパターン: クラスと継承

現代では、継承は基本的に使うべきではない。ということは同意が取れることとします。基本的には、「継承よりコンポジション」です。

それでもやらないといけないとしたら、ストラテジーパターンを想定したライブラリから、一回だけ、です。それも、プラットフォームが提供するような練られた実装だけから、です。

class Foo extends View {
  render(){...}
}

たしかに継承は、ライブラリなどのよく練られたAPIの一回目の継承は規約として強烈なのですが、それが多段に継承されると protected が乱用され閉鎖原則が破綻しがちで、経験上この先は最悪なコードしか見ません。リスコフの置換原則が守られないのは、歴史が明らかにしています。GUIでの標準コンポーネントを多段継承するのは最悪で、React / Backbone / Android / Flash / Unity で地獄を見ました。

(Rails の Controller 継承は悩ましくて、認証漏れを起こすぐらいだったら規約で縛るのもアリな気がするんですが、読み解くのオーバーヘッド大きくてあんまり好きじゃなくて、それこそ mixin とかでどうにかなるような…)

Go や Rust など、最近の言語ではそもそも継承は実装されないことも増えてきました。コンポジションの手段として Scala では trait, Swift では protocol が提供されているので、基本的に避けられると思います。完全コンストラクタ制約があれば実質イミュータブルみたいなもんでしょう。

イミュータブルだと思いこむ

副作用ではなくGCに頼り切ってイミュータブルなオブジェクトを返す、という実装のが最近は推奨されると思います。

function setA(obj, a) {
  return {...obj, a}
}

基本的にすべてをイミュータブルだと思って使って、古い参照はGCに落としてもらうことが前提です。これである時点でのその参照へのアクセスは保証されます。GC負荷が…と気にするのも早すぎる最適化ですね。

イミュータブル参照を守っていると、リスナーの関数クロージャでアクセスするオブジェクトがアクセスするたびに値が変わる、といったことは避けられます。redux の reducer なんて、それを実現するだけの関数ですからね。

POJOJSONのような、薄いオブジェクトを扱っていると、シリアライズしやすいというのあります。JSはJSONがあるので特殊な環境といえばそうなんですが、他の言語でも ORM にマップするときや、通信のためにシリアライズするときなんかも有用でしょう。クラスのインスタンスから関数以外のプロパティを落としてシリアライズするのは簡単ですが、その逆のデシリアライザを常に用意するのは大変なので、そもそも切り離す、という感じです。

データと実装を切り離して、常にデータだけをシリアライズする、というアプローチは、データの可用性を大きく高めてくれます。批判があるとしたら、それは「ドメインモデル貧血症」じゃないか?というのがありそうですが、それは名前空間の所属だけの問題だと、自分は思います。

ただ、これも型がある環境じゃないとやりづらいとは思います。

最後に

もっといろんな言語の立場を書きたかったけど、JSの立場により過ぎた気します。

書き始めた理由としては、以下の2つの記事が念頭にあります。

qiita.com

ubiteku.oinker.me

この2つの記事に同意してるわけじゃないですが、そういう意見が出る時代だろうという認識です。

最近読んだ本の中では、オブジェクト指向を振り返るにあたって、「Game Programming Patterns ソフトウェア開発の問題解決メニュー」 が最高の本でした。

ゲームプログラミングと銘打ってますが(実際にゲーム特有のパターンはあるものの)、単にゲームを題材にした、ステートフルな対象をどう扱うか、という本で、ゲームでよくあるC++の実装例だけではなく、筆者が関数型や動的型付けならこうなるからこのパターンは不要と切り捨てたりするので、非常にバランスが良いです。とくに継承批判とシングルトン批判が強烈です。

日頃思ってたことを書いてみたけど、すべての立場の想定反論を用意できたわけではないので、反論がたくさんありそう。ファイッ