10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

低凝集・密結合なモジュールを高凝集・疎結合なモジュールに作り替える

Posted at

はじめに

オブジェクト指向言語を使用する上で欠かせない考え方「凝集度・結合度」のお話です。
クラスを設計する際にどんなことを意識すべきことは

  • そのクラスに関連するデータとロジックが一体となり、そのクラス単体で動作が保証できるようなモジュールを作成すること

です。

そんな高凝集•疎結合なモジュールを作成するために必要なテクニックをまとめていきます。
※対象業務領域(ドメイン)への深い理解をしていることが前提となります。

凝集度とは

そのモジュールの関心事にどれだけ関連した情報が集まっているかの度合いを示すもの。

結合度とは

そのモジュールがどれだけ他のモジュールに依存しているかの度合いを示すもの。

以下の参考文献が詳しく書いてあるので、ここでは説明を割愛します。
結合度と凝集度!!

テクニック

  • SOLID原則の1つである単一責任の原則に基づいたクラス設計を行う
  • データとロジックはひとまとまりにする
    • 値オブジェクト:システムにおける固有な値を表現する
    • エンティティ:システムにおけるライフサイクルのあるものを表現する
    • ファーストクラスコレクション:リストに関する判断や加工のロジックを集約する
    • ファクトリパターン:オブジェクトの生成に関するロジックを集約する(ここでは名前の紹介のみとさせていただきます。)

これ以外にも様々な考え方がありますが、基本的にはどのような単位でクラスを設計し、情報をカプセル化していくべきなのかという視点があると理解しやすくなると個人的には理解しやすいのではないかと思ってます。
以下、サンプルコードにて、各ワードの確認をしていきます。

悪いサンプルコード

※サンプルコードに関しては、現場にあった良くないコードを説明するために作成したものです。実際にこのようなクラス設計ではなかったですが、基本的にデータとロジックが分離しているような印象です。

値のチェックがモデルから漏れ出してしまっている例

  • 以下のクラスを考えてみます。
    image.png

  • 制約条件

    • IDは必須、5文字である
    • 名前は必須、Pから始まる任意の文字列
    • ステータスは必須(0:販売中、1:売り切れ)
    • ステータスが1の商品情報を表示させる場合は先頭に「売り切れ」を付与する
BadProduct.cs
	public sealed class Product
	{
		public string Id { get; set; }
		public string Name { get; set; }
		public string Status { get; set; }
	}

データしか保持しないデータクラスと呼ばれるやつです。
DTO(Data Transfer Object)として定義されるモデルであればこのような形でもよいですが、普通のモデルとしてはいまいちです。
なぜかといえば、先ほど挙げた制約条件の情報が何もないからです。つまり、このモデルのデータとロジックが分離している悪い状態です。

この商品クラスをチェックするクラスを作成してみます。

ProductChecker.cs
	public sealed class ProductChecker
	{
		private Product CheckTargetProduct { get; }

        public ProductChecker(Product product)
		{
			CheckTargetProduct= product;
		}

        public bool ArePropertiesCorrect()
		{
			// NULLかチェック
			if (string.IsNullOrEmpty(CheckTargetProduct.ID)
				|| string.IsNullOrEmpty(CheckTargetProduct.Name)
				|| string.IsNullOrEmpty(CheckTargetProduct.Status))
			{
				return false;
			}

			if (!CheckTargetProduct.ID.StartsWith("P") 
				&& CheckTargetProduct.ID.Length == 5)
			{
				return false;
			}

			if (CheckTargetProduct.Status != "0" 
				&& CheckTargetProduct.Status != "1")
			{
				return false;
			}

			bool isMatched = Regex.IsMatch(CheckTargetProduct.Name, @"[^\x01 -\x7E]");

			if (!isMatched) 
			{
				return false;
			}
			
			return true;
		}
	}

  • 商品クラスのプロパティが保持していればいい情報をチェックするクラスが保持してしまっています。
    プリミティブ型での実装はデータとロジックを分離させてしまいますので、値に対しての情報は値に集めたいです。
    これが値オブジェクトという考えになります。
    ※単一項目に対して判断・加工することができないようなものは商品クラスに集めます。

  • ProductCheckerクラスがProductクラスに依存している。他のモジュールに依存しない形でクラスの動作を保証したいです。

  • [set;]がついていることによって商品クラスのプロパティを書き換えられてしまう可能性があるので、取り除いてイミュータブル(不変)なモデルに変化させたいです。

  • 「P」などがべた書きされてしまっているので、具体的な変数名で定義してあげたいです。
    これはマジックナンバーと言われます。ここでの文脈でいえばこの「P」は「IDの頭につける文字」なので、それを変数として定義し、説明してあげるべきです。

良いサンプルコード

ID.cs
	public sealed class ID
	{
		private readonly string PREFIX = "P";
		private readonly int ID_LENGTH = 5;

		public string Value { get; }
		
		public ID(string value)
		{
			if (string.IsNullOrEmpty(value))
			{
				throw new ArgumentNullException($"引数:{value}は無効です");
			}

			if (!value.StartsWith(PREFIX) && value.Length == ID_LENGTH)
			{
				throw new ArgumentNullException($"引数:{value}は無効です");
			}

			Value = value;
		}
	}
Status.cs
	public sealed class Status
	{
		public int Value { get; }

		public Status(int value)
		{
			if (value != (int)StatusFlg.nowSale && value != (int)StatusFlg.soldOut)
			{
				throw new ArgumentNullException($"引数:{value}は無効です");
			}

			Value = value;
		}
	}

	enum StatusFlg
	{
		nowSale = 0,
		soldOut = 1,
	}
Name.cs
	public sealed class Name
	{
		private readonly string NAME_REGEX = @"[^\x01 -\x7E]";

		public string Value { get; }


		public Name(string value)
		{
			bool isMatched = Regex.IsMatch(value, NAME_REGEX);

			if (!isMatched)
			{
				throw new ArgumentNullException($"引数:{value}は無効です");
			}

			Value = value;
		}
	}

Product.cs
	public sealed class Product
	{
		public ID ID { get; }
		public Name Name { get; }
		public Status Status { get;}

        public bool IsOnSale => Status == (int)StatusFlg.nowSale ? true : false
		
		public Product(ID id, Name name, Status status)
		{
			ID= id;
			Name= name;
			Status= status;
		}

		public override string ToString()
		{
			if (IsOnSale)
			{
				return $"ID:{ID.Value} 商品名:{Name.Value} ";
			}

			return $"売り切れ!ID:{ID.Value} 商品名:{Name.Value} ";
		}
	}
  • 値に関する情報は値オブジェクトに詰め込みました。
  • 不正値はコンストラクタではじき、不正値がクラスに入ってこないようにしました
    • これは完全コンストラクタパターンと呼ばれる考え方になります。
  • 無意味な情報に変数名を付けました。
  • ProductCheckerクラスがなくなり、値オブジェクト自身が他のモジュールに依存しなくなった。

ファーストクラスコレクションに関して

例えば、販売中の商品だけの商品リストが欲しいとします。
その場合はリストに関するロジックのため、ファーストクラスコレクションに集約します。

ProductList.cs
	public sealed class ProductList
	{
		private List<Product> Products { get; }

		public ProductList(List<Product> products)
		{
			Products = products;
		}

		/// <summary>
		/// 販売中の商品を返却します。
		/// </summary>
		public List<Product> GetProductsOnSale()
		{
			return Products.Where(product => product.IsOnSale).ToList();
		}
	}

参考文献
DDD基礎解説:Entity、ValueObjectってなんなんだ
良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
現場で役立つシステム設計の原則
〜変更を楽で安全にするオブジェクト指向の実践技法

10
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?