Недавно я задумался о разнице паттернов, позволяющих абстрагироваться от работы с хранилищем данных.
Я много раз поверхностно читал описания и различные реализации DAO и Repository, даже использовал их в своих проектах, видимо, не до конца понимая концептуальные различия.
Я решил разобраться, покопался в Гугле и нашел статью, которая мне все объяснила.
Я подумал, что было бы неплохо перевести это на русский язык.
Оригинал для англоязычных читателей Здесь .
Остальным желающим добро пожаловать под кат. Объект доступа к данным (DAO) — это широко используемый шаблон для хранения объектов бизнес-домена в базе данных.
В самом широком смысле DAO — это класс, содержащий методы CRUD для конкретного объекта.
Предположим, что у нас есть сущность Account, представленная следующим классом:
Давайте создадим интерфейс DAO для этой сущности: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; } }
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 необходимо изменить интерфейс и его реализации.
Это прямой результат двух новых вариантов использования, которые обновляют разные наборы полей учетной записи.
Они выглядят как невинная оптимизация и идеально вписываются в концепцию 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 #Промышленное программирование
-
Существует Ли Зло?
19 Oct, 24 -
Zone.su Подешевел В 5 Раз!
19 Oct, 24 -
Совместный Просмотр Google I/O В Офисе Авито
19 Oct, 24 -
Канобувости Выпуск 003
19 Oct, 24 -
Такс Дроид
19 Oct, 24