モジュールはプログラム言語におけるパッケージや名前空間にあたり、クラス群をまとめて管理する入れ物になる。この機能により、クラス群の責任と役割を明確にして、保守性や可読性を上げることができる。
モジュールを選択する際には、システムに関する物語を伝え、概念の凝集した集合を含んでいるものを選ぶこと。こうすることで、モジュール間は低結合になることが多い。(略)モジュールとその名前はドメインに対する洞察を反映していなければならない。
つまり、DDDにおいてモジュールとは、ただの入れ物ではなく、別のモジュールのクラス群との結合を少なくするという目的を持つ。そのため、モジュールに適切な名前をつけることは重要である。そのため、高凝縮で低結合なモジュール化されたモデルを、ユビキタス言語に従って構築することがポイントになる。
モデリングをする際には凝集度と結合度の2つを意識する
- 凝集度
- モジュールのまとまり具合に関する指標
- 特定の機能に対する責任を持って、正しくまとまって協調しているかを示す指標で、高いほど好ましい
- 結合度
- モジュールを変更しやすいように適切に分割できているかどうかを示す指標
- 密結合(結合度が高)なシステムでは修正が困難になるため、疎結合(結合度が低)なシステムが望ましい
必要な機能がまとまっていれば、外部のモジュールと連携する必要がないため、凝集度が高くなれば、結合度は低くなる
モジュール設計における重要なポイントとして、まず「モジュールの存在を軽視しない」こと。エンティティ/値オブジェクト/サービス/イベントと同じ扱いで、モジュールについても十分に議論する必要がある。
モデリングする際には、次のポイントに気をつける
- 正確な意図が伝わるようにモジュール名を検討する
- 必要があれば恐れずにモジュール名の変更を行う
- ドメインエキスパートとディスカッションしている最新情報がモジュールに反映されるようにする
- モジュールに含めるクラスの項目を整理する
- モジュール名に適したクラスを詰め込む
- モジュール凝集性を高める
モジュールを設計する指針として次の7つのポイントがある
- モジュールをDDDにおいて非常に重要な概念と認識し、モデリングの概念にフィットさせ設計する
- 例えば、集約に対して1つのモジュールを用意する
- モデリングの概念に従い、モジュール名をユビキタス言語に従う
- モジュール名を機械的に決めない
- 例えば、クラスの型や役割だけでまとめるようなモジュールを作らない
- DDDの本質を失い、豊かなモデリングから離れてしまう
- 目の前の問題の解決手段としてコンポーネントやパターンを実現してはいけない
- 疎結合に設計する
- 極力、他のモジュールに依存しないようにする
- クラスの疎結合と同様のメリットが得られる
- モジュール同士の結合が必要な場合に、循環依存が起きないようにする
- 意味的に双方向な依存関係があっても実装上は単一方向の依存関係が望ましい
- モジュール間の循環依存は避けるべきだが、親子関係のモジュールに限り、許可する
- 上位と下位関係がある循環依存はやむをえない
- 取りまとめているオブジェクト群に合わせた名前をつけ、そぐわない場合はリファクタリングする
- 表9-1:モジュールを設計する際のルールの最後に、モデルの概念に柔軟性があって、さまざまな状態や振る舞いや名前にたいおうできるものなら、それらをまとめるモジュールもまた、同じ特徴を持たせるべきだ。 という内容がよくわからない。
- ずばり、『モジュール』とは何を指すのだろうか?
- 定義がすっげー分かれる話
-
モジュール名も変えようっていう話。静的な名前にしないこと。
- モデルがだんだん変わっていくからこそ、モジュール名も変えていく
-
柔軟性 = 抽象度・汎用度など、と読みました
-
時として、再利用性にも繋がる。
-
IDDDにおけるモジュールの設計ルールの「3. モジュール名を機械的に決めない」の部分かと
-
抽象的・汎用的なモデル要素を集めたモジュールであれば、そのモジュールの内容や名前もその抽象性・汎用性 (=特徴) に応じたものにすべきだ、くらいの意味合いではないかと。
-
モジュールが取りまとめるオブジェクト郡に合わせてかえる
モジュール名の決め方は、JavaやC#の一般的な命名方針に従い、ピリオドにて区切られた階層構造にて記述する。
// Javaの場合
com.saasovation
// C#の場合
SaaSOvation
命名規約を使うことで、JARやDLLにおいて他プロジェクトとの衝突を避けることができる。
トップレベルの組織名を決めた後は、その下の名前を検討する。ここでのポイントは、idovation・collabovation・projectovationといったプロダクト名を入れていないことである。その代わり「境界づけられたコンテキスト」に沿った名前を追加する
SaaSOvation.IdentityAccess
SaaSOvation.Collaboration
SaaSOvation.AgilePM
コンテキスト名が必ず正しいわけではないが、SaaSOvationでは、この名称がユビキタス言語を示す上でわかりやすいといった理由で命名する。この場合、プロダクト名の変更の影響を受けないメリットがある。
コンテキスト名の下位にあたる階層名について考察する
SaaSOvation.IdentityAccess.Domain
SaaSOvation.Collaboration.Domain
SaaSOvation.AgilePM.Domain
ここでは「Domain」というモジュール名を追加している。この配下にはドメインに関する情報を格納する
この命名規則はレイヤードアーキテクチャやヘキサゴナルアーキテクチャと互換性がある。ヘキサゴナルアーキテクチャでは、アプリケーションの内部があり、そこにドメインが含まれる。
ドメインの部分はインターフェイス・クラスの情報を含まず、単にその下位のモジュールのコンテナとして働く。
次にDomain配下に「Model」を追加している。このModel配下のサブモジュールには、値オブジェクト、エンティティ、イベントといったクラスやインタフェースが定義される。
SaaSOvation.IdentityAccess.Domain.Model
SaaSOvation.Collaboration.Domain.Model
SaaSOvation.AgilePM.Domain.Model
ここから下位の階層は、モデルのクラスを定義する。パッケージのこのレベルで、再利用可能なインターフェイスや抽象クラス群を配置する。ここで、抽象クラスクラス群の名前をつける際に ドメインモデル貧血症 に陥る可能性がある命名を避ける。
// ドメインモデル貧血症になる可能性がある名前
SaaSOvation.AgilePM.Domain.Service
// 担当する機能のコンセプトを示すモジュール名を付与する
SaaSOvation.AgilePM.Domain.<コンセプト名>
ドメインモデル内でサービスという用語を使わず、ドメインのコンセプトを示す名前(Products、Tenants、Teams等)をつける。このモジュール名はチームで検討し、チームの会話で利用する。
ドメインとは、今取り組んでいる業務のノウハウの一面を捉えたものである。ここでは、ドメインモデルの設計・実装をしているため、モデルを設計していることを明確にする命名が望ましい
-
P.325(9.4章のちょい前) で突如出てきた
com.saasovation.identityaccess.domain.conceptname
ってなんだろう? -
P.323の以下のコンテキストと同じようにパッケージを切るやり方は、ノイズが生まれやすいとある。一見すると、これは良さそうに思えるが、何か問題が起きる?
-
domain.conceptname
- たぶん、プレースホルダー的なアレ。そこまでさして重要じゃないと思う。
-
コンテキストの名前に「あわせておくといい」といったが、「一字一句同じにしても良い」とは言っていない的な?
- 安易に略称するのはよく無さげ
- いいじゃん長くても
- トップレベルのあとに続く名前は、境界づけられたコンテキストを表す部分でる
コアドメインであるアジャイルプロジェクト管理コンテキストの設計を考察する。
まずはトップレベルのサブモジュールであるテナント(Tenants)・チーム(Teams)・プロダクト(Products)は次のように命名する
SaaSOvation.AgilePM.Domain.Model.Tenants
SaaSOvation.AgilePM.Domain.Model.Teams
SaaSOvation.AgilePM.Domain.Model.Products
サンプルプロジェクトでは上記の3つのサブモジュールが同じフォルダに格納されている。
テナント(Tenants)モジュールに存在するクラスは、TeanatId(値オブジェクト)だけである。これは一意な識別子で、個々のテナントを区別するキーになる(このTeanatIdは「アジャイル管理プロジェクト」だけの識別子ではなく、本来は「認証・アクセスコンテキスト」側で使われる識別子である)。
namespace SaaSOvation.Common.Domain.Model
{
using System;
public abstract class Identity : IEquatable<Identity>, IIdentity
{
public Identity()
{
this.Id = Guid.NewGuid().ToString();
}
public Identity(string id)
{
this.Id = id;
}
// currently for Entity Framework, set must be protected, not private.
// will be fixed in EF 6.
public string Id { get; protected set; }
public bool Equals(Identity id)
{
if (object.ReferenceEquals(this, id)) return true;
if (object.ReferenceEquals(null, id)) return false;
return this.Id.Equals(id.Id);
}
public override bool Equals(object anotherObject)
{
return Equals(anotherObject as Identity);
}
public override int GetHashCode()
{
return (this.GetType().GetHashCode() * 907) + this.Id.GetHashCode();
}
public override string ToString()
{
return this.GetType().Name + " [Id=" + Id + "]";
}
}
}
namespace SaaSOvation.AgilePM.Domain.Model.Tenants
{
using SaaSOvation.Common.Domain.Model;
public class TenantId : Identity
{
public TenantId() : base()
{
}
public TenantId(string id) : base(id)
{
}
}
}
このテナントモジュール(TeanatId)は、アジャイル管理コンテキストの他のほとんどのモジュールから参照される。しかし、逆方向の参照はしていないので、シンプルな依存関係である。
このモジュールには、チームを管理するために使う3つの集約(ProductOwner、Team、TeamMember)が含まれている。
namespace SaaSOvation.AgilePM.Domain.Model.Teams
{
using System;
using SaaSOvation.AgilePM.Domain.Model.Tenants;
using SaaSOvation.Common.Domain.Model;
public class ProductOwner : Member
{
public ProductOwner(
TenantId tenantId,
string username,
string firstName,
string lastName,
string emailAddress,
DateTime initializedOn)
: base(tenantId, username, firstName, lastName, emailAddress, initializedOn)
{
}
public ProductOwnerId ProductOwnerId { get; private set; }
public override string ToString()
{
return "ProductOwner [productOwnerId()=" + ProductOwnerId + ", emailAddress()=" + EmailAddress + ", isEnabled()="
+ Enabled + ", firstName()=" + FirstName + ", lastName()=" + LastName + ", tenantId()=" + TenantId
+ ", username()=" + Username + "]";
}
protected override System.Collections.Generic.IEnumerable<object> GetIdentityComponents()
{
yield return this.TenantId;
yield return this.Username;
}
}
public class Team : Entity
{
public Team(TenantId tenantId, string name, ProductOwner productOwner = null)
{
AssertionConcern.AssertArgumentNotNull(tenantId, "The tenantId must be provided.");
this.tenantId = tenantId;
this.Name = name;
if (productOwner != null)
this.ProductOwner = productOwner;
this.teamMembers = new HashSet<TeamMember>();
}
readonly TenantId tenantId;
string name;
ProductOwner productOwner;
readonly HashSet<TeamMember> teamMembers;
public TenantId TenantId
{
get { return this.tenantId; }
}
public string Name
{
get { return this.name; }
private set
{
AssertionConcern.AssertArgumentNotEmpty(value, "The name must be provided.");
AssertionConcern.AssertArgumentLength(value, 100, "The name must be 100 characters or less.");
this.name = value;
}
}
public ProductOwner ProductOwner
{
get { return this.productOwner; }
private set
{
AssertionConcern.AssertArgumentNotNull(value, "The productOwner must be provided.");
AssertionConcern.AssertArgumentEquals(this.tenantId, value.TenantId, "The productOwner must be of the same tenant.");
this.productOwner = value;
}
}
public ReadOnlyCollection<TeamMember> AllTeamMembers
{
get { return new ReadOnlyCollection<TeamMember>(this.teamMembers.ToArray()); }
}
public void AssignProductOwner(ProductOwner productOwner)
{
this.ProductOwner = productOwner;
}
public void AssignTeamMember(TeamMember teamMember)
{
AssertValidTeamMember(teamMember);
this.teamMembers.Add(teamMember);
}
public bool IsTeamMember(TeamMember teamMember)
{
AssertValidTeamMember(teamMember);
return GetTeamMemberByUserName(teamMember.Username) != null;
}
public void RemoveTeamMember(TeamMember teamMember)
{
AssertValidTeamMember(teamMember);
var existingTeamMember = GetTeamMemberByUserName(teamMember.Username);
if (existingTeamMember != null)
{
this.teamMembers.Remove(existingTeamMember);
}
}
void AssertValidTeamMember(TeamMember teamMember)
{
AssertionConcern.AssertArgumentNotNull(teamMember, "A team member must be provided.");
AssertionConcern.AssertArgumentEquals(this.TenantId, teamMember.TenantId, "Team member must be of the same tenant.");
}
TeamMember GetTeamMemberByUserName(string userName)
{
return this.teamMembers.FirstOrDefault(x => x.Username.Equals(userName));
}
}
public class TeamMember : Member
{
public TeamMember(TenantId tenantId, string userName, string firstName, string lastName, string emailAddress, DateTime initializedOn)
: base(tenantId, userName, firstName, lastName, emailAddress, initializedOn)
{
}
public TeamMemberId TeamMemberId
{
get
{
// TODO: consider length restrictions on TeamMemberId.Id
return new TeamMemberId(this.TenantId, this.Username);
}
}
protected override IEnumerable<object> GetIdentityComponents()
{
yield return this.TenantId;
yield return this.Username;
}
}
}
1つのモジュールの中に、チーム管理に関連する「リポジトリ」「集約」「値オブジェクト」といった複数のドメインオブジェクトが格納されている。その「プロダクトオーナー(ProductOwner)」の場合、「プロダクトモジュール」から参照され、「テナントモジュール」を参照している。これも比較的わかりやすい依存関係である。
ここにはドメインサービスのインターフェイスが含まれている。このインターフェイスが腐敗防止層として振る舞う。
モジュールの中には、本来60近いクラス群が含まれてた。そこで、SaaSOvationでは、ユビキタス言語に沿って「スプリント(Sprints)」「バックログ(BacklogItems)」「リリース(Release)」といったサブモジュールを作成し、その中にクラスを分けて配置するようにした。
このモジュールは親子関係内があり、双方向の依存関係となっている。本来は双方向の依存関係は避けるべきだが、最初に述べたルールにあるように、ここは管理のためにモジュールに分割した領域のため、双方向でも問題ないこととしている。
どんなアーキテクチャを採用しても、アーキテクチャにおけるモデル以外のコンポーネントについても、モジュールを作成して命名しなければならない。
ただし、アーキテクチャにあった命名をする。例えば、レイヤードアーキテクチャであれば、ユーザーインターフェース・アプリケーション・ドメイン・インフラストラクチャといった名前をつける
ユーザーインターフェース層の場合には次の2つのモジュールを用意することが想定される
- RESTfulリソースを扱うモジュール
- JSON/XML/HTTPを扱えるようにする
- Viewを扱うモジュール
- UIのレイヤウトやRESTfulな表現から変換する
ここでは、扱う責務に応じてモジュールを作成する。
次にアプリケーション層について考察する。DDDでは、ドメインオブジェクトを利用するアプリケーションレイヤ側にも処理を持つ。ここのモジュール設計も同様で、わかりやすく管理できるように分割する。
これらのモジュールの中には、アプリケーションサービスや、そのサービスで利用するコマンドが格納される。
SaaSOvation.AgilePM.Application.Teams
SaaSOvation.AgilePM.Application.Sprints
SaaSOvation.AgilePM.Application.Products
このように、各プロジェクトの状況に応じてモジュール分割を検討する。
モジュールによる分割か、境界づけられたコンテキストによる分割かで悩むことがある。モジュールはクラス群でまとめる仕組みであり、境界づけられたコンテキストは、ユビキタス言語の境界線(ビジネス的な境界線)をまとめる仕組みになる。
モデルが大きくなってきた場合、ドメインエキスパートの言葉に従い、どのような方法による分割が最適かを検討する。そのため最初の設計時は、無理して分割してしまうのではなく、ひとまとめのままのほうが無難である。
- 昔ながらのモジュールと、最近のデプロイ指向のモジュール化の違いを知った
- ユビキタス言語に沿ってモジュールを命名する重要性を学んだ
- モジュールの設計を間違え、機械的に考えてしまうと、モデリングの創造性を抑えてしまうということを知った
- アジャイルプロジェクト管理コンテキストのモジュールがどのように設計されたのか、そしてなぜその手法を選んだのかを検討した
- モデルの外部でモジュールを扱う際の、有用な指針を知った
- 最後に、新しい境界づけられたコンテキストを作るよりも、モジュールを使うことを考えるべき場面についてヒントを得た
-
今回のモジュールって、DDD文脈のモデルにおけるモジュールって話で、ソフトウェアアーキテクチャのモジュールとは別っぽい感じの印象があった。
-
そもそも、みんな実プロダクトでどんなふうにモジュール切る? 大体この章と同じようなやり方ですか?
- そもそも、みんな実プロダクトでどんなふうにモジュール切る? 大体この章と同じようなやり方ですか?
- それぞれ好みはあるので、それで進めてみて、あとはプロジェクトやチームの状況によって、変わる感じ
- そのパターンに近い形になることはありますね。集約が理由と言うよりも、あるクラスとそのライフサイクルに制約されるクラス群のまとまり、みたいなのがあって、それに集約が付随してくるという依存関係ですが。