画面とドメインオブジェクトの設計を連動させる
概要
この記事は、現場で役立つシステム設計の原則〜変更を楽で安全にするオブジェクト指向の実践技法を読み学習した内容を Ruby on Rails のコードに例を変換してまとめ直したものです。
また、書籍では Java のコードを例にしており、Ruby on Rails の慣習とは一部異なる部分があるので、その部分はオミットした内容となっています。
この記事では、アプリケーションの画面設計とそれに対応するドメインオブジェクトの設計のベストプラクティスについて解説しています。
アプリケーション画面の開発の難しさとは
画面には様々な利用者の関心事が詰め込まれている
利用者にとっては、画面こそがソフトウェアの実態であり、画面に表示する項目とその画面で利用できる機能が、利用者にとっての関心事である。(ドメインモデル方式で設計した場合は、その利用者の関心事はドメインオブジェクトと対応する。)
画面を見て実際に操作をしていくと、要求をヒアリングしたときには出なかった様々な要望が見えてくる。
しかし、それらの要望を取り入れるたびに、画面が入り組み、それにつれてソフトウェアの構造も見通しが悪くなっていく。
画面に引きずられた設計はソフトウェアの変更を大変にする
画面に対する要望を実現するには、画面単位にプログラミングし、画面の表示処理にロジックを埋め込むのが最も簡単な方法だが、これはソフトウェアの記述を不要に複雑にする
具体的には、画面ありきで開発すると、画面を表示するロジックと業務ルールを表現した業務ロジックが混在しがち。
例えば、以下のコードは注文後に時間経過した場合に注文情報を強調表示するロジックを記述しているが、この if 文による条件判断は業務ルールである。
div class=(['order', ('important' if amount > 100_000 && order_date < Time.zone.now)].compact.join(' '))
| 注文情報を表示
このような業務ロジックが画面を表示するコードに紛れ込むと、業務ルールの変更時に以下のような問題が発生する。
- 「注文」という関心事を表示する複数画面に同じ if 文が重複する
- どこに何が書いてあるかを調べ上げ、必要な箇所をすべて変更する必要がある
- 変更の副作用がないか、広い範囲をテストする必要がある
また、画面自体が入り組んでいるパターン(スクロールが必要なほど表示項目が多い、ボタンやリンクが多いなど)の場合は、画面単位で開発すると、画面の複雑さに比例してプログラムが肥大化し、さらに変更が難しい。
関心事を分けて整理する
前項までで述べたように画面アプリケーションのコードが複雑で変更が難しくなる原因は以下の二つである。
- 画面そのものが複雑
- 画面の表示ロジックと業務ロジックが分離できていない
これらの問題を解決するためには、次の方針で関心事を分けて整理する必要がある。
- 様々な表示項目やボタンを詰め込んだ何でもできる汎用画面ではなく、用途ごとのシンプルな画面に分ける
- 画面周りのロジックから業務のロジックを分離する
以下で、具体的な設計の考え方を述べる。
画面の関心事を小さく分けて独立させる
複雑な画面は異なる関心事が混ざっている
画面でもクラスでも、一つひとつが大きく複雑な場合、必ず異なる関心事が絡み合っている。
例えば、注文画面は以下のような関心事の組み合わせとなる。
- 注文者を特定する情報(氏名や顧客番号)
- 注文した商品と個数
- 決済方法
- 配送手段と配送先
- 連絡方法
注文登録時にはこららの情報の妥当性チェックが必要であるが、一つの大きなサービスクラスとメソッドで実現する場合は以下のような形となる。
class OrderService
# 注文データ内容を確認する全てのロジックを持つ大きなメソッド
# orderオブジェクトは注文に必要なデータを全て持つ大きなクラス
def register(order)
# 氏名の妥当性のチェック
# 顧客番号の妥当性のチェック
# 商品の妥当性のチェック
# 数量の妥当性のチェック
# ...
# 全てokなら注文を登録する
end
end
このような肥大化したクラスとメソッドは、例えば、配送手段を追加するときに、大きな注文クラスの「どこか」に追加し、大きな登録メソッドの「どこか」で配送手段の妥当性のロジックをを変更しなければならない、というように変更箇所の特定が難しくなる。
小さな単位に分けて考える
例えば、注文に関するドメインオブジェクトと注文登録のメソッドは以下のように分けて考えることができる。
| 対象 | ドメインオブジェクト | 登録メソッド |
|---|---|---|
| 注文者 | Customer | register(customer) |
| 注文内容 | Items | register(items) |
| 決済方法 | PaymentMethod | register(payment_method) |
| 配送手段 | DeliverySpecification | register(delivery_specification) |
| 連絡先 | ContactTo | register(contact_to) |
| 注文の確定 | Order | submit(order) |
このように関心事を分解すると、前項で挙げた配送手段を追加する例などは、DeliverySpecification クラスに追加するだけで良くなる。
そして、実際の注文登録は、これらを組み合わせて Order クラスと resister メソッドを作成すれば良い。
このようにドメインオブジェクトとメソッドを小さな単位に分けて設計することで、どこに何が書いてあるかが明確になり、変更箇所の特定が容易となる。
画面も分けてしまう
ドメインオブジェクトとサービスクラスの登録メソッドを小さな関心事に分離したように、画面にもこの関心事の分離を適用できる。
具体的には、一つの注文画面で全てを入力するのではなく、以下のような複数の画面を用意する。
- 顧客の氏名の登録
- 注文内容の登録
- 決済方法の登録
- 配送手段の登録
- 連絡先の登録
- 注文の登録
このように、用途を特定した小さな単位に分けた画面を提供することをタスクベースのユーザーインターフェースと呼ぶ。
タスクベースのユーザーインターフェースとして設計するメリットは以下の通り。
- 必要なときに必要な情報だけを登録できるので、一つの画面で全ての情報を集めてから入力する汎用画面と違い、入力時に全ての情報を用意しなくても良い
- 汎用画面を入力する場合と比べて、更新する必要がない項目は表示しないので、余計な情報が表示されなくなり、さらに不必要な変更が起きる可能性がなくなる。
タスクベースに分ける設計のメリット
画面の設計方針をタスクベースとすることで、全体の設計も以下のようなメリットが生まれる。
- 画面ごとに必要なドメインオブジェクトとサービスクラスがシンプルとなる
- タスクベースの構造にしておけば、タスクごとに独立性の高い開発ができる
- テストもタスク単位であれば単純となる
ただ、業務アプリケーション分野においてはまだまだ多目的な注文画面や様々な検索条件を組み合わせられる複合型の検索画面といった「何でも画面」のニーズが多い。
しかし、そのような場合でも、内部設計はタスクベース(独立性の高い単位に小さく分けて整理しておく)とすることで、複雑な画面であっても変更がやりやすくなる。(逆にいうと複合画面の全てを大きなドメインオブジェクトとするのは見通しが悪く、変更がしづらくなる)
画面とドメインオブジェクトを連動させる
画面とドメインオブジェクトも利用者の関心事のかたまり
三層(プレゼンテーション層+アプリケーション層+データソース層)+ドメインモデル方式で設計する場合、利用者の関心事(画面で表示する項目とその画面で利用可能な操作)はドメインオブジェクトと対応する。
| 画面 | ドメインオブジェクト |
|---|---|
| 商品の登録 | Product クラス |
| 商品詳細の表示 | Product クラス |
| 商品の一覧 | Products(コレクションオブジェクト。 Rails の場合は Product クラスをそのまま使う) |
| 商品の検索条件 | Criteria クラス |
つまり、画面はドメインオブジェクトを視覚的に表現したものと言える。そして、その表示方法にはいくつかの選択肢がある。
- ドメインオブジェクトをそのまま画面の表示にも使う
- 画面用のオブジェクトを別途用意する
- 画面用のデータクラスを別途用意する
画面は利用者の関心事の塊であり、ドメインオブジェクトは利用者の関心事のソフトウェア表現である。
つまり、両者は同じ関心事の異なる表現であるので、ひとつ目の選択肢の「ドメインオブジェクトをそのまま画面の表示にも使う」のが最も自然と思えるが、実際には以下のような問題が発生する。
- 画面は様々な関心事が複合していて、ドメインオブジェクトの粒度や構造と整合しにくい時がある
- 画面の表示だけに関係する判断や加工のロジックをドメインオブジェクトに持ち込みたくない
この問題に対応するためには、それ以外の二つの選択肢の「画面用のオブジェクトを別途用意する」か「画面用のデータクラスを別途用意する」のいずれかの方法がある。
しかし、後者の「画面用のデータクラスを別途用意する」方法は、データクラスを使うという方式自体がどこにでもロジックが書けてしまうというデメリットがあり、オブジェクト指向設計の良さを損なうので、この方法は避けたい。
選択肢としては、「ドメインオブジェクトをそのまま使う」か「画面用のオブジェクトを別途用意する」かの二つとなる。
ドメインオブジェクトとその画面の食い違いは設計改善の手がかり
では、「ドメインオブジェクトをそのまま使う」か「画面用のオブジェクトを別途用意する」とではどちらを選ぶべきか。
結論としては、「ドメインオブジェクトをそのまま使う」方を優先するべき。
利用者の関心事は、画面とドメインオブジェクトのどちらで表現しても基本は同じはずである。
もし、画面の関心事と、ドメインオブジェクトで表現する関心事が一致していない場合、なぜ一致していないかを分析し、整理の仕方に何か改善すべき点がないか検討すべき。
ただし、画面がタスクベースではなく、複合型の「何でも画面」の場合は、一つのドメインオブジェクトとして表現すると大きすぎて変更がしづらくなるので、「画面用のオブジェクトを別途用意する」方を選ぶべきである。
具体的には、ビュー専用のオブジェクトをプレゼンテーション層に用意し、そのオブジェクトの中で複数のドメインオブジェクトを組み合わせる。
ドメインオブジェクトに書くべきロジック
ビューとモデルの分離は、設計原則の一つ。
ビューに書くべきことと、ドメインオブジェクトに書くべきことを整理する考え方は次の三つがある。
- 論理的な構造情報はドメインオブジェクトで表現する
- 場合ごとの表示の違いをドメインオブジェクトで出し分ける
- HTML の class 属性をドメインオブジェクトから出力する
論理的な情報構造をドメインオブジェクトで表現する
ビューの記述は基本的に以下の二つに分けられる。
-
物理的なビュー記述
- 画面を表示する技術方式に依存したビュー表現
- 文章を HTML で表示する場合の、各段落を字下げするために使用する
<p>タグとその CSS クラス - メール本文をテキストで表示する場合の、段落の区切りに用いる改行コードや、字下げに用いる全角の空白文字
- ドメインオブジェクトに記述すべきではない
-
論理的なビュー記述
- 例えば、「複数の段落」といった視覚上のスタイルとは関係ない「構造」だけを表現する
# ruby on railsで dry-struct を使用して型を定義する例 require 'dry-struct' module Types include Dry.Types() end class Article < Dry::Struct attribute :description, Types::Array.of(Types::String) # stringの配列という論理構造を表現 end Article.new(description: ["概要1", "概要2"]) # ✅OK Article.new(description: [1, 2]) # ❌ Dry::Struct::Error- 「段落がいくつあるか」「最も長い段落の文字数カウント」といったロジック
- 元データの要約を作成するなどのデータを加工するロジック
- これらはドメインオブジェクトで表現するべき
用語とその用語の定義の対を表現する
ドメインオブジェクトでビューの論理構造を表現する例として、用語とその説明を一対とした「定義リスト」がある。
HTML では定義リストを<dl>タグで表現できるので、その内部の論理構造を以下のようにドメインオブジェクト内で表現することができる。
# 用語とその用語の説明の対をドメインオブジェクトで表現する
# キーが<dt>タグ、値が<dd>タグに対応する
definition_list = {
"HTTP" => ["HyperText Transfer Protocol", "Web通信に用いられるプロトコル"],
"HTML" => ["HyperText Markup Language", "Webページを構成する言語"]
}
# view(slim)での表示例
dl
- definition_list.each do |term, defs|
dt = term
- defs.each do |d|
dd = d
<dl>
<dt>HTTP</dt>
<dd>HyperText Transfer Protocol</dd>
<dd>Web通信に用いられるプロトコル</dd>
<dt>HTML</dt>
<dd>HyperText Markup Language</dd>
<dd>Webページを構成する言語</dd>
</dl>
場合ごとの表示の違いをドメインオブジェクトで出し分ける
画面表示で if 文を使っている場合は、その条件判断をドメインオブジェクトに移動できないかを検討すべき。
例えば、取得した商品の件数によってメッセージを出し分ける処理は、以下のようにドメインオブジェクトに実装することで、ビュー側に if 文の条件判断が不要となる。
# ビュー用のプレゼンターオブジェクトを定義する例
# ヘルパーモジュールに記述するパターンもある
class ItemsPresenter
def initialize(items)
@items = items
end
# 件数によってメッセージを出し分けるロジック
def found_message
return "見つかりませんでした" if @items.empty?
"#{@items.count}件見つかりました"
end
end
# view
<%= ItemsPresenter.new(@items).found_message %>
このように、情報の文字列表現そのものは、積極的にドメインオブジェクトに持たせた方が関心事を集約して閉じ込めやすくなる。
しかし、このようなドメインオブジェクトが返す文字列表現に、物理的な表示手段である改行コードや HTML タグを含めてしまうのは NG 。
HTML の class 属性をドメインオブジェクトから出力する
条件によって視覚表現を変える例としては、以下のようにドメインオブジェクトに is_unread? メソッドを用意し、その結果によって class 属性を出し分ける方法がある。
このように、ドメインオブジェクトが状態を表す情報を返し、それを class 属性で利用することで画面の表示ロジックから if 文を排除できる。
def read_status
return "unread" if is_unread?
"read"
end
# view
<p class="<%= read_status %>">
データの文字列表現という利用者の関心事に関わる加工や判断ロジックは、できるだけドメインオブジェクトに集約すると変更が楽で安全になる。
画面を表示するロジックに if 文が入り込み始めたら、要注意。
画面(視覚表現)とソフトウェア(論理構造)を関係づける
利用者の関心事を可視化した画面のデザインとその画面に対応するドメインオブジェクトの設計の不一致は、ソフトウェアの変更を難しく危険にさせる。
そして、両者の構造が一致していない場合は設計改善の良い手がかりとなる。
例えば、以下のような不一致のパターンが考えられるので、それぞれのパターンに対して適切な設計を考える。
- 画面での項目の並び順と、対応するドメインオブジェクトのフィールドの並び順が一致していない。
- 画面上の項目のグルーピングと、ドメインオブジェクトの単位が一致していない。
項目の並び順とドメインオブジェクトのフィールドの並び順
例えば以下の順番で項目が並ぶ書籍の一覧画面を考えてみる。
- 書名
- 価格
- 発行年月日
- 著者
- 本の種類
これに対して、以下のようなクラスは、利用者の関心事である画面の構造よりも、データベースのテーブル構造に引きずられた内容になっており、項目の内容も、その並び順も一致していない。
class Book
def initialize(id, book_number, title, author, publisher, book_type, price, published_at, resistrer_at)
@id = id # データベースの主キー(画面の構造とは一致していない)
@book_number = book_number # ISBN
@title = title # 書名
@author = author # 著者
@publisher = publisher # 出版社
@book_type = book_type # 本の種類
@price = price # 価格
@published_at = published_at # 発行年月日
@registered_at = registered_at # 登録年月日
end
end
一覧画面の関心事をそのまま表現すれば、ドメインオブジェクトは次のようになる。
class BookSummary
# 画面での項目の種類から並び順まで一致
def initialize(title:, price:, published_at:, author:, book_type:)
@title = title # 書名
@price = price # 価格
@published_at = published_at # 発行年月日
@author = author # 著者
@book_type = book_type # 本の種類
end
end
このように、ドメインオブジェクトの設計では、項目の種類だけでなく並び順まで同じとなるレベルまで画面と一致した設計にすべき。
もちろん、上記のBookクラスのような設計でもプログラムは動くが、画面から発生した変更要求に対応する際に、画面と構造が一致したBookSummaryクラスの方が余計な情報も含まれず変更箇所が特定しやすくメンテナンスが容易になる。
画面項目のグルーピング
画面デザインの基本原則には以下の 4 つがある。
| 原則 | 説明 |
|---|---|
| 近接 | 関係のあるものは近づける、関係のない情報は離す |
| 整列 | 同じ意味のものは同じラインに揃える(左端、上端など)、意味が異なれば異なるラインに揃える(インデントなど) |
| 対比 | 意味の重みの違いを文字の大きさや色の違いで区別する |
| 反復 | 同じ意味は同じパターンで視覚化する |
画面もドメインオブジェクトも利用者の関心事の表現であるので、これらのデザイン原則と、ドメインオブジェクトの設計は基本的に一致する。
- 近接
- 近接したグループはドメインオブジェクトの単位と一致するはずなので、関係のある情報ごとにドメインオブジェクトを作成する。
- 画面のデザインで、空白を使って分離してある複数の情報が一つのドメインオブジェクトにまとまっている場合は、そのドメインオブジェクトの設計に問題がある。
- 整列
- 画面上でインデントされているということは、意味としても異なるということ。
- ドメインオブジェクトの構造も、インデントされた部分を別のオブジェクトととして括り出すことで利用者の関心ごとの構造を適切に表現できる。
- 対比
- 画面上、強調表示されるような重要なものはクラス内の上の方で宣言し、そうでないものは下の方に宣言する。
- 小さいフォントや薄めのグレーとか、画面上で弱く表現しているものは別クラスを作成してその中に隠蔽することを検討する。
- パッケージ構成においても、重要なクラス以外はサブパッケージに隠すことで、そのパッケージで何が重要であるかを強調して表現できる。
- 反復
- 反復して表現された情報は、同じ型のオブジェクトとして表現する。
- 一つのクラスの別々のオブジェクトの場合も、インターフェースで同一の型として扱う複数クラスのオブジェクトの場合もある。
画面以外の利用者向けの情報もソフトウェアと整合させる
以下のような画面以外の多くの関係者が確認できる内容も、ソフトウェアの設計と一致させることで、ソフトウェアの変更を楽で安全にする。
- プレスリリース
- ソフトウェアの特徴・セールスポイントを簡潔に表現。
- 上記の特徴・要点がドメインオブジェクトの設計に反映されていることが望ましい。
- リリースノート
- ソフトウェアの新しいバージョンにおける変更点が列挙される。
- 内容はドメインオブジェクトの修正や拡張と一致するはず。
- 利用者ガイド
- ソフトウェアの仕様書そのもの。
- 開発が完了したら更新されない仕様ドキュメントよりも、逐一更新される利用者ガイドの方がソフトウェアの仕様を正しく表現している可能性が高い。
特に重要な新しい機能を追加する場合、以下の四つが整合していることが重要。
- プレスリリースに記載したセールスポイント
- リリースノートでの新機能の概要説明
- 利用者ガイドへの新機能の説明の追加
- ドメインオブジェクトの追加
参考文献
この記事は以下の情報を参考にして執筆しました。