🕌

スタンプ結合

2022/03/06に公開

はじめに

良い設計を考えるときの観点の一つに、モジュールの結合度があります。モジュールの結合度の階級の一つが、スタンプ結合です。

コードレビューをするときに、スタンプ結合という言葉を使うことが多いです。ですが、今までスタンプ結合の出典を知らなかったので、出典を調べて、その結果をまとめました。サンプルコードはGoで書いています。

スタンプ結合の定義

スタンプ結合は和書だと、Glenford J.Myersの著書、ソフトウェアの複合/構造化設計で以下のように定義されています。

2つのモジュールが内容・共通・外部・制御結合ではなく、かつ、おなじ非大域的データ構造を参照しているとき、これら2つのモジュールはスタンプ結合である。

スタンプ結合の定義を理解するために、モジュールの結合度が定義された背景と階級について説明します。

複合設計

ソフトウェアの複合/構造化設計では、複合設計(composite design)という、良い設計をするための方法論について紹介をしています。複合設計では、うまく構造化されたプログラムとそうでないプログラムの大きな違いは「複雑さ」であると見なし、「複雑さ」を減少させるため、モジュールの独立性を高めようとしています。

モジュール

ソフトウェアの複合/構造化設計では、モジュールは以下のように定義されています。

  1. 閉じたサブルーチンであること
  2. プログラム内の他のどんなモジュールからも呼び出すことができること
  3. 独立してコンパイルできる可能性をもっていること

ソフトウェアの複合/構造化設計は1979年に発売された本なので、モジュールの例として、FORTRANやCOBOLの機能を挙げています。Goでモジュールを考えるなら、適宜モジュールの定義を拡大解釈する必要があります。

モジュールの結合度

モジュールの独立性を高くする手段の一つとして、モジュールの結合度を定義しています。モジュールの結合度では、強い結合度(悪い)から弱い結合度(良い)への階級を、上から順に以下のように定義しています。

  1. 内容結合(content coupling)
  2. 共通結合(common coupling)
  3. 外部結合(external coupling)
  4. 制御結合(control coupling)
  5. スタンプ結合(stamp coupling)
  6. データ結合(data coupling)
  7. 非直接結合(no direct coupling)

スタンプ結合の定義と関係する階級のみ、強い結合度から順番に確認します。

内容結合

内容結合は、以下のように定義されています。

2つのモジュールで、1つが他の内部を直接参照するとき、あるいは、モジュール間の正常な連係方式を避けて通るとき、この2つのモジュールは内容結合である。

「モジュール間の正常な連係方式を避けて通る」場合についてGoでサンプルコードを書きます。定義を拡大解釈し、Goの構造体をモジュールとみなします。別のpackageで定義されている構造体のプライベート変数を、あらかじめ用意されているメソッドを使わずに、reflect.ValueOfとunsafe.Pointerを使って値の変更をします。

package task

type Task struct {
	Title     string
	isPublish bool
}

func NewTask(title string) *Task {
	return &Task{
		Title:     title,
		isPublish: false,
	}
}

func (t *Task) Publish() {
	t.isPublish = true
}
package main

import (
	"fmt"
	"go-sample/task"
	"reflect"
	"unsafe"
)

func main() {
	t := task.NewTask("homework")
	fmt.Printf("%+v\n", t)

	v := reflect.ValueOf(t).Elem()
	pv := v.FieldByName("isPublish")
	isPublish := (*bool)(unsafe.Pointer(pv.UnsafeAddr()))
	*isPublish = true

	fmt.Printf("%+v\n", t)
}
$ go run main.go
&{Title:homework isPublish:false}
&{Title:homework isPublish:true}

サンプルコードのように、不正な手段で別のpackageの構造体のプライベート変数の値を変更してしまうと、予期せぬ値の変更が行われ、バグを生み出す原因になります。サンプルコードの内容結合を取り除くなら、あらかじめ用意されているPublishメソッドを使うように変更すればよいです。

サンプルコードでは、無理やりプライベート変数のポインタを取り出して値の変更をしました。ただ、このようなコードを誤って書いてしまうことは無いと思います。Goだと内容結合のコードを書くほうが難しいかもしれません。

共通結合

共通結合は、以下のように定義されています。

共通結合は大域的(global)なデータ構造を参照するモジュールのグループのあいだでおきる。

例えば、グローバル変数を複数のpackageの関数から参照する場合、これは共通結合に当てはまります。以下のようにGoでサンプルコードを書いてみました。

package animal

type Value struct {
	Dog string
	Cat string
}

var Animal = Value{
	Dog: "Pochi",
	Cat: "Tama",
}

func GetDogName() string {
	return Animal.Dog
}

func GetCatName() string {
	return Animal.Cat
}
package main

import (
	"fmt"
	"go-sample/animal"
)

func main() {
	a1 := animal.Animal
	fmt.Println(a1.Dog)

	animal.Animal.Dog = "Shiro"
	a2 := animal.Animal
	fmt.Println(a2.Dog)
}

animal.Animalは大域的データであるため、どのpackageからも参照できてしまいます。animal.Animalの値を変更する場合は、animal.Animalの値を参照しているモジュールを常に認識しなければならないです。

また、main関数ではanimal.Animal.Dogしか使っていませんが、animal.Animal.Catも参照することができます。必要以上のデータ項目を外部に晒している、といえます。

外部結合

外部結合は、以下のように定義されています。

モジュールのあるグループが、内容結合でも共通結合でもなく、また、それらが同種の大域的データ項目を参照しているばあいは、これらのモジュールは外部結合している。

外部結合の同種のデータとは、以下の3つを示します。

・単一のスカラ変数である
・各項目が1フィールドだけのリストあるいはテーブル
・各要素が同じ意味を持っている行列(array)

以下のようにGoでサンプルコードを書いてみました。

package animal

var Dog = "Pochi"
package main

import (
	"fmt"
	"go-sample/animal"
)

func main() {
	fmt.Println(animal.Dog)

	animal.Dog = "Shiro"
	fmt.Println(animal.Dog)
}

外部結合と共通結合はよく似ています。ソフトウェアの複合/構造化設計では、共通結合から改善された点は以下の3つと書かれています。

・モジュール間の余分な依存性の発生は発生しない
・ダミー構造の作成をする必要はない
・別のモジュールに対して必要以上にデータ項目を晒さない

モジュール間の余分な依存性は発生しない

モジュール間で構造体を共有せず、必要な変数(animal.Dog)のみを参照しています。そのため、モジュール間の余分な依存性は発生しません。

ダミー構造の作成をする必要はない

共通結合と比較すると、main関数はanimal.Animal.Dogのみ必要であって、animal.Value構造体は必要ないです。外部結合では、main関数でanimal.Value構造体の変数を宣言したり、animal.Value構造体のフィールドの値を詰め込む構造体(ダミー構造)を定義することはないです。

別のモジュールに対して必要以上にデータ項目を晒さない

モジュールで必要な変数(animal.Dog)のみ参照しています。

制御結合

制御結合は、以下のように定義されています。

2つのモジュールが内容・共通・外部結合でなく、また、1つのモジュールが他のモジュールの論理をはっきりと制御する。すなわち、1つのモジュールが他のモジュールにはっきりした制御要素をわたすときは、2つのモジュールは制御結合である。

例えば、引数で、関数内で使用する別の関数を制御する場合です。以下のようにGoでサンプルコードを書いてみました。

package main

import (
	"fmt"
	"log"
)

func print(pkg, msg, version string) {
	switch pkg {
	case "fmt":
		fmt.Println(msg)
	case "log":
		log.Printf("%s: %s", version, msg)
	}
}

func main() {
	version := "v1.0"
	msg := "hello, world"
	print("fmt", msg, version)
	print("log", msg, version)
}

制御結合の定義に当てはまるコードを書くことは問題にならない場合もあります。ただ、コードが論理的強度の問題を含む場合は注意する必要があります。

サンプルコードのprint関数は複数の関数のインターフェースとして実装されています。print関数の引数のversionは、引数pkgが"fmt"のときは使用されません。fmt.Println関数ではprint関数の引数をpkgを除いて1つしか使いませんが、常に2つの引数を認識する必要があります。これは、インターフェスをわかりにくくします。

スタンプ結合

スタンプ結合の定義をあらためて確認します。

2つのモジュールが内容・共通・外部・制御結合ではなく、かつ、おなじ非大域的データ構造を参照しているとき、これら2つのモジュールはスタンプ結合である。

そして、ソフトウェアの複合/構造化設計では、以下のようにスタンプ結合の例示をしています。

たとえば、データ構造が1つの引数としてモジュールの間を受け渡しされる

さらに、渡されたデータ構造の項目は一部のみ使用する、と書かれています。つまり、モジュールの引数として必要以上のデータを渡している、ということです。

以下のようにGoでサンプルコードを書きました。

package main

import (
	"go-sample/logger"
)

type Env struct {
	Env                   string
	NekochanApiEndpoint   string
}

func newEnv() (*Env, error) {
...
}

func printEnv(e *Env) {
	fmt.Printf("Environment=%s\n", e.Env)
}

func main() {
	env, err := newEnv()
...	
	printEnv(env)
}

printEnv関数では、Env.Envは使いますが、Env.NekochanApiEndpointは使いません。必要以上にフィールドへの参照を可能にし、モジュールの複雑性を増加させています。

モジュール間の結合度を弱くするため、printEnv関数に渡す引数を必要な項目のみとし、printEnv関数が必要以上にEnv構造体について知らなくてもよいようにします。

func printEnv(env string) {
	fmt.Printf("Environment=%s\n", env)
}

func main() {
...
	printEnv(e.Env)

このようなモジュール同士の関係をデータ結合といいます。スタンプ結合の定義に直接関係無いので、以下に定義のみ書いておきます。

  1. それらが内容・共通・外部・制御・スタンプ結合でない
  2. 2つのモジュールがたがいに直接的に連絡しあっている
  3. モジュール間のすべてのインターフェース・データが同種のデータ構造である

まとめ

本記事では、スタンプ結合の定義を確認するため、モジュールの結合度の階級の定義を確認しました。そして、スタンプ結合の定義と具体例について説明をしました。スタンプ結合は、普段プログラミングをする中で発生する頻度が高いので、本記事ではメインで扱いました。

今回紹介した用語や定義は、複合設計(composite design)という、良い設計をするための方法論で登場したものを利用しました。よく知られている用語だと、モジュールの結合度のほか、構造化プログラミング、モジュール強度(凝集度)も登場します。設計に興味がある人はぜひ調べてみてください。

本記事で参考にしたソフトウェアの複合/構造化設計は古い本で、今は手に入りづらい状態です。複合設計について書いてある新しめの本があれば、そちらを読む方が良いと思います。

参考文献

ソフトウェアの複合/構造化設計
https://note.com/cyberz_cto/n/n26f535d6c575
https://stop-the-world.hatenablog.com/entry/2019/12/31/214058

Discussion