領(lǐng)域驅(qū)動設(shè)計(DDD):領(lǐng)域接口化設(shè)計

領(lǐng)域接口化設(shè)計

把服務(wù)對象(service)和資源庫對象(repository)設(shè)計成接口是最常見的。但是這對接口化的認識還遠遠不夠博秫,我們需要更深入地去分析接口化設(shè)計和更全面地應(yīng)用接口化編程。所以我們要討論的是全面接口化岳悟,尤其是對領(lǐng)域模型接口化的認識几缭。

領(lǐng)域接口化

通常的情況下我們會把領(lǐng)域模型設(shè)計成類(class),但是你有沒有想過把領(lǐng)域模型設(shè)計成接口(interface)阴挣?比如:

public interface User {
    // ...
}

public class UserImpl implements User {
    // ...
}

這樣的設(shè)計似乎沒有任何價值,那么繼續(xù)深入地看看纺腊。比如:

user-object-uml

這時候看起來有點東西畔咧,因為我們?yōu)榱诉m配不同的數(shù)據(jù)源,提供了不同的實現(xiàn)類揖膜。

最開始要把領(lǐng)域?qū)ο?/strong>設(shè)計成接口誓沸,確實是為了在不同的 ORM 框架之間實現(xiàn)無縫切換。因為 JPA 對面向?qū)ο蟮闹С肿詈靡妓冢?Mybatis 因為簡單在大環(huán)境下比較流行拜隧。在解決這個問題時,通常使用層內(nèi)包裹或者叫對象轉(zhuǎn)換的方式來解決趁仙。具體來說是在持久層使用持久化對象(PO)與領(lǐng)域?qū)ο螅―O)的之間進行轉(zhuǎn)換洪添。例如:

public class JpaUserRepository implements UserRepository {
    // ...
    @Override
    public Optional<User> findById(String id) {
        UserPO userPO = this.entityManager.find(UserPO.class, id);
        return Optional.ofNullable(userPO).map(UserPO::toUser);
    }

    @Override
    public User save(User user) {
        UserPO userPO = this.entityManager.find(UserPO.class, user.getId());
        userPO.setNickname(user.getNickname());
        // ...
        return this.entityManager.merge(userPO).toUser();
    }
}

其中 UserPO 對象基本上是對數(shù)據(jù)庫表的映射,然后將數(shù)據(jù)與 User 對象進行交換雀费。對于這種需要交換的方式既有性能的損失又比較繁瑣干奢,將 User 設(shè)計成接口后,這個交換的問題就比較簡單地解決了盏袄,如下:

public class JpaUserRepository implements UserRepository {
    // ...
    @Override
    public User create(String id) {
        return new JpaUser(id);
    }

    @Override
    public Optional<User> findById(String id) {
        JpaUser user = this.entityManager.find(JpaUser.class, id);
        return Optional.ofNullable(user);
    }

    @Override
    public User save(User user) {
        JpaUser target = JpaUser.of(user);
        return this.entityManager.merge(target);
    }
    // ...
}

補充 JpaUser.of() 方法的實現(xiàn):

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 等等各種不同的數(shù)據(jù)源忿峻,Spring data 都為此做了全面的支持。但由于 User 是接口辕羽,Spring data 提供的 Repository 接口泛型只支持具體類型逛尚,比如:

public interface ElasticsearchUserRepository
        extends 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;
    }

    @Override
    public User create(String id) {
        return new ElasticsearchUser(id);
    }

    @Override
    public Optional<User> findById(String id) {
        return CastUtils.cast(this.elasticsearchUserRepository.findById(id));
    }

    @Override
    public User save(User user) {
        return this.elasticsearchUserRepository.save(ElasticsearchUser.of(user));
    }
    // ...
}

關(guān)聯(lián)接口化

order-association

接口之間的關(guān)聯(lián)關(guān)系依然需要具體到子類的關(guān)聯(lián)關(guān)系上來討論绰寞。

對于需要持久化的實體來說,我們不可能直接在成員屬性上使用接口類型酌毡,因為持久化框架無法通過接口來判定具體實現(xiàn)類克握。如下:

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "mf_order")
public class JpaOrder implements Order {
    // ...
    // OrderItem 是一個接口類型蕾管,不能持久化枷踏。
    private List<OrderItem> items = new ArrayList<>();
    // ...
}

對于泛化關(guān)聯(lián)關(guān)系問題,我們可以使用 JPA 注解提供的 targetEntity 屬性來解決:

// ...
public class JpaOrder implements Order {
    // ...
    // 通過指定具體的 targetEntity 類型掰曾,來解決泛化與特化的問題旭蠕。
    @OneToMany(targetEntity = JpaOrderItem.class)
    private List<OrderItem> items = new ArrayList<>();
    // ...
}
  • 支持 targetEntity 屬性的注解包括:@OneToMany@OneToOne@ManyToOne掏熬、@ManyToMany佑稠。

對于不支持類似 targetEntity 屬性的框架或者其它持久化技術(shù),我們可以使用封裝來解決旗芬。如下:

@Getter
@Setter
@NoArgsConstructor
@Document(indexName = "user")
public class ElasticsearchOrder implements Order {
    // ...
    // 使用具體特化類型進行解決舌胶。
    private List<ElasticsearchOrderItem> items = new ArrayList<>();
    
    @Override
    public 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>

在解決掉不同數(shù)據(jù)源無縫切換和關(guān)聯(lián)關(guān)系特化的問題后疮丛,在創(chuàng)建 User 對象上就和以往使用 new 的方式有所不同了幔嫂,如下:

@Test
public void testCreateUser() {
    User user = this.userService.createUser(null); // new User()
    user.setNickname("Nickname");
    user.setGender(Gender.MALE);
    this.userService.addUser(user);
}

再過去創(chuàng)建對象都是使用 new 關(guān)鍵字,然而現(xiàn)在要使用 UserService 提供的 createUser(String id) 來創(chuàng)建誊薄。

這種思維的轉(zhuǎn)變可能讓你初次不太很適應(yīng)履恩,但在考慮另一個問題。

系統(tǒng)接口化

對于一個產(chǎn)品我們要考慮的不只是產(chǎn)品本身能解決的業(yè)務(wù)需求呢蔫,還需要在部署上有所追求切心。如果項目初期的并發(fā)量很小,客戶可能采用單進程的方式部署片吊,慢慢地單進程扛不住了會升級到集群的方式绽昏,最終還要升級到微服務(wù)的方式。如何在單進程俏脊、集群和微服務(wù)之間進行無縫切換呢而涉?

再過去單機和集群項目與微服務(wù)項目是不能兼容的,因為領(lǐng)域模型都是類(class)而不是接口(interface)联予。具體來說:服務(wù)提供者(provider)的 User 對象與服務(wù)消費者(Consumer)的 User 對象是不兼容啼县,不兼容將導(dǎo)致在單機項目中使用的是服務(wù)提供方的內(nèi)部 User 對象,而一旦遷移到微服務(wù)項目后沸久,需要大量的修改工作季眷。要把以前調(diào)用方使用內(nèi)部 User 對象替換為服務(wù)消費者提供的 User 對象。這樣的工作也是不可以逆的卷胯,一旦遷移成功就不能降級到單機環(huán)境了子刮。

再過去我們確實把服務(wù)(service)設(shè)計成了接口验辞,這種接口的設(shè)計對于內(nèi)部的開發(fā)看似會有幫助篷帅,但是從實戰(zhàn)的經(jīng)驗來看卻不像大家想象的那樣可以為 Service 提供不同的實現(xiàn)。因為現(xiàn)在都是迭代開發(fā)鼻由,都是一個版本一個版本的去不斷完善應(yīng)用服務(wù)代碼担钮,而不是替換應(yīng)用服務(wù)代碼橱赠,所以在 IDDD 中把應(yīng)用服務(wù)(Application Service)類型由接口(Interface)改為了類(Class)。

如果我們把領(lǐng)域?qū)ο笤O(shè)計成接口類型箫津,并與服務(wù)接口以及其它接口一起組織在一個新的模塊內(nèi)狭姨,形成一個新的接口(API)模塊宰啦。然后為各種不同地端口提供適配此端口的實現(xiàn),這樣的設(shè)計是不是可以解決在運行環(huán)境中無縫切換的問題饼拍,如下:

user-modules

這樣的設(shè)計使得調(diào)用者只需要使用 User 接口(user-api)開發(fā)業(yè)務(wù)赡模,并且在單進程(Standalone)環(huán)境中只需要依賴 user 模塊,在微服務(wù)環(huán)境中只需要依賴 user-openfeign-client 模塊师抄,在外部環(huán)境中只需要依賴 user-rest-client 模塊漓柑。調(diào)用者通過依賴不同地實現(xiàn)模塊來解決不同環(huán)境的無縫切換,并且調(diào)用者使用的代碼是不需要改變的叨吮。

開源電商

Mallfoundry 是一個完全開源的使用 Spring Boot 開發(fā)的多商戶電商平臺欺缘。它可以嵌入到已有的 Java 程序中,或者作為服務(wù)器挤安、集群谚殊、云中的服務(wù)運行。

  • 領(lǐng)域模型采用領(lǐng)域驅(qū)動設(shè)計(DDD)蛤铜、接口化以及面向?qū)ο笤O(shè)計嫩絮。

項目地址:https://gitee.com/mallfoundry/mall

總結(jié)

領(lǐng)域?qū)ο蠼涌诨沟梦覀冊趦?nèi)部實現(xiàn)了一套統(tǒng)一的接口,并將領(lǐng)域?qū)ο蠼涌诨瘮U展到系統(tǒng)級別時围肥,我們又在系統(tǒng)層次上設(shè)計出一套統(tǒng)一地全局接口來開發(fā)業(yè)務(wù)和應(yīng)對未來變化的環(huán)境剿干。這樣的設(shè)計雖然非常好,但對軟件設(shè)計人員穆刻、軟件架構(gòu)師以及開發(fā)人員的專業(yè)性也有了一定的要求置尔,但是它所帶來的好處是可見的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末氢伟,一起剝皮案震驚了整個濱河市榜轿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌朵锣,老刑警劉巖谬盐,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诚些,居然都是意外死亡飞傀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門诬烹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砸烦,“玉大人,你說我怎么就攤上這事绞吁〈倍唬” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵掀泳,是天一觀的道長雪隧。 經(jīng)常有香客問我,道長员舵,這世上最難降的妖魔是什么脑沿? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮马僻,結(jié)果婚禮上庄拇,老公的妹妹穿的比我還像新娘。我一直安慰自己韭邓,他們只是感情好措近,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著女淑,像睡著了一般瞭郑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸭你,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天屈张,我揣著相機與錄音,去河邊找鬼袱巨。 笑死阁谆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的愉老。 我是一名探鬼主播场绿,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嫉入!你這毒婦竟也來了焰盗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤咒林,失蹤者是張志新(化名)和其女友劉穎姨谷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體映九,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡梦湘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了件甥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捌议。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖引有,靈堂內(nèi)的尸體忽然破棺而出瓣颅,到底是詐尸還是另有隱情,我是刑警寧澤譬正,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布宫补,位于F島的核電站檬姥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏粉怕。R本人自食惡果不足惜健民,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贫贝。 院中可真熱鬧秉犹,春花似錦、人聲如沸稚晚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽客燕。三九已至鸳劳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間也搓,已是汗流浹背棍辕。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留还绘,地道東北人楚昭。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像拍顷,于是被迫代替她去往敵國和親抚太。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容