きっかけ
DDD関連の書籍を見てるとクラス図が頻繁に出てくるのだが、「クラス間の関係を示す矢印だったり線の意味を知らなすぎ」 で理解の妨げになってる。
あと、基本的にJavaでサンプルコードが書いてあることが多いのだが、実務で使ってる 「Goだとそれぞれの関係についてどう実装すべきなのかイメージができてない」。
このままじゃ勉強もはかどらんし実務に落とし込むのも難しいしということで、情報収集して自分の中で一回整理してみようと決意。
クラス間の関係を表す線の意味を整理
線 | 名前 | 意味 |
---|---|---|
-----▷ | 実現 | 「インターフェースと実装」の関係。「実装-------▷インターフェース」というように表す |
──▷ | 汎化 | 「子クラス-親クラスの関係」。「子クラス───▷親クラス」というように表す。 |
-----> | 依存 | クラス間にそこまで強い関係はないものの、一方のクラスを変更した際にもう一方のクラスも変更の必要が生じること |
──> | 関連 | 参照関係を示す。クラスAはクラスBを参照し、クラスBからはクラスAを参照できない状態のとき、「クラスA───>クラスB」とする。お互いに参照できる場合は、「クラスA────クラスB」とする。 |
◇── | 集約 | 「関連」にグルーピングする側される側の概念が追加されたもの。グルーピングする側がクラスA、される側がクラスBである関係である場合、「クラスA◇───クラスB」となる。 |
◆── | コンポジション | 「集約」にクラスの生存期間の概念が追加されたもの。グルーピングする側がクラスA、される側がクラスBである関係である場合で、かつクラスAのインスタンスを削除するとクラスBのインスタンスも削除されるようになるような状態であるとき、「クラスA◆───クラスB」となる |
補足
矢印の頭が▷になる「実現」・「汎化」は、それぞれ「インターフェース-実装」・「継承」というだけなのでわかりやすい。
「関連」・「集約」・「コンポジション」のトリオについては、
関連 ∋ 集約 ∋ コンポジション
という関係になるらしく、関連 → 集約 → コンポジション の順で条件が追加されていくようなイメージらしい。
関連については、グルーピング関係がないので、「参照される側の生存期間は参照する側の生存期間に依存しない」と考えて良さそう。
一番謎なのが「依存(------->)」 だが、実際には以下のような状況で使うらしい。
- クラスAがクラスBを「生成する」が、クラスAはその後クラスBと関わりなしというようなときに、「クラスA------->クラスB」とするらしい
- クラスAがクラスBを「使用する」場合「クラスA------>クラスB」とするらしい
- 「実現(-------▷)」の意味で使われることがあるらしい
また、依存の場合は場面場面で意味合いが分かれるので、矢印の上に<<依存の意味>>
を書いておく ものらしい。
多重度
関連・集約・コンポジションは、クラスの根本に数を描いて、結びつくクラスの数を表現する。
# 1 vs 1
クラスA ───────── クラスB
1 1
# 1 vs 1以上
クラスA ───────── クラスB
1 1..n
# 1 vs 0以上
クラスA ───────── クラスB
1 0..n
Goのコード
前提:
- Goはクラスという機能は用意されてない: 「構造体+メソッド」をクラスに相当するものとして扱う。
- デストラクタも存在しない
汎化
Goには「継承」がないので考えなくていいはず。
構造体の埋め込み機能というやつがあるが、あれは親を継承して子が利用するじゃなくて、子を親に埋め込むなので、意味が違う。
そういうことで、Goでの実装時には、設計段階(クラス図作る時点)で使っちゃ「汎化」の矢印が登場しちゃダメ(という結論)。
実現
「クラスA(インターフェース) ◁--- クラスB(実装)」
なとき、以下のような実装になる
type ClassA interface {
DoSomething()
}
type ClassB struct {
// 任意のフィールド
}
func (b *ClassB) DoSomething(){
// 実装処理
}
関連と集約コンポジション
「クラスA ───> クラスB」 or 「クラスA ◇───> クラスB」 な場合
共通のクラスB関連コード
type ClassB struct{
// 任意のフィールド
}
func NewClassB *ClassB {
return &ClassB{
...
}
}
集約
以下の方針で実装する
- ClassAのフィールドとしてClassBをもつ
- コンストラクタで、クラス外のどこかで生成しているClassBをClassAにセットすることで「関連」させつつ、ClassAの生存期間終了 → ClassBの生存期間終了とならなくすることで集約の条件を満たすようにする
1 vs 1
type ClassA struct{
...
classB *ClassB
}
// コンストラクタ
func NewClassA *ClassA(classB *ClassB){
classA := &ClassA{
...
classB: classB,
}
return classA
}
1 vs n
type ClassA struct{
...
classBList []*ClassB
}
// コンスラクタ
func NewClassA *ClassA(classBList []*ClassB){
classA := &ClassA{
...
classBList: classBList,
}
return classA
}
コンポジション
以下の方針で実装する
- ClassAのフィールドとしてClassBをもつ
- 「デストラクタが存在しない」ので、「コンストラクタ内でClassBを生成して、ClassAにセットすることで「関連」させつつ、ClassAの生存期間終了 → ClassBの生存期間終了とさせる」ことでコンポジションの条件を満たすようにする
・・・・が多くの場面では、ClassBを生成するのに必要な情報は結局外部から引っ張って来ないといけないだろうし、あらゆる局面での実現は難しそう。
コンポジションも基本的にGoでの実装では設計段階で考えなくてよさそう。
1 vs 1
type ClassA struct{
...
classB *ClassB
}
// コンストラクタ
func NewClassA *ClassA(){
// ここでなんとかしてClassBを生成
classA := &ClassA{
...
classB: classB,
}
return classA
}
1 vs n
type ClassA struct{
...
classBList []*ClassB
}
// コンスラクタ
func NewClassA *ClassA(classBList []*ClassB){
// ここでなんとかしてClassBのリストを生成
classA := &ClassA{
...
classBList: classBList,
}
return classA
}
参考