不変クラスを使った安全なオブジェクト設計

不変クラスを使った安全なオブジェクト設計 プログラミング

オブジェクト指向プログラミングにおいて、可変クラスは便利です。
しかし、誤用されやすいという問題があります。

本記事では、可変クラスの問題点と、その対策としての不変クラスの設計方法について解説します。
具体的なコード例を交えながら、不変クラスを使った安全なオブジェクト設計のベストプラクティスを紹介します。

可変クラスが引き起こす問題

以下のコードは、ユーザー情報を表す可変クラスの例です。

class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

    def set_name(self, new_name: str):
        self.name = new_name

    def set_email(self, new_email: str):
        self.email = new_email

この User クラスは、name と email のプロパティを持っています。
そして、それぞれ set_name と set_email のセッター関数で変更できます。

しかし、この可変性が以下のような問題を引き起こす可能性があります。

予期せぬ変更
User オブジェクトを他のコードに渡した場合、そのコードがオブジェクトの状態を予期せずに変更してしまう可能性があります。

def process_user(user: User):
    # ユーザー名を大文字に変更
    user.set_name(user.name.upper())

    # ユーザー情報を処理する
    # ...

# ユーザーを作成
user = User("Alice", "alice@example.com")

# ユーザー情報を処理
process_user(user)

# ユーザー名が変更されている
print(user.name)  # 出力: "ALICE"

この例では、process_user 関数が User オブジェクトを受け取り、内部でユーザー名を大文字に変更しています。
しかし、user オブジェクトを渡した側は、ユーザー名が変更されることを予期していない可能性があります。

この場合、process_user 関数の呼び出し後に user.name が “ALICE” に変更されてしまいます。

マルチスレッドでの問題
複数のスレッドから同じ User オブジェクトにアクセスする場合、不整合が発生する可能性があります。
それは、あるスレッドが状態を変更している最中に、別のスレッドが状態を読み取ろうとするような場合です。

不変クラスによる対策

可変クラスの問題を防ぐには、不変クラスを設計することが有効となります。
不変クラスは、一度作成されたら状態を変更できないクラスです。

以下は、User クラスを不変クラスに変更した例です。

class User:
    def __init__(self, name: str, email: str):
        self._name = name
        self._email = email

    @property
    def name(self):
        return self._name

    @property
    def email(self):
        return self._email

    def with_name(self, new_name: str):
        return User(new_name, self._email)

    def with_email(self, new_email: str):
        return User(self._name, new_email)

この不変版の User クラスでは、以下のような変更を加えています。

セッター関数の削除
セッター関数を削除し、プロパティをプライベート化しました。
これにより、外部からの直接の状態変更を防ぎます。

コピーオンライト関数の追加
状態を変更する代わりに、with_name と with_email のコピーオンライト関数を追加しました。
これらの関数は、新しい状態を持つ新しい User オブジェクトを返します。
元のオブジェクトの状態は変更されません。

以下は、コピーオンライト関数の利用例となります。

# ユーザーを作成
user = User("Alice", "alice@example.com")

# ユーザー名を変更した新しいオブジェクトを作成
updated_user = user.with_name("Bob")

# 元のオブジェクトは変更されていない
print(user.name)  # 出力: "Alice"
print(user.email)  # 出力: "alice@example.com"

# 新しいオブジェクトには変更が反映されている
print(updated_user.name)  # 出力: "Bob"
print(updated_user.email)  # 出力: "alice@example.com"

この例では、user オブジェクトに対して with_name 関数を呼び出し、新しいユーザー名 “Bob” を指定しています。
with_name 関数は、新しいユーザー名を持つ新しい User オブジェクトを返します。

元の user オブジェクトは変更されず、user.name は “Alice” のままです。
新しく作成された updated_user オブジェクトには、変更が反映され、updated_user.name は “Bob” になります。

不変クラスを使用することで、以下のようなメリットがあります。

状態の明示的なコントロール
状態の変更を明示的にコントロールできます。
コピーオンライト関数を使って新しいオブジェクトを作成することで、状態の変更を意図的に行うことができます。

安全性の向上
マルチスレッドでの安全性が向上します。
複数のスレッドが同じオブジェクトを参照していても、状態が変更されないため、不整合が発生するリスクが軽減されます。

ただし、不変クラスを設計する際は、以下の点に注意が必要です。

パフォーマンスへの影響
コピーオンライト関数では、新しいオブジェクトを作成するためのオーバーヘッドが発生します。
大量のオブジェクトを扱う場合は、パフォーマンスへの影響を考慮する必要があります。

循環参照の回避
不変クラスが他の不変クラスを参照している場合、循環参照が発生する可能性があります。
循環参照を避けるために、適切な設計が必要です。

まとめ

可変クラスは、予期せぬ変更やマルチスレッドでの問題を引き起こす可能性があります。
これらの問題を防ぐために、不変クラスを設計することが有効です。

不変クラスでは、状態を直接変更できないようにします。
そのために、コピーオンライト関数を使って新しい状態を持つオブジェクトを作成します。

それらにより、オブジェクトの状態変更を明示的に管理し、予期せぬ変更を防ぐことができます。

ただし、パフォーマンスへの影響や循環参照の回避など、設計時の注意点にも留意が必要です。
不変クラスを適切に活用することで、より堅牢で安全なコードを書くことができるでしょう。

タイトルとURLをコピーしました