Забудьте О Dao, Используйте Репозиторий

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

Я много раз поверхностно читал описания и различные реализации DAO и Repository, даже использовал их в своих проектах, видимо, не до конца понимая концептуальные различия.

Я решил разобраться, покопался в Гугле и нашел статью, которая мне все объяснила.

Я подумал, что было бы неплохо перевести это на русский язык.

Оригинал для англоязычных читателей Здесь .

Остальным желающим добро пожаловать под кат. Объект доступа к данным (DAO) — это широко используемый шаблон для хранения объектов бизнес-домена в базе данных.

В самом широком смысле DAO — это класс, содержащий методы CRUD для конкретного объекта.

Предположим, что у нас есть сущность Account, представленная следующим классом:

  
  
  
  
  
  
  
   

package com.thinkinginobjects.domainobject; public class Account { private String userName; private String firstName; private String lastName; private String email; private int age; public boolean hasUseName(String desiredUserName) { return this.userName.equals(desiredUserName); } public boolean ageBetween(int minAge, int maxAge) { return age >= minAge && age <= maxAge; } }

Давайте создадим интерфейс DAO для этой сущности:

package com.thinkinginobjects.dao; import com.thinkinginobjects.domainobject.Account; public interface AccountDAO { Account get(String userName); void create(Account account); void update(Account account); void delete(String userName); }

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

Модель имеет следующие преимущества:

  • Отделяет бизнес-логику, использующую этот шаблон, от механизмов хранения данных и используемых ими API;
  • Сигнатуры методов интерфейса не зависят от содержимого класса Account. Если вы добавите поле TelephoneNumber в класс Account, вам не потребуется вносить изменения в AccountDAO или классы, которые его используют.
Однако данная закономерность оставляет много вопросов без ответа.

Что делать, если нам нужно получить список учетных записей с определенной фамилией? Можно ли добавить метод, который обновляет только поле электронной почты для учетной записи? Что, если мы хотим использовать в качестве идентификатора длинный идентификатор вместо имени пользователя? В чем конкретно заключается ответственность DAO? Проблема в том, что обязанности DAO четко не определены.

Большинство людей думают о DAO как о шлюзе к базе данных и добавляют к нему методы, как только находят новый способ взаимодействия с базой данных.

Поэтому нередко можно увидеть раздутый DAO, как в следующем примере:

package com.thinkinginobjects.dao; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface BloatAccountDAO { Account get(String userName); void create(Account account); void update(Account account); void delete(String userName); List getAccountByLastName(String lastName); List getAccountByAgeRange(int minAge, int maxAge); void updateEmailAddress(String userName, String newEmailAddress); void updateFullName(String userName, String firstName, String lastName); }

В BloatAccountDAO мы добавили методы поиска аккаунтов по различным параметрам.

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

  • Сложнее имитировать интерфейс DAO во время модульного тестирования.

    Было бы необходимо реализовать больше методов DAO даже в тестовых случаях, где они не используются;

  • Интерфейс DAO все больше привязывается к полям класса Account. При изменении типов полей класса Account необходимо изменить интерфейс и его реализации.

Чтобы сделать ситуацию еще более понятной, мы добавили в DAO дополнительные методы обновления.

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

Они выглядят как невинная оптимизация и идеально вписываются в концепцию AccountDAO, если рассматривать интерфейс как шлюз к хранилищу данных.

Шаблон DAO и имя класса AccountDAO слишком расплывчаты, чтобы отговорить нас от этого шага.

Конечным результатом является раздутый интерфейс DAO, и я уверен, что мои коллеги добавят еще больше методов в будущем.

Через год у нас будет класс с более чем 20 методами, и мы будем ругать себя за выбор этого шаблона.



Шаблон репозитория

Лучшим решением было бы использовать шаблон Repository. Рик Вэнс дал точное описание в своей книге.

книга : «Репозиторий представляет все объекты определенного типа как концептуальный набор.

Его поведение похоже на поведение коллекции, за исключением более продвинутых возможностей запросов».

Давайте вернемся назад и спроектируем AccountRepository в соответствии с этим определением:

package com.thinkinginobjects.repository; import java.util.List; import com.thinkinginobjects.domainobject.Account; public interface AccountRepository { void addAccount(Account account); void removeAccount(Account account); void updateAccount(Account account); // Think it as replace for set List query(AccountSpecification specification); }

Методы добавления и обновления выглядят идентично методам AccountDAO. Метод удаления отличается от метода удаления, определенного в DAO, тем, что он принимает в качестве параметра учетную запись вместо имени пользователя.

Представление о репозитории как о коллекции меняет его восприятие.

Вы избегаете раскрытия типа идентификатора учетной записи в репозитории.

Это облегчит вам жизнь, если вы захотите использовать long для идентификации аккаунтов.

Если вы думаете о контрактах методов добавления/удаления/обновления, просто подумайте об абстракции коллекции.

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

Однако метод запроса особенный.

Я бы не ожидал увидеть такой метод в классе коллекции.

Что он делает? Репозиторий отличается от коллекции возможностями запросов.

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

Репозиторий работает с большим набором объектов, чаще всего находящихся за пределами оперативной памяти на момент выполнения запроса.

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

Вместо этого мы передаем критерии в репозиторий, чтобы он мог найти один или несколько объектов.

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

Одной из часто используемых реализаций критерия является шаблон «Спецификация» (далее — спецификация).

Спецификация — это простой предикат, который принимает объект бизнес-домена и возвращает логическое значение:

package com.thinkinginobjects.repository; import com.thinkinginobjects.domainobject.Account; public interface AccountSpecification { boolean specified(Account account); }

Таким образом, мы можем создать реализации для каждого способа запроса AccountRepository. Обычная спецификация хорошо работает для репозитория в памяти, но не может использоваться с базой данных из-за неэффективности.

Для AccountRepository, работающего с базой данных SQL, спецификация должна реализовывать интерфейс SqlSpecification:

package com.thinkinginobjects.repository; public interface SqlSpecification { String toSqlClauses(); }

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

Если бы мы использовали Hibernate в качестве серверной части репозитория, мы бы использовали интерфейс HibernateSpecification, который генерирует Criteria. Репозитории SQL и Hibernate не используют указанный метод. Однако мы обнаружили, что реализация этого метода во всех классах является преимуществом, поскольку таким образом мы можем использовать заглушку для AccountRepository в целях тестирования, а также в реализации кэширования репозитория перед отправкой запроса непосредственно на серверную часть.

Мы можем даже пойти еще дальше и использовать композицию Spicification с ConjunctionSpecification и DisjunctionSpecification для выполнения более сложных запросов.

Нам кажется, что этот вопрос выходит за рамки статьи.

Заинтересованный читатель может найти подробности и примеры в книга Эванса.



package com.thinkinginobjects.specification; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Restrictions; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.HibernateSpecification; public class AccountSpecificationByUserName implements AccountSpecification, HibernateSpecification { private String desiredUserName; public AccountSpecificationByUserName(String desiredUserName) { super(); this.desiredUserName = desiredUserName; } @Override public boolean specified(Account account) { return account.hasUseName(desiredUserName); } @Override public Criterion toCriteria() { return Restrictions.eq("userName", desiredUserName); } }



package com.thinkinginobjects.specification; import com.thinkinginobjects.domainobject.Account; import com.thinkinginobjects.repository.AccountSpecification; import com.thinkinginobjects.repository.SqlSpecification; public class AccountSpecificationByAgeRange implements AccountSpecification, SqlSpecification{ private int minAge; private int maxAge; public AccountSpecificationByAgeRange(int minAge, int maxAge) { super(); this.minAge = minAge; this.maxAge = maxAge; } @Override public boolean specified(Account account) { return account.ageBetween(minAge, maxAge); } @Override public String toSqlClauses() { return String.format("age between %s and %s", minAge, maxAge); } }



Заключение

Шаблон DAO дает расплывчатое описание контракта.

Используя его, вы в конечном итоге получите потенциально неправильно используемые и раздутые реализации классов.

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

Теги: #шаблоны проектирования #dao #репозиторий #java #перевод #программирование #java #Промышленное программирование

Вместе с данным постом часто просматривают: