1.引言
eShopOnWeb是基于ASP.NET Core構(gòu)建,官方創(chuàng)建這樣一個示例項(xiàng)目的目的籍凝,我想無非以下幾點(diǎn):
- 推廣ASP.NET Core
- 指導(dǎo)利用ASP.NET Core如何進(jìn)行架構(gòu)設(shè)計
- 普及架構(gòu)設(shè)計思想
eShopOnWeb 與另外一個eShopOnContainers互相補(bǔ)充。eShopOnContainers是基于微服務(wù)和容器技術(shù)的應(yīng)用程序架構(gòu)苗缩,支持多重部署。而eShopOnWeb相較于它就簡單的多声诸,其是基于傳統(tǒng)Web應(yīng)用開發(fā)酱讶,僅支持單一部署。
本文就簡單梳理下自己的所學(xué)所得彼乌。
2.MPA Or SPA
eShopOnWeb的示例項(xiàng)目中包含兩個Web項(xiàng)目泻肯,一個是基于MVC創(chuàng)建的MPA多頁面應(yīng)用渊迁,一個是基于Razor創(chuàng)建的SPA單頁面應(yīng)用。在此之間我該如何選擇呢灶挟?
- 是否需要豐富的交互行為琉朽?
- 是否足夠的前端技術(shù)積累?
- 是否主要通過API進(jìn)行交互稚铣?
3. 架構(gòu)設(shè)計
eShopOnWeb中應(yīng)用了DDD和整潔架構(gòu)的部分思想箱叁,值得了解一下。
3.1 架構(gòu)原則
關(guān)注點(diǎn)分離:簡稱SOP惕医。在分層架構(gòu)設(shè)計中耕漱,關(guān)注點(diǎn)分離是核心設(shè)計思想,每一層獨(dú)自負(fù)責(zé)不同的職責(zé)抬伺。從架構(gòu)上講螟够,可以通過將核心業(yè)務(wù)與基礎(chǔ)設(shè)施和用戶界面邏輯分離來實(shí)現(xiàn)。該原則旨在避免緊耦合峡钓,又可確保各個模塊獨(dú)立發(fā)展妓笙。
封裝:封裝的是什么?是對象的狀態(tài)和行為能岩。外部對象無需關(guān)注其內(nèi)部的實(shí)現(xiàn)機(jī)制寞宫。
在類中,通過使用訪問修飾符來限制外部的訪問來實(shí)現(xiàn)封裝捧灰。 如果外部想要操縱對象的狀態(tài)淆九,它應(yīng)該通過定義良好的函數(shù)(或?qū)傩栽O(shè)置器)來實(shí)現(xiàn),而不是直接訪問對象的私有狀態(tài)毛俏。
而不同模塊之間通過公開定義良好的接口進(jìn)行方法調(diào)用炭庙,來實(shí)現(xiàn)封裝。以隔離內(nèi)部的實(shí)現(xiàn)機(jī)制煌寇。通過封裝來確保應(yīng)用程序間不同部分之間的隔離焕蹄,正確使用封裝有助于在應(yīng)用程序設(shè)計中實(shí)現(xiàn)松耦合和模塊化。
依賴倒置:簡稱DIP阀溶。高層模塊不應(yīng)該依賴低層模塊腻脏,均應(yīng)該依賴與抽象;抽象不應(yīng)該依賴于細(xì)節(jié)银锻;細(xì)節(jié)應(yīng)該依賴于抽象永品。DIP是構(gòu)建松耦合應(yīng)用的關(guān)鍵部分,從而確保應(yīng)用程序模塊化击纬,更易于測試和維護(hù)鼎姐。 通過遵循DIP绊起,可以應(yīng)用依賴注入洁段。
顯式依賴:方法和類應(yīng)明確指定所需的協(xié)作對象(依賴)以確保正常運(yùn)行。簡單來說,對于類而言践美,提供明確的構(gòu)造函數(shù)(即在構(gòu)造函數(shù)參數(shù)中指定該類需要正常工作所需的依賴對象)朗涩,以便調(diào)用者正確傳參以正確實(shí)例化對象愿待。
單一職責(zé):簡稱SRP糠惫。SRP作為面向?qū)ο笤O(shè)計的原則之一,也適用于架構(gòu)原則姊途。其與SOP類似涉瘾。它強(qiáng)調(diào)對象應(yīng)該只有一個責(zé)任,他們只應(yīng)該僅有一個改變的理由吭净。換言之睡汹,對象應(yīng)該改變的唯一情況是它的職責(zé)需要被更新。遵守該原則寂殉,可以編寫松耦合和模塊化的應(yīng)用囚巴。因?yàn)榇罅康男碌男袨槎紤?yīng)該創(chuàng)建新類去實(shí)現(xiàn),而不是添加到已經(jīng)存在的類中友扰。添加新類永遠(yuǎn)比修改一個類安全彤叉,因?yàn)樯袩o代碼依賴于新類。
在復(fù)雜的大型應(yīng)用中村怪,可以將SRP應(yīng)用到分層應(yīng)用的各個層秽浇。展現(xiàn)職責(zé)應(yīng)保留在UI項(xiàng)目中,而數(shù)據(jù)訪問職責(zé)應(yīng)保留在基礎(chǔ)設(shè)施項(xiàng)目中甚负, 業(yè)務(wù)邏輯應(yīng)該保留在應(yīng)用程序核心項(xiàng)目中柬焕。如此,即易于測試又可以獨(dú)立于其他職責(zé)持續(xù)演化梭域。
該原則的更高級應(yīng)用斑举,就是微服務(wù)了。每個微服務(wù)負(fù)責(zé)獨(dú)立的職責(zé)病涨。
摒棄重復(fù):當(dāng)出現(xiàn)重復(fù)時富玷,應(yīng)該實(shí)施重構(gòu)。避免當(dāng)功能改進(jìn)時既穆,需要同時修改多個部分赎懦。
透明持久化:要求可以輕松切換持久化技術(shù),而實(shí)現(xiàn)持久化無感知(透明持久化)幻工。
限界上下文:該概念是DDD戰(zhàn)略設(shè)計的一部分励两,通過限界上下文來劃分領(lǐng)域,作為領(lǐng)域的顯式邊界囊颅,為領(lǐng)域提供上下文語境伐蒋,保證在領(lǐng)域之內(nèi)的一些術(shù)語工三、業(yè)務(wù)相關(guān)對象等(通用語言)有一個確切的含義迁酸,沒有二義性先鱼。
3.2. 傳統(tǒng)分層架構(gòu)和整潔架構(gòu)
傳統(tǒng)的分層架構(gòu)是大家所熟知的三層架構(gòu)。
這樣的架構(gòu)的缺點(diǎn)是:
- 依賴關(guān)系由上至下奸鬓,不易解耦
- 不易測試焙畔,需要測試數(shù)據(jù)庫
那如何解決三層架構(gòu)的問題呢,借助【依賴倒置原則】串远。
DDD的分層架構(gòu)思想和整潔架構(gòu)中都是借助【依賴倒置原則】實(shí)現(xiàn)層與層之間強(qiáng)依賴關(guān)系的解耦宏多。我們來看下整潔架構(gòu):
從該洋蔥視圖中我們可以看到:
- 依賴關(guān)系由外而內(nèi)。
- 處于核心的是實(shí)體和接口澡罚,不依賴任何其他項(xiàng)伸但。其次是領(lǐng)域服務(wù),僅依賴實(shí)體和接口留搔,也相對獨(dú)立更胖。它們統(tǒng)稱為應(yīng)用程序內(nèi)核。
- 應(yīng)用程序內(nèi)核之外是基礎(chǔ)架構(gòu)層和展現(xiàn)層隔显,彼此也不一定依賴却妨。
由于應(yīng)用程序內(nèi)核不依賴于基礎(chǔ)設(shè)施層,所以可以很容易編寫單元測試括眠。
由于UI層也不直接依賴于基礎(chǔ)設(shè)施層彪标,所以我們可以輕松置換基礎(chǔ)設(shè)施層的實(shí)現(xiàn)(比如使用內(nèi)存數(shù)據(jù)庫),以進(jìn)行集成測試掷豺。
下面我們就來看看eShopOnWeb是如何應(yīng)用整潔架構(gòu)的捞烟。
4. 項(xiàng)目結(jié)構(gòu)
首先我們看下模板架構(gòu)的項(xiàng)目結(jié)構(gòu)。
從上圖來看其項(xiàng)目結(jié)構(gòu)十分簡單当船,簡單的三層题画,加上三個測試項(xiàng)目。
三層對應(yīng):
- ApplicationCore:領(lǐng)域?qū)?/li>
- Infrastructure:基礎(chǔ)設(shè)施層
- Web/WebRazorPages:展現(xiàn)層
其實(shí)該項(xiàng)目架構(gòu)是DDD經(jīng)典四層架構(gòu)生年,只不過其將應(yīng)用層集成到展現(xiàn)層中去了婴程。
4.1 基礎(chǔ)設(shè)施層
主要提供通用的基礎(chǔ)服務(wù)和持久化。
從上圖的代碼結(jié)構(gòu)我們可以看出:
- 在Data文件夾下定義了用于持久化的商品目錄數(shù)據(jù)庫上下文
CatalogContext
和泛型倉儲EfRepository
抱婉。 - Identity文件夾下定義了身份數(shù)據(jù)庫上下文的档叔。
- Logging文件夾定義了一個日志適配器。
- Services定義了一個通用的郵件發(fā)送基礎(chǔ)服務(wù)蒸绩。
4.2. 領(lǐng)域?qū)?/h2>
領(lǐng)域?qū)邮且粋€項(xiàng)目的核心衙四,用來定義業(yè)務(wù)規(guī)則并實(shí)現(xiàn)。其主要用來實(shí)體患亿、值對象传蹈、聚合押逼、倉儲、領(lǐng)域服務(wù)和領(lǐng)域事件等惦界。
從上圖來看:
- Entities文件夾下定義了三個聚合根和相關(guān)的實(shí)體及值對象挑格。
- Exceptions文件夾定義了公共的異常。
- Interfaces文件夾定義了系列接口沾歪。
- Services文件夾定義了兩個領(lǐng)域服務(wù)漂彤。
- Specifications文件夾下是實(shí)現(xiàn)的規(guī)約模式。
4.2.1. 聚合根的相關(guān)實(shí)現(xiàn)
這里我們來看下聚合根的相關(guān)定義和實(shí)現(xiàn)灾搏。
///抽象的聚合根空接口
public interface IAggregateRoot
{ }
//所有的實(shí)體基類
public class BaseEntity
{
public int Id { get; set; }
}
//購物車聚會根
public class Basket : BaseEntity, IAggregateRoot
{
public string BuyerId { get; set; }
private readonly List<BasketItem> _items = new List<BasketItem>();
public IReadOnlyCollection<BasketItem> Items => _items.AsReadOnly();
public void AddItem(int catalogItemId, decimal unitPrice, int quantity = 1)
{
if (!Items.Any(i => i.CatalogItemId == catalogItemId))
{
_items.Add(new BasketItem()
{
CatalogItemId = catalogItemId,
Quantity = quantity,
UnitPrice = unitPrice
});
return;
}
var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
existingItem.Quantity += quantity;
}
}
從這個實(shí)現(xiàn)中我們可以學(xué)習(xí)到:
通過定義一個空的接口
IAggregateRoot
挫望,要求所有的聚會根來實(shí)現(xiàn)它。
這樣做的體現(xiàn)了什么思想:
- 面向接口編程
- 約定大于配置
- 依賴注入
通過定義一個
BaseEntity
狂窑,要求所有的實(shí)體繼承它媳板。
為什么這樣做?
- 因?yàn)閷?shí)體的特征是具有唯一的身份標(biāo)識泉哈,所以通過在父類來定義
Id
屬性來實(shí)現(xiàn)蛉幸。這也就是層超類型的實(shí)現(xiàn)方式。
這樣做有什么缺點(diǎn)旨巷?
因?yàn)樗袑?shí)體的主鍵類型不一定都是int類型巨缘,所以這個基類型最好改成泛型。
Basket聚合根中將Items定位為Readonly采呐,是為了封裝集合若锁,避免子項(xiàng)被其他地方更改。
4.2.2. 倉儲的相關(guān)實(shí)現(xiàn)
倉儲是用來透明持久化領(lǐng)域?qū)ο蟮摹?/p>
public interface IRepository<T> where T : BaseEntity
{
T GetById(int id);
T GetSingleBySpec(ISpecification<T> spec);
IEnumerable<T> ListAll();
IEnumerable<T> List(ISpecification<T> spec);
T Add(T entity);
void Update(T entity);
void Delete(T entity);
}
public interface IAsyncRepository<T> where T : BaseEntity
{
Task<T> GetByIdAsync(int id);
Task<List<T>> ListAllAsync();
Task<List<T>> ListAsync(ISpecification<T> spec);
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
從以上代碼我們可以學(xué)到兩點(diǎn):
- 面向接口編程
- 職責(zé)分離斧吐,同步異步接口分離又固。
4.2.3. 領(lǐng)域服務(wù)相關(guān)實(shí)現(xiàn)
領(lǐng)域服務(wù)用來實(shí)現(xiàn)業(yè)務(wù)邏輯的。
public interface IOrderService
{
Task CreateOrderAsync(int basketId, Address shippingAddress);
}
public class OrderService : IOrderService
{
private readonly IAsyncRepository<Order> _orderRepository;
private readonly IAsyncRepository<Basket> _basketRepository;
private readonly IAsyncRepository<CatalogItem> _itemRepository;
public OrderService(IAsyncRepository<Basket> basketRepository,
IAsyncRepository<CatalogItem> itemRepository,
IAsyncRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
_basketRepository = basketRepository;
_itemRepository = itemRepository;
}
public async Task CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
var items = new List<OrderItem>();
foreach (var item in basket.Items)
{
var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri);
var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity);
items.Add(orderItem);
}
var order = new Order(basket.BuyerId, shippingAddress, items);
await _orderRepository.AddAsync(order);
}
從以上代碼我們可以學(xué)習(xí)到:
- 依賴注入
- 領(lǐng)域服務(wù)負(fù)責(zé)實(shí)現(xiàn)真正的業(yè)務(wù)邏輯
4.3. 應(yīng)用層和展現(xiàn)層
如上面所闡述煤率,在示例項(xiàng)目中應(yīng)用層和展現(xiàn)層合二為一仰冠。應(yīng)用層負(fù)責(zé)展現(xiàn)層與領(lǐng)域?qū)又g的協(xié)調(diào),協(xié)調(diào)業(yè)務(wù)對象來執(zhí)行特定的應(yīng)用程序蝶糯。
5. 面向切面編程(AOP)
eShopOnWeb中也提到了AOP洋只,介紹了在ASP.NET Core中如何應(yīng)用過濾器來進(jìn)行AOP,比如:身份驗(yàn)證昼捍、模型驗(yàn)證识虚、輸出緩存和錯誤處理等。
5. 簡明DDD
在eShopOnWeb中妒茬,也對DDD的概念担锤,是否使用,何時使用乍钻,何時不用肛循,都略有介紹铭腕。這里就摘錄一二,當(dāng)然也可以參考我之前的寫的DDD理論學(xué)習(xí)系列多糠。
結(jié)論
- DDD首先是一個方法論累舷,其注重于領(lǐng)域的合理建模,分為戰(zhàn)略建模和戰(zhàn)術(shù)建模熬丧。
- 如果你不知道你需要它笋粟,那么你可能不需要它。
- 如果你不知道到DDD用于解決什么問題析蝴,那么你可能沒有遇到這些問題。
- DDD倡導(dǎo)者也經(jīng)常指出其僅適用于大型項(xiàng)目 (>6個月)绿淋。
相關(guān)概念
- DDD是用來對真實(shí)世界系統(tǒng)或流程的建模闷畸。
- 使用DDD時,你需要和領(lǐng)域?qū)<揖o密合作吞滞,領(lǐng)域?qū)<夷軌蚪忉屨鎸?shí)的系統(tǒng)該如何運(yùn)行佑菩。在和領(lǐng)域?qū)<业慕涣髦写_定通用語言,其主要用來描述系統(tǒng)中的一些概念裁赠。而之所以是通用殿漠,是因?yàn)椴还苁情_發(fā)人員還是領(lǐng)域?qū)<叶紤?yīng)能夠讀懂。而通用語言描述的概念將構(gòu)成面向?qū)ο笤O(shè)計的基礎(chǔ)佩捞。其體現(xiàn)在代碼中的理想狀態(tài)是代碼即設(shè)計绞幌。
戰(zhàn)術(shù)
- 值對象:不可變。
- 實(shí)體:具有唯一標(biāo)識符可變一忱。
- 聚會根:在DDD中莲蜘,用來表示整體與部分的關(guān)系,聚合是將相關(guān)聯(lián)的領(lǐng)域?qū)ο筮M(jìn)行顯式分組帘营,來表達(dá)整體的概念(也可以是單一的領(lǐng)域?qū)ο螅┢鼻1热鐚⒈硎居唵闻c訂單項(xiàng)的領(lǐng)域?qū)ο筮M(jìn)行組合,來表達(dá)領(lǐng)域中訂單這個整體概念芬迄。
- 倉儲:一種持久化的模式问顷,用于隔離具體持久化措施,實(shí)現(xiàn)透明持久化禀梳。
- 工廠:用于對象的創(chuàng)建杜窄。
- 服務(wù):應(yīng)用服務(wù)和領(lǐng)域服務(wù)。領(lǐng)域服務(wù)負(fù)責(zé)業(yè)務(wù)邏輯出皇,應(yīng)用服務(wù)用于表達(dá)業(yè)務(wù)用例和用戶故事羞芍。
戰(zhàn)略
- 限界上下文:來為領(lǐng)域提供上下文語境,保證在領(lǐng)域之內(nèi)的一些術(shù)語郊艘、業(yè)務(wù)相關(guān)對象等(通用語言)有一個確切的含義荷科,沒有二義性唯咬。
- 上下文映射圖:限界上下文之間的關(guān)聯(lián)關(guān)系。
6. 應(yīng)用測試
在eShopOnWeb中畏浆,還示例了三個測試項(xiàng)目胆胰,來指導(dǎo)我們合理的進(jìn)行測試。
7. 總結(jié)
總體而言蜀涨,示例項(xiàng)目簡單容易理解,也主要是為了便于推廣和演示蝎毡。但里面涉及的知識點(diǎn)并沒有想象的那么簡單厚柳,從架構(gòu)原則到設(shè)計和應(yīng)用,每一個環(huán)節(jié)都包含不簡單的知識體系沐兵。
所以等什么呢别垮?結(jié)合示例項(xiàng)目和官方文檔使用 ASP.NET Core 和 Azure 構(gòu)建新式 Web 應(yīng)用程序開始學(xué)習(xí)吧,相信你也會收獲頗豐扎谎。