使用元类代替工厂模式
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等),因为我想遵循Open-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 来构建特定的(即 SUVCarService
和 SportCarService
)。所以,我的控制器调用这样的东西:
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()
...
假设我们有一个实体 (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等),因为我想遵循Open-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 来构建特定的(即 SUVCarService
和 SportCarService
)。所以,我的控制器调用这样的东西:
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()
...