Забудьте о DAO, используйте Repository

от автора

Недавно задумался о том, чем отличаются паттерны, позволяющие абстрагироваться от работы с хранилищем данных. Много раз поверхностно читал описания и различные реализации DAO и Repository, даже применял их в своих проектах, видимо, до конца не понимая концептуальных отличий. Решил разобраться, закопался в Google и нашел статью, которая для меня разъяснила все. Подумал, что неплохо было бы перевести ее на русский. Оригинал для англочитающих здесь. Остальным интересующимся добро пожаловать под кат.

Data Access Object (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 или использующих его классах.

Тем не менее, паттерн оставляет множество вопросов без ответа. Что если нам необходимо получить список аккаунтов с определенным lastName? Можно ли добавить метод, который обновляет только поле email дла аккаунта? Что делать, если мы захотим использовать long id вместо userName в качестве идентификатора? Что именно является обязанностью 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

Лучшим решением будет использование паттерна Repository. Эрик Эванс дал точное описание в своей книге: «Respotory представляет собой все объекты определенного типа в виде концептуального множества. Его поведение похоже на поведение коллекции, за исключением более развитых возможностей для построения запросов».
Вернемся назад и спроектируем 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);   } 

Методы add и update выглядят идентично методам AccountDAO. Метод remove отличается от метода удаления, определенного в DAO тем, что принимает Account в качестве параметра вместо userName (идентификатора аккаунта). Представление репозитория как коллекции меняет его восприятие. Вы избегаете раскрытия типа идентификатора аккаунта репозиторию. Это сделает вашу жизнь легче в том случае, если вы захотите использовать long для идентрификации аккаунтов.
Если вы задумываетесь о контрактах методов add/remove/update, просто подумайте об абстрации коллекции. Если вы задумаетесь о добавлении еще одного метода update для репозитория, подумайте, имеет ли смысл добавлять еще один метод update для коллекции.
Однако, метод query является особенным. Я бы не ожидал увидеть такой метод в классе коллекции. Что он делает?
Репозиторий отличается от коллекции, если рассматривать возможности для построения запросов. Имея коллекцию объектов в памяти, довольно просто перебрать все ее элементы и найти интересующий нас экземпляр. Репозиторий работает с большим набором объектов, чаще всего, находящихся вне оперативной памяти в момент выполнения запроса. Нецелесообразно загружать все аккаунты в память, если нам необходим один конкретный пользователь. Вместо этого, мы передаем репозиторию критерий, с помощью которого он сможет найти один или несколько объектов. Репозиторий может сгенерировать SQL запрос в том случае, если он использует базу данных в качестве бекэнда, или он может найти необходимый объект перебором, если используется коллекция в памяти.
Одна из часто используемых реализаций критерия — паттерн Specification (далее спецификация). Спецификация — это простой предикат, который принимает объект бизнес-области и возвращает boolean:

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, мы бы использовали интерфейс HibernateSpicification, который генерирует Criteria.
SQL- и Hibernate-репозитории не используется метод specified. Тем не менее, мы находим наличие реализации данного метода во всех классах преимуществом, т.к. таким образом мы сможем использовать заглушку для 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 предоставляет размытое описание контракта. Используя его, выполучаете потенциально неверно используемые и раздутые реализации классов. Паттерн Репозиторий использует метафору коллекции, которая дает нам жесткий контракт и делает понимание вашего кода проще.

ссылка на оригинал статьи http://habrahabr.ru/post/263033/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *