使用元类而不是工厂模式

Posted

技术标签:

【中文标题】使用元类而不是工厂模式【英文标题】:Using metaclasses instead of factory pattern 【发布时间】:2021-12-27 12:26:01 【问题描述】:

假设我们有一个具有不同CarTypes 的实体(Car):

class CarTypes(Enum):
    SUV = 'suv'
    SPORT = 'sport'

@dataclass
class Car:
    id: UUID
    type: CarTypes
    owner: str

    def run(self):
        ...

    def idk_some_car_stuf(self, speed):
        ...

Car 类实现引用Car 的域规则,应用程序规则(即访问数据库以加载Car、访问外部 API、将消息放入队列、日志等)在一个服务类CarService:

class ServiceCar:
    def __init__(self, car_id: UUID):
        self._car = CarRepository.get(car_id)

    def run(self):
        log.info('Car is about to run')
        self._car.run()

        if self._car.type == CarTypes.SUV:
            suvAPI.suv_is_running(self._car)

        elif self._car.type == CarTypes.SPORT:
            ...

        rabbitmq.publish('car': self._car.__dict__, 'message': ...)

问题是不同的车种可以有不同的应用规则类型(例如调用不同的外部API等),由于我想遵循O​​pen-Closed原则,我不想实现这个ifs,所以我选择将CarServiceCarTypes 隔离,如下所示:

class CarService(ABC):
    @abstractmethod
    def run(self) -> None:
        ...

class SUVCarService(CarService):
    ''' Specific implementation here, following CarService interface'''
    ...

class SportCarService(CarService):
    ''' Specific implementation here, following CarService interface'''
    ...

class CarServiceFactory:
    @classmethod
    def build(cls, car_id: UUID) -> CarService:
        car = CarRepository.get(car_id)
        klass: CarService = SUVCarService if car.type == 'SUV' else SportCarService

        return klass(car)

这是我当前的实现(我在这里使用了一个通用和简单的示例)但我不满意,我真正想要的是使用元类来构建特定的(即SUVCarServiceSportCarService)。因此,我的控制器改为这样调用:


def controller_run(body):
    service = CarServiceFactory.build(body['car_id'])
    service.run()
    ...

它会被称为:

def controller_run(body):
    service = CarService(car_id=body['car_id'])
    # This CarService calls return the specific class, so
    # if car.type == 'suv' then type(service) == SUVCarService
    service.run()
    ...

但是关于元类的 python 文档让我感到困惑,(如果我需要使用元类本身的 __new__ 方法,或者 __prepare__ ,请注明)。

【问题讨论】:

【参考方案1】:

元类可以用于自动将“汽车”实例化为适当的子类。

但也许这会使事情变得复杂,超出了需要。 在您的示例中,比必要更官僚的是,汽车服务工厂不需要自己成为一个类 - 它可以是一个简单的函数。

所以,对于函数工厂:

def car_service_factory(cls, car_id: UUID) -> CarService:
    car = CarRepository.get(car_id)
    # klass: CarService = SUVCarService if car.type == 'SUV' else SportCarService
    # nice place to use the new pattern matching construct in Python 3.10. Unless you 
    # need to support new classes in a dynamic way (i.e. not all car types
    #are hardcoded)
    match car.type:
        case "SUV": 
            klass = SuvCarService
        case _:
            klass = SportsCarService

    return klass(car)

这就是“pythonland”:在不需要人为创建类的情况下使用普通函数并不“丑陋”。

如果你想要一个元类,你可以将工厂逻辑移动到元类__call__ 方法中。然后它可以在实例化它之前选择适当的子类。但是如果它更“优雅”就相当主观了,而且它肯定更不容易维护——因为元类是许多程序员没有完全掌握的高级主题。最终,您可以使用作为服务类注册表工作的普通 Python 字典,并以汽车类型为关键字。

既然问题是关于元类的,那就这样吧。唯一不同的是它可以利用__init__ 方法来保持所有汽车服务类的动态注册表。它可以从类名派生出来,作为一个字符串 - 但我认为在它们上也有一个显式的 type 属性就不那么老套了。


from abc import ABCMeta
from typing import Union, Optional

from enum import Enum

class CarTypes(Enum):
    SUV = 'suv'
    SPORT = 'sport'

class Car:
    ...

class MetaCarService(ABCMeta):
    service_registry = 
    def __init__(cls, name, bases, ns, **kw):
        cls.__class__.service_registry[cls.type] = cls
        return super().__init__(name, bases, ns, **kw)
   
    def __call__(cls, car_or_id: Union[UUID, Car]) -> "CarService":
        if not isinstance(car_or_id, Car):
            car = CarRepository.get(car_id)
        else:
            car = car_id
        # for hardcoded classses you may use your example code:
        # cls: CarService = SUVCarService if car.type == 'SUV' else SportCarService
        # For auto-discovery, you may do:
        try:
            cls = cls.__class__.service_registry[car.type.value]
        except KeyError:
            raise ValueError(f"No registered Service class for car type car.type" )
        instance = super.__call__(cls, car)
        return instance

class CarService(metaclass=MetaCarService):
    type: Optional[CarTypes] = None

    def __init__(self, car_or_id: Union[UUID, Car]): 
        # the annotation trick is a workaround so that you can use the UUID 
        # in your code, and the metaclass can pass the instantiated Car here.
        # You could accept just the UUID and create a new car instance,
        # disregarding the one build in the metaclass, of course
        # (I think the annotation linter will require you to 
        # copy-paste the `isinstance(car_or_id, Car)` block here)
        self.car = car_or_id

    @abstractmethod
    def run(self) -> None:
        ...

class SUVCarService(CarService):
    ''' Specific implementation here, following CarService interface'''
    type = CarTypes.SUV
    ...

class SportCarService(CarService)
    ''' Specific implementation here, following CarService interface'''
    type = CarTypes.SPORT
    ...

...

def controller_run(body):
    service = CarService(body['car_id'])
    service.run()
    ...

【讨论】:

很干净,我想现在我得到了元类点 在我的具体情况下,将服务作为类是有意义的,但我同意你的观点,python 不是 java,可以是函数的必须是函数(如我的控制器示例)跨度> “服务”可以是类。 “服务工厂”等价物可以是一个函数并返回正确的类——这是最直接的模式。

以上是关于使用元类而不是工厂模式的主要内容,如果未能解决你的问题,请参考以下文章

工厂模式和抽象工厂模式区别

java设计模式--六大原则

工厂模式&抽象工厂——HeadFirst设计模式学习笔记

工厂模式。啥时候使用工厂方法?

我们是不是需要以抽象工厂模式向客户公开具体工厂

享元模式