在講解物體的章節中我們曾經介紹說過如何有效的創建物體,主要包括兩種方式:工廠和建構式,本章我們工廠進行一下詳解,這種東西能有效的簡化物體使用的難度,畢竟你無法通過Spring這種容器來管理領域物件,實際的開發程序中,工廠的使用要比書中的講解會復雜一點,所以在本章我會對實踐中遇到的一些問題以及使用什么樣的模式去應對給出一些建議,
一、工廠的作用
學習過設計模式的人都應該知道“工廠模式”,尤其是其中的“簡單工廠”,感覺就沒什么可學的,太簡單了,但在DDD中,工廠卻比較常用,不過也正像書上說的一樣,其實算不上一等公民,畢竟其承擔的責任只是物體的創建,有點偏技術,但反過來說,少了這么一個東西還真不行,有些物體的創建起來很費勁,大部分情況下只有物體設計人能完全搞定,出現了知識壟斷的情況,可是在真實的作業中,我們需要團隊協作,也會出現人員更迭的情況,出現這種壟斷并不是什么好事兒,此外,作為設計者,讓自己研發出來的東西特別難以使用,這本身其實是失敗的,看看Spring框架,你就知道人家工程師的牛掰之處了,咱不管其內部如何復雜,你就告訴我使用起來是不是很方便吧?我這里有個小經驗與大家分享:不論是做后臺的代碼還是前端的功能,都把自己假設成為用戶,你就會在設計程序中自然而然的考慮易用性和安全性了,當然,也不排除有些不愿意思考的人,不過是自廢前程而矣,將自己當成用戶還有另外一個好處:之所以叫用戶,就代表你不能對他做任何假設,只要你提供出去功能就代表是可用的,把自己當成用戶正好可以檢驗代碼中是否存在不妥之處,之前我們說過物體的不變條件,當把客戶作為不可信任物件看待的時候,你就會在設計程序中增加約束來避免破壞不變性的情況出現,
扯扯就遠了,看看下面這段代碼,這是我在實際的專案中所設計的一個物體,前面我曾經說過,物體中必須包含一個可以讓所有屬性得到有效賦值的建構式,因為保障它的完整性和不變條件是在物體設計程序中需要遵守的重要原則,
public class DeploymentApprovalForm extends ApprovalFormBase { DeploymentApprovalForm(Long id, String name, ApplierInfo applierInfo, LocalDateTime createdDate, LocalDateTime updatedDate, List<ApprovalNodeBase> nodes, LocalDateTime deploymentDate, ProcessStatus status, PhaseType currentPhase, String service, ApplyType applyType) { super(id, name, applierInfo, createdDate, updatedDate, nodes); if (status != null) { this.status = status; } this.deploymentDate = deploymentDate; if (currentPhase != null && currentPhase != PhaseType.UNKNOWN) { this.currentPhase = currentPhase; } this.changeService(service); this.applyType = applyType; if (applyType == null || applyType == ApplyType.UNKNOWN) { this.applyType = ApplyType.FORMAL; } } }
我如果直接把這樣的設計給其它程式員使用,保準被罵爹!這個物件的構造太復雜了,你需要了解每一個引數是如何構造了,簡單型別還好,其中還包含了許多的值物件,使用人需要了解每一個值物件的構造方式和理,別跟我說使用Spring 的IoC,這可是領域物件,其實也不是故意要寫成這樣,業務復雜的情況物體也不可能簡單了,要不然誰還用OOP,整個面向程序不是挺香的嗎?您其實不需要考慮上述代碼是什么含義,只需要關注其建構式即可,之所以給出這段代碼,是想向您證明我們本章的主題:雖然工廠不是一等公平,但不代表其不重要,當然了,你可能會抬杠說沒有工廠就不能創建物件了?也不是不行,成本高啊,如果這段代碼是別人寫的,現在你要用,我就問你是不是得問對方怎么搞,沒人可問的話你是不是需要自己把代碼都看一遍?一個物體這樣干可以,十個呢?百個呢?這不是作業,是自虐!針對上述代碼,您可能還會說可以使用視圖模型作為引數,相當于把建構式作為工廠來使用,這種情況下的確可以隱藏物件創建細節,不過領域模型主要是用于為某個業務的執行進行支撐,過重的建構式從另一方面又增加了其責任,另外就是代碼量很大,反正我覺得這樣做不好,單一責任原則其實是值得遵守的,
回歸正題,對于上面的反例,相信在此刻我根本不需要再解釋引入工廠的好處,事實已經證明了,這樣的場景我相信您在實踐中肯定遇到過,而且不會少,那么要如何使用工廠,請繼續跟著我的腳步前行,
二、工廠使用模式
工廠模式的使用有三種,您可別一見到工廠就以為需要創建一個“*Factory”的類,這種方式的確比較常用,但并不是全部,不同的場景需要使用不同的方法,畢竟我們考慮問題的時候不能太過于狹隘,實作情況還是很復雜的,
1、物體包含工廠方法
一種經常被使用的方式是在物體中加入用于創建該物體的靜態方法,如下面代碼片段所示,在物體不那么復雜的情況下,這種方式其實可以接受,雖然說這樣會造成物體承擔了過多的責任,不過在實踐中有些模棱兩可的規則是可以打破,您完全可以新建一個單獨的類,責任雖然單一了,可又多了一個類檔案,維護起來也是需要成本的,
public class Order extends EntityModel<Long> { private String name; public static Order create(OrderVo orderInfo) { …… } }
另外一種方式是通過物體中的業務方法創建另外的物體,這種方法最常見于領域事件的創建,如下代碼片段所示,此種方式所帶來的好處是其有效的表達出了所謂的通用語言,直白來說就是反應了業務術語,我早期寫代碼的時候謹遵一個模式:命令型方法無回傳值,我記得應該是在《代碼大全》中有過類似的說明,所以遇到需要使用事件的場景,都是在應用服務中進行構造,近兩年則使用類似下面這種方式,這代碼看起來多么優雅,所以各位看君切莫像我一樣陷入教條主義,
public class Order extends EntityModel<Long> { private OrderStatus staus; public OrderPaid pay(Money fee) { this.status = OrderStatus.PAID; return new OrderPaid(this.getId()); } }
什么?你懷疑我水文字,上述的案例看不出來哪里反應了通用語言?較勁唄?那我就再整一個,我曾經設計過一個類似作業流的東西,叫作“業務申請單”,你也不管到底申請什么的,反正有申請就會涉及到審批,需求中說明“每次審批的操作都需要記錄操作結果,用戶可以查看某個審批單的所有操作記錄”,下面為部分代碼的片段,通過示例您可以看到“ApprovalFormBase”物體的“approve”方法在業務執行完結后回傳一個“審批記錄”物體,這里它不僅承擔了工廠的作用,也表達了業務意圖,說到這份兒應該不能算是水文字了吧?
public abstract class ApprovalFormBase extends EntityModel<Long> { private ApprovalNodeGroup nodeGroup = new ApprovalNodeGroup(); public ApprovalRecord approve(Advice advice) throws ApprovalFormOperationException { this.throwExceptionIfTerminatedOrInvalidated(); if (advice == null) { throw new ApprovalFormOperationException(OperationMessages.INVALID_APPROVAL_INFO); } …… return this.nodeGroup.approve(approvalContext, advice); } }
2、物體的子類作為工廠
這種方式在本系列的第十六章中介紹過,相對來說也比較優雅,雖然多出來一個新的檔案,方便起見,我還是把代碼再貼一下并稍微多做一些解釋,“Order”代碼中,我將其建構式設計為“protected”,這樣就可以限制住不經過工廠而創建其實體的情況,另外,這種方式也可以讓您在工廠類中呼叫一些父類的方法,實踐中此等應用場景并不多見,因為工廠的職責只能用于物體的實體化不應承擔業務規則,不過也讓我們在開發作業中遇到某些需要抉擇的場景時多了一個選擇,
public class Order extends EntityModel<Long> { private String name; private Contact contact; protected Order(Long id, String name, Contact contact) throws OrderCreationException { super(id); this.name = name; this.contact = contact; } } final public class OrderFactory extends Order { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } Contact contact = new Contact(orderInfo.getEmail(), orderInfo.getName()); return new Order(0L, orderInfo.getName(), contact); } }
3、業務服務類作為工廠
業務服務類作為工廠其實類似于上面的工廠子類,只是這種工廠并不會從某個物體繼承,這種方式其實在實踐中比較常用,因為夠直觀,雖然我們通常會采用“*Factory”這樣的命名方式,但其本質上是一個領域服務(回想一下領域服務的使用規則),通常情況下,我們工廠服務存在兩個使用模式:一是簡單領域物體工廠,此種模式使用方式簡單明了,一目了然,請參看如下代碼,此處請您務必注意一下,下面的代碼片段僅僅是為演示用,真實的場景下代碼相對要復雜一點,本章后面部分我會著重以此說明;工廠服務另外的一個模式使用起來簡單,不過其具備較強的業務含義,下一節我會對此做詳細解釋,不過在繼續之前,我們給下面這種工廠一個名字以方便后面參考,就叫其為“物體工廠”吧,
final public class OrderFactory{ public final static OrderFactory INSTANCE = new OrderFactory(); private OrderFactory() { } public Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } Contact contact = new Contact(orderInfo.getEmail(), orderInfo.getName()); return new Order(0L, orderInfo.getName(), contact); } }
工廠服務的第二個模式在命名上一般不會使用“*Factory”模式,而是使用“*Service”代替之,其包含的創建型方法基本上只用于構造新的物件;而“物體工廠”除了此項責任外還會用于物體資料反序列化后的構造,為方便起見,我們給第二個模式所描述的工廠一個新的名稱“工廠服務”,下面我們來著重介紹一下“工廠服務”的使用,
舉一個例子更能說明問題,這個業務很簡單:訂單項需要包含要購買的商品資訊,通過名字您可以看出來“訂單項”與“商品”肯定屬于兩個不同的限界背景關系:一個是訂單BC,一個是銷售品BC,兩個限界背景關系間只能通過什么物件來傳遞資訊來著?“視圖模型”,千萬別忘了,訂單項是一個領域模型,從銷售品限界背景關系傳過來的資訊是一個視圖模型,這兩個物件不能放在一起,這個應該不會有疑問吧?此外,銷售品域中的銷售品資訊屬性非常多比如“規格”、“生產廠商”、“質量保證資訊”等,但傳到訂單域后也就一兩種是被使用的,您也見天兒在淘寶或京東買東西,沒見訂單項中包含生產廠家、詳細規格等資訊吧?這些根本就不是訂單項所關注的內容,它所在意的是:產品名稱、價格,假如我們在深入想一想,你所買的東西在銷售品域中其實不能被稱之為“商品”的,它還沒被銷售出去,叫商品不合適;而到了訂單域后,它已經被訂購了,此刻才能真正的被稱之為商品,當然了,“商品”也好、“銷售品”也好,叫什么聽領域專家的,這是人為的規定,案例中的叫法也只是為了演示效果,其實類似的例子我在前面已經舉過,即“訂單和客戶資訊的領域模型設計”,之所以再拿出來說明,是想讓您在設計程序中要注意通用語言的使用以及從始至終都通過業務來驅動領域模型設計的作業思路,其實通用語言這個概念挺虛的,您只需要遵守如下原則:在設計程序中仔細考慮領域模型的命名,這個命名一旦在溝通中使用,大家就會明白其具體指向的是什么;通過閱讀代碼也能知曉某個物體所指代的領域物件,對于上面的需求,我們的代碼可以寫成下面這樣,
final public class GoodsCreatorService { public final static GoodsCreator INSTANCE = new GoodsCreator(); private GoodsCreator() { } public List<Goods> create(List<ProductVO) products) { return products.stream() .map(e -> new Goods(e.getName(), e.getID())) .collect(Collectors.toList()); } }
在上面的代碼中,“create”方法的引數“products”由應用服務呼叫銷售品BC配接器獲取并傳入到“GoodsCreatorService”中,請務必別忘了這是一個領域服務,不要讓其直接呼叫基礎設施層的配接器,
三、物體工廠實踐
我特意把“物體工廠”的設計提取出來,是因為在實踐中需要關注工廠的構建方法所適用的場景,并不是只有一個如“create”或“build”方法就能搞定的,前面我們說過,物體的創建有兩個場景:一是根據外部資訊從無到有的創建;二是根據資料庫資訊反序列化,雖然本質上都是進行物體的創建,但由于場景不同,其實作思路也不一樣,讓我們仔細的說,
新建物體時我們有時會根據業務需要硬性的給某個物體屬性一個默認值;構建程序中如果外部資訊不全,我們也可能需要給其某個屬性一個默認值,比如下面的代碼片段,這段代碼展示了:1)新建訂單時將其狀態強制設定為“待支付”;2)“是否需要發票”屬性如未在引數中包含資訊則默認為“否”,這段代碼看起來沒有錯誤,但不能用于物體反序列化時,否則每次從資料庫反序列化后訂單的狀態都是“待支付”,物體序列化后必然會涉及反序列化的程序,除非你只序列一次,那不就成了日志了嗎?
final public class OrderFactory { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } status = OrderStatus.WAIT_PAY; boolean needFapiao = false; if (orderInfo.needFapiao() != null) { needFapiao = true; } return new Order(0L, status, needFapiao); } } public enum OrderStatus { public static OrderStatus of(Integer status) { if (status == null) { return OrderStatus.UNKNOWN } } }
我其實等著您回懟呢,你可能會說“你這代碼是騙人的,我可以首先判斷傳入的狀態資訊是否為空,為空時我再設定默認值;不為空我就使用傳入的值”,也就是下面這段代碼,其實這段代碼才會有潛在的問題:如果某個工程師手欠,把資料庫中訂單“狀態”列的值變成了“null”,這種訂單從資料庫反序列化后會出現什么結果?實際上從資料的層面來看已經違反了業務的約束,這種物件在創建程序中應該報錯,但如果按下面代碼的方式,往小了看是一個Bug,往大了看可能會引發更多的賬務問題或投訴,實踐中,如果物件屬性多、創建復雜時,創建程序可能會引發比較大的問題,看得到的還能及時處理,那些潛在的問題才是致命的,此等情況下簡單的使用上面的物體工廠肯定不行,親愛的螢屏前的您,何解?
final public class OrderFactory { public static Order create(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } OrderStatus status = OrderStatus.of(orderInfo.getStatus()); if (status == OrderStatus.UNKNOWN) { status = OrderStatus.WAIT_PAY; } return new Order(0L, status, needFapiao); } } public enum OrderStatus { public static OrderStatus of(Integer status) { if (status == null) { return OrderStatus.UNKNOWN } } }
在說出答案前我其實挺想展示一下在實際專案中工廠方法的復雜度的真實情況,不過貼出這些案例反而會影響我們敘述的思路,所以我先針對上述的問題給出解決方案:既然創建物件會出現在兩個場景中即新建和加載,而我們期望物體的創建不論針對哪種場景最好都通過一個工廠來完成,那我們就索性為每個場景都創建一個單獨的方法并統一放到一個工廠物件中,如下代碼所示,這是一個物體工廠的基類,我們定義了兩個用于物體創建的方法,當然,您也可以根據需要決策是否建立這樣的基類,因為我們更強調思想的正確,
public abstract class EntityFactoryBase<TEntity extends EntityModel, TParameter extends VOBase> { protected abstract TEntity create(TParameter modelInfo) throws OrderCreationException; protected abstract TEntity load(TParameter modelInfo) throws OrderCreationException; }
別震驚啊,就這么簡單,這里唯一的約束是:你在創建或從持久化設施加載領域物體的時候,引數應該是“視圖模型”,因為工廠主要就是為了應對復雜場景而存在的,你構造一個物件就三個引數,要毛線的工廠啊,方法的實作我不給代碼了,“create”和前面的示例一樣,可做一些初始化或默認值的作業;“load”方法,根據傳入的引數(這些引數來源于持久化設施,查詢出來后將資料模型轉換為視圖模型),不做任何的默認值設定,要不還是寫一下“load”吧,免得您說我只打嘴炮兒,
final public class OrderFactory extends EntityFactoryBase<Order, OrderVO> { public final static OrderFactory INSTANCE = new OrderFactory(); public Order load(OrderVO orderInfo) throws OrderCreationException { if (orderInfo == null) { throw new OrderCreationException(); } //代碼省略 return new Order(0L, orderInfo.getStatus()); } } public class OrderRepository { private OrderMapper orderMapper; public Order findBy(Long id) { OrderDataEntity entity = this.orderMapper.getById(id); OrderVO orderInfo = OrderVO.of(entity); return OrderFactory.INSTANCE.load(orderInfo); } }
上述的解決方案其實很簡單,您在使用的時候完全可以使用不同的方式,我之所以特意提出是因為在真實的專案中經常會有這樣的問題而且你繞不開,咱寫這一系列文章當然不能別人寫什么我就寫什么,我喜歡把現實中自己遇到的一些問題都拋出來,為解決問題提供一種思路,當然了,代碼肯定不是真實的,是因為我故意為之,想通過一些大家喜聞樂見的案例把思想描繪清楚,如果貼一些專案代碼,由于您沒有需求背景,反而為學習增加了負擔,
總結
本章主要講解了工廠,不用提它是否能對應統一語言,僅就能簡化領域模型的創建你就值得擁有,著重說明一句,工廠是一種可有可無的組件,具體視您的領域模型的復雜度,實踐中,基本上一個聚合都會有一個工廠對應的,畢竟能夠成為物體的東西其構造程序也簡單不了,
附:本節寫得不好,可能是受作業影響比較大,心態不太理想,無論你多么努力與追求上進,面對權力時不得不進行妥協,本來想踏實的做一些東西,奈何樹欲靜而風不止,可悲,雖說“人有凌云之志非運不能騰達”,不過這個運到底什么時候到來????
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/458573.html
標籤:其他
上一篇:戲說領域驅動設計(廿三)——工廠
下一篇:排查線上問題的9種方式
