Python中的SOLID原則實例詳解
前言
SOLID 是一組面向對象的設計原則,旨在使代碼更易于維護和靈活。它們是由 Robert “Uncle Bob” Martin 于 2000 年在他的論文 設計原則和設計模式中創(chuàng)造的。SOLID 原則適用于任何面向對象的語言,但在本文中我將重點關注它們在 Python 應用程序中的含義。
我最初以 PHP 為基礎撰寫有關 SOLID 原則的文章,但由于此處的課程可以輕松應用于任何面向對象的語言,我認為我會考慮使用 Python 重新編寫它。如果您只熟悉 PHP 或 Python,那么這將是學習另一面的一個很好的學習資源。
在這里我們還應該注意,Python 并沒有真正的接口系統(tǒng),所以我使用元類來創(chuàng)建所需的情況。有關元類的更多說明,請參閱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 字符串的輸出,或者向類中添加另一種類型的輸出,會發(fā)生什么情況呢?我們需要更改類以添加另一個方法或更改現(xiàn)有方法以適應。這對于像這樣簡單的類來說很好,但如果它包含更多屬性,那么更改格式將更加復雜。
一個更好的方法是修改Page類,這樣它只知道數(shù)據是句柄。然后我們創(chuàng)建一個名為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())
這樣做意味著如果我們想創(chuàng)建一個 XML 格式,我們只需添加一個名為XmlPageFormatter的類并編寫一些簡單的代碼來輸出 XML。我們現(xiàn)在只有一個理由來更改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
我們有一個包含矩形數(shù)據的Rectangle類,以及一個用作Rectangle對象集合的Board類。使用此設置,我們可以通過循環(huán)遍歷rectangles集合屬性中的項目并計算它們的面積來輕松找出板的面積。
此設置的問題在于我們受到可以傳遞給Board類的對象類型的限制。例如,如果我們想將一個Circle對象傳遞給Board類,我們需要編寫條件語句和代碼來檢測和計算Board的面積。
解決這個問題的正確方法是將面積計算代碼移到形狀類中,并讓所有形狀類都擴展一個Shape接口。我們現(xiàn)在可以創(chuàng)建一個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
現(xiàn)在 可以重新設計Board類,使其不關心傳遞給它的形狀類型,只要它們實現(xiàn) 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
我們現(xiàn)在已經設置了這些對象,這意味著如果我們有不同類型的對象,我們不需要改變Board類。我們只是創(chuàng)建實現(xiàn)Shape的對象,并以與其他類相同的方式將其傳遞到集合中。
里氏替換原則
由 Barbara Liskov 在 1987 年創(chuàng)建,它指出對象應該可以被它們的子類型替換而不改變程序的工作方式。換句話說,派生類必須可以替代它們的基類而不會導致錯誤。
下面的代碼定義了一個Rectangle類,我們可以用它來創(chuàng)建和計算矩形的面積。
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。例如,鴨子嘎嘎叫,但橡皮鴨不叫(好吧,也許它會吱吱叫),鴨子是活的,但橡皮鴨不是。
覆蓋類中的大量代碼以適應特定情況可能會導致維護問題。您為覆蓋特定條件而添加的代碼越多,您的代碼就會變得越脆弱。
矩形與正方形情況的一種解決方案是創(chuàng)建一個名為Quadrilateral的接口,并在單獨的Rectangle和Square 類中實現(xiàn)它。在這種情況下,我們允許類負責它們自己的數(shù)據,但強制要求某些方法足跡可用。
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
這里的底線是,如果你發(fā)現(xiàn)你覆蓋了很多代碼,那么你的架構可能是錯誤的,你應該考慮 Liskov 替換原則。
接口隔離原則
這表明許多特定于客戶端的接口優(yōu)于一個通用接口。換句話說,不應強制類實現(xiàn)它們不使用的接口。
讓我們以Worker接口為例。這定義了幾種不同的方法,可以應用于典型開發(fā)機構的工作人員。
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
問題是因為這個接口太通用了,我們不得不在實現(xiàn)這個接口的類中創(chuàng)建方法來適應這個接口。
例如,如果我們創(chuàng)建一個Manager類,那么我們將被迫實現(xiàn)一個 write_code() 方法,因為這是接口所需要的。因為經理通常不編寫代碼,所以我們實際上無法在此方法中執(zhí)行任何操作,因此我們只返回 false。
class Manager(WorkerInterface):
def write_code(self):
pass
此外,如果我們有一個實現(xiàn)Worker的Developer類,那么我們將被迫實現(xiàn)一個 call_client() 方法,因為這是接口所需要的。
class Developer(WorkerInterface):
def call_client(self):
pass
擁有一個臃腫的接口意味著必須實現(xiàn)什么都不做的方法。
正確的解決方案是將我們的界面拆分成單獨的部分,每個部分處理特定的功能。在這里,我們從我們的通用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
有了這個,我們就可以實現(xiàn)我們的子類,而不必編寫我們不需要的代碼。所以我們的Developer和Manager類看起來像這樣。
class Manager(WorkerInterface, ClientFacerInterface):
pass
class Developer(WorkerInterface, CoderInterface):
pass
擁有許多特定接口意味著我們不必編寫代碼來支持接口。
依賴倒置原則
也許是最簡單的原則,它指出類應該依賴于抽象,而不是具體化。本質上,不依賴于具體類,依賴于接口。
以使用MySqlConnection類從數(shù)據庫加載頁面的PageLoader類為例,我們可以創(chuàng)建這些類,以便將連接類傳遞給PageLoader類的構造函數(shù)。
class MySqlConnection():
def connect(self):
pass
class PageLoader():
def __init__(self, mysql_connection: MySqlConnection):
self._mysql_connection = mysql_connection
這種結構意味著我們基本上只能在數(shù)據庫層使用 MySQL。如果我們想將其換成不同的數(shù)據庫適配器會怎樣?我們可以擴展MySqlConnection類以創(chuàng)建到 Memcache 或其他東西的連接,但這會違反 Liskov 替換原則??赡軙褂脗溆脭?shù)據庫管理器來加載頁面,因此我們需要找到一種方法來執(zhí)行此操作。
這里的解決方案是創(chuàng)建一個名為DbConnectionInterface的接口,然后在MySqlConnection類中實現(xiàn)這個接口。然后,我們不再依賴傳遞給PageLoader類的MySqlConnection對象,而是依賴任何實現(xiàn)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
有了這個,我們現(xiàn)在可以創(chuàng)建一個MemcacheConnection類,只要它實現(xiàn)了DbConnectionInterface,我們就可以在PageLoader類中使用它來加載頁面。
這種方法還迫使我們以這樣一種方式編寫代碼,以防止不關心它的類中的特定實現(xiàn)細節(jié)。因為我們已經將MySqlConnection類傳遞給了PageLoader類,所以我們不應該在PageLoader類 中編寫 SQL 查詢。這意味著當我們傳入MemcacheConnection對象時,它的行為方式與任何其他類型的連接類相同。
當考慮接口而不是類時,它迫使我們將特定域代碼移出我們的PageLoader類并移入MySqlConnection類。
如何發(fā)現(xiàn)它?
一個更大的問題可能是,如果您需要將 SOLID 原則應用于您的代碼,或者您正在編寫的代碼不是 SOLID,您如何才能發(fā)現(xiàn)。
了解這些原則只是成功的一半,您還需要知道什么時候應該退后一步并考慮應用 SOLID 原則。我想出了一個快速列表,列出了您需要關注的“告訴”,表明您的代碼可能需要重新編寫。
- 您正在編寫大量“if”語句來處理目標代碼中的不同情況。
- 你寫了很多代碼,實際上并沒有做任何事情只是為了滿足界面設計。
- 你一直打開同一個類來更改代碼。
- 您在與該類沒有任何關系的類中編寫代碼。例如,將 SQL 查詢放在數(shù)據庫連接類之外的類中。
結論
SOLID 不是一種完美的方法,它可能會導致包含許多移動部件的復雜應用程序,并且偶爾會導致編寫代碼以備不時之需。使用 SOLID 意味著編寫更多類并創(chuàng)建更多接口,但許多現(xiàn)代 IDE 將通過自動代碼完成來解決該問題。
也就是說,它確實會迫使您分離關注點、考慮繼承、防止重復代碼并謹慎編寫應用程序。畢竟,考慮對象如何在應用程序中組合在一起是面向對象代碼的全部內容。
到此這篇關于Python中的SOLID原則詳解的文章就介紹到這了,更多相關Python SOLID原則內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
python 實現(xiàn)存儲數(shù)據到txt和pdf文檔及亂碼問題的解決
這篇文章主要介紹了python 實現(xiàn)存儲數(shù)據到txt和pdf文檔及亂碼問題的解決,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-03-03
使用Py2Exe for Python3創(chuàng)建自己的exe程序示例
今天小編就為大家分享一篇使用Py2Exe for Python3創(chuàng)建自己的exe程序示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10

