Uenishi.Web

大阪に生息しているプログラマーのブログ

「現場で役立つシステム設計の原則」を読んでのメモ

概要

タイトルの通り、書籍「(https://www.amazon.co.jp/dp/B073GSDBGT/)」の1章から5章まで読んでの要点まとめです。 業務で度々発生する仕様変更に対して、いかにして「楽に対応できるようにするか」が設計に興味を持ったきっかけでした。 個人的なメモとしての要素が強いので、詳細は是非書籍をお読みください。

1. 小さくまとめてわかりやすくする
プログラムの変更が楽になる書き方

  • わかりやすい名前を使う(名前重要
  • メソッドは「段落」に分けて読みやすくする(意味のかたまりで分ける)
  • 意味のあるコードのまとまりをメソッドとして抽出し、独立させる
  • 目的ごとに変数を用意する(1つの変数をいろんな用途に使いまわさない)
  • 特定の関心事に特化したクラス(ドメインオブジェクト)を作る

完全コンストラクタ

値オブジェクトは「不変」にする。 値オブジェクトを不変にするやり方

  • インスタンス変数はコンストラクタでオブジェクトの作成時に設定する
  • インスタンス変数を変更するメソッド(setterメソッド)を作らない
  • 別の値が必要になったら、別のオブジェクトを用意する

オブジェクトの値が変わることを前提にすると、そのオブジェクトが「現在どのような値」を持っているのかいつも心配することになる。

Javaでは、String / BigDecimal / LocalDateなどの基本的なデータ型は全てこの「完全コンストラクタ」スタイルの値オブジェクト。

  • JavaのStringは「不変」で、変更されないオブジェクト

値(Value)オブジェクト
int quantity; // 数量
BigDecimal amount; // 

上記のようなコードは、

  • 数量(quantity)をマイナス21億からプラス21億まで
  • 金額(amount)を実質無限大かつ小数点21億桁まで

扱えるように宣言しており、現実(業務の関心事)とはかけ離れた異常な値を扱うことができてしまう。

そのため、業務の関心事に合わせた、正しい数量を扱うための独自クラス(Quantityクラス)を定義しそちらを使うようにする。

class Quantity {
    static final int MIN = 1;
    static final int MAX = 100;

    int value

    Quantity(int value) {
        if(value < MIN) throw new IllegalArgumentException("不正:" + MIN + "未満");

        if(value > MAX) throw new IllegalArgumentException("不正:" + MAX + "超");

        this.value = value;
    }

    boolean canAdd(Quantity other) {
        // Addできるかチェックする処理
    }

    Quantity add(Quantity other) {
        // Addする処理
    }
}

 

コレクションオブジェクト

「ファーストクラスコレクション」とも呼ばれる。

考え方は値オブジェクトと同じ。

コレクション型の変数を1つだけ持った専用のクラスを宣言して、その中に処理を閉じ込める。

2. 場合分けのロジックを整理する

ガード節

else句を使わずに早期リターンをする書き方。 マーティン・ファウラーの『リファクタリング』で「条件分岐の単純化」として紹介されている設計の改善方法。

インターフェースを使う

区分ごとのロジックを別クラスに分ける。

インターフェースを用いてポリモーフィズムを実現する。

クラスとクラスの関係は、お互いに知っていることが多いほど密結合になる。 その逆で「知らないこと」が多いほど結びつきが弱くなる。結合が弱いほど、独立性が高くなり、変更による影響を受けづらくなる。

3. 業務ロジックをわかりやすく整理する
メソッドは必ずインスタンス変数を使う Tips

インスタンス変数 = (メンバ変数、フィールド、データメンバ)

一方、インスタンスごとではなくクラス自身に付随する変数は「クラス変数」「静的メンバ変数」「静的フィールド」という。

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

public int amount(int unitPrice, int quantity){
		int total = unitPrice + quantity;
		return total;
}

このようなメソッドは「引数だけで計算」しておりインスタンス変数を使用していないので、このクラスに置く必要がない。 データの近くにロジックを置くクラス設計を行うと、どこに何が書いてあるのか推測しやすくなる。  

クラスが肥大化したら小さく分ける
  • インスタンス変数とメソッドを対応付けて、1つのグループとしてまとめていく。
  • メソッドがすべてのインスタンス変数を扱うようにする

関連性の強いデータとロジックだけを集めたクラスを「凝集性が高い」と表現する。

業務ロジックを小さなオブジェクトに分けて記述する
  • 関連する業務データと業務ロジックを1つにまとめたオブジェクトを「ドメインオブジェクト」という
  • 「ドメイン」とは、対象領域や問題領域という意味。

3層 + ドメインモデル

すべての業務ロジックはドメインモデルに集める。

  • プレゼンテーション層
    • UIなど、外部との入出力を担当。
  • アプリケーション層
    • 業務機能のマクロな手順を記述。
  • データソース層
    • データベースとの入出力を担当。
  • ドメインモデル
    • 業務データと関連する業務ロジックを表現したドメインオブジェクトの集合。

4. ドメインモデルの考え方で設計する

業務に使っている用語をクラス名にする

ドメインモデルの設計は、業務で使われる具体的な用語(概念)を手がかりに進めます。そして、その用語が、データとロジックをひとかたまりとしたプログラミング単位として使えそうなことを検証する。

ドメインモデルの設計でありがちな失敗
  • 業務では実際に使っていない抽象的な言葉をクラス名として使ってしまう

抽象的で意味の広い名前をクラス名やパッケージ名にすると、さまざまな要素をシンプルにスッキリと整理できたように錯覚しがち。

しかし、意味の広い抽象的な名前を使ったクラスは、具体的には何も説明していない。

例えば、「取引」というクラスに、

  • 販売
  • 仕入

といった業務ルールを入れてしまうと、プログラムが複雑になってしまうだけ。

ドメインモデルの関心は「業務ロジック」であり、「データ」ではない。

手続型のアプローチ

全体を俯瞰し定義するところからスタート。

トップダウンのアプローチ。

オブジェクト指向のアプローチ

部分から全体を組み立てて、作り上げていくイメージで、ときおり全体を俯瞰しながら、ボトムアップ型のアプローチで設計を進めていく。

全体を俯瞰する道具としては、以下の2つがある。

  • パッケージ図
  • 業務フロー図

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

プログラムを開発するときに機能を中心に考え、機能を分解しながらプログラム部品を作っていくと、一つ一つの部品は機能の分解構造に依存する。

ドメインモデルを構成する個々のドメインオブジェクトの設計では、こういう機能の分解構造や時間的な依存関係を持ち込まないようにする。

特定の機能や処理の順番からは独立させて、単体で動作確認ができる独立性の高い部品として開発する。

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

ドメインオブジェクトの見つけ方

業務の重要な関心事とそれほど重要でない関心事を区別して、重要な関心事から手を付けていく。

  • 業務知識は理解しやすい内容もあるが、本当に役立つ情報は表面には現れていないことが多い。
  • 業務の関心事を「ヒト/モノ/コト」の3つに分割する。
    • コト(事象)の基本属性
      • 対象:何についての発生した事象か。
      • 種別:どういう種類の事象か。
      • 時点:いつ起きた事象か。
    • コトに注目することで次の関係も明らかになる。
      • コトはヒトとモノとの関係として出現する(誰の何についての行動か)。
      • コトは時間軸に沿って明確な前後関係を持つ。

ドメインモデルの設計のアプローチは、まず部品を特定し、その部品ごとに独立したクラスを設計する。ドメインモデルで開発していても、過程の作業で手っ取り早く「ちょっとしたif文」を追加すると、次第に増殖していきトランザクションスクリプトになってしまう。

トランザクションスクリプトとは以下のようなものである。

トランザクションスクリプトは、いわゆる手続き型プログラミングを使って実装する方式です。データとgetter、setterだけを持つようなDTOといった入れ物と「サービス」クラスを作成し、サービスに処理を書くのが定番です。

MVC、3層アーキテクチャから設計を学び始めるための基礎知識

トランザクションスクリプト(手続き型プログラミング)は、業務ルールが増えれば増えるほどif文が増える。

ドメインオブジェクトの基本の設計パターン

値オブジェクト:数値、日付、文字列をラッピングしてロジックを整理する。

コレクションオブジェクト:配列やコレクションをラッピングしてロジックを整理する。

区分オブジェクト:区分の定義と区分ごとのロジックを整理する。

列挙型の集合操作:状態遷移ルールなどを列挙型の集合として整理する。

業務の関心ごとのパターン

口座(Amount)パターン:現在の値(現在高)を表現し、妥当性を管理する。

期日(DueDate)パターン:約束の期日と判断を表現する。

方針(Policy)パターン:さまざまなルールが複合する、複雑な業務ロジックを表現する。

状態(State)パターン:状態と、状態遷移のできる/できないを表現する。

ドメインオブジェクトの設計を段階的に改善する

ドメインオブジェクトは一度作って動けば完成ではありません。

実際に使ってみて、使い勝手を確認しながら改善を続けます。

改善するポイントとしては3つあります。

  • クラス名やメソッド名の変更
  • ロジックの移動
  • 取りまとめ役のクラスの導入

ドメインモデルの開発とは、小さな独立性の高いドメインオブジェクトを揃えていく活動です。

役に立つドメインオブジェクトは、クラス名やメソッド名がそのまま業務の言葉と一致します。

業務を学びながら、業務知識を増やし、より深く理解していきます。

学んだことをコードで表現し、ドメインオブジェクトの設計に反映させていくことがドメインモデルの設計です。

開発者が業務を深く理解するにつれ、ドメインモデルの構造が洗練されていきます。

「聞きなれない言葉」や使い方に「違和感」のある言葉こそ、業務を理解する重要な手がかりとなります。

ドメインモデルの設計に模範解答や最終解答はありません。より良い解答として、ドメインモデルの設計とはより良い解答を探し続けることです。

5. アプリケーション機能を組み立てる

三層+ドメインモデルの設計では、アプリケーション層は処理の流れの進行役であり、調整役。

Spring Frameworkは、開発者が業務ロジックに集中できるように、ドメインモデル以外の三層の実装基盤を提供することを重視して開発されたフレームワーク。

https://spring.io/projects/spring-framework/

サービスクラスの設計はごちゃごちゃしやすい

アプリケーションの機能が増え、仕様が複雑になるにつれ、サービスクラスに業務ロジックが入り込んでくる。

サービスクラスの設計では、次の方針を徹底する。

  • 業務ロジックは、サービスクラスに書かずにドメインオブジェクトに任せる(サービスクラスで判断・加工・計算しない)
  • 画面の複雑さをそのままサービスクラスに持ち込まない
  • データベースの入出力の都合からサービスクラスを独立させる

サービスクラスに業務ロジックを書き始めると、手続き型のプログラミングで起こりがちなコードの重複が始まる。

業務ロジックの置き場所として、より適切な場所を探す。

適切なドメインオブジェクトがなければ、ドメインオブジェクトの追加を考える。

業務ロジックを追加する方法は2つ

  • ドメインオブジェクトを追加したり修正してドメインモデルを充実させる
  • 不足している業務ロジックをサービスクラスに直接書いてしまう

動かすだけであれば、後者の方が簡単だが、三層+ドメインモデルの良さを活かすにはサービスクラスに安易にロジックを追加してはいけない。

サービスクラスに業務ロジックを安易に追加すると、ドメインモデルの成長が止まってしまう。

ドメインモデルの成長が止まると、三層+ドメインモデルで実現できる変更の容易性が劣化する。

サービスクラスに業務ロジックを書きたくなったら、それはドメインモデルの改良の機会として積極的に活用する。

サービスを利用する側と、サービスを提供する側とで、サービス提供の約束事を決め、設計をシンプルに保つ技法を「契約による設計」と呼ぶ。

契約による設計と対照的な技法が「防御的プログラミング」

防御的プログラミングはさまざまな検証のコードを書くため、無意味にコードを複雑にし読みにくくしてしまう。

データベースの都合から分離する

データベース操作ではなく業務の関心事で考える

業務の関心事としての記録と参照を記述する仕組みを「リポジトリ」として用意する。

具体的には以下のようなインターフェイス宣言をドメインモデルに追加する。

interface BankAccountRepository {
	boolean canWithdraw(Amountamount);
	Amountbalance();
	voidwithDraw(Amountamount);
}

まとめ

めちゃくちゃ長くなってしまった……。

6章以降も時間があれば書いていこうと思います!!