📜 ⬆️ ⬇️

Design patterns for Android development. Part 4 - Saving data. Domain Model, Repository, Singleton and BDD

At once I want to say that in the article I will not describe how to work with Data Provider. This can be found in the documentation and in numerous articles on the Internet.
Here I will talk about the Domain Model, Singleton, Repository design patterns, the Behavior Driven Development (BDD) approach and how I used them in my program.

The Domain Model pattern is used in cases where development is conducted from the subject area, in such cases there is an understandable subject area and its terms are simply embodied in bytes.

For example, in my program, the subject area consists of schedule data specified in the form of several alarms; for an alarm clock, you can set the days of the week and the time, as well as the sign “alarm is on”. There are also several algorithms, for example, to get an alarm clock, which will work next and the date and time of its activation. Since the alarm clock can be dormant, it turns out that one alarm clock has several triggers with different actions: the first one is activated, the drowsiness, and the last drowsiness, when the snooze button is no longer available. Therefore, there is an algorithm for teaching the nearest absolute time and action.
There are also algorithms for creating a new alarm clock and editing / deleting existing ones.
')
That is, in my data domain there is data in the form of several alarms and several algorithms that implement the logic of the data domain.

Why I decided to use this design pattern at all. Alternatively, I could make a separate class that creates / edits alarm clocks and stores them in a database, and the algorithms for calculating the nearest alarm clock could be done in another class.

First, in terms of the subject area, we work with one list of alarms, add new alarms to it and ask what alarm is next. That is, it looks comfortable in terms of encapsulating data and algorithms. This model is more convenient for human perception.

Secondly, in that case, when I test a model, I can test the usual user behavior. For example, create an alarm clock, set a schedule for it, check that the model gives the correct next time, then I add a new alarm clock, and disable the old one and check that this time the next time is the right one.

This approach is called Behavior Driven Development. Its advantage is that I can test the model in terms of the subject area, that is, in the unit tests, I simulate the usual user behavior. Due to the fact that this is implemented through the unit test mechanism, before each release I can get rid of these tests and be sure that my program normally handles the main actions of the user.

If I used a separate class to edit / save alarms and a separate class to calculate the nearest alarm, I would certainly test them separately, but I could not check them together and I could not check the basic actions of the user.


Model and its interfaces


When implementing the domain, I implemented a model in the form of a class AlarmListModel, which stores alarms and implements these algorithms. I want to emphasize that the model knows nothing about saving to the database. The model is maintained by the repository, which I will discuss below.

The model has a list of alarms, which is used when calculating the next alarm clock or when editing. That is, the model stores both data and implements algorithms. It is obvious that in the class “alarm clock” it is impossible to implement these algorithms, because the one who calculates the time of the next trigger must know about all the alarms turned on, and the “alarm clock” object knows only about itself. Therefore, my alarm clock is a simple structure with data and without any algorithms.

The model implements two interfaces.
IAlarmListModel - implements methods related to the subject area, for example, create a new alarm clock and find out the time of the next trigger.
IListModelData - the interface that is used by the repository to save the model in the database or vice versa to read from the database.

I want to note that my repository reads all the data entirely, read the schedules of all the alarms from the database. This is due to the fact that there are not so many alarm clocks so that I can climb into the database for each one, as well as the fact that in order to calculate the next alarm clock, I need all the alarms turned on, and to display in the list I need all the alarms at all.

Below are the main sources of the model and interfaces:

public interface IAlarmListModel { IDisplayAlarm getNextDisplayAlarm(Date curTime); IDisplayAlarm createAlarm(); void updateAlarm(IDisplayAlarm iDisplayAlarm); void addAlarm(IDisplayAlarm iDisplayAlarm); void deleteAlarm(IDisplayAlarm item); void takeAlarmForEdit(int alarmID); IDisplayAlarm getEditingAlarm(); void saveEditingAlarm(boolean stayEditing); } 


The takeAlarmForEdit () and saveEditingAlarm () methods are needed to clone the alarm for editing and be able to discard the changes made by the user if he clicked Cancel. These methods do not save anything to the database, only to the internal list of alarms.

 public interface IListModelData<T> { ArrayList<T> getItemList(); ArrayList<T> getDeletedItemList(); } 


This interface uses a repository to access the internal alarm lists in the model. As you can see, there is a list of remote alarms. This includes alarms that the user has decided to delete, then the repository scans this list and removes alarms from the database.

 public class AlarmListModel implements IAlarmListModel, IListModelData<AlarmItem> { private ArrayList<AlarmItem> alarmArray = new ArrayList<AlarmItem>(); private ArrayList<AlarmItem> alarmArrayDeleted = new ArrayList<AlarmItem>(); @Override public synchronized IDisplayAlarm getNextDisplayAlarm(Date curTime) { // ,      . } @Override public synchronized AlarmItem createAlarm() { AlarmItem alarm = new AlarmItem(); alarm.setId(0); //  ,      , //  ID     alarm.setState(EntityState.ADDED); alarm.setName("New alarm");// alarm.setIssue(8, 0, false);//8    alarm.setEnable(true);//  //    // false ,      alarm.setDay(Calendar.MONDAY, true, false); alarm.setDay(Calendar.TUESDAY, true, false); alarm.setDay(Calendar.WEDNESDAY,true, false); alarm.setDay(Calendar.THURSDAY, true, false); alarm.setDay(Calendar.FRIDAY, true, false); alarm.setDay(Calendar.SATURDAY, false, false); alarm.setDay(Calendar.SUNDAY, false, false); return alarm; } @Override public synchronized void addAlarm(IDisplayAlarm item) { AlarmItem alarm = (AlarmItem)item; if(item.getId() > 0) { ex = new Exception("    "); } alarm.setState(EntityState.ADDED); alarmArray.add(alarm); } @Override public synchronized void updateAlarm(IDisplayAlarm item) { AlarmItem alarm = (AlarmItem)item; alarmArray.set(getPositionByID(item.getId()), alarm); } @Override public synchronized void deleteAlarm(IDisplayAlarm item) { AlarmItem alarm = (AlarmItem)item; alarmArray.remove(alarm); alarm.setState(EntityState.DELETED); alarmArrayDeleted.add(alarm); } public synchronized void takeAlarmForEdit(int alarmID) { if(alarmID > 0) editAlarm = ((AlarmItem)getDisplayAlarmByID(alarmID)).clone(); else editAlarm = createAlarm(); } @Override public synchronized AlarmItem getEditingAlarm() { return editAlarm; } @Override public synchronized void saveEditingAlarm(boolean stayEditing) { if(editAlarm == null)return; if(editAlarm.getId() > 0) updateAlarm(editAlarm); else addAlarm(editAlarm); if(!stayEditing)editAlarm = null; } @Override public synchronized ArrayList<AlarmItem> getItemList() { return alarmArray; } @Override public synchronized ArrayList<AlarmItem> getDeletedItemList() { return alarmArrayDeleted; } } 


So, let's see what is inside our model. Why all methods are marked as synchronized, read the section on the repository.

The createAlarm () method is needed for the model to be a factory for alarms. I want to note that the factory does not add the created object to the internal list of budilics for this there is a method addAlarm ()

The addAlarm (), updateAlarm (), deleteAlarm () methods are needed to work with the internal list of alarms. As you have noticed, the AlarmItem class has methods for monitoring status, these states indicate the status of the record relative to the database:

 public enum EntityState { ADDED, NOT_CHANGED, CHANGED, DELETED } 


This is a standard technique used in ORM engines to synchronize with the database in order to reduce the number of calls to the database and make changes only for changed records.
As you understand, the AlarmItem class in all its setters sets the status that the record has changed. The repository analyzes these states in order to decide what needs to be updated in the database and what is not.

I want to note that in the addAlarm method I use one trick. If I have an exception when an incorrect alarm clock is added to the collection, then I do not throw an exception, but I silently store this exception in the field:

 ex = new Exception("    "); 


This field is checked in my tests. After the test is executed, this field must be null, otherwise the test is not passed. Of course, it would be possible to use Assert or fully throw an exception, but there are situations when an exception cannot be thrown. For example, if your class implements the interface and you have an exception in the method, and the interface cannot exclude the exception from this method. In order to catch this exception, I place it in the field of the class under test and check it at the end of the test.
I think there is a more elegant solution to this problem, but I have not yet found it.

Methods takeAlarmForEdit (), getEditingAlarm (), saveEditingAlarm () are mainly needed for the alarm editing form. As I said, they are needed to clone an alarm, change it and, if necessary, discard changes.

Meet the repository


The repository has only two methods, save and read the model:

 public interface IAlarmRepository { public abstract void load(); public abstract void save(); } 


As you can see there is no mention of the model. The model I have implemented as a Singleton template, that is, the entire program has only one instance of the model and if one part of the program, for example, UI, changes something in the model, then another part, for example, a server or repository, reading data from the same instance models will immediately receive the latest changes. It is very convenient when you need to transfer the only existing object between parts of the application.

The Singleton template has a number of flaws, for which it even anathematized and called antipattern. Usually, Singleton is implemented through a static class, which leads to the fact that it cannot be imitated in tests and also to the fact that if a class uses Singleton, then this is not visible from the declaration of the class itself.
There is also a problem in that you need to be sure that the Singleton is always in the correct state, if to transfer a Singleton from one state to another requires two calls, then another Singleton call can be inserted between them and catch a Singleton in the wrong state, which can lead to the wrong result.

So I solved the problem with the static class due to the fact that I use RoboGuice, which in this case is a model factory. Also, using RoboGuice allows you to show that the class uses Singleton and through RoboGuice I can replace Singleton with simulations in tests.

The problem of the correct state is solved by the fact that all changes with the model are made in one method call and all methods are marked as synchronized, so that the model does not have two calls at the same time.

In this form, Singleton is very useful :). My model is loaded from the database once and therefore, when the program is running, it is not necessary to spend resources on loading the model in each activity, be it form or service. Just do not think how to transfer data from one activity to another. For example, in the editing activity, I transmit only the ID, which in the model I find the alarm clock I need.

So learn to cook singleton properly.

Here is the repository code:

 public class AlarmRepository implements IAlarmRepository { // Singleton . //,   Singleton   RoboGuice @Inject private IAlarmListModel alarmListModel; @Inject Context context; @Inject public AlarmRepository() { db = (new DBHelper(context)).getWritableDatabase(); } @Override public synchronized void load() { IListModelData<AlarmItem> res = (IListModelData<AlarmItem>)alarmListModel; res.getItemList().clear(); res.getDeletedItemList().clear(); Cursor c = db.query(DBHelper.TABLE_NAME, projection, null, null, null, null, DBHelper.A_ID); c.moveToNext(); AlarmItem alarm = null; while(!c.isAfterLast()) { alarm = cvToAlarm(c); res.getItemList().add(alarm); c.moveToNext(); } c.close(); alarmListModel.setLoaded(true); } @Override public synchronized void save() { IListModelData<AlarmItem> model = (IListModelData<AlarmItem>)alarmListModel; ContentValues v = null; for(AlarmItem item : model.getItemList()) { switch(item.getState()) { case CHANGED: v = alarmToCV(item); int retVal = db.update(DBHelper.TABLE_NAME, v, DBHelper.A_ID + "=" + item.getId(), null); break; case ADDED: v = alarmToCV(item); int id = (int)db.insert(DBHelper.TABLE_NAME, null, v); item.setId(id); break; case DELETED: ex = new Exception("      DELETED   "); break; } item.setState(EntityState.NOT_CHANGED); } for(AlarmItem item : model.getDeletedItemList()) switch(item.getState()) { case CHANGED: ex = new Exception("      CHANGED   "); break; case ADDED: ex = new Exception("      ADDED   "); break; case DELETED: int retVal = db.delete(DBHelper.TABLE_NAME, DBHelper.A_ID + "=" + item.getId(), null); break; } model.getDeletedItemList().clear(); } } 


I did not show the cvToAlarm () and alarmToCV () methods in them just a lot of lines with the mapping of the alarm fields into the fields of the database table and back, which I don’t want to clutter up with the article.

Once again I will say that I will not consider here how to work with Conten providers. Therefore, what do db.update () and db.insert () mean in the SDK.

Here I will talk about the design pattern "Repository". As you understand by the secondary features as a declaration of synchronized methods, I also have a repository Singleton.

The repository is needed only to save the model in the repository. The storage can be a file, database or service on the Internet. As a result, the storage method can be separated from the model and even replaced, if necessary.
Since the repository uses the interface, in tests it can be easily replaced by imitation. As a result, you can do not only simple unit tests, when only one class is tested, and all dependent parts are replaced by simulations, but also to carry out functional tests. In functional tests, several classes are tested at once. As a rule, they take Presenter, model and other auxiliary classes that are needed, and everything below Presenter is replaced by imitation. It turns out that you can test the supplements using the BDD technique to make sure that the user's main actions are processed normally.

As you can see, the load () method reads the entire table of alarms and shifts them to the model. And the save () method looks at what should be saved in the model and saves only the changed data.

PS
Since the article and it turned out great, I did not show tests for the model and the repository. There is nothing fundamentally new in comparison with Presenter testing. We also do an instance of the test class in the test, replace the dependencies with imitations via RoboGuice and test the resulting carcass.

Read in other articles.


- Introduction
- MVP and Unit tests. Jedi Way
- User Interface, Testing, AndroidMock
- Saving data. Domain Model, Repository, Singleton and BDD
- Server side implementation, RoboGuice, testing
- Small tasks, settings, logging, ProGuard

Source: https://habr.com/ru/post/132646/


All Articles