设计模式(1)

如无特殊说明,本站设计模式相关文章均使用Python3实现(后期也可能会加入Go)

本文最近一次更新时间为20220325

个人理解,设计模式是一种指导思想,一种类似于内功心法的东西,讲究的就是一个融会贯通,了然于胸。总结学习以下设计模式,也是为了将来再面对问题时,更有“抓手”。写代码设计层,总让我有一种高中数学/物理或初中语文的感觉,一切似乎虚无缥缈,但冥冥中自己的心又似乎受到牵引,不失方向。想起物理老师的一段话:解题有法,但无定法;无法之法,是为智法。不要问我为什么,宇宙就是甩出来的~

设计模式

设计模式,是指软件设计问题的推荐方案(既不是唯一,也不一定存在最优)。设计模式一般是描述如何组织代码和使用最佳实践来解决常见的设计问题,但并不会关注具体的实现细节,是一种高层次的方案,是一种指导性的方案。

随着技术和语言的演进,一些设计模式可能过时了,也可能被内置到语言中。

需要注意的是,写代码就应该使用设计模式这种想法是不对的,1+1又何必演化成1+2-3+2呢?设计模式是在已有的方案上发现更好的方案,而不是发明或者直接空中楼阁。不要为了设计模式而设计模式,设计模式也绝非万能,一般来说,只有代码存在坏味道的时候才有使用的必要,简单直白几乎不会出错,不要为了秀而瞎搞。

我会总结归纳实践三种大类十六中小类的设计模式:

  • 创建型模式:处理对象创建相关的问题。目的是当创建对象不太方便时,提供更好的方式。
    • 工厂模式
    • 建造者模式
    • 原型模式
  • 结构型模式
    • 适配器模式
    • 修饰器模式
    • 外观模式
    • 享元模式
    • MVC模式(模型-视图-控制器模式)
    • 代理模式
  • 行为型模式
    • 责任链模式
    • 命令模式
    • 解释器模式
    • 观察者模式
    • 状态模式
    • 策略模式
    • 模板模式

创建型模式

工厂模式

调用者请求对象的时候,无需知道这个对象来自于哪里(也就是使用哪个类来生成的对象)。工厂模式的思想是简化对象的创建。基于一个中心化函数来实现对象,更易于追踪创建了哪些对象,并且将创建对象的代码和使用对象的代码解耦,降低维护的复杂度。

工厂通常有两种形式:

  • 工厂方法:对不同的输入参数返回不同的对象
  • 抽象工厂:一组用于创建一系列相关事物对象的工厂方法

工厂方法

定性:执行单个函数,传入一个参数(提供信息表明我们想要什么),但并不要求知道任何关于对象如何实现以及对象来自哪里的细节。

比如Django使用工厂方法模式创建表单字段。Django中的forms模块支持不同种类字段的创建(CharField,EmailField)和定制(max_length、required)。

适用情况

如果应用创建对象的代码分布在多个不同的地方难以跟踪,那么可以使用工厂方法模式。集中在一个地方创建对象,使对象跟踪更为容易;当然,创建多个工厂方法也完全没有问题。比如对相似的对象进行逻辑分组,每个工厂方法只负责一个分组。例如一个工厂方法负责连接到不同的数据库,另外一个负责创建一个查询函数。

如果想要达到对象的创建和使用进行解耦,工厂方法也是适合的。创建对象时,并没有与某个特定的类耦合或者绑定,而只是通过调用某个函数来提供关于我们想要的部分信息。

另外就是工厂方法可以在有必要的时候创建新的对象,从而提供性能和内存使用率。

Python例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# coding:utf-8

import json
import xml.etree.ElementTree as etree


class JsonConn:
def __init__(self, path):
self.data = dict()
with open(path, mode="r", encoding="utf-8") as f:
self.data = json.load(f)

@property
def parsed_data(self):
return self.data


class XMLConn:
def __init__(self, path):
self.tree = etree.parse(path)

@property
def parsed_data(self):
return self.tree


def conn_factory(path):
if path.endwith("json"):
connector = JsonConn
elif path.endwith("xml"):
connector = XMLConn
else:
raise ValueError(f"cannot connect to {path}")
return connector(path)


def conn(path):
factory = None
try:
factory = conn_factory(path)
except ValueError as e:
print(e)
return factory


def main():
f = conn("/data/xxx.json")
if not f:
pass
data = f.parsed_data
# do sth with data

虽然上面两个有相同的parsed_data接口,但是接口返回的数据并不能统一处理。

抽象工厂

抽象工厂设计模式是抽象方法的一种泛化。一个抽象工厂是(逻辑上的)一组工厂方法,其中的每个工厂方法负责产生不同种类的对象。

适用情况

抽象工厂模式是工厂方法模式的一种泛化,所以他的优点和工厂方法模式一样:更容易追踪对象的创建、将对象的创建和使用解耦、提供优化内存占用和性能的可能。

那什么时候使用呢?通常一开始使用工厂方法,因为他更简单。如果后来发现应用需要许多工厂方法,那么将创建一系列对象的过程合并到一起更合理,也就是使用了抽象工厂。

抽象工厂一个通常被忽视的优点是能够通过改变激活的工厂方法动态地改变应用行为。

Python例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# coding:utf-8

class Bee:
def __init__(self, name):
self.name = name

def __str__(self):
return self.name

def interact_with(self, obstacle):
print(f"{self} see {obstacle} and {obstacle.action()}")


class Flower:
def __str__(self):
return "a flower"

def action(self):
return "make honey"


class BeeWorld:
def __init__(self, name):
print(self)
self.player_name = name

def __str__(self):
return "\n\n\t-------Bee World--------"

def make_character(self):
return Bee(self.player_name)

def make_action(self):
return Flower()


class DNF:
def __init__(self, name):
self.name = name

def __str__(self):
return self.name

def interact_with(self, obstacle):
print(f"{self} against {obstacle} and {obstacle.action()}")


class BadMan:
def __str__(self):
return "a bad man"

def action(self):
return "kill him"


class DNFWorld:
def __init__(self, name):
print(self)
self.player_name = name

def __str__(self):
return "\n\n\t-------DNF World--------"

def make_character(self):
return DNF(self.player_name)

def make_action(self):
return BadMan()


class GameEnv:
def __init__(self, factory):
self.hero = factory.make_character()
self.obstacle = factory.make_action()

def play(self):
self.hero.interact_with(self.obstacle)


def validate_age(name):
try:
age = input(f"welcome {name}. how old are you?")
age = int(age)
except ValueError as e:
print(f"age {age} error!")
return False, age
return True, age


def main():
name = input("请输入用户名:")
valid_input = False
while not valid_input:
valid_input, age = validate_age(name)
game = BeeWorld if age < 18 else DNFWorld
environment = GameEnv(game(name))
environment.play()


if __name__ == '__main__':
main()
输出:
请输入用户名:just
welcome just. how old are you?15


-------Bee World--------
just see a flower and make honey

总结

工厂模式适用于

  • 想要追踪对象的创建
  • 想要将对象的创建和使用解耦
  • 优化性能和资源占用

工厂方法设计模式的实现是一个不属于任何类的单一函数,负责单一种类对象的创建(比如数据库、一些逻辑上类似的方法)。

抽象工厂设计模式的实现是同属于单个类的许多个工厂方法用于创建一系列种类的相关对象(比如一个游戏环境)。

建造者模式

建造者模式是 将一个复杂对象的构造过程与其表现分离,这样,同一个构造过程可用于创建多个不同的表现。该模式中,有两个参与者:建造者(builder) 和指挥者(director)。建造者负责创建复杂对象的各个组成部分。指挥者使用一个建造者实例控制建造的过程。

适用情况

如果我们知道一个对象必须经过多个步骤来创建,并且要求同一个构造过程可以产生不同的表现,就可以使用建造者模式。

建造者模式也可用于解决可伸缩构造函数问题。 当我们为支持不同的对象创建方式而不得不创建一个新的构造函数时,可伸缩构造函数问题就发生了,这种情况最终产生许多构造函数和长长的形参列表,难以管理。但是在Python中可以用以下两种方式解决:

  • 适用命名形参
  • 适用实参列表展开

在这一点上,建造者模式和工厂模式的差别并不太明确。主要的区别在于工厂模式以单个步骤创建对象,而建造者模式以多个步骤创建对象,并且几乎始终会使用一个指挥者。一些有针对性的建造者模式实现并未使用指挥者,如Java的StringBuilder,但这只是例外。

另一个区别是,在工厂模式下,会立即返回一个创建好的对象;而在建造者模式下,仅在需要时客户端代码才显式地请求指挥者返回最终的对象

Python例子

使用建造者模式实现一个订购包子的应用。面团有发面团和死面团,馅料有猪肉和牛肉,包法有天津包法和东北包法,包完后有速冻和已经做好的包子(决定是否有蒸的时间)。

最终的产品是一个包子,由BaoZi类描述。若使用建造者模式,则最终产品(类)并没有多少职责,因为它不支持直接实例化。建造者会创建一个最终产品的实例,并确保这个实例完全准备好。这就是BaoZi类这么短小的缘由。它只是将所有数据初始化为合理的默认值,唯一的例外 是方法prepare_dough()。将prepare_dough方法定义在BaoZi类而不是建造者中,是考虑到 以下两点。

  • 为了澄清一点,就是虽然最终产品类通常会最小化,但这并不意味着绝不应该给它分配任何职责
  • 为了通过组合提高代码复用

在这个例子中,指挥者就是服务员。Waiter类的核心是construct_包子方法,该方法接 受一个建造者作为参数,并以正确的顺序执行包子的所有准备步骤。选择恰当的建造者(甚至可以在运行时选择),无需修改指挥者(Waiter)的任何代码,就能制作不同的包子。Waiter类 还包含baozi()方法,会向调用者返回最终产品(准备好的包子),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from enum import Enum

BaoZiProgress = Enum("BaoZiProgress", "queued preparation ready")
BaoZiDough = Enum("BaoZiDough", "ferment unferment")
BaoZiStuffing = Enum("BaoZiStuffing", "pork beef")
BaoZiMethod = Enum("BaoZiMethod", "steam freeze")


# BaoZiDough = Enum("BaoZiDough", "发酵 死面")
# BaoZiStuffing = Enum("BaoZiStuffing", "猪肉 牛肉")
# BaoZiMethod = Enum("BaoZiMethod", "蒸熟 速冻")

class BaoZi:
def __init__(self, name):
self.name = name
self.dough = None
self.stuffing = None
self.method = None

def __str__(self):
return self.name

def prepare_dough(self, dough):
self.dough = dough
print(f"在为你的 {self.name} 准备 {dough.name} 面团")


class PorkBaoZiBuilder:
def __init__(self):
self.baozi = BaoZi("猪肉包子")
self.progress = BaoZiProgress.queued

def prepare_dough(self):
self.progress = BaoZiProgress.preparation
self.baozi.prepare_dough(BaoZiDough.ferment)

def add_stuffing(self):
print("往里加猪肉馅了~")
self.baozi.stuffing = BaoZiStuffing.pork
print("猪肉馅放好了~")

def choose_method(self):
print("直接给你蒸了哈~")
self.baozi.method = BaoZiMethod.steam
print("猪肉馅包子蒸熟了")

def deliver(self):
self.progress = BaoZiProgress.ready
print("猪肉馅包子出餐了~自取还是送餐?你自取吧~")


class BeefBaoZiBuilder:
def __init__(self):
self.baozi = BaoZi("牛肉包子")
self.progress = BaoZiProgress.queued

def prepare_dough(self):
self.progress = BaoZiProgress.preparation
self.baozi.prepare_dough(BaoZiDough.unferment)

def add_stuffing(self):
print("往里加牛肉馅了~")
self.baozi.stuffing = BaoZiStuffing.beef
print("牛肉馅放好了~")

def choose_method(self):
print("牛肉馅包子只能速冻了~")
self.baozi.method = BaoZiMethod.freeze
print("牛肉馅包子速冻好了~")

def deliver(self):
self.progress = BaoZiProgress.ready
print("牛肉包子出餐了~自取还是送餐?你自取吧~")


class Waiter:
def __init__(self):
self.builder = None

def construct_baozi(self, builder):
self.builder = builder
[step() for step in (builder.prepare_dough, builder.add_stuffing,
builder.choose_method, builder.deliver)]

@property
def baozi(self):
return self.builder.baozi


def validate_style(builders):
try:
baozi_style = input('想要什么包子? [p]猪肉 or [b]牛肉? ')
builder = builders[baozi_style]()
except KeyError as err:
print('Sorry, only margarita (key m) and creamy bacon (key c) are available')
return (False, None)
return (True, builder)


"""
实现的最后一部分是main()函数。main()函数实例化一个包子建造者,然后指挥者Waiter
使用包子建造者来准备包子。创建好的包子可在稍后的时间点交付给客户端。
"""


def main():
builders = dict(p=PorkBaoZiBuilder, b=BeefBaoZiBuilder)
valid_input = False
while not valid_input:
valid_input, builder = validate_style(builders)
print("-" * 20)
waiter = Waiter()
waiter.construct_baozi(builder)
baozi = waiter.baozi
print("-" * 20)
print('好好吃你的 {} 吧!'.format(baozi))


if __name__ == '__main__':
main()

输出:
想要什么包子? [p]猪肉 or [b]牛肉? b
--------------------
在为你的 牛肉包子 准备 unferment 面团
往里加牛肉馅了~
牛肉馅放好了~
牛肉馅包子只能速冻了~
牛肉馅包子速冻好了~
牛肉包子出餐了~自取还是送餐?你自取吧~
--------------------
好好吃你的 牛肉包子 吧!

变体

如果 链式 的调用建造者方法,通过将建造者本身定义为内部类并从其每个设置器方法返回自身的方式返回最终对象,可以称为流利的建造者模式。但还是要有一个方法返回我们想要的最终类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ColdNoodles:
def __init__(self, builder):
self.egg = builder.egg
self.beef = builder.beef

def __str__(self):
egg = "yes" if self.egg else "no"
beef = "yes" if self.beef else "no"
return f"egg: {egg}, beef: {beef}"
class Builder:
def __init__(self):
self.egg = False
self.beef = False

def add_egg(self):
self.egg = True
return self

def add_beef(self):
self.beef = True
return self
def builder(self):
# 这里的self指的是类Builder的实例
return ColdNoodles(self)

cold_noodles = ColdNoodles.Builder().add_beef().add_egg().builder()
print(cold_noodles)

输出:
egg: yes, beef: yes

上面的这个例子可以在add_beefadd_egg中增加参数实现动态的建造。只有当调用builder函数的时候才能返回coldnoodles这个类的实例化对象,当然你也可以传一个builder对象进去实现实例化~

总结

在以下几种情况下,与工厂模式相比,建造者模式是更好的选择。

  • 想要创建一个复杂对象(对象由多个部分构成,且对象的创建要经过多个不同的步骤, 这些步骤也许还需遵从特定的顺序)
  • 要求一个对象能有不同的表现,并希望将对象的构造与表现解耦
  • 想要在某个时间点创建对象,但在稍后的时间点再访问

原型模式

原型设计模式(Prototype design pattern)帮助我们创建对象的克隆,其最简单的形式就是一个clone()函数,接受一个对象作为输入参数,返回输入对象的一个副本。在Python中,这可以使用copy.deepcopy()函数来完成。

适用情况

当我们已经有一个对象,并希望创建该对象的一个完整副本的时候,就可以使用原型模式。比如想修改某些功能但是还想保持原对象不变,就可以使用原型模式。

上面提到的副本都是深副本,浅副本指的是引用(就像deepcopy和copy)

我们可以引入数据共享和写时复制一类的技术来优化性能(例如, 减小克隆对象的创建时间)和内存使用。如果可用资源有限(例如,嵌入式系统)或性能至关重要(例如,高性能计算),那么使用浅副本可能更佳。

Python例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import copy


class Book:
def __init__(self, name, authors, price, **kwargs):
self.name = name
self.authors = authors
self.price = price
self.__dict__.update(kwargs)

def __str__(self):
string_list = list()
for k, v in self.__dict__.items():
string_list.append(f"{k}: {v}")
return "; ".join(string_list)


class NewBook:
def __init__(self):
self.objects = dict()

def register(self, ident, obj):
self.objects[ident] = obj

def unregister(self, ident):
del self.objects[ident]

def clone(self, ident, **kwargs):
found = self.objects.get(ident)
if not found:
raise ValueError("not found ident")
obj = copy.deepcopy(found)
obj.__dict__.update(kwargs)
return obj


def main():
old_book = Book(name="西游记", authors="曹雪芹", price="998刀", publisher="作家出版社")
all_book = NewBook()
CIP = "978-7-111-55797-5"
all_book.register(CIP, old_book)
new_book = all_book.clone(CIP, edition=2)
for i in (old_book, new_book):
print(i)


if __name__ == '__main__':
main()
输出:
name: 西游记; authors: 曹雪芹; price: 998刀; publisher: 作家出版社
name: 西游记; authors: 曹雪芹; price: 998刀; publisher: 作家出版社; edition: 2

总结

原型模式用于创建对象的完全副本。确切地说,创建一个对象的副本可以指代以下两件事情:

  • 当创建一个浅副本时,副本依赖引用
  • 当创建一个深副本时,副本复制所有东西

第一种情况中,我们关注提升应用性能和优化内存使用,在对象之间引入数据共享,但需要小心地修改数据,因为所有变更对所有副本都是可见的。

第二种情况中,我们希望能够对一个副本进行更改而不会影响其他对象,不会进行数据共享,所以需要关注因对象克隆而引入的资源耗用问题。

创建型模式总结

创建型模式,更多的是为了更好的创建对象而设计、总结、实践的,平时开发中可能用的相对较少。

工厂模式:生产对象的工厂;提供一个接口,传入参数生产对应类型的对象。适用于比如数据库切换的场景。

抽象工厂模式:生产工厂的工厂。在工厂方法模式中,我们的具体创建者每次使用都只能创建一个同类型的对象,假如我们现在需要的是多个不同类型的对象,工厂方法就满足不了需求了。此时就可以进一步封装,使用抽象工厂模式。

建造者模式:打包创建对象,将对象拆分(比如电脑拆成主机、键盘、显示器等等)。

原型模式:创建好一个对象后,放到一个地方,然后可以基于它实现新的对象(copy)。

如果觉得写的还行,赞助瓶脉动~
0%