使用元类代替工厂模式

Using metaclasses instead of factory pattern

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

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):
        ...

classCar实现域规则参考Car,应用规则(即访问DB加载Car,访问外部API,把队列、日志等上的消息)在服务中实现 class 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, 所以我选择用 CarTypes 分隔 CarService 像这样:

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)

这是我目前的实现(oc 我在这里使用了一个通用和简单的例子)但我不满意,我真正想要的是使用 Metaclasses 来构建特定的(即 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()
    ...

但是关于 metaclasses 的 python 文档让我感到困惑,(idk 如果我需要使用 metaclass 本身的 __new__ 方法,或者__prepare__).

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

但也许这会使事情复杂化,超出需要。 在您的示例中,比必要更官僚的是,汽车服务工厂本身不需要 class - 它可以是一个简单的功能。

因此,对于函数工厂:

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”:在不需要人为创建 class.

的地方使用普通函数并不“丑陋”

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

既然问题是关于元class的,那么就这样吧。唯一不同的是,它可以利用 __init__ 方法来保持所有汽车服务 class 的动态注册。它可以从 class 名称派生,作为一个字符串 - 但我认为在这些名称上也有一个明确的 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()
    ...