工厂模式和if-else结构, 2024-04-14

(2024.04.14 Sun @KLN)
工厂模式为公共接口创建具体的实现方法(create concrete implements of a common interface),它将创建对象的过程和依赖该对象接口的代码分离。

Case Study

以指定的格式,将Song对象转换为string。该过程经常被称作序列化(serialising)。该函数最简单的实现方式如下:

# 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提供了歌的进属性,即idtitleartist。类SongSerializer将歌转化为string表达,具体的形式由参数format指定。.seraliaze()方法这里支持两种格式JSONXML,遇到其他格式则返回ValueError错误。

使用上面代码

>>> 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

该案例用于演示目的,想象在具体项目中,可能会提供多种format可选,即参数指定的实现方式可以有多种,将导致代码难以维护。

Optimisation

到目前为止,该案例的实现方式不容易维护,下面对其做优化。记得SOLID原则中的single responsibility principle提到,每个模块、类、方法应该只做单一的、定义清晰的(well-defined)的职责,只做一件事,因一个原因而改变。

.serialize()方法不符合single responsibility principle原则,发生如下情况将修改该方法:

  • 引入新的格式:在if-else结构中加入新分支
  • Song对象改变:Song对象一旦修改了属性,则.serialize()方法需要对每个格式做修改
  • 格式的表达变化:仅就目前代码中JSONXML的表达而言,一旦其实现方法出现变化,则方法需要修改。

理想情况是需求中的任何改变都不会导致.serialize()方法做出修改。

Looking for a Common Interface
观察复杂的条件判断代码,首先需要识别出每个执行路径(逻辑路径)的共同目标(common goal of each of the execution path/logical paths)。
比如前面代码每个逻辑路径都将song对象转换为不同格式的string表达。基于该目标,需要找到一个共同接口(common interface),该接口可用于替换每个路径上的逻辑。在该案例中,这个接口输入song对象并输出一个string

有了共同接口,就可为每个逻辑路径提供独立的实现方法。

接下来就需要提供一个独立的部分用于决定使用哪种具体的实现方法。该独立部分根据format的值返回其对应的实现方式。

下面对代码进行重构(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
目标:建立接口/函数,对其输入Song对象并返回string表达。

第一步将其中一个逻辑路径重构为这种接口。加入一个新的方法._serialize_to_json(),并将JSON序列化的内容放在新方法中

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)

接着实现另一个操作逻辑

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')

重构后的代码已经便于阅读和维护,但仍然可以进一步优化,下面用基本的工厂方法实现。

工厂方法的基本实现

工厂方法的核心是提供一个独立的部分,用于决定在指定参数的情况下该选择哪个具体的实现方式。在本案例中,该指定参数就是format

为完成工厂方法的实现,加入新方法._get_serializer(),输入参数为format,该方法将根据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()犯法不会调用具体的实现方法,仅仅返回函数对象本身。

接下来可以改变SongSerializer类的.serialize()方法,调用._get_serializer()方法完成工厂方法的实现,如下

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()方法是应用代码,它依赖一个接口完成序列化任务,被称作模式的客户端部分(the client component of the pattern)。在本案例中,产品定义为输入Song对象且返回string表达的一个函数。._serialize_to_json()._serialize_to_xml()方法都是产品中的具体实现。最终._get_serializer()方法称为创建器(creator),决定使用哪种实现方法。

到目前为止的代码中所有方法都是SongSerializer类的成员,但注意到新加入的方法并没有使用self参数。这表明他们不该成为SongSerializer类的方法而该成为外部函数。

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')

工厂方法的机制总是相同:

  • 一个客户端(client)(SongSerializer.serializer())依赖接口的具体实现,并要求创建者(creator)(.get_serializer())来实现,并传入识别符(identifier)(format)。
  • 创建者根据客户端传入的参数值返回具体的实现,客户端由创建者提供的对象完成任务。

执行如下

>>> 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

盘点

工厂方法的核心在于提供一个独立的部分,该部分将根据特定参数来决定才用哪个具体的方法。该参数在上面案例中就是format

工厂方法的使用场景:应用(客户端)依赖于某个接口(产品)用于执行特定任务,且该接口提供任务的多种具体实现方式。开发者提供参数用于决定具体采用哪种实现方式,并在创建器(creator)中使用。

Case Study: Car Factory

(2024.04.20 Sat @KLN)
场景:需要支持不同车型和诸如启动、停止的功能,但在运行之前不知道需要哪些车型。

Brute force approach:运行时(at runtime),得到车型名字时,用if-else/switch语句决定创建哪个车型。

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()

这段代码正常工作,但缺少可扩展性,加入新车型需要修改实现并加入更多车型的import命令,这将破坏SOLID中的open/close原则。此外,直接实例化car类,破坏了SOLID中的dependency-inversion原则,因为依赖了这些类的实现。

对上面代码重构,按照这个框架流程图实现。

image.png

在框架中,看到AbsCars类,一个抽象类,其中的startstop指明了在实现时需要具体实现这些方法。另有三个子类,JeepFordFerrari,均实现了AbsCars类。

另有一个CarFactory类,用于创建需要的车类实例。

import abc

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

  @abc.abstractmethod
  def stop(self):
    pass

startstop都被声明为抽象类方法,在继承时需要实现。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")

一个通用的类可表示为

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方法用于构建cars字典,先在cars包中找到不是抽象类的所有类,再找到AbsCars类的子类,并将其加入到cars字典。

create_instance方法从cars字典中找到车名,如果有则返回类的实例,如果没有则返回GenericCars类的实例。

在主代码中,仅仅引入和实例化AutoCars类,并遍历(loop through)车名字,为每个车名调用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()

通过这种方法,在加入新车型时仅需要增加AbsCars的子类,而不需要修改过多代码。

Other Cases

(2024.04.20 Sat)
在工厂方法中可将不同逻辑路径(logical path)的内容置于拥有共同接口的不同的函数/类中,使用创建者作为具体的实现。输入条件中的参数决定了使用哪种具体的实现。

  • 从外部数据构建相关对象:一个应用需要从外部数据源或库获取雇员信息。数据包括雇员角色或类型,manager,office clerk,sales associate之类。应用存储一个识别符用于代表雇员类型,可使用工厂方法创建具体的雇员对象

  • 相同特征的多种实现方式:图像处理应用需要将卫星图片从一种坐标系统转换到另一种,转换方式根据准确度的级别有多种实现算法。该应用允许用户选择不同的具体算法,工厂方法根据用户选择提供了具体实现。

  • 在共同接口下整合相似特征:仍然考虑前一个图像处理的案例,某应用需要对图像应用一个filter,可根据用户输入来识别/指定filter,工厂方法提供具体的filter实现。

  • 整合相关应用的外部服务:音乐播放器应用需要整合多个外部服务,允许用户选择音乐来源。该应用可以定义个公共接口,使用工厂方法根据用户选择创建正确的整合路径。

上述情况的共同之处在于,都定义了一个依赖于共同接口(称作产品)的客户端,都提供了用于识别产品的具体实现的方式,也就都可以使用工厂方法。

Reference

1 realpython: factory method python
2 Refactoring: Improving the Design of Existing Code
3 dev点to, Factory Design Pattern in Python, Khushboo Parasrampuria
`

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容