iT邦幫忙

2022 iThome 鐵人賽

DAY 30
0

最後一天旅程。今天前半段分享程式如何利用Polymorphism技術「重構」,後半段則是總結和心得報告。


程式中如有多重if'scase's判斷物件,可考慮重構

  • 程式如果原本沒有使用繼承,又出現較長的if/elifmatch/case來判斷物件,隨著系統擴展,陸續加入新物件或刪除原物件,於是elif或case就必須跟著訂正,而且訂正的可能不是一處而是幾百處。大家都知道,修改邏輯是有風險的,可能牽一髮而動全身。

  • 此現象可能是由於原程式架構設計不良,導致日後擴充和維護不便,帶來較大風險。

  • 先看原程式:

    • 類別部分:

      • 地球上的樹:

        class EarthlyTree():   # 地球上的樹
            def __init__(self, breed: str, age: int):   # constructor
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            @property
            def breed(self) -> str:
                return self.__breed
        
            @breed.setter
            def breed(self, breed: str):
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
        
            @property
            def age(self) -> int:
                return self.__age
        
            @age.setter
            def age(self, age: int):
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            def grow(self):        # 生長。
                print(f'{__class__.__name__} {self.breed} is growing.')
        
            def reproduce(self):   # 繁殖。
                print(f'{__class__.__name__} {self.breed} is reproducing.')
        
            def get_sick(self):     # 得病。
                print(f'{__class__.__name__} {self.breed} is getting sick.')
        
            def die(self):          # 死亡(假設死亡也有「行為」)。
                print(f'{__class__.__name__} {self.breed} is dying.')
        
      • 金星樹:

        class VenusianTree():   # 金星樹
            def __init__(self, breed: str, age: int):   # constructor
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            @property
            def breed(self) -> str:
                return self.__breed
        
            @breed.setter
            def breed(self, breed: str):
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
        
            @property
            def age(self) -> int:
                return self.__age
        
            @age.setter
            def age(self, age: int):
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            def grow(self):        # 生長。
                print(f'{__class__.__name__} {self.breed} grows younger and younger.')
        
            def reproduce(self):   # 繁殖。
                print(f'{__class__.__name__} {self.breed} reproduces on a daily basis.')
        
            def get_sick(self):     # 得病。
                print(f'{__class__.__name__} {self.breed} always revovers from illness.')
        
            def die(self):          # 死亡(假設死亡也有「行為」)。
                print(f'{__class__.__name__} {self.breed} raises itself from the dead.')
        
        
      • 火星樹:

        class MartianTree():   # 火星樹
            def __init__(self, breed: str, age: int):   # constructor
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            @property
            def breed(self) -> str:
                return self.__breed
        
            @breed.setter
            def breed(self, breed: str):
                is_valid_breed = True   # 判斷條件略。
                if is_valid_breed:
                    self.__breed = breed
                else:
                    raise Exception('Invalid breed.')
        
            @property
            def age(self) -> int:
                return self.__age
        
            @age.setter
            def age(self, age: int):
                is_valid_age = True   # 判斷條件略。
                if is_valid_age:
                    self.__age = age
                else:
                    raise Exception('Invalid age.')
        
            def grow(self):        # 生長。
                print(f'{__class__.__name__} {self.breed} stops growing.')
        
            def reproduce(self):   # 繁殖。
                print(f'{__class__.__name__} {self.breed} does not reproduce.')
        
            def get_sick(self):     # 得病。
                print(f'{__class__.__name__} {self.breed} is healthy.')
        
            def die(self):          # 死亡(假設死亡也有「行為」)。
                print(f'{__class__.__name__} {self.breed} is immortal.')
        
            def jog(self):          # 慢跑是火星樹的獨特行為。
                print(f'{__class__.__name__} {self.breed} is jogging.')
        
    • 主程式:

      tree_infos = {'Earth': {'breed': 'ebony', 'age': 2_500},
                    'Venus': {'breed': 'vitex', 'age': -3_000},
                    'Mars': {'breed': 'mvule', 'age': 500_000_000}
      }
      tree_on_earth = EarthlyTree(tree_infos['Earth']['breed'], tree_infos['Earth']['age'])
      tree_on_venus = VenusianTree(tree_infos['Venus']['breed'], tree_infos['Venus']['age'])
      tree_on_mars = MartianTree(tree_infos['Mars']['breed'], tree_infos['Mars']['age'])
      
      trees = {tree_infos['Earth']['breed']: tree_on_earth, tree_infos['Venus']['breed']: tree_on_venus, tree_infos['Mars']['breed']: tree_on_mars}
      
      breed = input('Enter a tree breed: ').strip().lower()  # 執行期間輸入樹種。
      if trees.get(breed) is not None:
          # 如果code有一大堆if/elif或match/case,就得考慮refactor為polymorphism了。
          match breed:
              case 'ebony':         # 這些case's埋下日後維護上的地雷。
                  tree_on_earth.grow()
                  tree_on_earth.reproduce()
                  tree_on_earth.get_sick()
                  tree_on_earth.die()
              case 'vitex':
                  tree_on_venus.grow()
                  tree_on_venus.reproduce()
                  tree_on_venus.get_sick()
                  tree_on_venus.die()
              case 'mvule':
                  tree_on_mars.grow()
                  tree_on_mars.reproduce()
                  tree_on_mars.get_sick()
                  tree_on_mars.die()
      else:
          print('Oops, this tree is not in our list.  Maybe you misspelled it?')    
      
  • 程式執行無誤,暫時相安無事。不過好景不長,不久後必須新增一個「太陽樹類別」。於是程式改寫如下:

    • 類別:

      class EarthlyTree():    # 地球上的樹
          ...     # 實作略
      class VenusianTree():   # 金星樹
          ...     # 實作略
      class MartianTree():    # 火星樹
          ...     # 實作略
      class SolarTree():      # 新增的太陽樹
          ...     # 實作略
      
    • 主程式:

      tree_infos = {'Earth': {'breed': 'ebony', 'age': 2_500},
                    'Venus': {'breed': 'vitex', 'age': -3_000},
                    'Mars': {'breed': 'mvule', 'age': 500_000_000},
                    'Sun': {'breed': 'salix', 'age': 0}
                    }
      
      tree_on_earth = EarthlyTree(tree_infos['Earth']['breed'], tree_infos['Earth']['age'])
      tree_on_venus = VenusianTree(tree_infos['Venus']['breed'], tree_infos['Venus']['age'])
      tree_on_mars = MartianTree(tree_infos['Mars']['breed'], tree_infos['Mars']['age'])
      tree_on_sun = SolarTree(tree_infos['Sun']['breed'], tree_infos['Sun']['age'])
      
      trees = {tree_infos['Earth']['breed']: tree_on_earth,
               tree_infos['Venus']['breed']: tree_on_venus,
               tree_infos['Mars']['breed']: tree_on_mars,
               tree_infos['Sun']['breed']: tree_on_sun}
      
      breed = input('Enter a tree breed: ').strip().lower()  # 執行期間輸入樹種。
      tree = trees.get(breed)
      if tree is not None:
          # 如果code有一大堆if/elif或match/case,就得考慮refactor為polymorphism了。
          match breed:  
              case 'ebony':    # 這些case's埋下日後維護的地雷。
                  tree_on_earth.grow()
                  tree_on_earth.reproduce()
                  tree_on_earth.get_sick()
                  tree_on_earth.die()
              case 'vitex':
                  tree_on_venus.grow()
                  tree_on_venus.reproduce()
                  tree_on_venus.get_sick()
                  tree_on_venus.die()
              case 'mvule':
                  tree_on_mars.grow()
                  tree_on_mars.reproduce()
                  tree_on_mars.get_sick()
                  tree_on_mars.die()
              case 'salix':    # 這裡要加一個case來判斷是否為太陽樹。
                  tree_on_sun.grow()
                  tree_on_sun.reproduce()
                  tree_on_sun.get_sick()
                  tree_on_sun.die()
      else:
          print('Oops, this tree is not in our list.  Maybe you misspelled it?')
      
  • 程式任何要判斷樹種的地方,都要加一個case 'salix':,少改一個地方bugs就來了。更麻煩的是:有些bugs可不會立即出現,也許潛伏很長一段時間才爆發。筆者以前曾開發過一個系統,其中有一個bug隱藏了14年(沒寫錯,是14年)才發現。而且這個還不是潛水最久的bug呢。

  • 這時該考慮將原系統重構(refactor)為多型機制了。方法就是昨天講的那招:增加一個抽樣類別作為介面,其他類別繼承自這個抽象類別並覆寫(override)其方法。說白就是將昨天的版本加一個太陽樹類別而已。由於主程式本來就沒有match/case,商業邏輯改動幅度相對很小或壓根不必改。

  • 重構後的完整程式如下:

    • 類別:

      from abc import ABC, abstractmethod
      
      class Tree(ABC):   # abstract class
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @abstractmethod
          def grow(self):        # 生長(樹的共同行為)
              ...
          @abstractmethod        # 繁殖(樹的共同行為)
          def reproduce(self):
              ...
          @abstractmethod        # 生病(樹的共同行為)
          def get_sick(self):
              ...
          @abstractmethod        # 死亡(樹的共同行為)
          def die(self):
              ...
      
      class EarthlyTree(Tree):   # 地球上的樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} is growing.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} is reproducing.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} is getting sick.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} is dying.')
      
      class VenusianTree(Tree):   # 金星樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} grows younger and younger.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} reproduces on a daily basis.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} always revovers from illness.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} raises itself from the dead.')
      
      class MartianTree(Tree):   # 火星樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} stops growing.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} does not reproduce.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} is healthy.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} is immortal.')
          def jog(self):          # 慢跑是火星樹的獨特行為。
              print(f'{__class__.__name__} {self.breed} is jogging.')
      
      class SolarTree(Tree):     # 太陽樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} grows for a billion years.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} reproduces nuclear power.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} never gets sick.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} will die in 7.5 billion years.')
          def nuclear_fuse(self):          # 核融合(聚變)是太陽樹的獨特行為。
              print(f'{__class__.__name__} {self.breed} operates nuclear fusion.')
      
    • 主程式:

      tree_infos = {'Earth': {'breed': 'ebony', 'age': 2_500},
                    'Venus': {'breed': 'vitex', 'age': -3_000},
                    'Mars': {'breed': 'mvule', 'age': 500_000_000},
                    'Sun': {'breed': 'salix', 'age': 0}
                    }
      
      tree_on_earth = EarthlyTree(tree_infos['Earth']['breed'], tree_infos['Earth']['age'])
      tree_on_venus = VenusianTree(tree_infos['Venus']['breed'], tree_infos['Venus']['age'])
      tree_on_mars = MartianTree(tree_infos['Mars']['breed'], tree_infos['Mars']['age'])
      tree_on_sun = SolarTree(tree_infos['Sun']['breed'], tree_infos['Sun']['age'])
      
      trees = {tree_infos['Earth']['breed']: tree_on_earth,
               tree_infos['Venus']['breed']: tree_on_venus,
               tree_infos['Mars']['breed']: tree_on_mars,
               tree_infos['Sun']['breed']: tree_on_sun}
      
      breed = input('Enter a tree breed: ').strip().lower()
      tree = trees.get(breed)
      if tree is not None:
          tree.grow()
          tree.reproduce()
          tree.get_sick()
          tree.die()
      else:
          print('Oops, this tree is not in our list.  Maybe you misspelled it?')
      
  • 試輸入剛才新增的太陽樹,結果成功呼叫到太陽樹的諸方法:
    https://ithelp.ithome.com.tw/upload/images/20221015/20148485f1JjPE6P0p.png

進一步重構:將資料部分抽出

  • 以上程式將資料和商業邏輯放在同一檔案,這是不好的。現在我們來切割一下,將資料部分抽出,重組成多個.py檔:

    • 設定檔(tree_settings.py)

      # tree_settings.py
      import tree_classes
      
      tree_infos = {'Earth': {'breed': 'ebony', 'age': 2_500},
                    'Venus': {'breed': 'vitex', 'age': -3_000},
                    'Mars': {'breed': 'mvule', 'age': 500_000_000},
                    'Sun': {'breed': 'salix', 'age': 0}
                    }
      
      tree_on_earth = tree_classes.EarthlyTree(tree_infos['Earth']['breed'], tree_infos['Earth']['age'])
      tree_on_venus = tree_classes.VenusianTree(tree_infos['Venus']['breed'], tree_infos['Venus']['age'])
      tree_on_mars = tree_classes.MartianTree(tree_infos['Mars']['breed'], tree_infos['Mars']['age'])
      tree_on_sun = tree_classes.SolarTree(tree_infos['Sun']['breed'], tree_infos['Sun']['age'])
      
      trees = {tree_infos['Earth']['breed']: tree_on_earth,
               tree_infos['Venus']['breed']: tree_on_venus,
               tree_infos['Mars']['breed']: tree_on_mars,
               tree_infos['Sun']['breed']: tree_on_sun}
      
    • 類別檔(tree_classes.py)
      為講解方便,目時將所有類別放在同一個檔案。其實拆分開一個類別一個檔案可能更好,方便個別管理維護。

      # tree_classes.py   # 以後再分拆成一個類別一個.py檔。
      from abc import ABC, abstractmethod
      
      class Tree(ABC):   # abstract class
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
          @property
          def breed(self) -> str:
              return self.__breed
          @abstractmethod
          def grow(self):        # 生長(樹的共同行為)
              ...
          @abstractmethod        # 繁殖(樹的共同行為)
          def reproduce(self):
              ...
          @abstractmethod        # 生病(樹的共同行為)
          def get_sick(self):
              ...
          @abstractmethod        # 死亡(樹的共同行為)
          def die(self):
              ...
      
      class EarthlyTree(Tree):   # 地球上的樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} is growing.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} is reproducing.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} is getting sick.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} is dying.')
      
      class VenusianTree(Tree):   # 金星樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} grows younger and younger.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} reproduces on a daily basis.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} always revovers from illness.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} raises itself from the dead.')
      
      class MartianTree(Tree):   # 火星樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} stops growing.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} does not reproduce.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} is healthy.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} is immortal.')
          def jog(self):          # 慢跑是火星樹的獨特行為。
              print(f'{__class__.__name__} {self.breed} is jogging.')
      
      class SolarTree(Tree):     # 太陽樹
          def __init__(self, breed: str, age: int):   # constructor
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          @property
          def breed(self) -> str:
              return self.__breed
          @breed.setter
          def breed(self, breed: str):
              is_valid_breed = True   # 判斷條件略。
              if is_valid_breed:
                  self.__breed = breed
              else:
                  raise Exception('Invalid breed.')
      
          @property
          def age(self) -> int:
              return self.__age
          @age.setter
          def age(self, age: int):
              is_valid_age = True   # 判斷條件略。
              if is_valid_age:
                  self.__age = age
              else:
                  raise Exception('Invalid age.')
      
          def grow(self):        # 生長。
              print(f'{__class__.__name__} {self.breed} grows for a billion years.')
          def reproduce(self):   # 繁殖。
              print(f'{__class__.__name__} {self.breed} reproduces nuclear power.')
          def get_sick(self):     # 得病。
              print(f'{__class__.__name__} {self.breed} never gets sick.')
          def die(self):          # 死亡(假設死亡也有「行為」)。
              print(f'{__class__.__name__} {self.breed} will die in 7.5 billion years.')
          def nuclear_fuse(self):          # 核融合(聚變)是太陽樹的獨特行為。
              print(f'{__class__.__name__} {self.breed} operates nuclear fusion.')
      
    • 主程式(poly_trees.py)

      # poly_trees.py
      from tree_settings import trees
      
      breed = input('Enter a tree breed: ').strip().lower()
      tree = trees.get(breed)
      if tree is not None:
          tree.grow()
          tree.reproduce()
          tree.get_sick()
          tree.die()
      else:
          print('Oops, this tree is not in our list.  Maybe you misspelled it?')
      
  • 主程式變得非常簡潔,基本上新增刪除類別,主程式都不必更動。只要修改設定檔和類別檔就行。

  • 如果進一步將設定檔改寫,存到諸如Excel或CSV等外部檔案,或存入資料庫,主程式再讀取這個外部檔案或從資料庫取得資料,程式將更加保險。終端使用者(end users)完全碰不到任何的Python code,資料有修改就改Excel或CSV檔。要是再寫一個user friendly的好用介面給終端使用者操作資料的修改新增刪除,那就更加理想。


總結

在此對本系列文章給個總結及摘要。

  • 物件導向(Object-Oriented)是一種paradigm,屬於「戰略層次」的思維模式。
  • 物件導向三大支柱(註1):
    • 封裝(Encapsulation)
    • 繼承(Inheritance)
    • 多型(Polymorphism)
  • 封裝(Encapsulation):
    • 將一些模擬現實世界的「東西」和如何使用這些東西的行為封在一個稱為類別(class)的神秘房子內。封在房子內家具雜物就是變數(variables),使用這些家具雜物的行為動作(例如打掃、整理、丟棄...等)則是函數(functions)。
    • 物件導向的術語是:類別內的變數叫做「屬性」(attributes),函數改口稱為「方法」(methods)。
    • 剛剛將類別說成房子,這個比喻其實不夠精準。正確講法是:類別是房子的「藍圖」,某個社區依此藍圖而建的所有房屋通稱為「物件」(objects),如果指明該社區某街某號的那棟特定房子,則是「實例」(instance)。不過實務上物件和實例經常混用,並不嚴格區分。
    • 封裝有三個P's,即三個「保護層級」,屬性和方法都得遵守:
      • 公開(public)
      • 保護(protected)
      • 私有(private)
    • Python在protected這層只是大家的默契和慣例,並未在語法層次真正支援,筆者大多跳過不提。
    • 即使是私有,Python也有道「後門」(註2)可以「繞過」。其中原委有此一說:仁慈獨裁者Guido van Rossum認為programmers都是「互有默契願意順從規範的大人」,沒有必要將語言定得太過僵化。這個論調筆者並不同意。
    • 一般原則是:屬性盡量設為私有,方法則大多公開。
    • Attribute設為私有後,外界即無法存取。這時又出現一個名為property的機制。Property中文通常亦譯作「屬性」,為免和attribute混淆,筆者直接用英文。
    • 對類別使用者,property就和attribute一樣,可直接以物件.屬性表示式存取。基本上類別使用者對他寫的物件.屬性中的屬性究竟是attribute還是property,幾乎無感。
    • Property外表貌似變數骨子裡卻是方法。因為是方法,就可以在裡面寫code作任何邏輯判斷或檢誤。而attribute只是變數,只能被動「受值」,在受值前無法檢誤。
    • Python以@property裝飾器實作property機制。
    • 有一個@dataclass裝飾器,是Python用來減輕碼農重複撰寫類別內boilerplate code的工具。
    • 除保護層級外,屬性和方法的另一個分類的向度是「類別等級vs實例等級」(class level vs instance level)。交叉下來就有類別屬性(class attributes)、類別方法(class methods)、實例屬性(instance attributes)和實例方法(instance methods)四種。它們都得遵從上面講的保護層級(公開、私有)原則。
    • PEP 8規範:實例方法的第一個參數用self,而類別方法的第一個參數則請用cls
    • Python的方法,除了類別方法和實例方法外,還有第三種「靜態方法」(static methods)。
  • 繼承(Inheritance):
    • 繼承的目的之一是「程式碼重用」。
    • 繼承也是為多型作準備的重要機制,沒有繼承就沒有多型
    • 只有一個父類別的是單一繼承,多個父類別者則為多重繼承。
    • 有些物件導向程式語言並不支援多重繼承,例如Java和C#就不行(註3)。有些語言則可以,如C++。Python也支援多重繼承。
    • 多重繼承威力當然比單一繼承強大,不過有時會造成混亂(gets messy),建議小心使用。如果沒有把握駕馭,設計繼承系統時父類別最好不要太多,繼承的垂直層次也不要太深。否則日後可能陷入難以控制的局面。
    • 覆寫(overriding)是繼承機制的重點。假如子類別完全不覆寫父類別,繼承意義就不大。覆寫通常也是多型的實現門徑
    • 抽象方法(abstract methods)是「只有皮沒有餡」的方法,作用是定義一組API,規範子類別必須遵守。
    • 繼承和組合(composition)的「君子之爭」由來已久。筆者認為兩者各領風騷,當視需要而用。
  • 多型(Polymorphism):
    • 很多人說多型是物件導向的終極目標
    • 多型分為編譯時期(靜態)和執行時期(動態)兩種,以下講的全是動態多型。但是Python可否沿用這個分類架構?存疑。
    • 在一些靜態型別語言(statically typed languages)中,物件可以有兩種型別:「形式型別」(reference type)和「實際型別」(value type)。run-time時執行的一定是物件實際型(類)別所提供的方法,不會執行到形式型別(通常是其父類別)提供的方法。不過筆者認為此「形實之別」應該不適用於Python這類動態型別語言(dynamically typed languages)。
    • 多型要達成的效果是:在程式執行時期,依不同物件去執行不同方法
    • 多型的基礎是繼承,通常利用覆寫(overriding)達成
    • 多型可讓子類別有一致的介面,易於日後擴充及維護
    • 程式如用了大量的多重if/elifmatch/case來判斷物件,很可能是設計不良的徵候。可考慮利用多型技術加以重構(refactor)。
    • 多型好些細節和疑問筆者未能參透,無法提綱挈領、一語破的,說不定還錯漏連篇。這是筆者個人能力不足,舛誤處懇請方家指正。
  • 對物件導向程式設計這個大主題,很多地方本系列文章都沒有涉及。沒提到的大致有:
    • 萃取/摘要(Abstraction):很多人認為Abstraction是物件導向的另一支柱。不過相對於其他三大支柱,Abstraction的重要性似乎較低(筆者主觀認定),而且不一定是物件導向獨有的特性,故略而不談。
    • 介面(Interface):Python支援多重繼承,所以沒有介面這個機制。
    • 委派(Delegation):暫無時間了解Python的Delegation。日後再學。
    • 所有的Design Patterns:這是個更大而且筆者更沒把握的主題(GoF book可是天書耶)。等更有長進時再挑戰吧。
    • And counting...

心得報告

  • 借著這次參賽,筆者學到很多新「物件」及新「方法」,同時釐清和實證了一推平常模糊不清的觀念。
  • 學然後知不足。不足之處筆者已默記在心,日後定必找時間好好研究,希望能有吋進,甚而更上層樓。
  • 參與鐵人賽是訓練意志力和自我成長的最佳途徑。感謝主辦單位給我這個學習機會

互勉

最後用以下對聯(註4)和大家互勉:

當從實地建基柱,莫在浮沙築高樓。


莎翁名句:

"Though this be madness, yet there is method in’t." (William Shakespeare, Hamlet, Act 2, Scene 2)

瘋而有「。所以請放心,總找得到「方法」的。


註1: 近來很多人在原三大支柱前加上Abstraction作為另一支柱。本系列文則採三支柱論。

註2: 這裡的「後門」是指利用_<class>__<attribute>方式,在類別外部以物件.屬性表示式存取私有屬性。詳見本系列Day 8《在外部修改私有屬性,真行嗎?》

註3: Java等語言以「介面」(Interface)機制來替代多重繼承。

註4: 本聯下聯出自曾寫過《多型與虛擬》一書的侯捷老師,上聯則為筆者狗「頭」續貂。


上一篇
Polymorphism in Python
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言