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