python多种创建工厂模式场景

2023-03-03 22:46:39 浏览数 (2)

工厂模式使用场景

  • 不清楚用户需要创建什么对象
  • 使用方法来代替new实例化对象的过程

它可以是用户自定义输入,也可以是通过接口或配置文件传入。如输入"Message",可以创建Message类的实例。

工厂模式指的是程序传入一个输入参数,自动创建所对应的对象。调用端并不需要关心类实例化的过程。基于工厂模式,可以实现可扩展、易维护的代码。

当你想扩展新增一个子类的时候,只需要关注于类本身。这样就可以遵守代码的"开闭原则",对扩展开放,对修改封闭。

场景举例

下面演示一个需求场景,我们需要开发一个消息通知功能,这个消息通知需要支持多种通知方式,比如邮件、短信、微信、钉钉等。

传统的工厂模式

传统的工厂模式就是if-else多重判断,指定传参匹配指定类。

代码语言:python代码运行次数:0复制
# main.py
from factory import factory_sender
from config import message_config

def send_message(message: dict):
    
    for msg_cfg in message_config:
        for name, config in msg_cfg.items():
            sender = factory_sender(name, config)
            sender.send_message(**message)

if __name__ == '__main__':

    send_message(
        {
            "title": "警告消息",
            "body": "这是一条警告消息",
            "type": "warning"
        }
    )
代码语言:python代码运行次数:0复制
# factory.py
from channels import Ding, WeChat, Email

def factory_sender(name, config):
    if name == "ding":
        return Ding(config)
    elif name == "wechat":
        return WeChat(config)
    elif name == "email":
        return Email(config)
    else:
        raise Exception("Unknown message type")
代码语言:python代码运行次数:0复制
# channels.py
class Notification:

    def send_message(self, title, body, *args, **kwargs):
        raise NotImplementedError()

class Ding(Notification):

    def __init__(self, config):
        self.token = config['token']
    
    def send_message(self, title, body, *args, **kwargs):
        print(f"Ding send message: {title} {body}")

class WeChat(Notification):

    def __init__(self, config):
        self.key = config['key']
        self.secret = config['secret']

    def send_message(self, title, body, *args, **kwargs):
        print(f"WeChat send message: {title} {body}")


class Email(Notification):

    def __init__(self, config):
        self.username = config['username']
        self.password = config['password']
    
    def send_message(self, title, body, *args, **kwargs):
        print(f"Email send message: {title} {body}")
代码语言:python代码运行次数:0复制
# config.py
message_config = [
    {
        "ding": {
            "token": "Ding Token",
        },
        "wechat": {
            "key": "WeChat Key",
            "secret": "WeChat secret"
        },
        'email': {
            'username': 'Email username',
            'password': 'Email password'
        }
    }
]
  • main.py是程序的入口,在不更改需求的前提下我们是不会去修改这个入口文件的。
  • factory.pyfactory_sender负责实例化对象。
  • channels.py就是我们开发的消息发送渠道,未来大部分工作应该专注于渠道的开发。
  • config.py是消息发送的配置项,因为不同的消息渠道有各自不同的配置。

传统模式下的一点小弊端:

我们的工作不仅限于渠道的开发,新增渠道后我们需要在factory_sender中进行手动匹配。

比如新增短信渠道就需要:elif name == "sms":

自动导入工厂模式

在自动导入模式中,我们依然会保持上面的channels.pyconfig.py文件不变。

代码语言:python代码运行次数:0复制
# main.py
from util import import_object
from config import message_config

def send_message(message: dict):
    
    for cfg in message_config:
        for key, value in cfg.items():
            Obj = import_object(f'channels.{key}')
            Obj(value).send_message(**message)

if __name__ == '__main__':

    send_message(
        {
            "title": "警告消息",
            "body": "这是一条警告消息",
            "type": "warning"
        }
    )
代码语言:python代码运行次数:0复制
# util.py
def import_object(name: str):
    """字符串导入模块方法"""
    if name.count(".") == 0:
        return __import__(name)
    parts = name.split(".")
    obj = __import__(".".join(parts[:-1]), fromlist=[parts[-1]])
    try:
        return getattr(obj, parts[-1])
    except AttributeError:
        raise ImportError("No module named %s" % parts[-1])

main.py入口文件中可以发现,原来的factory_sender方法变成了import_object

它的作用是根据传参字符串自动导入包:

"channels.Ding"即导入channels包下面的Ding类

那么事情就变得相对简单,我们依然只需要把重心放在渠道扩展上,当新增了一个渠道Sms,自动就拥有了此类的功能。

代码语言:python代码运行次数:0复制
# channels.py
……

class Sms(Notification):

    def __init__(self, config):
        ...
    
    def send_message(self, title, body, *args, **kwargs):
        print(f"Sms send message: {title} {body}")

不需要改变main.py文件,直接运行main.py主程序。

代码语言:shell复制
Ding send message: 警告消息 这是一条警告消息
WeChat send message: 警告消息 这是一条警告消息
Email send message: 警告消息 这是一条警告消息
Sms send message: 警告消息 这是一条警告消息

注册中心工厂模式

上述方式是通过动态导入类来实现的工厂模式,接下来继续使用动态语言特性,通过字典映射关系来注册类。

同样的,我们无需更改channels.pyconfig.py中的代码。

代码语言:python代码运行次数:0复制
# main.py
from config import message_config
import channels
import factory


factory.register('Ding', channels.Ding)
factory.register('WeChat', channels.WeChat)
factory.register('Email', channels.Email)

def send_message(message: dict):
    
    for cfg in message_config:
        for key, value in cfg.items():
            Obj = factory.run(key)
            Obj(value).send_message(**message)

if __name__ == '__main__':
    send_message(
        {
            "title": "警告消息",
            "body": "这是一条警告消息",
            "type": "warning"
        }
    )
代码语言:python代码运行次数:0复制
# factory.py
services = {}

def register(name, service):
    services[name] = service

def unregister(name):
    services.pop(name, None)

def run(name):
    return services[name]

不难发现,所谓注册中心,其实就是字典的关系映射,将所有渠道注册到字典中,通过字典key来调用类value。

聪明的同学会说:那我岂不是还是需要手动一个个注册这些渠道?

于是我们可以在注册中心中做个自动注册

代码语言:python代码运行次数:0复制
# main.py

from config import message_config
import factory

factory.auto_register()

def send_message(message: dict):
    
    for cfg in message_config:
        for key, value in cfg.items():
            Obj = factory.run(key)
            Obj(value).send_message(**message)

if __name__ == '__main__':
    send_message(
        {
            "title": "警告消息",
            "body": "这是一条警告消息",
            "type": "warning"
        }
    )
代码语言:python代码运行次数:0复制
# factory.py
services = {}

def register(name, service):
    services[name] = service

def unregister(name):
    services.pop(name, None)

def run(name):
    return services[name]

def auto_register():
  import channels
  for name in channels.__dict__:
    if name.startswith('__'):
      continue
    register(name, getattr(channels, name))

总结

利用python动态语言的特性,可以开发出不一样的工厂模式来解耦实际场景。本次就分享两种工程模式的设计实现。

以上案例笔者已在一项目中体现:GitHub-simple-notify

0 人点赞