shimapapa.io

.NET,VB,C#,AzureなどMS関連中心の技術ブログ

【読了】「現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法」(のCHAPTER1)

前置き

今回はこちらの書籍のCHAPTER1についての感想文です。

現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法

現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法

以下、本書籍の「はじめに」からどんな書籍かのさわりを。

本書では 、私が業務アプリケ ーションの変更に苦しんだ経験をもとに 、オブジェクト指向設計のやり方と考え方 、効果があった解決策を 、具体的なソースコ ードを示しながら紹介していきます 。特に「なぜそうするのか 」を重視して説明します 。

ドメイン駆動設計の考え方が基本とされ、UI、データベースやAPIなどの設計も交えた、オブジェクト指向設計のプラクティスを学ぶことができます。

CHAPTER1要約

ドメインオブジェクト

第1章では、「なぜソフトウェアの変更は大変なのか」という問いかけから始まります。

では、その原因になってしまう要因とは…

そしてその原因とは設計に問題があるからです。

変更が大変なプログラムは以下のような特徴(問題点)があります。

変更が大変なプログラムの特徴は次の 3つです 。

・メソッドが長い

・クラスが大きい

・引数が多い

プログラムを読みやすくし、変更をしやすくするに以下のような方法があります。

・名前は (略語ではなく )普通の単語を使う

・数行のコ ードを意味のある単位として 「段落 」に分ける

・ 「目的別の変数 」を使う ( 1つの変数を使いまわさない )

・意味のあるコ ードのまとまり (段落 )を 「メソッド 」として独立させる

・業務の関心事に対応したクラス (ドメインオブジェクト )を作る

上記の上から4つの方法を順に行なっていくと、意味的に重複のあるメソッドは1つの関心ごとに対応したクラスに集約されていき、5つ目のドメインオブジェクトが作られていくのだと思います。

ドメインオブジェクト」とは

「送料 」クラスのように 、業務で使われる用語に合わせて 、その用語の関心事に対応するクラスをドメインオブジェクトと呼びます 。

アプリケ ーションの対象領域 (ドメイン )の関心事を記述したオブジェクトという意味です 。

ドメインオブジェクト」を作ることこそ、オブジェクト指向らしい設定のアプローチです。

このように業務の用語と 、直接対応するドメインオブジェクトを用意することが 、業務アプリケ ーションの変更を容易にするオブジェクト指向らしい設計のアプロ ーチです 。また 、業務を理解するために要求を分析し 、そこで発見した業務の関心事の単位を 、そのままプログラミング単位としてクラスで表現するのが 、オブジェクト指向開発のやり方です 。

値オブジェクト

クラスはデータとロジックの固まりです。

ロジックはデータを使った演算(加工/判断/計算)です。

業務アプリケーションにおける基本データ型は

  • 数値
  • 日付
  • 文字

です。

ところが「数量」や「金額」をプログラム言語における数値型=intやBig Decimalでそのまま定義してしまうと、小数点以下や非常に大きな数値など、業務的に不正な値も扱えることになってしまう落とし穴があります。

そこで正しい数量を扱うためのQuantityという独自クラスを定義します。

このような独自クラスを値オブジェクト(Value Object)と呼びます。

値の種類ごとに専用の型を用意するとコ ードが安定し 、コ ードの意図が明確になります 。このように 、値を扱うための専用クラスを作るやり方を値オブジェクト ( V a l u e O b j e c t )と呼びます 。

値オブジェクトは「不変」にする、というプラクティスがあります。

不変にすると以下のようなメリットがあります。

値ごとに別のオブジェクトを用意することで 、一つひとつのオブジェクトの用途が限定され 、プログラムが安定します 。プログラムの途中で内部の値が変化するときに起きがちな副作用を防ぐことができます 。

値オブジェクトを不変にするやり方を「完全コントラクタ」と呼びます。

このような設計のやり方を完全コンストラクタと呼びます 。オブジェクトの生成時に 、オブジェクトの状態を完全に設定してしまうやり方です 。

※本書籍では語られておりませんが、値オブジェクトは「全プロパティ値が同じ値なら、インスタンスが異なっても同じオブジェクト」という特徴も持っています。

以下、参考サイト。

www.infoq.com

値オブジェクトにより独自の型を定義することにより、引数の渡し間違えも防げるようになります。

例えば「金額」と「数量」をそれぞれint型の引数で受け取る関数は、それらを逆に渡されてもコンパイルエラーにはなりません。 つまり、プログラム的には正解でも業務ロジック的には間違い、という状態になりえてしまいます。 そこで引数の型をMoney型、Quantity型といった独自の型にすれば、逆に渡そうとしてしまうコードを書いても、コンパイルエラーで検知できるようになります。

コレクションオブジェクト/ファーストクラスコレクション

オブジェクトを複数持つ配列やコレクションの演算コードは複雑になりがちですが、値オブジェクトと同じく専用の小さなクラスにまとめることでプログラムがわかりやすくなります。

考え方は値オブジェクトと同じです 。デ ータと関連するロジックは 、 1つのクラスに集めます 。 i n t型の変数を 1つ持った 「数量 」の専用クラスを独自に作ったように 、コレクション L i s t < C u s t o m e r >型の変数を 1つだけ持った 「顧客一覧 」の専用クラスを独自に宣言します 。

このようなクラスをコレクションオブジェクトまたはファーストクラスコレクションと呼びます。

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

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

素数のチェックやル ープ処理は 、すべてコレクションオブジェクトがやってくれます 。使う側はコレクションオブジェクトのメソッドを呼ぶだけです 。

値オブジェクトと同様に、コレクションオブジェクトも出来るだけ「不変」に設計することでプログラムが安定します。

コレクションの操作を安定させる方法は 、 3つあります 。

・コレクション操作のロジックをコレクションオブジェクトに移動する

・コレクション操作の結果も同じ型のコレクションオブジェクトとして返す

・コレクションを 「不変 」にして外部に渡す

※.NETにおいてコレクションオブジェクトを作る場合、Collectionクラスを使うのが良さそうです。 以下.NETの公式リファレンスより。

docs.microsoft.com

まとめ

以上、本書籍のチャプター1では

  • ドメインオブジェクト
  • 値(バリュー)オブジェクト
  • コレクションオブジェクト/ファーストクラスコレクション

といったオブジェクト指向設計/DDDのプラクティスが学べます。

この3つだけでも知っている/知らないでクラス設計にかなり差が出てくるのはと感じました。

もっと経験年数が若いうちに、これらのプラクティスをキャッチできていたら…

CHAPTER2以降の感想はまたの機会に。

コラム

あの時、値オブジェクトを導入していれば…!と過去の案件の事例を思い出しました。

前職では介護サービス業向けのパッケージソフトを開発していました。

介護サービスは国で規定された種類が定義されており、コード化されております。

介護保険サービス種類コード一覧  

2ページ目右側を見ると、平成27年4月より背景黄色で新しいサービス種類コードが追加されています。
介護保険法の改正によりサービス種類が追加され、それまで数値のみだったコード体系にA1とアルファベットが混在するコードが追加されました。
開発していたシステムではサービス種類コードをint型で定義してしまっていたので、この法改正によりstring型で扱う変更を入れる羽目になってしまいました…。
サービス種類コードはシステムの至る所でint型で取り扱われていたので、修正作業にかなりの工数を取られてしまいました。
このような業務レベルの変更に対しても、サービス種類コードを「ServiceTypeCode」といった値オブジェクトで定義しておけば、被害は少なかったでしょう。
詳細の説明は省きますが、サービス種類コードと「サービス項目コード」を組み合わせた「サービスコード」という概念もあります。 これらも踏まえてコード化すると以下のような設計が考えられます。

public class ServiceTypeCode
    {
        public string value { get; private set; }

        public ServiceTypeCode(string value)
        {
            if (value.Length != 2)
            {
                throw new ArgumentException("サービス種類コードは2桁です");
            }

            this.value = value;
        }
    }

    public class ServiceItemCode
    {
        public string value { get; private set; }

        public ServiceItemCode(string value)
        {
            if (value.Length != 4)
            {
                throw new ArgumentException("サービス項目コードは4桁です");
            }

            this.value = value;
        }
    }

    public class ServiceCode
    {
        public ServiceTypeCode serviceTypeCode { get; private set; }
        public ServiceItemCode serviceItemCode { get; private set; }

        public ServiceCode(ServiceTypeCode serviceTypeCode, ServiceItemCode serviceItemCode)
        {
            this.serviceTypeCode = serviceTypeCode;
            this.serviceItemCode = serviceItemCode;
        }

        public string GetValue()
        {
            return this.serviceTypeCode.value + this.serviceItemCode.value;
        }

    }