2014-03-04 94 views
0

我有一个(希望)简单的软件设计问题:我想让我的实体(=获得持久化到DB的域对象)有点不可变。意思是:主体应由服务和应用程序的每个另一部分获得创建与interface其中只有getter方法的工作原理。Spring服务,存储库,实体设计模式建议需要

例子:

  1. MyController要检索MyEntityid=5

  2. MyController有权要求MyService为了获取对象:myService.getMyEntityById(5)

  3. MyService会问MyEntityRepository从数据库获取对象

  4. MyService返回MyEntityInterfaceMyController

包装设计:

root 
    |--- service 
    |   |--- MyService.java 
    |   |--- MyServiceImpl.java 
    |   | 
    |   |--- MyEntity.java 
    |   |--- MyEntityImpl.java 
    |   | 
    |   |--- MyEntityRepository.java 
    | 
    | 
    |------- web 
      |--- MyController.java 

思路:

我的第一个想法是只需使用一个pac卡格保护的构造函数MyEntityImpl,但这不工作我使用一些其他的库(即Orika)。因此,它必须是public

接下来的想法是使用MyEntity接口。但现在我已经得到了一些问题:

问题:

MyService(Impl)有一个名为方法:updateMyEntityData(MyEntity e, Data data)。现在,我不能肯定我的服务,这MyEntity对象是真正的MyEntityImpl一个实例内。当然,我可以做一个if(e instanceof MyEntityImpl) ...,但是这正是我想做的事情。

接下来的问题是:此服务方法使用MyEntityRepository,它可以保存和检索MyEntityImpl对象,但无法处理接口MyEntity。作为一种变通方法,我可以做一个额外的数据库查询,但同样这我想:

void updateMyEntityData(MyEntity e, Data data) { 
    MyEntityImpl impl = repo.findOne(e.getId()); 
    impl.setData(data); 
    repo.saveToDB(impl); 
} 

这是不必要的数据库查询,因为我知道MyEntityMyEntityImpl一个实例,它已经由此服务创建,所以它必须是来自DB的对象。另一种可能是使用强制:

void updateMyEntityData(MyEntity e, Data data) { 
    MyEntityImpl impl = (MyEntityImpl) e; 
    impl.setData(data); 
    repo.saveToDB(impl); 
} 

摘要:

  • 只有服务允许构建MyEntityImpl
  • MyService(Impl)必须能够修改MyEntityImpl事后领域(指:必须有setter)
  • 避免不必要的数据库查询

提前谢谢!

+0

封装受保护的setters?你也可以使用组合而不是继承 - 返回一个包装'MyEntityImpl'和'implements MyEntity'的类。它应该提供一个DAO可以用来持久化的包私有'getMyEnitityImpl'方法。如果你使用[Lombok](http://projectlombok.org/),那么[@Delegate](http://projectlombok.org/features/Delegate.html)可以在3行代码中做到这一点... –

+1

You'重新考虑这个问题,并将复杂性引入一点或者几乎没有好处 - 一旦对象被构建,那么不可变性的各种好处就来了,那里的构造发生在这方面并不重要。强迫你的服务被用来构造域对象会增加你的应用程序的耦合性,因为你最终会传递服务而不仅仅取决于域类。类似地将接口和impl之间的域类拆分只会增加代码库的复杂性,并且不必要。 –

+0

嗯...我最初的想法是:*没有它,一些控制器或其他服务可以创建一个'MyEntity'实例并调用'myService.updateMyEntityData(...)'*。然后我不能确定传递的对象是否真的是来自数据库的对象,或者它是否在其他地方创建。 –

回答

3

我认为你需要克服公共构造函数。由于只有从存储库/数据库中检索到的对象可以分配有效的标识,因此可以使用它来控制更新。

是的,你可以猜测身份,但你可以做一些愚蠢的事情来处理任何保护,你认为你放在适当的位置 - 我可以创建一个实例并重新分配字段,如果我选择。至少在多线程环境下(如果你不在多线程执行更新的环境中,那么好处不明显,而且不值得花费),那么不变性是更高贵的目标。

问题是不可变性与通常会发生变异的域实体冲突。解决这个问题的一种常见方法是在时间戳中加入一个时间戳,指示最后一次突变的提交时间和使用突变的拷贝。这里有一个清洁的方式使用建设者格局产生突变的拷贝一个例子:

public MyEntity 
{ 
    private Object identity; 
    private long mutated; 
    private Data data; 

    public MyEntity(Object identity, long mutated, Data data) 
    { 
    this.identity = identity; 
    this.mutated= mutated; 
    this.data = data;   
    } 

    public Object getIdentity() 
    { 
    return this.identity; 
    } 

    public Data getData() 
    { 
    return this.data; 
    } 

    public Builder copy() 
    { 
    return new Builder(); 
    } 

    public class Builder 
    { 
    private Data data = MyEntity.this.data; 

    public Builder setData(Data data) 
    { 
     this.data = data; 
    } 

    public MyEntity build() 
    { 
     return new MyEntity(MyEntity.this.identity, MyEntity.this.mutated, this.data); 
    } 
    } 
} 

调用代码是这样的:

MyEntity mutatedMyEntity = myEntity.copy().setData(new Data()).build(); 

虽然这种方法使事情变得比较干净,它介绍了多线程同时创建多个变异副本的问题。

根据您的确切需求,这意味着您需要在提交更改时检测冲突(saveToDB方法),方法是检查最新版本的变异时间戳(以避免两次数据库命中,最好做在存储过程中很多,但替代方法是在执行写操作的类中将身份缓存保留为突变时间戳)。冲突解决方案将再次降低到您的具体要求,同时将对同一实体的其他实例进行更改。

+0

谢谢。我使用的是MongoDB,因此DB中的每个对象都有一个UUID。下面是我的应用程序中的一个真实世界的问题,其中不变性是很好的:用户密码只能使用passwordRecoveryKey设置,因此只有UserService可以从数据库访问User和PasswordRecoveryKey。现在,如果用户是可变的,可以调用'u.setPassword(newPassword); userService.updateUser(User u,DataToUpdate d);'('updateUser'方法只更新'u'中的字段,然后将整个obj保存到DB中)。在这种情况下,密码可以从任何地方进行更新,而无需使用令牌 –

+0

如果只有'setPassword'方法,则不能通过'Builder'来解决,并且避免冲突以避免使用密码恢复密钥次? –

0

我现在用一个更简单的办法:

public class MyEntity { 

    MyEntity() { 

    } 

    @Id 
    private ObjectId id; 
    public ObjectId getId() { return id; } 

    private String someOtherField; 
    public String getSomeOtherField() { return someOtherField; } 
    setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; } 

} 

如果实体有一些“最后”的字段,它获得的第二构造,因为春天的数据抛出一个异常,如果它不能映射字段名的构造函数的参数名称,这样总是工作:

public class MyEntity { 

    protected MyEntity() {} // this one is for Spring Data, 
          // because it can't map 
    MyEntity(Integer i) { // this constructor param "i" 
    this.finalInt = i; // to a field named "i". (The 
    }      // field is called "finalInt") 

    @Id 
    private ObjectId id; 
    public ObjectId getId() { return id; } 

    private Integer finalInt; 
    public Integer getFinalInt() { return finalInt; } 

    private String someOtherField; 
    public String getSomeOtherField() { return someOtherField; } 
    setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; } 

} 

封装布局是这样的:

root 
    |--- service 
    |   |--- MyService.java (public interface) 
    |   |--- MyServiceImpl.java (package protected class implements MyService) 
    |   | 
    |   |--- MyEntity.java (public class) 
    |   | 
    |   |--- MyEntityRepository.java (package protected) 
    | 
    | 
    |------- web 
      |--- MyController.java 

现在Controller不能构建自己的Entity对象(至少在使用构造函数时不会),并且它必须使用Service(由Spring连接到ServiceImpl)。

Repository不能被Controller访问,因为它受封装保护,因此只能由Service使用。

只有Service(和Repository)才能修改Entity的内容,因为所有设置程序都受到程序包保护。

我觉得这是一个相当不错的解决方案,它可以防止大量的恶意代码一样

  • 控制器代码里面存储库访问

  • 实体修改在控制器和保存到数据库,而不Service有控制

  • 通过应用程序(例如没有ID)传递无效(自建)对象。

当然,仍然可以使用反射绕过它,但这不是重点。整个事情是不是安全,它是关于干净的代码结构良好的应用其中数据和控制流程明确规定