DDD відповідає OOP: Як реалізувати об'єктно-орієнтований сховище?


12

Типова реалізація сховища DDD не виглядає дуже OO, наприклад save()метод:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Інфраструктурна частина:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Такий інтерфейс очікує, що це Productбуде анемічна модель, принаймні з геттерами.

З іншого боку, ООП каже, що Productоб'єкт повинен знати, як врятувати себе.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Справа в тому, що коли Productзнає, як зберегти себе, це означає, що код інфраструктури не відокремлений від доменного коду.

Можливо, ми можемо делегувати збереження іншому об'єкту:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Інфраструктурна частина:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

Який найкращий підхід для досягнення цього? Чи можлива реалізація об'єктно-орієнтованого сховища?


6
OOP каже, що об’єкт Product повинен знати, як зберегти себе - я не впевнений, що це дійсно правильно ... OOP сам по собі насправді не диктує це, це скоріше проблема дизайну / шаблону (саме там, де DDD / що завгодно ви
-виходить вхід

1
Пам’ятайте, що в контексті ООП мова йде про об’єкти. Просто об’єкти, а не збереження даних. Ваше твердження вказує на те, що стан об'єкта не повинен управлятися поза собою, з чим я згоден. Репозиторій несе відповідальність за завантаження / збереження з деякого стійкого шару (який знаходиться поза сферою OOP). Властивості та методи класу повинні підтримувати власну цілісність, так, але це не означає, що інший об'єкт не може відповідати за збереження стану. І, геттери і сетери повинні забезпечувати цілісність вхідних / вихідних даних об'єкта.
jleach

1
"це не означає, що інший об'єкт не може нести відповідальність за збереження держави". - Я цього не казав. Важливе твердження полягає в тому, що об’єкт повинен бути активним . Це означає, що об'єкт (і ніхто інший) не може делегувати цю операцію іншому об'єкту, але не навпаки: жоден об’єкт не повинен просто збирати інформацію з пасивного об'єкта для обробки власної егоїстичної операції (як це робило РЕПО з дітьми) . Я намагався реалізувати такий підхід у фрагментах вище.
ttulka

1
@jleach Ви маєте рацію, наша невибагливість до OOP різна, для мене геттери + сетери зовсім не є ООП, інакше моє запитання не мало сенсу. В будь-якому випадку, дякую! :-)
ttulka

1
Ось стаття про мій погляд : martinfowler.com/bliki/AnemicDomainModel.html Я не знаю анемічної моделі в усіх випадках, наприклад, це хороша стратегія функціонального програмування. Тільки не ООП.
ttulka

Відповіді:


7

Ви написали

З іншого боку, OOP каже, що об'єкт Product повинен знати, як зберегти себе

і в коментарі.

... повинен відповідати за всі операції, зроблені з нею

Це звичайне непорозуміння. Productє доменним об'єктом, тому він повинен відповідати за доменні операції, які включають один об'єкт продукту, ні менше, ні більше - так, безумовно, не для всіх операцій. Зазвичай наполегливість не сприймається як операція над доменом. Зовсім навпаки, у корпоративних додатках не рідкість намагаються домогтися стійкого незнання в доменній моделі (принаймні певною мірою), а збереження стійкості механіки в окремому класі сховищ є популярним рішенням для цього. "DDD" - це техніка, яка спрямована на подібні програми.

Отже, що може бути розумною операцією над доменом для Product? Це фактично залежить від доменного контексту прикладної системи. Якщо система невелика і підтримує виключно операції CRUD, то, Productможливо, може залишатися досить "анемічним", як у вашому прикладі. Для такого роду додатків це може бути дискусійним, якщо розміщення операцій з базою даних в окремий клас репо або взагалі використання DDD коштує зайвих клопотів.

Однак, як тільки ваша програма підтримує реальні бізнес-операції, такі як купівля чи продаж товарів, зберігання їх на складі та управління ними, або нарахування податків за них, ви часто починаєте виявляти операції, які можна розумно розмістити в Productкласі. Наприклад, може бути операція, CalcTotalPrice(int noOfItems)яка розраховує ціну на `n предметів певного товару при врахуванні об'ємних знижок.

Отже, коротко кажучи, коли ви розробляєте заняття, вам потрібно подумати про свій контекст, у якому з п'яти світів Джоела Спольського ви знаходитесь, і якщо система містить достатню логіку домену, тож DDD буде корисним. Якщо відповідь "так", навряд чи ви закінчитеся з анемічною моделлю лише тому, що ви тримаєте механіку стійкості поза класами домену.


Ваша думка звучить для мене дуже розумно. Отже, продукт перетворюється на анемічну структуру даних при перетині кордону контексту анемічних структур даних (бази даних), а сховище є шлюзом. Але це все ще означає, що я маю забезпечити доступ до внутрішньої структури об'єкта через getter та setters, які потім стають частиною його API і можуть бути легко використані іншим кодом, що не має нічого спільного з наполегливістю. Чи є хороша практика, як цього уникнути? Дякую!
ttulka

"Але це все ще означає, що я повинен забезпечити доступ до внутрішньої структури об'єкта через геттер і сетери" - малоймовірно. Внутрішній стан об'єкта домену, який не враховує стійкість, зазвичай надається виключно набором атрибутів, пов'язаних з доменом. Для цих атрибутів повинні існувати геттери та задачі (або ініціалізація конструктора), інакше жодна "цікава" операція з доменом не буде можливою. У кількох рамках також доступні функції стійкості, які дозволяють зберігати приватні атрибути шляхом відображення, тому інкапсуляція порушена лише для цього механізму, а не для "іншого коду".
Doc Brown

1
Я погоджуюся, що наполегливість зазвичай не є частиною доменних операцій, однак вона повинна бути частиною "реальних" операцій над доменом всередині об'єкта, який цього потребує. Наприклад, Account.transfer(amount)слід зберігати передачу. Як це відбувається, це відповідальність об'єкта, а не зовнішньої сутності. Зображення об'єкта з іншого боку, як правило, доменна операція! Вимоги зазвичай дуже докладно описують, як повинні виглядати речі. Це частина мови серед учасників проекту, бізнесу чи іншого.
Robert Bräutigam

@ RobertBräutigam: класичний Account.transferдля, як правило, включає два об’єкти рахунку та об'єкт роботи. Операція, що зберігається в транзакції, може потім бути частиною останньої (перемикання з викликами до відповідних репост), тому вона не виходить із методу "передачі". Таким чином, Accountможна залишатись наполегливим ігнором. Я не кажу, що це обов'язково краще, ніж ваше передбачуване рішення, але ваш також є лише одним із кількох можливих підходів.
Doc Brown

1
@ RobertBräutigam Досить впевнений, що ти занадто багато думаєш про співвідношення об’єкта та таблиці. Подумайте про об'єкт як про стан для себе, все в пам'яті. Здійснивши перекази в об’єктах вашого облікового запису, вам залишиться новий об'єкт. Це те, що ви хотіли б зберегти, і на щастя, об’єкти облікового запису дозволяють вам повідомити про їх стан. Це не означає, що їх стан повинен дорівнювати таблицям у базі даних, тобто перерахована сума може бути грошовим об’єктом, що містить неоплачену суму та валюту.
Стів Чамайлард

5

Практикуйте теорію козирів.

Досвід вчить, що Product.Save () призводить до безлічі проблем. Щоб подолати ці проблеми, ми винайшли шаблон сховища.

Впевнені, що це порушує правило OOP про приховування даних про продукт. Але це працює добре.

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


3

DDD відповідає OOP

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

З іншого боку, OOP каже, що об'єкт Product повинен знати, як зберегти себе.

Не так. Об'єкти інкапсулюють власні структури даних. Представлення Вашого продукту в пам'яті несе відповідальність за демонстрацію поведінки товару (якою б вони не були); але постійне сховище там (за сховищем) і має свою роботу.

Потрібно існувати певний спосіб копіювати дані між представленням в пам'яті бази даних та її збереженим пам'яттю. На кордоні речі, як правило, отримують досить примітивний характер.

По суті, лише бази даних не є особливо корисними, а їхні еквіваленти в пам'яті не корисніші, ніж "зберігається" сортування. Немає сенсу вкладати інформацію в Productоб’єкт, якщо ви ніколи не збираєтеся виносити цю інформацію. Ви не обов'язково використовуватимете "гетерів" - ви не намагаєтеся поділитися структурою даних про продукт, і ви, звичайно, не повинні ділитися змінним доступом до внутрішнього представництва Продукту.

Можливо, ми можемо делегувати збереження іншому об'єкту:

Це, безумовно, працює - ваше постійне сховище фактично стає зворотним дзвоном. Можливо, я б зробив інтерфейс простішим:

interface ProductStorage {
    onProduct(String name, double price);
}

Існує зв'язок між представленням пам’яті та механізмом зберігання, оскільки інформація повинна надходити звідти (і знову). Зміна інформації, якою слід поділитися, вплине на обидва кінці розмови. Тож ми можемо також зробити це явним, де можемо.

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

Зауважте, що передача інформації до зворотного дзвінка має всі ті ж обмеження, що і повернення інформації із запиту - ви не повинні передавати змінні копії ваших структур даних.

Такий підхід трохи суперечить тому, що Еванс описав у Блакитній книзі, де повернення даних за допомогою запиту було нормальним способом розбиратися в речах, а об’єкти домену були спеціально розроблені таким чином, щоб уникнути змішування у "постійних проблемах".

Я розумію DDD як техніку OOP і тому хочу повністю зрозуміти це, здавалося б, протиріччя.

Одне, що потрібно пам’ятати - Блакитна книга була написана п'ятнадцять років тому, коли Java 1.4 бродила по землі. Зокрема, книга передує дженерикам Java - у нас є набагато більше технічних засобів, доступних нам уже тоді, коли Еванс розробляв свої ідеї.


2
Також варто зазначити: "зберегти себе" завжди вимагатиме взаємодії з іншими об'єктами (або об'єктом файлової системи, або базою даних, або віддаленою веб-службою; деякі з них можуть додатково вимагати встановлення сеансу для контролю доступу). Тож такий об’єкт не був би самостійним і незалежним. Таким чином, OOP не може цього вимагати, оскільки його метою є інкапсуляція об'єкта та зменшення зв'язку.
Крістоф

Дякую за чудову відповідь. Спочатку я розробив Storageінтерфейс так само, як і ви, потім я розглядав високу муфту і змінив його. Але ви маєте рацію, так чи інакше є неминуча зв'язок, то чому б не зробити це більш явним.
ttulka

1
"Такий підхід трохи суперечить тому, що Еванс описав у" Синій книзі " - тому все-таки є певна напруга :-) У цьому насправді було питання мого питання, я розумію DDD як техніку OOP, і тому я хочу повністю зрозуміти це, здавалося б, протиріччя.
ttulka

1
На мій досвід, кожна з цих речей (OOP загалом, DDD, TDD, pick-your-абревіатура) всі звучать добре і добре самі по собі, але щоразу, коли мова заходить про реалізацію в реальному світі, завжди є якийсь компроміс або менш ідеалізм, який повинен бути для того, щоб він працював.
jleach

Я не погоджуюся з поняттям, що наполегливість (і уявлення) якимось чином "особливі". Вони не є. Вони повинні бути частиною моделювання для розширення вимог попиту. Всередині програми не повинно бути штучної межі (на основі даних) , якщо немає фактичних вимог, що суперечать.
Robert Bräutigam

1

Дуже хороші спостереження, я повністю погоджуюся з вами щодо них. Ось моя розмова (виправлення: лише слайди) саме на цю тему: Об'єктно-орієнтований дизайн, керований доменом .

Коротка відповідь: ні. У вашій програмі не повинно бути об'єкта, який суто технічний і не має доменної релевантності. Це як реалізація системи реєстрації журналів у програмі обліку.

StorageПриклад вашого інтерфейсу є відмінним, якщо вважати, що Storageтоді вважається деяким зовнішнім фреймворком, навіть якщо ви його пишете.

Крім того, save()в об'єкт має бути дозволено лише, якщо це частина домену ("мова"). Наприклад, від мене не потрібно вимагати явного "збереження" Accountпісля дзвінка transfer(amount). Я справедливо сподіваюся, що бізнес-функція transfer()буде зберігати мою передачу.

Загалом, я думаю, ідеї DDD є хорошими. Використання всюдисущої мови, використання домену за допомогою розмови, обмеженого контексту тощо. Однак будівельні блоки потребують серйозного капітального ремонту, щоб бути сумісним із орієнтацією на об'єкт. Деталі див. На пов'язаній колоді.


Ваша розмова десь дивитися? (Я бачу лише слайди під посиланням). Дякую!
ttulka

У мене є лише німецький запис бесіди, тут: javadevguy.wordpress.com/2018/11/26/…
Роберт

Чудова розмова! (На щастя, я розмовляю німецькою). Я думаю, що весь ваш блог варто прочитати ... Дякую за вашу роботу!
ttulka

Дуже проникливий слайдер Роберт. Я вважаю це дуже наочним, але я відчув, що врешті-решт, багато рішень, спрямованих на те, щоб не порушити інкапсуляцію та LoD, ґрунтуються на наданні багато відповідальності об’єкту домену: друк, серіалізація, форматування інтерфейсу тощо. t, що збільшує зв'язок між доменом і технічним (деталі реалізації)? Наприклад, AccountNumber у поєднанні з Apache Wicket API. Або Обліковий запис з будь-яким об’єктом Json? Ви вважаєте, що це муфта, яку варто мати?
Лаїв

@Laiv Граматика вашого запитання говорить про те, що з використанням технології для реалізації бізнес-функцій щось не так? Скажемо так: проблема не в з’єднанні між доменом і технологією, а в поєднанні між різними рівнями абстракції. Наприклад, AccountNumber слід знати, що вона може бути представлена ​​як a TextField. Якщо інші (наприклад , в «View») буде знати це, що це з'єднання , які не повинні існувати, тому що компонент повинен був би знати , що AccountNumberскладається з них , тобто внутрішні органи .
Роберт

1

Можливо, ми можемо делегувати збереження іншому об’єкту

Уникайте поширення знань з полів без потреби. Чим більше речей, які знають про окреме поле, тим важче додати або видалити поле:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Тут продукт не має жодної ідеї, якщо ви зберігаєте файл журналу, базу даних або те і інше. Тут метод збереження поняття не має, якщо у вас є 4 або 40 полів. Це вільно поєднано. Це гарна річ.

Звичайно, це лише один приклад того, як можна досягти цієї мети. Якщо вам не подобається складання та розбір рядка для використання як DTO, ви також можете використовувати колекцію. LinkedHashMapє моїм старим фаворитом, оскільки він зберігає порядок, і toString () добре виглядає у файлі журналу.

Однак якщо ви це зробите, будь ласка, не поширюйте знання з полів навколо. Це форма сполучення, яку люди часто ігнорують, поки не пізно. Я хочу, щоб якомога менше речей статистично знало, скільки полів має мій об’єкт. Таким чином, додавання поля не включає багато редагувань у багатьох місцях.


Це насправді код, який я опублікував у своєму запитанні, правда? Я використав a Map, ви пропонуєте a Stringчи a List. Але, як згадував @VoiceOfUnreason у своїй відповіді, зв'язок все ще є, просто не явний. Поки що не потрібно знати структуру даних продукту, щоб зберегти їх як у базі даних, так і у файлі журналу, принаймні, коли читається назад як об’єкт.
ttulka

Я змінив метод збереження, але в іншому випадку так - це майже те саме. Різниця полягає в тому, що з’єднання більше не є статичним, що дозволяє додавати нові поля без примушування зміни коду до системи зберігання даних. Це робить систему зберігання багаторазовою для багатьох продуктів. Це просто змушує вас робити такі речі, які неприємні, як перетворити дубль у струну і назад у дубль. Але це також можна вирішити, якщо це справді проблема.
candied_orange


Але, як я вже говорив, я бачу, що з'єднання все ще є (шляхом розбору), тільки як не статичне (явне) призводить до того, що компілятор не може бути перевірений недоліком і тим більше схильним до помилок. Це Storageчастина домену (як і інтерфейс сховища) і робить такий API постійності. Після зміни краще повідомити клієнтів під час компіляції, оскільки вони так чи інакше повинні реагувати, щоб не зірватися під час виконання.
ttulka

Це неправильне уявлення. Компілятор не може перевірити файл журналу або БД. Перевіряється лише те, чи відповідає один файл коду іншому кодовому файлу, який також не гарантовано відповідає файлу журналу або БД.
candied_orange

0

Існує альтернатива вже згаданим зразкам. Шаблон Memento відмінно підходить для інкапсуляції внутрішнього стану об’єкта домену. Об'єкт memento являє собою знімок публічного стану об'єкта домену. Об'єкт домену знає, як створити цей державний стан із його внутрішнього стану та навпаки. Потім сховище працює лише з представництвом держави. При цьому внутрішня реалізація відривається від будь-якої стійкості, і вона просто повинна підтримувати державний договір. Також ваш доменний об’єкт не повинен піддавати жодним дітям, які справді зробили б це трохи анемічним.

Детальніше на цю тему я рекомендую чудову книгу: "Шаблони, принципи та практики дизайну, керованого доменом" Скотта Міллета та Ніка Туна

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.