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

    I want to say right away that in the article I will not describe how to work with the Data Provider. This can be found both 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 template is used in those cases when the development is conducted from the subject area, in such cases there is a clear subject area and its terms are simply embodied in bytes.

    For example, in my program, the subject area consists of schedule data set in the form of several alarms, for the alarm you can set the days of the week and time, as well as the sign “alarm is on”. There are also several algorithms, for example, to get an alarm that will work next and the date and time it was triggered. Since the alarm can snooze, it turns out that one alarm has several triggers with different actions: the first alarm, snooze, and the last snooze, when the snooze button is no longer available. Therefore, there is also 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 subject area there is data in the form of several alarms and several algorithms that implement the logic of the subject area.

    Why did I even decide to use this design pattern. Alternatively, I could make a separate class that creates / edits alarms and saves them in the database, and the algorithms for calculating the closest alarm could be made in another class.

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

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

    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 unit tests I imitate the usual behavior of the user. Due to the fact that this is implemented through the unit test mechanism, before each release I can run these tests out and be sure that my program program normally fulfills the main user actions.

    If I used a separate class for editing / saving alarms and a separate class for calculating 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 subject area, I implemented a model in the form of the AlarmListModel class, which stores alarms and implements these algorithms. I want to emphasize that the model does not know anything about saving to the database. Saving the model is done by the repository, which I will discuss below.

    The model has a list of alarms, which is used when calculating the next alarm or when editing. That is, the model stores data and implements algorithms. Obviously, these algorithms cannot be implemented in the alarm class, because the one who calculates the next time should know about all the included alarms, and the alarm 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 operation.
    IListModelData - the interface that is used by the repository to save the model in the database or vice versa read from the database.

    I want to note that the repository completely reads all the data from me, read the schedules of all alarms from the database. This is due to the fact that there are not so many alarms to climb into the database for each, and also because I need all the included alarms to calculate the next alarm, and to display in the list I need all alarms in general.

    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 to 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
    {
    	ArrayList getItemList();
    	ArrayList getDeletedItemList();
    }


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

    public class AlarmListModel
    				implements IAlarmListModel,
    							IListModelData
    {
    	private ArrayList alarmArray = new ArrayList();
    	private ArrayList alarmArrayDeleted = new ArrayList();
    	@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 getItemList()
    	{
    		return alarmArray;
    	}
    	@Override
    	public synchronized ArrayList getDeletedItemList()
    	{
    		return alarmArrayDeleted;
    	}
    }


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

    The createAlarm () method, you need the model to be an alarm clock factory. I want to note that the factory does not add the created object to the internal list of alarms; for this, there is the addAlarm () method.

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

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


    This is a standard technique used in ORM engines, so that when synchronizing with the database, 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 to decide what needs to be updated in the database and what not.

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

    ex = new Exception("Попытка добавить запись с ключем");


    This field is checked in my tests. After the test is completed, this field must be null, otherwise the test failed. Of course, one could use Assert or throw an exception fully, but there are situations when an exception cannot be thrown. For example, if your class implements an interface and an exception occurs in the method, and you cannot throw an exception from this method on the interface. To catch this exception, I put it in the field of the tested class and check it at the end of the test.
    I believe there is a more elegant solution to this problem, but I have not found it yet.

    The methods takeAlarmForEdit (), getEditingAlarm (), saveEditingAlarm () are mainly needed for the edit alarm form. As I said, they are needed to clone the alarm, change it and, if necessary, discard the 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. My model is implemented as a Singleton template, that is, in the entire program there is only one instance of the model and if one part of the program, for example, UI, changes something in the model, then the other 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 the parts of the application.

    The Singleton template has a number of shortcomings, for which it was even anathematized and is called an 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 some class uses Singleton, this is not visible from the declaration of the class itself.
    There is also a problem that you need to be sure that Singleton is always in the correct state, if it takes two calls to transfer a Singleton from one state to another, then another Singleton call from another component may wedge between them and catch Singleton in the wrong condition, which can lead to an incorrect result.

    So I solved the problem with the static class due to the fact that I use RoboGuice, which in this case is the model factory. Using RoboGuice also allows us to show that the class uses Singleton and through RoboGuice I can replace Singleton with simulation in tests.

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

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

    So learn how 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 res = (IListModelData)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 model = (IListModelData)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 methods cvToAlarm () and alarmToCV () in them just a lot of lines with mapping of the alarm fields to the fields of the database table and vice versa, which I do not want to clutter up the article.

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

    Here I will talk about the Repository design pattern. As you understand by the secondary features in the form of a declaration of synchronized methods, my repository is also Singleton.

    The repository is needed only to save the model in storage. A repository can be a file, a database or a 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 an interface, in tests it can be easily replaced by imitation. As a result, it is possible to do not only simple unit tests when only one class is tested, but all the dependent parts are replaced by imitations, but also to carry out functional tests. In functional tests, several classes are tested at once. As a rule, they take Presenter, the model and other auxiliary classes that are needed, and replace everything below the Presenter with imitation. It turns out that you can test the application using the BDD technique to make sure that the main user actions are processed normally.

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

    PS
    Since the article turned out to be large, I did not start showing tests for the model and repository. There is nothing fundamentally new compared to testing Presenter in them. We also make an instance of the tested class in the test, replace dependencies with simulations through RoboGuice, and test the resulting carcass.

    Read in other articles


    - Introduction
    - MVP and Unit tests. Jedi Path
    - User Interface, Testing, AndroidMock
    - Saving Data. Domain Model, Repository, Singleton and BDD
    - Server Part Implementation, RoboGuice, Testing
    - Small Tasks, Settings, Logging, ProGuard

    Also popular now: