網站首頁 編程語言 正文
前言
SOLID 是一組面向對象的設計原則,旨在使代碼更易于維護和靈活。它們是由 Robert “Uncle Bob” Martin 于 2000 年在他的論文 設計原則和設計模式中創造的。SOLID 原則適用于任何面向對象的語言,但在本文中我將重點關注它們在 Python 應用程序中的含義。
我最初以 PHP 為基礎撰寫有關 SOLID 原則的文章,但由于此處的課程可以輕松應用于任何面向對象的語言,我認為我會考慮使用 Python 重新編寫它。如果您只熟悉 PHP 或 Python,那么這將是學習另一面的一個很好的學習資源。
在這里我們還應該注意,Python 并沒有真正的接口系統,所以我使用元類來創建所需的情況。有關元類的更多說明,請參閱Python 中面向對象編程入門文章的基礎知識中的接口部分。
SOLID 是一個首字母縮寫詞,代表以下內容:
- 單一職責原則
- 開放/封閉原則
- Liskov替代原則
- 接口隔離原則
- 依賴倒置原則
我們將依次解析它們。
單一職責原則
這表明一個類應該有單一的責任,但更重要的是,一個類應該只有一個改變的理由。
以名為Page的(簡單)類為例。
import json
class Page():
def __init__(self, title):
self._title = title
def get_title(self):
return self._title
def set_title(self, title):
self._title = title
def get_page(self):
return [self._title]
def format_json(self):
return json.dumps(self.get_page())
此類知道 title 屬性并允許通過 get() 方法檢索此 title 屬性。我們還可以使用此類中名為 format_json() 的方法將頁面作為 JSON 字符串返回。這似乎是個好主意,因為類負責自己的格式。
但是,如果我們想要更改 JSON 字符串的輸出,或者向類中添加另一種類型的輸出,會發生什么情況呢?我們需要更改類以添加另一個方法或更改現有方法以適應。這對于像這樣簡單的類來說很好,但如果它包含更多屬性,那么更改格式將更加復雜。
一個更好的方法是修改Page類,這樣它只知道數據是句柄。然后我們創建一個名為JsonPageFormatter的輔助類,用于將Page對象格式化為 JSON。
import json
class Page():
def __init__(self, title):
self._title = title
def get_title(self):
return self._title
def set_title(self, title):
self._title = title
def get_page(self):
return [self._title]
class JsonPageFormatter():
def format_json(page: Page):
return json.dumps(page.get_page())
這樣做意味著如果我們想創建一個 XML 格式,我們只需添加一個名為XmlPageFormatter的類并編寫一些簡單的代碼來輸出 XML。我們現在只有一個理由來更改Page類。
開閉原則
在開閉原則中,類應該 對擴展開放,對修改關閉。本質上意味著類應該被擴展以改變功能,而不是被改變成其他東西。
以下面兩個類為例。
class Rectangle():
def __init__(self, width, height):
self._width = width
self._height = height
def get_width(self):
return self._width
def set_width(self, width):
self._width = width
def get_height(self):
return self._height
def set_height(self, height):
self._height = height
class Board():
@property
def rectangles(self):
return self._rectangles
@rectangles.setter
def rectangles(self, value):
self._rectangles = value
def calculateArea(self):
area = 0
for item in self.rectangles:
area += item.get_height() * item.get_width()
return area
我們有一個包含矩形數據的Rectangle類,以及一個用作Rectangle對象集合的Board類。使用此設置,我們可以通過循環遍歷rectangles集合屬性中的項目并計算它們的面積來輕松找出板的面積。
此設置的問題在于我們受到可以傳遞給Board類的對象類型的限制。例如,如果我們想將一個Circle對象傳遞給Board類,我們需要編寫條件語句和代碼來檢測和計算Board的面積。
解決這個問題的正確方法是將面積計算代碼移到形狀類中,并讓所有形狀類都擴展一個Shape接口。我們現在可以創建一個Rectangle和Circle形狀類,它們將在被要求時計算它們的面積。
import math
class ShapeMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'area') and callable(subclass.area))
class ShapeInterface(metaclass=ShapeMeta):
pass
class Rectangle(ShapeInterface):
def __init__(self, width, height):
self._width = width
self._height = height
def get_width(self):
return self._width
def set_width(self, width):
self._width = width
def get_height(self):
return self._height
def set_height(self, height):
self._height = height
def area(self):
return self.get_width() * self.get_height()
class Circle(ShapeInterface):
def __init__(self, radius):
self._radius = radius
def get_radius(self):
return self._radius
def set_radius(self, radius):
self._radius = radius
def area(self):
return self.get_radius() * self.get_radius() * math.pi
現在 可以重新設計Board類,使其不關心傳遞給它的形狀類型,只要它們實現 area() 方法即可。
class Board():
def __init__(self, shapes):
self._shapes = shapes
def calculateArea(self):
area = 0
for shape in self._shapes:
area += shape.area()
return area
我們現在已經設置了這些對象,這意味著如果我們有不同類型的對象,我們不需要改變Board類。我們只是創建實現Shape的對象,并以與其他類相同的方式將其傳遞到集合中。
里氏替換原則
由 Barbara Liskov 在 1987 年創建,它指出對象應該可以被它們的子類型替換而不改變程序的工作方式。換句話說,派生類必須可以替代它們的基類而不會導致錯誤。
下面的代碼定義了一個Rectangle類,我們可以用它來創建和計算矩形的面積。
class Rectangle():
def __init__(self, width, height):
self._width = width
self._height = height
def get_width(self):
return self._width
def set_width(self, width):
self._width = width
def get_height(self):
return self._height
def set_height(self, height):
self._height = height
def area(self):
return self.get_width() * self.get_height()
使用它,我們可以將其擴展為Square類。因為正方形與矩形略有不同,我們需要重寫一些代碼以允許正方形正確存在。
class Square(Rectangle):
def __init__(self, width):
self._width = width
self._height = width
def get_width(self):
return self._width
def set_width(self, width):
self._width = width
self._height = width
def get_height(self):
return self._height
def set_height(self, height):
self._height = height
self._width = height
這看起來不錯,但最終正方形不是矩形,因此我們添加了代碼來強制這種情況起作用。
我讀過的一個很好的類比是考慮類代表的鴨子和橡皮鴨。盡管可以將 Duck 類擴展為 Rubber Duck 類,但我們需要重寫許多 Duck 功能以適應 Rubber Duck。例如,鴨子嘎嘎叫,但橡皮鴨不叫(好吧,也許它會吱吱叫),鴨子是活的,但橡皮鴨不是。
覆蓋類中的大量代碼以適應特定情況可能會導致維護問題。您為覆蓋特定條件而添加的代碼越多,您的代碼就會變得越脆弱。
矩形與正方形情況的一種解決方案是創建一個名為Quadrilateral的接口,并在單獨的Rectangle和Square 類中實現它。在這種情況下,我們允許類負責它們自己的數據,但強制要求某些方法足跡可用。
class QuadrilateralMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'area') and callable(subclass.area)) \
and (hasattr(subclass, 'get_height') and callable(subclass.get_height)) \
and (hasattr(subclass, 'get_width') and callable(subclass.get_width)) \
class QuadrilateralInterface(metaclass=QuadrilateralMeta):
pass
class Rectangle(QuadrilateralInterface):
pass
class Square(QuadrilateralInterface):
pass
這里的底線是,如果你發現你覆蓋了很多代碼,那么你的架構可能是錯誤的,你應該考慮 Liskov 替換原則。
接口隔離原則
這表明許多特定于客戶端的接口優于一個通用接口。換句話說,不應強制類實現它們不使用的接口。
讓我們以Worker接口為例。這定義了幾種不同的方法,可以應用于典型開發機構的工作人員。
class WorkerMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'take_break') and callable(subclass.take_break)) \
and (hasattr(subclass, 'write_code') and callable(subclass.write_code)) \
and (hasattr(subclass, 'call_client') and callable(subclass.call_client)) \
and (hasattr(subclass, 'get_paid') and callable(subclass.get_paid))
class WorkerInterface(metaclass=WorkerMeta):
pass
問題是因為這個接口太通用了,我們不得不在實現這個接口的類中創建方法來適應這個接口。
例如,如果我們創建一個Manager類,那么我們將被迫實現一個 write_code() 方法,因為這是接口所需要的。因為經理通常不編寫代碼,所以我們實際上無法在此方法中執行任何操作,因此我們只返回 false。
class Manager(WorkerInterface):
def write_code(self):
pass
此外,如果我們有一個實現Worker的Developer類,那么我們將被迫實現一個 call_client() 方法,因為這是接口所需要的。
class Developer(WorkerInterface):
def call_client(self):
pass
擁有一個臃腫的接口意味著必須實現什么都不做的方法。
正確的解決方案是將我們的界面拆分成單獨的部分,每個部分處理特定的功能。在這里,我們從我們的通用Worker接口中分離出Coder和ClientFacer接口。
class WorkerMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'take_break') and callable(subclass.take_break)) \
and (hasattr(subclass, 'get_paid') and callable(subclass.get_paid))
class WorkerInterface(metaclass=WorkerMeta):
pass
class ClientFacerMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'call_client') and callable(subclass.call_client))
class ClientFacerInterface(metaclass=ClientFacerMeta):
pass
class CoderMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'write_code') and callable(subclass.write_code))
class CoderInterface(metaclass=CoderMeta):
pass
有了這個,我們就可以實現我們的子類,而不必編寫我們不需要的代碼。所以我們的Developer和Manager類看起來像這樣。
class Manager(WorkerInterface, ClientFacerInterface):
pass
class Developer(WorkerInterface, CoderInterface):
pass
擁有許多特定接口意味著我們不必編寫代碼來支持接口。
依賴倒置原則
也許是最簡單的原則,它指出類應該依賴于抽象,而不是具體化。本質上,不依賴于具體類,依賴于接口。
以使用MySqlConnection類從數據庫加載頁面的PageLoader類為例,我們可以創建這些類,以便將連接類傳遞給PageLoader類的構造函數。
class MySqlConnection():
def connect(self):
pass
class PageLoader():
def __init__(self, mysql_connection: MySqlConnection):
self._mysql_connection = mysql_connection
這種結構意味著我們基本上只能在數據庫層使用 MySQL。如果我們想將其換成不同的數據庫適配器會怎樣?我們可以擴展MySqlConnection類以創建到 Memcache 或其他東西的連接,但這會違反 Liskov 替換原則。可能會使用備用數據庫管理器來加載頁面,因此我們需要找到一種方法來執行此操作。
這里的解決方案是創建一個名為DbConnectionInterface的接口,然后在MySqlConnection類中實現這個接口。然后,我們不再依賴傳遞給PageLoader類的MySqlConnection對象,而是依賴任何實現DbConnectionInterface接口的類。
class DbConnectionMeta(type):
def __instancecheck__(self, instance):
return self.__subclasscheck__(type(instance))
def __subclasscheck__(self, subclass):
return (hasattr(subclass, 'connect') and callable(subclass.connect))
class DbConnectionInterface(metaclass=DbConnectionMeta):
pass
class MySqlConnection(DbConnectionInterface):
def connect(self):
pass
class PageLoader():
def __init__(self, db_connection: DbConnectionInterface):
self._db_connection = db_connection
有了這個,我們現在可以創建一個MemcacheConnection類,只要它實現了DbConnectionInterface,我們就可以在PageLoader類中使用它來加載頁面。
這種方法還迫使我們以這樣一種方式編寫代碼,以防止不關心它的類中的特定實現細節。因為我們已經將MySqlConnection類傳遞給了PageLoader類,所以我們不應該在PageLoader類 中編寫 SQL 查詢。這意味著當我們傳入MemcacheConnection對象時,它的行為方式與任何其他類型的連接類相同。
當考慮接口而不是類時,它迫使我們將特定域代碼移出我們的PageLoader類并移入MySqlConnection類。
如何發現它?
一個更大的問題可能是,如果您需要將 SOLID 原則應用于您的代碼,或者您正在編寫的代碼不是 SOLID,您如何才能發現。
了解這些原則只是成功的一半,您還需要知道什么時候應該退后一步并考慮應用 SOLID 原則。我想出了一個快速列表,列出了您需要關注的“告訴”,表明您的代碼可能需要重新編寫。
- 您正在編寫大量“if”語句來處理目標代碼中的不同情況。
- 你寫了很多代碼,實際上并沒有做任何事情只是為了滿足界面設計。
- 你一直打開同一個類來更改代碼。
- 您在與該類沒有任何關系的類中編寫代碼。例如,將 SQL 查詢放在數據庫連接類之外的類中。
結論
SOLID 不是一種完美的方法,它可能會導致包含許多移動部件的復雜應用程序,并且偶爾會導致編寫代碼以備不時之需。使用 SOLID 意味著編寫更多類并創建更多接口,但許多現代 IDE 將通過自動代碼完成來解決該問題。
也就是說,它確實會迫使您分離關注點、考慮繼承、防止重復代碼并謹慎編寫應用程序。畢竟,考慮對象如何在應用程序中組合在一起是面向對象代碼的全部內容。
原文鏈接:https://blog.csdn.net/qq_44273429/article/details/128851099
相關推薦
- 2023-02-27 Python獲取"3年前的今天"的日期時間問題_python
- 2023-03-05 Redis緩存工具封裝實現_Redis
- 2023-02-17 Go語言Gin處理響應方式詳解_Golang
- 2022-04-17 aspx頁面報“XPathResult未定義”的解決方法
- 2023-01-14 GoLang逃逸分析講解_Golang
- 2022-09-03 Python流程控制if條件選擇與for循環_python
- 2022-10-15 Qt網絡編程實現TCP通信_C 語言
- 2022-12-08 Anaconda中pkgs文件夾及如何清空PKGS_相關技巧
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支