简体   繁体   English

使用元类代替工厂模式

[英]Using metaclasses instead of factory pattern

Lets suppose we have an entity ( Car ) with different CarTypes :假设我们有一个具有不同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):
        ...

The class Car implements the domain rules referring to Car , and the application rules (ie, acces DB to load Car , access external APIs, put messages on queues, logs, etc) are implemented in a service class CarService :该类Car实现了域规则指的是Car ,以及应用程序规则(即存取权限DB装载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': ...})

The problem is that different car types can have different application rule types (eg calling different external APIs, etc.) and since I want to follow the Open-Closed principle, I dont want to implements this ifs , so I choose to segregate CarService by CarTypes like this:问题是不同的汽车类型可以有不同的应用规则类型(例如调用不同的外部API等),由于我想遵循开闭原则,我不想实现这个ifs ,所以我选择隔离CarService CarTypes是这样的:

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)

That is my current implementation (oc I used an generic and simples example here ) but im not satisfied, what I really want is to use Metaclasses to build the specific (ie SUVCarService and SportCarService ).这是我目前的实现(我在这里使用了一个通用和简单的例子)但我不满意,我真正想要的是使用元类来构建特定的(即SUVCarServiceSportCarService )。 So, instead my controllers call something like this:所以,相反,我的控制器调用了这样的东西:


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

It would be call something like:它会被称为:

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

But the python documentation about metaclasses are confuse to me, (idk if I need to use __new__ method from the metaclass itself, or __prepare__ ).但是关于元类的 python 文档让我感到困惑(如果我需要使用元类本身的__new__方法,或者__prepare__ ,我想__new__ )。

A metaclass could be used there to automatically instantiate a "Car" to the appropriate subclass.可以在那里使用元类来自动将“汽车”实例化为适当的子类。

But maybe it would be complicating things beyond what is needed.但也许这会使超出需要的事情变得复杂。 What is more bureaucratic than necessary in your examples is that the car service factory has no need to be a class on its own - it can be a simple function.在您的示例中,比必要性更官僚的是汽车服务工厂不需要自己成为一个类 - 它可以是一个简单的功能。

So, for a function-factory:因此,对于函数工厂:

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)

This is "pythonland": it is not "ugly" to use plain functions where you don't need to artificially create a class.这是“pythonland”:在不需要人为创建类的情况下使用普通函数并不“丑陋”。

If you want a metaclass, you could move the factory logic into the metaclass __call__ method.如果你想要一个元类,你可以将工厂逻辑移动到元类__call__方法中。 It then could select the appropriate subclass before instantiating it.然后它可以在实例化它之前选择适当的子类。 But it is rather subjective if it is more "elegant", and it is certainly less maintainable - as metaclasses are an advanced topic a lot of programmers don't grasp in full.但是,如果它更“优雅”,那就相当主观了,而且它的可维护性肯定较差——因为元类是一个高级主题,很多程序员都没有完全掌握。 Ultimately, you could get away with a plain Python dictionary working as a Service class registry, keyed to the car types.最终,您可以使用一个普通的 Python 字典作为服务类注册表,以汽车类型为键。

Since the question is about a metaclass anyway, here it goes.既然问题是关于元类的,那就来吧。 The only different thing is that it can take advantage of the __init__ method to keep a dynamic registry of all car Service classes.唯一不同的是它可以利用__init__方法来保持所有汽车服务类的动态注册表。 It could be derived from the class name, as a string - but I think it is less hacky to have an explicit type attribute on those as well.它可以从类名派生,作为一个字符串 - 但我认为在那些上也有一个显式的type属性不那么hacky。


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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM