使用元类而不是工厂模式
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等),由于我想遵循Open-Closed原则,我不想实现这个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)
这是我当前的实现(我在这里使用了一个通用和简单的示例)但我不满意,我真正想要的是使用元类来构建特定的(即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()
...
但是关于元类的 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,可以是函数的必须是函数(如我的控制器示例)跨度> “服务”可以是类。 “服务工厂”等价物可以是一个函数并返回正确的类——这是最直接的模式。以上是关于使用元类而不是工厂模式的主要内容,如果未能解决你的问题,请参考以下文章