読書メモ:現場で役立つシステム設計の原則

読書メモ:現場で役立つシステム設計の原則

September 2, 2022

以下の本を読んだ。あとで見返したいところについてメモを残しておく。


第1章:小さくまとめてわかりやすくする #

「値」を扱うための専用のクラスを作る - P.32 #

業務アプリケーションで数量を扱うとき、int のすべての範囲(マイナス 21 億からプラス 21 億)が必要になることはありません。「0 より大きく 100 より小さい」など、もっと狭い範囲の値が業務的に正しい値です。

その業務的に正しい値を扱うためには、クラスを独自に宣言して、異常な値を扱わないようにします。

文字情報も同じです。String 型は全ての文字種を、実質的に無制限の長さで扱えます。しかし、そういう業務のニーズはありません。

業務アプリケーションをオブジェクト指向で設計する場合には、業務で扱うデータの種類ごとに専用の「箱」(クラスやインターフェース)を用意します。

このように、値を扱うための専用クラスを作るやり方を値オブジェクト(Value Object)と呼びます。

これに対して値オブジェクトを使わない場合はどうでしょうか。コードは int 型や String 型だらけになります。

int 型や String 型が扱える値の範囲は、業務で必要な値の範囲とはかけ離れています。そういう値を扱えてしまうプログラムでは、思わぬバグを生みがちです。

ほとんどの値オブジェクトは基本データ型のインスタンス変数を1つか2つと、操作メソッドを持つだけの小さなクラスです。

コレクション型を扱うロジックを専用クラスに閉じ込める - P.40 #

考え方は値オブジェクトと同じです。データと関連するロジックは、1つのクラスに集めます。

int 型の変数を1つ持った「数量」の専用クラスを独自に作ったように、コレクション List<Customer> 型の変数を1つだけ持った「顧客一覧」の専用クラスを独自に宣言します。

そして、List<Customer> を操作するロジックは、すべてこの Customers クラスに集めます。顧客の追加や削除、顧客数のカウント、特定条件で絞り込んだ顧客の抽出などのロジックです。

このように、コレクション型のデータとロジックを特別扱いにして、コレクションを1つだけ持つ専用クラスを作るやり方をコレクションオブジェクトあるいはファーストクラスコレクションと呼びます。

コレクションを操作するロジックをコレクションオブジェクトに閉じ込めると、コレクションオブジェクトを使う側のコードが単純になります。

第3章:業務ロジックをわかりやすく整理する #

データとロジックを別のクラスに分けることがわかりにくさを生む - P.68 #

従来の手続き型の設計では、アプリケーションのクラス構成を、データを格納するデータクラスと、ロジックを記述する機能クラス(ロジッククラス)に分けることが基本になります。

データクラスは、getQuantity(), setQuantity() など、いわゆる getter, setter と呼ばれるメソッド群だけを持ちます。データを使った判断、加工、計算のロジックは、ロジッククラスに記述します。

しかしこの構成は以下の状況を招きがちです。

  • 変更の対象箇所を特定するために、プログラムの広い範囲を調べる
  • 1つの変更要求に対して、プログラムのあちこちの修正が必要
  • 変更の副作用が起きていないことを確認するための大量のテスト

こうなってしまうのは、データクラスとロジッククラスを分ける手続き型の設計では、業務ロジックが入り込んでくると、次の問題が顕著になるからです。

  • 同じ業務ロジックがあちこちに重複して書かれる
  • どこに業務ロジックが書いてあるか見通しが悪くなる

データクラスを使う設計では、ロジックはロジッククラスに書きます。このやり方だと、データクラスを参照できる場所であれば、どこにでもロジックが書けます。その結果、データクラスのデータを使うロジックは、プレゼンテーション層、アプリケーション層、データソース層のどの層のクラスにも書けてしまいます。また、同じロジックが異なるクラスに重複して記述されがちです。

そもそもデータクラスとロジッククラスに分ける設計は、「クラス」が意図する本来の使い方ではありません。クラスはデータとロジックを1つの単位にまとめるしくみです。データをインスタンス変数として持ち、そのインスタンス変数を使った判断、加工、計算のロジックをメソッドに書くのが、クラスの本来の使い方です。

コラム:データクラスが広く使われているのはなぜか #

オブジェクト指向言語として登場した Java を使ったアプリケーションで、なぜデータクラスのようなオブジェクト指向らしくない設計スタイルが広まってしまったのでしょうか。

業務アプリケーションを Web アプリケーションとして開発するようになると、業務アプリケーションの開発言語も、COBOL や C 言語から Java に移行していきます。その際、設計のやり方や開発の進め方は、従来の業務アプリケーション開発をそのまま引き継ぎました。COBOL や C 言語でやっていた設計を、そのまま Java にも適用したのです。

COBOL や C 言語は手続き型のプログラム言語です。手続き型の設計では、プログラムをデータ構造とロジックの記述に分けます。そして、プログラムの構造は、トップダウンの機能分割が基本です。

この手続き型の設計スタイルを Java で踏襲した結果が、データクラスとロジッククラスに分ける設計です。Java のプログラムは、必ずクラス単位で書く必要があります。Java で手続き型の設計を踏襲すればデータ構造を記述するクラスがデータクラスになり、処理を記述するクラスがロジッククラスになるわけです。つまり、オブジェクト指向を意図した Java のクラスを、手続き型プログラミングの実装単位として採用したのです。

使う側のクラスに業務ロジックを描き始めたら設計を見直す - P.80 #

データを持つクラスに、そのデータを使った判断、加工、計算のロジックを書くのがオブジェクト指向のクラス設計の基本です。

データを持つクラスからデータを get して、そのデータを使って判断、加工、計算するロジックを書き始めたら、何か変だと考えましょう。

メソッドは必ずインスタンス変数を使う - P.81 #

インスタンス変数を使わないメソッドは、そのクラスのメソッドとしては不適切です。ロジックの置き場所を再検討すべきです。

引数だけを使い、インスタンス変数を使わないメソッドがあるとします。インスタンス変数を使わないのであれば、このメソッドはこのクラスに置く意味がありません。

検討の結果、そのロジックを引数を渡している側のクラスに移動するのがよいかもしれません。引数で渡すデータを持っていたのがさらにその先の別のクラスであれば、そのクラスにロジックを移動すべきかもしれません。

第4章:ドメインモデルの考え方で設計する #

データモデルではなくオブジェクトモデル - P.99 #

ドメインモデルの設計に取り組むときに、ドメインモデルとデータモデルを別のものとして考えることが大切です。手続き型の設計では当たり前だったデータモデルの発想ではドメインモデルの設計はうまくいきません。

ドメインモデルは、業務ロジックの整理の手法です。業務データを判断、加工、計算するための業務ロジックを、データとひとまとまりにして「クラス」という単位で整理するのがオブジェクト指向の考え方です。関心の中心は業務ロジックであり、データではありません。

一方、データモデルは、文字どおりデータが主役です。業務で発生するさまざまなデータを整理して、どうテーブルに記録するかを考えます。

ドメインモデルでは、「年齢」が業務の関心事であれば、年齢クラスを作ります。年齢クラスは内部的に生年月日をインスタンス変数に持ち、そのインスタンス変数を使って年齢を計算するロジックをメソッドとして持ちます。年齢を知りたいという関心事があり、それを計算するロジックの置き場所が必要だから年齢クラスを作る、というアプローチです。

一方、データモデルでは、年齢は記録すべきデータではありません。計算の結果です。テーブルには計算のもとになる生年月日だけを記録します。手続き型の設計では、データとロジックの整理を分けて考えるため、年齢という関心事はデータモデルには登場しないのです。

このように、業務ロジックに注目し、それをクラスという単位で設計するドメインモデルと、データの整理を目的とするデータモデル(テーブル設計)は、本質的に違うものなのです。

データモデルを中心にプログラム設計を進めると、データクラスとロジッククラスに分ける手続き型の設計になります。テーブルのレコードをデータクラスに対応させ、そのデータクラスによって渡されたデータを使って判断、加工、計算するロジックはロジッククラスに記述します。

データモデルではロジックを描く場所があいまいです。データクラスを参照できるクラスであれば、どのクラスにも書けてしまうからです。

オブジェクト指向で設計するドメインモデルは、手続き型のプログラムにありがちな、業務ロジックが散在し重複する問題を解決する工夫です。

部分を作りながら全体を組み立てていく - P.104 #

ドメインモデルとデータモデルでは、その分析と設計のやり方が異なります。

大きく複雑な全体を、どのように小さい単位の構造に持ち込むかの発想がオブジェクト指向と手続き型では正反対なのです。

  • 手続き型のアプローチ
    • 手続き型の設計では、全体を俯瞰し定義するところからスタートします。段階的に分割をしながら、より詳細に定義を進めます。いわゆるトップダウンのアプローチです。
  • オブジェクト指向のアプローチ
    • オブジェクト指向は、部分に注目します。個々の部品を作り始め、それを組み合わせながら、段階的に全体を作っていきます。ボトムアップのアプローチです。

ある部分に注目して、その部分に必要なデータとそのデータを使った判断、加工、計算のロジックだけに範囲を限定すれば、データとロジックを一緒に考え、そのままクラスとして設計することは難しいことではありません。

そうやって部分に注目してデータと業務ロジックを一体にした小さな部品(ドメインオブジェクト)を設計しながら、全体を組み立てていくのがオブジェクト指向のやり方です。

独立した部品を組み合わせて機能を実現する - P.109 #

ドメインモデルは、業務アプリケーションの部品となるドメインオブジェクトを集めて整理した部品の倉庫です。業務機能を実現するためには、ドメインオブジェクトを、部品倉庫であるドメインモデルから取り出して組み合わせます。

ドメインオブジェクトを組み合わせて、業務の機能を実現するのはアプリケーション層のクラスの役割です。

ドメインオブジェクトを機能の一部として設計しない - P.110 #

プログラムを開発するときに機能を中心に考え、機能を分解しながらプログラム部品を作っていくと、一つひとつの部品は、機能の分解構造に依存します。つまり、上位の機能部品と、それを分解して定義した下記の機能部品はかんたんに切り離せなくなります。

また、機能中心にプログラムを書いていくと、プログラムの構造が、処理の順番に依存します。複雑な処理を小さな処理単位に分割したときに、それぞれの部品の前後関係に強く依存しがちです。処理の前後関係に依存した部品を組み合わせたプログラムは、何か変更があった時に、その前後関係への依存によって変更の影響範囲が広がります。

ドメインモデルを構成する個々のドメインオブジェクトの設計では、こういう機能の分解構造や時間的な依存関係を持ち込まないようにします。特定の機能や処理の順番からは独立させて、単体で動作確認ができる独立性の高い部品として開発します。

どうやって機能を実現するかに注目するのではなく、ある特定の業務のデータとそのデータを使った判断、加工、計算の業務ロジックだけを切り出した独立したオブジェクトを作ります。

たとえば、消費税クラスは消費税率と税額計算や端数の丸めロジックをまとめた独立した部品として開発します。消費税クラスのオブジェクトが、どの業務機能のどこで使われるかがはっきりしなくても、設計し開発が可能です。

何でも約束してよいわけではない - P.117 #

ドメインモデルの設計のアプローチは、まず部品を特定し、その部品ごとに独立したクラスを設計することです。受注時のルールすべてを扱う大きなクラスは、考えないようにします。数量パッケージと同じように、与信パッケージや基本契約パッケージについても独立して設計します。

そうやって、ある程度の部品がそろってきたら、組み合わせ方を考えます。組み合わせてみながら、個々の部品を調整したり、不足している部品を追加することで、受注ルールに網羅できるだけのドメインオブジェクトが整っていきます。

業務ルールの記述 〜 手続き型とオブジェクト指向の違い - P.121 #

膨大な業務ルールを整理して定義していけば、数値、日付、文字列という基本データ型に対する判断ロジックになります。

そして、true だった場合のアクション、false だった場合のアクションが存在します。膨大で複雑な業務ルールも、その最小の構成単位は、if 文の判定と場合ごとのアクションに分解できます。

手続き型の場合は、データクラスを受け取ったロジッククラスで、if 文、switch 文を使って必要な条件判断と分岐を実行します。条件の組み合わせは、基本的に if 分の入れ子構造になります。これがトランザクションスクリプトと呼ばれる手続き型の業務ルールの記述方法です。

オブジェクト指向の場合は、判断のもとになるデータとロジックごとにオブジェクトを生成します。それぞれのオブジェクトは自分で判断ができます。判断結果によって何をすべきかの情報を持つことができます。さまざまな判断を担当するドメインオブジェクトを用意したうえで、適切に組み合わせて判断するのがオブジェクト指向のアプローチです。if 文の入れ子構造にはなりません。

このアプローチの違いによって、業務ルールの追加や変更が発生した際に、どのようにプログラミングしていくかの違いが生まれます。

手続き型のトランザクションスクリプトでは、新しいルールを追加するには、もともとの if 文の分岐構造に新たな分岐を追加します。業務ルールが増えるほど、分岐構造が複雑になります。また、同じデータを受け取る可能性がある別のトランザクションスクリプトにも同じような分岐の追加が必要かもしれません。

オブジェクト指向のドメインモデルでは、新たなルールの追加は、ルールの判断のもとになるデータを持つオブジェクトの判断ロジックを追加します。変更の影響範囲も、そのクラスに閉じ込めやすくなります。

取りまとめ役のクラスの導入 - P.131 #

ドメインオブジェクトの最小単位は、1つの数値、日付、文字列をラッピングした値オブジェクトや、コレクションオブジェクトなどです。

しかし、これらは業務の関心事の粒度としては少し小さすぎます。業務の主たる関心事はもう少しまとまった単位です。たとえば住所です。

値オブジェクトとしては、郵便番号クラス、市区町村クラス、街区クラス、番地クラスなどに分けることはできます。

しかし、業務の主たる関心事は、それらを組み合わせた住所クラスです。住所クラスのように、最小単位のドメインオブジェクトをいくつか組み合わせたクラスが、業務の主たる関心事になります。

小さなドメインオブジェクトを組み合わせたクラスは、その組み合わせ方を改善することで、ドメインモデルが洗練され、業務の関心事をより適切に表現できるようになります。

第6章:データベースの設計とドメインオブジェクト #

業務アプリケーションの中核の関心事は「コト」の管理 - P.182 #

業務アプリケーションの中核の関心事は「コト」の管理です。業務アプリケーションでデータベースが必要なのは、コトを正しく記録し参照するためです。

  • 現実に起きたコトの記録
  • 将来起きるコトの記録(約束の記録)

コトを記録するだけで、理論的には現在の状態は導出可能です。たとえば、銀行口座テーブルに現在高を持つカラムを用意しなくても、入金記録と出金記録をすべて合算すれば現時点の残高を計算できます。テーブル設計にあたっては、まず、このコトの記録を徹底します。

コトを正確に記録することだけに注力したテーブル設計が、データベース設計の最重要事項ということを肝に銘じてください。

コトの記録に注力したテーブル設計の問題 - P.185 #

もちろん、理論的に導出できるといっても実際には状態を参照したいニーズも多くあります。そのたびに残高や現在の状態を動的に導出するのは、ロジックが複雑になり性能面でも問題があります。

データベースでは状態を以下のように扱いましょう。

  • 基本はコトの記録テーブル
  • 導出の性能を考慮して、コトの記録のたびに状態を更新するテーブルも用意する
  • 状態を更新するテーブルはコトの記録からいつでも再構築可能な二次的な導出データ

たとえば、口座に入金があったら入金テーブルにコトを記録する。そして、残高テーブルのその口座の残高も増やす。口座から出金があったら、出金テーブルにコトを記録する。そして残高テーブルのその口座の残高を減らす。

データベースの本質は事実の記録です。まず、コトの記録を徹底することが基本です。状態テーブルは補助的な役割であり、コトの記録から派生させる二次的な情報です。

UPDATE 文は使わない - P.186 #

残高テーブルは UPDATE 文ではなく、DELETE 文、INSERT 文の対で実行します。

UPDATE 文はデータの不整合が混入しやすい動作です。それは、コトの記録のところで述べた「記録の同時性」に違反するからです。

そうではなく、レコード単位で古い残高を DELETE し、新しい残高を INSERT するのが正しいデータの記録方法です。

なお、UPDATE 文を使ったやり方は、新規口座の場合は INSERT するなど追加のしくみが必要になります。DELETE、INSERT 方式では、該当する口座の残高レコードの有無にかかわらず操作できるので、ロジックがシンプルになるメリットがあります。

残高更新は同時でなくてもよい - P.187 #

コトの記録と残高の更新を厳密なトランザクションとして処理することは、考え方としては正しくありません。

コトの記録はデータの本質的な記録であり、残高の更新は二次的な導出処理です。ですので、残高の更新に失敗したらコトの記録も取り消すというのは、データの記録の考え方としてまちがっているのです。

もちろん、残高の更新が失敗したことを検知し、何らかの対応をとるしくみは必要です。しかし、そのしくみは、本来のコトの記録からは独立させるべきなのです。

このことは、コトの記録と残高更新が同時でなくてもよいことを示唆します。要求にもよりますが、たとえば数ミリ秒の遅れで残高が更新されても、何も問題が起きないことが普通です。

残高更新を、たとえば、非同期メッセージングで別システムに任せてしまうような方法で処理の独立性を高め、システムの設計をシンプルにできる可能性が生まれます。

オブジェクトの設計とテーブルの設計 - P.191 #

オブジェクトは、データとロジックを一体に考えます。プログラムのロジックを重複させないしくみであることが本質です。

テーブルはデータの管理が本質です。導出結果や加工結果ではない元データの記録と整理の手段です。

オブジェクトの設計とテーブルの設計は大きく異なります。このため、オブジェクトとテーブルを自動的にマッピングするアプローチはうまくいきません。どちらかの設計のアプローチに大きく制約されます。その結果、それぞれの変更の足枷になります。

たとえば、テーブル設計を優先してオブジェクトをそれに合わせるアプローチは、ロジックの整理に失敗します。ロジックを重視したオブジェクトの設計にテーブル設計を合わせるアプローチは、データの正しい管理に失敗します。

オブジェクトとテーブルは別物です。オブジェクトはオブジェクトとして、テーブルはテーブルとして設計と改善を進め、オブジェクトとテーブルの間のマッピングは、その両方の設計の進度に合わせながら明示的に定義するようにします。こうすることで、お互いの設計変更の影響をマッピング定義に局所化でき、オブジェクトの設計とテーブルの設計をより良いものにできます。

オブジェクトとテーブルのマッピングのしくみとして、さまざまなフレームワークが利用できます。ただし、ドメインオブジェクトに、このようなフレームワークの都合を持ち込んではいけません。ドメインオブジェクトの設計をゆがめ、コードの見通しを悪くします。

ドメインオブジェクトの設計を、業務ロジックの整理に集中させるためには、ドメインオブジェクトの設計の自由度を保ちやすいフレームワークと技術方式を選択しなければいけません。

Java の場合、自由度を保ちやすいフレームワークの例が MyBatis SQL Mapper です。

第7章:画面とドメインオブジェクトの設計を連動させる #

画面に引きずられた設計はソフトウェアの変更を大変にする - P.198 #

利用者にとって、画面こそがソフトウェアの実体です。画面に表示する項目とその画面で利用できる機能が、利用者にとっての関心事です。

画面に対する要望を実現するためには、画面単位にプログラミングし、画面の表示処理に、ロジックを埋め込むのが手っ取り早い方法です。単純なアプリケーションであれば、この作り方が最も確実で早いでしょう。

しかし、画面単位のプログラムは、ソフトウェアの記述を不要に複雑にします。

画面ありきで開発すると、画面を表示するロジックと業務ルールを表現した業務ロジックが混在しがちです。

画面を使って利用者の要望を理解することと、業務ロジックをどういう構造で整理してプログラムを組み立てるかを一緒にしてはいけません。

たとえば、優先して処理すべき注文について考えてみましょう。一定金額を超え、かつ、注文後1日以上を経過した注文を、画面で強調表示するとします。

手取り早いのは、画面で強調表示したい箇所に、if 文を使って判断ロジックを書くことです。

if( amount > 100000 && orderDate.before(localDate.now()) )
    classAppend('important'); // 強調表示する CSS クラスを追加

この if 文の条件判断は業務ルールです。このような業務ロジックが画面を表示するコードに紛れ込むと、業務ルールの変更時に問題が起きます。

注文を表示する複数の画面に応じ if 文が書かれます。どこに何が書いてあるかを調べ上げ、必要な箇所をすべて変更し、変更の副作用がないか、広い範囲のテストが必要になります。

タスクベースのインターフェースが増えている2つの理由 - P.206 #

何でもできる汎用画面ではなく、用途を特定した小さな単位に分けた画面を提供することをタスクベースのユーザインターフェースと呼びます。

最近は、タスクベースのユーザインターフェースが増えています。主な理由は以下の2つです。

  • スマートフォンの理由が増えた
  • 通信環境の変化

スマートフォンは、パソコンのブラウザに比べ、画面の表示や操作がシンプルです。また、回線がつながらないことや、なにかの都合で操作を中断するといったことも普通に起きます。そのため、たくさんの情報を入力する画面は、スマートフォンの利用形態として、使いにくくなります。

それに対して、タスクベースのユーザインタフェースにすると、そのときだけやりたいことだけに限定した、ちょっとした操作を提供できます。スマートフォンを利用するときには、そういう小さなタスク単位での操作が当たり前になりました。

ソフトウェアの実行基盤でも大きな変化が進行中です。

かつては通信は高価な手段でした。可能な限り、通信量や通信の頻度を少なくすることが重要な課題でした。

現在は、小さな単位で頻繁に通信することを前提としても、それほど大きな問題は起きません。むしろ通信状態が悪い場面では、大きなデータを一括して通信するより、小さな単位で頻繁に通信する方が良い場面すらあります。

サーバ側も、こういう小さい単位で多頻度の通信を処理することがやりやすくなりました。その代表がクラウドの利用です。かつてはピーク時に合わせた資源を確保するためにコストが跳ね上がりました。しかし、クラウドを利用して、必要な資源を必要なとき、必要なだけ確保することで、実行基盤のコストを柔軟にコントロールできるようになりました。

このようなスマートフォンの普及と、通信の処理環境の変化が引っ張る形で、小さな単位で頻繁に通信し、処理をする形態が当たり前になってきました。

画面もドメインオブジェクトも利用者の関心事のかたまり - P.209 #

利用者にとっては、目の前にある「画面」こそがソフトウェアの実体です。画面に表示する項目とその画面で利用可能な操作が、利用者にとっての関心事です。

画面はドメインオブジェクトを視覚的に表現したものです。ドメインオブジェクトを画面に表示するには、いくつかの選択肢があります。

  • ドメインオブジェクトをそのまま画面の表示にも使う
  • 画面用のオブジェクトを別途用意する
  • 画面用のデータクラスを別途用意する

画面は利用者の関心事のかたまりです。ドメインオブジェクトは、利用者の関心事のソフトウェア表現です。つまり、同じ関心事の異なる表現です。だとすれば、画面の表示にドメインオブジェクトをそのまま使うのがよいはずです。

しかし、実際にはいくつかの問題が起きます。

  • 画面はさまざまな関心事が複合していて、ドメインオブジェクトの粒度や構造と整合しにくいときがある
  • 画面の表示だけに関係する判断や加工のロジックをドメインオブジェクトに持ち込みたくない

この問題に対応するために、2つのやり方があります。

ひとつは、ビュー専用オブジェクトを用意する方法です。画面表示用のデータを保持し、かつ、そのデータを表示用に加工するロジックを1つにまとめたオブジェクトです。

もうひとつは、データの受け渡しのためのデータクラスを用意する方法です。ただし、データクラスを使った方式はオブジェクト指向の良さを失います。データクラスを使うと、どこにでもロジックを書けてしまいます。その結果、どこに何が書いてあるかわかりにくくなり、コードの重複も増えます。変更時の副作用も起きやすくなります。データクラス方式は選択すべきではありません。

となると、残された選択肢は、ドメインオブジェクトをそのまま使うか、表示用のロジックを持つビュー専用オブジェクトを別に用意するかの2つです。

ドメインオブジェクトをそのまま使うのとレビュー専用オブジェクトを別に用意する方式とでは、どちらを選べば良いでしょうか。

結論から言えば、ドメインオブジェクトをそのまま使うことを重視すべきです。

ビューとモデルの分離は、設計原則として広く知られています。そのため、モデルの設計に画面などビューの影響が出ることを気にしすぎているケースをときどき見かけます。

ビューに書くべきことと、ドメインオブジェクトに書くべきことを整理する考え方は次の3つです。

  • 論理的な情報構造はドメインオブジェクトで表現する
  • 場合ごとの表示の違いをドメインオブジェクトで出し分ける
  • HTML の class 属性をドメインオブジェクトから出力する

それぞれについて具体的に考えてみましょう。

論理的な情報構造をドメインオブジェクトで表現する #

ビューの記述は基本的に次の2つに分かれます。

  • 物理的なビュー
  • 論理的なビュー

物理的なビューは、画面を表示する技術方針に依存したビューの表現です。論理的なビューは、技術方式には依存しない概念的な構造です。

例として、複数の段落がある「説明文」を考えてみましょう。

画面に表示するときに、段落と段落の間に空白を入れたり、段落の先頭を字下げしたりします。

HTML で表示する場合、段落は <p> タグを使います。各段落の字下げは、<p> 要素の視覚化の指定として CSS で指定します。

メールの本文をテキストで表示する場合は、段落の区切りは改行コードを使います。字下げは、全角の空白1文字を使います。

このような HTML のタグや、改行コードを使うのが物理的なビューの表現です。

それに対して論理的なビューは「複数の段落」という「構造」だけを表現します。Java で記述すれば次のようになります。

String[] description;

段落の場合を String の配列として表現しています。

ドメインオブジェクトでは、この論理的な構造を表現します。たとえば「段落がいくつあるか」とか「最も長い段落の文字数のカウント」などが、ドメインオブジェクトが持つべきロジックです。

また、一覧画面で要点だけ表示する仕様を実現するために「最初の段落の、最初の 20 文字だけ表示する」というような、要約文に加工するロジックもドメインオブジェクトが持つべきです。それが利用者の関心事であり、かつ、論理的な構造とその操作だからです。

ドメインオブジェクトが持ってはいけないのは、<p> タグとか、段落の先頭を全角一文字で字下げするといった物理的な手段です。

また、物理的な表現手段である改行コードを含む文字列も持つべきではありません。この場合は、改行コードを含む1つの String ではなく、String[] として複数の行を持つという論理構造を表現すべきです。

場合ごとの表示の違いをドメインオブジェクトで出し分ける #

画面表示で if 文を使っている場合は、その条件判断をドメインオブジェクトに移動できないか検討します。

たとえば、検索結果の件数表示を考えてみましょう。ゼロ件だった場合は「見つかりませんでした」、見つかった場合は「N 件見つかりました」と表示するものとします。

よくある方法は、ドメインオブジェクトに件数の判断ロジック isFound() を持たせ、isFound() メソッドの結果をビュー側の if 分で判定してメッセージ内容の記述を変えるやり方です。

ところが、次のようにドメインオブジェクトで実装することで、ビュー側に if 文の条件判断が不要になります。

class Items {
    List<Item> items;

    String found() {
        if(items.count() == 0) return "見つかりませんでした";

        return String.format( "%s 件見つかりました",
            items.count());
    }
}

このロジックをドメインオブジェクトに書くことは、ビューとモデルの分離の原則に違反しているように感じる人もいるかもしれません。

しかし、変更を楽で安全にすることを重視するなら、一覧の結果を持つ Items クラスに、検索結果に応じたメッセージの出し分けのロジックを持たせた方が、コードがシンプルになります。1000 件以上見つかった場合とか、1 件だけだった場合とかの条件分岐を増やすときも、Items クラスだけが変更の対象です。画面側に if 文の条件分岐で書いてしまうと、すべての箇所を洗い出して変更することが必要になってしまいます。

情報の文字列表現は利用者の関心事そのものです。ドメインオブジェクトに記述することは、むしろ自然です。

ドメインオブジェクトが返す文字列表現に、物理的な表示手段である改行コードや HTML タグを含めるべきではありません。しかし、情報の文字列表現そのものは、むしろ積極的にドメインオブジェクトが持つべきです。そのほうが、関心事を1ヵ所に集約し閉じ込めやすくなります。

HTML の class 属性をドメインオブジェクトから出力する #

条件によって視覚表現を変える例として、「新着」は赤字 + ボールドで強調表示する、という例を考えてみましょう。

ドメインオブジェクトに isUnread() メソッドを用意し、画面の表示ロジックの中で if 文を使って「もし新着だったら unread を class 属性として指定する」というやり方があります。

これについても、次のようにドメインオブジェクトが状態を表す情報を返し、それを class 属性で利用するやり方で、画面の表示ロジックから if 文をなくすことができます。

String readStatus() {
    if( isUnread() ) return "unread";

    return "read";
}

// HTML 記述での使い方
// <p class="${mail.readStatus()}">

ドメインオブジェクトの返す情報を class 属性として利用するこのやり方は、モデルにビューの関心事が入り込んでいると感じる人がいるかもしれません。しかし、そうではありません。ドメインオブジェクトで表現する論理的な状態を、ビュー側が利用する、という考え方です。

このほかにも、カンマ編集や千円単位の表示なども、ドメインオブジェクトが持つべき加工ロジックの候補です。データの文字列表現は、利用者の関心事です。そういう関心事に関わる加工や判断のロジックは、できるだけドメインオブジェクトに集約した方が、変更が楽で安全になります。

画面を表示するロジックに if 文が入り込み始めたら、要注意です。それは、ドメインオブジェクトに書くべきロジックの可能性があります。

第8章:アプリケーション間の連携 #

中核となる API のセットを設計する - P.250 #

  • 登録と参照を分ける
  • リソース(データのかたまり)の単位を分ける

この2つを常に意識することが良い Web API の発見につながります。それぞれについて具体的に考えてみましょう。

登録と参照は別の API にする #

例として指定席を予約する API を考えてみましょう。

予約が POST されたときに、その結果は何を返すべきでしょうか。画面アプリケーションであれば、予約した内容の詳細を予約完了画面に表示するのが一般的でしょう。

この予約機能を実現する API 設計として次の2つの選択肢があります。

  • POST のレスポンスとして、予約内容の詳細を返す
  • POST のレスポンスは予約番号だけを返し、予約内容はその予約番号で別途 GET する

前者は、Web API ではなく Web サービスの発送です。

アプリケーションとして組み立てるための部品としては、後者のように登録の API と参照の API を別々にした方が柔軟にアプリケーションを組み立てることができます。

POST では、新たに作られたリソースの識別番号のみを返します。そして、その内容を確認するために、参照用の別の API を用意します。

このように登録と参照を分けるのは、アプリケーション設計の一般原則です。そして、Web API をシンプルに保つための重要な設計原則です。

予約の結果の参照方法や応答内容に変更があっても、予約登録の API に影響しません。参照の API を登録 API への影響を心配せずに修正や拡張ができます。

参照と登録を別の API にするのは、関心事を分離し、プログラムの記述をわかりやすくシンプルにする設計の基本原則なのです。

リソースの単位を分ける #

次のような会員情報の登録と参照を行う Web API を考えてみましょう。

  • 氏名
  • 性別
  • 生年月日
  • 連絡先
  • 住所

すべてを1つの API で取得させる形にすると、氏名だけが必要な場合でも、毎回、性別、生年月日、連絡先、住所を取得することになります。

そのやり方ではなく、用途別により小さな単位の個別データを受け取る API を提供する方法があります。

GET members/1234/name           名前を返す
GET members/1234/gender         性別を返す
GET members/1234/dateOfBirth    生年月日を返す
GET members/1234/contactMethods 電話番号とメールアドレスを返す
GET members/1234/address        住所を返す

このように、対象とするリソースを必要な最小の単位に分けることを重視して設計します。

また、使い方を限定した用途が明確な API を組み合わせる方が、多様な機能を実現しやすくなります。そういう小さい単位の API に分けておけば、特定の API に修正や変更があっても、ほかの API への影響を小さくできます。

単純でよく使われる API は早く安定します。修正や変更がだらだらと続き、なかなか安定しないのは、より小さな単位に分割すべき明らかな兆候です。

Web API バージョンの管理 - P.253 #

URI にバージョン番号を入れるという考え方があります。

http://api.example.com/v2/members

しかし、プログラミングの部品を提供することを重視する Web API では、全体のバージョン管理を行うことにあまり意味がありません。

リソースを小さな単位に分ける API では、新たな小さな API の追加が中心になります。その場合、既存の API には何も影響しませんから、API バージョンとして管理する意味がありません。現在の API セットが有効な唯一のバージョンとして提供し、利用すれば良いわけです。

あるタイミングでは古い API とそれに代わる新しい API が両方とも存在します。これは、プログラミング言語や標準ライブラリの API と同じです。そして、古い API はある程度の時間をかけて廃止していきます。

廃止する場合は、一定の移行期間を設けて段階的に廃止します。この場合も API 全体のバージョンという管理ではなく、個別の API の廃止の予告と実施という流れになります。

たとえば次のような手順で段階的に廃止します。

  1. 新しい API を追加しても、互換性のため古い API も提供する
  2. 古い API は残すが、「303 See Other」 を返すように変更する(新しい API の情報を返す)
  3. 古い API のレスポンスとして「404 Not Found」を返すように変更する
  4. API 自体を削除する

データ形式とドメインオブジェクトを変換する際に起こる不一致 - P.255 #

ドメインモデルで設計した場合、Web API のおもな役割はドメインオブジェクトと、JSON などのテキスト表現との変換です。

GET リクエストは、ドメインオブジェクトを JSON 形式に変換して応答します。POST リクエストは、JSON 形式やフォームパラメータで送られてきたテキストデータをドメインオブジェクトに変換します。

しかし、このような JSON とドメインオブジェクトを単純に変換するだけでは不都合な場合があります。以下の不一致が大きい場合です。

  • データ構造の不一致
  • 関心事の不一致

データ構造の不一致 #

データ構造の不一致は、データ項目が多い場合に起きる問題です。ドメインオブジェクトはロジックの整理を軸にクラス分けします。オブジェクトのネットワーク構造である情報のかたまりを構成します。コードの重複を防ぐためです。

一方、API で使うデータ形式は、データだけが関心事です。できるだけ単純な構造のほうが使いやすくなります。データだけに注目した場合、ロジックの整理を重視したドメインオブジェクトの階層構造を、そのまま階層的なデータ構造として表現することは、あまり意味を持ちません。

関心事の不一致 #

ドメインオブジェクトの持つすべての情報が、API を利用する側で必要だとは限りません。また、ドメインオブジェクトが期待するデータ項目がすべて POST されるとは限りません。ある項目については、デフォルト値のような約束事で埋める処理が必要になります。

アプリケーションが異なる以上、このような関心事のずれはどうしても発生します。ずれが小さい場合は、ドメインオブジェクトと JSON の単純なマッピングで済ませることができます。しかし、ずれが大きい場合は、変換用の中間オブジェクトを用意した方が、コードをシンプルに保ちやすくなります。

やり方としては、まずレスポンス用にドメインオブジェクトを変換します。

// ドメインオブジェクトからレスポンスオブジェクトを生成する

class BookResponse {
    ...
    static BookResponse fromBook(Book book) {
        // Book オブジェクトから BookResponse を生成するファクトリメソッド
    }
}

この例ではレスポンス用のデータ形式に合わせた BookResponse クラスを用意しています。そしてそのクラスの static なファクトリメソッドを用意して、ドメインオブジェクト Book から、レスポンス用の BookResponse クラスを生成します。構造の違いや項目の違いを、このファクトリメソッドが吸収します。

次に、リクエストをドメインオブジェクトに変換します。

// リクエストオブジェクトからドメインオブジェクトを生成する

class BookRequest {
    ...
    Book toBook() {
        // BookRequest から Book を生成する
    }
}

HTTP で POST されたデータを、まず、BookRequest オブジェクトにマッピングします。BookRequest クラスに、ドメインオブジェクト Book を返す toBook() メソッドを用意します。

BookRequest クラスや BookResponse クラスは、プレゼンテーション層のビューとして定義します。ドメイン層のクラスとして Book を定義します。

すべての Web API でこのように変換を作り込む必要はありません。このような変換を採用するのは、ドメインオブジェクトと外部形式の不一致を吸収するための変換が必要なときだけです。

変換の複雑さを持ち込む理由は、ドメインオブジェクトにほかのアプリケーションのデータ形式や、JSON などの実装依存のコードを持ちこまないためです。ドメインオブジェクトは業務の関心事だけに集中します。それ以外の関心事が紛れ込みそうになったら、変換のしくみをプレゼンテーション層に追加して、ドメインモデルへの悪影響を防止します。

第9章:オブジェクト指向の開発プロセス #

見積もりと契約 - P.283 #

従来のフェーズモデルの場合、分析や設計段階は、期間を決めて、その期間内の作業に対する対価を支払います。準委任契約と呼ばれる方式が一般的です。そして、分析と基本設計をもとに、詳細設計と実装の見積りを行います。全体の見積もりは、分析と基本設計が終わっていることが前提です。

これに対して、オブジェクト指向の切れ目のないやり方では、開発の初日から分析し、設計し、実装も始まります。フェーズモデルでいえば準委任契約の段階です。開発者がコードを書き始めているけれども、まだ分析の段階だということです。

フェーズモデルでは、一定期間、分析と基本設計を進めたあとで、見積りを行います。そして、詳細設計と実装は、完成に対して対価を支払う請負契約の方式が一般的です。

オブジェクト指向で開発する場合も、開発の初期は準委任の形式で分析、設計、実装を進め、見積もりの基礎情報が把握できてきたタイミングからは請負契約に切り替える、ということは可能です。双方にとってもリスクは小さくなります。これは結果として、前半、つまり分析と基本設計の段階は準委任で契約し、後半の詳細設計と実装のフェーズは請負契約という、フェーズ方式の契約モデルと同じようになります。

ただし、オブジェクト指向の変更容易性をより効果的に活かすには、後半も準委任の契約にすべきです。

使用の変更や開発の優先順位を変えたいときに、準委任契約のほうが柔軟に対応できます。オブジェクト指向で開発するよさは、変化に適応していくための、ソフトウェアの変更を楽に安全にする点です。そこを活かすためには、後半も準委任契約という形態がより合理的です。

請負契約は、発注側も受注側もリスクの多いやり方です。要求が変化したときに、契約に縛られて柔軟に対応できません。事業環境の変化が止まらない現在の状況では、変化に対応しにくい固定的な決め事は大きなリスクなのです。