🤖

実は普段からやってるだけのこと 契約による設計: 不変条件編

2023/07/30に公開

事前条件編、事後条件編とやってきていた記事の続きで、今回は不変条件編です!

不変条件とは?

不変条件(Invariants)は、DbCにおける重要な概念であり、あるメソッドが呼び出される前後で常に保持されるべき状態または条件を指します。不変条件は、クラスまたはオブジェクトの整合性を保つためのルールで、プログラムの実行中に一貫して遵守されるべきです。

例えば、銀行のアカウントに関連する不変条件として、アカウントの残高が負になってはならない、というものが考えられます。これはアカウントに対する全ての操作(入金、出金など)前後で、常に保持されるべき条件です。

Go言語における不変条件の実装

Go言語は契約プログラミングまたは不変条件を直接サポートする機能を持っていませんが、開発者自身が論理を適切に設計し、条件を守ることで、このパラダイムを適用することが可能です。以下に、Go言語で銀行口座を管理するクラスを設計し、その中に不変条件を組み込む例を示します。

type Account struct {
	balance int
}

func NewAccount(initialBalance int) (*Account, error) {
	if initialBalance < 0 {
		return nil, errors.New("初期残高は0以上である必要があります")
	}
	return &Account{balance: initialBalance}, nil
}

func (a *Account) Deposit(amount int) error {
	if amount <= 0 {
		return errors.New("預金額は0より大きい必要があります")
	}
	a.balance += amount
	return nil
}

func (a *Account) Withdraw(amount int) error {
	if amount <= 0 {
		return errors.New("引き出し額は0より大きい必要があります")
	}
	if a.balance < amount {
		return errors.New("残高不足です")
	}
	a.balance -= amount
	return nil
}

func (a *Account) Balance() int {
	return a.balance
}

この例では、Accountクラスとそのメソッドの実装を示しています。口座の初期化(NewAccount)、預金(Deposit)、引き出し(Withdraw)、残高照会(Balance)という基本的な操作が定義されています。

不変条件は、「口座の残高が負にならない」という形で組み込まれています。この条件は、NewAccount関数で初期残高が0以上であることを確認すること、またWithdraw関数で引き出し額が口座の残高を超えないことを確認することで、保証されています。

不変条件の重要性

不変条件はプログラムの安全性と整合性を保つために非常に重要です。不変条件が満たされていないと、プログラムは予期しない方法で動作する可能性があり、データの不整合やバグを引き起こす可能性があります。

不変条件を遵守することは、特に並行プログラミングにおいて重要です。複数のスレッドが同時にデータにアクセスし、それを変更する可能性がある場合、不変条件が保証されていなければデータの整合性が失われる可能性があります。Go言語は並行プログラミングをサポートしているため、不変条件の実装は特に重要です。

不変条件とテスト

Goにはテストフレームワークが組み込まれているため、不変条件を保持するためのテストケースを作成することが容易になります。テストケースはプログラムの正しさを確認するための重要な手段ですが、それだけでなく不変条件が維持されているかどうかを確認する手段としても有効です。

以下に、上記の銀行口座クラスの不変条件をテストする例を示します。

func TestAccount(t *testing.T) {
    // 初期残高を100とする口座を作成
    account, err := NewAccount(100)
    if err != nil {
        t.Errorf("Failed to create account: %v", err)
    }

    // 50を預ける
    if err := account.Deposit(50); err != nil {
        t.Errorf("Failed to deposit: %v", err)
    }

    // 残高が150になっていることを確認
    if balance := account.Balance(); balance != 150 {
        t.Errorf("Unexpected balance. got: %d, want: %d", balance, 150)
    }

    // 200を引き出す(残高不足)
    if err := account.Withdraw(200); err == nil {
        t.Error("Expected error but no error returned")
    }

    // 残高が依然として150であることを確認(不変条件が保持されていることの確認)
    if balance := account.Balance(); balance != 150 {
        t.Errorf("Unexpected balance. got: %d, want: %d", balance, 150)
    }
}

このテストでは、アカウントの作成、預金、引き出しの操作を行い、各ステップで残高を確認しています。また、不足する残高で引き出しを試みた場合にはエラーが返されること、そしてその操作後も残高が変化していないことを確認することで、不変条件が保持されているかをテストしています。

不変条件の取り扱いにおける注意点

不変条件を実装する際には、以下のような注意点があります。

不変条件は業務ロジックと密接に関連しているため、その設計には業務知識が必要となります。例えば、銀行のアカウントでは「口座の残高が負にならない」という不変条件が考えられますが、これは銀行業務の基本的なルールに基づいています。不適切な不変条件を設定すると、システムの挙動が業務ルールに反するものになりかねません。
不変条件は常に満たされるべきであると述べましたが、一部の操作中は一時的に不変条件が満たされない状態になることがあります。このような場合、その操作がアトミック(不可分)であることを保証することで、操作の前後で不変条件が満たされるようにしなければなりません。

まとめ

Design by Contractと不変条件は、プログラムの正確性を確保し、エラーやバグを防ぐための有用な手段です。Go言語でこれらの概念を用いることで、堅牢なソフトウェアを設計することが可能となります。

また、テストによって不変条件が確実に保持されていることを確認することができ、更に信頼性の高いシステムを構築するための助けとなります。ソフトウェア開発においては、不変条件の考慮とそれに対するテストが重要な役割を果たします。

このように、不変条件の適切な設定とそれを維持するためのロジックの実装は、品質の高いソフトウェアを作成するにあたって重要です。

Discussion