戲說領域驅動設計(廿三)——工廠


  在講解實體的章節中我們曾經介紹說過如何有效的創建實體,主要包括兩種方式:工廠和構造函數。本章我們工廠進行一下詳解,這種東西能有效的簡化實體使用的難度,畢竟你無法通過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);
    }
}

  上述的解決方案其實很簡單,您在使用的時候完全可以使用不同的方式。我之所以特意提出是因為在真實的項目中經常會有這樣的問題而且你繞不開。咱寫這一系列文章當然不能別人寫什么我就寫什么,我喜歡把現實中自己遇到的一些問題都拋出來,為解決問題提供一種思路。當然了,代碼肯定不是真實的,是因為我故意為之,想通過一些大家喜聞樂見的案例把思想描繪清楚。如果貼一些項目代碼,由於您沒有需求背景,反而為學習增加了負擔。

總結

  本章主要講解了工廠,不用提它是否能對應統一語言,僅就能簡化領域模型的創建你就值得擁有。着重說明一句,工廠是一種可有可無的組件,具體視您的領域模型的復雜度。實踐中,基本上一個聚合都會有一個工廠對應的,畢竟能夠成為實體的東西其構造過程也簡單不了。

 

附一:本節寫得不好,可能是受工作影響比較大,心態不太理想。無論你多么努力與追求上進,面對權力時不得不進行妥協。本來想踏實的做一些東西,奈何樹欲靜而風不止,可悲。雖說“人有凌雲之志非運不能騰達”,不過這個運到底什么時候到來????

附二:本節討論了工廠的三個模式及在實踐中要如何的使用。其中三個模式是要被重點關注的內容,您需要根據不同的情況選擇合格的那一種。那么這三個模式到底有什么區別以及最合適的用應用場景是什么呢?大概說明一下。1)實體包含工廠方法:一般不用於構造當前的實體,把自身的創建過程放到實體中會增加實體的復雜性。當你使用了領域事件的時候則強烈推薦這種模式。一般情況下,事件組成的主體信息來自實體的內部,所以由某個方法執行后再創建一個事件更顯得更優雅,這個也就是我們常聽說的知識專家。2)實體子類作為工廠,如果在實體的創建過程中涉及某些私有方法的調用,這種方式當然更好。再說了,你不想把讓實體承擔更多的責任又想把實體的創建過程進行封裝,怎么着也得建一個工廠對象來完成這個事情,既然繼承的方式能讓工廠存在更多的擴展能力,為什么不用呢?所以這種方式一般用於實體自身的創建。3)業務服務類作為工廠。首先,這種模式一般用於實體的創建而非反序列化;第二,其創建的實體一般是其它BC的聚合到當前BC的投影,比如上例中說的銷售BC中的產品映射到訂購BC中的商品。注意,這里的BC是廣義的,不僅僅是某個服務,也可能是某個包或名稱空間。這類實體的創建,其信息來自外部,你不可能放到某個實體的方法中,也不可能使用繼承機制,肯定要使用第三個模式了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM