Использование паттерна data access object в клиентском приложении

Рассказывает Стенли Винтергрин, наш подписчик 

Всем привет! В этой статье я поделюсь своим опытом по работе с базой данных в приложении для Android. Это будет один из вариантов реализации паттерна data access object на языке Java.

Постановка задачи

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

Проектирование показало, что у приложения будет 3 модуля:

  1. Авторизация.
  2. Проведение осмотра.
  3. Синхронизация.

Сущности

Есть сущности, которыми мы будем оперировать в коде, опираясь на техзадание этого проекта и API сервера: пользователи, шаблоны и осмотры. Пользователи получают от сервера шаблон сценария и проводят осмотр. Составленный на основе полученной информации готовый осмотр отправляется на сервер.

Доступ к данным

Выделим для этих сущностей 3 вида доступа. В данном случае это будут следующие интерфейсы:

  • UsersAccess — для пользователей;
  • ScenariosAccess — для шаблонов сценариев;
  • InspectionsAccess — для осмотров.

Далее создадим data access object, который предоставляет доступы. Этот интерфейс будет выглядеть так:

interface DAO
{
   UsersAccess usersAccess();
   ScenariosAccess scenariosAccess();
   InspectionsAccess inspectionsAccess();
}

Теперь у нас есть все необходимые виды доступа, но пока они не запрашивают и не изменяют данные.

Модели данных

Создадим модели, которые будут описывать необходимые операции с сущностями. Начнём с пользователей. Что в данном проекте нужно с ними делать? В техзадании сказано, что одним устройством могут пользоваться разные люди, поэтому нужно хранить список пользователей. Также известно, что пользователи ничего не будут знать друг о друге. В таком случае можно создать модель:

interface UsersModels
{
   interface Users
   {
       User get(int id);
       void add(User user);
   }
}

Очевидно, что у нас также должны быть модели ScenariosModels и InspectionsModels. Они будут оперировать несколькими сущностями. Шаблон будет состоять из типов этапов осмотра, а типы этапов из типов элементов. Нужно организовать возможность добавлять данные в базу, получать шаблон по уникальному ключу и строить список типов этапов по ссылке на шаблон и список типов элементов по ссылке на тип этапа:

interface ScenariosModels
{
   interface ScenarioTypes
   {
       ScenarioType get(int id);
       void add(ScenarioType type);
   }
   interface StepTypes
   {
       StepType get(int id);
       List<StepType> getAll(ScenarioType scenarioType);
       void add(StepType type);
   }
   interface ItemTypes
   {
       ItemType get(int id);
       List<ItemType> getAll(StepType stepType);
       void add(ItemType type);
   }
}

Готовый осмотр будет состоять из элементов, а элементы из данных. Нужно реализовать возможность получения осмотра по ключу, построение списка черновиков осмотров, списка готовых осмотров, а также возможность удалить осмотр. Таким образом, для всех подсущностей осмотра создадим модели:

interface InspectionsModels
{
   interface Inspections
   {
       Inspection get(int id);
       List<Inspection> getAll(User user);
       void add(Inspection inspection);
       void update(Inspection inspection);
       void remove(Inspection inspection);
   }
   interface Steps
   {
       Step get(int id);
       List<Step> getAll(Inspection inspection);
       void add(Step step);
       void removeAll(Inspection inspection);
   }
   interface Items
   {
       Item get(int id);
       List<Item> getAll(Step step);
       void add(Item item);
       void removeAll(Step step);
   }
   interface Datas
   {
       Data get(int id);
       List<Data> getAll(Item item);
       void add(Data data);
       void update(Data data);
       void remove(Data data);
       void removeAll(Item item);
   }
}

Теперь передадим доступам их модели:

interface UsersAccess
{
   UsersModels.Users users();
}
interface InspectionsAccess
{
   InspectionsModels.Inspections inspections();
   InspectionsModels.Steps steps();
   InspectionsModels.Items items();
   InspectionsModels.Datas datas();
}
interface ScenariosAccess
{
   ScenariosModels.ScenarioTypes scenarioTypes();
   ScenariosModels.StepTypes stepTypes();
   ScenariosModels.ItemTypes itemTypes();
}

Теперь наш data access object готов.

Использование data access object

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

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

При прохождении осмотра нужно опираться на шаблон и на его основе составлять объект осмотра. Придется определять, как отображать этапы осмотра и его элементы, т. е. в этом случае необходим полный доступ ко всем сущностям шаблона и сущностям осмотра. Этому модулю можно передать ScenariosAccess вместе с InspectionsAccess.

Во время синхронизации необходимо получать информацию о готовых осмотрах для передачи на сервер со всеми элементами и прикреплёнными данными. В этот модуль стоит передать только InspectionsAccess.

Для сравнения посмотрите на код, который используется для получения данных, если не использовать DAO:

class SomeModel
{
   private final Settings settings;
   SomeModel(Settings s)
   {
       settings = s;
   }
   public List<Inspection> getInspections()
   {
       User currentUser = SQLite.getInstance().getUsers().getFromId(settings.getCurrentUserId());
       return SQLite.getInstance().getInspections().getAllFromUser(currentUser);
   }
}

Обратите внимание, что класс неявно получает зависимость от конкретной реализации SQLite. Более того, он получает доступ ко всем частям базы данных, хотя не должен иметь возможности читать, изменять и удалять данные вне его круга задач. А вот как выглядит реализация аналогичной задачи с применением DAO:

class SomeModel
{
   private final Settings settings;
   private final UsersModels.Users users;
   private final InspectionsModels.Inspections inspections;
   SomeModel(Settings s, UsersModels.Users us, InspectionsModels.Inspections is)
   {
       settings = s;
       users = us;
       inspections = is;
   }
   public List<Inspection> getInspections()
   {
       User currentUser = users.get(settings.getCurrentUserId());
       return inspections.getAll(currentUser);
   }
}

Теперь класс явно зависит только от модели Users и от модели Inspections. Стоить заметить, что SQLite нигде не упоминается.

Тестирование

У нас уже описана логика по работе с базой данных. Мы используем её в модулях, хотя реализации работы с данными пока нет. Однако, мы уже можем написать тесты для всех доступов и моделей. Итак, DAO должен успешно передавать доступы (не null). Точно так же для доступов: они не производят никаких действий, а только передают данные. А вот для моделей можно написать тесты. Например, модель Users изначально ничего не возвращает по ключу, но после добавления объекта станет возвращать новый объект со всеми полями в таком же виде, в котором он был добавлен в базу. По такому же принципу нужно покрыть тестами все модели.

class DAOTest
{
   private DAO dao;
   @Test
   public void checkAccesses()
   {
       assertNotNull("users access", dao.usersAccess());
       assertNotNull("scenarios access", dao.scenariosAccess());
       assertNotNull("inspections access", dao.inspectionsAccess());
   }
}

class UsersTest
{
   private UsersModels.Users users;
   @Test
   public void checkUsers()
   {
       int id = randomId();
       assertNull("user must be null!", users.get(id));
       User user1 = makeFakeUser(id);
       users.add(user1);
       User user2 = users.get(id);
       assertNotNull("user with id:" + id + " must exist!", user2);
       assertEquals("users must be equals!", user1, user2);
   }
}

Реализация

Теперь осталось выбрать, с помощью чего реализовать обработку данных для нашего data access object – например, это может быть SQLite. Удобство DAO в том, что мы сможем изменить реализацию логики работы приложения, не затрагивая при этом блок по работе с базой данных. Например, в проекте MyM1y (система учёта финансовых операций) с самого начала для хранения данных был выбран SQLite. В процессе работы над проектом было решено заменить реализацию на более легковесную. Была выбрана другая библиотека — Boxes, и реализация работы с базой данных в проекте была полностью заменена. Это можно наблюдать в коммите painless jump from sqlite to custom nosql orm. Класс stan.mym1y.clean.db.SQLite.java был полностью вырезан со всеми зависимостями и заменён на stan.mym1y.clean.boxes.Boxes.java. Обратите внимание, что это было сделано без изменения модулей по взаимодействию с базой данных.

Заключение

Используя DAO, можно удобно разделять уровни доступа при работе с базой данных, чётко видеть эти уровни доступа и легко оперировать ими, не привязываясь к конкретной реализации хранения данных. Это позволяет применять такой подход вместе с TDD и точнее настраивать реализацию работы с БД (без изменений в других модулях) или полностью заменить одну реализацию на другую.