主旨
- 以前はシステムの状態をオブジェクト指向でカプセル化し、オブジェクト同士の通信でシステムの制御をしようとしていた
- しかし、Webアプリケーションのように状態をメモリ上に保持し続けるのが難しい環境が増えると、上記のことがやりにくくなった(ORMのインピーダンスミスマッチの影響が大きくなった)
- 現在では、システム全体の状態を管理するためにオブジェクト指向を用いるシーンは減っているが、要所要所でシステムを抽象化する道具の一つとして用いるシーンはあり、適材適所で使い続ければ良い
はじめに
一時期あれだけもてはやされた「オブジェクト指向」ですが、現在では「業務システム開発においてオブジェクト指向で作るとろくなことがない」、とか、いっそ「不要である」、という意見もよく見かけます。
オブジェクト指向、この記事では特に「オブジェクト指向プログラミング」を対象として話をしますが、その利点は以下の3点に集約されると私は思っています。
- カプセル化による状態のスコープ範囲の極小化
- 多態性による処理の抽象化
- 継承による差分プログラミング
このうち 3. は、現在ではあまり声高にメリットとして語られることはなく、どちらかというと「考えなしに多用するとスパゲッティソースになる」というようなデメリットと共に語られることが増えています。
継承は割とピーキーな「テクニック」の部類であって、「図形」と「円」のような関係を語る際には本来は 2. の多態性を中心に考えるべきであり、差分プログラミングは必須でなかった(コンポジションによるコード共有でも実現できる)ということでしょう。
また、1. はネームスペースやモジュール分割でもある程度果たせますし、2. は 3. ほどではないにせよ割と難易度が高い為、使いこなせない技術者がいてもおかしくはないかもしれません。
結局 1. ~ 3.を用いたバリバリの(?)オブジェクト指向プログラミングは、ライブラリやフレームワークの構築の際には有用なことが多い為、不要になったわけではないし、それを使う側もある程度のオブジェクト指向の知識は依然として必要なものの、業務システムを作る際にはあくまでも「オブジェクトの利用側」になるだけであって、業務システム自体をオブジェクト指向で作ろうとするとハマるよ、というのが、よく聞かれる「オブジェクト指向不要論」の骨子ではないかと思います。
※多くのフレームワークやミドルウェア、ライブラリは、オブジェクト指向で作られていることが多いです。この記事は、それらを用いて作る業務システムそのものがオブジェクト指向である必要があるのか、という事を考察しています。
業務システムをオブジェクト指向で作る際の最大の障害のひとつ、ORM
ORM(Object-relational mapping)とは、ObjectとRDBのマッピングのことを言います。つまり、オブジェクトの状態をRDBに永続化し、RDBのレコードからオブジェクトの状態をロードすることです。
この時、よほどシンプルなデータ構造でない限りは、ほとんどのケースで「インピーダンスミスマッチ」と呼ばれるマッピングの不整合が起こり、その解消のために多くのコストがかけられます。コードは複雑化し、実行効率も下がっていきます。
例えば「社員一覧」というデータには「社員ID、社員名、所属部署」だけが必要だとします。しかし、データベースには「社員テーブル」「部署テーブル」「所属部署テーブル」のように保存されており、これに対応する「社員」「部署」というエンティティクラスの為の「DBからの取得処理」をそのままで使うことはできません。
なぜなら、「社員」や「部署」のすべてのフィールドが必要なわけではなく、そのままだと不要なフィールドまでロードしてしまって非効率だからです。結局「社員一覧」をDBから取得するための処理を別途記述する必要があります。
このような構造は様々な場面に登場し、その都度、「必要なフィールドだけを持つクラス」と「そのフィールドだけをDBから取得する処理」が作られていくと、「社員クラスの責務ってどっからどこまでだっけ?」みたいな感じになっていきます。
社員クラスは所属する部署を知っているはずです。「いや、社員は部署を知らないようにして、所属部署クラスに問い合わせると部署が返るようにして…」みたいなこともできると思いますが、それが果たして使いやすいかというと微妙な気がします。社員クラスが所属する部署を知っているとして、その部署のインスタンスがまだ存在しない場合には、ロードする必要があります。そのための仕組みが必要になります。
社員オブジェクトのリストがあり、ここから先ほどの「社員一覧」データを作って表示したい場合、所属部署の名前を取得するために個別に社員オブジェクトから部署オブジェクトを取得する処理を回していては、社員オブジェクトの数に等しい部署のSELECT文がRDBに対して投げられることになります。
結局、「社員IDと社員名までは社員オブジェクトとしてメモリ上に持ってるけど、改めて社員一覧データをRDBから取得するメソッドを読んだ方が効率的」という話になっていきます。
つまりどういうことかというと、オブジェクト指向とは「オブジェクトの状態がプログラムのメモリ空間に存在する」ことを要求するのに、実際の「業務の状態」はRDBに存在しているため、そこに不整合が生じ続けるのです。
ネイティブのデスクトップアプリにおけるORM
例えばWindows用のデスクトップアプリケーションをオブジェクト指向で作った場合、先ほどの問題はある程度回避できます。
なぜならば、アプリケーションの存在期間中、必要なオブジェクトを常にメモリ上にロードし続けることが可能だからです。
MVCで作成した場合ならば、コントローラが必要なモデルをRDBからロードし、それを様々なビューに渡して表示・加工し、必要であれば入力内容をモデルに書き戻してRDBにセーブすればよいわけです。これはひとえに、一つのコンテキスト(ある目的の間の複数の画面遷移)中に、ロードしたモデルをメモリ上に保持しつづけ、「必要な状態が全てメモリ上にある」という、オブジェクト指向プログラミングにとって都合の良い状態を作ることができるからです。
WebアプリケーションにおけるORM
ところが、Webアプリケーションではそうはいきません。社員情報編集画面から「所属部署の変更」へと画面遷移した場合、前の画面でロードしていた社員情報は失われています。「所属部署の変更」画面を表示するためのサーバ側のコントローラは、改めて社員情報と所属部署のデータをRDBから取得して表示したり、コントローラパラメータとして必要な社員情報と現在の所属部署を引き継ぐ、のような処理を実装する必要があります。
つまり、Webアプリケーションにおいて、あるコンテキストの間に必要なオブジェクトをメモリ上に保持しつづけることは困難です。そのため、常にORMのインピーダンスミスマッチがオブジェクト指向プログラミングを阻害し続けます。
コンテキストに必要なオブジェクトを全て「セッション情報」としてサーバ側のメモリに保持し続ける手法もありますが、近年ではスケールアウトの為にサーバをステートレスに設計するのが主流であり、あまり歓迎されていません。かといって、JWTとして全ての必要オブジェクトをクライアント(ここではブラウザ)に保持し、全てのリクエストに付随させて回るというのもやりすぎでしょう。
結局、Webアプリケーションではサーバ側に状態を保持しづらい為、オブジェクト指向プログラミングとの相性はあまりよくありません。
Single Page Appicationの場合
SPA(Single Page Application)の場合、アプリケーションの状態をブラウザ側で管理することができます。これあある意味でデスクトップアプリケーションと同じ状況である為、オブジェクト指向プログラミングがやりやすいと言えます。
つまり、必要な「社員」オブジェクトや「部署」オブジェクトのリストがメモリ上に存在する、という状態を作りやすいわけです。
ビューそのものの実装は、Reactのような状態非依存のフレームワークが示すように、「ビューの状態はDOMに丸投げし、ビューの描画はステートレスにしよう」というやり方もありますが、例えば「社員」のようなオブジェクトは状態として別途管理する必要があり、ここではオブジェクト指向の考え方が生きてきます。
これに対してサーバ側の実装はどうなるかというと、クライアントの持つ状態との同期のための処理がほとんど占めることになります。これは、やはり状態を持たない、ある意味で「DBの抽象化」のようなAPIだけになりがちです。この場合、サーバ側の実装はほとんどがORMを行っているだけとなり、オブジェクト指向プログラミングがやりづらい、というか、オブジェクト指向で作る意味が薄いでしょう。
システムのWeb化に伴い、オブジェクト指向がやりづらくなった
この流れからわかるのは、以下のようなことです。
- 業務システムのようなデータベースの利用を前提とするシステム開発においてはそもそもORMのインピーダンスミスマッチによりオブジェクト指向でやりづらい場面が多かった
- デスクトップアプリケーションならばORMの部分と実際の業務処理をある程度切り分けて実装することができる為、オブジェクト指向は割と実用的だった
- Webアプリケーションに移ると、それらの手法がやりにくくなり、オブジェクト指向をやりづらい側面がまた浮き彫りになった
これに加えて関数型プログラミングのような新しいパラダイムの浸透もあり、最近オブジェクト指向が声高に取り上げられなくなっている理由は、こういうあたりにあるのではないかと思いました。
ドメインモデルとトランザクションスクリプト
こうなってくると、業務システムは無理にオブジェクト指向分析・設計を使ってドメインモデルを抽出してオブジェクト指向プログラミングで・・・みたいなことをするより、「ドメインモデルなど無関係にユースケース単位に関数を作って処理」というトランザクションスクリプトの方がシンプルだ、という考え方にも説得力が生まれてきます。
つまり、状態を保持するのが難しいのなら、入力の結果をDBに出力する手続き型や関数型プログラミングで十分である、ということです。
私はこの考え方も割と正しいと思っているのですが、実際にトランザクションスクリプトで業務システムを作っていくと、同じ処理があちこちに現れたり、無理やり処理を共有するためにif文だらけのツギハギプログラムが出来上がったりしていくのを目の当たりにしています。
なぜなら、トランザクションスクリプトで作るとドメインモデル単位での視点が希薄になる為、ユースケースの各所で似たような処理が現れてしまうのです。
これらは動作はしますが、長期間メンテナンスを続けていくと、いずれ「ここを修正して正しく動くかわからないから触れない」という限界を迎え、メンテナンス不可の状態に追い込まれます。
業務システムでオブジェクト指向は不要なのか?
ようやく本題です。
対象の業務システムがデスクトップアプリケーションのように、そのコンテキスト中のドメインオブジェクトをメモリ上に保持しづつけられるような環境であるならば、ある程度はオブジェクト指向で作った方が作りやすいでしょう。
しかし、Webアプリケーションとして業務システムを作る場合には、オンメモリに必要なオブジェクトが常に存在する、みたいな状況を作りにくい為、デスクトップアプリケーションと同じようなオブジェクト指向の手法は使いにくなります。
だからといってオブジェクト指向プログラミングやオブジェクト指向分析・設計が不要かというとそうは思いません。
前述のトランザクションスクリプトでは、ドメインモデルの視点で処理を共通化したり、抽象化したりするのが苦手な為、処理の重複が生まれやすくなっていました。しかし、オブジェクト指向プログラミングはそういう「データの抽象化」は得意分野です。
1つの処理のコンテキストにおいては状態を持たない手続き型・関数型プログラミング的な作り方をしつつ、関数の引数やその処理ではオブジェクト指向を活用すればよい、というか、活用すべきでしょう。
例えば、引数の型を具象クラスではなく抽象クラスやインタフェースにすることで、その関数が扱える場面を飛躍的に増やすことができます。関数の引数に妙なフラグやパラメータを増やしてif文で場合分けするのではなく、インタフェース越しに処理を記述するようにすれば良いのです。
適切に、似たような処理が複製される危険性を減らし、複雑さをうまく抽象化して扱うことで、システムの変更をしやすい形に保つことができるようになります。そのような抽象化は関数型プログラミングでも可能ですので、シーンに応じて使分けていけばよいでしょう。
例えば外部システムとのやりとりを行うような処理において、トランザクションの間だけでも外部システムの状態を保持しつづけたい、というようなことはあるでしょう。そのような場合には、オブジェクト指向は強みを発揮できます。システム全体の状態管理ではなく、局所的に「状態」をカプセル化して隠ぺいしたいようなシーンでは、オブジェクト指向はわかりやすいと思います。
オブジェクト指向を使う目的や場面が変わってきている
そのような設計にするためには、オブジェクト指向の知識や経験が不可欠です。
オブジェクト指向が不要になったわけではなく、オブジェクト指向を使う場面が以前とは少し変わってきている、とことではないでしょうか。
以前は、「システムが持つ状態をオブジェクトとして表現し、それらが互いに通信しあってシステムを制御する」ことがオブジェクト指向の究極のゴールでした。
しかし今は、「システム全体の状態を管理するため」ではなく、「システムを抽象化して捉え、変更に対して柔軟なシステムにする」ことが、オブジェクト指向を使う目的や理由に変わっているのではないかと思います。つまり、オブジェクト指向のみで全体を設計するというよりは、必要に応じて道具として用いるようになってきている、ということです。
例えばDIなんかもオブジェクト指向を上記の目的で使うための手法と捉えてよいと思います。
この記事でいう「オブジェクト指向との相性が良くない」という例は、基本的には「システムの状態がオブジェクトとしてメモリ上に存在し、それらに対して指令を出すことでシステムを制御する」という、従来のオブジェクト指向設計をベースに考えた場合の話であったということです。
まとめ
オブジェクト指向は今でも業務システムで使います。
しかし、以前と違ってシステム全体の状態をカプセル化し、制御しようという目的で用いられることは減ってきました。なぜならば、システムの状態はデータベースに存在し、そのORMインピーダンスミスマッチが解決できない上、Webシステムのように状態を保持しづらい環境が増えてきた為です。
現在では、システム全体をオブジェクト指向で管理するためではなく、例えばDIのように、システムを抽象化するための道具のひとつとしてオブジェクト指向が用いられます。また、ユースケースを関数として実装する場合でも、その引数や処理の内部ではデータを抽象化して扱うためにオブジェクト指向が用いられます。
個人的にはオブジェクト指向の概念において現在、最も重要なのは「抽象化」や「インタフェース」である、と考えています。「抽象化オブジェクト指向」とか「インタフェース指向」と言い換えてもいいかもしれません。オブジェクトの状態をカプセル化した上で、どのようにそれを抽象化して扱うか、という視点が大事なのだろうと思います。
以上、この内容は筆者の一意見にすぎません。いろんな方の視点があり、もちろんオブジェクト指向は不要であるという考え方もまた真である現場もあると思います。私自身も、今後の経験の積み重ねで少しずつ意見が変わっていくかもしれません。
あなたの視点のひとつとして、この記事が役立てば幸いです。
追記
2024.02.13
関数型プログラミングでも、ドメインモデルを用いた設計手法があるんですね。Twitter(現X)のコメントで知りました。とても興味があるので、いずれ調べてみたいです。
2024.02.13
※2024.2.13 頂いたアドバイスを記事に反映しました。
2024.02.11
この記事を公開した後、「プロになるJava」の執筆者であるきしだなおきさんよりTwitter(現X)でアドバイスを頂きました。こうして識者の方から直接アドバイスを頂けるのは、本当にありがたいです。
最初、アドバイスの意味がよく理解できなかったのですが、きしださんに丁寧に教えて頂き、以下のような事だと理解しました(私の理解としてのまとめであってきしださんの意図を正しく汲めているかはわかりませんが)。
- 抽象化の側面だけを捉えては、オブジェクト指向ならではという事にならず、関数型のみで問題ない場面が多い為、「オブジェクト指向が必要」とする根拠として弱い
- オブジェクト指向の得意分野はやはり状態管理なので、関数の入出力の内部に限定すれば、オブジェクト指向は今も通用する
この記事の主旨は「システム全体の状態をオブジェクト指向で管理しようとすると永続化周りで大変だから、オブジェクト指向の状態管理機能については一旦忘れて、インタフェースや多態性の方に目を向けよう」というものなのですが、それだけだと「だったら関数型でもいいじゃん」という事にもなりかねないのだなぁと思いました。
システム全体の状態をオブジェクト指向で管理するのはやめて、ユースケースを関数として書いてその引数や処理の内部でインタフェースやオブジェクトを使おう、という考え方はきしださんの考え方にも近いと思っているのですが、それはそれとして、使うオブジェクト自身の状態管理についてもやはり、重要ということで、追記とさせて頂きます。
きしださんのご指摘はとても腑に落ちるもので、私の考察に抜けていたものが正しく補間されたと感じています。この場をお借りして改めて感謝いたします。