如何写好仓储Repository?

10-12-26 banq
How To Write A Repository

仓储Repository模式已经成为最主流的模式,数据库持久化很长时间以来是一个讨论热点,目前主要问题是:主流软件并不容易有效地将需要存储的数据映射到外部存储空间如关系数据库或NoSQL数据库。

技术难点虽然已经被一些ORM工具如Hibernate等解决了,过去,我们通常使用DAO或ORM来进行业务对象和持久化数据表之间进行转换,这些技术很好,但是他们还是属于底层技术,并不能透明地和我们统一语言整合。下面的问题是如何将持久化动作和对象获取方式以及领域模型Domain Model结合起来,更进一步地说:如何更加统一我们的语言(Ubiquitous Language)(不至于出现Hibernate等底层技术语言,统一为业务语言)。

一个整合持久化技术的好办法是仓储Repositories,在Evans书籍中定义它是:一种机制,用于封装存储 获取 搜寻一群对象集合等行为。这个定义很容易向领域专家解释,整合到统一语言中。

第一:命名
一般人仓储类如下命名:

class OrderRepository {
  List<Order> getOrdersFor(Account a){...}
}
<p class="indent">

而Rodrigo Yoshima 认为应该如下取名:

class AllOrders {
   List<Order> belongingTo(Account a){...}
}
<p class="indent">

似乎是一个小的改变,但是有帮助得多,如果两个仓储包含不属于他们的方法,我们如何分辨他们呢?

//classic naming style
class UserRepository{
 User retrieveUserByLogin(String login){...}
 void submitOrder(Order order){...}
}
<p class="indent">

客户端调用代码:

User u = userRepository.retrieveUserByLogin(“pcalcado”);
userRepository.submitOrder(new Order());
<p class="indent">


而Yoshima的取名方式如下:

//Yoshima’s naming style
class AllUsers{
 User withLogin(String login){...}
 void submitOrder(Order order){...}
}
 
//client code //非常符合英语风格,象说话一样。
User u = allusers.withLogin(“pcalcado”);
allusers.submitOrder(new Order());
<p class="indent">


一定要记住:你使用的语言方式会影响你如何思考。以retrieve或get为开始方法名是坏味道。

第二:避免方法爆炸
一个好的仓储将领域概念建模在其接口中,让我们看一个业务规则:
每个订单在周末都要增加10%的附加费。

如果我们要显示所有订单,如下:

List<Order> surchargedOrders = allOrders.placed(user, IN_A_SATURDAY);
surchargedOrders.addAll(allOrders.placed(user, IN_A_SUNDAY));
return surchargedOrders;
<p class="indent">

这虽然工作很好,但是忘记了抽象,增加附加费这个规则是不应该暴露给调用客户端的,如下会更好些:
return allOrders.surchargedFor(user);

为了实现这点,可能需要多个实体反复查询,你可以使用规格模式Specification来实现,如下:

Specification surcharged = specifications.surcharged();
return allOrders.thatAre(user, surchanged);

如果我们要获取没有附加费或者有附加费的订单,需要两个仓储AllOrders 和 SurchargedOrders.如下:

//returns all orders
return allOrders.from(user);
 
//returns only orders with applied surcharge
return surchargedOrders.from(user);
<p class="indent">


Subset Repositories子集仓储一般被建模成带有参数实例的仓储类,如下:

//a base Repository
class Users {
 private User.Status desiredStatus = null;
 
 public Users(){
   this(User.Status.ANY);
 }
 
 public Users(User.Status desiredStatus){
   this.desiredStatus= desiredStatus;
 }
 //methods go here...
}
 
//instantiated somewhere as
private Users allUsers = new Users();
private Users activeUsers = new Users(User.Status.ACTIVE);
private Users inactiveUsers = new Users(User.Status.INACTIVE);
<p class="indent">


第三:只有一个类型
如下代码,虽然使用了好的命名规则

public interface AllServices {
 
    List<Service> belongingTo(List<Account> accounts);
 
    Service withNumber(String serviceNumber);
 
    List<Service> relatedTo(Service otherService);
 
    List<Product> allActiveProductsBelongingTo(List<Account> accounts);
 
    List<Product> allProductsBelongingTo(List<Account> accounts);
 
    ContractDetails retrieveContractDetails(String serviceNumber);
}

<p class="indent">


方法返回了Service类型集合,还返回了Product集合,是多个集合,好的设计最好返回同一个类型集合:

public interface AllServices {
 
    List<Service> belongingTo(List<Account> accounts);
 
    Service withNumber(String serviceNumber);
 
    List<Service> relatedTo(Service otherService);
}
 
public interface AllProducts {
 
    List<Product> activeBelongingTo(List<Account> accounts);
 
    List<Product> belongingTo(List<Account> accounts);
}
 
public interface AllContractDetails {
   ContractDetails forServiceNumber(String serviceNumber);
}
<p class="indent">

如果特别情形需要,你可以创建一个Wrapper类,将这些方法都放入其中,如下:

public class BillingSystemGateway implements AllServices, AllProducts , AllContractDetails {
 
    List<Service> belongingTo(List<Account> accounts){...}
 
    Service withNumber(String serviceNumber) {...}
 
    List<Service> relatedTo(Service otherService) {...}
 
List<Product> activeBelongingTo(List<Account> accounts) {...}
 
List<Product> belongingTo(List<Account> accounts) {...}
 
ContractDetails forServiceNumber(String serviceNumber) {...}

<p class="indent">

最后:仓储不只是持久,仓储经常用于对象存储持久化,但是这不一定是这种情况,也适合于系统整合,甚至简单内存缓存中集合返回值对象等等。

一定要记住:仓储好处是高调说明对象来自某个地方,将这些对象作为统一语言的一部分,仓储尽可能贴近我们的领域模型。

[该贴被banq于2010-12-26 20:50修改过]

[该贴被admin于2011-01-02 09:03修改过]

11
banq
2010-12-28 14:02
这篇文章很实用,很多人对仓储还没听说过,顶一下。

bloodrate
2011-01-04 23:01
一直都觉得Repository就是DAO在DDD里的另一种称呼,因为平常没用DDD的时候就是这么写DAO的,虽然不如这么严格。

cgttian
2011-01-25 23:48
2011年01月04日 23:01 "bloodrate"的内容
一直都觉得Repository就是DAO在DDD里的另一种称呼,因为平常没用DDD的时候就是这么写DAO的,虽然不如这么严格。 ...

我的理解是 DAO 是为Repository 服务的,DAO本身并不知道如何生产或组装一个领域对象 更多的是生产一些“配件”,然后在Repository中 将这些个“配件”组装成一个个对象.

不知道理解的对不对,还请大牛指导!!!

janwen
2011-01-26 15:18
没看懂,有深度了

cgttian
2011-04-12 11:54
昨晚看再了一遍InfoQ中下载的DDD精简版,重点看了Factory 与 Reposiory章节。
Factory 与 Repository 都是为客户端提供对象 一个已组装完成的聚合根,
不同的地方再于Factory负责对象从无到有的创建,而Repository则是负责对象
从持久化状态中唤醒或将对象持久化。
这里的理解应该没有什么大错吧?!

Repository 与 DAO的关系,看到一篇搏文写了这么一段:
1. 两个接口两个实现类
Repository和Dao各自独立接口,而通过Repository实现类转发请求给Dao实现类
这种方式虽然正统,但是维护成本太高;一次更新最多要改四处地方

2.两个接口一个实现类
Repository和Dao各自独立接口,一个实现类同时实现两个接口
这种方式就大大简化维护成本;一次更新最多只改一个接口和一个实现类

3. 两个接口一个实现类
与方式2不同是,Dao接口继承Repository接口,一个实现类实现Dao接口
这种方式的维护成本和方式2差不多,但是当接口方法在这个两个接口间流动时,可以通过开发工具完成

作者从维护的角度比较他所列举的三种Repository 与DAO 的实现方式。
这样的实现方式我觉得有些奇怪,但又说不出一个所以然。
我觉得两者是为解决不同问题的因此两者因实现不同的接口,两者接口也有着不同细粒度。
DAO接口属于细粒度接口 负责与持久化介质打交道,Repository接口为粗粒度接口 通过对DAO接口的访问对对象进行组装进而提供一个完整的对象。

对于文中所说的维护成本我是这么想的,DAO可能变化会较多,但Repository 应当是相对不变或不怎么变的吧...

不知道我对以上内容的理解有些什么问题,请大家指导。

banq
2011-04-12 12:38
2011年04月12日 11:54 "@cgttian"的内容
Repository 与 DAO的关系 ...


有Repository就不要再用DAO,除非你原来是有DAO,升级到使用Repository,不得已保留原来的DAO,如果新项目两者都用,复杂麻烦,又在做对象和SQL转换的无谓工作,ORM等Hibernate实际是也一种Repository,问题是这些框架Hibernate把领域模型劫持到自己框架内部,如果曹操携天子以令诸侯,这是框架对模型侵入太厉害的表现。

cgttian
2011-04-12 22:28
2011年04月12日 12:38 "@banq"的内容
有Repository就不要再用DAO,除非你原来是有DAO,升级到使用Repository,不得已保留原来的DAO,如果新项目两者都用,复杂麻烦 ...

那... 是不是这样理解:
从DAO-->Repository
首先,从语义上显示声明了一个对象的出处,比如MessageRepository 就声明这是一个Message的仓库 从这里就可以拿到Message对象。
其次,DAO只是负责从持久化介质中获取复杂对象的组件或者是一个相对简单的对象,而Repository 它负责获取一整个复杂对象而非对象的组件,将对象的组装放入自身的职责范围内。

猜你喜欢