前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用工厂模式优化 if/elif/else 代码

使用工厂模式优化 if/elif/else 代码

作者头像
哒呵呵
发布2019-04-26 13:44:30
5470
发布2019-04-26 13:44:30
举报
文章被收录于专栏:鸿的学习笔记

编译自https://realpython.com/factory-method-python/ 源代码有一定的修改

导论

工厂模式不需要详细解释了,具体的可以在 Design Patterns: Elements of Reusable Object-Oriented Software 书中找到。直接进入正题吧。

简单直接的第一版代码

假设现在需要序列化一条数据,这条数据是一首歌,它包含了歌的名称、创作者和id。序列化的方式暂时有json、xml两种,因此你第一时间想到的代码可能像下面一样。

代码语言:javascript
复制
# In serializer_demo.py

import json
import xml.etree.ElementTree as et
from dataclasses import dataclass

@dataclass
class Song:
     song_id: str
     title: str
     artist: str


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类以及将Song序列化的类,通过运行serialize可以将Song类序列化成相应格式的字符串,例如:

代码语言:javascript
复制
>>> import serializer_demo as sd
>>> song = sd.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = sd.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 "./serializer_demo.py", line 30, in serialize
    raise ValueError(format)
ValueError: YAML

如果像序列化其他格式,例如输入YAML时会报错,抛出异常表示不支持这种序列化。

评价

显然如果是紧急需求,或者是没有经过思考的话,上面的代码的确是一挥而就,完成任务。作为一名不断精进的工程师,肯定是不会满足于上述代码,那么上述代码存在什么问题需要去改进呢?

  • 当一个新的序列化格式引入的时候,serialize方法需要进行修改以适应这种变化。
  • Song这个对象发生改变时,也需要重新修改SongSerializer这个类以适应Song结构的变化。
  • 当序列化格式需要发生变化或者对应的API接口发生变化时,serialize方法也必须更改,因为这些东西都是硬编码在代码里的。

开始重构

在重构代码之前,应先思考抽象出其核心逻辑,确定每一个 if/elif/else 的执行路径(或者是逻辑路径)的公共目标。基于上述目标,应该将json、xml的序列化方法抽象出相应的接口替代具体执行方法,再具体实现每一种序列化格式对应序列化方法,最后抽象 if/elif/else 的运行逻辑。

将序列化的执行方式抽象
代码语言:javascript
复制
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')

上述代码中把 if/elif/else 对应的逻辑路径下的序列化方法抽象出了 _serialize_to_json_serialize_to_xml 方法。这一版的代码相对于之前更易读和容易理解了,但是之前提到的问题依然没有得到解决,需要进一步重构。

基本工厂模式

上一版代码只是把具体的序列化方法进行了重构,这一版代码把 if/elif/else 抽象成 _get_serializer 方法,从而将选择执行相应的逻辑路径的方法也抽离出来。

代码语言:javascript
复制
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 方法内部所有逻辑都被抽象成相应的接口, if/elif/else 逻辑被放在了 _get_serializer 的内部方法里,也就是工厂模式里的creator(选择要使用哪个序列化方法)。具体的序列化方法 _serialize_to_json_serialize_to_xml 就是product(具体执行序列化的逻辑)。SongSerializer则是所谓的client

更为灵活的增减product

为了使得_serialize_to_xml_serialize_to_json可以更灵活的添加和实现,而不需要修改原始的SongSerializer类变成彻底的client,可以将相应的工厂方法外提成同一个文件下的外部函数。

代码语言:javascript
复制
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')

这一版代码基本上完成了简单的工厂模式重构,SongSerializer.serialize作为一个client决定着接口的具体执行,根据相应的标识符调用creator组件get_serializer中实现序列化。creator根据client传入的参数返回相应的对象,选择对应的product(_serialize_to_json_serialize_to_xml)完成相应的序列化。

小结

经过相当大的重构,这一版的代码依然还不够尽善尽美,因为还没考虑到如下的情形:

  • if/elif/else 逻辑 依然没有完全替代,如果需要添加新的逻辑就需要修改get_serializer方法。
  • 可以根据外部数据的不同的创建对应的对象。
  • 可以对同一特征灵活地拥有不同的执行方式,例如上述的Song对象可以拥有json和xml的序列化方式,而没有yaml地序列化方式。
  • 将相同的特征抽象成统一的接口。
工厂模式的最终抽象

首先将_serialize_to_xml_serialize_to_json从方法变成对应的抽象接口Serializer,以方便扩展序列化支持的数据。

代码语言:javascript
复制
# In serializers.py
from abc import ABCMeta, abstractmethod
import json
import xml.etree.ElementTree as et

class Serializer(metaclass=ABCMeta):
    @abstractmethod
    def start_object(self, object_name, object_id):
        pass

    @abstractmethod
    def add_property(self, name, value):
        pass

    @abstractmethod
    def to_str(self):
        pass

class JsonSerializer(Serializer):
    def __init__(self):
        self._current_object = None

    def start_object(self, object_name, object_id):
        self._current_object = {
            'id': object_id
        }

    def add_property(self, name, value):
        self._current_object[name] = value

    def to_str(self):
        return json.dumps(self._current_object)


class XmlSerializer(Serializer):
    def __init__(self):
        self._element = None

    def start_object(self, object_name, object_id):
        self._element = et.Element(object_name, attrib={'id': object_id})

    def add_property(self, name, value):
        prop = et.SubElement(self._element, name)
        prop.text = value

    def to_str(self):
        return et.tostring(self._element, encoding='unicode')

Serializer接口将上述的json和xml序列化方法抽象成了三个方法:

  • start_object(object_name, object_id)
  • add_property(name, value)
  • to_str()

XmlSerializerJsonSerializer都是Serializer接口的具体实现。

于是具体的Serializer的执行,也就是client,就可以抽象成下述模式:

代码语言:javascript
复制
# In serializers.py

class ObjectSerializer:
    def serialize(self, serializable, format):
        serializer = factory.get_serializer(format)
        serializable.serialize(serializer)
        return serializer.to_str()

这里的ObjectSerializer的实现便通用许多,只需要serializableformat参数。format用于确定Serializer的具体执行方法,serializable可以按照自己本身确定的的serialize方式实现serialize方法。

于是将Song类就重构成serializable的具体实现:

代码语言:javascript
复制
# In songs.py

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

    def serialize(self, serializer):
        serializer.start_object('song', self.song_id)
        serializer.add_property('title', self.title)
        serializer.add_property('artist', self.artist)

Song类实现了Serializable的具体接口serialize,在这个接口里Song类使用serializer对象写入自己的信息而无需关心具体序列格式,使得Song类灵活的转换成不同的序列化格式。

到目前为止,这一版代码的client(ObjectSerializer)和product(serializer)有了。作为creatorfactory该如何实现。

factory方法的实现

最简单的factory方法可以依旧使用 if/else/elif 的逻辑处理数据。

代码语言:javascript
复制
# In serializers.py

class SerializerFactory:
    def get_serializer(self, format):
        if format == 'JSON':
            return JsonSerializer()
        elif format == 'XML':
            return XmlSerializer()
        else:
            raise ValueError(format)


factory = SerializerFactory()

经过前面的讨论,if/else/elif 使得上述代码依旧不够灵活,例如要添加yaml的序列化格式就需要修改SerializerFactory类的源码。因此可以提供类似于注册的模式,也就是register_format方法,将各个格式注册进SerializerFactory类,从而灵活的添加对应的序列化方式。

代码语言:javascript
复制
# In serializers.py

class SerializerFactory:

    def __init__(self):
        self._creators = {}

    def register_format(self, format, creator):
        self._creators[format] = creator

    def get_serializer(self, format):
        creator = self._creators.get(format)
        if not creator:
            raise ValueError(format)
        return creator()


factory = SerializerFactory()
factory.register_format('JSON', JsonSerializer)
factory.register_format('XML', XmlSerializer)

yaml格式的序列化可以单独写成一个文件。

代码语言:javascript
复制
# In yaml_serializer.py

import yaml
import serializers

class YamlSerializer(serializers.JsonSerializer):
    def to_str(self):
        return yaml.dump(self._current_object)


serializers.factory.register_format('YAML', YamlSerializer)

现在执行结果依旧和之前的代码一样:

代码语言:javascript
复制
>>> import serializers
>>> import songs
>>> import yaml_serializer
>>> song = songs.Song('1', 'Water of Love', 'Dire Straits')
>>> serializer = serializers.ObjectSerializer()

>>> print(serializer.serialize(song, 'JSON'))
{"id": "1", "title": "Water of Love", "artist": "Dire Straits"}

>>> print(serializer.serialize(song, 'XML'))
<song id="1"><title>Water of Love</title><artist>Dire Straits</artist></song>

>>> print(serializer.serialize(song, 'YAML'))
{artist: Dire Straits, id: '1', title: Water of Love}

结论

最后一版代码解决了 if/elif/else 模式需要硬编码和不易进行修改的问题,使得代码更易复用和灵活,解耦了clientcreatorproduct的逻辑,避免了增加新的序列化格式就需要修改源代码的问题。

那么你的代码属于哪一层级?

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-02-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 鸿的笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导论
  • 简单直接的第一版代码
    • 评价
    • 开始重构
      • 将序列化的执行方式抽象
        • 基本工厂模式
          • 更为灵活的增减product
            • 小结
          • 工厂模式的最终抽象
            • factory方法的实现
        • 结论
        相关产品与服务
        文件存储
        文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档