工廠模式和if-else結(jié)構(gòu), 2024-04-14

(2024.04.14 Sun @KLN)
工廠模式為公共接口創(chuàng)建具體的實(shí)現(xiàn)方法(create concrete implements of a common interface),它將創(chuàng)建對(duì)象的過程和依賴該對(duì)象接口的代碼分離。

Case Study

以指定的格式,將Song對(duì)象轉(zhuǎn)換為string。該過程經(jīng)常被稱作序列化(serialising)。該函數(shù)最簡(jiǎn)單的實(shí)現(xiàn)方式如下:

# In serializer_demo.py

import json
import xml.etree.ElementTree as et

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist


class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            song_info = {
                'id': song.song_id,
                'title': song.title,
                'artist': song.artist
            }
            return json.dumps(song_info)
        elif format == 'XML':
            song_info = et.Element('song', attrib={'id': song.song_id})
            title = et.SubElement(song_info, 'title')
            title.text = song.title
            artist = et.SubElement(song_info, 'artist')
            artist.text = song.artist
            return et.tostring(song_info, encoding='unicode')
        else:
            raise ValueError(format)

Song提供了歌的進(jìn)屬性,即idtitleartist。類SongSerializer將歌轉(zhuǎn)化為string表達(dá),具體的形式由參數(shù)format指定。.seraliaze()方法這里支持兩種格式JSONXML,遇到其他格式則返回ValueError錯(cuò)誤。

使用上面代碼

>>> song = Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = SongSerializer()
>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'
>>> serializer.serialize(song, 'XML')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'
>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in serialize
ValueError: YAML

該案例用于演示目的,想象在具體項(xiàng)目中,可能會(huì)提供多種format可選,即參數(shù)指定的實(shí)現(xiàn)方式可以有多種,將導(dǎo)致代碼難以維護(hù)。

Optimisation

到目前為止,該案例的實(shí)現(xiàn)方式不容易維護(hù),下面對(duì)其做優(yōu)化。記得SOLID原則中的single responsibility principle提到,每個(gè)模塊、類、方法應(yīng)該只做單一的、定義清晰的(well-defined)的職責(zé),只做一件事,因一個(gè)原因而改變。

.serialize()方法不符合single responsibility principle原則,發(fā)生如下情況將修改該方法:

  • 引入新的格式:在if-else結(jié)構(gòu)中加入新分支
  • Song對(duì)象改變:Song對(duì)象一旦修改了屬性,則.serialize()方法需要對(duì)每個(gè)格式做修改
  • 格式的表達(dá)變化:僅就目前代碼中JSONXML的表達(dá)而言,一旦其實(shí)現(xiàn)方法出現(xiàn)變化,則方法需要修改。

理想情況是需求中的任何改變都不會(huì)導(dǎo)致.serialize()方法做出修改。

Looking for a Common Interface
觀察復(fù)雜的條件判斷代碼,首先需要識(shí)別出每個(gè)執(zhí)行路徑(邏輯路徑)的共同目標(biāo)(common goal of each of the execution path/logical paths)。
比如前面代碼每個(gè)邏輯路徑都將song對(duì)象轉(zhuǎn)換為不同格式的string表達(dá)。基于該目標(biāo),需要找到一個(gè)共同接口(common interface),該接口可用于替換每個(gè)路徑上的邏輯。在該案例中,這個(gè)接口輸入song對(duì)象并輸出一個(gè)string

有了共同接口,就可為每個(gè)邏輯路徑提供獨(dú)立的實(shí)現(xiàn)方法。

接下來就需要提供一個(gè)獨(dú)立的部分用于決定使用哪種具體的實(shí)現(xiàn)方法。該獨(dú)立部分根據(jù)format的值返回其對(duì)應(yīng)的實(shí)現(xiàn)方式。

下面對(duì)代碼進(jìn)行重構(gòu)(refactor),即the process of changing a software system in such a way that does not alter the external behavior of the code yet improves its internal structure。

Refactoring
目標(biāo):建立接口/函數(shù),對(duì)其輸入Song對(duì)象并返回string表達(dá)。

第一步將其中一個(gè)邏輯路徑重構(gòu)為這種接口。加入一個(gè)新的方法._serialize_to_json(),并將JSON序列化的內(nèi)容放在新方法中

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        # The rest of the code remains the same

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

接著實(shí)現(xiàn)另一個(gè)操作邏輯

class SongSerializer:
    def serialize(self, song, format):
        if format == 'JSON':
            return self._serialize_to_json(song)
        elif format == 'XML':
            return self._serialize_to_xml(song)
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

重構(gòu)后的代碼已經(jīng)便于閱讀和維護(hù),但仍然可以進(jìn)一步優(yōu)化,下面用基本的工廠方法實(shí)現(xiàn)。

工廠方法的基本實(shí)現(xiàn)

工廠方法的核心是提供一個(gè)獨(dú)立的部分,用于決定在指定參數(shù)的情況下該選擇哪個(gè)具體的實(shí)現(xiàn)方式。在本案例中,該指定參數(shù)就是format

為完成工廠方法的實(shí)現(xiàn),加入新方法._get_serializer(),輸入?yún)?shù)為format,該方法將根據(jù)format值放回匹配的序列化方法。

class SongSerializer:
    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

注意該._get_serializer()犯法不會(huì)調(diào)用具體的實(shí)現(xiàn)方法,僅僅返回函數(shù)對(duì)象本身。

接下來可以改變SongSerializer類的.serialize()方法,調(diào)用._get_serializer()方法完成工廠方法的實(shí)現(xiàn),如下

class SongSerializer:
    def serialize(self, song, format):
        serializer = self._get_serializer(format)
        return serializer(song)

    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')class SongSerializer:
    def serialize(self, song, format):
        serializer = self._get_serializer(format)
        return serializer(song)

    def _get_serializer(self, format):
        if format == 'JSON':
            return self._serialize_to_json
        elif format == 'XML':
            return self._serialize_to_xml
        else:
            raise ValueError(format)

    def _serialize_to_json(self, song):
        payload = {
            'id': song.song_id,
            'title': song.title,
            'artist': song.artist
        }
        return json.dumps(payload)

    def _serialize_to_xml(self, song):
        song_element = et.Element('song', attrib={'id': song.song_id})
        title = et.SubElement(song_element, 'title')
        title.text = song.title
        artist = et.SubElement(song_element, 'artist')
        artist.text = song.artist
        return et.tostring(song_element, encoding='unicode')

.serialize()方法是應(yīng)用代碼,它依賴一個(gè)接口完成序列化任務(wù),被稱作模式的客戶端部分(the client component of the pattern)。在本案例中,產(chǎn)品定義為輸入Song對(duì)象且返回string表達(dá)的一個(gè)函數(shù)。._serialize_to_json()._serialize_to_xml()方法都是產(chǎn)品中的具體實(shí)現(xiàn)。最終._get_serializer()方法稱為創(chuàng)建器(creator),決定使用哪種實(shí)現(xiàn)方法。

到目前為止的代碼中所有方法都是SongSerializer類的成員,但注意到新加入的方法并沒有使用self參數(shù)。這表明他們不該成為SongSerializer類的方法而該成為外部函數(shù)。

class SongSerializer:
    def serialize(self, song, format):
        serializer = get_serializer(format)
        return serializer(song)


def get_serializer(format):
    if format == 'JSON':
        return _serialize_to_json
    elif format == 'XML':
        return _serialize_to_xml
    else:
        raise ValueError(format)


def _serialize_to_json(song):
    payload = {
        'id': song.song_id,
        'title': song.title,
        'artist': song.artist
    }
    return json.dumps(payload)


def _serialize_to_xml(song):
    song_element = et.Element('song', attrib={'id': song.song_id})
    title = et.SubElement(song_element, 'title')
    title.text = song.title
    artist = et.SubElement(song_element, 'artist')
    artist.text = song.artist
    return et.tostring(song_element, encoding='unicode')

工廠方法的機(jī)制總是相同:

  • 一個(gè)客戶端(client)(SongSerializer.serializer())依賴接口的具體實(shí)現(xiàn),并要求創(chuàng)建者(creator)(.get_serializer())來實(shí)現(xiàn),并傳入識(shí)別符(identifier)(format)。
  • 創(chuàng)建者根據(jù)客戶端傳入的參數(shù)值返回具體的實(shí)現(xiàn),客戶端由創(chuàng)建者提供的對(duì)象完成任務(wù)。

執(zhí)行如下

>>> song = Song("1", "Water of Love", "Dire Straits")
>>> serializer = SongSerializer()
>>> serializer.serialize(song, 'JSON')
'{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}'
>>> serializer.serialize(song, 'XML')
'<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>'
>>> serializer.serialize(song, 'YAML')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in serialize
  File "<stdin>", line 7, in get_serializer
ValueError: YAML

盤點(diǎn)

工廠方法的核心在于提供一個(gè)獨(dú)立的部分,該部分將根據(jù)特定參數(shù)來決定才用哪個(gè)具體的方法。該參數(shù)在上面案例中就是format

工廠方法的使用場(chǎng)景:應(yīng)用(客戶端)依賴于某個(gè)接口(產(chǎn)品)用于執(zhí)行特定任務(wù),且該接口提供任務(wù)的多種具體實(shí)現(xiàn)方式。開發(fā)者提供參數(shù)用于決定具體采用哪種實(shí)現(xiàn)方式,并在創(chuàng)建器(creator)中使用。

Case Study: Car Factory

(2024.04.20 Sat @KLN)
場(chǎng)景:需要支持不同車型和諸如啟動(dòng)、停止的功能,但在運(yùn)行之前不知道需要哪些車型。

Brute force approach:運(yùn)行時(shí)(at runtime),得到車型名字時(shí),用if-else/switch語句決定創(chuàng)建哪個(gè)車型。

from cars.ford import Ford
from cars.ferrari import Ferrari
from cars.generic import car

def get_car(car_name):
  if car_name == 'Ford':
    return Ford()
  elif car_name == 'Ferrari':
    return Ferrari()
  else:
    return GenericCar()

for car_name in ['Jeep', 'Ferrari', 'Tesla']:
  car = get_car(car_name)
  car.start()
  car.stop()

這段代碼正常工作,但缺少可擴(kuò)展性,加入新車型需要修改實(shí)現(xiàn)并加入更多車型的import命令,這將破壞SOLID中的open/close原則。此外,直接實(shí)例化car類,破壞了SOLID中的dependency-inversion原則,因?yàn)橐蕾嚵诉@些類的實(shí)現(xiàn)。

對(duì)上面代碼重構(gòu),按照這個(gè)框架流程圖實(shí)現(xiàn)。

image.png

在框架中,看到AbsCars類,一個(gè)抽象類,其中的startstop指明了在實(shí)現(xiàn)時(shí)需要具體實(shí)現(xiàn)這些方法。另有三個(gè)子類,JeepFordFerrari,均實(shí)現(xiàn)了AbsCars類。

另有一個(gè)CarFactory類,用于創(chuàng)建需要的車類實(shí)例。

import abc

class AbsCars(abc.ABC):
  @abc.abstractmethod
  def start(self):
    pass

  @abc.abstractmethod
  def stop(self):
    pass

startstop都被聲明為抽象類方法,在繼承時(shí)需要實(shí)現(xiàn)。Ford類如下,其他車類都類似。

from cars.abscars import AbsCars

class Ford(AbsCars):
  def start(self):
    if is_fuel_present:
      print("ford engine is now running")
      return
    print("Low on fuel")

  def stop(self):
    print("Ford engine shutting down")

一個(gè)通用的類可表示為

from cars.abscars import AbsCars

class GenericCar(AbsCars):
  def __init__(self, car_name):
    self.name = car_name

  def start(self):
    print(f"{self.name} engine starting!")

  def stop(self):
    print(f"{self.name} engine stopping!")

定義CarFactory類如下

from inspect import getmembers, isclass, isabstract
import cars

class CarFactory():
  cars = {}

  def __init__(self):
    self.load_cars()

  def load_cars(self):
    classes = getmembers(cars, lambda m: isclass(m) and not isabstract(m))
    for name, _type in classes:
      if isclass(_type) and issubclass(_type, autos.AbsCars):
        self.cars.update([[name, _type]])

  def create_instance(self, car_name):
    if car_name in self.cars:
      return self.cars[car_name]()
    else:
      return cars.GenericCar(car_name)

load_cars方法用于構(gòu)建cars字典,先在cars包中找到不是抽象類的所有類,再找到AbsCars類的子類,并將其加入到cars字典。

create_instance方法從cars字典中找到車名,如果有則返回類的實(shí)例,如果沒有則返回GenericCars類的實(shí)例。

在主代碼中,僅僅引入和實(shí)例化AutoCars類,并遍歷(loop through)車名字,為每個(gè)車名調(diào)用create_instance方法。

from cars.carfactory import CarFactory

factory = CarFactory()

for car_name in ['Ford', 'Ferrari', 'Tesla']:
  car = factory.create_instance(car_name)
  car.start()
  car.stop()

通過這種方法,在加入新車型時(shí)僅需要增加AbsCars的子類,而不需要修改過多代碼。

Other Cases

(2024.04.20 Sat)
在工廠方法中可將不同邏輯路徑(logical path)的內(nèi)容置于擁有共同接口的不同的函數(shù)/類中,使用創(chuàng)建者作為具體的實(shí)現(xiàn)。輸入條件中的參數(shù)決定了使用哪種具體的實(shí)現(xiàn)。

  • 從外部數(shù)據(jù)構(gòu)建相關(guān)對(duì)象:一個(gè)應(yīng)用需要從外部數(shù)據(jù)源或庫(kù)獲取雇員信息。數(shù)據(jù)包括雇員角色或類型,manager,office clerk,sales associate之類。應(yīng)用存儲(chǔ)一個(gè)識(shí)別符用于代表雇員類型,可使用工廠方法創(chuàng)建具體的雇員對(duì)象

  • 相同特征的多種實(shí)現(xiàn)方式:圖像處理應(yīng)用需要將衛(wèi)星圖片從一種坐標(biāo)系統(tǒng)轉(zhuǎn)換到另一種,轉(zhuǎn)換方式根據(jù)準(zhǔn)確度的級(jí)別有多種實(shí)現(xiàn)算法。該應(yīng)用允許用戶選擇不同的具體算法,工廠方法根據(jù)用戶選擇提供了具體實(shí)現(xiàn)。

  • 在共同接口下整合相似特征:仍然考慮前一個(gè)圖像處理的案例,某應(yīng)用需要對(duì)圖像應(yīng)用一個(gè)filter,可根據(jù)用戶輸入來識(shí)別/指定filter,工廠方法提供具體的filter實(shí)現(xiàn)。

  • 整合相關(guān)應(yīng)用的外部服務(wù):音樂播放器應(yīng)用需要整合多個(gè)外部服務(wù),允許用戶選擇音樂來源。該應(yīng)用可以定義個(gè)公共接口,使用工廠方法根據(jù)用戶選擇創(chuàng)建正確的整合路徑。

上述情況的共同之處在于,都定義了一個(gè)依賴于共同接口(稱作產(chǎn)品)的客戶端,都提供了用于識(shí)別產(chǎn)品的具體實(shí)現(xiàn)的方式,也就都可以使用工廠方法。

Reference

1 realpython: factory method python
2 Refactoring: Improving the Design of Existing Code
3 dev點(diǎn)to, Factory Design Pattern in Python, Khushboo Parasrampuria
`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,874評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,095評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評(píng)論 1 280
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容