主頁 > 軟體設計 > DDD之應用架構

DDD之應用架構

2021-05-04 08:33:31 軟體設計

一,前言

之所以寫這些文章,很大程度上,是因為閱讀了阿里技術專家的文章,讀完之后,對我內心觸動很大,文章多出參考了阿里技術文章的內容,僅作為個人學習用途,也是作為技術人,希望技術可以被更多人學習到,

作為一個實習生,談架構,未免讓人感覺,好高騖遠,以下陳述只屬于個人淺薄意見,

依個人淺薄意見,架構的本質就是應用的拆分和聚合,

拆分也就是應用微服務化,聚合,并不是指將多個微服務聚合成一個應用,而是指聚合多個微服務的功能,讓他完成一個大的功能,這樣做的靈活性,可擴展性就會更高,

所謂應用架構,個人理解,更可以指的是應用中固定不變的代碼結構,設計模式,規范和組件間的通信,

一個好的應用架構,通過規定一套規范,可以讓團隊內能力參差不齊的開發人員更好的共同開發,降低開發成本,提升開發效率和代碼質量,

要求,或者原則?

  • 獨立于框架:架構不應該依賴某個外部的庫或框架,不應該被框架的結構所束縛,
  • 獨立于前端:前臺展示的樣式可能會隨時發生變化(今天可能是網頁、明天可能變成console、后天是獨立app),但是底層架構不應該隨之而變化,
  • 獨立于底層資料源:無論今天你用MySQL、Oracle還是MongoDB、CouchDB,甚至使用檔案系統,軟體架構不應該因為不同的底層資料儲存方式而產生巨大改變,
  • 獨立于外部依賴:無論外部依賴如何變更、升級,業務的核心邏輯不應該隨之而大幅變化,
  • 測驗覆寫率:無論外部依賴了什么資料庫、硬體、UI或者服務,業務的邏輯應該都能夠快速被驗證正確性,

通過一個需求,來論證,

用戶可以通過銀行網頁轉賬給另一個賬號,支持跨幣種轉賬,同時因為監管和對賬需求,需要記錄本次轉賬活動,

二,傳統互聯網架構下的架構設計

1、從MySql資料庫中找到轉出和轉入的賬戶,選擇用 MyBatis 的 mapper 實作 DAO;

2、從 Yahoo(或其他渠道)提供的匯率服務獲取轉賬的匯率資訊(底層是 http 開放介面);

3、計算需要轉出的金額,確保賬戶有足夠余額,并且沒超出每日轉賬上限;

4、實作轉入和轉出操作,扣除手續費,保存資料庫;

5、發送 Kafka 審計訊息,以便審計和對賬用;

public class TransferController {
 
    private TransferService transferService;
 
    public Result<Boolean> transfer(String targetAccountNumber, BigDecimal amount, HttpSession session) {
        Long userId = (Long) session.getAttribute("userId");
        return transferService.transfer(userId, targetAccountNumber, amount, "CNY");
    }
}
 
public class TransferServiceImpl implements TransferService {
 
    private static final String TOPIC_AUDIT_LOG = "TOPIC_AUDIT_LOG";
    private AccountMapper accountDAO;
    private KafkaTemplate<String, String> kafkaTemplate;
    private YahooForexService yahooForex;
 
    @Override
    public Result<Boolean> transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {
        // 1. 從資料庫讀取資料,忽略所有校驗邏輯如賬號是否存在等
        AccountDO sourceAccountDO = accountDAO.selectByUserId(sourceUserId);
        AccountDO targetAccountDO = accountDAO.selectByAccountNumber(targetAccountNumber);
 
        // 2. 業務引數校驗
        if (!targetAccountDO.getCurrency().equals(targetCurrency)) {
            throw new InvalidCurrencyException();
        }
 
        // 3. 獲取外部資料,并且包含一定的業務邏輯
        // exchange rate = 1 source currency = X target currency
        BigDecimal exchangeRate = BigDecimal.ONE;
        if (sourceAccountDO.getCurrency().equals(targetCurrency)) {
            exchangeRate = yahooForex.getExchangeRate(sourceAccountDO.getCurrency(), targetCurrency);
        }
        BigDecimal sourceAmount = targetAmount.divide(exchangeRate, RoundingMode.DOWN);
 
        // 4. 業務引數校驗
        if (sourceAccountDO.getAvailable().compareTo(sourceAmount) < 0) {
            throw new InsufficientFundsException();
        }
 
        if (sourceAccountDO.getDailyLimit().compareTo(sourceAmount) < 0) {
            throw new DailyLimitExceededException();
        }
 
        // 5. 計算新值,并且更新欄位
        BigDecimal newSource = sourceAccountDO.getAvailable().subtract(sourceAmount);
        BigDecimal newTarget = targetAccountDO.getAvailable().add(targetAmount);
        sourceAccountDO.setAvailable(newSource);
        targetAccountDO.setAvailable(newTarget);
 
        // 6. 更新到資料庫
        accountDAO.update(sourceAccountDO);
        accountDAO.update(targetAccountDO);
 
        // 7. 發送審計訊息
        String message = sourceUserId + "," + targetAccountNumber + "," + targetAmount + "," + targetCurrency;
        kafkaTemplate.send(TOPIC_AUDIT_LOG, message);
 
        return Result.success(true);
    }
 
}

一段業務代碼里經常包含了引數校驗、資料讀取存盤、業務計算、呼叫外部服務、發送訊息等多種邏輯,在這個案例里雖然是寫在了同一個方法里,在真實代碼中經常會被拆分成多個子方法,但實際效果是一樣的,而在我們日常的作業中,絕大部分代碼都或多或少的接近于此類結構,在Martin Fowler的 P of EAA書中,這種很常見的代碼樣式被叫做Transaction Script(事務腳本),雖然這種類似于腳本的寫法在功能上沒有什么問題,但是長久來看,他有以下幾個很大的問題:可維護性差、可擴展性差、可測驗性差,

1.可維護性差

一個應用最大的成本一般都不是來自于開發階段,而是應用整個生命周期的總維護成本,所以代碼的可維護性代表了最終成本,

可維護性 = 當依賴變化時,有多少代碼需要隨之改變,

  • 資料結構的不穩定性:AccountDO類是一個純資料結構,映射了資料庫中的一個表,這里的問題是資料庫的表結構和設計是應用的外部依賴,長遠來看都有可能會改變,比如資料庫要做Sharding,或者換一個表設計,或者改變欄位名,

  • 依賴庫的升級:AccountMapper依賴MyBatis的實作,如果MyBatis未來升級版本,可能會造成用法的不同(可以參考iBatis升級到基于注解的MyBatis的遷移成本),同樣的,如果未來換一個ORM體系,遷移成本也是巨大的,

  • 第三方服務依賴的不確定性:第三方服務,比如Yahoo的匯率服務未來很有可能會有變化:輕則API簽名變化,重則服務不可用需要尋找其他可替代的服務,在這些情況下改造和遷移成本都是巨大的,同時,外部依賴的兜底、限流、熔斷等方案都需要隨之改變,

  • 第三方服務API的介面變化:YahooForexService.getExchangeRate回傳的結果是小數點還是百分比?入參是(source, target)還是(target, source)?誰能保證未來介面不會改變?如果改變了,核心的金額計算邏輯必須跟著改,否則會造成資損,

  • 中間件更換:今天我們用Kafka發訊息,明天如果要上阿里云用RocketMQ該怎么辦?后天如果訊息的序列化方式從String改為Binary該怎么辦?如果需要訊息分片該怎么改?

案例里的代碼對于任何外部依賴的改變都會有比較大的影響,如果你的應用里有大量的此類代碼,你每一天的時間基本上會被各種庫升級、依賴服務升級、中間件升級、jar包沖突占滿,最終這個應用變成了一個不敢升級、不敢部署、不敢寫新功能、并且隨時會爆發的炸彈,終有一天會給你帶來驚喜,

2.可擴展性差

事務腳本式代碼的第二大缺陷是:雖然寫單個用例的代碼非常高效簡單,但是當用例多起來時,其擴展性會變得越來越差,

可擴展性 = 做新需求或改邏輯時,需要新增/修改多少代碼,

如果今天需要增加一個跨行轉賬的能力,你會發現基本上需要重新開發,基本上沒有任何的可復用性,

  • 資料來源被固定、資料格式不兼容:原有的AccountDO是從本地獲取的,而跨行轉賬的資料可能需要從一個第三方服務獲取,而服務之間資料格式不太可能是兼容的,導致從資料校驗、資料讀寫、到例外處理、金額計算等邏輯都要重寫,

  • 業務邏輯無法復用:資料格式不兼容的問題會導致核心業務邏輯無法復用,每個用例都是特殊邏輯的后果是最侄訓造成大量的if-else陳述句,而這種分支多的邏輯會讓分析代碼非常困難,容易錯過邊界情況,造成bug,

  • 邏輯和資料存盤的相互依賴:當業務邏輯增加變得越來越復雜時,新加入的邏輯很有可能需要對資料庫schema或訊息格式做變更,而變更了資料格式后會導致原有的其他邏輯需要一起跟著動,在最極端的場景下,一個新功能的增加會導致所有原有功能的重構,成本巨大,

在事務腳本式的架構下,一般做第一個需求都非常的快,但是做第N個需求時需要的時間很有可能是呈指數級上升的,絕大部分時間花費在老功能的重構和兼容上,最終你的創新速度會跌為0,促使老應用被推翻重構,

3.可測驗性能差

除了部分工具類、框架類和中間件類的代碼有比較高的測驗覆寫之外,我們在日常作業中很難看到業務代碼有比較好的測驗覆寫,而絕大部分的上線前的測驗屬于人肉的“集成測驗”,低測驗率導致我們對代碼質量很難有把控,容易錯過邊界條件,例外case只有線上爆發了才被動發現,而低測驗覆寫率的主要原因是業務代碼的可測驗性比較差,

可測驗性 = 運行每個測驗用例所花費的時間 * 每個需求所需要增加的測驗用例數量

  • 設施搭建困難:當代碼中強依賴了資料庫、第三方服務、中間件等外部依賴之后,想要完整跑通一個測驗用例需要確保所有依賴都能跑起來,這個在專案早期是及其困難的,在專案后期也會由于各種系統的不穩定性而導致測驗無法通過,

  • 運行耗時長:大多數的外部依賴呼叫都是I/O密集型,如跨網路呼叫、磁盤呼叫等,而這種I/O呼叫在測驗時需要耗時很久,另一個經常依賴的是笨重的框架如Spring,啟動Spring容器通常需要很久,當一個測驗用例需要花超過10秒鐘才能跑通時,絕大部分開發都不會很頻繁的測驗,

  • 耦合度高:假如一段腳本中有A、B、C三個子步驟,而每個步驟有N個可能的狀態,當多個子步驟耦合度高時,為了完整覆寫所有用例,最多需要有N * N * N個測驗用例,當耦合的子步驟越多時,需要的測驗用例呈指數級增長,

在事務腳本模式下,當測驗用例復雜度遠大于真實代碼復雜度,當運行測驗用例的耗時超出人肉測驗時,絕大部分人會選擇不寫完整的測驗覆寫,而這種情況通常就是bug很難被早點發現的原因,

4.總結

以上的代碼違背了至少以下幾個軟體設計的原則:

  • 單一性原則(Single Responsibility Principle):單一性原則要求一個物件/類應該只有一個變更的原因,但是在這個案例里,代碼可能會因為任意一個外部依賴或計算邏輯的改變而改變,

  • 依賴反轉原則(Dependency Inversion Principle):依賴反轉原則要求在代碼中依賴抽象,而不是具體的實作,在這個案例里外部依賴都是具體的實作,比如YahooForexService雖然是一個介面類,但是它對應的是依賴了Yahoo提供的具體服務,所以也算是依賴了實作,同樣的KafkaTemplate、MyBatis的DAO實作都屬于具體實作,

  • 開放封閉原則(Open Closed Principle):開放封閉原則指開放擴展,但是封閉修改,在這個案例里的金額計算屬于可能會被修改的代碼,這個時候該邏輯應該需要被包裝成為不可修改的計算類,新功能通過計算類的拓展實作,

三,基于DDD重構

在這里插入圖片描述

這是一個傳統的三層分層結構:UI層、業務層、和基礎設施層,上層對于下層有直接的依賴關系,導致耦合度過高,在業務層中對于下層的基礎設施有強依賴,耦合度高,我們需要對這張圖上的每個節點做抽象和整理,來降低對外部依賴的耦合度,

1.抽象資料存盤層

將Data Access層做抽象,降低系統對資料庫的直接依賴,

  • 新建Account物體物件:一個物體(Entity)是擁有ID的域物件,除了擁有資料之外,同時擁有行為,Entity和資料庫儲存格式無關,在設計中要以該領域的通用嚴謹語言(Ubiquitous Language)為依據,

  • 新建物件儲存介面類AccountRepository:Repository只負責Entity物件的存盤和讀取,而Repository的實作類完成資料庫存盤的細節,通過加入Repository介面,底層的資料庫連接可以通過不同的實作類而替換,

@Data
public class Account {
    private AccountId id;
    private AccountNumber accountNumber;
    private UserId userId;
    private Money available;
    private Money dailyLimit;
 
    public void withdraw(Money money) {
        // 轉出
    }
 
    public void deposit(Money money) {
        // 轉入
    }
}
public interface AccountRepository {
    Account find(AccountId id);
    Account find(AccountNumber accountNumber);
    Account find(UserId userId);
    Account save(Account account);
}
 
public class AccountRepositoryImpl implements AccountRepository {
 
    @Autowired
    private AccountMapper accountDAO;
 
    @Autowired
    private AccountBuilder accountBuilder;
 
    @Override
    public Account find(AccountId id) {
        AccountDO accountDO = accountDAO.selectById(id.getValue());
        return accountBuilder.toAccount(accountDO);
    }
 
    @Override
    public Account find(AccountNumber accountNumber) {
        AccountDO accountDO = accountDAO.selectByAccountNumber(accountNumber.getValue());
        return accountBuilder.toAccount(accountDO);
    }
 
    @Override
    public Account find(UserId userId) {
        AccountDO accountDO = accountDAO.selectByUserId(userId.getId());
        return accountBuilder.toAccount(accountDO);
    }
 
    @Override
    public Account save(Account account) {
        AccountDO accountDO = accountBuilder.fromAccount(account);
        if (accountDO.getId() == null) {
            accountDAO.insert(accountDO);
        } else {
            accountDAO.update(accountDO);
        }
        return accountBuilder.toAccount(accountDO);
    }
 
}

Account物體類和AccountDO資料類的對比如下:

  • Data Object資料類:AccountDO是單純的和資料庫表的映射關系,每個欄位對應資料庫表的一個column,這種物件叫Data Object,DO只有資料,沒有行為,AccountDO的作用是對資料庫做快速映射,避免直接在代碼里寫SQL,無論你用的是MyBatis還是Hibernate這種ORM,從資料庫來的都應該先直接映射到DO上,但是代碼里應該完全避免直接操作 DO,

  • Entity物體類:Account 是基于領域邏輯的物體類,它的欄位和資料庫儲存不需要有必然的聯系,Entity包含資料,同時也應該包含行為,在 Account 里,欄位也不僅僅是String等基礎型別,而應該盡可能用上一講的 Domain Primitive 代替,可以避免大量的校驗代碼,

DAO 和 Repository 類的對比如下:

  • DAO對應的是一個特定的資料庫型別的操作,相當于SQL的封裝,所有操作的物件都是DO類,所有介面都可以根據資料庫實作的不同而改變,比如,insert 和 update 屬于資料庫專屬的操作,

  • Repository對應的是Entity物件讀取儲存的抽象,在介面層面做統一,不關注底層實作,比如,通過 save 保存一個Entity物件,但至于具體是 insert 還是 update 并不關心,Repository的具體實作類通過呼叫DAO來實作各種操作,通過Builder/Factory物件實作AccountDO 到 Account之間的轉化

Repository和Entity

  • 通過Account物件,避免了其他業務邏輯代碼和資料庫的直接耦合,避免了當資料庫欄位變化時,大量業務邏輯也跟著變的問題,

  • 通過Repository,改變業務代碼的思維方式,讓業務邏輯不再面向資料庫編程,而是面向領域模型編程,

  • Account屬于一個完整的記憶體中物件,可以比較容易的做完整的測驗覆寫,包含其行為,

  • Repository作為一個介面類,可以比較容易的實作Mock或Stub,可以很容易測驗,

  • AccountRepositoryImpl實作類,由于其職責被單一出來,只需要關注Account到AccountDO的映射關系和Repository方法到DAO方法之間的映射關系,相對于來說更容易測驗,

在這里插入圖片描述

2.抽象第三方服務

類似對于資料庫的抽象,所有第三方服務也需要通過抽象解決第三方服務不可控,入參出參強耦合的問題,在這個例子里我們抽象出 ExchangeRateService 的服務,和一個ExchangeRate的Domain Primitive類:

public interface ExchangeRateService {
    ExchangeRate getExchangeRate(Currency source, Currency target);
}
 
public class ExchangeRateServiceImpl implements ExchangeRateService {
 
    @Autowired
    private YahooForexService yahooForexService;
 
    @Override
    public ExchangeRate getExchangeRate(Currency source, Currency target) {
        if (source.equals(target)) {
            return new ExchangeRate(BigDecimal.ONE, source, target);
        }
        BigDecimal forex = yahooForexService.getExchangeRate(source.getValue(), target.getValue());
        return new ExchangeRate(forex, source, target);
    }
 
 

3.ACL防腐層

很多時候我們的系統會去依賴其他的系統,而被依賴的系統可能包含不合理的資料結構、API、協議或技術實作,如果對外部系統強依賴,會導致我們的系統被”腐蝕“,這個時候,通過在系統間加入一個防腐層,能夠有效的隔離外部依賴和內部邏輯,無論外部如何變更,內部代碼可以盡可能的保持不變,

ACL 不僅僅只是多了一層呼叫,在實際開發中ACL能夠提供更多強大的功能:

  • 配接器:很多時候外部依賴的資料、介面和協議并不符合內部規范,通過配接器模式,可以將資料轉化邏輯封裝到ACL內部,降低對業務代碼的侵入,在這個案例里,我們通過封裝了ExchangeRate和Currency物件,轉化了對方的入參和出參,讓入參出參更符合我們的標準,

  • 快取:對于頻繁呼叫且資料變更不頻繁的外部依賴,通過在ACL里嵌入快取邏輯,能夠有效的降低對于外部依賴的請求壓力,同時,很多時候快取邏輯是寫在業務代碼里的,通過將快取邏輯嵌入ACL,能夠降低業務代碼的復雜度,

  • 兜底:如果外部依賴的穩定性較差,一個能夠有效提升我們系統穩定性的策略是通過ACL起到兜底的作用,比如當外部依賴出問題后,回傳最近一次成功的快取或業務兜底資料,這種兜底邏輯一般都比較復雜,如果散落在核心業務代碼中會很難維護,通過集中在ACL中,更加容易被測驗和修改,

  • 易于測驗:類似于之前的Repository,ACL的介面類能夠很容易的實作Mock或Stub,以便于單元測驗,

  • 功能開關:有些時候我們希望能在某些場景下開放或關閉某個介面的功能,或者讓某個介面回傳一個特定的值,我們可以在ACL配置功能開關來實作,而不會對真實業務代碼造成影響,同時,使用功能開關也能讓我們容易的實作Monkey測驗,而不需要真正物理性的關閉外部依賴,

在這里插入圖片描述

4.抽象中間件

對各種中間件的抽象的目的是讓業務代碼不再依賴中間件的實作邏輯,因為中間件通常需要有通用型,中間件的介面通常是String或Byte[] 型別的,導致序列化/反序列化邏輯通常和業務邏輯混雜在一起,造成膠水代碼,通過中間件的ACL抽象,減少重復膠水代碼,

@Value
@AllArgsConstructor
public class AuditMessage {
 
    private UserId userId;
    private AccountNumber source;
    private AccountNumber target;
    private Money money;
    private Date date;
 
    public String serialize() {
        return userId + "," + source + "," + target + "," + money + "," + date;   
    }
 
    public static AuditMessage deserialize(String value) {
        // todo
        return null;
    }
}
 
public interface AuditMessageProducer {
    SendResult send(AuditMessage message);
}
 
public class AuditMessageProducerImpl implements AuditMessageProducer {
 
    private static final String TOPIC_AUDIT_LOG = "TOPIC_AUDIT_LOG";
 
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
 
    @Override
    public SendResult send(AuditMessage message) {
        String messageBody = message.serialize();
        kafkaTemplate.send(TOPIC_AUDIT_LOG, messageBody);
        return SendResult.success();
    }
}

在這里插入圖片描述

5.封裝業務邏輯

在這個案例里,有很多業務邏輯是跟外部依賴的代碼混合的,包括金額計算、賬戶余額的校驗、轉賬限制、金額增減等,這種邏輯混淆導致了核心計算邏輯無法被有效的測驗和復用,在這里,我們的解法是通過Entity、Domain Primitive和Domain Service封裝所有的業務邏輯:

1)用DP封裝跟物體無關的無狀態計算邏輯

使用ExchangeRate來封裝匯率計算邏輯:

BigDecimal exchangeRate = BigDecimal.ONE;
if (sourceAccountDO.getCurrency().equals(targetCurrency)) {
    exchangeRate = yahooForex.getExchangeRate(sourceAccountDO.getCurrency(), targetCurrency);
}
BigDecimal sourceAmount = targetAmount.divide(exchangeRate, RoundingMode.DOWN);

變為:

ExchangeRate exchangeRate = exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());
Money sourceMoney = exchangeRate.exchangeTo(targetMoney);

2)用Entity封裝單物件的有狀態的行為,包括業務校驗

用Account物體類封裝所有Account的行為,包括業務校驗如下:

@Data
public class Account {
 
    private AccountId id;
    private AccountNumber accountNumber;
    private UserId userId;
    private Money available;
    private Money dailyLimit;
 
    public Currency getCurrency() {
        return this.available.getCurrency();
    }
 
    // 轉入
    public void deposit(Money money) {
        if (!this.getCurrency().equals(money.getCurrency())) {
            throw new InvalidCurrencyException();
        }
        this.available = this.available.add(money);
    }
 
    // 轉出
    public void withdraw(Money money) {
        if (this.available.compareTo(money) < 0) {
            throw new InsufficientFundsException();
        }
        if (this.dailyLimit.compareTo(money) < 0) {
            throw new DailyLimitExceededException();
        }
        this.available = this.available.subtract(money);
    }
}

原有的業務代碼則可以簡化為:

sourceAccount.deposit(sourceMoney);
targetAccount.withdraw(targetMoney);

3)用Domain Service封裝多物件邏輯

在這個案例里,我們發現這兩個賬號的轉出和轉入實際上是一體的,也就是說這種行為應該被封裝到一個物件中去,特別是考慮到未來這個邏輯可能會產生變化:比如增加一個扣手續費的邏輯,這個時候在原有的TransferService中做并不合適,在任何一個Entity或者Domain Primitive里也不合適,需要有一個新的類去包含跨域物件的行為,這種物件叫做Domain Service,

public interface AccountTransferService {
    void transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate);
}
 
public class AccountTransferServiceImpl implements AccountTransferService {
    private ExchangeRateService exchangeRateService;
 
    @Override
    public void transfer(Account sourceAccount, Account targetAccount, Money targetMoney, ExchangeRate exchangeRate) {
        Money sourceMoney = exchangeRate.exchangeTo(targetMoney);
        sourceAccount.deposit(sourceMoney);
        targetAccount.withdraw(targetMoney);
    }
}

原始代碼則簡化為一行:

accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);

在這里插入圖片描述

6.重構后效果

public class TransferServiceImplNew implements TransferService {
 
    private AccountRepository accountRepository;
    private AuditMessageProducer auditMessageProducer;
    private ExchangeRateService exchangeRateService;
    private AccountTransferService accountTransferService;
 
    @Override
    public Result<Boolean> transfer(Long sourceUserId, String targetAccountNumber, BigDecimal targetAmount, String targetCurrency) {
        // 引數校驗
        Money targetMoney = new Money(targetAmount, new Currency(targetCurrency));
 
        // 讀資料
        Account sourceAccount = accountRepository.find(new UserId(sourceUserId));
        Account targetAccount = accountRepository.find(new AccountNumber(targetAccountNumber));
        ExchangeRate exchangeRate = exchangeRateService.getExchangeRate(sourceAccount.getCurrency(), targetMoney.getCurrency());
 
        // 業務邏輯
        accountTransferService.transfer(sourceAccount, targetAccount, targetMoney, exchangeRate);
 
        // 保存資料
        accountRepository.save(sourceAccount);
        accountRepository.save(targetAccount);
 
        // 發送審計訊息
        AuditMessage message = new AuditMessage(sourceAccount, targetAccount, targetMoney);
        auditMessageProducer.send(message);
 
        return Result.success(true);
    }
}
  • 業務邏輯清晰,資料存盤和業務邏輯完全分隔,

  • Entity、Domain Primitive、Domain Service都是獨立的物件,沒有任何外部依賴,但是卻包含了所有核心業務邏輯,可以單獨完整測驗,

  • 原有的TransferService不再包括任何計算邏輯,僅僅作為組件編排,所有邏輯均delegate到其他組件,這種僅包含Orchestration(編排)的服務叫做Application Service(應用服務),

在這里插入圖片描述

通過對外部依賴的抽象和內部邏輯的封裝重構,應用整體的依賴關系變了:

  • 最底層不再是資料庫,而是Entity、Domain Primitive和Domain Service,這些物件不依賴任何外部服務和框架,而是純記憶體中的資料和操作,這些物件我們打包為Domain Layer(領域層),領域層沒有任何外部依賴關系,

  • 再其次的是負責組件編排的Application Service,但是這些服務僅僅依賴了一些抽象出來的ACL類和Repository類,而其具體實作類是通過依賴注入注進來的,Application Service、Repository、ACL等我們統稱為Application Layer(應用層),應用層 依賴 領域層,但不依賴具體實作,

  • 最后是ACL,Repository等的具體實作,這些實作通常依賴外部具體的技術實作和框架,所以統稱為Infrastructure Layer(基礎設施層),Web框架里的物件如Controller之類的通常也屬于基礎設施層,

寫這段代碼,考慮到最終的依賴關系,我們可能先寫Domain層的業務邏輯,然后再寫Application層的組件編排,最后才寫每個外部依賴的具體實作,這種架構思路和代碼組織結構就叫做Domain-Driven Design(領域驅動設計,或DDD),所以DDD不是一個特殊的架構設計,而是所有Transction Script代碼經過合理重構后一定會抵達的終點,

四,總結

DDD不是一個什么特殊的架構,而是任何傳統代碼經過合理的重構之后最終一定會抵達的終點,DDD的架構能夠有效的解決傳統架構中的問題:

  • 高可維護性:當外部依賴變更時,內部代碼只用變更跟外部對接的模塊,其他業務邏輯不變,

  • 高可擴展性:做新功能時,絕大部分的代碼都能復用,僅需要增加核心業務邏輯即可,

  • 高可測驗性:每個拆分出來的模塊都符合單一性原則,絕大部分不依賴框架,可以快速的單元測驗,做到100%覆寫,

  • 代碼結構清晰:通過POM module可以解決模塊間的依賴關系, 所有外接模塊都可以單獨獨立成Jar包被復用,當團隊形成規范后,可以快速的定位到相關代碼,

用傳統的白話講:

DDD就是你寫一個東西,規定好入參和出參的格式,里面加上各種各樣的強規范,拓展就是實作你的介面,開發成本邊低,總之,你寫的業務邏輯在爛,也只會影響你自己的,不會影響別人的,系統的設計,要根據業務的需求,都是業務驅動技術,paas化就是為了更好的擴展業務的復雜度,只需要規定一系列規范,開展一個新的業務線,只是一個新介面的問題,甚至不用,只要垂直領域規劃好了,領域邊界明確了,只要增加擴展點也就是切點就可以了,paas化的難點就是邊界的明確,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/282603.html

標籤:其他

上一篇:LAMP架構之phpmyadmin搭建實作

下一篇:Redis哨兵+keepalived高可用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more