Python Coding之设计模式

我们来分别介绍一下设计模式中的工厂模式、建造者模式、单例模式和策略模式。前三个都属于创建型模式(Creational Patterns),它们主要关注对象的创建过程,旨在将对象的创建与使用解耦,提高系统的灵活性和可维护性,策略模式属于行为型模式 (Behavioral Pattern),它侧重于对象之间的职责分配和算法封装。为了方便理解,每个模式我们都会用一个改造实例来说明。

1. 工厂模式 (Factory Pattern)

工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式,隐藏了对象的具体创建逻辑,客户端只需要知道所需产品的接口或抽象类,而无需关心其具体实现。

  • 核心思想:定义一个用于创建对象的接口(或抽象类),让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
  • 解决的问题
    • 客户端代码与具体产品类的创建紧密耦合。如果需要更换或增加产品,就需要修改客户端代码。
    • 对象的创建过程可能比较复杂,包含很多初始化逻辑,不希望这些逻辑散布在客户端各处。
  • 主要变体
    • 简单工厂模式 (Simple Factory Pattern):一个工厂类根据传入的参数决定创建哪种产品类的实例。严格来说它不属于 GoF 23 种设计模式,但非常常用。缺点是增加新产品需要修改工厂类,违反开闭原则。
    • 工厂方法模式 (Factory Method Pattern):定义一个创建对象的接口,但让实现这个接口的子类来决定实例化哪个类。每个具体产品对应一个具体工厂。符合开闭原则。
    • 抽象工厂模式 (Abstract Factory Pattern):提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。它创建的是一个“产品族”。
  • 优点
    • 解耦:将对象的创建和使用分离,客户端不依赖具体产品类。
    • 灵活性:更容易更换、增加新的产品实现。
    • 封装性:隐藏了复杂的对象创建逻辑。
  • 缺点
    • 每增加一个产品,可能需要增加一个具体产品类和对应的工厂类(工厂方法模式),导致类的个数成倍增加。
    • 抽象工厂模式理解和实现相对复杂。
  • 适用场景
    • 当你不知道需要创建的具体对象类型时。
    • 当你希望将对象的创建与使用分离时。
    • 当你需要创建一系列相关的对象时(抽象工厂)。
    • 例如:数据库连接 Connection 的创建(根据不同数据库类型返回不同的 Connection 实现)、日志记录器、UI 控件的创建等。

1.1原代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DatabaseConnection:
def __init__(self, host, port, username, password):
self.host = host
self.port = port
self.username = username
self.password = password

def connect(self):
return f'连接到数据库 host:{self.host}, port:{self.port}, username{self.username}'

def client():
main_db = DatabaseConnection('localhost',3306,'root','password123')
analysis_db = DatabaseConnection('localhost',3307,'root','password123')
cache_db = DatabaseConnection('localhost',3308,'root','password123')

print(main_db.connect())
print(analysis_db.connect())
print(cache_db.connect())

if __name__ == '__main__':
client()

1.2工厂模式改造:

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
# 工厂模式优化
'''
配置与使用分离:工厂模式将数据库连接的配置信息与客户端代码分离,客户端只需知道需要什么类型的数据库连接,而无需了解具体的配置细节。
降低耦合度:客户端代码与具体的数据库连接参数解耦,只依赖于工厂方法和数据库类型字符串。
集中管理配置:所有数据库连接的配置信息集中在一个地方管理(db_configs字典),使配置更易于维护和修改。
'''
class DatabaseConnection:
def __init__(self, host, port, username, password):
self.host = host
self.port = port
self.username = username
self.password = password

def connect(self):
return f'连接到数据库 host:{self.host}, port:{self.port}, username{self.username}'

def connection_factory(db_type):
'''
读取配置文件,并创建对象
'''
# 可将该部分移动至配置文件
db_configs = {
'main':{
'host':'localhost',
'port':'3306',
'username':'root',
'password':'password123',
},
'analysis':{
'host':'localhost',
'port':'3307',
'username':'root',
'password':'password123',
},
'cache':{
'host':'localhost',
'port':'3308',
'username':'root',
'password':'password123',
},

}
# 创建对象
return DatabaseConnection(**db_configs[db_type])

def client():
main_db = connection_factory('main')
analysis_db = connection_factory('analysis')
cache_db = connection_factory('cache')

print(main_db.connect())
print(analysis_db.connect())
print(cache_db.connect())

if __name__ == '__main__':
client()

2. 建造者模式 (Builder Pattern)

建造者模式是一种创建型设计模式,它将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。它允许你分步骤地构建复杂对象。

  • 核心思想:使用多个简单的对象一步一步构建成一个复杂的对象。一个 Builder 类通常用于封装构建过程的细节。
  • 解决的问题
    • 当一个对象的构造函数参数过多(尤其是很多可选参数)时,构造函数会变得非常冗长和难以使用(Telescoping Constructor 问题)。
    • 直接通过 setter 方法设置属性,可能导致对象在构建完成前处于不一致或不完整状态。
    • 需要精细控制对象的构建过程,或者构建过程有多个步骤。
  • 主要组成部分
    • Product(产品):要构建的复杂对象。
    • Builder(抽象建造者):定义了构建产品各个部分的接口。
    • ConcreteBuilder(具体建造者):实现了 Builder 接口,负责具体构建和装配产品的各个部分。
    • Director(指挥者,可选):负责安排构建步骤的顺序,使用 Builder 接口来构建产品。客户端也可以直接充当指挥者。
  • 优点
    • 封装性好:构建过程和最终表示分离。
    • 控制精细:可以分步构建,更好地控制构建过程。
    • 代码可读性强:对于多参数对象,链式调用 builder.setXxx().setYyy().build() 比长参数列表的构造函数更清晰。
    • 易于扩展:可以方便地增加新的具体建造者来改变产品的内部表示。
  • 缺点
    • 需要为每个产品创建对应的 Builder 类,增加了代码量。
    • 模式本身相对复杂一些。
  • 适用场景
    • 需要创建的对象具有复杂的内部结构(多个组成部分)。
    • 对象的属性之间有依赖关系或创建顺序有要求。
    • 希望对象的构建过程和最终表示分离。
    • 当构造函数参数过多时,或者想创建不可变对象时。
    • 例如:StringBuilder / StringBuffer(虽然简单,但体现了分步构建思想)、构建复杂的配置对象、生成复杂的报表文档、组装车辆等。

2.1原代码

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
# 原代码
'''
参数较多,容易输错
'''
class DatabaseConnection:
def __init__(self, host, port, username, password,
max_connections=None, timeout=None,
enable_ssl=False,
ssl_cert=None, connection_pool=None,
retry_attempts=None,
compression=False, read_preference=None):
self.host = host
self.port = port
self.username = username
self.password = password
self.max_connections = max_connections

# validate timeout
if timeout is not None and timeout <= 0:
raise ValueError("Connect timeout must be positive")
self.timeout = timeout
self.enable_ssl = enable_ssl
self.ssl_cert = ssl_cert
self.connection_pool = connection_pool
self.retry_attempts = retry_attempts
self.compression = compression
self.read_preference = read_preference

def connect(self):
return f"Connecting to database at {self.host}:{self.port} with username '{self.username}'"
def client():
connection = DatabaseConnection(
"localhost", 5432, "admin", "password",
max_connections=100, timeout=30, enable_ssl=True,
ssl_cert="/path/to/cert", connection_pool=20,
retry_attempts=3, compression=True,
read_preference="secondary"
)
print(connection.connect())

if __name__ == "__main__":
client()

2.2建造者模式改造:

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
# 建造模式改造
class DatabaseConnection:
def __init__(self, host, port, username, password,
max_connections=None, timeout=None,
enable_ssl=False,
ssl_cert=None, connection_pool=None,
retry_attempts=None,
compression=False, read_preference=None):
self.host = host
self.port = port
self.username = username
self.password = password
self.max_connections = max_connections

# validate timeout
if timeout is not None and timeout <= 0:
raise ValueError("Connect timeout must be positive")
self.timeout = timeout
self.enable_ssl = enable_ssl
self.ssl_cert = ssl_cert
self.connection_pool = connection_pool
self.retry_attempts = retry_attempts
self.compression = compression
self.read_preference = read_preference

def connect(self):
return f"Connecting to database at {self.host}:{self.port} with username '{self.username}'"

class DatabaseConnectionBuilder:
# 必选变量
def __init__(self, host, port, username, password):
self._config = {
'host' : host,
'port' : port,
'username' : username,
'password' : password,
}

def set_max_connections(self, max_connections):
self._config['max_connections'] = max_connections
return self #链式调用

def set_timeout(self, timeout):
if timeout <0:
raise ValueError('超时时间必须为正数')
self._config['timeout'] = timeout
return self
def enable_ssl(self, ssl_cert=None):
self._config['enable_ssl'] = True
self._config['ssl_cert'] = ssl_cert
return self

def set_connection_pool(self, pool_size):
self._config['connection_pool'] = pool_size
return self

def set_retry_attempts(self, attempts):
self._config['retry_attempts'] = attempts
return self

def enable_compression(self):
self._config['compression'] = True
return self

def set_read_preference(self, preference):
self._config['read_preference'] = preference
return self

def build(self):
return DatabaseConnection(**self._config)


def client():
builder = DatabaseConnectionBuilder("localhost", 5432, "admin", "password")

connection = builder.set_max_connections(200)\
.set_timeout(30)\
.enable_ssl(ssl_cert='/path/to/mongo')\
.set_connection_pool(30)\
.set_retry_attempts(5)\
.enable_compression()\
.set_read_preference('primarity')\
.build()

print(connection.connect())

if __name__ == "__main__":
client()

3. 单例模式 (Singleton Pattern)

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

  • 核心思想:限制类的实例化,保证在整个应用程序生命周期中,特定类只有一个对象存在。
  • 解决的问题
    • 某些类只需要一个实例来协调系统全局的行为,例如配置管理器、日志记录器、线程池、数据库连接池等。
    • 频繁创建和销毁全局使用的对象会造成性能开销。
    • 需要一个全局唯一的访问点来获取这个实例。
  • 实现要点
    • 私有化构造函数:防止外部通过 new 操作符直接创建实例。
    • 静态私有成员变量:持有类的唯一实例。
    • 静态公有工厂方法(通常命名为 getInstance()):负责创建(如果尚未创建)并返回这个唯一实例。
  • 常见实现方式
    • 饿汉式 (Eager Initialization):类加载时就创建实例,线程安全,但可能造成资源浪费(如果实例一直未使用)。
    • 懒汉式 (Lazy Initialization):第一次调用 getInstance() 时才创建实例。需要处理多线程环境下的线程安全问题(例如使用 synchronized 或双重检查锁定 Double-Checked Locking)。
    • 静态内部类 (Static Inner Class):结合了懒加载和线程安全,推荐使用。
    • 枚举 (Enum):最简洁、最安全的实现方式,可以防止反射和反序列化攻击,推荐使用。
  • 优点
    • 保证唯一实例:确保了资源的一致性访问(如配置文件)。
    • 全局访问点:方便访问。
    • 延迟实例化(懒汉式):节省资源。
  • 缺点
    • 违反单一职责原则:类既负责业务逻辑,又负责管理自己的实例。
    • 隐藏依赖:全局状态使得代码的依赖关系不明确,不易于理解和测试。
    • 可测试性差:全局状态很难模拟(mock)和隔离,给单元测试带来困难。
    • 对继承不友好:私有构造函数限制了继承。
    • 多线程问题:懒汉式需要特别注意线程安全。
    • 常被认为是反模式(Anti-Pattern)或代码坏味(Code Smell),应谨慎使用。
  • 适用场景
    • 需要严格控制实例数量的类,例如系统日志、配置信息类、线程池、计数器等。
    • 需要全局访问的共享资源。

3.1原函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 原函数
class DatabaseConnection:
def __init__(self, host, port, username, password):
self.host = host
self.port = port
self.username = username
self.password = password

def connect(self):
return f'连接到数据库 host:{self.host}, port:{self.port}, username{self.username}'

def client():
# 重复被创建的对象,占用额外资源,造成浪费
db_1 = DatabaseConnection('localhost',3306,'root','password123')
db_2 = DatabaseConnection('localhost',3306,'root','password123')

print(db_1 is db_2)


if __name__ == '__main__':
client()

3.2单例模式改造

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
class DatabaseConnection:
# 类变量,用于存储单例实例
_instance = None

def __new__(cls, host=None, port=None, username=None, password=None):
# 如果实例不存在,则创建一个
if cls._instance is None:
cls._instance = super(DatabaseConnection, cls).__new__(cls)
# 初始化实例属性
cls._instance.host = host
cls._instance.port = port
cls._instance.username = username
cls._instance.password = password
return cls._instance

def connect(self):
return f'连接到数据库 host:{self.host}, port:{self.port}, username:{self.username}'

def client():
# 创建两个看似不同的对象,但实际上是同一个实例
db_1 = DatabaseConnection('localhost', 3306, 'root', 'password123')
db_2 = DatabaseConnection('localhost', 3306, 'root', 'password123')

# 此时应该输出 True,因为它们是同一个对象
print(db_1 is db_2)

# 测试连接功能
print(db_1.connect())

if __name__ == '__main__':
client()

好的,我们来详细讲解一下设计模式中的策略模式 (Strategy Pattern)

策略模式属于行为型模式 (Behavioral Pattern),它侧重于。

4.策略模式 (Strategy Pattern)

  • 定义:策略模式定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户。
  • 核心思想:将可能发生变化的行为(算法)抽象出来,定义一个统一的接口(策略接口),然后为每种具体的行为(算法)提供一个实现类(具体策略类)。在使用时,客户端(上下文)可以根据需要选择并注入具体的策略对象,从而动态地改变对象的行为。
  • 解决的问题
    • 避免在一个类中使用大量的 if-elseswitch-case 语句来选择不同的行为逻辑,这使得代码难以维护和扩展。
    • 当有多种算法或行为,并且它们之间可以互换时,使用策略模式可以更好地组织代码。
    • 希望算法的实现细节对客户端透明。
  • 主要组成部分
    • Context(上下文):
      • 持有一个 Strategy 接口的引用。
      • 通常有一个方法,该方法会调用 Strategy 对象的算法方法。
      • Context 不知道具体的策略实现,只与 Strategy 接口交互。
      • 可以提供一个方法(如 setStrategy())来让客户端在运行时切换策略。
    • Strategy(策略接口或抽象类):
      • 定义了所有支持的算法的公共接口。
      • Context 使用这个接口来调用某个 ConcreteStrategy 定义的算法。
    • ConcreteStrategy(具体策略类):
      • 实现了 Strategy 接口。
      • 封装了具体的算法或行为。
      • 每个 ConcreteStrategy 代表一种特定的算法实现。

工作流程:

  1. 客户端决定使用哪种具体策略。
  2. 客户端创建一个具体策略 (ConcreteStrategy) 对象。
  3. 客户端将这个具体策略对象设置到上下文 (Context) 对象中。
  4. 当客户端请求上下文执行某个操作时,上下文会将请求委托给它所持有的策略对象的相应方法去执行。

优点:

  1. 遵循开闭原则:可以轻松地增加新的策略(算法),而无需修改上下文类或其他现有策略类。只需添加新的 ConcreteStrategy 实现即可。
  2. 避免多重条件语句:将 if-elseswitch 逻辑分散到各个策略类中,使得上下文类更简洁,职责更单一。
  3. 算法封装:每个算法都封装在独立的策略类中,易于理解、测试和维护。
  4. 策略切换灵活:客户端可以在运行时动态地改变上下文对象的策略(行为)。
  5. 复用性:策略类可以被多个上下文对象复用。

缺点:

  1. 类的数量增多:每增加一种策略就需要增加一个类,可能导致类的数量大幅增加。
  2. 客户端需要了解策略:客户端必须知道有哪些不同的策略,并需要自行决定在何时使用何种策略,然后将策略对象设置给上下文。(可以通过结合工厂模式等来隐藏具体策略类,减轻客户端的负担)。
  3. 上下文与策略间的通信:如果策略需要访问上下文的数据,可能需要将上下文对象(this)传递给策略,或者定义更复杂的数据传递接口,这可能导致一定的耦合。

适用场景:

  1. 当一个系统需要在多种算法中选择一种时,可以将这些算法封装成独立的策略类。
  2. 当一个对象有很多行为,并且这些行为使用 if-elseswitch 语句来选择时,可以考虑使用策略模式。
  3. 当一个算法的实现细节不希望暴露给客户端时。
  4. 当希望算法可以独立于使用它的客户端进行变化时。
  5. 需要动态地在几种算法中切换的场景。

4.1原代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 原代码功能冗杂,易读性差,拓展性差

class FileProcessor:
def __init__(self, file_path):
self.file_path = file_path
self.file_type = file_path.split('.')[-1]

def process_excel(self):
print('Processing excel file')

def process_csv(self):
print('Processing csv file')

def process_txt(self):
print('Processing txt file')

def process_file(self):
if self.file_type == 'xlsx':
self.process_excel()
elif self.file_type == 'csv':
self.process_csv()
elif self.file_type == 'txt':
self.process_txt()

4.2策略模式改造

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
# 策略模式改造
from abc import ABC, abstractmethod
'''
使用不同方法做同一件事,如处理不同格式文件
'''

# 该写法使基类不可被示例化,且继承时必须实现抽象方法()
class processStrategy(ABC):
@abstractmethod
def process(self, filepath):
pass

class ExcelStrategy(processStrategy):
def process(self, filepath):
print(f"Processing Excel file: {filepath}")
# Add Excel processing logic here

class CsvStrategy(processStrategy):
def process(self, filepath):
print(f"Processing csv file: {filepath}")
# Add Excel processing logic here

class TxtStrategy(processStrategy):
def process(self, filepath):
print(f"Processing txt file: {filepath}")
# Add Excel processing logic here

class FileProcessor:
def __init__(self, filepath):
self.file_path = filepath
self.file_type = filepath.split('.')[-1]

def process_file(self):
if self.file_type == 'xlsx':
strategy = ExcelStrategy()
elif self.file_type == 'csv':
strategy = CsvStrategy()
elif self.file_type == 'txt':
strategy = TxtStrategy()
else:
raise ValueError("Unsupported file type")

strategy.process(self.file_path)
if __name__ == "__main__":
file_path = 'example.xlsx' # Change this to test different file types
processor = FileProcessor(file_path)
processor.process_file()

5.总结

  • 工厂模式:侧重于隐藏创建逻辑,根据需要返回不同类型的对象实例。
  • 建造者模式:侧重于分步构建复杂对象,控制构建过程,得到不同表示的对象。
  • 单例模式:侧重于保证全局唯一实例,并提供全局访问点。
  • 策略模式:侧重于对象之间的职责分配和算法抽象封装与实例分离

理解这些模式的核心意图和它们所解决的问题,有助于在实际开发中选择合适的模式来优化代码结构,提高代码的可维护性、可扩展性和可重用性。


Python Coding之设计模式
https://linxkon.github.io/python与设计模式.html
作者
linxkon
发布于
2022年4月20日
许可协议