🚀 ニフティ’s Notion

【オブジェクト指向2024 #3】カプセル化

目次

season2

~あらすじ~

クラスの概念を身につけた(クラスを作ることしか知らない)プログラマーのAさん。チーム開発でもクラスを用いてブイブイ言わせるが….?

A: 既存の社員管理システムもクラスを用いて刷新しました!今後はクラスを使って開発していきましょう!
B: はい!・・・これが社員管理システムかぁ(よく分かっていない)
class Employee:
    def __init__(self, name, rating, rank_multiplier, base_salary):
        self.name = name
        self.rating = rating
        self.rank_multiplier = rank_multiplier
        self.salary = self.calculate_salary()
        self.base_salary = base_salary
        
    # 給料を計算するメソッド
    def calculate_salary(self):
        return self.rating * self.rank_multiplier * self.base_salary

    # 給料を更新するメソッド
    def update_salary(self):
        self.salary = self.calculate_salary()

# 従業員オブジェクトの作成
employee = Employee(name="田中太郎", rating=85, rank_multiplier=1.2, base_salary=50000)


print(f"Name: {employee.name}")
print(f"Salary: {employee.salary}")

ある日、偉い人からシステムの変更を依頼されました。

偉い人: 社員のベース給料を1000円あげといて

B: たぶんこのsalaryってやつがベース給料だと思うからこれを+1000にしてと…
employee = Employee(name="田中太郎", rating=85, rank_multiplier=1.2, base_salary=50000)

employee.salary += 1000 

~後日~

偉い人: 田中太郎さんの給料が10万円ほどおかしいんだけど?
B: えっ
A: あっ

ドメイン知識もなかったBさんは本来(評価点、階級倍率、ベース給料)の3つから計算される給料にあやまって変更を加えてしまい、システム全体のデータ整合性が損なわれてしまいました。

いったいどうすればこのような問題を防げたのでしょうか?

カプセル化を用いない問題点

  • データに自由にアクセスできたため、データに不整合が生じた。
  • データに自由にアクセスできたため、コードが複雑になる可能性があった。
  • データにバリデーションがないため base_salary=-50000  とすると給料がマイナスになる可能性があった。

カプセル化

  • オブジェクト内部のデータや振る舞いを隠蔽してオブジェクト外部からの操作を制御することで、オブジェクトの独立性を保つための仕組み

1. データの保護

カプセル化により、オブジェクト内部のデータは直接アクセスや変更から保護されます。これにより、データの不整合や誤った操作を防ぐことができます。

2. 複雑さの管理

オブジェクト内部の実装の詳細を隠すことで、複雑さを管理しやすくなります。外部からはインターフェース(メソッドやプロパティ)を通じてのみアクセスできるため、内部の変更が外部に影響を与えにくくなります。

3. メンテナンスの容易化

内部実装を変更する場合でも、外部インターフェースさえ変更しなければ、既存のコードに影響を与えずに済みます。これにより、システムのメンテナンスが容易になります。

4. モジュール化の推進

カプセル化は、ソフトウェアのモジュール化を推進します。オブジェクトごとに独立した機能を持たせることで、各オブジェクトが自己完結型のモジュールとなり、再利用性が向上します。

5. デバッグの容易化

データの隠蔽により、バグが発生した場合に原因を特定しやすくなります。データのアクセスが制限されることで、問題のあるコードがどこにあるかを絞り込みやすくなります。

6. 安全性の向上

データに対するアクセス権を制御することで、誤った使用や不正なアクセスからシステムを保護できます。特にセキュリティが重要なシステムでは、カプセル化は大きなメリットとなります。

7. 一貫性の維持

オブジェクトが提供するインターフェースを通じてのみデータを操作するため、データの一貫性を保つことができます。これにより、予期しない動作やバグの発生を防ぐことができます。

カプセル化の方法

インスタンス変数に対してネームマングリングを行った場合
  • 直接インスタンス変数を参照することはできず、メソッドを介して参照できる
    class Order:
        def __init__(self, order_id, item_name, price, quantity):
            self.__order_id = order_id
            self.__item_name = item_name
            self.__price = price
            self.__quantity = quantity
    
        def get_total_price(self):
            return self.__price * self.__quantity
    
        def get_order_info(self):
            return (f'注文番号: {self.__order_id}, 商品名: {self.__item_name}, '
                    f'価格: {self.__price}, 数量: {self.__quantity}, 合計金額: {self.get_total_price()}')
    
    
    order1 = Order(1, "apple", 120, 4, )
    
    print(order1.price)
    Traceback (most recent call last):
      File "/home/jail/prog.py", line 18, in <module>
        print(order1.price)
              ^^^^^^^^^^^^
    AttributeError: 'Order' object has no attribute 'price'

    実際に動かしてみる

    実際に動かしてみる(ネームマングリングをしない場合)

    メソッドに対してネームマングリングを行った場合
    • メソッドを呼び出すことはできない(クラス内のメソッドからは呼び出せる)
      class Order:
          def __init__(self, order_id, item_name, price, quantity):
              self.__order_id = order_id
              self.__item_name = item_name
              self.__price = price
              self.__quantity = quantity
      
          def get_total_price(self):
              return self.__price * self.__quantity
      
          def __get_order_info(self):
              return (f'注文番号: {self.__order_id}, 商品名: {self.__item_name}, '
                      f'価格: {self.__price}, 数量: {self.__quantity}, 合計金額: {self.get_total_price()}')
      
      
      order1 = Order(1, "apple", 120, 4, )
      
      print(order1.__get_order_info())
      Traceback (most recent call last):
        File "/home/jail/prog.py", line 18, in <module>
          print(order1.__get_order_info())
                ^^^^^^^^^^^^^^^^^^^^^^^
      AttributeError: 'Order' object has no attribute '__get_order_info'. Did you mean: '_Order__get_order_info'?

      実際に動かしてみる

      社員管理システムをカプセル化する
      class Employee:
          def __init__(self, name, rating, rank_multiplier, base_salary):
              self.name = name
              self.__rating = rating
              self.__rank_multiplier = rank_multiplier
              self.__base_salary = base_salary
              self.__salary = self.__calculate_salary()
      
          # 評価点のゲッターとセッター
          @property
          def rating(self):
              return self.__rating
      
          @rating.setter
          def rating(self, value):
              if 0 <= value <= 100:
                  self.__rating = value
                  self.__update_salary()
              else:
                  raise ValueError("Rating must be between 0 and 100")
      
          # 階級倍率のゲッターとセッター
          @property
          def rank_multiplier(self):
              return self.__rank_multiplier
      
          @rank_multiplier.setter
          def rank_multiplier(self, value):
              if value > 0:
                  self.__rank_multiplier = value
                  self.__update_salary()
              else:
                  raise ValueError("Rank multiplier must be positive")
      
          # 基本給のゲッターとセッター
          @property
          def base_salary(self):
              return self.__base_salary
      
          @base_salary.setter
          def base_salary(self, value):
              if value >= 0:
                  self.__base_salary = value
                  self.__update_salary()
              else:
                  raise ValueError("Base salary must be non-negative")
      
          # 給料のゲッター
          @property
          def salary(self):
              return self.__salary
      
          # 給料を計算するメソッド
          def __calculate_salary(self):
              return self.__rating * self.__rank_multiplier * self.__base_salary
      
          # 給料を更新するメソッド
          def __update_salary(self):
              self.__salary = self.__calculate_salary()
      
      # 従業員オブジェクトの作成
      employee = Employee(name="田中太郎", rating=85, rank_multiplier=1.2, base_salary=50000)
      
      print(f"Name: {employee.name}")
      print(f"Salary: {employee.salary}")
      
      # 不正な操作を試みる
      try:
          employee.salary = 600000000  # 直接変更できない(無視される)
      except AttributeError as e:
          print(e)
      
      try:
          employee.rating = 110  # 不正な評価点(例外が発生)
      except ValueError as e:
          print(e)
      
      try:
          employee.rank_multiplier = -1.3  # 不正な階級倍率(例外が発生)
      except ValueError as e:
          print(e)
      
      # 有効な値の設定
      employee.rating = 90
      employee.rank_multiplier = 1.5
      employee.base_salary = 55000
      
      print(f"Updated Salary: {employee.salary}")  # 更新された給料を表示
      
      

      1. プライベート属性 : __rating , __rank_multiplier , __base_salary , __salary などの属性はプライベート(先頭に __ を付ける)として定義されています。これにより、これらの属性は外部から直接アクセスできなくなりました。
      2. プロパティ : rating , rank_multiplier , base_salary , salary などの属性に対してプロパティを使用し、ゲッターとセッターを定義しています。これにより、属性にアクセスする際に検証が行われ、不正な値の設定を防止しました。
      3. 例外処理 : 不正な値が設定されると、 ValueError を発生させます。これにより、データの整合性が保たれました。
      4. 給料の自動計算 : rating , rank_multiplier , base_salary のいずれかが変更されると、 __update_salary メソッドが呼び出され、給料が自動的に再計算されました。

      この実装により、従業員管理クラスはカプセル化され、安全で一貫性のあるデータ管理が可能になりました。

まとめ

カプセル化は、オブジェクト内部のデータや振る舞いを隠蔽しメソッドを通して操作することで、不用意なデータの変更を防いだり、変更箇所を最小にすることができる。
データを隠蔽するためには、フィールドのアクセス範囲を制御する必要がある。
Pythonにおいてはネームマングリングを用いて、アクセス範囲を制御する。

次へ: 📄 【オブジェクト指向2024 #4】お題: サブスク管理システムを作ろう