オブジェクト指向の本当の姿 前編 @Tomofiles
目次
はじめに
こんにちは、Tomofilesです。
今回も、プログラミングパラダイムのお話をしようかと思います。
オブジェクト指向プログラミングは、扱いこなせれば非常に強い武器になります。ただ、習得まで時間と手間はかかります。
特に、ネット上の解説は、なかなかその核心をつかめる内容になっていないこともあり、それも手伝って習得しづらいというのも、あるかもしれません。
オブジェクト指向の考え方が身についている人には、プログラムがどのように見えているのかを、この記事では説明していきたいと思います。
きっと、プログラムの先に見えているものがわかれば、オブジェクト指向はそれほど難解ではないとわかるようになるでしょう。
今回は長くなりすぎたので、前後編に分けました。本記事は前編です。
それでは、いってみましょう。
圧倒的な抽象化
まず、初心者エンジニアの皆さんには、オブジェクト指向プログラミングを追求していくと、どんな姿を表すようになるのか、それをお見せしたいと思います。
ソフトウェア設計手法の一つに、ドメイン駆動設計(DDD)というものがあります。
(DDDは、オブジェクト指向プログラミングを追求した一例と言えると、個人的に考えています)
DDDは、一言で説明するのが難しいテクニックです。
プログラムというのが伝統的に手続き(命令)であると、前回の記事でお話したと思います。しかし、様々なユーザーの要求を、一つの長大な手続きで実現してしまうと、新たな要求に答えたり、バグを直したりなどの、変更に弱いという特徴があることは、前回お話しました。
DDDは、そんなユーザーの要求を、手続きというアプローチは基本的には変えずに、変化に強い形で実現するための、統一的な見方・考え方を提供するものです。
とにかく、抽象度が高い話なので、具体的なお話がしづらいのですが、最初はそれで構わないと私は考えます。
まずは、圧倒的な抽象化の肌感覚を、覚えてほしいです。
DDDが提示する理想郷
戦術的DDD
ここからお話するのは、戦術的DDDと呼ばれるテクニックです。
DDDには、戦略的なアプローチと、戦術的なアプローチがあり、どちらも大切な視点です。(エンジニアは戦術面に偏りがちです)
戦略と戦術については、以前もう一つのブログでも触れたことがあるので、リンクを貼っておきます。
(リンク先記事の中程にあります)
国内外のドローン関連ウェブサービスを調べる(4) 〜UTMと分離の戦略・戦術〜 - Tomofiles Note
DDDというのは、ソフトウェアが解決したいビジネス知識を、(命令的な)手続きのみで構築するのではなく、ビジネス知識をモデル化して、モデル同士を協調させて構築する手法です。このモデルのことを、ドメイン・モデルといいます。
戦術的DDDでは、ドメイン・モデルを構築するための、プログラムの見方・考え方を開発者に与える、各種ソフトウェアコンポーネントの概念が提示されています。
最初に言っておきますが、ドメイン・モデルおよび、これらのソフトウェアコンポーネントは「具体的に言語仕様をどのように使ってコーディングするか」という点は、まずは重要ではありません。
DDD界隈はもともと、実装方法・コーディングを置いてけぼりにしてしまいがちだと、個人的に思っていて、そもそもDDDは実践しにくい手法と言われているのですが。
ですが、戦術的DDDの理解と、実装方法の検討は分けて考えないと混乱するので、しょうがないことなんですよね。
それでは、戦術的DDDの示す、ソフトウェアの統一的な見方と考え方について、細かく見ていきましょう。
ソフトウェアコンポーネントとその協調
戦術的DDDにおけるソフトウェアコンポーネントの中で、重要なものを以下に挙げます。(意図的に、いくつか削ってます)
- エンティティ
- リポジトリー
- ファクトリー
- イベント
- サービス
エンティティは、識別可能・追跡可能な(同一性のある、という)、ビジネス上の概念のまとまりのことをいいます。
概念のまとまり、とわざと言葉を濁したのですが、このエンティティをどのように見つけるかが、DDDの究極的な目的でもあるので、一言では語れないのです。エンティティを見つける取り組みは、戦略的DDDの領域であり、結構難しい…。
見つけるためには、ドメイン・モデルをイテレーティブに作っては壊してを行い、徐々にフォーカスのあったモデルに導くように、設計と開発を進めていくことになります。
リポジトリーは、エンティティが格納されている場所のことをいいます。
どのように永続化するか? という問題は、まず無視してください。
リポジトリーとは、識別子により一意に判別できるエンティティが格納してあり、新しいエンティティを追加でき、識別子により1件エンティティを取り出すことができ、識別子により1件エンティティを変更・削除ができるもの、のことといいます。
ファクトリーは、エンティティを生成する場所のことをいいます。
エンティティは、基本的に複雑な構造のオブジェクトになるので、自分自身を構築・生成する仕事は、自分自身では行いません。そのため、エンティティを生成してくれる場所を設けて、それに頼ることになります。
イベントは、あるビジネス上の変更や判断をトリガーに、何かが発生・変化したことを伝えるものをいいます。
エンティティは、ビジネス上の概念のまとまりであり、エンティティ内では、そのビジネス上のルールに則って、様々な判断や状態の変化を起こします。
エンティティの変化やある条件の発生は、イベントとしてエンティティの外部に伝えられ、そのイベントに関心のあるオブジェクトが購読して、次のアクションにつなげる役目があります。
サービスは、あるビジネス上の知識でユーザーの要求を解決するための、一連の手続きをまとめたものをいいます。
今回はあまり触れません。
以上、簡単なソフトウェアコンポーネントの説明でした。
お気づきの通り、どれも抽象度の高い概念レベルの話ばかりです。
皆さんには、全然ピンと来てないかもしれないので、もう一歩話を進めてみましょう。
これらのソフトウェアコンポーネントを、イメージ図に表したものが、下図になります。
ビジネス上の知識でユーザーの要求を解決するために、これらソフトウェアコンポーネントはどのように協調して、機能を実現するのでしょうか?
いくつか、例を挙げて、見てみましょう。
新しいリソースを作成して登録する
- 新しいリソースを作成するには、ファクトリーにエンティティの生成を依頼します。
- 生成されたエンティティは初期状態であるため、ユーザーから要求された、リソースに求める状態をエンティティに反映します。
- 最後に、エンティティをリポジトリーに格納します。
JavaのSpring FWを使った場合の、ソースコードサンプルは以下のようになります。
@Component
public class ResourceRegisterService {
@Autowired
private Repository resourceRepository;
@Transactional
public void add(ResourceId id, ResourceUserInputInfo info) {
ResourceEntity resource = ResourceFactory.create(id);
resource.write(info);
this.resourceRepository.save(resource);
}
}
これだけです。
既存のリソースを取得して更新する
- リポジトリーから、識別子を指定してエンティティを取得する。
- ユーザーから要求された、リソースに求める状態をエンティティに反映します。
- 最後に、エンティティをリポジトリーに格納します。
同様に、ソースコードサンプルは以下のようになります。
@Component
public class ResourceUpdateService {
@Autowired
private Repository resourceRepository;
@Transactional
public void update(ResourceId id, ResourceUserInputInfo info) {
ResourceEntity resource = this.resourceRepository.getById(id);
resource.write(info);
this.resourceRepository.save(resource);
}
}
まぁ、同じですね。
リソースの変更時に何らかの処理を行う
- ユーザーから要求された、リソースに求める状態をエンティティに反映します。
- このとき、エンティティ内では、あるルールに基づいて判断を行い、条件が一致したらイベントを生成する
- エンティティの外側に、イベントを発行する
- イベントハンドラがエンティティのイベントを購読して、イベントの発行を検出して、処理を実行する
同様に、ソースコードサンプルは以下のようになります。
@Component
public class ResourceNotificationService {
@Autowired
private Repository resourceRepository;
@Autowired
private EventHandler resourceChangeEventHandler;
@Transactional
public void update(ResourceId id, ResourceUserInputInfo info) {
ResourceEntity resource = this.resourceRepository.getById(id);
resource.subscrive(event -> {
if (event.type == EventType.ResourceChanged) {
this.resourceChangeEventHandler.handle(event);
}
});
resource.write(info);
this.resourceRepository.save(resource);
}
}
resouceのwriteメソッドを実行する前に、subscribeメソッドを実行して、イベントハンドラをエンティティに登録しています。
「○○が◇◇だったとき、××を行う」という機能要件は、このイベント発行・購読の仕組みで、判断処理をエンティティ内、判断結果の処理をエンティティ外で行うようにして、重要なビジネス上の知識をエンティティ内に閉じ込めているところが、この構成のキモです。
アプリケーションサービスとドメイン・モデルのサービス
ここまで見てきたシナリオの、各サンプルソースコードでは、各クラスが○○サービスと名付けられています。
ちょっとややこしいのですが、この○○サービスは、ユースケースと同じであり、ドメイン・モデルのコンポーネントとしてのサービスとは、別物です。
- アプリケーションサービス : ユーザーの要求を解決するための一連の手続きであり、ビジネス上の知識を高い抽象度のレベルで用いて、解決するもの
- ドメイン・モデルのサービス : ビジネス上の知識の一部として、一連の手続きにまとめて、名前を付けて再利用可能にしたもの
まぁ、この程度の理解で、今回は大丈夫だと思います。
ドメイン・モデルが示すソフトウェアの見方
ここまで、かなり難解な話でしたが、ついてこれましたかね?
人によっては、ここまでの話はそんなに難しくないと感じるかもしれません。
しかし、そう感じる人ほど、もしかすると、ドメイン・モデルを構築する営みの、本当の難しさに気づけていないのかもしれません。
このドメイン・モデルにおいて、物理的な永続化システムであるデータベースが関連するのは、リポジトリーのみになります。
この条件を前提にすると、ドメイン・モデル構築時に、様々な障害に阻まれることになります。
例えば、「エンティティ内で判断を行う際に、追加でデータベースから情報を取得したい」と思っても、それを実装してはいけません。
リポジトリーはあくまで、エンティティをまるごと取り出す部品なので、部分的な呼び出しは禁止です。
それから、エンティティと、別のエンティティの関連があったときも、同様に障害に阻まれます。
例えば、「Aエンティティを変更したあと、その結果を、Bエンティティに反映させたい」と思っても、それを実装してはいけません。
2つのエンティティに関連があるのであれば、その2つのエンティティは、2つに分かれているべきではないです。
もしくは、同時に2つのエンティティを変更するのではなく、変更を伝播させていくようにするべきです。(これについては、すこし難しいので理解できなくていいです)
DDDで重要なのは、こういった各ソフトウェアコンポーネントにルールが設けられているので、ルールに従うことで、ソフトウェアの構成が疎結合・高凝集になり、再利用性や柔軟性が向上します。
が、ルールが圧倒的に実現難易度高なので、準拠させるのが難しいのです。ときに、発想の転換みたいな、ひらめきが求められるかもしれません。
それでも、苦労してでも、このアプローチを行うことが有効なのは、これらソフトウェアコンポーネントのルールに従って作ることで、解決しようとしているビジネス上の知識の関係性を明らかにして、関係性をしっかり維持し続けることで、密結合・重要ロジックの分散を防ぐことができます。
つまり、ソフトウェアの設計・開発を通して、ビジネス上の知識を研究し、関係性を明らかにして、モデル化するという、アカデミックなアプローチこそが、DDDの真価なのです。
現在のソフトウェア開発のシーンは、ビジネスの展開スピードとリンクするようになってきているので、ソフトウェアの変化に求められるスピードも、どんどん上がってきています。
そんな状況下で、密結合・重要ロジックが分散したソースコードを、正確に把握しながら、新しい価値・サービスを追加していくのは、実質不可能です。
常に、ソフトウェアのビジネス的価値を、把握可能な粒度と関係性で整理しておき、新しい変化を取り入れやすくしておくことが、重要になります。
前編まとめ
前編は、圧倒的な抽象化の例として、ドメイン駆動設計(DDD)についてお話しました。
オブジェクト指向というのは、ネット検索すると、型にはめたように「オブジェクトをモノとして扱う」とか、「カプセル化されたクラスを作る」だとか、「ポリモーフィズムという性質を示す」っていう説明がされています。
これらの説明は、間違っていません。間違っていないですが、どちらかというと、オブジェクト指向で設計・開発されたプログラムは、「モノとして扱われている」性質があり、「カプセル化されている」性質があり、「ポリモーフィズムを示す」性質がある、という、結果として示す性質が、主な目線となります。
そのため、ただ、この性質を示すようにプログラムを書くだけでは、実はオブジェクト指向の本当の価値を、享受できているわけではないのです。
オブジェクト指向の真価は、解決しようとしているビジネス上の知識の関係性を、明らかにして、モデル化して、それをプログラムで実装する際に、やっと発揮されます。
つまり、まずは、ビジネス上の知識をしっかりと勉強して、適切な概念の単位に分割して、お互いの関係性を明らかにして、モデル化することが重要です。
そのモデルの構成要素を、カプセル化されたクラスとして実装し、構成要素同士を強調させ(メッセージ・パッシング)、ときにポリモーフィズムも活用して、柔軟に構築します。
オブジェクト指向はツールであって、本当に大事なのは、解決したいビジネス上の知識を研究することであって、その結果を表現する際に、ツールとして効果を発揮する、ということだと、私は考えています。