Python 中基于系统和设计问题的工厂方法

25 阅读4分钟

我正在用 Python 实现一个工具,用于收集各种系统指标(例如,CPU 利用率、CPU 饱和度、内存错误等),并将它们呈现给最终用户。这个工具理想情况下应该支持尽可能多的平台(Linux、FreeBSD、Windows 等)。

我已经为 Linux 系统实现了一些我认为很重要的指标,并且刚刚开始为 FreeBSD 系统实现相同的指标。这个工具必须以一种允许支持更多系统指标和未来更多平台的方式设计。此外,很快就会添加一个 Web 界面,它将从我的工具接收数据。

2. 我的设计决策

出于以上原因,我决定用 Python 实现这个工具(在许多系统上从不同来源读取数据很方便,而且,我多少更熟悉它:)),并且为每个系统指标遵循类结构(继承很重要,因为一些系统具有相同的特性,所以无需重写代码)。此外,我决定我可以有效地使用工厂方法。

2.1 类结构

这里是一个用于 CPU 指标的示例类图(为了便于理解,已简化):

                  CpuMetrics        (Abstract Base Class)
                 /      |    \
                /       |     \
               /        |      \
              /         |       \
             /          |        \
            /           |         \
LinuxCpuMetrics  FreeBSDCpuMetrics WindowsCPUMetrics   (per OS)
           /  \ 
          /    \
         /      \
        /        \
       /          \
      /            \
     /              \
    /                \
ArchLinuxCpuMetrics  DebianLinuxCpuMetrics             (sometimes important per Distro or Version)

2.2 工厂方法

在称为 CpuMetrics 的抽象基类中,定义了一些应该由继承类实现的抽象方法,以及一个名为 get_impl() 的工厂方法。我研究了一些关于我何时应该使用工厂方法的问题(例如,这样的答案),我相信在我的案例中使用它是合理的。

例如,我希望客户端(例如我的 web 界面)调用我的工具来获取 CPU 利用率指标,如下所示:

cpu_metrics = CpuMetrics.get_impl() # factory method as an alternative constructor
cpu_metrics.get_cpu_util() # It is completely transparent for the client which get_cpu_util() is returned.

3. 我的担忧和问题(最后)

按照上述分析的设计,我的工厂方法必须了解我们现在使用的系统(“这是 Linux、Windows 吗?现在应该使用哪个实现?”)。因此,我不得不严重依赖于 platform.system() 或它的替代函数。所以我的工厂方法所做的是(大致):

def get_impl():
    """Factory method returning the appropriate implementation depending on system."""
    try:
        system = platform.system() # This returns: { Linux, Windows, FreeBSD, ... }
        system_class = getattr("cpu", system + "CpuMetrics" )
    except AttributeError, attr_err:
        print ("Error: No class named " + system + "CpuMetrics in module cpu. ")
        raise attr_err
    return system_class()

出于以下两个原因,我对此感到非常不安:

1)我强迫未来的程序员(甚至我自己)遵循他的类的命名约定。例如,如果有人决定为我的系统扩展,比如 Solaris,他绝对必须将他的类命名为 SolarisCpuMetrics。

2)如果在未来的 Python 版本中,platform.system() 的值(或我选择使用的其他替代方法)被修改,那么我必须更改我的命名约定并大量修改我的工厂方法。

所以我的问题是:我的担忧有没有解决办法?我的代码会变得不可读,还是我的担忧无效?如果您认为有一个解决办法,我需要修改/重构多少代码并更改我的设计?

我没有从头开始设计项目的经验,所以任何建议对我来说都有用。此外,我在 Java 方面有更多的经验。在写 Python 时,我尽量以一种尽可能 Pythonic 的方式思考,但有时无法在两者之间进行适当的分离。您的建设性批评是非常可取的。

3. 解决方案

我们可以在 classes.py 文件中定义一个类装饰器来枚举类:

sysmap = {}

class metric:
  def __init__(self, system):
    self.system = system
  def __call__(self, cls):
    sysmap[self.system] = cls
    return cls

然后,我们可以在 cpu_metrics.py 文件中创建 CpuMetrics 类,该类使用 __new__ 方法来实例化适当的具体 CpuMetrics 类:

class CpuMetrics:
  def __new__(self):
    cls = sysmap.get(platform.system)
    if not cls:
      raise RuntimeError('No metric class found!')
    else:
      return cls()

接下来,我们可以创建一些具体的 CpuMetrics 类,例如 LinuxCpuMetrics 和 FreeBSDCpuMetrics,并使用 @metric 装饰器来注册它们:

...
@metric('Linux')
class SomeLinuxMetrics(CpuMetrics):
   ...

...
metrics = CpuMetrics()

通过这种方式,我们可以动态地实例化适当的具体 CpuMetrics 类,而无需在工厂方法中使用 if-else 语句或 switch 语句。这种方法更加灵活,并且允许我们在不修改工厂方法的情况下添加新的具体 CpuMetrics 类。