原文链接:https://medium.com/@krzychukosobudzki/repository-design-pattern-bc490b256006#.r0my8xrj6
在Android中我们需要将数据存储起来以持久化,大部分情况下我们使用SQLite存储数据。但当你想通过一种简单而只读的方式存储,那该怎么做呢?没错,你要找一个好的ORM。你可以开始写制表的类并设计所有需要的方法。
public interface NewsesDao {
void add(News news);
void update(News news);
void remove(News news);
News getById();
List
getNewest(); List
getAll(); List
getByCategory(Category category); List
getNewerThan(long date); }
看起来熟悉吗?这个有错吗?当然没有,但很有提高的空间。
与其写一个(或两个)超级类做所有的事情,还不如遵循单一职责原则并应用Repository模式。
Repository是domain层与数据层的中介,如同一个内存中的domain模型集合。使用端会以声明的方式构造查询条件并将其发送给Repository以获取数据。Repository可以增删对象,就如一个对象集合,而其内部封装的数据映射代码会在后台进行逻辑操作。
我们根据Repository的基础定义开始写代码:
public interface Repository
{ void add(T item); void update(T item); void remove(T item); List
query(Specification specification); }
在这里我用Specification替代了Criteria,二者区别除了名字以外,还有Specification不关注数据会存在哪里。在后续使用过程中,我发现有两个方法特别有用,极建议添加。
public interface Repository
{ void add(T item); void add(Iterable
items); void update(T item); void remove(T item); void remove(Specification specification); List
query(Specification specification); }
当然还是要根据具体需求来写。在几乎所有的应用中你都需要同时存储于删除多个条目。Repository是不是简化了你的工作?
回到Specification,这只是一个普通的名字接口,一旦定义了Repository是什么,就可以开始实现Specification了:
public interface SqlSpecification extends Specification {
String toSqlQuery();
}
基于数据库的基本Repository实现如下:
public class NewsSqlRepository implements Repository
{ private final SQLiteOpenHelper openHelper; private final Mapper
toContentValuesMapper; private final Mapper
toNewsMapper; public NewsSqlRepository(SQLiteOpenHelper openHelper) { this.openHelper = openHelper; this.toContentValuesMapper = new NewsToContentValuesMapper(); this.toNewsMapper = new CursorToNewsMapper(); } @Override public void add(News item) { add(Collections.singletonList(item)); } @Override public void add(Iterable
items) { final SQLiteDatabase database = openHelper.getWritableDatabase(); database.beginTransaction(); try { for (News item : items) { final ContentValues contentValues = toContentValuesMapper.map(item); database.insert(NewsTable.TABLE_NAME, null, contentValues); } database.setTransactionSuccessful(); } finally { database.endTransaction(); database.close(); } } @Override public void update(News item) { // TODO to be implemented } @Override public void remove(News item) { // TODO to be implemented } @Override public void remove(Specification specification) { // TODO to be implemented } @Override public List
query(Specification specification) { final SqlSpecification sqlSpecification = (SqlSpecification) specification; final SQLiteDatabase database = openHelper.getReadableDatabase(); final List
newses = new ArrayList<>(); try { final Cursor cursor = database.rawQuery(sqlSpecification.toSqlQuery(), new String[]{}); for (int i = 0, size = cursor.getCount(); i < size; i++) { cursor.moveToPosition(i); newses.add(toNewsMapper.map(cursor)); } cursor.close(); return newses; } finally { database.close(); } } }
当使用Lambda和Kotlin时代码会显得简短一些。在这个例子中我故意只实现了add和query方法因为其他的方法非常类似。相信你已经注意到了Mapper接口,其职责是将一个对象映射到另一个。在这个例子中Mapper起到了很大的作用。在使用Repository模式时,Mapper类让你可以只关注重要的事情。
public interface Mapper
{ To map(From from); }
为了进行数据库或其他存储的查询,我们需要实现Specification:
NewestNewsesSpecification.class
public class NewestNewsesSpecification implements SqlSpecification {
@Override
public String toSqlQuery() {
return String.format(
"SELECT * FROM %1$s ORDER BY `%2$s` DESC;",
NewsTable.TABLE_NAME,
NewsTable.Fields.DATE
);
}
}
NewsByIdSpecification.class
public class NewsByIdSpecification implements SqlSpecification {
private final int id;
public NewsByIdSpecification(final int id) {
this.id = id;
}
@Override
public String toSqlQuery() {
return String.format(
"SELECT * FROM %1$s WHERE `%2$s` = %3$d';",
NewsTable.TABLE_NAME,
NewsTable.Fields.ID,
id
);
}
}
NewsesByCategorySpecification.class
public class NewsesByCategorySpecification implements SqlSpecification {
private final Category category;
public NewsesByCategorySpecification(final Category category) {
this.category = category;
}
@Override
public String toSqlQuery() {
return String.format(
"SELECT * FROM %1$s WHERE `%2$s` = '%3$d'",
NewsTable.TABLE_NAME,
NewsTable.Fields.CATEGORY_ID,
category.getId()
);
}
}
Specification很简单,一般只有一个方法。你想写多少Specification都行,而不会影响DAO类。而且这样测试就变得更加简单,通过接口,Specification可以被随意模拟、替换,甚至可以用假数据测试。你能想象与又臭又长的DAO相比Specification类有多么简单易懂吗?
Repository可以与MVP很好地融合,也能在第三方开源框架中使用。如果你用Dagger2你需要指定在某处要注入哪种Repository实现。
public class LatestNewsesPresenter implements Presenter
{ private final LatestNewsesView view; private final Repository
repository; public LatestNewsesPresenter(LatestNewsesView view, Repository
repository) { this.view = view; this.repository = repository; } @Override public void onCreate(Bundle savedInstanceState) { final List
newses = repository.query(new NewestNewsesSpecification()); view.setNewses(newses); } @Override public void onDestroy() { } }
下面开始复杂一些
现在需求变了,你不能用SQLite了,现在要用Realm。
//那边的程序员我听到了你们的声音!!!
如果你已经用DAO实现了应用,难道需要重写一堆一堆的类,包括那些非数据存储层的吗?因为你在使用Repository模式,这都是不必的,你只需要实现Repository和Specification来适应新需求即可(下面就以Realm为例)。你不需要修改其他的类,现在让我们看一下新的实现:
public class NewsRealmRepository implements Repository
{ private final RealmConfiguration realmConfiguration; private final Mapper
toNewsMapper; public NewsRealmRepository(final RealmConfiguration realmConfiguration) { this.realmConfiguration = realmConfiguration; this.toNewsMapper = new NewsRealmToNewsMapper(); } @Override public void add(final News item) { final Realm realm = Realm.getInstance(realmConfiguration); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { final NewsRealm newsRealm = realm.createObject(NewsRealm.class); newsRealm.setCategory(item.getCategory()); newsRealm.setDate(item.getDate()); newsRealm.setTitle(item.getTitle()); newsRealm.setText(item.getText()); } }); realm.close(); } @Override public void add(Iterable
items) { // TODO to be implemented } @Override public void update(News item) { // TODO to be implemented } @Override public void remove(News item) { // TODO to be implemented } @Override public void remove(Specification specification) { // TODO to be implemented } @Override public List
query(Specification specification) { final RealmSpecification realmSpecification = (RealmSpecification) specification; final Realm realm = Realm.getInstance(realmConfiguration); final RealmResults
realmResults = realmSpecification.toRealmResults(realm); final List
newses = new ArrayList<>(); for (NewsRealm news : realmResults) { newses.add(toNewsMapper.map(news)); } realm.close(); return newses; } }
Specification接口也要重写一下:
public interface RealmSpecification extends Specification {
RealmResults
toRealmResults(Realm realm); }
以及一个RealmSpecification的实现:
public class NewsByIdSpecification implements RealmSpecification {
private final int id;
public NewsByIdSpecification(final int id) {
this.id = id;
}
@Override
public RealmResults
toRealmResults(Realm realm) { return realm.where(NewsRealm.class) .equalTo(NewsRealm.Fields.ID, id) .findAll(); } }
现在你就可以拿Realm替代SQLite了。(还记得Presenter中依赖是如何被定义的吗?)
奖
这里还有一个用例,你可以用Repository模式提供缓存。就像前面的例子一样,写一个CacheSpecification。
public interface CacheSpecification
extends Specification { boolean accept(T item); }
与其他接口一起使用:
public class NewsByIdSpecification implements RealmSpecification, CacheSpecification
{ private final int id; public NewsByIdSpecification(final int id) { this.id = id; } @Override public RealmResults
toRealmResults(Realm realm) { return realm.where(NewsRealm.class) .equalTo(NewsRealm.Fields.ID, id) .findAll(); } @Override public boolean accept(News item) { return id == item.getId(); } }
最重要的是,不是所有的Specification都需要实现所有的接口,如果在缓存中只用到NewsByIdSpecification,那就不需要让NewsByCatetorySpecification也实现CacheSpecification。
现在你的代码更加整洁了,你的队友更加爱你了,产品经理不用死了,就算你跳槽了,接替你的人也不会砍你了。
欢迎关注我的公众号,将零碎时间都用在刷干货上!