Сучасна диджитал-освіта для дітей — безоплатне заняття в GoITeens ×
Mazda CX 5
×

Anemic Domain Model vs Rich Domain Model в Spring

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Коли я тільки починав свій шлях програміста я знав всього кілька основних шаблонів проєктування. Тоді я думав, що досвідчені розробники їх використовують кожен день, навіть пробував їх сам ліпити там де не треба, за що і отримував по руках. З часом я змінив декілька команд, потім перейшов в іншу фірму, але широкого використання патернів я не побачив ніде. Звісно мова не йде про Builder, Singleton, Abstract Fabric, а про більш складні і менш розповсюджені шаблони. Знайома ситуація?

Щоб повністю зрозуміти проблему — треба почати з основ.

Low coupling and High cohesion говорять нам, що кожен клас не має мати багатьох залежностей від інших класів (Low coupling), але кожен клас має містити тільки ті методи і поля, які йому необхідні для роботи і виконувати тільки одну конкретну задачу (High cohesion, Single responsibility). Якщо весь код помістити в один клас — це буде Low coupling, але не буде High cohesion.

Як виглядають всі типові проекти написані на Spring? Є класична трьохшарова аплікація. Контроллер приймає виклик якогось ендпоінта, далі він викликає якийсь метод сервісу з присланими параметрами в DTO, в сервісі робиться бізнес логіка, створюється domain об’єкт і кладеться в базу (або дістається\видаляється).

Як нас вчать офіційні гайди Спрінга — всі доменні об’єкти не мають жодних методів окрім гетерів\сетерів, а представлені у вигляді POJO. Багато авторів (такі як Мартін Фаулер) вважають це антипатерном і називають його — Anemic Domain Model.

При Anemic domain design, всі логіка програми тримається в шарі бізнес логіки (сервіси, BO). Тоді доменні об’єкти не мають методів щоб оперувати своїми полями, а слідом і поведінки, що порушує принципи ООП, патерни GRASP, не дає реалізувати патерни проєктування.

Приклад анемічної моделі:

@Data
@Entity
@Table(name = "expense", schema = "expenses")
public class ExpenseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "price")
    private Integer price;

    @Column(name = "comment")
    private String comment;

    @Column(name = "date")
    private LocalDateTime date;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private UserEntity user;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "expense_to_expense_type_dict",
            joinColumns = @JoinColumn(name = "expense_id"),
            inverseJoinColumns = @JoinColumn(name = "expense_type_id"))
    @Fetch(FetchMode.SUBSELECT)
    private List<ExpenseTypeDictEntity> expenseTypeDict;

}

GRASP

Information Expert каже, що методи мають лежати в тому об’єкті поля якого використовуються. Або поля мають належати об’єкту який їх використовує.

Creator каже, що створювати об’єкт класу А має клас Б, який використовує, або має залежності від класу А. Якщо в коді є ланцюг методів, що передають один одному той самий об’єкт — це є порушенням патерну Creator, бо створювати об’єкт треба там де він буде використовуватись. Хоча інколи нам необхідно передавати об’єкт (викликати метод на іншій машині, передати дані з фронту), тоді використовують DTO, що є порушенням парадигми ООП, бо DTO — це об’єкт без поведінки, тому що поведінку неможливо передати через мережу, з цього виходить, що ми свідомо ідемо на цей крок.

Конструктор є методом, тому Creator є уточненням Information expert.

Дотримання цих принципів веде до Low coupling. Недотримання суперечить принципу інкапсуляції (один шматок коду не має лізти своїми брудними руками в інший шматок коду). Навіть якщо в об’єкт передає інформацію через гетери, це все одно інформація. Не правильно передавати і обробляти в іншому місці. З часом систему буде дуже важко змінювати.

Що ми маємо в типовій аплікації Spring? Вся логіка тримається в сервісах, які є сінглтонами, що є варіацією антипатерну Singletonism, що веде до процедурного стилю програмування, коли маємо багато дубльованого коду, методи з багатьма параметрами і локальними змінними, що в свою чергу унеможливлює розбити метод на менші і перевикористати код.

Protected Variations — проблема модифікації системи найбільш актуальна в умовах вимог, що динамічно змінюються. Найчастіше вдається виділити так звані точки нестійкості системи, які найчастіше будуть схильні до зміни / модифікації. Тоді, сутність шаблону «стійкий до змін» полягає в усуненні точок нестійкості, шляхом визначення їх як інтерфейсів і реалізації для них різних варіантів поведінки.

Вирішує проблему, коли зміна одного елементу системи веде до зміни інших елементів.

На практиці це виглядає так: є метод в якому є безліч вкладених IFів. Зміна одного вкладеного оператору веде до зміни поведінки інших зовнішніх. Щоб уникнути цього можна застосувати техніку Replace Conditional with Polymorphism. Замість сотень іфів краще створити сотню класів з поліморфним методом в яких інкапсулювати логіку, що і заохочує Information Expert. Але в анемічній моделі ця логіка буде триматись в сервісах і створювати ці об’єкти буде ніде.

Багато недосвідчених програмістів бояться створювати додаткові класи. Часто можна почути фразу «забагато класів», але якщо великі не розділяти на малі — це буде недотриманням патерну High cohesion.

Число помилок на стрічку коду є константою. На приклад програміст робить 1 помилку на 1000 рядків коду. То на практиці це буде виглядати так, що в методі на 1000 стрічок будуть постійно з’являтись баги які тяжко виправляти. А якщо той метод розбити на багато дрібних класів чи інших методів — там буде кілька стрічок коду в яких не буде тієї помилки і програміст туди без потреби не полізе. Якщо в якомусь класі і виявиться та єдина помилка — виправити її буде дуже легко, після чого ми знову забуваємо про даний клас.

В ідеалі, треба працювати з класами тільки через інтерфейси. Я б радив запозичити ідею з Test Driven Development, тобто створювати спочатку зручний інтерфейс, а потім його реалізовувати. Це дуже добре вплине на якість коду, тому що ми робимо себе клієнтами свого коду і зразу бачимо на скільки зручно використовувати новий метод.

Rich Domain Model

Що якщо спробувати перенести логіку в саму модель, як того і вимагає ООП? Є різні варіації Rich Model.

Приклад доменного об’єкту написаного в стилі Rich Domain Model Extreme:

@Entity
public class ExpenseEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Integer id;
 
  @Column(name = "price")
  private Integer price;
  ...
  

  @Autowired // модель має залежність від інфраструктури!
  private final ExpenseRepo expenseRepo;
  @Autowired
  private final AuthenticationBO authenticationBO;
  @Autowired
  private final UserRepo userRepo;
  ...
  
  
    @Transactional
    public void createExpense(ExpenseDTO expenseDTO) { 
        //модель робить забагато!
        validateExpense(expenseDTO);
        ExpenseEntity expenseEntity = new ExpenseEntity();
        initExpenseEntity(expenseEntity, expenseDTO);
        expenseRepo.save(expenseEntity);
    }
 
    private void validateExpense(ExpenseDTO expenseDTO) {
        ...
    }
 
    private void initExpenseEntity(ExpenseEntity expenseEntity, ExpenseDTO expenseDTO) {
        UserEntity userEntity = authenticationBO.getLoggedUser();
 
        expenseEntity.setUser(userEntity);
        expenseEntity.setPrice(expenseDTO.getPrice());
        expenseEntity.setComment(expenseDTO.getComment());
        expenseEntity.setDate(LocalDateTime.now());
        initExpenseTypes(expenseEntity, expenseDTO.getTypes());
    }
 
    private void initExpenseTypes(ExpenseEntity expenseEntity, List<String> types) { 
        // модель записює в базу інші об'єкти!
        List<ExpenseTypeDictEntity> expenseTypes = new ArrayList<>();
 
        types.forEach(x -> {
            ExpenseTypeDictEntity typeDict = expenseTypeDictRepo
                    .findByNameIgnoreCase(x.trim())
                    .orElseGet(() -> createExpenseTypeDict(x));
            typeDict.setUsedCount(typeDict.getUsedCount() + 1);
            typeDict = expenseTypeDictRepo.save(typeDict);
            expenseTypes.add(typeDict);
        });
 
        expenseEntity.setExpenseTypeDict(expenseTypes);
    }
 
    private ExpenseTypeDictEntity createExpenseTypeDict(String name) {
        ...
    }
}

Як ми бачимо, ми познайомили шар моделі з DI і іншою автомагією, що порушує Layered архітектуру. Крім того, в такому об’єкті може бути забагато методів і полів, які не мають нічого спільного з предметною областю.

Такий стиль є альтернативою, але на мою думку, в доменні класи не треба передавати засоби для роботи з базою даних. На практиці, такий код швидко стане пеклом для тестування і непідтримуваним, тому що завжди стилі програмування в команді дуже відрізняються і велику роль грає рівень досвідченості програмістів. Тому треба знайти певний баланс. На цю тему рекомендую чудову статтю: dzone.com/...​jects-finding-the-balance

Що нам важливо, автор рекомендує дотримуватись трьох правил:

  1. Operations/methods added to a Domain Object should mutate ONLY in the current JVM memory state of that object, and NOT any external state (i.e. state in database).
  2. Operations in the orchestrating objects (Manager/Service) should NOT break if the state of a Domain Object changes. This can be achieved if they always ask domain objects for answers to domain questions instead of trying to figure out the answers themselves.
  3. Operations on the Domain Object should ONLY use the in current JVM memory state of the object.

Тепер ми бачимо всю глибину проблеми. Обидва підходи мають серйозні недоліки. Варто спробувати зробити гібрид двох моделей. В якомусь сенсі це буде анемінча модель на стероїдах. В сервісах ми тримаємо логіку доступу до даних які лежать в базі, а в самих доменних об’єктах ми тримаємо методи для роботи з даними конкретного класу. В такому випадку доменні класи виглядатимуть приблизно так:

@Data
@Entity
@Table(name = "user", schema = "expenses")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "email")
    private String email;
    ...

    @OneToMany(mappedBy="user")
    private List<ExpenseEntity> expenses;

    ...

    public Integer calculateAllExpensesPrice() {
        return expenses.stream()
                .mapToInt(expenseEntity -> expenseEntity.getPrice() * getUserDiscount(expenseEntity))
                .sum();
    }
    
    public List<ExpenseTypeDictEntity> getExpenseTypes() {
        return expenses.stream()
                .flatMap(expenseEntity -> expenseEntity.getExpenseTypeDict().stream())
                .collect(Collectors.toList());
    }

}

Такий клас легко покрити тестами, використовувати його буде UserService, в який достатньо вставити кілька моків — інтергаційний тест готовий!

Крім усього вище сказаного, слід зазначити, що навіть якщо дотримуватись усіх best practices, все одно, інколи не можна зробити рефакторинг через зовнішні обмеження. В першу чергу це база даних. Ми не можемо просто так переміщувати чи міняти поля в доменній моделі, бо до цього треба ще міняти стан самої бази, що може призвести до поганих наслідків.

Як вже говорилось, використання анемічної доменної моделі веде до унеможливлення або зниження ефективності використання багатьох технік і патернів.

На приклад Builder має чудове використання в доменному об’єкті. Можна легко перенести величезні методи ініціалізації об’єктів в самі об’єкти. В сервісі більше не буде методу

initExpenseEntity(expenseDTO)
 на 20 ліній з setParameter(value)... Можна додати методи які приймають DTO або інші об’єкти, ініціалізують поля значеннями за умовчанням і тд. Головне, що тепер цей код не буде знаходитись в сервісі.

Замість:

private void initExpenseEntity(ExpenseEntity expenseEntity, ExpenseDTO expenseDTO) {
    UserEntity userEntity = authenticationBO.getLoggedUser();

    expenseEntity.setUser(userEntity);
    expenseEntity.setPrice(expenseDTO.getPrice());
    expenseEntity.setComment(expenseDTO.getComment());
    if (expenseDTO.getDate() != null)
        expenseEntity.setDate(expenseDTO.getDate());
    else
        expenseEntity.setDate(LocalDateTime.now());
    initExpenseTypes(expenseEntity, expenseDTO.getTypes());
    
    ...
}

Буде:

@Data
@Entity
@Table(name = "expense", schema = "expenses")
public class ExpenseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "price")
    private Integer price;

    @Column(name = "comment")
    private String comment;

    @Column(name = "date")
    private LocalDateTime date;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private UserEntity user;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "expense_to_expense_type_dict", schema = "expenses",
            joinColumns = @JoinColumn(name = "expense_id"),
            inverseJoinColumns = @JoinColumn(name = "expense_type_id"))
    @Fetch(FetchMode.SUBSELECT)
    private List<ExpenseTypeDictEntity> expenseTypeDict;


    public static class Builder {

        private ExpenseEntity expense;

        public Builder() {
            expense = new ExpenseEntity();
        }

        public Builder fromDTO(ExpenseDTO expenseDTO) {
            expense.price = expenseDTO.getPrice();
            expense.comment = expenseDTO.getComment();
            if (expenseDTO.getDate() != null)
                expense.date = expenseDTO.getDate();
            else
                expense.date = LocalDateTime.now();

            return this;
        }

        public Builder withUser(UserEntity user) {
            expense.user = user;
            return this;
        }

        public Builder withExpenseTypes(List<ExpenseTypeDictEntity> expenseTypes) {
            expense.expenseTypeDict = expenseTypes;
            return this;
        }


        public ExpenseEntity build() {
            return expense;
        }
    }

}

Тоді сворення нового об’єкту в ExpenseService буде виглядати так:

@Override
@Transactional
public void addNewExpense(@Valid ExpenseDTO expenseDTO) {
    ExpenseEntity expenseEntity = createExpenseEntity(expenseDTO);
    expenseRepo.save(expenseEntity);
}

private ExpenseEntity createExpenseEntity(ExpenseDTO expenseDTO) {
    return new ExpenseEntity.Builder()
        .fromDTO(expenseDTO)
        .withUser(authenticationBO.getLoggedUser())
        .withExpenseTypes(findExpenseTypes(expenseDTO.getTypes()))
        .build();
}

Стоп. Хіба це не буде порушенням патерну GRASP — Creator? Так, буде! Взагалі, ледь не половина патернів GoF в якійсь мірі щось порушують, згадати один тільки Visitor, який ненавидить ООП, але бувають ситуації, коли без того ніяк. Як ми вже переконались, не можливо написати Enterprise проект не порушивши жодного принципу, практики чи шаблону. Тому питання варто ставити інакше. Якщо ми щось порушуємо, які це принесе наслідки? Що це нам дасть? В випадку з Builder ми не ускладнюємо код. Даний шаблон тяжко зламати і легко використовувати. Є дуже зрозумілий для нових програмістів на проєкті. Тому його використання є більш ніж виправдане.

Так само стають недоступними багато технік рефакторингу. На приклад Replace method with Method Object (створюємо клас в конструктор якого передаємо this і викликаємо метод .compute()). В анемічній моделі проблема просто переноситься в інше місце, бо без сенсу передавати об’єкт сервісу.

В наших прикладах тяжко придумати якусь тяжку логіку, щоб використання цієї техніки було виправдане, але уявіть, що нам треба рахувати всі витрати даного юзера на підставі багатьох полів. В такому випадку наш метод не буде мати локальних змінних, а всі вони стануть полями нового класу, що може суттєво покращити чительність методу. Тоді буде легко розбити метод на багато дрібних, які не будуть передавати 4+ параметрів з метода в метод. Виглядатиме це так:

@Data
@Entity
@Table(name = "user", schema = "expenses")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @Column(name = "email")
    private String email;
    ...

    @OneToMany(mappedBy="user")
    private List<ExpenseEntity> expenses;

    ...

    public Integer calculateAllExpensesPrice() {
        return new ExpensesPriceCalculator(this).compute();
    }

}

class ExpensesPriceCalculator {
    
    private List<ExpenseEntity> expenses;
    
    private Integer discount;
    
    
    public ExpensesPriceCalculator(UserEntity user) {
        // Copy relevant information from the expense
        ...
        this.expenses = user.getExpenses();
    }
    
    public Integer compute() {
        // Perform long computation.
        ...
        initDiscount();
        validateExpenses();
        filterExpenses();
        ...
        return calculateFinalPrice();
    }
    
}

Навіщо це все?

«Кожен дурень може написати код, зрозумілий компілятору, не кожен може написати його зрозумілим для людини». Знаю, що чули це вже багато разів. Але чи кожен розуміє навіщо це?

Для бізнесу, в першу дуже важливо наскільки швидко розробник може зробити новий функціонал. Для розробника головним чинником є якість існуючої кодової бази (не беручи до уваги досвідченість самого програміста). Якщо в нас зрозуміла архітектура і чистий код — процес розробки іде швидко, це заощаджує гроші замовнику і підвищує наш професійний рівень. Але якщо в нас Spaghetti code — часом може бути неможливо додати нову фічу без рефакторингу всього проєкту, що може зайняти тижні чи навіть місяці.

В кожного часто буває ситуація, коли треба швидко пофіксити якийсь баг. Ми розібрались що треба наклацати на фронті, викликаємо якийсь метод на сервері, вмикаємо дебаг та йдемо по довгих ланцюгах методів в сервісі, проходимо кілька методів на 20+ ліній і в кінці знаходимо проблему. Це займає багато часу, треба подивитись зразу багато коду, який нас не цікавить. Набагато краще розділити методи, і тримати логіку моделі в самій моделі, а доступ до неї в сервісах. Тоді в нас збільшиться кількість методів, що позитивно вплине на читабельність коду, так як кожен метод буде свого роду коментарем для коду який він робить.

Висновок

Rich Domain Model чудово звучить в теорії, дозволяє писати значно якісніший код з повнішим використанням ООП, але вимагає професійної команди, програмісти в якій добре знають стиль програмування один одного. Anemic Domain Model в свою чергу, є значно легшим в імплементіції і підтримці, але сильно урізає проєктувальні можливості і сприяє процедурному стилю програмування. Тому, в реальному світі, можна пробувати знаходити баланс і частину логіки переносити в доменну модель, а виклики залишати в сервісах.

Такий, «гібридний», підхід дає значну перевагу — ми можемо розділити логіку. Методи які будуть використовувати ініціалізовані поля об’єкту — помістимо в сам об’єкт (доменна модель), а логіку доступу до цієї моделі (мається на увазі діставання\запис в базу) — в сервіси. Таким чином, цей підхід робить зрозумілішим розділення моделі в шарі MVC на модель і сервіси, полегшує тестування, сприяє чистішому коду за рахунок логічнішого груповання методів.

Resources

Martin Fowler blog:
Book Refactoring — Martin Fowler.
dzone.com/...​jects-finding-the-balance
www.javaworld.com/...​esign-with-java-ee-6.html

Patterns.

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

В одном чате по архитектуре ПО очень любят холиварить на эту тему, месяц назад был последний эпичный холивар на эту тему.

В целом стоит сказать, что рекомендации всех фаулеров и прочих дядей бобов надо делать поправку на момент написания ибо нестареющей классикой это не является, и делить на 10, потому что формулировки у этих авторов предельно аморфные и расплывчатые.

Рекомендации фаулеров, изобретённые паттерны и прочее прибиты гвоздями к техническим возможностям языков изобретённых в тот момент. Как мы знаем во времена когда это всё придумывалось и писалось, языки были в большинстве были так себе, а сейчас гораздо лучше.

Что анемичная модель, что толстая модель — одни и те же яйца только в профиль. И то — и другое набор переменных, которым раздали теги-имена, и навесили некоторые правила преобразования и условия на их значения(хотя, чтобы всё аккуратно это формализовать нужно разводить выкладки на несколько страниц. моделька у меня есть но пост и так уже многабукав). Просто в случае рич модели, ведомый принципом не лезь юда во внутрь, ломаешь блжад, ты спихиваешь всё в кучу и получаешь @Traditional джава класс в более 9000 строчек бизнеслогики магических аннотаций фреймворка и какой-нибуть левой логики для работы с какими-нибуть либами который подвешиваетвает как editor services в IDE и редакторах так и мозги читателя при попытке что то тут поменять. Ну и по самому строению рич модели — она масштабируется только вертикально дописывая 9001 первую строчку для добавления нового функционала. Это уже само по себе вызывает ожоги ниже пояса, но тут ещё вмешивается сильная контекстная зависимость кода и усугубляет ситуацию. Так что рич модель это хорошо, если вы умеете эффективно справятся с означенными проблемами, т.е. у вас есть мозг размером с бегемота который может это всё переварить, или у вас есть мощная система типов которую можно заставить запрещать стрелять себе в ногу.

В случае анемик модели мы можем растащить правила преобразования в маленькие кусочки с которыми мы можем работать немного более удобно, но нам достаточно часто придётся отказываться от идеи «не лезь сюда вовнутрь, сломаешь, блжад!» в пользу других способов контроля валидности состояния. Анемик модель масштабируется и в верх, и вширь, по тому и любима разработчиками, чтобы пробросить новое правило преобразования, или же метод как вы любите его называть, вам не нужно лопатить все 9000 строк кода и распутывать клубок контекстной зависимости. Если вы можете разграничить те саме правила преобразования и условия валидности на взаимо независимые, то ваша задача существенно упрощается. Кроме того она ОЧЕНЬ упрощается если вам надо что то поменять по тем же причинам. Единственное что, некоторые языки программирования не дают возможности при этом сохранять анемик модель + преобразователи

Так само стають недоступними багато технік рефакторингу.

Вам техножрецы разрешают использовать только освященные верховным архимагосом ритуалы рефакторинга? Естественно, что важные дяди придумывали рефакторинг под своё видение чистого кода. Если оно отличается от их, то естественно что ими придуманные рефакторинги поломаются и надо будет применять что-то другое. Вообще говоря паттерны, рефакторинги и проч. полностью зависия от фич языка в котором они все исопльзуются.

Rich Domain Model чудово звучить в теорії, дозволяє писати значно якісніший код з повнішим використанням ООП, але вимагає професійної команди, програмісти в якій добре знають стиль програмування один одного.

Да не команда вам нужна а пруф ассистант с подержкой зависмой типизации, и смогут даже бибизяны которые в состоянии осознать что такое зависимая типизация... Как извесно пюр ООП моделировать на языках с хреновой системой типов это надо уметь, вам же LSP надо в голове доказывать вместо того чтобы прувер его проверил и проч.

але сильно урізає проєктувальні можливості

предназначенные для работы с рич моделью. Для анемика есть очень много своего, вон «эФПэ» языки успешно работают с анемик моделью в виде кейс/дата/рекорд\индуктив классов, ничего не урезается, просто смотреть надо чуть шире.

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

Тайпчекай пожалуйста свои высказывания. Это не очень хорошо с той точки зрения что может закончится фьюжном анемик и доменной модели, который собирает недостатки обеих. Тут нужно вставить «взаимонезависимые части логики которые соприкасаются по минимуму, и так чтобы не закончилось тем что указано выше».

Ну и ещё по мелочам.

бо DTO — це об’єкт без поведінки, тому що поведінку неможливо передати через мережу,

бредятина, всегда можно же, вон спарк джобы как то же с этим справляются. Фримонадки или ТФ так вообще рубят на нет этот вопрос.

порушенням парадигми ООП

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

один шматок коду не має лізти своїми брудними руками в інший шматок коду

ну опять же, этот от импотенции системы типов. Если вы можете залезть ничего не сломав особого смысла в «я запрестчаю сюда лесть» нет.

що веде до процедурного стилю програмування, коли маємо багато дубльованого коду, методи з багатьма параметрами і локальними змінними, що унеможливлює розбити метод на менші і перевикористати код.

дражайший, гонка за пуризмом ФП\ООП\ПП\АОП\(допишите своё) ради пуризма не имеет смысла. Дупликация целиком оправданна в тех местах которые делают разные, взаимонезависимые вещи с точки зрения постановки задачи, дабы соблюдать SRP. А переиспользование кода сильно зависит от конструктов системы типов, больше чем от паттэрнов.

На практиці це виглядає так: є метод в якому є безліч вкладених IFів. Зміна одного вкладеного оператору веде до зміни поведінки інших зовнішніх

Для того чтобы перестать уйти из простыни, набросанной из хаотичного сочетания ифов, траев, кетчей, форов и прочего были давно выдуманы монадки, функторы, фолдаблы, аппликативы и патмат. пишем на них и большинство ифов остаётся в их реализации, а наш код остаётся без ифов. И нет, он не перестаёт быть читабельным.

Число помилок на стрічку коду є константою.

Это точно бред. Баги зависят от: 1) essential complexity задачи 2) скила разарба 3) мгновенного состояни разраба 4) багофильтра языка. дальнейшие построения на основе этого по понятным причинам неверны.

TL:DR, автор смотрит на вещи как то однобоко и иррационально.

Есть паттерн Null Object. Вместо того, чтобы проверять IsNotNull(hook) перед вызовом hook(), приписываем стратегию, которая ничего не делает. Код становится чище и перестает падать, если где-то забыли сделать проверку.

Наличие единичного/нулевого объекта во множестве делает его значительно лучше и удобнее.

Вот если бы ещё кто сподобился в «рабоче-крестьянский» туториал по фри монадам / tagless final...

Я на практике осилил ФП до уровня IO / State / Reader, а вот до уровня free / TF уже мозгов не хватило :( поскольку все найденные на эту тему статьи писались, видимо, математиками для математиков

Олег Нижник писал, думаю это будет чуть более простым и понятным. habr.com/ru/post/325874

Ох, не сказал бы, поскольку понимание этого материала требует знания Scala на достаточно хорошем уровне :(

В этом, кстати, основная беда многих обучающих материалов по FP — они предполагают знание специфических языков программирования, а то, и ещё хуже — особенностей конкретных библиотек типа scalaz / ZIO / Dotty.

В идеальном мире, авторам стоило бы вначале иллюстрировать саму концепцию чуть ли не на псевдокоде — или вообще в виде «весёлых картинок», как это сделал автор «The three useful monads» — и только потом переходить к описанию реализации на конкретном языке.

ты шо, так же любой дурак сможет разобраться, кому оно надо-то? надо охранять свое болото

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

Но, да, увы — есть и те, кто ставят себя выше других и кичатся причастностью к «тайному знанию».

Дружище, ты начал с правильных мыслей, но пришел к ужасной херне.

@Entity
public class ExpenseEntity {
  ...  
  @Autowired
  private final ExpenseRepo expenseRepo;

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

Те що так робити не треба — згоден на 100%. Я подав приклад чистої Rich Model

Rich Domain Model є альтернативою, але на мою думку, в доменні класи не треба передавати засоби для роботи з базою даних.
в такому об’єкті може бути забагато методів і полів, які не мають нічого спільного з самим доменним об’єктом.

Это вообще не имеет никакого отношения к Rich Model. Это просто херня, которая херней является при любой модели.

Що тоді по-вашому є Rich model і де тоді мають інжектитись репозиторії?

В command handler’ы они должны инжектится. Модель предметной области — хоть анемичная, хоть нет — не должна ничего знать о persistence. От слова «совсем».

С другой стороны, это уже дискуссия про onion architecture / hexagonal, но уж никак не про rich vs. anemic (в дискурсе которой, по-хорошему- только лишь распределение ответственности между domain entities, domain services, и application services — с сохранением запрета пробрасывать в слой domain model зависимостей от внешних слоёв)

В layered architecture, яку я і описував, нема command handler.

onion architecture / hexagonal

Це вже зовсім інша історія, і щоб цю архітектуру успішно використати — треба відмовитись від hibernate, що є неприпстимо для переважній більшості проєктів.

Модель предметной области — хоть анемичная, хоть нет — не должна ничего знать о persistence. От слова «совсем».

Rich Domain Object Extreme якраз і радить це робити. Чи це є погано? Так, молдель не має знати як її в базу зберігати, саме того я писав про знаходження балансу.

В layered architecture, яку я і описував, нема command handler.

Я навскидку не помню, что там есть. Контроллер? Application service?

Обычно, всё же, есть какая-то прослойка, транслирующая запрос пользователя в одну или более операций бизнес-слоя. Вот туда и инжектить.

Важно здесь то, что эта прослойка, как бы она не называлась в конкретном подходе, отвечает за композицию бизнес-операций, которые нужно выполнить для того или иного user flow.

щоб цю архітектуру успішно використати — треба відмовитись від hibernate

Я не джавист, поэтому, возможно, глупый вопрос — а Hibernate разве не позволяет задавать отображение на базу для конкретной сущности извне, а не только аннотациями на самой сущности?

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

Обычно, всё же, есть какая-то прослойка, транслирующая запрос пользователя в одну или более операций бизнес-слоя. Вот туда и инжектить.

В класичному спрінгу це сервіси і контроллери. Саме туди я і радив додавати ці залежності. А при використанні rich model, взагалі нема шару сервісів, бо інтерфейс для роботи з інфраструктурою є в моделі.

Я не джавист, поэтому, возможно, глупый вопрос — а Hibernate разве не позволяет задавать отображение на базу для конкретной сущности извне, а не только аннотациями на самой сущности?

На скільки я знаю — ніяк. XML це застарілий підхід. Можна винести конфіг туди, але що це дасть? Всеодно всі поля треба буде описати і жорстоко закріпити, але тепер це буде в іншому файлі.

А при використанні rich model, взагалі нема шару сервісів, бо інтерфейс для роботи з інфраструктурою є в моделі.

Вы меня простите, пожалуйста, но в каком именно учебнике вы такое прочли?!

XML це застарілий підхід. Можна винести конфіг туди, але що це дасть? Всеодно всі поля треба буде описати і жорстоко закріпити, але тепер це буде в іншому файлі.

Даст то, что сами бизнес-сущности и бизнес-сервисы вообще никак не будут привязаны к механизму persistence. А устарел XML, разве что, в умах юных хипстеров.

Даст то, что сами бизнес-сущности и бизнес-сервисы вообще никак не будут привязаны к механизму persistence. А устарел XML, разве что, в умах юных хипстеров.

он устарел потому-что по факту оно у тебя по прежнему связано только разорвано между двумя местами — что плохо (ибо надо синхронизировать что неудобно), потому и считается устаревшим в этом контексте.

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

В этом же и смысл, что бизнес-логика никак не привязана к конкретной реализации доступа к данным и не тянет за собой никаких искусственных зависимостей из серии Hibernate.

Так а при отвязке бизнес-сущностей от механизма persistence в любом случае нужно синхронизировать

Знову таки, модель ніяк не від’вязується, просто кофігурація переноситься в інший файл. Ви думаєте в правильну сторону, але в Spring так не робиться.

Вы меня простите, пожалуйста, но в каком именно учебнике вы такое прочли?!

Patterns of Enterprise Application Architecture, Martin Fowler. Половина прикладів в книжці про схожу архітектуру.

Даст то, что сами бизнес-сущности и бизнес-сервисы вообще никак не будут привязаны к механизму persistence.

Будуть. Якщо поле привз’язане до колонки в базі, яка різниця чи воно прив’язане в xml чи над полем буде анотація?

А устарел XML, разве что, в умах юных хипстеров.

Саме того він і безнадійно застарів в концепції спрінга. Конфігурувати не зручно, а жодних переваг в порівнянню з annotation based конфігурацією воно не дає.

Patterns of Enterprise Application Architecture, Martin Fowler. Половина прикладів в книжці про схожу архітектуру.

В этой книге отличают модель и ентити, вы не отличаете. Я понимаю что «чукча писатель, а не читатель», но даже в таком случае переделайте статью с нормальной аргументацией, а не пытайтесь спорить с коментами, которые были написаны «8 днів тому».

Будуть. Якщо поле привз’язане до колонки в базі, яка різниця чи воно прив’язане в xml чи над полем буде анотація?

В случае с хмл не будут. Вы можете взять энтити и модели и использовать их с другим персистансом или выдать другому компоненту системы.
А что случится если вы расшарите энтити с хибернейт аннотациями? Код скомпилится без зависимости на хибер? А как будем в том втором модуле менять имена таблиц и полей?

P.S.

в Spring

Откуда взялась эта приписка в заголовке темы? Решили еще и в «мамкиного сеошника» поиграть?

але вимагає професійної команди

А чого такий суворий і майже недосяжний критерій? Може можна все таки аматорами всюди обійтися?

На практиці, стабільніший код буде при анемічній моделі. Якщо в Rich Model присвячувати не достатньо часу на рев’ю коду — проект швидшко скотиться в лайнокод, а відрефакторити це буде тяжко, тому, що модель прив’язана до бази.

Имхо как раз наоборот, Rich Model гораздо сложнее неправильно использовать, ибо стейт защищен методами и костыль который что то мутирует сбоку сложнее пропустить на ревью. Ну и при правильном написании она практически не связана с моделью базы (репозитории принимающие и возвращающие исключительно объекты доменной модели в помощь, а что там внутри репозитория его сложности, пусть хоть в несколько баз раскладывает в соответствии с требованиями того же GDPR). Но с чем согласен, что, как минимум, на начальных этапах разработки требуется достаточно высокая квалификация разработчиков. Ну и весьма дорога в разработке на начальном этапе, поэтому имеет смысл применять на долгоиграющих проектах.

Сильно зависит от того какого рода сервис. Сервис начисления процентов по банковским вкладам, сервис который просто возвращает каталог продуктов и сервис который является фасадом для внешних вызовов, и проксирует вызовы на внутренние сервисы, три абсолютно разных примера. Это примерно как «что лучше танк или снайперская винтовка»

В третьем примере их нет (если это чистое проксирование, плюс, возможно, секьюрити). Во втором условно нет, но зависит от того, из чего собирается продукт. Допустим под капотом, может быть поход еще в три сервиса, на основании которых вы соберете и вернете «продукт каталога». Возможно, у вас будет логика вычисления применимости продукта для разных групп потребителей или еще что то в этом духе. Ее можно выразить стейтлес сервисом, а можно методами модели. Мне больше нравится когда доменный сервис отвечает за получение данных (в самом простом случае это может быть даже аппликейшн сервис), а модель за вычисления, гораздо проще покрыть логику вычисления юнит тестами ничего не мокая от слова совсем. Если вы будете кешировать данные, и у вас нет иммутабельности из коробки, то написание рид онли моделей поможет реализовать иммутабельность для безопасного кеширования. Или еще вариант, вы не доверяете полученным данным, и хотите сразу создать объект модели, который проверит их валидность и будет гарантированно в валидном состоянии либо упадет с исключением (формально это уже не про рич модель), и только после етого смапить его на ДТО и отдать наружу. В первом примере результат начисления процентов может быть представлен в виде явно выраженного агрегата который будет сохраняться, и как вариант проценты по вкладу будут набрасываться на этот агрегат в виде доменных событий каждый месяц. Дисклаймер: все выше перечисленное не является единственно «правильным» вариантом реализации, а скорее одним из вариантов решения проблемы.

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

дык я и не писал что это будет решать сам товар с каталога. В модели такую применимость я бы выразил спецификацией (спецификацию я тоже отношу к модели).

До того ж, каталог товарів read only, немає зміни стану об’єктів, немає запису їх в базу, тобто це, ну, ніяк не загальний кейс rich domain model.

Именно поэтому я написал оговорку

(формально это уже не про рич модель)

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

З.Ы. а вообще я побывал в обоих лагерях, как ярых противников, так и сторонников. И действительно, проблема в том, что мало кто умеет готовить те самые модели. Часто приходится наблюдать классическое «из чего состоит слон? Из хобота ушей и бегемота». Ну или классический пример, с Product.IsAvailable который вы привели выше. С другой стороны меня еще больше кумарят помойки с «SuperUtility» в которые пихают все подряд, вместо того, чтобы разбросать ответственность по специфичным классам. Тоже классика, если не знаешь что это, назови «Utility» и запихни куда нить в инфрастрактучур.

Цікаво, а як ми взагалі без оцього всього жили десятки років і зовнішній світ розуміли? Так пишеш ніби людина походить від Homo Bureaucraticus.

Нащо взагалі знати, що поведінкова модель коду має бути відповідною ПРОЦЕСАМ та СТАНАМ, які власне і підлягають кодуванню. Якщо це CRUD, то класи мають бути дуже близькими до моделі даних, не виключено що мати методи для керування їхнім життєвим циклом. Так, це погана практика з точки зору бюрократа... але ідеальна з точки зору ООП, та читабельності коду, заради якого власне ООП і створене.

Основна мета читабельності коду — уникнення помилок. Бо якщо починати розкидати функціонал по лісу абстракцій, то для читабельності коду знадобиться документації вчетверо більше ніж сам код. А от підтримка її цілісності — то адище, на відміну від коду, який легко рефакториться.

Ключове правило: клади все до купи, ПОТІМ рефактори. Нема що класти — клади документацію, клади тести.

Так само тести — мають тестувати усе в купі, а не поведінку окремих структур. Власне кажучи, поведінку примітивів взагалі не варто тестувати, зайве покриття тестами лише додає дьогтю та 1-2 тижні до викатування фінальної версії. Не кажучи вже про бета-версії, які по-хорошому мають містити тести, яких не буде в продакшені.

Тому патерни патернами, але спробуй написати тести так, щоб їх можна було читати — і побачиш як рефакторити код. І коли. І чому зазвичай краще пистати так, як процес розумієш, а не так як треба бюрократам (і забудеш половину дрібниць, які потім будуть занозами у дупі).

PS. Все що ти здатен мислити — є патерном. Тому це маразм створювати собі ідоли із 20, 40, чи 100500 патернів лише тому що десь колись вони були гарною практикою. Краще вчи мови, зокрема англійську, пиши більше коментів та докумнетації, і тебе зрозуміють. І в жодному разі не віддавай написання документації «професійним писакам» — бо тоді продуктом не зможуть користуватися.

Бо якщо починати розкидати функціонал по лісу абстракцій, то для читабельності коду знадобиться документації вчетверо більше ніж сам код. А от підтримка її цілісності — то адище, на відміну від коду, який легко рефакториться.

Система типов лучше документации. Если она достаточно мощная чтобы впилить туда выполнение констреинтов целостности или хотя бы найти все куски которые относятся к отдельно взятому. И да, не та хреновина которая в жабке, а что-то посерьёзнее.

з точки зору ООП, та читабельності коду, заради якого власне ООП і створене.

готов поспорить про то что ООП лучше всего с этим справляется, в том тулинге в котором его используют.

Ключове правило: клади все до купи, ПОТІМ рефактори

ПОТОМ СТРАДАЙ выясняя какого хрена у тебя в этом классе из 80 полей и 300 методов случается фигня.

Якщо цей клас добре документований та настільки самодостатній — то чом би й ні? Подивися свої класи з іменами *settings*, щось підказує там зроблено багато виключень з хороших практик, але найбільші маразми ретельно збережені.

Подивися свої класи з іменами *settings*

таких нету, звиняй, а если ты про конфиги,то во первых это простая АДТ без наследования побитая на секции для удобства девопсинга, а во вторых, пюрконфиг и никакого

найбільші маразми

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

Ровно до тех пор, пока тебе не доверили программить действительно сложные объекты и процессы.

И вот тогда встаёт вопрос цены ошибки — и ты можешь сделать или по канонам бюрократии, раскидав всё на 100500 абстракций, и собирая потом баги десятилетиями — или прописать «мастера», в том числе циклами обслуживания, которые могут проверить зависимости и кошерности, выдать ворнинги... в общем, вся вкусняшка ООП.

Сама суть ООП в том, что данные и управляющий код это абсолютно разные вещи с абсолютно разным жизненным циклом, но для человека оно лежит в одном месте. И чем ближе оно находится, чем компактнее (но разумеется не всё в одной большой куче) — тем проще этим всем управлять.

Так что как бы не напрягались бюрократы над своими системами правильных правильностей, но человеческий фактор — слабое звено, узкое горлышко всего процесса не только творения, но и поддержки существования кода. И под него приходится прогибаться. Если так не сделать — бобик сдохнет, и дешевле взять нового бобика чем измываться над трупиком старого.

Ровно до тех пор, пока тебе не доверили программить действительно сложные объекты и процессы.

Прикол в том что моему тулингу по барабану сколько там переменных в конфиге, что 100 что 1000. Время компиляции будет дольше, да и всего то.

и ты можешь сделать или по канонам бюрократии, раскидав всё на 100500 абстракций, и собирая потом баги десятилетиями

Это в вашей джавке где у разрабов рантайм головного мозга и «сложные системы типов для задротов» это основная парадигма разработки. Мой тулинг позволяет держать всё в куче даже если оно распилиено.

раскидав всё на 100500 абстракций,
что данные и управляющий код

это с точки зрения погромиста одно и то же. Int, Int => Int, Kleisli[F, Foo, Bar] это просто термы, у них жизненный цикл это с момента написания погромистом до момента стирания. Одержимость вот этим вашим «а как оно там себя поведёт рантайме» явный признак того что абстракции вашей системы погроммирования не просто протекают, а фонтанируют. Объект/класс это ровно такой же терм как и все остальные, и он тоже никак себя не ведёт.

но для человека оно лежит в одном месте

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

бюрократы над своими системами правильных правильностей

системы правильных правильностей прекрасно эрадицируют фактор дурака в тех пределах в которых они разработаны его эрадицировать.

Я немало читал про ФП, но и то затрудняюсь понять термин «терм» (не говоря уже о том, что стрелки Клейсли тоже не то, чтобы каждый день встречались в ФП практике, хоть они и удобны при композиции монад, да)

Уверен, в том, что вы пишете, есть смысл — но было бы неплохо для иллюстрации идеи набросать пример, более-менее понятный «простым смертным»

Если это статья, то где выводы?
Если вопрос, то слишком много букв.

Дякую за коментар, додав висновок.

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

Садись, два!
Смешав 2 подхода ви получите «худшее из 2-х миров»:
— вы не сможете легко жонглировать (переопределять поведение) сервисами и скорее всего понадобится дополнительные «наследники» доменных моделей и фабрики, которые их порождают;
— вам прийдется тратить больше времени на дизайн.

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

Щоб чіткіше донести свою думку, я додав ще один приклад з

UserEntity

. Я не пропоную створювати ієрархії сервісів. Я мав на увазі, що обидві моделі мають свої недоліки, а те що в кінці запропонував, є покращеною версією анемічної моделі. Замість того щоб тримати доменні класи у вигляді DTO, я пропоную класти в них методи які будуть працювати з їх даними, внутрішні класи такі як білдери тощо. В свою чергу, такі об’єкти буде важче перетворити в лайнокод, навідміну від rich model.

UserEntity

Так вы еще и хибер используете? На «expenses.stream()» НПЕ не вилитал?

— Что будете делать когда дискаунт будет зависеть от структури покупок (например 3 товара в корзине и тогда на последний по цене скидка)?
— Что делаем с отменами платижей?
— Если человек не получил скидку то getDiscount вернет 1?
— Зачем вы експозите метод calculateAllExpensesPrice даже тем кому просто нужен email?

Замість того щоб тримати доменні класи у вигляді DTO

DTO — a data transfer object (DTO[1][2]) is an object that carries data between processes. Это паттерн такой, для передачи данных. Доменная модель (не важно анемик или рич) — это про модели внутри (логического) процесса.

я пропоную класти в них методи які будуть працювати з їх даними, внутрішні класи такі як білдери тощо.
fromDTO(ExpenseDTO expenseDTO)

Поздравляю у вас антипаттерн № 2: модель которая знает про ДТО.
Что произойдет когда нужно будет добавить еще одну ДТО для работи с другим процессом? Где будет находится мапинг в доменную модель?

return new PriceCalculator(this).compute();

А теперь пересчитайте цену в багзах и евро по актуальному курсу на момент сделки и на конец квартала.

Перш за все, дані приклади я написав для даної статті і їх слід розглядати швидше як псевдокод, так як їх ціль — показати реалізацію, а не робити щось корисне.

На «expenses.stream()» НПЕ не вилитал?

Не вилітав, чого б він мав? Перевірте локально, якщо в юзера не буде жодного об’єкту expense, то в полі буде порожній ліст.

DTO — a data transfer object (DTO[1][2]) is an object that carries data between processes. Это паттерн такой, для передачи данных. Доменная модель (не важно анемик или рич) — это про модели внутри (логического) процесса.

Тут не зовсім згоден. Я мав на увазі клас, який має тільки гетери\сетери, тобто POJO. Але якщо розлядати анемічну модель, то сервіси і модель — два різні шари. Тому модель є свого роду DTO, які переносять інформацію з бази в сервіси.

Поздравляю у вас антипаттерн № 2: модель которая знает про ДТО.

Вся анемічна модель це антипатерн. Чи варто з цим боротись? Не думаю, але можна взяти хороші практики rich model, щоб очистити сервіси від зайвих методів.

Что произойдет когда нужно будет добавить еще одну ДТО для работи с другим процессом? Где будет находится мапинг в доменную модель?

Хороше питання. З іншого боку, де ці методи тримати в класичній анемічній моделі? В сервісах, бо більш ніде, тоді сервіс розростеться і 80% коду буде з гетерами\сетерами які ініціалізують модель. Пам’ятаємо, що число помилок на лінію коду є константою, тому з часом сервіс перетвориться в смітник.
Можна спробувати їх тримати в моделі, але в якийсь момент їх буде дуже багато. Я думаю, що найкращий варіант — створити білдер накшталт Builder в ExpenseEntity, але в такому випадку тримати його в окремому пакеті звичайними класом, а не статичним.

А теперь пересчитайте цену в багзах и евро по актуальному курсу на момент сделки и на конец квартала.

Знову таки, ціль прикладу — донести ідею в зрозумілій формі, а не робити корисну бізнес логіку.

Вся анемічна модель це антипатерн

Автор статьи с вами не согласен:

Тому, в реальному світі, можна пробувати знаходити баланс і частину логіки переносити в доменну модель, а виклики залишати в сервісах

---

Знову таки, ціль прикладу — донести ідею в зрозумілій формі

Тратить на розбирание «по предложениям» у меня нет желания, поэтому выжымка:
Статья плохо проработана, имеент слабую аргументацию и непроработаные выводы.

Тратить на розбирание «по предложениям»

я сделал за тебя, верхний коментарий.

Спробував структурувати стяттю і змінити приклади на більш зрозумілі. Щось таке писав перший раз, тому пшепрашам =)

Коментар порушує правила спільноти і видалений модераторами.

Підписатись на коментарі