我有一个(希望)简单的软件设计问题:我想让我的实体(=持久保存到DB的域对象)有点不可变。意味着:实体应仅由服务创建,并且应用程序的每个其他部分都使用仅具有getter方法的interface
。
示例:
MyController
希望使用MyEntity
id=5
MyController
必须询问MyService
才能获取对象:myService.getMyEntityById(5)
MyService
会要求MyEntityRepository
从数据库中获取对象
MyService
将MyEntityInterface
返回MyController
包装设计:
root
|--- service
| |--- MyService.java
| |--- MyServiceImpl.java
| |
| |--- MyEntity.java
| |--- MyEntityImpl.java
| |
| |--- MyEntityRepository.java
|
|
|------- web
|--- MyController.java
思路:
我的第一个想法是在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);
}
这是一个不必要的数据库查询,因为我知道 MyEntity
是MyEntityImpl
的一个实例并且它已由此服务创建,因此必须< / strong>是来自DB的对象。另一种可能性是使用演员:
void updateMyEntityData(MyEntity e, Data data) {
MyEntityImpl impl = (MyEntityImpl) e;
impl.setData(data);
repo.saveToDB(impl);
}
要点:
MyEntityImpl
MyService(Impl)
之后必须能够修改MyEntityImpl
的字段(表示:必须有设置者)提前谢谢!
答案 0 :(得分: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
方法)时检测冲突(以避免两次数据库命中,它是最好在存储过程中做很多事情,尽管替代方法是在执行写入的类中保持身份的缓存到变异的时间戳。冲突解决方案将再次降低到您的特定要求,因为它将传播对同一实体的其他实例的更改。
答案 1 :(得分: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; }
}
如果Entity有一些“final”字段,那么它就是第二个构造函数,因为如果它不能将字段名称映射到构造函数参数名称,Spring Data会抛出异常,这种方式总是有效:
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
1}})。
Repository
无法访问Controller
,因为它受到包保护,因此只能由Service
使用。
只有Service
(和Repository
)才能修改Entity
的内容,因为所有的设置者都受到包保护。
我认为这是一个非常好的解决方案,它可以防止很多错误的代码,比如
Controller代码中的存储库访问
Controller中的实体修改并保存到DB而Service
无法控制
通过应用程序传递无效(自构造)对象,例如没有ID。
当然,仍然可以使用反射绕过它,但这不是重点。整个事情不是关于安全性,而是关于清洁代码和结构良好的应用程序,其中数据和控制流明确定义