ASP.NET-Core的应用架构 (1)
ASP.NET Core非常适合在云上部署,因为它对内存的占用很小,并且具有很高的吞吐量。所以不需要强大的服务器即可流畅运行,非常适合云的环境特点。特别是近来随着.net的开源以及对Linux平台的支持和Docker Container的支持,.Net也越来越在国外流行起来成为主流开发技术和平台。
在具体的前端架框架上,我们可以选择传统的Web Apps (又分为MVC和Razor)、SPA(如Angular)和Blazor(仍然在不断完善中)。
当我们设计应用程序架构时,需要注意以下几点:
- Seperation of concerns: 也就是需要根据做的事情的不同来划分模块或功能,比如如果要选择并按照一定格式显示某种商品,那么选择的逻辑和格式化的逻辑应该封装在不同的模块里。
- Encapsulation(封装):模块的完整逻辑应该都封装在内部,和外部的交互仅依赖于external contract。这个理论上很好理解,但真正做到并不容易,典型的反例是在DDD中,为了保证领域层的纯粹性,往往会把一些仓储操作放到仓储层,造成部分业务逻辑也随着搬移到仓储层。比如确保用户修改的邮件地址需要在整个系统中唯一,那么这个查询邮件是否重复的函数是定义在User里呢,还是定义在Repository里?前者会造成User对仓储的访问从而影响领域模型的纯洁性,后者则相当于部分业务逻辑分布到了领域层。
- Dependency inversion (依赖反转):这个又和依赖注入密切相关。传统上我们的User Class会直接调用UserRepository, 但如果我们在User Class所在的Layer定义IRepository接口,然后将该接口注入到User中,并且基础架构层里的UserRepository也实现该接口,这样依赖的方向就反转了。如此一来可以极大的提高程序的可维护性、模块化和可测试性。
如果我们按照依赖反转的原则和Domain-Driven Design设计我们的架构,那么最后的架构往往是非常相似的,这样的架构近来被称为Clean Architecture。这样的架构把业务逻辑和应用模型作为整个架构的核心。它的依赖反转体现在infrastructure层依赖于Application Core. 其实也就是在Application层定义该层需要的接口,然后infrastructure层来实现。UI和基础架构层都依赖于Application Core.
Controller的调停者模式
在开发ASP.NET页面时,我们可以选择Razor,也可以选择MVC. 使用MVC时,由于单个controller可以包含多个View和Action, 那我们需要注意不要让单个Controller变得太臃肿。(这一点和在设计Repository时要避免方法过多类似)。具体方法,除了换成Razor Page外,还可以考虑使用mediator模式(调停者)。我们的问题可以进一步描述如下:
controller中action太多,导致太多的dependency, 从而又导致需要把大量的关联类注入到controller中。
引入mediator后,controller只需要依赖一个mediator的实例,然后在controller的action中需要通过该mediator发送message, 该message再有单独的handler处理。所以该模式的实质是把controller的责任优雅的通过mediator分配到不同的handler中处理,下面是一个例子:
public class OrderController Controller { private readonly IMediator mediator; public OrderController(IMediator mediator) { mediator mediator; } [HttpGet] public async Task<IActionResult> MyOrders() { var viewModel await mediator.Send(new GetMyorders(User.Identity.Name)); return View(viewModel); } //other actions implemented similarly
在这个例子中,action MyOrders用于查询当前用户的order.该请求封装在GetMyOrders类中,传递给mediator, 并由下面的GetMyOrdersHandler处理。
public class GetMyordersHandler : IRequestHandler<GetMyorders, IEnumerable<OrderViewModel>> { private readonly IOrderRepository orderRepository; public GetMyordersHandler(IOrderRepository orderRepository) { _orderRepository orderRepository; } public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request, CancellationToken cancellationToken) { var specification = new CustomerOrdersWithItemsSpecification(request.UserName); var orders = await orderRepository.ListAsync(specification); return orders.Select(o = new OrderViewModel { OrderDate = o.OrderDate, OrderItems = o.OrderItems?.Select(oi = new OrderItemViewModel() { PictureUrl = oi.Itemordered.PictureUri, ProductId = oi.Itemordered.CatalogItemId, ProductName = oi.ItemOrdered.ProductName, UnitPrice = oi.UnitPrice }) } }
这样controller可以重新聚焦在它的本质工作:路由和模型绑定上。而处理每个end point请求的重任则落在在每个handler身上。
最后我们再来看看依赖注入。
- 陷阱之一:Static Cling
在设计系统的依赖时,我们要避免Static Cling。所谓的Static Cling,就是代码直接调用静态方法产生强耦合。这样因为无法替换具体实现而对测试造成影响。特别是当这段代码并不是核心业务逻辑不需要测试时,因为难以绕开而产生干扰。比如下面的例子。
public class CheckoutController { public void Checkout(Order order) { // verify payment // verify inventory LogHelper.LogOrder(order); } } public static class LogHelper { public static void LogOrder(Order order) { using (System.IO.StreamWriter file = new System.IO.StreamWriter(@"C:\\Users\\Steve\\OrderLog.txt", true)) { file.WriteLine("{0} checked out.", order.Id); } } } public class Order { public int Id { get; set; } }
这段代码如何重构?当然是把静态方法改成实例方法并注入,但是如果该静态方法无法修改呢?那我们可以通过再封装一层adaptor来调用,进而实现重构,代码如下:
public class CheckoutController { private readonly IOrderLoggerAdapter _orderLoggerAdapter; public CheckoutController(IOrderLoggerAdapter orderLoggerAdapter) { _orderLoggerAdapter = orderLoggerAdapter; } public CheckoutController() : this(new FileOrderLoggerAdapter()) { } public void Checkout(Order order) { // verify payment // verify inventory _orderLoggerAdapter.LogOrder(order); } } public static class LogHelper { public static void LogOrder(Order order) { using (System.IO.StreamWriter file = new System.IO.StreamWriter(@"C:\\Users\\Steve\\OrderLog.txt", true)) { file.WriteLine("{0} checked out.", order.Id); } } } public interface IOrderLoggerAdapter { void LogOrder(Order order); } public class FileOrderLoggerAdapter : IOrderLoggerAdapter { public void LogOrder(Order order) { LogHelper.LogOrder(order); } } public class Order { public int Id { get; set; } }
- 陷阱之二:new is glue
也就是避免使用new,从而产生耦合。new就像一纸婚约,把两个class牢牢的绑定在了一起。
在.net 6和Visual Studio 2022中,我们默认在Program.cs中声明我们所有的dependencies.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· Open-Sora 2.0 重磅开源!