Python 工匠:寫好面向對象代碼的原則(上)

前言

這是 「Python 工匠」系列的第 12 篇文章。[查看系列全部文章]html

Python 是一門支持多種編程風格的語言,面對相同的需求,擁有不一樣背景的程序員可能會寫出風格迥異的 Python 代碼。好比一位習慣編寫 C 語言的程序員,一般會定義一大堆函數來搞定全部事情,這是「過程式編程」的思想。而一位有 Java 背景的程序員則更傾向於設計許多個相互關聯的類*(class)*,這是 「面向對象編程(後簡稱 OOP)」node

雖然不一樣的編程風格各有特色,沒法直接比較。可是 OOP 思想在現代軟件開發中起到的重要做用應該是毋庸置疑的。python

不少人在學習如何寫好 OOP 代碼時,會選擇從那 23 種經典的「設計模式」開始。不過對於 Python 程序員來講,我認爲這並不是是一個最佳選擇。git

Python 對 OOP 的支持

Python 語言雖然擁有類、繼承、多態等核心 OOP 特性,但和那些徹底基於 OOP 思想設計的編程語言*(好比 Java)*相比,它在 OOP 支持方面作了不少簡化工做。好比它 沒有嚴格的類私有成員,沒有接口(Interface)對象 等。程序員

而與此同時,Python 靈活的函數對象、鴨子類型等許多動態特性又讓一些在其餘語言中很難作到的事情變得很是簡單。這些語言間的差別共同致使了一個結果:不少經典的設計模式到了 Python 裏,就丟失了那個「味道」,實用性也大打折扣。github

拿你們最熟悉的單例模式來講。你能夠花上一大把時間,來學習如何在 Python 中利用 __new__ 方法或元類*(metaclass)*來實現單例設計模式,但最後你會發現,本身 95% 的需求均可以經過直接定義一個模塊級全局變量來搞定。算法

因此,與具體化的 設計模式 相比,我以爲一些更爲抽象的 設計原則 適用性更廣、更適合運用到 Python 開發工做中。而談到關於 OOP 的設計原則,「SOLID」 是衆多原則中最有名的一個。編程

SOLID 設計原則

著名的設計模式書籍《設計模式:可複用面向對象軟件的基礎》出版於 1994 年,距今已有超過 25 年的歷史。而這篇文章的主角: 「SOLID 設計原則」一樣也並不年輕。小程序

早在 2000 年,Robert C. Martin 就在他的文章 "Design Principles and Design Patterns" 中整理並提出了 「SOLID」 設計原則的雛型,以後又在他的經典著做《敏捷軟件開發 : 原則、模式與實踐》中將其發揚光大。「SOLID」 由 5 個單詞組合的首字母縮寫組成,分別表明 5 條不一樣的面向對象領域的設計原則。設計模式

在編寫 OOP 代碼時,若是遵循這 5 條設計原則,就更可能寫出可擴展、易於修改的代碼。相反,若是不斷違反其中的一條或多條原則,那麼很快你的代碼就會變得不可擴展、難以維護。

接下來,讓我用一個真實的 Python 代碼樣例來分別向你詮釋這 5 條設計原則。

寫在最前面的注意事項:

  1. 「原則」不是「法律」,它只起到指導做用,並不是不能夠違反
  2. 「原則」的後兩條與接口(Interface)有關,而 Python 沒有接口,因此對這部分的詮釋是個人我的理解,與原版可能略有出入
  3. 文章後面的內容含有大量代碼,請作好心理準備 ☕️
  4. 爲了加強代碼的說明性,本文中的代碼使用了 Python3 中的 類型註解特性

SOLID 原則與 Python

Hacker News(後簡稱 HN) 是一個在程序員圈子裏很受歡迎的站點。在它的首頁,有不少由用戶提交後基於推薦算法排序的科技相關內容。

我常常會去上面看一些熱門文章,但我以爲每次打開瀏覽器訪問有點麻煩。因此,我準備編寫一個腳本,自動抓取 HN 首頁 Top5 的新聞標題與連接,並用純文本的方式寫入到文件。方便本身用其餘工具閱讀。

圖:Hacker News 首頁截圖

編寫爬蟲幾乎是 Python 天生的拿手好戲。利用 requests、lxml 等模塊提供的好用功能,我能夠輕鬆實現上面的需求。下面是我第一次編寫好的代碼:

import io
import sys
from typing import Generator

import requests
from lxml import etree


class Post:
    """HN(https://news.ycombinator.com/) 上的條目 :param title: 標題 :param link: 連接 :param points: 當前得分 :param comments_cnt: 評論數 """
    def __init__(self, title: str, link: str, points: str, comments_cnt: str):
        self.title = title
        self.link = link
        self.points = int(points)
        self.comments_cnt = int(comments_cnt)


class HNTopPostsSpider:
    """抓取 HackerNews Top 內容條目 :param fp: 存儲抓取結果的目標文件對象 :param limit: 限制條目數,默認爲 5 """
    ITEMS_URL = 'https://news.ycombinator.com/'
    FILE_TITLE = 'Top news on HN'

    def __init__(self, fp: io.TextIOBase, limit: int = 5):
        self.fp = fp
        self.limit = limit

    def fetch(self) -> Generator[Post, None, None]:
        """從 HN 抓取 Top 內容 """
        resp = requests.get(self.ITEMS_URL)

        # 使用 XPath 能夠方便的從頁面解析出你須要的內容,如下均爲頁面解析代碼
        # 若是你對 xpath 不熟悉,能夠忽略這些代碼,直接跳到 yield Post() 部分
        html = etree.HTML(resp.text)
        items = html.xpath('//table[@class="itemlist"]/tr[@class="athing"]')
        for item in items[:self.limit]:
            node_title = item.xpath('./td[@class="title"]/a')[0]
            node_detail = item.getnext()
            points_text = node_detail.xpath('.//span[@class="score"]/text()')
            comments_text = node_detail.xpath('.//td/a[last()]/text()')[0]

            yield Post(
                title=node_title.text,
                link=node_title.get('href'),
                # 條目可能會沒有評分
                points=points_text[0].split()[0] if points_text else '0',
                comments_cnt=comments_text.split()[0]
            )

    def write_to_file(self):
        """以純文本格式將 Top 內容寫入文件 """
        self.fp.write(f'# {self.FILE_TITLE}\n\n')
        # enumerate 接收第二個參數,表示從這個數開始計數(默認爲 0)
        for i, post in enumerate(self.fetch(), 1):
            self.fp.write(f'> TOP {i}: {post.title}\n')
            self.fp.write(f'> 分數:{post.points} 評論數:{post.comments_cnt}\n')
            self.fp.write(f'> 地址:{post.link}\n')
            self.fp.write('------\n')


def main():

    # with open('/tmp/hn_top5.txt') as fp:
    # crawler = HNTopPostsSpider(fp)
    # crawler.write_to_file()

    # 由於 HNTopPostsSpider 接收任何 file-like 的對象,因此咱們能夠把 sys.stdout 傳進去
    # 實現往控制檯標準輸出打印的功能
    crawler = HNTopPostsSpider(sys.stdout)
    crawler.write_to_file()


if __name__ == '__main__':
    main()
複製代碼

你能夠把上面的代碼稱之爲符合 OOP 風格的,由於在上面的代碼裏,我定義了兩個類:

  1. Post:表示單個 HN 內容條目,其中定義了標題、連接等字段,是用來銜接「抓取」和「寫入文件」兩件事情的數據類
  2. HNTopPostsSpider:抓取 HN 內容的爬蟲類,其中定義了抓取頁面、解析、寫入結果的方法,是完成主要工做的類

若是你本地的 Python 環境配置正常,那麼能夠嘗試執行一下上面這段代碼,它會輸出下面這樣的內容:

❯ python news_digester.py
> TOP 1: Show HN: NoAgeismInTech – Job board for companies fighting ageism in tech
> 分數:104 評論數:26
> 地址:https://noageismintech.com/
------
> TOP 2: Magic Leap sues former employee who founded the China-based Nreal for IP theft
> 分數:17 評論數:2
> 地址:https://www.bloomberg.com/news/articles/2019-06-18/secretive-magic-leap-says-ex-engineer-copied-headset-for-china
------
... ...
複製代碼

這個腳本基於面向對象的方式編寫*(換句話說,就是定義了一些 class 😒)*,能夠知足個人需求。可是從設計的角度來看,它卻違反了 SOLID 原則的第一條:「Single responsibility principle(單一職責原則)」,讓咱們來看看是爲何。

S:單一職責原則

SOLID 設計原則裏的第一個字母 S 來自於 「Single responsibility principle(單一職責原則)」 的首字母。這個原則認爲:**「一個類應該僅僅只有一個被修改的理由。」**換句話說,每一個類都應該只有一種職責。

而在上面的代碼中,HNTopPostsSpider 這個類違反了這個原則。由於咱們能夠很容易的找到兩個不一樣的修改它的理由:

  • 理由 1: HN 網站的程序員忽然更新了頁面樣式,舊的 xpath 解析算法重新頁面上解析不到內容,須要修改 fetch 方法內的解析邏輯。
  • 理由 2: 用戶*(也就是我)*忽然以爲純文本格式的輸出很差看,想要改爲 Markdown 樣式。須要修改 write_to_file 方法內的輸出邏輯。

因此,HNTopPostsSpider 類違反了「單一職責原則」,由於它有着多個被修改的理由。而這背後的根本緣由是由於它承擔着 「抓取帖子列表」 和 "將帖子列表寫入文件" 這兩種徹底不一樣的職責。

違反「單一職責原則」的壞處

若是某個類違反了「單一職責原則」,那意味着咱們常常會由於不一樣的緣由去修改它。這可能會致使不一樣功能之間相互影響。好比,可能我在某天調整了頁面解析邏輯,卻發現輸出的文件格式也所有亂掉了。

另外,單個類承擔的職責越多,意味着這個類的複雜度也就越高,它的維護成本也一樣會水漲船高。違反「單一職責原則」的類一樣也難以被複用,假如我有其餘代碼想複用 HNTopPostsSpider 類的抓取和解析邏輯,會發現我必需要提供一個莫名其妙的文件對象給它才行。

那麼,要如何修改代碼才能讓它遵循「單一職責原則」呢?辦法有不少,最傳統的是:把大類拆分爲小類

拆分大類爲多個小類

爲了讓 HNTopPostsSpider 類的職責更純粹,咱們能夠把其中與「寫入文件」相關的內容拆分出去做爲一個新的類:

class PostsWriter:
    """負責將帖子列表寫入到文件 """
    def __init__(self, fp: io.TextIOBase, title: str):
        self.fp = fp
        self.title = title

    def write(self, posts: List[Post]):
        self.fp.write(f'# {self.title}\n\n')
        # enumerate 接收第二個參數,表示從這個數開始計數(默認爲 0)
        for i, post in enumerate(posts, 1):
            self.fp.write(f'> TOP {i}: {post.title}\n')
            self.fp.write(f'> 分數:{post.points} 評論數:{post.comments_cnt}\n')
            self.fp.write(f'> 地址:{post.link}\n')
            self.fp.write('------\n')
複製代碼

而在 HNTopPostsSpider 類裏,能夠經過調用 PostsWriter 的方式來完成以前的工做:

class HNTopPostsSpider:
    FILE_TITLE = 'Top news on HN'
    
    <... 已省略 ...>

    def write_to_file(self, fp: io.TextIOBase):
        """以純文本格式將 Top 內容寫入文件 實例化參數文件對象 fp 被挪到了 write_to_file 方法中 """
        # 將文件寫入邏輯託管給 PostsWriter 類處理
        writer = PostsWriter(fp, title=self.FILE_TITLE)
        writer.write(list(self.fetch()))
複製代碼

經過這種方式,咱們讓 HNTopPostsSpiderPostsWriter 類都各自知足了「單一職責原則」。我只會由於解析邏輯變更纔去修改 HNTopPostsSpider 類,一樣,修改 PostsWriter 類的緣由也只有調整輸出格式一種。這兩個類各自的修改能夠單獨進行而不會相互影響。

另外一種方案:使用函數

「單一職責原則」雖然是針對類說的,但其實它的適用範圍能夠超出類自己。好比在 Python 中,經過定義函數,一樣也可讓上面的代碼符合單一職責原則。

咱們能夠把「寫入文件」的邏輯拆分爲一個新的函數,由它來專門承擔起將帖子列表寫入文件的職責:

def write_posts_to_file(posts: List[Post], fp: io.TextIOBase, title: str):
    """負責將帖子列表寫入文件 """
    fp.write(f'# {title}\n\n')
    for i, post in enumerate(posts, 1):
        fp.write(f'> TOP {i}: {post.title}\n')
        fp.write(f'> 分數:{post.points} 評論數:{post.comments_cnt}\n')
        fp.write(f'> 地址:{post.link}\n')
        fp.write('------\n')
複製代碼

而對於 HNTopPostsSpider 類來講,改動能夠更進一步。此次咱們能夠直接刪除其中和文件寫入相關的全部代碼。讓它只負責一件事情:「獲取帖子列表」。

class HNTopPostsSpider:
    """抓取 HackerNews Top 內容條目 :param limit: 限制條目數,默認爲 5 """
    ITEMS_URL = 'https://news.ycombinator.com/'

    def __init__(self, limit: int = 5):
        self.limit = limit

    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
複製代碼

相應的,類和函數的調用方 main 函數就須要稍做調整,它須要負責把 write_posts_to_file 函數和 HNTopPostsSpider 類之間協調起來,共同完成工做:

def main():
    crawler = HNTopPostsSpider()

    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)
複製代碼

將「文件寫入」職責拆分爲新函數是一個 Python 特點的解決方案,它雖然沒有那麼 OO*(面向對象)*,可是一樣知足「單一職責原則」,並且在不少場景下更靈活與高效。

O:開放-關閉原則

O 來自於 「Open–closed principle(開放-關閉原則)」 的首字母,它認爲:「類應該對擴展開放,對修改封閉。」這是一個從字面上很難理解的原則,它一樣有着另一種說法:「你應該能夠在不修改某個類的前提下,擴展它的行爲。」

這原則聽上去有點讓人犯迷糊,如何能作到不修改代碼又改變行爲呢?讓我來舉一個例子:你知道 Python 裏的內置排序函數 sorted 嗎?

若是咱們想對某個列表排序,能夠直接調用 sorted 函數:

>>> l = [5, 3, 2, 4, 1]
>>> sorted(l)
[1, 2, 3, 4, 5]
複製代碼

如今,假如咱們想改變 sorted 函數的排序邏輯。好比,讓它使用全部元素對 3 取餘後的結果來排序。咱們是否是須要去修改 sorted 函數的源碼?固然不用,只須要在調用 sort 函數時,傳入自定義的排序函數 key 參數就好了:

>>> l = [8, 1, 9]
# 按照元素對 3 的餘數排序,能被 3 整除的 9 排在了最前面,隨後是 1 和 8
>>> sorted(l, key=lambda i: i % 3)
[9, 1, 8]
複製代碼

經過上面的例子,咱們能夠認爲:sorted 函數是一個符合「開放-關閉原則」的絕佳例子,由於它:

  • 對擴展開放:你能夠經過傳入自定義 key 函數來擴展它的行爲
  • 對修改關閉:你無需修改 sort 函數自己

如何違反「開放-關閉原則」

如今,讓咱們回到爬蟲小程序。在使用了一段時間以後,用戶*(仍是我)*以爲每次抓取到的內容有點不合口味。我其實只關注那些來自特定網站,好比 github 上的內容。因此我須要修改 HNTopPostsSpider 類的代碼來對結果進行過濾:

class HNTopPostsSpider:
    # <... 已省略 ...>

    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        counter = 0
        for item in items:
            if counter >= self.limit:
                break

            # <... 已省略 ...>
            link = node_title.get('href')

            # 只關注來自 github.com 的內容
            if 'github' in link.lower():
                counter += 1
                yield Post(... ...)
複製代碼

完成修改後,讓咱們來簡單測試一下效果:

❯ python news_digester_O_before.py
# Top news on HN

> TOP 1: Mimalloc – A compact general-purpose allocator
> 分數:291 評論數:40
> 地址:https://github.com/microsoft/mimalloc
------
> TOP 2: Olivia: An open source chatbot build with a neural network in Go
> 分數:53 評論數:19
> 地址:https://github.com/olivia-ai/olivia
------
<... 已省略 ...>
複製代碼

看上去新加的過濾代碼起到了做用,如今只有連接中含有 github 的內容纔會被寫入到結果中。

可是,正如某位哲學家的名言所說:*「這世間惟一不變的,只有變化自己。」某天,用戶(永遠是我)*忽然以爲,來自 bloomberg 的內容也都頗有意思,因此我想要把 bloomberg 也加入篩選關鍵字邏輯裏。

這時咱們就會發現:如今的代碼違反了"開放-關閉原則"。由於我必需要修改現有的 HNTopPostsSpider 類代碼,調整那個 if 'github' in link.lower() 判斷語句才能完成個人需求。

「開放-關閉原則」告訴咱們,類應該經過擴展而不是修改的方式改變本身的行爲。那麼我應該如何調整代碼,讓它能夠遵循原則呢?

使用類繼承來改造代碼

繼承是面向對象理論中最重要的概念之一。它容許咱們在父類中定義好數據和方法,而後經過繼承的方式讓子類得到這些內容,並能夠選擇性的對其中一些進行重寫,修改它的行爲。

使用繼承的方式來讓類遵照「開放-關閉原則」的關鍵點在於:找到父類中會變更的部分,將其抽象成新的方法(或屬性),最終容許新的子類來重寫它以改變類的行爲。

對於 HNTopPostsSpider 類來講。首先,咱們須要找到其中會變更的那部分邏輯,也就是*「判斷是否對條目感興趣」*,而後將其抽象出來,定義爲新的方法:

class HNTopPostsSpider:
    # <... 已省略 ...>

    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            
            post = Post( ... ... )
            # 使用測試方法來判斷是否返回該帖子
            if self.interested_in_post(post):
                counter += 1
                yield post

    def interested_in_post(self, post: Post) -> bool:
        """判斷是否應該將帖子加入結果中 """
        return True
複製代碼

若是咱們只關心來自 github 的帖子,那麼只須要定義一個繼承於 HNTopPostsSpider 子類,而後重寫父類的 interested_in_post 方法便可。

class GithubOnlyHNTopPostsSpider(HNTopPostsSpider):
    """只關心來自 Github 的內容 """
    def interested_in_post(self, post: Post) -> bool:
        return 'github' in post.link.lower()

def main():
    # crawler = HNTopPostsSpider()
    # 使用新的子類
    crawler = GithubOnlyHNTopPostsSpider()
    <... ...>
複製代碼

假如咱們的興趣發生了變化?不要緊,增長新的子類就行:

class GithubNBloomBergHNTopPostsSpider(HNTopPostsSpider):
    """只關係來自 Github/BloomBerg 的內容 """
    def interested_in_post(self, post: Post) -> bool:
        if 'github' in post.link.lower() \
                or 'bloomberg' in post.link.lower():
            return True
        return False
複製代碼

全部的這一切,都不須要修改本來的 HNTopPostsSpider 類的代碼,只須要不斷在它的基礎上建立新的子類就能完成新需求。最終實現了對擴展開放、對改變關閉。

使用組合與依賴注入來改造代碼

雖然類的繼承特性很強大,但它並不是惟一辦法,依賴注入(Dependency injection) 是解決這個問題的另外一種思路。與繼承不一樣,依賴注入容許咱們在類實例化時,經過參數將業務邏輯的變化點:帖子過濾算法 注入到類實例中。最終一樣實現「開放-關閉原則」。

首先,咱們定義一個名爲 PostFilter 的抽象類:

from abc import ABC, abstractmethod

class PostFilter(metaclass=ABCMeta):
    """抽象類:定義如何過濾帖子結果 """
 @abstractmethod
    def validate(self, post: Post) -> bool:
        """判斷帖子是否應該被保留"""
複製代碼

Hint:定義抽象類在 Python 的 OOP 中並非必須的,你也能夠不定義它,直接從下面的 DefaultPostFilter 開始。

而後定義一個繼承於該抽象類的默認 DefaultPostFilter 類,過濾邏輯爲保留全部結果。以後再調整一下 HNTopPostsSpider 類的構造方法,讓它接收一個名爲 post_filter 的結果過濾器:

class DefaultPostFilter(PostFilter):
    """保留全部帖子 """
    def validate(self, post: Post) -> bool:
        return True


class HNTopPostsSpider:
    """抓取 HackerNews Top 內容條目 :param limit: 限制條目數,默認爲 5 :param post_filter: 過濾結果條目的算法,默認爲保留全部 """
    ITEMS_URL = 'https://news.ycombinator.com/'

    def __init__(self, limit: int = 5, post_filter: Optional[PostFilter] = None):
        self.limit = limit
        self.post_filter = post_filter or DefaultPostFilter()

    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            
            post = Post( ... ... )
            
            # 使用測試方法來判斷是否返回該帖子
            if self.post_filter.validate(post):
                counter += 1
                yield post
複製代碼

默認狀況下,HNTopPostsSpider.fetch 會保留全部的結果。假如咱們想要定義本身的過濾算法,只要新建本身的 PostFilter 類便可,下面是兩個分別過濾 GitHub 與 BloomBerg 的 PostFilter 類:

class GithubPostFilter(PostFilter):
    def validate(self, post: Post) -> bool:
        return 'github' in post.link.lower()


class GithubNBloomPostFilter(PostFilter):
    def validate(self, post: Post) -> bool:
        if 'github' in post.link.lower() or 'bloomberg' in post.link.lower():
            return True
        return False
複製代碼

main() 函數中,我能夠用不一樣的 post_filter 參數來實例化 HNTopPostsSpider 類,最終知足不一樣的過濾需求:

def main():
    # crawler = HNTopPostsSpider()
    # crawler = HNTopPostsSpider(post_filter=GithubPostFilter())
    crawler = HNTopPostsSpider(post_filter=GithubNBloomPostFilter())

    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)
複製代碼

與基於繼承的方式同樣,利用將「過濾算法」抽象爲 PostFilter 類並以實例化參數的方式注入到 HNTopPostsSpider 中,咱們一樣實現了「開放-關閉原則」。

使用數據驅動思想來改造代碼

在實現「開放-關閉」原則的衆多手法中,除了繼承與依賴注入外,還有一種常常被用到的方式:「數據驅動」。這個方式的核心思想在於:將常常變更的東西,徹底以數據的方式抽離出來。當需求變更時,只改動數據,代碼邏輯保持不動。

它的原理與「依賴注入」有一些類似,一樣是把變化的東西抽離到類外部。不一樣的是,後者抽離的一般是類,而前者抽離的是數據。

爲了讓 HNTopPostsSpider 類的行爲能夠被數據驅動,咱們須要使其接收 filter_by_link_keywords 參數:

class HNTopPostsSpider:
    """抓取 HackerNews Top 內容條目 :param limit: 限制條目數,默認爲 5 :param filter_by_link_keywords: 過濾結果的關鍵詞列表,默認爲 None 不過濾 """
    ITEMS_URL = 'https://news.ycombinator.com/'

    def __init__(self, limit: int = 5, filter_by_link_keywords: Optional[List[str]] = None):
        self.limit = limit
        self.filter_by_link_keywords = filter_by_link_keywords

    def fetch(self) -> Generator[Post, None, None]:
        # <... 已省略 ...>
        for item in items:
            # <... 已省略 ...>
            
            post = Post( ... ... )
            
            if self.filter_by_link_keywords is None:
                counter += 1
                yield post
            # 當 link 中出現任意一個關鍵詞時,返回結果
            elif any(keyword in post.link for keyword in self.filter_by_link_keywords):
                counter += 1
                yield post
複製代碼

調整了初始化參數後,還須要在 main 函數中定義 link_keywords 變量並將其傳入到 HNTopPostsSpider 類的構造方法中,以後全部針對過濾關鍵詞的調整都只須要修改這個列表便可,無需改動 HNTopPostsSpider 類的代碼,一樣知足了「開放-關閉原則」。

def main():
    # link_keywords = None
    link_keywords = [
        'github.com',
        'bloomberg.com'
    ]
    crawler = HNTopPostsSpider(filter_by_link_keywords=link_keywords)

    posts = list(crawler.fetch())
    file_title = 'Top news on HN'
    write_posts_to_file(posts, sys.stdout, file_title)
複製代碼

與前面的繼承和依賴注入方式相比,「數據驅動」的代碼更簡潔,不須要定義額外的類。但它一樣也存在缺點:它的可定製性不如前面的兩種方式。假如,我想要以「連接是否以某個字符串結尾」做爲新的過濾條件,那麼如今的數據驅動代碼就有心無力了。

如何選擇合適的方式來讓代碼符合「開放-關閉原則」,須要根據具體的需求和場景來判斷。這也是一個沒法一蹴而就、須要大量練習和經驗積累的過程。

總結

在這篇文章中,我經過一個具體的 Python 代碼案例,向你描述了 「SOLID」 設計原則中的前兩位成員:「單一職責原則」「開放-關閉原則」

這兩個原則雖然看上去很簡單,可是它們背後蘊藏了許多從好代碼中提煉而來的智慧。它們的適用範圍也不只僅侷限在 OOP 中。一旦你深刻理解它們後,你可能會驚奇的在許多設計模式和框架中發現它們的影子*(好比這篇文章就出現了至少 3 種設計模式,你知道是哪些嗎?)*。

讓咱們最後再總結一下吧:

  • **「S: 單一職責原則」**認爲一個類只應該有一種被修改的緣由
  • 編寫更小的類一般更不容易違反 S 原則
  • S 原則一樣適用於函數,你可讓函數和類協同工做
  • **「O: 開放-關閉原則」**認爲類應該對改動關閉,對擴展開放
  • 找到需求中頻繁變化的那個點,是讓類遵循 O 原則的重點所在
  • 使用子類繼承的方式可讓類遵照 O 原則
  • 經過定義算法類,並進行依賴注入,也可讓類遵循 O 原則
  • 將數據與邏輯分離,使用數據驅動的方式也是改造代碼的好辦法

看完文章的你,有沒有什麼想吐槽的?請留言或者在 項目 Github Issues 告訴我吧。

附錄

系列其餘文章:

相關文章
相關標籤/搜索