オブジェクト指向と20年戦ってわかったこと

  • 871
    Like
  • 2
    Comment
More than 1 year has passed since last update.

この記事の内容

Twitterやはてブコメントを見たら、「わかりやすかった」というコメントもあったのですが、どちらかというとネガティブ方面なコメントが多く目につきました。マサカリという用語で忌憚なく意見を言う風潮については別にいいんですが、「わかりにくい」「間違っている」「古い」みたいなコメントは何も生み出さないし、みんなでニコニコポエムを投稿しあうやさしいインターネッツになったらいいなって思ったので、僕もオブジェクト指向について投稿しようと思います。

何原則?

3原則じゃなくて4では?みたいなコメントもあったのですが、別に3でも4でも5でも重要ではないかなって思います。この4原則の出どころがどこかは知らないですが、C++かSmalltalkあたり(このあたりの話を見かけたのはJava登場前だった気がする)をターゲットとしている気がします。Java以降ならインタフェースが、今の時代なら委譲とか入ってそうですし。ちなみに4と言われるのは以下の項目のことを言っていると思います。

  • カプセル化
  • ポリモーフィズム
  • データ抽象
  • 継承

僕が3原則として見かけたのはC/C++の本だった気がしますが、データ抽象はいなかった気がします。この2005年のまつもとゆきひろさんの連載は、カプセル化がない3原則ですね。

オブジェクト指向は4つある

オブジェクト指向言語によって、微妙に考え方とか、理想の形式とかが違っています。「どの言語でも通用するオブジェクト指向のイデア」というものがこの世にあるとしたら、という前提でよく話がされるんですが、基本的にそんなものはほとんどないと思っています。だいたい4系統に類型化できるんじゃないかなって考えています。オブジェクト指向言語によって、何に重きを置いているかが違う気がします。

  • クラス指向
  • インタフェース指向
  • オブジェクト指向
  • メッセージパッシング指向

昔はコンポーネント指向もありましたが、今は絶滅したと思われます。

例えば、C++なんかはオブジェクト指向というよりも、クラス指向ですよね。これはなるべく静的に解決して速度ペナルティをなくそう、というC++のゼロオーバーヘッドの原則の上にオブジェクト指向を乗っけるにはこうせざるを得ないかなと思います。

クラス指向ほどスパルタンにゼロオーバーヘッドが実現できない代わりに、実装のしやすさを多少向上させたのが、インタフェース指向です。とはいっても、「型」というものを意識したコードにはなります。Javaはインタフェースを宣言し、それを実装するクラスは、「どのインタフェースを使っているか」を宣言しつつ実装を書きます。Goは推論でインタフェースを実装しているかどうかを判定するので、インタフェース実装宣言は不要です。

いちばんゆるいのがオブジェクト指向です。動的言語が実装しているのはこれかなと。汎用の辞書/ハッシュ、もしくはそれに似た仕組みが言語で提供されていて、それに対して、名前を付けてデータやメソッドを追加していきます。クラスやインタフェースではなくて、そのメソッドがあるかどうかでそのオブジェクトが識別されます。ダックタイピングってやつですね。動的型付け言語と相性が良かったからか、ここ最近はこのタイプを見かける機会が増えた気がします。Objective-Cはコンパイル言語だけど確かこれ。

Erlangこそが真のオブジェクト指向だ!というのは、Elixirのお陰もあって最近はだいぶ言う人が増えてきました。各プロセスがオブジェクト。プロセスはメッセージボックスを持っていて、何か依頼するには、そいつにメッセージを投稿する。すべてが非同期で、それぞれ別プロセスで動いていて、「シミュレーションのためのオブジェクト指向」といったらこれしかないですよね?Smalltalkとか、Rubyはオブジェクト指向について、「メッセージパッシング」を強調していたりするんですが、どちらかというとインタフェース指向とかオブジェクト指向な気がします。

言語によって重視するものが違うといっても、1種類しかサポートしないわけではありません。C++も、virtualキーワードを使えばインタフェース指向のこともできますし、Qtはmocというプリプロセッサを利用して「名前でメソッドやデータにアクセスする」オブジェクト指向を実現しています。Javaもインタフェースを大事にしますが、クラス継承もサポートしつつリフレクションもサポートしています。クラスという存在を消したいのに作成するところで消しきれないからDIの仕組み用意するわー、というのもJavaっぽいですよね。JavaScriptは基本はオブジェクト指向ですが、DOMなどのAPIをハードに使おうとすればクラスというものがチラチラ見え隠れします。

まず、オブジェクト指向の論客がどのオブジェクト指向を思い浮かべているかを把握すれば無用な争いは避けられると思います。まず小うるさいこと言ってくるのはクラス指向だと思いますが。

オブジェクト指向は、メソッドディスパッチの手法の1つ

で、言語ごとの多少の表記法の違いはあれど、「最大公約数」を取り出そうとしたら、たぶん

オブジェクト.メソッド(引数...)

という部分しか残らないのでは、と思います。裏の実装方法はいろいろ考えられますが(上記の4通りの指向によっても違う)、使う側からすれば

  • メソッド呼び出しを書いたら、オブジェクトの種類によって最適な処理(関数/サブルーチン)が選択される
  • オブジェクトの種類によって、呼び出し可能なメソッドの種類が絞れる

前者はディスパッチと呼ばれるものです。多くの言語では、メソッドの左側のオブジェクトを隠れた第一引数として持った関数としてメソッドを実現しているものと思われます。C言語のような非オブジェクト指向言語と比べると、オブジェクトの種類を判別されるifとかswitch文を1つ消せますよ、というのが言語の文法から見たオブジェクト指向の有無の違いかと。

C++なんかはこれを実現する方法が継承しかなかったからオブジェクト指向の原則に継承が入っているんだと思いますが、動的言語だったら継承とか重要じゃないですよね?C++でもイベントハンドラとして一部の処理を上書きする程度なら、継承を使わずに、C++11以降で使えるlambdaを使うほうがシンプルに書けます。大人の言語のPythonとかやっていたら、「privateを人前で触るような分別のない人はプログラマに向いてないから辞めるべき」って感じなので、カプセル化もどうでもいいですよね。

とはいえ、ディスパッチというのは他にも種類がたくさんあります。C++だったら、プリプロセッサマクロや、テンプレート、オーバーロードもディスパッチの手段として使えます。関数型言語だったらパターンマッチも使えます。やってないけどScalaなら両方使えてお得!な感じなんですかね。

後者は、人間の脳みその記憶容量を節約する効果があります。オブジェクトの種類によって、可能な操作が限定される。オブジェクト指向じゃないAPIリファレンスとかと比べると、オブジェクト指向言語は自然と階層化されて、検索性も上がります。IDEでのコード補完の恩恵も得られるでしょう。初めてRubyをサポートしたNetBeansは、全クラスの全メソッドが表示されてうひゃあとなりましたが、今時のIDEはみんなもっと賢いからそんなことはないですよね。

つーか、ディスパッチの手法かどうかも(最初は)気にする必要はない

というか、「オブジェクト指向の原則はこれ!クラスを作るにはこれを守れ!そしてそれでアプリを作れ!」というのは時代遅れな考え方な気がしていて、今時の言語だったらだいたい頭のいい人達が長時間かけて洗練させてきたオブジェクト指向のライブラリが標準でついてきます。便利ですよね?これを使ってオブジェクト指向じゃないコードを書いているだけでも、オブジェクト指向は学べると思います。そのうち「標準の配列型みたいに、この処理をメソッド化すればコードが短くなって読みやすくなるね」というコードの改善方法も分かってくるでしょう。標準ライブラリとか、フレームワークをお手本にしていけばいいんです。オブジェクト指向の原則だかをいくら読んでもいいコードが書けるようにはなる気はしません。

fluentインタフェースなんかは、オブジェクト指向分析とか設計とかでは出てきにくい構造だと思いますが、パイプ構造に処理をつなげていく、もしくは検索条件の絞込のクエリーを作ったりする文脈では便利だったりします。そういう手法を真似つつ、コードをきれいにしていけばいいかと思います。

「言語非依存のオブジェクト指向の技術で分析して、設計して、それをコードに落とせばきれいなコードが書ける」というのは幻想だとさえ思っています。標準のクラスライブラリのお手本と違うメソッドの規約のコードがまざったコードってだいたいみんな「読みにくいコードだな」って感じるところだと思います。標準的な文字列クラスもなくて(誰も使ってなくて)、ライブラリごとに文字列クラスを持っていた20世紀のC++なら、トップダウンで設計してゼロから世界を作っていく必要がありますが、すでに大量のライブラリやフレームワークがあって、それを利用してアプリを作っていく21世紀では(自分の好みと合わなかったとしても)言語や標準ライブラリとインピーダンスを合わせるのも大事だと思います。

僕の中学生のころとか、教えてくれる人もなく、インターネットもなく、パソコン通信とかお小遣いでは無理で、プログラミングの本も高いし、オブジェクト指向の話を読んでも意義とか全く分からなかったし、結局クラスとか作れるようになったのは大学ぐらいになってからで、5年ぐらいは「オブジェクト指向分からん」という時期をすごしました。いまどきそんなに時間かかったという話は聞かないし、有り物のライブラリや、それで規程されている方式のコーディングスタイルを使うということからオブジェクト指向を学習するというのは学習曲線もなだらかで、効果の高い方法なんじゃないかと思います。

とはいえ、良いコードを書く指針はある

SOLID 5原則(最初に説明した原則はFundamentalsで、これはPrinciples)というものもありますし、リファクタリング本のボトムアップでコードをきれいにしていく手法もオブジェクト指向的で読みやすいコードにしていく手法もあります。これはどちらでも良いというか、同じものを別の側面から見た手法です。分かりやすい方を選べば良いかと思います。読んでないけど、リーダブルコードとかもこれの一種なんだろうなって(勝手に)思っています。

例えば、単一責務の原則(SRP)が守られていたら、「特定の他のオブジェクトのメソッドの処理ばかりを呼び出すメソッド」は存在しないはずです。

UIのウインドウの処理で、ラベルのメソッドを呼びまくる処理があったとします。

label.setColor(red);
label.setVisible(true);
label.setBlink(true);
label.setText("エラー発生!");

リファクタリング本の作法に従ってコード改善すれば、一連のメソッド呼び出しはラベル側に移動し、ウインドウ側では下記のメソッドだけが残るはずです。

label.setError("エラー発生!");

結果として、ラベルの表示内容に関する責務はラベル側に移動したことになるため、SRPが守られてめでたしめでたし、となります。JSの今時ならMVCの仮想DOMでそもそも「エラー発生を明示的に通知する」ことも不要にするでしょうけど、まあそこは置いときます。

リファクタリング本に限らず、デザインパターン、Writing Solid Code、Code Complete、テスト駆動開発入門、ドメイン駆動設計など、それぞれの書籍で、それぞれのアプローチで「良いコードを書く方法」を紹介しています。どれもSOLID 5原則の一部を分かりやすいステップに分解しつつ、各著者が興味のある言語に適した形のtipsを追加していたり、といった感じですよね。

心理学のアンケートは、特定の回答に関する選択肢に偏らないように、統計手法を用いて少ない設問数でうまく回答がばらけて分類できるように選択肢を調整したりといったことをするのですが(僕は心理学の専門化ではないけど)、これらの本で語っているテクニックのベクトルをきれいに還元化して、網羅性の高いパターンに分解するような研究、どっかの大学でやってくれたらいいのになぁ、と思ったり。というか、パターン・ランゲージ界隈もシェファーディングとか言ってないで統計と組み合わせて機械的に作れるような手法を宜しく。

再利用性とか変更に柔軟というファンタジー

たいていオブジェクト指向の話が出てきたら二言目には「再利用性が」と続くんですが、再利用性とか基本的に期待しない方がいいと思います。

だいたい、再利用されたっていいことあんまりないです。たいてい、再利用後に「何か機能を追加したい」みたいな話が出てきます。機能を追加というからには、たいてい新しいメソッドやら引数が追加されます。利用しているすべてのアプリケーションを壊さないように気をつけて機能改善する必要があります。「将来の拡張に備えて今はいらない機能をわんさか実装しておいたよ」みたいなのは今はあまり推奨されてなくて(だよね?)、必要最低限なコードしか無い中、特定のアプリ向けのコードが少しずつ増えていき、エントロピーが増大していきます。こういうコードって、他のコードと共通化してまとめる(インタフェースも変える)みたいな修正がしにくく、基本的に太っていく方向でしか進化しません。GoogleでのGolangの利用時のルールは、ライブラリを使う側が、ライブラリの変更に追従する義務を持って常に最新を使うようにする(ので、ライブラリのバージョン固定とか必要ないので1.5までvendoringとか無かった)ということですが、多くの現場ではアプリケーションの安定性を崩す変更を入れるのをOKとする文化にはなってないと思うので、難しいですよね。

npmみたいな気軽なコード共有手段があれば、細かいこと言わずに小さいコード片でも共有はできるかもしれません。変更したければフォーク!みたいなこともできるかもしれませんが、メンテナンスし続けるのがどれだけ可能か・・・長期的には「古いコードがいたるところに残り続ける」結果になっちゃうのかなぁと。コードを大人数で複数チームで、複数プロダクトを並行開発していると、意図して「コード統一」「分断の防止」をしかけていかないと、すぐにフォークされてお互いあさっての方向に進化し続けます。

今までの経験上、中途半端にライブラリとしてコード共有して再利用するよりは、ワンバイナリでいろんなユーザに使ってもらうように、ユーザ側の仕事の仕方を変えてもらう方が圧倒的に効率が良いです。リサイクルよりもリユース。

再利用不要だからといって、好き勝手にコードを書いていけばいいわけではないです。「再利用性が高い」と言われてきたコードというのは

  • 他のコードへの依存性が少ない/整理されていて、切り出しやすい
  • 細かいロジックがうまく隠蔽されていて、少ないインタフェースコードで利用できる
  • レイヤーがうまく分かれている

あたりに落ち着いてくると思うんですが、そういうコードって、理解しやすい構造になるので、普段からメリットありますよね。アプリケーションを超えた再利用は難しいかもしれないけど、同じアプリ内の複数箇所で共有化したりもしやすくなります。

DDDという負け犬

DDDってのがあります。ユーザの用語を使いましょうね、ユビキタス言語ですよー、ということころと、きちんとレイヤー分けして作りましょうね、あたりが核だと思うんですが、本当にそうなんですかねってのが最近思っているところ。いや、レイヤー分けは問題ないですよ。

ビジネスの根幹をITで動かしていく時代が来たら、ドメインエキスパートは利用者ではなくて、その会社のシステム部門の中にしかない、という時代が来たよ、という話を聞きました。システムを作ってしまったら、細部まで把握している人はコードを書いた人しかいなくなりますよね。

http://blog.shibu.jp/article/163682630.html

例えば、機械用や住居の設計図面、回路図であれば、多少メーカーごとの方言があるとしても、基本的にはJISやISOである程度決まった表記があります。設計図をユーザに合わせた表記で作るなんてのは聞いたことがありません。設計図は単なる絵ではなく、部品表システムと連係したり、NCとかと連動したり(100%そのまま丸投げは無理でしょうけど)、ある種のプログラミング言語ですよね。市販のコンパイラが規格準拠していない時代に書かれて、Visual C++ 7.1が出て「ようやく100%コンパイルできるようなりましたよ」というアナウンスされたBoostという変態ライブラリとかもありますが、設計図面も、CADやCADを動かすOSの寿命よりも、図面の寿命の方が長いというのは日常茶飯事でしょう。

CASEツールやMDAかっこいいとか思っていたこともありましたが、何もないところからUMLみたいなトップダウンで最大公約数で作ってから発展させていくのはうまくいきませんでした。ユーザに近いところにある実働可能なプログラミング言語(Excelマクロとか、あるいはAdobe JSX、MayaのMEL/Python、統計処理のRやPythonあたり、UE4のBlueprint)が発展していくことで、今後20年か30年のうちぐらいに、構造化が得意で見通しが良くて、プログラム知らない人でも、ゲームブックやらあみだくじの延長でなんとなく動きが分かる、どこでも使えるマシンリーダブルな設計用言語がソフトウェア開発方面から作られて、設計図としてさまざまな分野のビジネスを記述するのに使われていく、という流れになっていくんじゃないかなと思っています。

そういう意味で、ユビキタス言語を定義してそれで作っていくというDDDの提唱する方向はソフトウェア開発の敗北であって、電気回路図のように、顧客も設計言語を学び、会話も設計言語で行っていく方面にしていく方向にしていかなくちゃいけない気がします。「ユビキタス言語は過渡期だから仕方ないよね」って可哀想な目で見られるぐらいにならないと、ソフトウェアの技術者が、会社のビジネスを円滑に回すためのプロ集団としてユーザ企業内で尊重されることはなく、「管理職にならないと給料が・・・」みたいな雰囲気を変えるのは難しいと思います。

ポエムじゃない堅いオブジェクト指向の説明

ここを読んでおけば十分な気がします。ちなみに日本語の方と内容が大分違うです。