【Day 08】工厂方法设计模式(Python)

前言

上一篇我们讨论DDD的战术设计,它建议引用各种设计模式,提高生产力,因此接下来,就来介绍各种设计模式(Design Patterns),我们会使用Python开发程序,由於Python的自由开放,可以使各种设计模式变的更强大,可以藉由下列程序码得到印证。

我们先从最简单的工厂方法(Factory Method)开始讨论。

工厂方法(Factory Method)

『工厂方法』顾名思义,这个设计模式是一个生产物件的工厂,使用者不须知道物件是如何生产的,只要指名要生产的物件名称,工厂就会帮使用者生产所需的物件。例如,一个工厂有生产轿车(Car)、公车(Bus)、卡车(Truck)等车子,这些车种各自定义为一个类别如下:

class Car:
    def __init__(self, maker='', num_of_doors=4,  fuel_type='oil'):
        self.maker, self.num_of_doors,  self.fuel_type = maker, num_of_doors,  fuel_type

    def __repr__(self):
        return 'I\'m a car.'

class Bus:
    def __init__(self, maker='', size=''):
        self.maker, self.size = maker, size

    def __repr__(self):
        return 'I\'m a bus.'

class Truck:
    def __init__(self, maker='', weight=10):
        self.maker, self.weight = maker, weight

    def __repr__(self):
        return 'I\'m a truck.'

每个类别的属性都不同,也不须继承同一父类别。

接着定义一个很简单的工厂方法,其中A/B/C各对应一种车子:

def FactoryInterface(classname):
    # 物件名称与类别对照表
    name_class_mapping = {'A':Car, 'B':Bus, 'C':Truck}
    
    # 建立物件
    return name_class_mapping[classname]()

关键程序码 dict1classname,直接产生一个物件。

测试看看:

# 测试
obj1 = FactoryInterface('A')
print(obj1)

obj2 = FactoryInterface('B')
print(obj2)

obj3 = FactoryInterface('C')
print(obj3)

输出如下:
I'm a car.
I'm a bus.
I'm a truck.

使用 eval(),甚至不需要 FactoryInterface 方法,虽然方便,但不易侦错,只有在执行时,才能发觉错误。

obj_name = 'Car'
obj = eval(obj_name + '()')
print(obj)

产生有不同属性的物件

上述车种类别各有不同的属性,我们如何在建立物件时,指定各自所需的属性,解决方案如下:

  1. 修改工厂方法:加一参数 arg,接收一个字典。
# 工厂方法
def FactoryInterface_2(classname, arg):
    # 物件名称与类别对照表
    name_class_mapping = {'A':Car, 'B':Bus, 'C':Truck}
    
    # 建立物件
    return name_class_mapping[classname](**arg)
  1. 指定物件时,夹带一个字典,指定物件各个参数及对应的值:
# 测试
arg = {'maker':'BMW', 'num_of_doors':4,  'fuel_type':'oil'}
obj1 = FactoryInterface_2('A', arg)
print(obj1)
print(obj1.maker)
print()

arg = {'maker':'Volvo', 'size':'large'}
obj2 = FactoryInterface_2('B', arg)
print(obj2)
print(obj2.size)
print()

输出如下,执行无误:
I'm a car.
BMW

I'm a bus.
large

接受多余的参数

有时候使用者不知道物件的详细规格,可能会多传送一些参数,如何避免错误产生,很简单,将每个物件加一个**ignore 即可,多余的参数会被它全部接收。

class Car:
    def __init__(self, maker='', num_of_doors=4,  fuel_type='oil', **ignore):
        self.maker, self.num_of_doors,  self.fuel_type = maker, num_of_doors,  fuel_type

    def __repr__(self):
        return 'I\'m a car.'

class Bus:
    def __init__(self, maker='', size='', **ignore):
        self.maker, self.size = maker, size

    def __repr__(self):
        return 'I\'m a bus.'

class Truck:
    def __init__(self, maker='', weight=10, **ignore):
        self.maker, self.weight = maker, weight

    def __repr__(self):
        return 'I\'m a truck.'

测试一下:

# 多 size 参数
arg = {'maker':'BMW', 'num_of_doors':4,  'fuel_type':'oil', 'size':'large'}
obj1 = FactoryInterface_2('A', arg)
print(obj1)
print(obj1.maker)
print()

# 多 num_of_doors 参数
arg = {'maker':'Volvo', 'num_of_doors':4, 'size':'large'}
obj2 = FactoryInterface_2('B', arg)
print(obj2)
print(obj2.size)
print()

输出如下,执行无误:
I'm a car.
BMW

I'm a bus.
large

如果传送时,缺少一些参数也不会有问题,因为每个参数都可以指定预设值,这都拜Python强大的威力。

应用时机

『工厂方法』可以用在甚麽场合呢? 以笔者开发经验,常发生在资料交换时,譬如 LINE BOT 讯息格式有很多种:

  • Text message
  • Sticker message
  • Image message
  • Video message
  • Audio message
  • Location message
  • Imagemap message
  • Template message
  • Flex Message

每一种讯息内容包含的栏位各不相同,通常会使用类别来描述每一种讯息格式,当 LINE BOT 在接收到请求时,要依据表头(Header),决定要创建何种讯息回应,这正好可以使用『工厂方法』来实践此一情境。
https://ithelp.ithome.com.tw/upload/images/20211018/20001976wXtRDAjlKJ.png

结语

从以上介绍,可以知道『工厂方法』可以用在有很多讯息种类或类别的情境中,不需要写很多的If ... elif... else,去判断要创建何种物件,透过『工厂方法』可以免除判断式的维护,同时,利用 Python 语言强大的威力,可以适用不同参数的物件,使设计模式更有弹性。

除了上面的工厂方法外,还有以下的延伸,我们後续再一一讨论,Happy coding。

  1. 抽象工厂(Abstract Factory)
  2. 物件创建者(Builder)

市面上很多设计模式的书籍都只介绍各种模式的架构与实践的程序码,缺乏实际应用的场景与应用范例,如果不是开发过很多应用系统的资深工程师,其实是很难想像如何在开发专案应用设计模式,因此,也盼望各位读者大德可以一起分享自身应用的经验,惟有如此,设计模式才有可能被广泛研究与使用。


<<:  Day 30 完赛心得

>>:  课堂笔记 - 深度学习 Deep Learning (1)

Day -10 tuple与List

tuple 可放任意物件,但不可变 ; tuple用法如下 建立多个元素 tupleSample =...

D20/ 怎麽在 compose 与 non-compoe 间传资料 - Compose Side-Effect part 2

今天大概会聊到的范围 rememberUpdateState 上一篇聊到,SideEffect 周...

Day 3 - A short introduction to gcc usage - 2

Today I will introduce the order of compiling. Fir...

Day 22: Informix(3)

Day 22: Informix(3) Concept StackOverflow-Dirty Re...

Day 11 - 基本语法6(回圈2)

昨天我们讲到的是for回圈,今天我们要讲到的也是回圈:while 正文 while 是什麽? whi...