logo

该视频仅会员有权观看

立即开通课程「Python 入门」权限。

¥
199
/ 年

工厂模式(实战)

现在我们的主机都是远程主机,那么如果我们还希望支持 Docker 主机呢?是不是就需要在添加一个 Docker 的主机类,然后再去修改执行引擎,让其支持 Docker 主机?这样是不是就太麻烦了,那么有什么方法可以让我们在添加主机的时候,不需要去修改执行引擎,让其支持 Docker 主机呢?

这里我们可以使用一种设计模式叫做 工厂模式,通过工厂模式我们可以将主机的创建封装起来,从而在添加主机的时候,不需要去修改执行引擎,让其支持 Docker 主机。这里我们可以简单解释下什么是工厂模式:

工厂模式就像一个制造各种产品的工厂,想象你在点餐:

  1. 你不需要知道厨房如何制作食物(创建对象的过程)。
  2. 你只需要告诉服务员(工厂)你想要什么(比如"汉堡")。
  3. 厨房(工厂内部)根据你的要求制作食物(创建对象)。
  4. 你得到了想要的食物(对象),而不需要了解制作过程。

工厂模式的核心思想是:

  • 将对象的创建与使用分离
  • 通过一个统一的接口(工厂)来获取不同类型的对象
  • 隐藏对象创建的复杂性

这样做的好处是:

  • 代码更整洁:对象创建的逻辑集中在一处
  • 更容易修改:如果需要改变对象创建的方式,只需修改工厂,而不是到处修改
  • 更容易扩展:添加新类型的对象时,只需要修改工厂,不影响使用方

简而言之,工厂模式就是一种让你更容易创建和管理对象的方法,它把创建对象的复杂过程隐藏起来,让使用者可以更简单地获取所需的对象。在工厂模式中,我们不会对客户端暴露创建逻辑,而是通过使用一个共同的接口来指向新创建的对象。让我详细介绍一下工厂模式:

工厂模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。下面我们用一个例子来说明下工厂模式:

from abc import ABC, abstractmethod # 抽象产品 class Animal(ABC): @abstractmethod def speak(self): pass # 具体产品 class Dog(Animal): def speak(self): return "Woof!" class Cat(Animal): def speak(self): return "Meow!" # 抽象工厂 class AnimalFactory(ABC): @abstractmethod def create_animal(self): pass # 具体工厂 class DogFactory(AnimalFactory): def create_animal(self): return Dog() class CatFactory(AnimalFactory): def create_animal(self): return Cat() # 客户端代码 def client_code(factory: AnimalFactory): animal = factory.create_animal() print(f"动物说: {animal.speak()}") # 使用 dog_factory = DogFactory() cat_factory = CatFactory() client_code(dog_factory) # 输出: 动物说: Woof! client_code(cat_factory) # 输出: 动物说: Meow!

上面代码中,我们定义了一个抽象产品 Animal,然后定义了两个具体产品 DogCat,接着定义了一个抽象工厂 AnimalFactory,然后定义了两个具体工厂 DogFactoryCatFactory,最后在客户端代码中,我们使用的还是抽象工厂 AnimalFactory,但是具体创建的是哪个产品是由子类决定的,需要哪个产品就使用哪个工厂来创建即可,这样我们就可以在客户端代码中,不需要关心具体的产品是什么,只需要关心抽象的产品是什么就可以了,这样我们就可以很方便的扩展新的产品,而不需要修改客户端代码。

在对工厂模式有了一个基本的了解之后,我们就可以来改造下我们的任务执行引擎了,让其支持 Docker 主机。同样的我们也可以将主机的创建封装为一个工厂,首先我们需要定义一个 DockerHost 类,这里我们可以抽象一个 Host 的基类,将之前的类更名为 RemoteHost,然后 DockerHostRemoteHost 都继承自 Host 基类,分别去实现自己的 execute_command 方法,代码如下:

# host.py from abc import ABC, abstractmethod import paramiko import docker from docker import DockerClient class Host(ABC): @abstractmethod def execute_command(self, command: str) -> dict[str, str | int]: pass class RemoteHost(Host): """主机类""" # ...... 省略其他代码 class DockerHost(Host): def __init__(self, container_id: str, address: str = '127.0.0.1', port: int = 2375): self.container_id = container_id self.address = address self.port = port if address != '127.0.0.1': # 使用指定地址和端口连接到 Docker 守护进程 self.client: DockerClient = docker.DockerClient(base_url=f"tcp://{address}:{port}") else: # 使用默认的 Docker 守护进程连接 self.client: DockerClient = docker.from_env() def execute_command(self, command: str) -> dict[str, str | int]: try: container = self.client.containers.get(self.container_id) result = container.exec_run(command) # 类似 docker exec print(result) exit_code = result.exit_code output = result.output return { "stdout": output.decode() if exit_code == 0 else "", "stderr": output.decode() if exit_code != 0 else "", "exit_status": exit_code } except Exception as e: return { "stdout": "", "stderr": str(e), "exit_status": -1 }

接着就可以创建工厂类了,在 factory.py 文件中,代码如下:

# factory.py from abc import ABC, abstractmethod from host import Host, RemoteHost, DockerHost from task import Task class HostFactory(ABC): @abstractmethod def create_host(self, host_config: dict) -> Host: pass class TaskFactory(ABC): @abstractmethod def create_task(self, task_config: dict) -> Task: pass class DefaultHostFactory(HostFactory): def create_host(self, host_config: dict) -> RemoteHost: return RemoteHost( address=host_config['address'], username=host_config['username'], password=host_config['password'] ) class DockerHostFactory(HostFactory): def create_host(self, host_config: dict) -> DockerHost: return DockerHost( address=host_config['address'], port=host_config['port'], container_id=host_config['container_id'] ) class DefaultTaskFactory(TaskFactory): def create_task(self, task_config: dict) -> Task: return Task( name=task_config['name'], command=task_config.get('command', ''), module=task_config.get('module', ''), args=task_config.get('args', []), depends_on=task_config.get('depends_on', []) )

这里我们定义了两个工厂类,一个是 DefaultHostFactory,一个是 DockerHostFactory,分别用于创建 HostDockerHost 对象,当然对于 Task 对象,我们也定义了一个 DefaultTaskFactory 工厂类,用于创建 Task 对象,因为一样的我们可以去支持更多类型的任务,比如定时任务、长时间任务等,这里我们就不去实现了,有兴趣的可以自己去实现下。

main.py 文件中,我们就可以使用这两个工厂类来创建主机和任务了,代码如下:

# main.py from config import load_configuration from engine import ExecutionEngine from factory import DefaultHostFactory, DockerHostFactory, DefaultTaskFactory if __name__ == '__main__': conf = load_configuration('playbook.yaml') engine = ExecutionEngine() # 加载插件 engine.load_plugins('plugins') # 添加主机到执行引擎 for host_conf in conf['hosts']: if 'container_id' in host_conf: host_factory = DockerHostFactory() # 创建 Docker 主机工厂 else: host_factory = DefaultHostFactory() # 创建远程主机工厂 engine.add_host(host_factory.create_host(host_conf)) # 使用工厂创建主机,并添加到执行引擎 # 添加任务到执行引擎 task_factory = DefaultTaskFactory() # 创建任务工厂 for task_conf in conf['tasks']: engine.add_task(task_factory.create_task(task_conf)) # 运行任务执行引擎 engine.run() print('所有任务已执行完毕。')

上面代码中,我们根据主机配置中的 container_id 字段来判断是创建 DockerHost 还是 Host,从而实现了一个简单的工厂模式。

现在我们就可以在 playbook.yaml 文件中添加一个支持 Docker 主机的配置,代码如下:

hosts: - address: 127.0.0.1 port: 2375 # docker default port container_id: 2e2e349c445c # Container name or ID. tasks: - name: List all files in the home directory command: ls -l - name: Configure the file module: ConfigureFileModule args: - /etc/nginx/nginx.conf - Hello, World!

上面的 hosts 配置中,我们添加了一个 container_id 字段,表示要连接到这个容器 ID 或者名称,然后执行下面的任务。

假设上面的 container_id 是我们通过下面的命令启动的一个容器的 ID:

docker run -d busybox sleep infinity

然后我们运行上面的 main.py 文件,看看效果如何:

PLAY [127.0.0.1] ***************************************************************************** TASK [List all files in the home directory] ************************************************** ok: [127.0.0.1] stdout: total 4 drwxr-xr-x 1 root root 4758 May 18 2023 bin drwxr-xr-x 5 root root 320 Sep 12 06:50 dev drwxr-xr-x 1 root root 56 Sep 12 06:50 etc drwxr-xr-x 1 nobody nobody 0 May 18 2023 home drwxr-xr-x 1 root root 272 May 18 2023 lib lrwxrwxrwx 1 root root 3 May 18 2023 lib64 -> lib dr-xr-xr-x 254 root root 0 Sep 12 06:50 proc drwx------ 1 root root 0 May 18 2023 root dr-xr-xr-x 11 root root 0 Sep 12 06:50 sys drwxrwxrwt 1 root root 0 May 18 2023 tmp drwxr-xr-x 1 root root 14 May 18 2023 usr drwxr-xr-x 1 root root 16 May 18 2023 var TASK [Configure the file] ******************************************************************** ok: [127.0.0.1] stdout: Hello, World! > /etc/nginx/nginx.conf PLAY RECAP *********************************************************************************** 127.0.0.1 : ok=2 failed=0 skipped=0 rescued=0 ignored=0 所有任务已执行完毕。

可以看到现在我们就可以在 Docker 容器中执行命令了,并且任务执行成功了,我们并没有去修改执行引擎的代码,只是通过工厂模式来创建主机,同样的方式,如果以后我们想要支持 K8s 的 Pod 主机,我们只需要去创建一个 K8sHost 类,然后去创建一个 K8sHostFactory 工厂类,然后在 main.py 文件中,根据 hosts 中的配置来决定创建哪个工厂类,从而创建出不同的主机对象就行,这样我们就可以很方便的扩展新的主机类型了。

不过也需要注意的是工厂模式虽然可以很方便的扩展新的主机类型,但是也会带来一些问题,比如如果我们想要支持更多的主机类型,那么我们就需要去创建更多的工厂类,这样就会导致代码变得复杂,可维护性降低,所以我们不能滥用工厂模式,需要根据实际情况来决定是否使用工厂模式。