如何渲染数据?
用户界面有很多种比如命令行,桌面应用,web应用。
那么如何将领域对象渲染到用户界面,或者如何将操作反映到领域模型上?
用户界面常需要渲染多个聚合实例中的属性,但是只能修改其中一个聚合。
我们会向用户界面提供一些额外的数据。这是有好处的,因为这些额外的信息可以对用户操作起到帮助作用。这些额外数据可以包含一些选项数据。
1渲染数据的方法
1.1通过数据传输对象DTO渲染多个聚合实例
DTO将包含所有需要显示的属性,应用服务通过资源库读取多个聚合,再通过DTO组装器将多个属性映射到DTO中。用户界面将访问DTO中每一个属性,并渲染到显示界面。
这样做好处是不会存在延迟加载问题,并且解决展现层和业务层物理分离的情况。
有趣的是,DTO模式原本就是用于在远程的展现层中显示数据的。此时,DTO在业务层中创建,再序列化,然后通过网络发送,最后在展现层中反序列化。如果你的展现层不是远程的(前后端分离),那么这种模式在很多时候将给你的系统带来没有必要的复杂性。
class User {
private String username;
private String nickname;
private String mobile;
private String address;
}
class Order {
private String productName;
private String price;
}
class OrderDTO {
private String username;
private String nickname;
private String mobile;
private String address;
private String productName;
private String price;
}
class OrderDTOAssemble{
private static OrderDTO convertDTO(Order order ,User user){
...
}
}
class OrderService {
@Autowired
OrderRepository orderRepository;
@Autowired
UserRepository userRepository;
private OrderDTO getDTO(Long orderId,Long userId){
Order order = orderRepository.getOrder(orderId);
User user= userRepository.getUser(userId);
return OrderDTOAssemble.convertDTO(order ,user);
}
}
在使用DTO时,我们的聚合设计需要考虑到DTO组装器对聚合数据的查询。
此时,我们需要慎重考虑,因为我们不应该暴露出太多的聚合内部结构。我们应该尽量将客户端从聚合的内部状态中完全解耦。
允许DTO组装器深度访问聚合的状态?
这并不是一个好的主意,因为它使将客户端与聚合实现紧密地耦合起来。
1.2使用调停者模式
要解决客户端和领域模型之间的耦合问题,我们可以使用调停者模式[Gamma et al.],即双分派(Double-Dispatch)和回调(Callback)。
此时,聚合将通过调停者接口来发布内部状态。客户端将实现调停者接口,然后把实现对象的引用作为参数传给聚合。
之后,聚合双分派给调停者以发布自身状态,在这个过程中,聚合并没有向外暴露自身的内部结构。这里的诀窍在于,不要将调停者接口与任何显示规范绑定在一起,而是关注于对所感兴趣的聚合状态的渲染:
class Person {
private String id;
private String name;
private Address address;
//聚合内部发布自身状态
public void providePersonStatus(PersonInterest anInterest){
anInterest.informName(this.name);
anInterest.informProvince(this.address.getProvince());
anInterest.informCity(this.address.getCity());
anInterest.informStreet(this.address.getStreet());
}
}
//内部值对象
class Address {
private String province;
private String city;
private String street;
//省略getter setter
}
//调停者关注感兴趣的聚合状态的渲染
interface Mediator {
void interest();
}
//具体的调停者实现类
class UIMediator implements Mediator {
//调停客户端和多个聚合,解耦
private HashMap<String,String> map;
private Person person;
private Object aggregate1;
private Object aggregate2;
...
//关注感兴趣的聚合
public void interest(){
PersonInterest anInterest = new PersonInterest(map);
person.providePersonStatus(anInterest);
//也可以关注其他聚合
...
}
}
//兴趣类,存储特定聚合的状态
class PersonInterest{
private HashMap<String,String> map;
public PersonInterest(HashMap<String, String> map) {
this.map = map;
}
public void informName(String result){
map.put("name",result);
}
public void informProvince(String result){
map.put("province",result);
}
public void informCity(String result){
map.put("city",result);
}
public void informStreet(String result){
map.put("street",result);
}
}
请注意,有些人认为这种方式完全不应该属于聚合的职责,而还有人则认为这是对领域模型的自然扩展。所以这样做,好不好由自身团队进行讨论决定。
1.3通过领域负载对象渲染
在没有必要使用DTO时,我们可以使用另一种改进方法。该方法将多个聚合实例中需要显示的数据汇集到一个领域负载对象(Domain Payload Object,DPO)中。
DPO与DTO相似,但是它的优点是可以用于单虚拟机应用架构中。DPO中包含了对整个聚合实例的引用,而不是单独的属性。此时,聚合实例集群可以在多个逻辑层之间传输。
应用服务通过资源库获取到所需聚合实例,然后创建DPO实例,该DPO持有对所有聚合实例的引用。之后,展现组件通过DPO获得聚合实例的引用,再从聚合中访问需要显示的属性。
public class DPO {
//资源库查询到聚合后,都存储在DPO中,展现组件通过DPO访问聚合中的属性
private Person person;
private Order order;
private Article article;
}
该方法优点是简化不同逻辑层输出集群数据,更容易设计,节约内存。因为聚合在装在到DPO之前就被读取到内存了。
缺点是需要提供聚合内部属性访问方法,暴露了领域结构,可以使用调停者模式弥补。
另外由于延迟加载问题,某些属性并没有。这可以选择即时加载,通过反射机制强行访问。
1.4 聚合实例的状态展现
如果你的程序提供了REST资源,那么你便需要为领域模型创建状态展现以供客户端使用。
有一点非常重要:我们应该基于用例来创建状态展现,而不是基于聚合实例。意思是我们需要根据界面的需求创建状态展现,而不是根据业务对象的聚合对象提供给客户端。
从这一点来看,创建状态展现和DTO是相似的,因为DTO也是基于用例的。然而,更准确的是将一组REST资源看作一个单独的模型——视图模型(View Model)或展现模型(Presentation Model)。
我们所创建的展现模型不应该与领域模型中的聚合状态存在一一对应的关系。否则,你的客户端便需要像聚合本身一样了解你的领域模型。此时,客户端需要紧跟领域模型中行为和状态的变化,你也随之失去了抽象所带来的好处。
展现模型代码示例:
//控制器资源
@Path("/tenants/{tenantId}/users")
public class UserResource extends AbstractResource {
@GET
@Path("{username}")
@Produces({ OvationsMediaType.ID_OVATION_TYPE })
public Response getUser(@PathParam("tenantId") String aTenantId,@PathParam("username") String aUsername,@Context Request aRequest) {
User user = this.identityApplicationService().user(aTenantId, aUsername);
if (user == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
Response response = this.userResponse(aRequest, user);
return response;
}
//私有方法
private Response userResponse(Request aRequest, User aUser) {
Response response = null;
EntityTag eTag = this.userETag(aUser);
ResponseBuilder conditionalBuilder = aRequest.evaluatePreconditions(eTag);
if (conditionalBuilder != null) {
response =conditionalBuilder.cacheControl(this.cacheControlFor(3600)).tag(eTag).build();
} else {
String representation =
ObjectSerializer.instance().serialize(new UserRepresentation(aUser));//序列化
response =
Response
.ok(representation)
.cacheControl(this.cacheControlFor(3600))
.tag(eTag)
.build();
}
return response;
}
}
//服务类
@Service
public class IdentityApplicationService {
@Autowired
private UserRepository userRepository;
@Transactional(readOnly=true)
public User user(String aTenantId, String aUsername) {
User user =this.userRepository().userWithUsername(new TenantId(aTenantId),aUsername);
return user;
}
}
//资源库
public interface UserRepository {
public void add(User aUser);
public Collection<User> allSimilarlyNamedUsers(TenantId aTenantId,String aFirstNamePrefix,String aLastNamePrefix);
public void remove(User aUser);
public User userFromAuthenticCredentials(TenantId aTenantId,String aUsername,String anEncryptedPassword);
public User userWithUsername(TenantId aTenantId,String aUsername);
}
//资源库实现类
public class HibernateUserRepository extends AbstractHibernateSession implements UserRepository {
@Override
public void add(User aUser) {
try {
this.session().saveOrUpdate(aUser);
} catch (ConstraintViolationException e) {
throw new IllegalStateException("User is not unique.", e);
}
}
@Override
@SuppressWarnings("unchecked")
public Collection<User> allSimilarlyNamedUsers(
TenantId aTenantId,
String aFirstNamePrefix,
String aLastNamePrefix) {
if (aFirstNamePrefix.endsWith("%") || aLastNamePrefix.endsWith("%")) {
throw new IllegalArgumentException("Name prefixes must not include %.");
}
Query query = this.session().createQuery(
"from com.saasovation.identityaccess.domain.model.identity.User as _obj_ "
+ "where _obj_.tenantId = ? "
+ "and _obj_.person.name.firstName like ? "
+ "and _obj_.person.name.lastName like ?");
query.setParameter(0, aTenantId);
query.setParameter(1, aFirstNamePrefix + "%", Hibernate.STRING);
query.setParameter(2, aLastNamePrefix + "%", Hibernate.STRING);
return query.list();
}
@Override
public void remove(User aUser) {
this.session().delete(aUser);
}
@Override
public User userFromAuthenticCredentials(
TenantId aTenantId,
String aUsername,
String anEncryptedPassword) {
Query query = this.session().createQuery(
"from com.saasovation.identityaccess.domain.model.identity.User as _obj_ "
+ "where _obj_.tenantId = ? "
+ "and _obj_.username = ? "
+ "and _obj_.password = ?");
query.setParameter(0, aTenantId);
query.setParameter(1, aUsername, Hibernate.STRING);
query.setParameter(2, anEncryptedPassword, Hibernate.STRING);
return (User) query.uniqueResult();
}
@Override
public User userWithUsername(
TenantId aTenantId,
String aUsername) {
Query query = this.session().createQuery(
"from com.saasovation.identityaccess.domain.model.identity.User as _obj_ "
+ "where _obj_.tenantId = ? "
+ "and _obj_.username = ?");
query.setParameter(0, aTenantId);
query.setParameter(1, aUsername, Hibernate.STRING);
return (User) query.uniqueResult();
}
}
//展现模型值对象Representation
public class UserRepresentation {
private String emailAddress;
private boolean enabled;
private String firstName;
private String lastName;
private TenantId tenantId;
private String username;
private void initializeFrom(User aUser) {
this.emailAddress = aUser.person().emailAddress().address();
this.enabled = aUser.isEnabled();
this.firstName = aUser.person().name().firstName();
this.lastName = aUser.person().name().lastName();
this.tenantId = aUser.tenantId();
this.username = aUser.username();
}
}
//领域模型domain
public class User extends ConcurrencySafeEntity {
private Enablement enablement;//开启状态
private String password;//密码
private Person person;//个人信息
private TenantId tenantId;//租户ID
private String username;//账户名
//业务逻辑1....
public void changePassword(String aCurrentPassword, String aChangedPassword) {
this.assertArgumentNotEmpty(aCurrentPassword,"Current and new password must be provided.");
this.assertArgumentEquals(this.password(),this.asEncryptedValue(aCurrentPassword),"Current password not confirmed.");
this.protectPassword(aCurrentPassword, aChangedPassword);
//发布领域事件
DomainEventPublisher.instance().publish(new UserPasswordChanged(this.tenantId(),this.username()));
}
//业务逻辑2
public void changePersonalContactInformation(ContactInformation aContactInformation) {
this.person().changeContactInformation(aContactInformation);
}
//业务逻辑3
public void defineEnablement(Enablement anEnablement) {
this.setEnablement(anEnablement);
//发布领域事件
DomainEventPublisher.instance().publish(new UserEnablementChanged(this.tenantId(),this.username(),this.enablement()));
}
public boolean isEnabled() {
return this.enablement().isEnablementEnabled();
}
//获取属性
public Person person() {
return this.person;
}
public TenantId tenantId() {
return this.tenantId;
}
public String username() {
return this.username;
}
protected String password() {
return this.password;
}
//创建值对象
public UserDescriptor userDescriptor() {
return new UserDescriptor(
this.tenantId(),
this.username(),
this.person().emailAddress().address());
}
protected User(TenantId aTenantId,String aUsername,String aPassword,Enablement anEnablement,Person aPerson) {
this.setEnablement(anEnablement);
this.setPerson(aPerson);
this.setTenantId(aTenantId);
this.setUsername(aUsername);
this.protectPassword("", aPassword);
aPerson.internalOnlySetUser(this);
DomainEventPublisher
.instance()
.publish(new UserRegistered(
this.tenantId(),
aUsername,
aPerson.name(),
aPerson.contactInformation().emailAddress()));
}
protected void setPassword(String aPassword) {
this.password = aPassword;
}
protected void setPerson(Person aPerson) {
this.assertArgumentNotNull(aPerson, "The person is required.");//赋值方法中添加验证逻辑
this.person = aPerson;
}
//省略其他setter方法...
}
public class Person extends ConcurrencySafeEntity {
private ContactInformation contactInformation;//值对象
private FullName name;
private TenantId tenantId;
private User user;//互相依赖,懒加载
public Person(TenantId aTenantId,FullName aName,ContactInformation aContactInformation) {
this();
this.setContactInformation(aContactInformation);
this.setName(aName);
this.setTenantId(aTenantId);
}
//业务逻辑
public void changeName(FullName aName) {
this.setName(aName);
DomainEventPublisher
.instance()
.publish(new PersonNameChanged(
this.tenantId(),
this.user().username(),
this.name()));
}
//相当于getter方法
public ContactInformation contactInformation() {
return this.contactInformation;
}
public EmailAddress emailAddress() {
return this.contactInformation().emailAddress();
}
public FullName name() {
return this.name;
}
protected Person() {
super();
}
protected TenantId tenantId() {
return this.tenantId;
}
protected void setTenantId(TenantId aTenantId) {
this.assertArgumentNotNull(aTenantId, "The tenantId is required.");
this.tenantId = aTenantId;
}
//懒加载
protected User user() {
return this.user;
}
public void internalOnlySetUser(User aUser) {
this.user = aUser;
}
}
//值对象
public final class ContactInformation extends AssertionConcern implements Serializable {
private static final long serialVersionUID = 1L;
private EmailAddress emailAddress;
private PostalAddress postalAddress;
private Telephone primaryTelephone;
private Telephone secondaryTelephone;
public ContactInformation(
EmailAddress anEmailAddress,
PostalAddress aPostalAddress,
Telephone aPrimaryTelephone,
Telephone aSecondaryTelephone) {
super();
this.setEmailAddress(anEmailAddress);
this.setPostalAddress(aPostalAddress);
this.setPrimaryTelephone(aPrimaryTelephone);
this.setSecondaryTelephone(aSecondaryTelephone);
}
public ContactInformation(ContactInformation aContactInformation) {
this(aContactInformation.emailAddress(),
aContactInformation.postalAddress(),
aContactInformation.primaryTelephone(),
aContactInformation.secondaryTelephone());
}
//省略getter和setter方法
}
1.5用例优化资源库查询(推荐)
我们应该基于用例,而不是基于聚合创建状态展现,所以与DTO类似,这可以看成单独地展现模型。
我们可以在资源库创建查询方法,返回所有聚合实例的超集。
查询方法动态地将结果放在值对象中,该值对象是为了当前用例特别设计的,可直接用于渲染界面。
public class UserController{
@Autowired
UserService service;
@GetMapping("/{userid}")
public UserDTO getUserInfo(Long userid){
return service.getUserInfo(userid);
}
}
public class UserService{
@Autowired
UserRepository repository;
public UserDTO getUserInfo(Long userid){
UserDTO userDTO = repository.getUserInfo(userid);//直接写定制的SQL语句映射到DTO
return userDTO;
}
}
查询方法依然使用资源库,而不是直接与数据库打交道。当然,继续优化下去,就走上CQRS的道路。
这种可以使用CQRS方式, 定制查询逻辑,不走聚合,直接查询然后映射为DTO。
2 处理不同的客户端类型
有很多种客户端类型,图形界面,REST服务和消息系统等。对于不同客户端类型,应用服务可以使用数据转换器。其中,数据转换器可以依赖注入。
PersonData personData = service.person(person, new PersonDataXMLTransformer());
return Response.ok(personData);
3渲染适配器
展现模型不只向外提供领域模型或DTO属性,在渲染视图时,还根据领域模型的状态做出一些决定。
因为领域模型不会对显示属性做特别的支持,而这些职责分给展现模型。
展现模型根据领域模型的状态推导一些特定于视图的指示器和属性值,例如分页功能。
同时,展现模型类似于适配器,可以消除getUser()和user()通用语言的阻抗失配。
public class UserRepresentation {
private String emailAddress;
private boolean enabled;
private String firstName;
private String lastName;
private TenantId tenantId;
private String username;
public UserRepresentation(User aUser) {
super();
this.initializeFrom(aUser);
}
public String getEmailAddress() {
return this.emailAddress;
}
public boolean getEnabled() {
return this.enabled;
}
//...
private void initializeFrom(User aUser) {
this.emailAddress = aUser.person().emailAddress().address();
this.enabled = aUser.isEnabled();
this.firstName = aUser.person().name().firstName();
this.lastName = aUser.person().name().lastName();
this.tenantId = aUser.tenantId();
this.username = aUser.username();
}
}
展现模型提供从视图到模型,从模型到视图的匹配。可以跟踪用户的编辑。
public class UserRepresentation {
private User user;
private EditTracker editTracker;
public UserRepresentation(User aUser) {
this.user = aUser;
this.editTracker = new EditTracker(aUser);
}
public String getEmailAddress() {
return this.emailAddress;
}
public boolean getEnabled() {
return this.enabled;
}
//...
//反映用户编辑的操作
private void changeName(User aUser) {
this.applicationService.changeName(this.editTracker.name);
}
}
当然,反映用户编辑的操作通常被我们的Controller层代劳了。