Krab

DDD – Polityka i Specyfikacja

Witaj drogi czytelniku! Sieć zawiera mnóstwo informacji na temat elementów konstrukcyjnych DDD takich jak agregaty, encje czy value objects (obiekty wartości? dziwnie to brzmi 🙂). W dzisiejszym wpisie chciałem omówić dwa elementy o których ciężko znaleźć artykuły w sieci, a są bardzo przydatne – specyfikacja i polityka. Postaram Ci się wyjaśnić oba wzorce na prostych przykładach a także na faktycznym kodzie. Napisałem wszystkie przykłady w trzech językach (PHP, JavaScript i Rust), tak by każdy mógł znaleźć coś dla siebie 😁.

Gwoli wyjaśnienia, przygodę z DDD zacząłem około 2 lata temu, nie uważam się za eksperta, uwagi i krytyka są mile widziane 🙂!

Spis treści:

Czym jest Specyfikacja?

Zadaniem specyfikacji jest odpowiedź tak lub nie na pytanie czy parametr wejściowy spełnia specyficzne wymagania.

Przykład: czy opakowanie jest czerwone?

Ten element konstrukcyjny zawsze zwraca boolean, co pozwala nam na tworzenie złożonych specyfikacji za pomocą wyrażeń logicznych, jak and, or, not, czy xor.

Dzięki temu możemy wzbogacić nasz poprzedni przykład: czy opakowanie jest czerwone i jest okrągłe?

Przykład w PHP

<?php

declare(strict_types=1);

class Package
{
    public string $colour;
    public string $shape;

    public function __construct(string $colour, string $shape)
    {
        $this->colour = $colour;
        $this->shape = $shape;
    }
}

interface PackageSpecification
{
    public function isSatisfiedBy(Package $package): bool;
}

class PackageColourSpecification implements PackageSpecification
{
    public string $expectedColour;

    public function __construct(string $expectedColour)
    {
        $this->expectedColour = $expectedColour;
    }

    public function isSatisfiedBy(Package $package): bool
    {
        return $package->colour === $this->expectedColour;
    }
}

class PackageShapeSpecification implements PackageSpecification
{
    public string $expectedShape;

    public function __construct(string $expectedShape)
    {
        $this->expectedShape = $expectedShape;
    }

    public function isSatisfiedBy(Package $package): bool
    {
        return $package->shape === $this->expectedShape;
    }
}

class AndPackageSpecification implements PackageSpecification
{
    public PackageSpecification $left;
    public PackageSpecification $right;

    public function __construct(
        PackageSpecification $left,
        PackageSpecification $right
    ) {
        $this->left = $left;
        $this->right = $right;
    }

    public function isSatisfiedBy(Package $package): bool
    {
        return $this->left->isSatisfiedBy($package)
            && $this->right->isSatisfiedBy($package);
    }
}

function shouldBePossibleToCreateSpecificationForRedOvalPackage()
{
    $redOvalPackage = new Package('red', 'oval');
    $redSquarePackage = new Package('red', 'square');
    $greenOvalPackage = new Package('green', 'oval');
    $redOvalPackageSpecification = new AndPackageSpecification(
        new PackageColourSpecification('red'),
        new PackageShapeSpecification('oval'),
    );

    assert($redOvalPackageSpecification->isSatisfiedBy($redOvalPackage) === true);
    assert($redOvalPackageSpecification->isSatisfiedBy($redSquarePackage) === false);
    assert($redOvalPackageSpecification->isSatisfiedBy($greenOvalPackage) === false);
}

shouldBePossibleToCreateSpecificationForRedOvalPackage();

Przykład w JavaScript

'use strict';

class Box {
    colour;
    shape;

    /**
     * @param {string} colour
     * @param {string} shape
     */
    constructor(colour, shape) {
        this.colour = colour;
        this.shape = shape;
    }
}

class BoxSpecification {
    /**
     * @param {Box} box
     * @returns {boolean}
     */
    isSatisfiedBy(box) {
        return box instanceof Box;
    }
}

class BoxColourSpecification extends BoxSpecification {
    _expectedColour;

    /**
     * @param {string} expectedColour
     */
    constructor(expectedColour) {
        super();
        this._expectedColour = expectedColour;
    }

    /**
     * @param {Box} box
     * @returns {boolean}
     */
    isSatisfiedBy(box) {
        return super.isSatisfiedBy(box) && box.colour === this._expectedColour;
    }
}

class BoxShapeSpecification extends BoxSpecification {
    _expectedShape;

    /**
     * @param {string} expectedShape
     */
    constructor(expectedShape) {
        super();
        this._expectedShape = expectedShape;
    }

    /**
     * @param {Box} box
     * @returns {boolean}
     */
    isSatisfiedBy(box) {
        return super.isSatisfiedBy(box) && box.shape === this._expectedShape;
    }
}

class AndBoxSpecification extends BoxSpecification {
    _left;
    _right;

    /**
     * @param {BoxSpecification} left
     * @param {BoxSpecification} right
     */
    constructor(left, right) {
        super();
        this._left = left;
        this._right = right;
    }

    /**
     * @param {Box} box
     * @returns {boolean}
     */
    isSatisfiedBy(box) {
        return super.isSatisfiedBy(box)
            && this._left.isSatisfiedBy(box)
            && this._right.isSatisfiedBy(box);
    }
}

const redOvalBox = new Box('red', 'oval');
const redSquareBox = new Box('red', 'square');
const greenOvalBox = new Box('green', 'oval');
const redOvalBoxSpecification = new AndBoxSpecification(
    new BoxColourSpecification('red'),
    new BoxShapeSpecification('oval'),
)

console.assert(
    redOvalBoxSpecification.isSatisfiedBy(redOvalBox) === true,
    'should be possible to create specification for red oval package',
);
console.assert(
    redOvalBoxSpecification.isSatisfiedBy(redSquareBox) === false,
    'should be possible to create specification for red oval package',
);
console.assert(
    redOvalBoxSpecification.isSatisfiedBy(greenOvalBox) === false,
    'should be possible to create specification for red oval package',
);

Przykład w Rust

struct Package<'a> {
    colour: &'a str,
    shape: &'a str
}

trait PackageSpecification {
    fn is_satisfied_by(&self, package: &Package) -> bool;
}

struct PackageColourSpecification<'a> {
    expected_colour: &'a str
}

impl PackageSpecification for PackageColourSpecification<'_> {
    fn is_satisfied_by(&self, package: &Package) -> bool {
        package.colour == self.expected_colour
    }
}

struct PackageShapeSpecification<'a> {
    expected_shape: &'a str
}

impl PackageSpecification for PackageShapeSpecification<'_> {
    fn is_satisfied_by(&self, package: &Package) -> bool {
        package.shape == self.expected_shape
    }
}

struct AndPackageSpecification<'a> {
    left: &'a dyn PackageSpecification,
    right: &'a dyn PackageSpecification
}

impl PackageSpecification for AndPackageSpecification<'_> {
    fn is_satisfied_by(&self, package: &Package) -> bool {
        self.left.is_satisfied_by(package) && self.right.is_satisfied_by(package)
    }
}

#[test]
fn should_be_possible_to_create_specification_for_red_oval_package() {
    let red_oval_package = Package {
        colour: "red",
        shape: "oval"
    };
    let red_square_package = Package {
        colour: "red",
        shape: "square"
    };
    let green_oval_package = Package {
        colour: "green",
        shape: "oval"
    };
    let red_oval_package_specification = AndPackageSpecification {
        left: &PackageColourSpecification { expected_colour: "red" },
        right: &PackageShapeSpecification { expected_shape: "oval" }
    };
    assert_eq!(
        red_oval_package_specification.is_satisfied_by(&red_oval_package),
        true,
    );
    assert_eq!(
        red_oval_package_specification.is_satisfied_by(&red_square_package),
        false,
    );
    assert_eq!(
        red_oval_package_specification.is_satisfied_by(&green_oval_package),
        false,
    );
}

Czym jest Polityka?

Polityka pozwala rozwiązać problem w dany sposób, w zależności od warunków.

Przykład: jesteś w sklepie i robisz zakupy. Sklep ma politykę rabatową – gdy posiadasz zainstalowaną aplikację sklepu to otrzymasz rabat 10%. A gdy jesteś emerytem, otrzymasz rabat 15%.

Polityka enkapsuluje logikę biznesową która jest zależna od pewnych warunków. Polityka wykonuje akcje. W naszym konkretnym przykładzie daje rabat na zakupy, pod warunkiem posiadania aplikacji czy też bycia emerytem. Jest to wariant wzorca projektowego strategia.

Polityka bardzo często jest używana wraz ze specyfikacją do określenia czy odpowiednie warunki są spełnione.

Przykład w PHP

<?php

declare(strict_types=1);

class Order
{
    private float $total;

    public function __construct(float $total)
    {
        $this->total = $total;
    }

    public function getTotal(): float
    {
        return $this->total;
    }
}

class Customer
{
    private bool $isUsingApplication;
    private bool $isPensioner;

    public function __construct(bool $isUsingApplication, bool $isPensioner)
    {
        $this->isUsingApplication = $isUsingApplication;
        $this->isPensioner = $isPensioner;
    }

    public function isUsingApplication(): bool
    {
        return $this->isUsingApplication;
    }

    public function isPensioner(): bool
    {
        return $this->isPensioner;
    }
}

interface CustomerSpecification
{
    public function isSatisfied(): bool;
}

class CustomerUsingApplicationSpecification implements CustomerSpecification
{
    private Customer $customer;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }

    public function isSatisfied(): bool
    {
        return $this->customer->isUsingApplication();
    }
}

class PensionerCustomerSpecification implements CustomerSpecification
{
    private Customer $customer;

    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }

    public function isSatisfied(): bool
    {
        return $this->customer->isPensioner();
    }
}

interface DiscountPolicy
{
    public function apply(Order $order): Order;
}

class CustomerUsingApplicationDiscountPolicy implements DiscountPolicy
{
    private CustomerUsingApplicationSpecification $specification;

    public function __construct(CustomerUsingApplicationSpecification $specification)
    {
        $this->specification = $specification;
    }

    public function apply(Order $order): Order
    {
        if ($this->specification->isSatisfied()) {
            return new Order($order->getTotal() * 0.9);
        }

        return $order;
    }
}

class PensionerDiscountPolicy implements DiscountPolicy
{
    private PensionerCustomerSpecification $specification;

    public function __construct(PensionerCustomerSpecification $specification)
    {
        $this->specification = $specification;
    }

    public function apply(Order $order): Order
    {
        if ($this->specification->isSatisfied()) {
            return new Order($order->getTotal() * 0.85);
        }

        return $order;
    }
}

function applicationUserDiscountPolicyShouldReduceTheOrderTotalByTenPercents()
{
    $initialOrder = new Order(100);
    $customer = new Customer(true, false);
    $specification = new CustomerUsingApplicationSpecification($customer);
    $policy = new CustomerUsingApplicationDiscountPolicy($specification);
    $discountedOrder = $policy->apply($initialOrder);
    assert($discountedOrder->getTotal() === 90.0);
}

function pensionerDiscountPolicyShouldReduceTheOrderTotalByFifteenPercents()
{
    $initialOrder = new Order(100);
    $customer = new Customer(false, true);
    $specification = new PensionerCustomerSpecification($customer);
    $policy = new PensionerDiscountPolicy($specification);
    $discountedOrder = $policy->apply($initialOrder);
    assert($discountedOrder->getTotal() === 85.0);
}

applicationUserDiscountPolicyShouldReduceTheOrderTotalByTenPercents();
pensionerDiscountPolicyShouldReduceTheOrderTotalByFifteenPercents();

Przykład w JavaScript

'use strict';

class Order {
    _total;

    /**
     * @param {number} total
     */
    constructor(total) {
        this._total = total;
    }

    /**
     * @returns {number}
     */
    get total() {
        return this._total;
    }
}

class Customer {
    _isUsingApplication;
    _isPensioner;

    /**
     * @param {boolean} isUsingApplication
     * @param {boolean} isPensioner
     */
    constructor(isUsingApplication, isPensioner) {
        this._isUsingApplication = isUsingApplication;
        this._isPensioner = isPensioner;
    }

    /**
     * @returns {boolean}
     */
    get isUsingApplication() {
        return this._isUsingApplication;
    }

    /**
     * @returns {boolean}
     */
    get isPensioner() {
        return this._isPensioner;
    }
}

class CustomerSpecification {
    _customer;

    /**
     * @param {Customer} customer
     */
    constructor(customer) {
        this._customer = customer;
    }

    /**
     * @abstract
     * @returns {boolean}
     */
    isSatisfied() {}
}

class CustomerUsingApplicationSpecification extends CustomerSpecification {
    /**
     * @returns {boolean}
     */
    isSatisfied() {
        return this._customer.isUsingApplication;
    }
}

class PensionerCustomerSpecification extends CustomerSpecification {
    /**
     * @returns {boolean}
     */
    isSatisfied() {
        return this._customer.isPensioner;
    }
}

class DiscountPolicy {
    _specification;

    /**
     * @param {CustomerSpecification} specification
     */
    constructor(specification) {
        this._specification = specification;
    }

    /**
     * @abstract
     * @param {Order} order
     * @returns {Order}
     */
    apply(order) {}
}

class CustomerUsingApplicationDiscountPolicy extends DiscountPolicy {
    /**
     * @param {CustomerUsingApplicationSpecification} specification
     */
    constructor(specification) {
        super(specification);
    }

    /**
     * @param {Order} order
     * @returns {Order}
     */
    apply(order) {
        if (this._specification.isSatisfied()) {
            return new Order(order.total * 0.9);
        }

        return order;
    }
}

class PensionerDiscountPolicy extends DiscountPolicy {
    /**
     * @param {PensionerCustomerSpecification} specification
     */
    constructor(specification) {
        super(specification);
    }

    /**
     * @param {Order} order
     * @returns {Order}
     */
    apply(order) {
        if (this._specification.isSatisfied()) {
            return new Order(order.total * 0.85);
        }

        return order;
    }
}

function applicationUserDiscountPolicyShouldReduceTheOrderTotalByTenPercents() {
    const initialOrder = new Order(100);
    const customer = new Customer(true, false);
    const specification = new CustomerUsingApplicationSpecification(customer);
    const policy = new CustomerUsingApplicationDiscountPolicy(specification);
    const discountedOrder = policy.apply(initialOrder);
    console.assert(discountedOrder.total === 90.0);
}

function pensionerDiscountPolicyShouldReduceTheOrderTotalByFifteenPercents() {
    const initialOrder = new Order(100);
    const customer = new Customer(false, true);
    const specification = new PensionerCustomerSpecification(customer);
    const policy = new PensionerDiscountPolicy(specification);
    const discountedOrder = policy.apply(initialOrder);
    console.assert(discountedOrder.total === 85.0);
}

applicationUserDiscountPolicyShouldReduceTheOrderTotalByTenPercents();
pensionerDiscountPolicyShouldReduceTheOrderTotalByFifteenPercents();

Przykład w Rust

struct Order {
    total: f64
}

struct Customer {
    is_using_application: bool,
    is_pensioner: bool
}

trait CustomerSpecification {
    fn is_satisfied(&self) -> bool;
}

struct CustomerUsingApplicationSpecification<'a> {
    customer: &'a Customer
}

impl CustomerSpecification for CustomerUsingApplicationSpecification<'_> {
    fn is_satisfied(&self) -> bool {
        self.customer.is_using_application
    }
}

struct PensionerCustomerSpecification<'a> {
    customer: &'a Customer
}

impl CustomerSpecification for PensionerCustomerSpecification<'_> {
    fn is_satisfied(&self) -> bool {
        self.customer.is_pensioner
    }
}

trait DiscountPolicy {
    fn apply(&self, order: &Order) -> Order;
}

struct CustomerUsingApplicationDiscountPolicy<'a> {
    specification: &'a CustomerUsingApplicationSpecification<'a>
}

impl DiscountPolicy for CustomerUsingApplicationDiscountPolicy<'_> {
    fn apply(&self, order: &Order) -> Order {
        if self.specification.is_satisfied() {
            Order {
                total: order.total * 0.9
            }
        } else {
            Order {
                total: order.total
            }
        }
    }
}

struct PensionerDiscountPolicy<'a> {
    specification: &'a PensionerCustomerSpecification<'a>
}

impl DiscountPolicy for PensionerDiscountPolicy<'_> {
    fn apply(&self, order: &Order) -> Order {
        if self.specification.is_satisfied() {
            Order {
                total: order.total * 0.85
            }
        } else {
            Order {
                total: order.total
            }
        }
    }
}

#[test]
fn customer_using_application_discount_policy_should_reduce_the_order_total_by_ten_percents() {
    let initial_order = Order {
        total: 100.0
    };
    let customer = Customer {
        is_using_application: true,
        is_pensioner: false
    };
    let specification = CustomerUsingApplicationSpecification {
        customer: &customer
    };
    let policy = CustomerUsingApplicationDiscountPolicy {
        specification: &specification
    };
    let discounted_order = policy.apply(&initial_order);
    assert_eq!(discounted_order.total, 90.0);
}

#[test]
fn pensioner_discount_policy_should_reduce_the_order_total_by_fifteen_percents() {
    let initial_order = Order {
        total: 100.0
    };
    let customer = Customer {
        is_using_application: false,
        is_pensioner: true
    };
    let specification = PensionerCustomerSpecification {
        customer: &customer
    };
    let policy = PensionerDiscountPolicy {
        specification: &specification
    };
    let discounted_order = policy.apply(&initial_order);
    assert_eq!(discounted_order.total, 85.0);
}

Podsumowanie

Od kiedy poznałem i zacząłem używać oba elementy konstrukcyjne, mój kod stał się zdecydowanie lepszy. Jestem pewny, że dzięki nim, twój model stanie się bogatszy i czytelniejszy.

A może znasz już te elementy konstrukcyjne? Czy zgadzasz się z moimi przemyśleniami? Zapraszam do dyskusji w komentarzach!

Źródła

2 myśli w temacie “DDD – Polityka i Specyfikacja”

  1. Czyli trzeba stworzyć wszystkie możliwe polityki i ich specyfikacje (niezależnie od tego czy ostatecznie będą nałożone czy nie) a następnie aplikować je na zamówieniu aby otrzymać finalną wartość zamówienia, trochę overkill? Przykładowe testy pokazują nałożenie jedynie pojedynczej polityki. Co jeśli logika wymagałaby tego żeby zniżki się nie sumowały? Mam wrażenie że w takim przypadku kod stałby się nieczytelny.

    1. Hej pharuney.

      Przykłady które poruszyłem w tym wpisie są proste i skrojone pod ten konkretny przypadek.

      Nie musisz tworzyć wszystkich polityk i specyfikacji. Tak naprawdę możesz nie posiadać żadnej specyfikacji, a mieć repozytorium polityk które w zależności od aktualnego stanu zwróci Ci właściwe polityki dla danego przypadku.

      Jeśli chodzi o ostatnie pytanie, to tak, to byłoby mocno nie czytelne. Trzeba by pomyśleć nad sensownym rozwiązaniem tego problemu.

      Pozdrawiam 🙂

Możliwość komentowania jest wyłączona.