<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          領(lǐng)域驅(qū)動設(shè)計 DDD 簡介

          共 9299字,需瀏覽 19分鐘

           ·

          2021-02-08 18:24

          本篇文章屬于《領(lǐng)域驅(qū)動設(shè)計》系列的第一篇文章。本系列教程將會梳理領(lǐng)域驅(qū)動的各種關(guān)鍵概念,并采用 Spring Data Jpa 進行一個領(lǐng)域驅(qū)動設(shè)計的 Sass 軟件的實踐。

          軟件是為了幫助我們處理現(xiàn)代生活中復雜問題而創(chuàng)建的工具,DDD (Domain-Driven Design)是用來設(shè)計軟件的一種設(shè)計方法。在軟件設(shè)計之初,我們就需要定義軟件要解決某一場景的問題,DDD 中稱該業(yè)務(wù)場景為領(lǐng)域(Domain)。

          通常技術(shù)人員對新軟件項目的領(lǐng)域知識是不了解的,懂得該領(lǐng)域業(yè)務(wù)知識的人在 DDD 中稱為領(lǐng)域?qū)<遥ㄍǔJ擒浖O(shè)計者,也可能是銷售、技術(shù)支持等收集業(yè)務(wù)需求的人),技術(shù)人員與領(lǐng)域?qū)<抑g需要使用領(lǐng)域通用語言(Ubiquitous Language)進行業(yè)務(wù)溝通,領(lǐng)域通用語言可以是 UML、需求文檔等提前規(guī)定好的雙方都能理解的語言。

          領(lǐng)域驅(qū)動設(shè)計 DDD 的最終目的是得到軟件設(shè)計遵從的領(lǐng)域模型(Domain Model),領(lǐng)域模型是關(guān)于特定業(yè)務(wù)領(lǐng)域的軟件模型,通常使用程序中的對象模型實現(xiàn),對象的數(shù)據(jù)和行為準確表達該領(lǐng)域的業(yè)務(wù)含義。

          DDD 可以為團隊開發(fā)帶來什么?

          使用 DDD 可以使業(yè)務(wù)人員向技術(shù)人員準確地傳遞業(yè)務(wù)規(guī)則。業(yè)務(wù)人員與技術(shù)人員溝通變得更容易。

          業(yè)務(wù)知識也是需要時間進行學習的,DDD 可以幫助對業(yè)務(wù)知識進行集中,這樣可以保證軟件業(yè)務(wù)知識不止掌握在少數(shù)人手中。

          DDD 提煉出的領(lǐng)域模型使業(yè)務(wù)精簡明確,易于理解學習和后期維護,開發(fā)上手快,人員可替代性強。

          DDD 基礎(chǔ)

          在我們進入 DDD 之前我們先分析一下當前常用的開發(fā)方式(Java 為例)。當前大多數(shù) Java 開發(fā)工程師采用貧血模型進行開發(fā),我們會解釋什么是貧血模型,它與充血模型有什么區(qū)別。

          貧血模型

          貧血模型(anaemic domain model)是指使用的領(lǐng)域?qū)ο笾兄挥?setter 和 getter 方法(POJO),所有的業(yè)務(wù)邏輯都不包含在領(lǐng)域?qū)ο笾卸欠旁跇I(yè)務(wù)邏輯層 Service 中。貧血模型,目前絕大多數(shù)開發(fā)者都采用的這種模式進行開發(fā)。

          貧血模型是不包含任何邏輯的領(lǐng)域模型,它只是客戶端可以更改和解釋的數(shù)據(jù)的容器。所有邏輯都放在貧血模型中的域?qū)ο笾狻?/p>

          下面的例子使用訂單類來演示貧血模型

          public class Order {
          private BigDecimal total = BigDecimal.ZERO;
          private List items = new ArrayList();

          public BigDecimal getTotal() {
          return total;
          }

          public void setTotal(BigDecimal total) {
          this.total = total;
          }

          public List getItems() {
          return items;
          }

          public void setItems(List items) {
          this.items = items;
          }
          }

          public class OrderItem {
          private BigDecimal price = BigDecimal.ZERO;
          private int quantity;
          private String name;

          public BigDecimal getPrice() {
          return price;
          }

          public void setPrice(BigDecimal price) {
          this.price = price;
          }

          public int getQuantity() {
          return quantity;
          }

          public void setQuantity(int quantity) {
          this.quantity= quantity;
          }

          ...

          }

          用于計算訂單總量的貧血域服務(wù)可能如下所示

          public class OrderService {

          public void calculateTotal(Order order) {
          if (order == null) {
          throw new IllegalArgumentException("order must not be null");
          }

          BigDecimal total = BigDecimal.ZERO;
          List items = order.getItems();

          for (OrderItem orderItem : items) {
          int quantity = orderItem.getQuantity();
          BigDecimal price = orderItem.getPrice();
          BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
          total = total.add(itemTotal);
          }
          order.setTotal(total);
          }
          }

          貧血領(lǐng)域模型更改和解釋數(shù)據(jù)的邏輯放在其他地方。大多數(shù)情況下,邏輯放在 xxxService 中。存放 xxxService 的包叫做服務(wù)層,服務(wù)層定義應用程序的邊界,建立一組可用的操作并協(xié)調(diào)每個操作中應用程序的響應。從架構(gòu)的角度來看,將業(yè)務(wù)邏輯保持在貧血模型中的“服務(wù)”是事務(wù)腳本。

          事務(wù)腳本:可以將大多數(shù)業(yè)務(wù)應用程序視為一系列事務(wù),事務(wù)腳本主要將所有業(yè)務(wù)邏輯組織為一個過程,按過程組織業(yè)務(wù)邏輯,其中每個過程處理一種請求。


          計算訂單總價的貧血模型服務(wù) Service 如下

          public class OrderService {

          public void calculateTotal(Order order) {
          if (order == null) {
          throw new IllegalArgumentException("order must not be null");
          }

          BigDecimal total = BigDecimal.ZERO;
          List items = order.getItems();

          for (OrderItem orderItem : items) {
          int quantity = orderItem.getQuantity();
          BigDecimal price = orderItem.getPrice();
          BigDecimal itemTotal = price.multiply(new BigDecimal(quantity));
          total = total.add(itemTotal);
          }
          order.setTotal(total);
          }
          }

          你可能認為這很清晰簡單,因為它是過程編程。但它也很致命,因為貧血模型永遠無法保證其正確性。貧血模型沒有邏輯來確保它隨時處于合法狀態(tài)。例如,訂單對象沒有對其項目列表的更改做出反應,因此無法更新其總數(shù),需要手動處理。

          對象將數(shù)據(jù)和邏輯結(jié)合起來,而貧血的模型則將它們分開。這與與基本的面向?qū)ο笤恚ɡ绶庋b,信息隱藏)相矛盾。

          下面例子說明了為什么貧血模型無法保證合法狀態(tài)

          public class OrderTest {

          /**
          * 貧血模型可能出現(xiàn)不一致的情況,因為它不處理狀態(tài)更改。
          *
          * 使用貧血模型時開發(fā)者必須知道對象操作應該轉(zhuǎn)變?yōu)槭裁礌顟B(tài),
          * 手動變化合適狀態(tài),以保證狀態(tài)合法
          *
          */
          @Test
          public void anAnemicModelCanBeInconsistent() {
          OrderService orderService = new OrderService();
          Order order = new Order();
          BigDecimal total = order.getTotal();

          /*
          * 訂單沒有訂單項,因此訂單總價一定為 0
          */
          assertEquals(BigDecimal.ZERO, total);

          OrderItem aGoodBook = new OrderItem();
          aGoodBook.setName("Domain-Driven");
          aGoodBook.setPrice(new BigDecimal("30"));
          aGoodBook.setQuantity(5);

          /*
          * 在這里我們打破了對象封裝,因為我們更改了訂單項目列表的內(nèi)部狀態(tài)
          * 這是貧血模型的常見編程模式
          */
          order.getItems().add(aGoodBook);

          /*
          * 當我們修改了訂單項,Order 對象就處于非法狀態(tài),我們需要手動修改訂單狀態(tài)
          */
          BigDecimal totalAfterItemAdd = order.getTotal();
          BigDecimal expectedTotal = new BigDecimal("150");

          boolean isExpectedTotal = expectedTotal.equals(totalAfterItemAdd);

          /*
          * 當然,訂單總數(shù)不能是預期的總數(shù),因為貧血模型不能處理它們的狀態(tài)變化。
          */
          assertFalse(isExpectedTotal);

          /*
          * 要解決這個問題,我們必須調(diào)用OrderService來重新計算總數(shù),并使Order對象再次處于合法狀態(tài)。
          */
          orderService.calculateTotal(order);

          /*
          * 現(xiàn)在,該 Order 對象再次處于合法狀態(tài)
          */
          BigDecimal totalAfterRecalculation = order.getTotal();
          assertEquals(expectedTotal, totalAfterRecalculation);
          }
          }

          貧血模型中數(shù)據(jù)的解釋是由無狀態(tài)服務(wù)完成的。雖然服務(wù)是無狀態(tài)的,但它不知道何時該執(zhí)行邏輯,何時不執(zhí)行,并且無狀態(tài)服務(wù)無法緩存其計算出的值。而充血域?qū)ο髸詣犹幚砥錉顟B(tài)更改,知道何時必須重新計算屬性的值。

          貧血領(lǐng)域模型不是面向?qū)ο蟮木幊蹋?strong>貧血模型是過程性編程

          在早期編程時期,Order 示例實現(xiàn)如下:

          struct order_item { 
          int amount;
          double price;
          char *name;
          };

          struct order {
          int total;
          struct order_item items[10];
          };

          int main(){
          struct order order1;
          struct order_item item;
          item.name = "Domain-Driven";
          item.price = 30.0;
          item.amount = 5;
          order.items[0] = item;
          calculateTotal(order1);
          }

          void calculateTotal(order o){
          int i, count;
          count = 0;
          for(i=0; i < 10; i++) {
          order_item item = o.items[i];
          o.total = o.total + item.price * item.amount;
          }
          }

          使用貧血模型意味著使用過程編程。過程式編程很簡單,但是理解應用程序的狀態(tài)處理很難。此外,貧血模型將狀態(tài)處理和數(shù)據(jù)解釋的邏輯移到客戶端,這通常會導致代碼重復或產(chǎn)生非常細粒度的服務(wù)。最終產(chǎn)生了大量的服務(wù)和服務(wù)方法,這些服務(wù)和方法在一個廣泛而復雜的對象網(wǎng)絡(luò)中相互連接,這是我們很難找出一個物體處于某種狀態(tài)的原因。要找出對象處于某種狀態(tài)的原因,就必須找到對象所經(jīng)過的方法調(diào)用層次結(jié)構(gòu)。

          充血模型

          與貧血領(lǐng)域模型相反,充血模型遵循面向?qū)ο蟮脑瓌t。因此,充血模型實際上是面向?qū)ο蟮木幊獭3溲P突蛎嫦驅(qū)ο缶幊痰哪康氖菍?shù)據(jù)和邏輯結(jié)合在一起。

          面向?qū)ο笠馕吨阂粋€對象管理它的狀態(tài),并保證它在任何時候處于合法的狀態(tài)。貧血模型的 Order 類可以很容易地轉(zhuǎn)換為面向?qū)ο蟮陌姹尽?/p>

          public class Order {

          private BigDecimal total;
          private List items = new ArrayList();

          /**
          * 返回訂單總價
          */
          public BigDecimal getTotal() {
          if (total == null) {
          /*
          * 必須計算總數(shù)并保存結(jié)果
          */
          BigDecimal orderItemTotal = BigDecimal.ZERO;
          List items = getItems();

          for (OrderItem orderItem : items) {
          BigDecimal itemTotal = orderItem.getTotal();
          //獲取某一訂單項的總價
          /*
          * 將每個 OrderItem 的總價加到我們的總數(shù)中。
          */
          orderItemTotal = orderItemTotal.add(itemTotal);
          }
          this.total = orderItemTotal;
          }
          return total;
          }

          /**
          * 添加 OrderItem 到 Order
          */
          public void addItem(OrderItem orderItem) {
          if (orderItem == null) {
          throw new IllegalArgumentException("orderItem must not be null");
          }
          if (this.items.add(orderItem)) {
          /*
          * 訂單項的列表發(fā)生了變化,因此我們將 total 字段重置為 null,
          * 讓 getTotal 重新計算 total。
          */
          this.total = null;
          }
          }

          /**
          *
          * 返回 Order 中的所有 OrderItem ,客戶端不能修改返回的 List
          */
          public List getItems() {
          /*
          * 我們對訂單項進行封裝,以防止客戶操縱我們的內(nèi)部狀態(tài)。
          */
          return Collections.unmodifiableList(items);
          }

          }

          import java.math.BigDecimal;

          public class OrderItem {
          private BigDecimal price;
          private int quantity;
          private String name = "no name";

          public OrderItem(BigDecimal price, int quantity, String name) {
          if (price == null) {
          throw new IllegalArgumentException("price must not be null");
          }
          if (name == null) {
          throw new IllegalArgumentException("name must not be null");
          }
          if (price.compareTo(BigDecimal.ZERO) < 0) {
          throw new IllegalArgumentException(
          "price must be a positive big decimal");
          }
          if (quantity < 1) {
          throw new IllegalArgumentException("quantity must be 1 or greater");
          }
          this.price = price;
          this.quantity = quantity;
          this.name = name;
          }

          public BigDecimal getPrice() {
          return price;
          }

          public int getQuantity() {
          return quantity;
          }

          public String getName() {
          return name;
          }

          /**
          * total = getPrice() * getAmount() 此處的 total 為訂單項總價
          */
          public BigDecimal getTotal() {
          int quantity = getQuantity();
          BigDecimal price = getPrice();
          BigDecimal total = price.multiply(new BigDecimal(quantity));
          return total;
          }
          }

          面向?qū)ο缶幊痰膬?yōu)點是,對象可以保證在任何時候都處于合法的狀態(tài),并且不再需要服務(wù)類。測試用例將顯示與貧血模型的差異,貧血模型不能保證它們在任何時候處于合法狀態(tài)。

          public class OrderTest {

          /**
          * 這個測試表明,充血模型模型保證它在任何時候都處于合法狀態(tài)*。
          */
          @Test
          public void richDomainModelMustEnsureToBeConsistentAtAnyTime() {
          Order order = new Order();
          BigDecimal total = order.getTotal();

          /*
          * 新訂單沒有項目,因此總金額必須為零。
          */
          assertEquals(BigDecimal.ZERO, total);

          OrderItem aGoodBook = new OrderItem(new BigDecimal("30"), 5,
          "Domain-Driven");
          List items = order.getItems();

          try {
          items.add(aGoodBook);
          } catch (UnsupportedOperationException e) {
          /*
          * 我們不能破壞封裝,因為 order 對象不會向客戶端公開它的內(nèi)部狀態(tài)。
          * 對象關(guān)心它自己的狀態(tài),并確保自己在任何時候都處于合法狀態(tài)。
          */
          }

          /*
          * 我們必須使用對象暴露的添加方法
          */
          order.addItem(aGoodBook);

          /*
          * 在添加了 OrderItem 之后。該對象仍然處于合法狀態(tài)。
          */
          BigDecimal totalAfterItemAdd = order.getTotal();
          BigDecimal expectedTotal = new BigDecimal("150");

          assertEquals(expectedTotal, totalAfterItemAdd);
          }
          }

          使用哪個模型

          應用程序應該盡可能多地使用面向?qū)ο蟮姆椒?。面向?qū)ο缶幊痰膬?yōu)點是對象可以保證它在任何時候都處于合法的狀態(tài)。如果您想要保證整個應用程序始終處于合法(預期)的狀態(tài),就需要使用充血模型。

          大多數(shù)工作中常見需求都是 CRUD,POJO 需要經(jīng)常修改,數(shù)據(jù)通過 json 在各個模塊流動。在這種背景下,應用的擴展往往是橫向的擴展,表的字段增加或者聯(lián)表,而非抽象關(guān)系的擴展,推薦使用貧血模型。

          充血模型可能更適合復雜且穩(wěn)定的業(yè)務(wù)領(lǐng)域。

          每種方法論都有自己的局限性和適用范圍。?從一個貧血模型開始可能很容易,但是以后將應用程序重構(gòu)為一個充血領(lǐng)域模型體系結(jié)構(gòu)可能會很麻煩并且容易失敗。

          總結(jié)

          DDD 的基礎(chǔ)就是充血模型,本篇文章我們先了解充血模型的使用方式,后續(xù)文章會逐漸解析 DDD 的各個概念,并給出代碼實踐。

          參考資料

          • 《領(lǐng)域驅(qū)動設(shè)計:解決軟件核心的復雜性》,Eric Evans著

          • Anemic vs. Rich Domain Models



          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  囯产精品久久久久久久久久98 | 日韩一区二区三区视频在线观看 | 老司机无码视频 | 黄片欧美在线观看 | 激情人妻网站 |