Типичная реализация хранилища DDD выглядит не очень хорошо, например, 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"));
}
}
Каков наилучший подход для достижения этого? Возможно ли реализовать объектно-ориентированный репозиторий?
Ответы:
Вы написали
и в комментарии.
Это распространенное недоразумение.
Product
является объектом домена, поэтому он должен отвечать за операции домена, которые включают в себя один объект продукта, ни меньше, ни больше - так что определенно не для всех операций. Обычно постоянство не рассматривается как операция домена. Напротив, в корпоративных приложениях нередки попытки добиться постоянного невежества в модели предметной области (по крайней мере, в определенной степени), и хранение механики постоянства в отдельном классе репозитория является популярным решением для этого. «DDD» - это метод, предназначенный для такого рода приложений.Так что же может быть разумной работой домена для
Product
? На самом деле это зависит от предметного контекста прикладной системы. Если система небольшая и поддерживает только операции CRUD, то, действительно, aProduct
может оставаться довольно «анемичным», как в вашем примере. Для такого рода приложений это может быть спорным, если размещение операций с базой данных в отдельном классе репо или использование DDD вообще стоит хлопот.Однако, как только ваше приложение поддерживает реальные бизнес-операции, такие как покупка или продажа продуктов, хранение их на складе и управление ими или расчет налогов для них, довольно часто вы начинаете обнаруживать операции, которые можно разумно поместить в
Product
класс. Например, может существовать операция,CalcTotalPrice(int noOfItems)
которая вычисляет цену для `n товаров определенного продукта с учетом скидок за объем.Короче говоря, когда вы разрабатываете классы, вам нужно подумать о своем контексте, в каком из пяти миров Джоэла Спольски вы находитесь, и если система содержит достаточно логики предметной области, так что DDD будет полезным. Если ответ «да», то маловероятно, что вы в конечном итоге получите анемичную модель только потому, что не используете механику персистентности в классах предметной области.
источник
Account.transfer(amount)
должен сохраниться перевод. Как это происходит, это ответственность объекта, а не какой-то внешней сущности. Отображение объекта с другой стороны , это , как правило , операция домен! Требования обычно подробно описывают, как все должно выглядеть. Это часть языка среди участников проекта, бизнеса или иным образом.Account.transfer
для обычно включает в себя два объекта счета и объект единицы работы. Транзакционная персистентная операция может затем быть частью последней (в дополнение к вызовам связанных репозиториев), поэтому она остается вне метода «передачи». Таким образом, выAccount
можете оставаться настойчивым невежественным. Я не говорю, что это обязательно лучше, чем ваше предполагаемое решение, но ваш также является лишь одним из нескольких возможных подходов.Практика превосходит теорию.
Опыт учит нас, что Product.Save () приводит к множеству проблем. Чтобы обойти эти проблемы, мы изобрели шаблон хранилища.
Конечно, это нарушает правило ООП о сокрытии данных о продукте. Но это работает хорошо.
Гораздо сложнее создать набор согласованных правил, которые охватывают все, чем создавать общие хорошие правила с исключениями.
источник
Помогает помнить, что между этими двумя идеями не должно быть напряженности - объекты ценностей, агрегаты, репозитории - это массив шаблонов, который некоторые считают правильным выполнением ООП.
Не так. Объекты инкапсулируют свои собственные структуры данных. Ваше представление продукта в памяти отвечает за демонстрацию поведения продукта (каким бы оно ни было); но постоянное хранилище находится там (за хранилищем) и имеет свою собственную работу.
Должен быть какой-то способ скопировать данные между представлением базы данных в памяти и ее постоянным памятным знаком. На границе вещи имеют тенденцию становиться довольно примитивными.
По сути, базы данных только для записи не особенно полезны, и их эквиваленты в памяти не более полезны, чем «постоянная» сортировка. Нет смысла помещать информацию в
Product
объект, если вы никогда не собираетесь ее извлекать. Вы не обязательно будете использовать «получатели» - вы не пытаетесь поделиться структурой данных продукта, и вам определенно не следует делиться изменяемым доступом к внутреннему представлению Продукта.Это, безусловно, работает - ваше постоянное хранилище становится обратным вызовом. Я бы, наверное, сделал интерфейс проще:
Там будет идти , чтобы быть связь между представлением в памяти и механизма хранения, потому что информация должна получить отсюда туда (и обратно). Изменение информации, которой нужно поделиться, повлияет на оба конца разговора. Таким образом, мы могли бы также сделать это явным, где мы можем.
Такой подход - передача данных через обратные вызовы, сыграл важную роль в разработке макетов в TDD .
Обратите внимание, что передача информации для обратного вызова имеет все те же ограничения, что и возврат информации из запроса - вы не должны передавать изменяемые копии своих структур данных.
Этот подход немного противоречит тому, что Эванс описал в «Синей книге», где возвращение данных с помощью запроса было нормальным способом решения проблем, а доменные объекты были специально разработаны, чтобы избежать смешения с «проблемами постоянства».
Следует помнить одну вещь - «Синяя книга» была написана пятнадцать лет назад, когда Java 1.4 бродила по земле. В частности, книга предшествует дженерикам Java - у нас сейчас гораздо больше техник, чем когда Эванс разрабатывал свои идеи.
источник
Storage
интерфейс так же, как вы, затем я подумал о высокой связи и изменил его. Но вы правы, в любом случае существует неизбежная связь, так почему бы не сделать ее более явной.Очень хорошие наблюдения, я с ними полностью согласен. Вот мой доклад (исправление: только слайды) именно на эту тему: Объектно-ориентированный доменно-ориентированный дизайн .
Краткий ответ: нет. В вашем приложении не должно быть объекта, который носит чисто технический характер и не имеет отношения к домену. Это похоже на реализацию каркаса ведения журнала в бухгалтерском приложении.
Ваш
Storage
пример интерфейса - отличный пример, если предположить, чтоStorage
он рассматривается как некоторая внешняя структура, даже если вы ее пишете.Кроме того,
save()
объект должен быть разрешен только в том случае, если он является частью домена («язык»). Например, мне не нужно явно «сохранять»Account
после вызоваtransfer(amount)
. Я должен справедливо ожидать, что бизнес-функцияtransfer()
сохранит мой перевод.В общем, я думаю, что идеи DDD хорошие. Использование вездесущего языка, использование предметной области в разговоре, ограниченном контексте и т. Д. Однако строительные блоки нуждаются в серьезном пересмотре, если они совместимы с объектной ориентацией. Смотрите связанную колоду для деталей.
источник
AccountNumber
следует знать, что он может быть представлен какTextField
. Если бы другие (например, «представление») знали бы об этом, это не должно существовать связывание, потому что этот компонент должен был бы знать, из чегоAccountNumber
состоит, то есть внутренности.Избегайте ненужного распространения знаний о полях. Чем больше вещей известно об отдельном поле, тем сложнее становится добавить или удалить поле:
Здесь продукт не имеет понятия, сохраняете ли вы файл журнала, базу данных или и то, и другое. Здесь метод сохранения не имеет понятия, есть ли у вас 4 или 40 полей. Это слабо связано. Это хорошая вещь.
Конечно, это только один пример того, как вы можете достичь этой цели. Если вам не нравится создавать и разбирать строку для использования в качестве DTO, вы также можете использовать коллекцию.
LinkedHashMap
это мой старый фаворит, так как он сохраняет порядок и toString () выглядит хорошо в файле журнала.Как бы вы это ни делали, пожалуйста, не распространяйте знания об областях вокруг. Это форма связи, которую люди часто игнорируют, пока не поздно. Я хочу, чтобы как можно меньше вещей статически знали, сколько полей имеет мой объект. Таким образом, добавление поля не требует много изменений во многих местах.
источник
Map
, вы предлагаетеString
илиList
. Но, как @VoiceOfUnreason упомянул в своем ответе, связь все еще там, просто не явно. По-прежнему нет необходимости знать структуру данных продукта, чтобы сохранять ее как в базе данных, так и в файле журнала, по крайней мере, при обратном чтении в виде объекта.Storage
Является частью домена (а также интерфейс хранилища) и делает такую настойчивость API. При изменении лучше информировать клиентов во время компиляции, потому что они все равно должны реагировать, чтобы не сломаться во время выполнения.Есть альтернатива уже упомянутым моделям. Шаблон Memento отлично подходит для инкапсуляции внутреннего состояния объекта домена. Объект memento представляет собой снимок открытого состояния объекта домена. Доменный объект знает, как создать это общедоступное состояние из его внутреннего состояния и наоборот. Тогда хранилище работает только с публичным представительством государства. При этом внутренняя реализация отделена от любых постоянных особенностей, и она просто должна поддерживать публичный контракт. Также ваш доменный объект не должен выставлять какие-либо методы получения, которые действительно сделали бы его немного анемичным.
Для получения дополнительной информации по этой теме я рекомендую большую книгу Скотта Миллета и Ника Тьюна "Образцы, принципы и практики доменного управления".
источник