日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Python中的SOLID原則實例詳解_python

作者:海擁? ? 更新時間: 2023-05-10 編程語言

前言

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

欄目分類
最近更新