Перейти к содержанию

Спецификация

Спецификация - это паттерн, который описывает бизнес-правило, которое можно комбинировать с другими бизнес-правилами при помощи булевой логики. Таким образом, сложные правила легко поддерживать, они так же легко читаются.

Паттерн часто используется в контексте domain driven design.

Реализация

Допустим, имеем класс товара:

@dataclass
class Product:
    price: float
    name: str
    quantity: int
    visible: bool

Делаем базовый класс спецификации:

class Specification(object):

    def satisfy(self, candidate: Product) -> bool: ...
Например, наша цель проверить, можно ли купить товар. Для этого нам надо проверять, является ли товар видимым и остался ли он на складе. Сделаем спецификации для этого.

Спецификация для проверки, видимый ли товар:

class IsProductVisible(Specification):
    def satisfy(self, candidate: Product) -> bool:
        return candidate.visible

А теперь спецификация для проверки, остались ли ещё товары:

class IsProductAvailable(Specification):
    def satisfy(self, candidate: Product) -> bool:
        return candidate.quantity > 0

Теперь эти 2 спецификации мы можем объеденить в одну:

class IsProductAvailableForPurchase(Specification):

    def __init__(self):
        self._is_visible = IsProductVisible()
        self._is_available = IsProductAvailable()

    def satisfy(self, candidate: Product) -> bool:
        return self._is_visible.satisfy(candidate) and self._is_available.satisfy(candidate)

И проверить:

spec = IsProductAvailableForPurchase()
product = Product(price=10, name="A", quantity=10, visible=True)
spec.satisfy(product) # True

Улучшаем работу с при помощи магических методов

Всё таки писать следующий код:

spec = IsProductVisible() & IsProductAvailable()
spec.satisfy(product)

намного лучше, чем делать для каждого такого сложного правила отдельный класс, как показано было выше.

К тому же, рано или поздно, количество правил и логики будет расти. А комбинировать маленькие правила лучше, чем писать новые большие.

Для этого нам необходимо добавить в класс Specification реализации магических методов __and__, __or__ и __invert__. Их суть проста - мы оборачиваем каждую из этих операций в специальный класс, который у себя реализует необходимую операцию:

class Specification(object):
    def satisfy(self, candidate: Product) -> bool: ...

    def __and__(self, other: "Specification") -> "Specification":
        return AndSpecification(spec_a=self, spec_b=other)

    def __or__(self, other: "Specification") -> "Specification":
        return OrSpecification(self, other)

    def __invert__(self, other: "Specification") -> "Specification":
        return NotSpecification(other)

А теперь реализуем их:

class AndSpecification(Specification):
    def __init__(self, spec_a: Specification, spec_b: Specification):
        self._spec_a = spec_a
        self._spec_b = spec_b

    def satisfy(self, candidate: Product) -> bool:
        return self._spec_a.satisfy(candidate) and self._spec_b.satisfy(candidate)


class OrSpecification(Specification):
    def __init__(self, spec_a: Specification, spec_b: Specification):
        self._spec_a = spec_a
        self._spec_b = spec_b

    def satisfy(self, candidate: Product) -> bool:
        return self._spec_a.satisfy(candidate) or self._spec_b.satisfy(candidate)


class NotSpecification(Specification):
    def __init__(self, spec: Specification):
        self._spec = spec

    def satisfy(self, candidate: Product) -> bool:
        return not self._spec.satisfy(candidate)

Тестируем!

product = Product(100, "test", 1, True)
spec = IsProductVisible() & IsProductAvailable()
spec.satisfy(product) # True

Ссылки