java 域模型_基于Spring实现领域模型模式 - RUP实践者指南 - JavaEye技术网站
事務腳本、領域模型及表模塊是Martin
Fowler在《企業應用架構模式》中總結的三種領域邏輯組織模式。各有各的優點和缺點,這里不打算討論它們各自的適用場景,只簡單總結一下在應用領域模
型模式方面的一些經驗與教訓。
1.??? 背景
近幾年Struts(1或2) + Spring + Hibernate(IBatis)組合在Java
EE企業應用開發中頻頻出現。這里也不例外,所采用的主要技術平臺即是SSH。本文總結的領域模型模式應用也主要是在這樣一個技術背景之下。
在企業應用的架構設計方面,我通常組合使用多種架構風格和模式,它們主要是:
按技術職責分層模式
組件化模式
按通用性程度分層
基于職責分層就是一般意義上的分層,劃分也采用相對常見的方式,即表現層、應用層、領域層和持久層。
組件化模式也是我最近幾年非常喜歡的一種架構模式,它可以提高系統的可維護性和可復用性。邏輯組件參考用例模型并主要在分析模型中初步創建,在設計模型中
進一步細化。組件的粒度級別為業務組件(各種組件級別包括:分布式組件、業務組件和系統級組件),業務組件可以包含所有4個職責層,即表示層、應用層、領
域層和持久層,且持久層邏輯上包括數據表。業務組件的對外接口一般設置在應用層級別,其他元素通常不對外暴露。對于數據庫表更不例外,一般地,屬于某個業
務組件的數據庫表禁止其他組件直接使用。建立一個良好的組件模型是很費工夫的事情,需要在分析模型上花費很大的精力,在設計模型中還需要進一步優化。但付
出總是會有回報的,系統可修改、可維護性、可復用性會有很大提升。當然有時我們確實不需要這種回報。
最后一個常用的架構模式是按組件的通用性進行分層,這也是RUP里的分層,即特定于應用層、通用業務層、中間件層和系統層。如果開發多個類似產品,各個產
品中不相同的組件位于特定應用層,各個產品在業務層面可以復用的組件位于通用業務層,與業務領域無關的組件放在中間件層。系統層主要是DBMS、OS級別
的東西。這種架構模式主要服務于系統化復用。
介紹完成應用領域模型模式的背景之后,應該進入正題了。
2.??? 應用領域模型
2.1.??? 主要設計元素
展現層主要包括:
View:主要是視圖元素,如JSP。
Command:主要是Struts2中的Action,通常Action以應用層中的DTO為屬性來充當PresentationModel。
應用層主要包括:
ApplicationService:由接口和實現兩部分組成,主要用來委托職責給實體或協調多個實體的協作,是真正的控制類,一般為每個用例
創建一個應用服務類。Struts1/2中的servlet或filter也常被稱為控制器,但在此種場景下,更準確的名稱應該是前端控制器,屬于展現
層。
DTO:數據傳輸對象。
Assembler:組裝器,負責將實體轉化為數據傳輸對象。
領域層主要包括:
Entity:處理核心領域邏輯。
DomainService:是不屬于單一實體且比較穩定的領域邏輯的處理者
持久層主要包括:
DAO:可以由接口和實現兩部分組成,數據訪問對象。
組件接口元素主要包括:
ComponentFacade:組件對外接口,其實現形式與ApplicationService的實現相同。
ComponentFacadeFactory:其他組件在使用該組件時,不能直接創建組件接口,該元素為創建組件接口提供了一個工廠,隱藏了組
件的內部實現。
DTO:與上面DTO同。
2.2.??? 讓實體處理領域邏輯
實體或領域對象是否是領域邏輯的核心處理者應該是事務腳本和領域模型模式之間的本質區別。“貧血領域模型”的本質應該更貼近事務腳本模式,它們有著類似的
優缺點。
以下是任務管理組件的領域模型:
注:示例中藍色字體的實體不屬于任務管理組件。
讓實體處理領域邏輯是一件簡單的事,但讓實體處理哪些領域邏輯卻是一個需要思考的問題。
為實體指派職責的基本原則是“誰擁有誰負責”,即“信息專家模式”。實體應該處理哪些它擁有相關信息和能力的領域職責。這與現實世界是一致的,如一個醫
生、一個木匠,生病了要找醫生,因為醫生擁有治病救人的專業知識,打家具要找木匠,因為木匠有做家具的技能。
在上面的領域模型中,創建、更新和刪除任務的職責應該放到Task上,這自不必說。那么統計項目實際工時的職責應該放到哪里呢?首先,考查Project
對象,它知道自己有哪些迭代,那么它可以用來合計各個迭代的工時。再考查迭代對象,它知道自己有哪些任務,那么它可以累加所有任務的工時。依此類推
Task則要負責自己工時的計算。當然這個過程中我們可能會遇到性能問題,這需要在性能目標和可維護性目標之間做出平衡。
“信息專家模式”是一個基本原則,但不是唯一的原則。關于如何分配職責是一個很大的話題這里就不再深入了。
在讓實體處理領域邏輯時,應用服務通常只是將職責委托給實體,或協調多個實體完成業務邏輯,應用服務層很薄。如:
@Service("taskFacade")
@Transactional(readOnly = true, rollbackFor = Throwable.class)
public class TaskFacadeImpl implements TaskFacade {
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
public long createTask(long projectId, long periodId, Integer
periodTag,????? Integer index, String name, int priority, Date
startDate, Date endDate,
int points, String description, List
resources)
throws BusinessException {
Task root = getRootTaskFromCache(projectId, periodId, periodTag);
if (root == null) {
root = EntityFactory.getEntity(Task.class);
root.save(projectId, periodId, periodTag);
addRootTaskToCache(projectId, periodId, periodTag, root.getId());
}
return root.addSubTask(index, name, priority, startDate, endDate,
points,description, resources);
}
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
public void moveUpTask(long taskId) throws BusinessException {
Task task = Task.get(taskId);
if (task == null)
throw new BusinessException("任務不存在。");
task.moveUp();
}
……
}
2.3.??? 賦予實體持久化能力
實體要處理領域邏輯,特別是相對復雜的領域邏輯,沒有持久化能力是不行的。Martin Fowler推薦Domain Model與Active
Record一起使用。在它們一起使用時,實體就有了持久化能力,如保存、更新和刪除自己。
那么在使用Struts2 + Spring +
Hibernate這樣的技術平臺時如何給實體賦予持久化能力呢?這里主要是在實體上加了一個靜態方法用于獲得管理該實體集合的Dao。如實體Task中
處理如下
public class Task implements Entity {
……
private static TaskDao dao() {
return IocBeans.getInstance().getBean("taskDao", TaskDao.class);
}
}
Dao是Spring的IOC管理對象,如TaskDao的具體實現:
@Repository("taskDao")
public class HibernateTaskDao extends HibernateDao implements TaskDao {
@Autowired
public void setSessionFactoryEx(SessionFactory sessionFactory) {
super.setSessionFactory(sessionFactory);
}
public Task get(long id) {
return super.get(Task.class, id);
}
public List findAll(QueryCriteria criteria, int
firstResult,
int maxResults) {
……
}
……
}
其中,HibernateTaskDao可以通過BeanFactory根據BeanId“taskDao”獲得。這里為了獲取BeanFactory并
方便獲得相關Bean實例而創建了類IocBeans,其內容如下
public final class IocBeans implements BeanFactoryAware {
private BeanFactory beanFactory;
public void setBeanFactory(BeanFactory beanFactory) throws
BeansException {
this.beanFactory = beanFactory;
}
private IocBeans() {
}
private static IocBeans instance;
public static IocBeans getInstance() {
if (instance == null)
instance = new IocBeans();
return instance;
}
@SuppressWarnings("unchecked")
public T getBean(String name, Class requiredType) {
return (T) beanFactory.getBean(name, requiredType);
}
}
實現了BeanFactoryAware接口的類可以在容器初始化時,將BeanFactory實例注入進來,從而獲得對BeanFactory的操控能
力。為了使這個類可以順利的實例化,還需要在Spring的配置文件中加入如下內容:
factory-method="getInstance" />
經過以上努力實體就擁有了持久化能力,與Active
Record不同的是這里將數據訪問邏輯移入Dao中。Dao中通常只有添加、刪除和查找方法,而沒有更新方法。使用Hibernate讓我們很幸運,我
們不需要在Dao中加入更新方法,因為在我們改變實體對象的屬性時,Hibernate會幫我們自動完成更新。下面是Task對象的幾個領域方法:
public long save(long projectId, long periodId, Integer periodTag)
throws BusinessException {
return save(projectId, periodId, periodTag, "任務虛根",
TaskDetail.PRIORITY_3,
null, null, 0, null, null);
}
protected long save(long projectId, long periodId, Integer periodTag,
String name, int priority, Date startDate, Date endDate, int
points,
String description, List resources)
throws BusinessException {
Assert.notNull(name);
setProjectId(projectId);
setPeriodId(periodId);
setPeriodTag(periodTag);
setName(name);
setPriority(priority);
set_startDate(startDate);
set_endDate(endDate);
setPoints(points);
setDescription(description);
setPercentageCompleted(0.0);
setStatus(TaskDetail.STATUS_OPEN);
long id = (Long) obtainDao().save(this);
if (resources != null && resources.size() > 0)
saveOrUpdateResources(resources);
return id;
}
public void saveOrUpdateResources(List resources)
throws BusinessException {
if (resources == null || resources.size() == 0) {
setResources(null);
IndividualTask.deleteAll(getId());
return;
}
Map resourcesMap = new HashMap
String>();
Set ids = new HashSet();
for (ResourceDetail resource : resources) {
IndividualTask itask = IndividualTask.find(getId(), resource
.getResourceId());
if (itask == null) {
itask = EntityFactory.getEntity(IndividualTask.class);
itask.save(this, resource.getResourceId(),
resource.getPercent(),
resource.isPrincipal());
} else {
itask.update(resource.getPercent(), resource.isPrincipal());
}
ids.add(itask.getId());
StringBuilder sbRes = new StringBuilder();
sbRes.append(itask.getPersonId());
sbRes.append("|");
sbRes.append(resource.getPercent());
sbRes.append("|");
sbRes.append(resource.isPrincipal() ? "Y" : "N");
resourcesMap.put(itask.getId(), sbRes.toString());
}
StringBuilder sb = new StringBuilder();
int i = 1, length = resourcesMap.keySet().size();
for (Long key : resourcesMap.keySet()) {
sb.append(resourcesMap.get(key));
if (i++ < length)
sb.append("_");
}
setResources(sb.toString());
IndividualTask.deleteAllExcept(getId(), new
ArrayList(ids));
}
public void moveTo(long parentId, Integer index) throws
BusinessException {
Assert.isTrue(index == null || index >= 0);
if (getParent() == null)
return;
if (getParent().getId().longValue() == parentId) {
int count = getParent().getChildCount();
if (count == 1)
return;
if (index == null && getIndex() == count - 1)
return;
if (index != null && index.intValue() == getIndex())
return;
}
Task parent = Task.get(parentId);
Task p = parent;
while (p != null) {
if (this.equals(p))
return;
p = p.getParent();
}
if (parent == null)
throw new BusinessException("父任務不存在。");
if (parent.getProjectId() != getProjectId())
throw new BusinessException("任務不在同一個項目中,不能移動。");
this.off();
if (!getParent().equals(parent)) {
this.setParent(parent);
if (this.getPeriodId() != parent.getPeriodId()) {
this.changePeriodTo(parent.getPeriodId(),
parent.getPeriodTag());
}
}
Task child = parent.getFirstChild();
if (child == null) {
parent.setFirstChild(this);
this.setNextSibling(null);
return;
}
int i = 0;
Task lastChild = null;
while (child != null) {
if (index != null && index.intValue() == i) {
if (index == 0)
parent.setFirstChild(this);
Task prev = child.getPrevSibling();
if (prev != null)
prev.setNextSibling(this);
this.setNextSibling(child);
return;
}
i++;
if (child.getNextSibling() == null)
lastChild = child;
child = child.getNextSibling();
}
this.setNextSibling(null);
lastChild.setNextSibling(this);
}
2.4.??? 隱藏數據訪問對象
這里使用數據訪問對象Dao分離數據訪問邏輯,并管理實體集合。在職責上,它同DDD中的Repository是同義的。這里叫Dao而沒有叫
Repository的主要原因是在實際項目中需要向很多人解釋這個并不算新的新概念是很麻煩的,所以就依然使用了Dao這個名稱。
無論是服務還是實體都需要使用其他實體或實體的集合來完成任務,如在服務中的方法:
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
public void moveUpTask(long taskId) throws BusinessException {
Task task = Task.get(taskId);
if (task == null)
throw new BusinessException("任務不存在。");
task.moveUp();
}
這里,要向上移動任務,必須先找到任務,再調用任務的移動方法。無論是查找單一任務實例還是集合,都最終需要使用Dao來完成。如果在這里直接使用Dao
來獲得實體,就必須先獲得Dao,這樣的代碼將會在很多地方出現,即麻煩又難看,所以將這類對集合操作或查找實體的操作封裝到實體當中。只是在邏輯上,這
樣的方法不屬于實體的任何一個實例,而是屬于類,所以全部采用靜態方法,如:
public class Task implements Entity {
……
public static Task get(long id) {
return dao().get(id);
}
public static Task findRoot(long projectId, long periodId, Integer
periodTag) {
return dao().findRoot(projectId, periodId, periodTag);
}
public static List findAll(QueryCriteria criteria, int
firstResult, int maxResults) {
return dao().findAll(criteria, firstResult, maxResults);
}
public static long count(QueryCriteria criteria) {
return dao().count(criteria);
}
private static TaskDao dao() {
return IocBeans.getInstance().getBean("taskDao", TaskDao.class);
}
}
所有這些靜態方法只是簡單的將職責委派給Dao來處理,這樣Dao就被隱藏在實體的背后,服務類看不到它,其他實體也看不到它,只有本實體類可以使用它。
這樣可以很大程度地簡化的領域模型實現。
2.5.??? 私有化實體屬性寫方法
對實體進行有效地封裝可以提高內聚性、降低耦合性。比較理想的情況下,我們追求最小暴露,即屬性和方法能私有就不設置為公共。這是一件很難在團隊中貫徹執
行事情,因為這會增加很多成本。這是一件我喜歡做但從不強求的工作,除了方法之外,對于屬性的寫方法我總會將其可見性設置為protected,如:
public class Comment implements Entity {
private Long id;
private Long taskId;
private Long personId;
private String comment;
private Date addedDate;
protected CommentDao obtainDao() {
return dao();
}
public Long getId() {
return id;
}
protected void setId(Long id) {
this.id = id;
}
public Long getTaskId() {
return taskId;
}
protected void setTaskId(Long taskId) {
this.taskId = taskId;
}
public Long getPersonId() {
return personId;
}
protected void setPersonId(Long personId) {
this.personId = personId;
}
public String getComment() {
return comment;
}
protected void setComment(String comment) {
this.comment = comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Date getAddedDate() {
return addedDate;
}
public void setAddedDate(Date addedDate) {
this.addedDate = addedDate;
}
……
}
對屬性的寫方法的限制訪問可以很大程度上降低來自服務類的對實體對象的誤操作,當然也需要付出比較高的成本。如在保存時,就需要這樣寫了
public void save(long taskId, long personId, String comment) {
setTaskId(taskId);
setPersonId(personId);
setComment(comment);
setAddedDate(DateUtils.today());
obtainDao().save(this);
}
2.6.??? 訪問其他組件
實體常常會訪問其他組件,這時就建立了實體同其他組件的耦合關系。但如果被引用的組件接口是相對穩定的,這一般也沒什么關系,如
public class Person implements Entity {
……
public Long save(String username, String password, String realName,
String email, String telephone, String mobileTelephone)
throws BusinessException {
Assert.notNull(realName, "Person的realName必須填寫。");
Assert.notNull(username, "Person的username必須填寫。");
if (!StringUtils.hasText(realName))
throw new BusinessException("用戶姓名必須包括有效字符。");
if (!StringUtils.hasText(username))
throw new BusinessException("用戶登錄名必須包括有效字符。");
SecurityExFacade securityExFacade =
SecurityExFacadeFactory.getInstance()
.createSecurityExFacade();
long userId = securityExFacade.createUser(username, password);
setUserId(userId);
setUsername(username);
setRealName(realName);
setEmail(email);
setTelephone(telephone);
setMobileTelephone(mobileTelephone);
// 檢查email是否已經存在?如果存在禁止新建人員。
if (obtainDao().exists(email))
throw new BusinessException("電子郵件地址已經存在。");
return (Long) obtainDao().save(this);
}
}
這里,在創建人員時,調用安全管理組件的接口來創建一個系統用戶。安全組件接口相對穩定,不會有太大的變化,因此實體在處理領域邏輯時,直接使用接口就可
以。但有的時候,被調用者并不是很穩定、或根本不知道被調用者是誰,這時就應該使用發布訂閱模式或者此種情境下的領域事件模式。還是那么幸
運,Spring為我們提供了這個模式的支持。使用這種模式我們可以對調用者和被調用者進行解耦。例如,在此處可以將黑色字體的代碼修改為:
UserCreatedEvent event = new UserCreatedEvent(username, password);
AppContext.getInstance().publishEvent(event);
long userId = event.getId();
在兩個組件的連接處編寫事件監聽器,
public class UserEventListener implements ApplicationListener {
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof UserCreatedEvent) {
SecurityExFacade securityExFacade =
SecurityExFacadeFactory.getInstance()
.createSecurityExFacade();
UserCreatedEvent e = (UserCreatedEvent) event;
long userId = securityExFacade.createUser(e.getUsername(), e
.getPassword());
e.setId(userId);
}
}
}
然后,在Spring的配置文件中加入如下內容,
這樣就可以以事件模式工作了,注意這里的事件模式是同步的。
領域事件模式可以用來解除耦合,但反對到處使用,如實體調用Dao或實體調用組件內的實體。
2.7.??? 使用數據傳輸對象
幾年來我一直在將實體轉化為DTO還是不轉DTO之間徘徊。不轉DTO好處是非常用明顯的,服務可以直接將查詢結果拋給表現層,這樣既減少代碼量,又降低
了數據復制過程中新建對象的開銷。但問題也是存在的,首先,組件間調用不轉DTO是不行的,因為如果不轉相當于將組件內部的細節給公開了,實體可以公開被
訪問,實體上的方法也可以被訪問,這樣的風險很大,也不利于組件間的解耦。那么組件內呢?其實實體也同樣暴露給了表現層,雖然是組件內,不同人開發不同層
時,這種問題也很嚴重。另外,實體也可能被表現層開發人員作為表現層模型的一部分,即在action中聲明一個屬性,這個屬性就是實體對象,表現層用它收
集頁面提交的數據或向頁面展示數據。這時各自需求不同很難協調。所以,最后決定除非小項目,否則一定引入DTO對象。實體在轉換成DTO時使用
Assembler,如
public final class CommentDetailAssembler {
private CommentDetailAssembler() {
}
public static CommentDetail toDetail(Comment comment) {
if (comment == null)
return null;
String personName = null;
Assert.notNull(comment.getPersonId());
PersonDetail person = OrgUtils.getPerson(comment.getPersonId());
if (person != null)
personName = person.getRealName();
return new CommentDetail(comment.getId(), comment.getTaskId(),
comment
.getPersonId(), personName, comment.getComment(), comment
.getAddedDate());
}
public static List toDetails(List
comments) {
Assert.notNull(comments);
List cts = new
ArrayList(comments.size());
for (Comment ct : comments) {
cts.add(toDetail(ct));
}
return cts;
}
}
以上是在使用領域模型過程的一些簡單總結,還有很多想說的,今天就寫這么多了。
總結
以上是生活随笔為你收集整理的java 域模型_基于Spring实现领域模型模式 - RUP实践者指南 - JavaEye技术网站的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美国总统,国务卿给做“广告”,黑莓手机想
- 下一篇: Java算法:牛客网腾讯笔试真题算法Ja