柏拉图、萨特、设计模式

柏拉图、萨特、设计模式

模板方法模式

本体论是哲学中的根本问题之一,它试图回答我们所经历的世界的根本本质,回答在我们所生活的世界中,最真实的事实是什么样的。柏拉图在思考这个问题的时候,提出了有趣的理型论。他认为所有存在的事物都有其对应的理型,理型有点像现实世界中的“模板”或“原型,就像烤饼干和制作饼干的模子一样。或者比如当我们看到一张桌子时,我们会认为它是“桌子”的一个例子,但这张桌子可能有各种各样的形状、大小、材料。柏拉图认为,除了我们所见到的这些具体的桌子之外,还有一个超越我们感官经验的纯粹世界,那里存在着完美的“桌子”,这个“桌子”是所有具体桌子的理想化表达。这个超越感官世界的层面,就是柏拉图所说的理型世界。

柏拉图的世界观,对应的方法就是要制造具体的事物,就要先制造这个事物的模板。假如我是一个小老板开了个工厂,要生产一系列不同风格的产品。每个产品的工序都是一样的,但是由于产品类型的不同,每一道工序用到的具体工艺制造生产线会有所区别,但也有一些相同的基本生产线。这时候想设计各种产品的制造流程单,一个清晰的方法是先制定好一套工序模板,然后对于相同的生产线,直接部署到工序模板中。当想制造某一款特定的产品时,就在这个模板的基础上,继续指定它专属的生产线。用这样的方式,不同产品的制造过程易于理解,也易于管理。

对于计算机程序的场景也是类似的,很多的程序都具有这样的结构,先进行初始化,然后接着进入主循环,在主循环中完成需要做的工作。最后,一旦完成了工作,程序就退出主循环,并且在程序中止前做些清理工作。比如我们在开发一款数据挖掘程序,用户需要向程序输入各种格式的文档,程序从文档中抽取有用的数据,以统一的格式返回给用户。程序的首个版本仅支持DOC文件,在接下来的版本中,我们希望程序还能支持PDF和CSV文件。我们发现尽管处理不同格式文件的代码不同,但是数据处理和分析的代码基本完全一样;或者我们在开发一套粒子跟踪程序,用户指定稳态还是瞬态,程序对粒子们的运动进行模拟,最后返回粒子分布。程序的首个版本仅支持稳态跟踪,在接下来的版本中,我们希望程序还能支持瞬态模拟。我们发现尽管稳态和瞬态的粒子跟踪逻辑有所不同,但是仿真初始化、并行以及粒子发射和抽样的代码基本完全一样。

这个时候,就可以采用模板方法模式(Template Method),对初始版本的程序进行重构,从工序的角度提取出各个步骤。一些通用的步骤可以有默认的实现,剩下与具体类型有关的,可以定义成抽象方法。干净地定义好每两道步骤之间的接口,在模板方法(具体执行流程函数)中依次调用各个步骤。然后对于应用于具体场景的子类,实现基类中的抽象方法。

一个例子

模板方法模式的主要思路就是定义一个操作中的算法的骨架,将一些步骤的实现延迟到子类中。这样,可以在不改变算法结构的情况下,重新定义算法的某些步骤。在实际的生产环境中,模板方法模式特别适合步骤固定但每个步骤的具体实现可能变化的场景。比如,在机器学习模型的训练过程中,我们可能需要按顺序执行如下步骤:数据加载、数据清洗、数据转换、模型训练、结果评估和结果保存。这个流程中的某些步骤(如数据清洗和模型训练)可能根据不同的项目需求有不同的实现方式。我们可以使用模板方法模式来设计这样的流程。

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
from abc import ABC, abstractmethod

class DataProcessingPipeline(ABC):
# 模板方法,定义了数据处理的流程
def process(self):
self.load_data()
self.clean_data()
self.transform_data()
self.train_model()
self.evaluate_model()
self.save_results()

def load_data(self):
print("加载数据")

@abstractmethod
def clean_data(self):
pass

@abstractmethod
def transform_data(self):
pass

@abstractmethod
def train_model(self):
pass

def evaluate_model(self):
print("评估模型")

def save_results(self):
print("保存结果")

class MyDataProcessingPipeline(DataProcessingPipeline):
def clean_data(self):
print("使用特定方法清洗数据")

def transform_data(self):
print("使用特定方法转换数据")

def train_model(self):
print("训练模型,使用特定算法")

# 使用
pipeline = MyDataProcessingPipeline()
pipeline.process()

在这个例子中,DataProcessingPipeline 类定义了训练模型的基本流程,包括加载数据、清洗数据、转换数据、训练模型、评估模型和保存结果。这些方法中,load_dataevaluate_modelsave_results 方法在基类中已经有了默认实现,它们可能对大多数数据处理任务都是通用的。而clean_datatransform_datatrain_model 方法则是抽象的,需要在子类中根据具体任务进行具体实现。MyDataProcessingPipeline 类继承自DataProcessingPipeline,提供了clean_datatransform_datatrain_model 方法的具体实现。这样,我们就可以根据不同的数据处理需求创建不同的子类,每个子类针对特定的数据清洗、转换和模型训练策略进行实现,而整个数据处理的流程则由基类控制,确保了处理流程的一致性和可重用性。

策略模式和萨特

模板方法模式相当于顶层的结构设计,当我们有把握这一个模块的算法框架和流程确定不变时,或者这一套结构在整个程序的很顶层时,我们可以采用模板方法。模板方法通过继承机制实现多态,在类层次上运作,因此它是静态的。算法的结构被视为不变和永恒的,而步骤的具体实现是可变的,这与柏拉图理念世界与现实世界的关系相似,这是一种从上而下的、强调整体结构的不变性的世界观。如果古希腊有计算机的话,柏拉图肯定会倾向于先写好一切部分的模板,然后派生出各种各样的具体实现。

不过我们也可以采取另一种世界观,萨特告诉我们“存在先于本质”,对于每个人所谓的永恒的、一成不变的人性模板这样的东西是不存在的,而一个人就只是他的一系列行径,他是构成这些行径的总和、组织和一套关系。人们首先存在于世界中,要通过他们的行动和选择来定义自己。这样的世界观相对于柏拉图的观点是倒置的,是一种从下而上的、强调个体选择的设计。在这样的观念下进行程序设计,要求我们可以在运行时动态切换对象的行为,这要求多态在对象层次上运作,这实际上就是策略模式。策略模式定义一系列算法,并将每种算法分别放入独立的类中。通过确保不同算法满足同样的接口,使得算法的对象能够相互替换。与模板方法模式不同,策略模式将算法的选择留给了使用算法的客户(通常是一个上下文环境)来决定。

同一个例子

把之前的模板方法模式的数据处理流程改写为策略模式,我们可以定义几个策略接口(或抽象基类):CleanStrategyTransformStrategyTrainStrategy。每个策略接口定义了相应操作的执行方法。然后,我们实现具体的策略类来执行具体的操作以实现这些接口。最后,我们有一个上下文类,它接受策略对象作为参数,并在数据处理流程中使用这些策略对象。

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
from abc import ABC, abstractmethod

# 策略接口
class CleanStrategy(ABC):
@abstractmethod
def clean(self):
pass

class TransformStrategy(ABC):
@abstractmethod
def transform(self):
pass

class TrainStrategy(ABC):
@abstractmethod
def train(self):
pass

# 具体策略实现
class SimpleCleanStrategy(CleanStrategy):
def clean(self):
print("使用简单方法清洗数据")

class AdvancedCleanStrategy(CleanStrategy):
def clean(self):
print("使用高级方法清洗数据")

class StandardTransformStrategy(TransformStrategy):
def transform(self):
print("使用标准方法转换数据")

class CustomTransformStrategy(TransformStrategy):
def transform(self):
print("使用自定义方法转换数据")

class RandomForestTrainStrategy(TrainStrategy):
def train(self):
print("训练模型,使用随机森林")

class NeuralNetworkTrainStrategy(TrainStrategy):
def train(self):
print("训练模型,使用神经网络")

# 上下文类
class DataProcessingContext:
def __init__(self, clean_strategy, transform_strategy, train_strategy):
self.clean_strategy = clean_strategy
self.transform_strategy = transform_strategy
self.train_strategy = train_strategy

def process(self):
print("加载数据")
self.clean_strategy.clean()
self.transform_strategy.transform()
self.train_strategy.train()
print("评估模型")
print("保存结果")

# 使用
context = DataProcessingContext(SimpleCleanStrategy(), StandardTransformStrategy(), RandomForestTrainStrategy())
context.process()

print("---")

context = DataProcessingContext(AdvancedCleanStrategy(), CustomTransformStrategy(), NeuralNetworkTrainStrategy())
context.process()

在这个例子中,首先定义了清洗、转换和训练的策略接口,以及这些接口的一些具体实现。然后创建了一个上下文类DataProcessingContext,它接受策略对象作为参数,并在process方法中调用这些策略对象的方法来执行数据处理流程。通过这种方式,可以在运行时更换策略对象来改变数据处理流程中的某些步骤,而不需要修改上下文类的代码。在一些数值模拟程序的编写中,边界条件的设定和修改,也可以采用策略模式的思路。

参考文献

https://refactoringguru.cn/design-patterns/template-method

敏捷软件开发:原则、模式与实践