eShopOnWeb 知多少

构建现代Web应用

1.引言

eShopOnWeb是基于ASP.NET Core构建,官方创建这样一个示例项目的目的,我想无非以下几点:

  1. 推广ASP.NET Core
  2. 指导利用ASP.NET Core如何进行架构设计
  3. 普及架构设计思想

eShopOnWeb 与另外一个eShopOnContainers互相补充。eShopOnContainers是基于微服务和容器技术的应用程序架构,支持多重部署。而eShopOnWeb相较于它就简单的多,其是基于传统Web应用开发,仅支持单一部署。

本文就简单梳理下自己的所学所得。

2.MPA Or SPA

eShopOnWeb的示例项目中包含两个Web项目,一个是基于MVC创建的MPA多页面应用,一个是基于Razor创建的SPA单页面应用。在此之间我该如何选择呢?

  1. 是否需要丰富的交互行为?
  2. 是否足够的前端技术积累?
  3. 是否主要通过API进行交互?
决策表:Mpa or Spa

3. 架构设计

eShopOnWeb中应用了DDD和整洁架构的部分思想,值得了解一下。

3.1 架构原则

关注点分离:简称SOP。在分层架构设计中,关注点分离是核心设计思想,每一层独自负责不同的职责。从架构上讲,可以通过将核心业务与基础设施和用户界面逻辑分离来实现。该原则旨在避免紧耦合,又可确保各个模块独立发展。

封装:封装的是什么?是对象的状态和行为。外部对象无需关注其内部的实现机制。
在类中,通过使用访问修饰符来限制外部的访问来实现封装。 如果外部想要操纵对象的状态,它应该通过定义良好的函数(或属性设置器)来实现,而不是直接访问对象的私有状态。
而不同模块之间通过公开定义良好的接口进行方法调用,来实现封装。以隔离内部的实现机制。通过封装来确保应用程序间不同部分之间的隔离,正确使用封装有助于在应用程序设计中实现松耦合和模块化。

依赖倒置:简称DIP。高层模块不应该依赖低层模块,均应该依赖与抽象;抽象不应该依赖于细节;细节应该依赖于抽象。DIP是构建松耦合应用的关键部分,从而确保应用程序模块化,更易于测试和维护。 通过遵循DIP,可以应用依赖注入。

显式依赖:方法和类应明确指定所需的协作对象(依赖)以确保正常运行。简单来说,对于类而言,提供明确的构造函数(即在构造函数参数中指定该类需要正常工作所需的依赖对象),以便调用者正确传参以正确实例化对象。

单一职责:简称SRP。SRP作为面向对象设计的原则之一,也适用于架构原则。其与SOP类似。它强调对象应该只有一个责任,他们只应该仅有一个改变的理由。换言之,对象应该改变的唯一情况是它的职责需要被更新。遵守该原则,可以编写松耦合和模块化的应用。因为大量的新的行为都应该创建新类去实现,而不是添加到已经存在的类中。添加新类永远比修改一个类安全,因为尚无代码依赖于新类。
在复杂的大型应用中,可以将SRP应用到分层应用的各个层。展现职责应保留在UI项目中,而数据访问职责应保留在基础设施项目中, 业务逻辑应该保留在应用程序核心项目中。如此,即易于测试又可以独立于其他职责持续演化。
该原则的更高级应用,就是微服务了。每个微服务负责独立的职责。

摒弃重复:当出现重复时,应该实施重构。避免当功能改进时,需要同时修改多个部分。

透明持久化:要求可以轻松切换持久化技术,而实现持久化无感知(透明持久化)。

限界上下文:该概念是DDD战略设计的一部分,通过限界上下文来划分领域,作为领域的显式边界,为领域提供上下文语境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。

3.2. 传统分层架构和整洁架构

传统的分层架构是大家所熟知的三层架构。


传统三层架构
三层架构示例

这样的架构的缺点是:

  1. 依赖关系由上至下,不易解耦
  2. 不易测试,需要测试数据库

那如何解决三层架构的问题呢,借助【依赖倒置原则】。
DDD的分层架构思想和整洁架构中都是借助【依赖倒置原则】实现层与层之间强依赖关系的解耦。我们来看下整洁架构:

整洁架构——洋葱视图

从该洋葱视图中我们可以看到:

  1. 依赖关系由外而内。
  2. 处于核心的是实体和接口,不依赖任何其他项。其次是领域服务,仅依赖实体和接口,也相对独立。它们统称为应用程序内核
  3. 应用程序内核之外是基础架构层和展现层,彼此也不一定依赖。

由于应用程序内核不依赖于基础设施层,所以可以很容易编写单元测试。


单元测试位置

由于UI层也不直接依赖于基础设施层,所以我们可以轻松置换基础设施层的实现(比如使用内存数据库),以进行集成测试。


集成测试位置
整洁架构——水平视图

下面我们就来看看eShopOnWeb是如何应用整洁架构的。

4. 项目结构

首先我们看下模板架构的项目结构。


eShopOnWeb Solution

从上图来看其项目结构十分简单,简单的三层,加上三个测试项目。
三层对应:

  1. ApplicationCore:领域层
  2. Infrastructure:基础设施层
  3. Web/WebRazorPages:展现层
DDD使用的传统分层架构

其实该项目架构是DDD经典四层架构,只不过其将应用层集成到展现层中去了。


Web应用服务

4.1 基础设施层

主要提供通用的基础服务和持久化。


Infrastructure

从上图的代码结构我们可以看出:

  1. 在Data文件夹下定义了用于持久化的商品目录数据库上下文CatalogContext和泛型仓储EfRepository
  2. Identity文件夹下定义了身份数据库上下文的。
  3. Logging文件夹定义了一个日志适配器。
  4. Services定义了一个通用的邮件发送基础服务。

4.2. 领域层

领域层是一个项目的核心,用来定义业务规则并实现。其主要用来实体、值对象、聚合、仓储、领域服务和领域事件等。


ApplicationCore

从上图来看:

  1. Entities文件夹下定义了三个聚合根和相关的实体及值对象。
  2. Exceptions文件夹定义了公共的异常。
  3. Interfaces文件夹定义了系列接口。
  4. Services文件夹定义了两个领域服务。
  5. Specifications文件夹下是实现的规约模式。

4.2.1. 聚合根的相关实现

这里我们来看下聚合根的相关定义和实现。

///抽象的聚合根空接口
public interface IAggregateRoot
    { }
//所有的实体基类
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;
    }
}

从这个实现中我们可以学习到:

通过定义一个空的接口IAggregateRoot,要求所有的聚会根来实现它。

这样做的体现了什么思想:

  1. 面向接口编程
  2. 约定大于配置
  3. 依赖注入

通过定义一个BaseEntity,要求所有的实体继承它。

为什么这样做?

  1. 因为实体的特征是具有唯一的身份标识,所以通过在父类来定义Id属性来实现。这也就是层超类型的实现方式。

这样做有什么缺点?
因为所有实体的主键类型不一定都是int类型,所以这个基类型最好改成泛型。

Basket聚合根中将Items定位为Readonly,是为了封装集合,避免子项被其他地方更改。

4.2.2. 仓储的相关实现

仓储是用来透明持久化领域对象的。

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);
}

从以上代码我们可以学到两点:

  1. 面向接口编程
  2. 职责分离,同步异步接口分离。

4.2.3. 领域服务相关实现

领域服务用来实现业务逻辑的。

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);
    }

从以上代码我们可以学习到:

  1. 依赖注入
  2. 领域服务负责实现真正的业务逻辑

4.3. 应用层和展现层

如上面所阐述,在示例项目中应用层和展现层合二为一。应用层负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序。

5. 面向切面编程(AOP)

eShopOnWeb中也提到了AOP,介绍了在ASP.NET Core中如何应用过滤器来进行AOP,比如:身份验证、模型验证、输出缓存和错误处理等。


通过过滤器和请求管道执行
执行顺序

5. 简明DDD

在eShopOnWeb中,也对DDD的概念,是否使用,何时使用,何时不用,都略有介绍。这里就摘录一二,当然也可以参考我之前的写的DDD理论学习系列

结论

  1. DDD首先是一个方法论,其注重于领域的合理建模,分为战略建模和战术建模。
  2. 如果你不知道你需要它,那么你可能不需要它。
  3. 如果你不知道到DDD用于解决什么问题,那么你可能没有遇到这些问题。
  4. DDD倡导者也经常指出其仅适用于大型项目 (>6个月)。

相关概念

  1. DDD是用来对真实世界系统或流程的建模。
  2. 使用DDD时,你需要和领域专家紧密合作,领域专家能够解释真实的系统该如何运行。在和领域专家的交流中确定通用语言,其主要用来描述系统中的一些概念。而之所以是通用,是因为不管是开发人员还是领域专家都应能够读懂。而通用语言描述的概念将构成面向对象设计的基础。其体现在代码中的理想状态是代码即设计

战术

  1. 值对象:不可变。
  2. 实体:具有唯一标识符可变。
  3. 聚会根:在DDD中,用来表示整体与部分的关系,聚合是将相关联的领域对象进行显式分组,来表达整体的概念(也可以是单一的领域对象)。比如将表示订单与订单项的领域对象进行组合,来表达领域中订单这个整体概念。
  4. 仓储:一种持久化的模式,用于隔离具体持久化措施,实现透明持久化。
  5. 工厂:用于对象的创建。
  6. 服务:应用服务和领域服务。领域服务负责业务逻辑,应用服务用于表达业务用例和用户故事。

战略

  1. 限界上下文:来为领域提供上下文语境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。
  2. 上下文映射图:限界上下文之间的关联关系。

6. 应用测试

在eShopOnWeb中,还示例了三个测试项目,来指导我们合理的进行测试。


单元测试、集成测试和功能测试的区别

7. 总结

总体而言,示例项目简单容易理解,也主要是为了便于推广和演示。但里面涉及的知识点并没有想象的那么简单,从架构原则到设计和应用,每一个环节都包含不简单的知识体系。

所以等什么呢?结合示例项目和官方文档使用 ASP.NET Core 和 Azure 构建新式 Web 应用程序开始学习吧,相信你也会收获颇丰。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,698评论 5 463
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,711评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,795评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,699评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,567评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,423评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,822评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,467评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,755评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,789评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,577评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,418评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,837评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,054评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,355评论 1 254
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,820评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,027评论 2 338

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,155评论 25 707
  • 引言 在讨论DDD分层架构的模式之前,我们先一起回顾一下DDD和分层架构的相关知识。 DDD DDD(Domain...
    _张晓龙_阅读 160,195评论 16 193
  • 如何和真我――宇宙真实有效地沟通 真我是宇宙的一部分,与真我连接,你可以得到你渴望得到的全部 伊朗,美国,中国人们...
    幸福的姿瑶宝贝阅读 1,375评论 0 2
  • 其实婚假休到了本周三,周四才回来上班,本周只上了两天班,不知为什么却感到很疲惫,整个人的身体感觉都很累。 休个周末...
    jiaojiao者阅读 385评论 0 3
  • 每个人的生活中会经历快乐,伤心,尴尬,惭愧的那一刻。有的不值一提,有的使我们铭记深刻。 曾经我经历过很...
    2020级1班阅读 425评论 4 0