分布领域驱动设计(DDD):领域接口化设计式缓存的选择
-? ? ?前言? ? -
把服務對象(service)和資源庫對象(repository)設計成接口是最常見的。但是這對接口化的認識還遠遠不夠,我們需要更深入地去分析接口化設計和更全面地應用接口化編程。所以我們要討論的是全面接口化,尤其是對領域模型接口化的認識。
-? ? ?領域接口化? ? -
通常的情況下我們會把領域模型設計成類(class),但是你有沒有想過把領域模型設計成接口(interface)?比如:
這樣的設計似乎沒有任何價值,那么繼續深入地看看。比如:
這時候看起來有點東西,因為我們為了適配不同的數據源,提供了不同的實現類。
最開始要把領域對象設計成接口,確實是為了在不同的 ORM 框架之間實現無縫切換。因為 JPA 對面向對象的支持最好,而 Mybatis 因為簡單在大環境下比較流行。在解決這個問題時,通常使用層內包裹或者叫對象轉換的方式來解決。具體來說是在持久層使用持久化對象(PO)與領域對象(DO)的之間進行轉換。例如:
public class JpaUserRepository implements UserRepository {// ...@Overridepublic Optional<User> findById(String id) {UserPO userPO = this.entityManager.find(UserPO.class, id);return Optional.ofNullable(userPO).map(UserPO::toUser);}@Overridepublic User save(User user) {UserPO userPO = this.entityManager.find(UserPO.class, user.getId());userPO.setNickname(user.getNickname());// ...return this.entityManager.merge(userPO).toUser();} }其中 UserPO 對象基本上是對數據庫表的映射。
將 User 設計成接口后,這個交換的問題就比較簡單地解決了,如下:
public class JpaUserRepository implements UserRepository { // ...@Overridepublic User create(String id) {return new JpaUser(id);}@Overridepublic Optional<User> findById(String id) {JpaUser user = this.entityManager.find(JpaUser.class, id);return Optional.ofNullable(user);}@Overridepublic User save(User user) {JpaUser target = JpaUser.of(user);return this.entityManager.merge(target);}// ... }補充 JpaUser.of()?方法的實現:public class JpaUser extends UserSupport {// ...public static JpaUser of(User user) {if (user instanceof JpaUser) {return (JpaUser) user;}var target = new JpaUser();BeanUtils.copyProperties(user, target);// ...return target;} }對于使用 JPA 或者 Elasticsearch 等等各種不同的數據源,Spring data 都為此做了全面的支持。但由于 User 是接口,Spring data 提供的 Repository 接口泛型只支持具體類型,比如:
public interface ElasticsearchUserRepositoryextends ElasticsearchRepository<ElasticsearchUser, String> {// extends ElasticsearchRepository<User, String> // Not supported }為了解決這個問題,我們需要使用委托的方式,如下:
public class DelegatingElasticsearchUserRepository implements UserRepository {private final ElasticsearchUserRepository elasticsearchUserRepository;public DelegatingElasticsearchUserRepository(ElasticsearchUserRepository elasticsearchUserRepository) {this.elasticsearchUserRepository = elasticsearchUserRepository;}@Overridepublic User create(String id) {return new ElasticsearchUser(id);}@Overridepublic Optional<User> findById(String id) {return CastUtils.cast(this.elasticsearchUserRepository.findById(id));}@Overridepublic User save(User user) {return this.elasticsearchUserRepository.save(ElasticsearchUser.of(user));}// ... }-? ? ?關聯接口化? ? -
接口之間的關聯關系依然需要具體到子類的關聯關系上來討論。
對于需要持久化的實體來說,我們不可能直接在成員屬性上使用接口類型,因為持久化框架無法通過接口來判定具體實現類。如下:
@Getter @Setter @NoArgsConstructor @Entity @Table(name = "mf_order") public class JpaOrder implements Order {// ...// OrderItem 是一個接口類型,不能持久化。private List<OrderItem> items = new ArrayList<>();// ... }對于泛化關聯關系問題,我們可以使用 JPA 注解提供的 targetEntity 屬性來解決:
// ... public class JpaOrder implements Order {// ...// 通過指定具體的 targetEntity 類型,來解決泛化與特化的問題。@OneToMany(targetEntity = JpaOrderItem.class)private List<OrderItem> items = new ArrayList<>();// ... }支持 targetEntity 屬性的注解包括:@OneToMany、@OneToOne、@ManyToOne、@ManyToMany。
對于不支持類似 targetEntity 屬性的框架或者其它持久化技術,我們可以使用封裝來解決。如下:
@Getter @Setter @NoArgsConstructor @Document(indexName = "user") public class ElasticsearchOrder implements Order {// ...// 使用具體特化類型進行解決。private List<ElasticsearchOrderItem> items = new ArrayList<>();@Overridepublic void setItems(List<OrderItem> items) {this.items = Objects.requireNonNullElseGet(items, (Supplier<List<OrderItem>>) ArrayList::new).stream().map(ElasticsearchOrderItem::of).collect(Collectors.toList());}// ... }如果使用的是 Mybatis 作為持久化框架,依然可以在 OrderMapper.xml 中進行配置來解決:
<resultMap id="Order" type="org.mallfoundry.order.repository.mybatis.MybatisOrder"><!-- ... --><collection property="items" ofType="org.mallfoundry.order.repository.mybatis.MybatisOrderItem"><!-- ... --></collection><!-- ... --> </resultMap>在解決掉不同數據源無縫切換和關聯關系特化的問題后,在創建 User 對象上就和以往使用 new 的方式有所不同了,如下:
再過去創建對象都是使用 new 關鍵字,然而現在要使用 UserService 提供的 createUser(String id)?來創建。
這種思維的轉變可能讓你初次不太很適應,但在考慮另一個問題。
-? ? ?系統接口化? ? -
對于一個產品我們要考慮的不只是產品本身能解決的業務需求,還需要在部署上有所追求。如果項目初期的并發量很小,客戶可能采用單進程的方式部署,慢慢地單進程扛不住了會升級到集群的方式,最終還要升級到微服務的方式。如何在單進程、集群和微服務之間進行無縫切換呢?
再過去單機和集群項目與微服務項目是不能兼容的,因為領域模型都是類(class)而不是接口(interface)。具體來說:服務提供者(provider)的 User 對象與服務消費者(Consumer)的 User 對象是不兼容,不兼容將導致在單機項目中使用的是服務提供方的內部 User 對象,而一旦遷移到微服務項目后,需要大量的修改工作。要把以前調用方使用內部 User 對象替換為服務消費者提供的 User 對象。這樣的工作也是不可以逆的,一旦遷移成功就不能降級到單機環境了。
再過去我們確實把服務(service)設計成了接口,這種接口的設計對于內部的開發看似會有幫助,但是從實戰的經驗來看卻不像大家想象的那樣可以為 Service 提供不同的實現。因為現在都是迭代開發,都是一個版本一個版本的去不斷完善應用服務代碼,而不是替換應用服務代碼,所以在 IDDD 中把應用服務(Application Service)類型由接口(Interface)改為了類(Class)。
如果我們把領域對象設計成接口類型,并與服務接口以及其它接口一起組織在一個新的模塊內,形成一個新的接口(API)模塊。然后為各種不同地端口提供適配此端口的實現,這樣的設計是不是可以解決在運行環境中無縫切換的問題,如下:
這樣的設計使得調用者只需要使用 User 接口(user-api)開發業務,并且在單進程(Standalone)環境中只需要依賴 user 模塊,在微服務環境中只需要依賴 user-openfeign-client 模塊,在外部環境中只需要依賴 user-rest-client 模塊。調用者通過依賴不同地實現模塊來解決不同環境的無縫切換,并且調用者使用的代碼是不需要改變的。
-? ? ?開源電商? ? -
Mallfoundry 是一個完全開源的使用 Spring Boot 開發的多商戶電商平臺。它可以嵌入到已有的 Java 程序中,或者作為服務器、集群、云中的服務運行。
領域模型采用領域驅動設計(DDD)、接口化以及面向對象設計。
項目地址:
https://gitee.com/mallfoundry/mall
-? ? ?總結? ? -
領域對象接口化使得我們在內部實現了一套統一的接口,并將領域對象接口化擴展到系統級別時,我們又在系統層次上設計出一套統一地全局接口來開發業務和應對未來變化的環境。這樣的設計雖然非常好,但對軟件設計人員、軟件架構師以及開發人員的專業性也有了一定的要求,但是它所帶來的好處是可見的。
作者:不夠具體
來源:https://juejin.cn/post/6894109393173315597
推薦書籍
? ? · END ·
微信淘寶等平臺要互通!?騰訊阿里字節回應
2021-09-14
一文詳解 API 設計最佳實踐
2021-09-12
12 種經典億級流量架構之資源隔離思想與方法論
2021-09-09
拼夕夕訂單超時未支付自動關閉實現方案!
2021-09-08
在騰訊,我們如何做 Code Review
2021-09-24
紫色飛豬:基于K8s的集群穩定架構
2021-09-23
2W 字詳解設計模式!
2021-09-22
巨人大哥聊聊電商微服務體系中分層設計和領域的劃分
2021-09-20
億級流量架構怎么做資源隔離?口琴這篇寫得太好了!
2021-09-17
螞蟻集團于雨:萬級規模 K8S 集群 Etcd 高可用建設之路
2021-09-16
干貨丨千萬流量大型分布式系統架構設計實戰
2021-09-15
京東面試官:你是怎么理解 MySQL 的優化原理的?
2021-09-26
在騰訊,我們如何做 Code Review
2021-09-24
紫色飛豬:基于K8s的集群穩定架構
2021-09-23
2W 字詳解設計模式!
2021-09-22
巨人大哥聊聊電商微服務體系中分層設計和領域的劃分
2021-09-20
億級流量架構怎么做資源隔離?口琴這篇寫得太好了!
2021-09-17
螞蟻集團于雨:萬級規模 K8S 集群 Etcd 高可用建設之路
2021-09-16
干貨丨千萬流量大型分布式系統架構設計實戰
2021-09-15
微信淘寶等平臺要互通!?騰訊阿里字節回應
2021-09-14
一文詳解 API 設計最佳實踐
2021-09-12
12 種經典億級流量架構之資源隔離思想與方法論
2021-09-09
美團技術:到店結算平臺實踐(膠片)
2021-09-06
Serverless實戰之路
2021-09-03
柴華:DDD在哈啰交易中臺的實踐
2021-09-02
總結
以上是生活随笔為你收集整理的分布领域驱动设计(DDD):领域接口化设计式缓存的选择的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot中controlle
- 下一篇: 简单理解session