# 一些程式碼的壞味道 (Bad Smells) ## 類別或方法過長 ### [Long Method](https://refactoring.guru/smells/long-method) ![](https://i.imgur.com/9XA5o3l.png) * 簡介: * 有個類的方法裡面邏輯很多/很雜亂/很長。 * 我們可能會想要盡量將邏輯拆分成多個專一的小邏輯,因為: * 如果可以細看到大房子如何以每個負責不同功能的小積木組成,將有助於理解大房子的架構。 * 小積木也比較好去個別理解/修改/除錯。 * 缺點: * 阻礙理解。 * 維護困難。 * 修改不易。 * 範例: Before: ```python= import cv2 import numpy as np class ImageRegistration: def __init__( self, fixed_image: np.ndarray, moving_image: np.ndarray ): self.fixed_image = fixed_image self.moving_image = moving_image def register_images(self): # Convert images to grayscale fixed_gray = cv2.cvtColor( self.fixed_image, cv2.COLOR_BGR2GRAY ) moving_gray = cv2.cvtColor( self.moving_image, cv2.COLOR_BGR2GRAY ) # Detect ORB features and compute descriptors orb = cv2.ORB_create() fixed_keypoints, fixed_descriptors = orb.detectAndCompute( fixed_gray, None ) moving_keypoints, moving_descriptors = orb.detectAndCompute( moving_gray, None ) # Match descriptors using Brute-Force Matcher bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) matches = bf.match(fixed_descriptors, moving_descriptors) matches = sorted(matches, key=lambda x: x.distance) # Find the homography matrix src_pts = np.float32([ fixed_keypoints[m.queryIdx].pt for m in matches ]).reshape(-1, 1, 2) dst_pts = np.float32( [moving_keypoints[m.trainIdx].pt for m in matches ]).reshape(-1, 1, 2) homography_matrix, _ = cv2.findHomography( dst_pts, src_pts, cv2.RANSAC, 5.0 ) # Warp the moving image to align with the fixed image (height, width) = self.fixed_image.shape[:2] registered_image = cv2.warpPerspective( self.moving_image, homography_matrix, (width, height) ) return registered_image ``` After: * 將 `register_images` 裡面的 sub-tasks 抽成獨立的方法。 * 較能夠一眼看出 `register_images` 的行為。 ```python= class ImageRegistration: def __init__( self, fixed_image: np.ndarray, moving_image: np.ndarray ): self.fixed_image = fixed_image self.moving_image = moving_image def register_images(self): fixed_gray = self.to_gray(self.fixed_image) moving_gray = self.to_gray(self.moving_image) fixed_keypoints, fixed_descriptors = self.detect_features( fixed_gray ) moving_keypoints, moving_descriptors = self.detect_features( moving_gray ) matches = self.match_descriptors( fixed_descriptors, moving_descriptors ) homography_matrix, _ = self.find_homography( fixed_keypoints, moving_keypoints, matches ) (height, width) = self.fixed_image.shape[:2] registered_image = self.warp_image( self.moving_image, homography_matrix, width, height ) return registered_image def to_gray(self, image: np.ndarray): return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) def detect_features(self, gray_image: np.ndarray): orb = cv2.ORB_create() keypoints, descriptors = orb.detectAndCompute(gray_image, None) return keypoints, descriptors def match_descriptors( self, fixed_descriptors: np.ndarray, moving_descriptors: np.ndarray ): bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True) matches = bf.match(fixed_descriptors, moving_descriptors) return sorted(matches, key=lambda x: x.distance) def find_homography( self, fixed_keypoints, moving_keypoints, matches ): src_pts = np.float32([ fixed_keypoints[m.queryIdx].pt for m in matches ]).reshape(-1, 1, 2) dst_pts = np.float32([ moving_keypoints[m.trainIdx].pt for m in matches ]).reshape(-1, 1, 2) return cv2.findHomography( dst_pts, src_pts, cv2.RANSAC, 5.0 ) def warp_image( self, image: np.ndarray, homography_matrix: np.ndarray, width: int, height: int ): return cv2.warpPerspective( image, homography_matrix, (width, height) ) ``` --- ## 物件導向濫用 ### [Switch Statements](https://refactoring.guru/smells/switch-statements) - [ ] Done ![](https://i.imgur.com/98Svh3O.png) * 簡介: * 冗長的 `switch` 或 `if-else` 判斷式 * 建議用 `polymorphism` 代替 `switch` 或 `if-else`語法 * 缺點: * 可讀性降低、維護困難。 * 範例: Before: ```python= def print_animal_sound(animal): if animal == "dog": print("Woof!") elif animal == "cat": print("Meow!") elif animal == "cow": print("Moo!") else: print("Unknown animal") print_animal_sound("dog") print_animal_sound("cat") print_animal_sound("cow") ``` After: ```python= class Animal: def print_sound(self): pass class Dog(Animal): def print_sound(self): print("Woof!") class Cat(Animal): def print_sound(self): print("Meow!") class Cow(Animal): def print_sound(self): print("Moo!") def print_animal_sound(animal): if isinstance(animal, Animal): animal.print_sound() else: print("Unknown animal") dog = Dog() cat = Cat() cow = Cow() print_animal_sound(dog) print_animal_sound(cat) print_animal_sound(cow) ``` ## 防止改動 ### [shutgun surgery](https://refactoring.guru/smells/shotgun-surgery) ![](https://i.imgur.com/BhvnBfK.png) * 範例: Before: ```python= class Order: def __init__(self, total_price): self.total_price = total_price def apply_discount(self): if self.total_price > 1000: self.total_price *= 0.9 class Customer: def __init__(self, age): self.age = age def apply_discount(self, order): if self.age > 65: order.total_price *= 0.85 # 可以根據訂單價格,或是客戶等級,來決定如何打折。 ``` After: ```python= class Discount: @staticmethod def apply_order_discount(total_price): if total_price > 1000: return total_price * 0.9 return total_price @staticmethod def apply_customer_discount(age, total_price): if age > 65: return total_price * 0.85 return total_price class Order: def __init__(self, total_price): self.total_price = total_price def apply_discount(self): self.total_price = Discount.apply_order_discount(self.total_price) class Customer: def __init__(self, age): self.age = age def apply_discount(self, order): order.total_price = Discount.apply_customer_discount(self.age, order.total_price) ``` * 心得: * 因為打折邏輯可能會一直變動。因此乾脆把容易更動的打折邏輯聚集,到`打折`類。如此,其他類就可以減少被改動的機會。 ## 過分耦合 過度耦合的跡象: 牽一髮動全身; 改來改去改來改去。==What the F...? 為何改了`客戶類`還不夠,其他像是`訂單類`,`工廠類`,明明感覺是分開的東西,卻還得隨同一起改動?== ### [Feature Envy](https://refactoring.guru/smells/feature-envy) ![](https://i.imgur.com/TRZp06c.png) * 範例: Before: ```python= class Customer: def __init__(self, first_name, last_name, age): self.first_name = first_name self.last_name = last_name self.age = age def full_name(self): return f"{self.first_name} {self.last_name}" class Order: def __init__(self, customer, total_amount): self.customer = customer self.total_amount = total_amount def get_customer_age(self): return self.customer.age def get_customer_full_name(self): return self.customer.full_name() def is_eligible_for_discount(self): return self.get_customer_age() > 60 # 要看客戶的某特殊特性,以決定訂單打折與否 # 因為是設計在客戶自身的打折條件,可以把此方法改放置於客戶類別,而非訂單類別。 ``` After: ```python= class Customer: def __init__(self, first_name, last_name, age): self.first_name = first_name self.last_name = last_name self.age = age def full_name(self): return f"{self.first_name} {self.last_name}" def is_eligible_for_discount(self): return self.age > 60 class Order: def __init__(self, customer, total_amount): self.customer = customer self.total_amount = total_amount def is_eligible_for_discount(self): return self.customer.is_eligible_for_discount() # 訂單類調用客戶類,請客戶類告知該客戶是否要打折。 # 未來打折邏輯若是改變,訂單類將不需要改動。因為訂單類不需要知道打折的實作細節。 ``` * 心得: 若打折的方法改為: 參考客戶是否為VIP, 是的話,打折。則要同時修改客戶類和訂單類(客戶追加是否VIP, 訂單類追加檢查客戶是否為VIP)。但經過重構,程式碼去耦合後,便只需要修改客戶類即可。 * 簡介 ==某類的方法中,常常拿他類的方法/資料去做事情。該類覬覦其他類別的功能或資料,將其他類的方法或資料,加以整合成為自己內部的一部分。== * 缺點 缺乏維護性。改動某一小功能時,被迫要於多個類裡面去做改動,而不是只要在單一類別內改動就好。 ### [**Message Chains**](https://refactoring.guru/smells/message-chains) ![](https://i.imgur.com/OWL2SZR.png) * 範例 Before: ```python= class Wheel: def __init__(self, diameter): self.diameter = diameter def get_diameter(self): return self.diameter class Bicycle: def __init__(self, wheel): self.wheel = wheel def get_wheel(self): return self.wheel class Cyclist: def __init__(self, bicycle): self.bicycle = bicycle def get_bicycle(self): return self.bicycle class RaceOrganizer: def __init__(self, racer): self.racer = racer def get_wheel_diameter(self): return self.racer.get_bicycle().get_wheel().get_diameter() # 寫了太多不必要細節。 比賽主辦者應該只需要請騎士給予車輪的半徑。 # 不需要請騎士叫他的特定車子(腳踏車),去取得輪子,然後請該輪子提供半徑。 # 這樣寫的話,未來這個類被改動的機會很大。因為哪天如果換成 # 汽車比賽,飛行器比賽等,這邊的程式碼就要改動了: # self.racer.get_car().get_wheel().get_diameter() # self.racer.get_flying_object().get_landing_wheel().get_diameter() ``` After: ```python= class Wheel: def __init__(self, diameter): self.diameter = diameter def get_diameter(self): return self.diameter # 我是輪子,我有自己輪胎直徑的資訊 class Bicycle: def __init__(self, wheel): self.wheel = wheel def get_wheel_diameter(self): return self.wheel.get_diameter() # 這個腳踏車可去檢查自己的輪子,藉此取得輪胎直徑 class Cyclist: def __init__(self, bicycle): self.bicycle = bicycle def get_wheel_diameter(self): return self.bicycle.get_wheel_diameter() # 這個腳踏車騎士可從他的腳踏車去找輪胎直徑 class RaceOrganizer: def __init__(self, racer): self.racer = racer def get_wheel_diameter(self): return self.racer.get_wheel_diameter() # 比賽主辦方可以問比賽者要輪子直徑資訊,但不需要知道 # 或是不需要指定比賽者怎麼去取得該資訊的 # (不用知道比賽者是使用什麼交通工具/ 交通工具是否有輪子能夠被測量出直徑) ``` * 謎之聲 ==A類調用B類,B類又去調用C類...。很明顯的,A類不去**直接**使用C,而是**間接**的去使用C。 試問,A類會不會只要能夠調用B即可。A真的有間接去使用C的必要嗎?== * 缺點 不好復用。未來改動時,很多程式碼可能會需要重寫。 ## 垃圾太多 ### Comments (過多的註解) * 範例 Before ```python= def find_largest(numbers): # Create a variable to store the largest number largest_number = None # Iterate through the list of numbers for number in numbers: # Check if the current number is greater than the largest number if largest_number is None or number > largest_number: # Update the largest number largest_number = number # Return the largest number return largest_number # Create a list of numbers numbers = [5, 3, 8, 1, 10, 7] # Call the find_largest function with the list of numbers result = find_largest(numbers) # Print the largest number print("The largest number is:", result) ``` After: ```python= def find_largest(numbers): largest_number = None for number in numbers: if largest_number is None or number > largest_number: largest_number = number return largest_number numbers = [5, 3, 8, 1, 10, 7] result = find_largest(numbers) print("The largest number is:", result) ``` * 謎之聲 註解是好事; 可是如果很多註解,有可能是因為程式碼寫的太恐怖,不好懂,才需要很多註解。