Сущность

Паттерны проектирования / Паттерны чистой архитектуры
image

//entity
struct OrderLine {
    sku: String,
    price: i64,
    count: i64,
}

impl OrderLine {
    //Rust has no exceptions: a broken rule comes back
    //as a value, and the caller cannot ignore it
    fn new(sku: &str, price: i64, count: i64)
        -> Result<OrderLine, String> {
        if count <= 0 {
            return Err(format!("bad count for {}", sku));
        }
        Ok(OrderLine {
            sku: sku.to_string(),
            price,
            count,
        })
    }

    fn cost(&self) -> i64 {
        self.price * self.count
    }
}

//entity
struct Order {
    id: i64,
    lines: Vec<OrderLine>,
    paid: bool,
}

impl Order {
    fn new(id: i64) -> Order {
        Order {
            id,
            lines: Vec::new(),
            paid: false,
        }
    }

    //adding changes the order, so it asks for &mut self;
    //reading rules below is enough with &self
    fn add(&mut self, line: OrderLine) {
        self.lines.push(line);
    }

    fn goods(&self) -> String {
        let names: Vec<&str> = self.lines.iter()
            .map(|line| line.sku.as_str())
            .collect();
        names.join(" ")
    }

    fn total(&self) -> i64 {
        self.lines.iter().map(|line| line.cost()).sum()
    }

    //business rule: 10% off from 200
    fn discount(&self) -> i64 {
        let total = self.total();
        if total >= 200 {
            total / 10
        } else {
            0
        }
    }

    fn payable(&self) -> i64 {
        self.total() - self.discount()
    }

    //business rule: an empty order is never paid
    fn pay(&mut self) -> Result<(), String> {
        if self.lines.is_empty() {
            return Err("empty order".to_string());
        }
        self.paid = true;
        Ok(())
    }
}

//client code
//no database and no screen anywhere above
let mut order = Order::new(1001);
order.add(OrderLine::new("pen", 30, 2).unwrap());
order.add(OrderLine::new("lamp", 190, 1).unwrap());
println!("order {}", order.id);
println!("lines {}", order.lines.len());
println!("goods {}", order.goods());
println!("total {}", order.total());
println!("discount {}", order.discount());
println!("payable {}", order.payable());
order.pay().unwrap();
println!("paid {}", order.paid);

match OrderLine::new("pen", 30, 0) {
    Ok(_) => println!("line added"),
    Err(reason) => println!("refused: {}", reason),
}
match Order::new(1002).pay() {
    Ok(()) => println!("paid"),
    Err(reason) => println!("refused: {}", reason),
}
//printed: order 1001
//         lines 2
//         goods pen lamp
//         total 250
//         discount 25
//         payable 225
//         paid true
//         refused: bad count for pen
//         refused: empty order


Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.
Read more

Задача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно.

Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят.

Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же.

Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры.

Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно.