(28)ASP.NET Core8.0 SOLID原则
1.概述
我们来深入探讨一下如何在ASP.NET Core项目中应用SOLID原则。我将为每个原则提供一个反面案例(Bad Example)和一个正面案例(Good Example),并结合ASP.NET Core的特性进行分析。SOLID是五个面向对象编程和设计的重要原则的首字母缩写,它们共同作用是构建出易于维护、扩展和测试的软件:
①S - 单一职责原则
②O - 开闭原则
③L - 里氏替换原则
④I - 接口隔离原则
⑤D - 依赖倒置原则
2.S - 单一职责原则
核心思想:一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一件事。
2.1反面案例
一个典型的违反SRP的案例是“肥胖控制器”或“肥胖服务”。这个类承担了过多的职责。请看如下示例:
// 违反 SRP 的 Controller
[ApiController]
[Route("api/[controller]")]
public class UglyOrderController : ControllerBase
{private readonly ApplicationDbContext _context;private readonly IEmailSender _emailSender;public UglyOrderController(ApplicationDbContext context, IEmailSender emailSender){_context = context;_emailSender = emailSender;}[HttpPost]public async Task<IActionResult> CreateOrder(UglyOrder order){// 职责 1:业务逻辑验证if (order.Quantity <= 0)return BadRequest("Invalid quantity");// 职责 2:数据访问操作_context.Orders.Add(order);await _context.SaveChangesAsync();// 职责 3:发送邮件通知var emailMessage = $"Order {order.Id} created successfully!";await _emailSender.SendEmailAsync("admin@store.com", "New Order", emailMessage);// 职责 4:返回结果return Ok(order);}// 它还可能有其他方法,比如获取订单、更新订单等,每个方法都混杂着各种逻辑...
}
问题:这个UglyOrderController的CreateOrder方法做了太多事:验证、数据持久化、发送邮件、协调流程。如果邮件模板需要修改、数据访问逻辑变化(比如换用Dapper)或者验证规则改变,你都需要修改这个类。这使得它非常脆弱,难以测试。
2.2正面案例
将不同的职责拆分到不同的类中,让每个类各司其职。Controller的职责应该仅仅是协调工作流、处理HTTP请求和响应。请看如下示例:
// 负责业务逻辑的Service
public interface IOrderService
{Task<Order> CreateOrderAsync(Order order);
}public class OrderService : IOrderService
{private readonly IOrderRepository _orderRepository;private readonly IOrderValidator _validator;private readonly INotificationService _notificationService;// 依赖通过接口注入,符合 DIPpublic OrderService(IOrderRepository repository, IOrderValidator validator, INotificationService notificationService){_orderRepository = repository;_validator = validator;_notificationService = notificationService;}public async Task<Order> CreateOrderAsync(Order order){// 使用专门的验证器if (!_validator.Validate(order))throw new ArgumentException("Invalid order");// 调用仓储进行数据持久化var createdOrder = await _orderRepository.AddAsync(order);// 调用通知服务发送邮件await _notificationService.SendOrderCreatedEmail(createdOrder);return createdOrder;}
}// 负责数据访问的 Repository
public interface IOrderRepository
{Task<Order> AddAsync(Order order);
}// 负责验证的 Validator
public interface IOrderValidator
{bool Validate(Order order);
}// 负责通知的 Service
public interface INotificationService
{Task SendOrderCreatedEmail(Order order);
}// 精简后的 Controller,职责单一
[ApiController]
[Route("api/[controller]")]
public class CleanOrderController : ControllerBase
{private readonly IOrderService _orderService;public CleanOrderController(IOrderService orderService){_orderService = orderService;}[HttpPost]public async Task<IActionResult> CreateOrder(Order order){try{var createdOrder = await _orderService.CreateOrderAsync(order);return Ok(createdOrder);}catch (ArgumentException ex){return BadRequest(ex.Message);}}
}
好处:
①可维护性:每个类的变化原因都是单一的。修改邮件模板只需改动NotificationService。
②可测试性:可以轻松对OrderService、OrderValidator等进行单元测试,通过Mock其依赖项。
③可读性:代码结构清晰,一目了然。
3. O - 开闭原则
核心思想:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
3.1反面案例
当需要添加新功能时,通过修改已有的、已经测试好的类来实现。请看如下示例:
public class PaymentProcessor
{public void ProcessPayment(PaymentRequest request, PaymentType type){if (type == PaymentType.CreditCard){// 处理信用卡支付的复杂逻辑Console.WriteLine("Processing credit card payment...");}else if (type == PaymentType.PayPal){// 处理 PayPal 支付的复杂逻辑Console.WriteLine("Processing PayPal payment...");}// 如果要新增一个支付宝支付,就必须回来修改这个类的代码,添加一个新的 else if// else if (type == PaymentType.Alipay) { ... }}
}public enum PaymentType
{CreditCard,PayPal,Alipay
}
问题:每次新增一种支付方式,都必须修改ProcessPayment方法。这破坏了原有的、可能已经稳定的逻辑,引入了新的风险,并且需要重新测试所有支付流程。
3.2正面案例
使用抽象(接口或抽象类)来实现开闭原则。通过添加新的实现来扩展功能,而不是修改旧代码。请看如下示例:
// 抽象策略
public interface IPaymentStrategy
{bool IsMatch(PaymentType type);Task ProcessPaymentAsync(PaymentRequest request);
}// 具体策略实现
public class CreditCardPaymentStrategy : IPaymentStrategy
{public bool IsMatch(PaymentType type) => type == PaymentType.CreditCard;public async Task ProcessPaymentAsync(PaymentRequest request){Console.WriteLine("Processing credit card payment...");await Task.CompletedTask;}
}public class PayPalPaymentStrategy : IPaymentStrategy
{public bool IsMatch(PaymentType type) => type == PaymentType.PayPal;public async Task ProcessPaymentAsync(PaymentRequest request){Console.WriteLine("Processing PayPal payment...");await Task.CompletedTask;}
}// 上下文或处理器(对修改关闭)
public class PaymentProcessorOCP
{private readonly IEnumerable<IPaymentStrategy> _paymentStrategies;// 通过依赖注入注入所有策略(ASP.NET Core DI 支持注入 IEnumerable<T>)public PaymentProcessorOCP(IEnumerable<IPaymentStrategy> paymentStrategies){_paymentStrategies = paymentStrategies;}public async Task ProcessPaymentAsync(PaymentRequest request, PaymentType type){var strategy = _paymentStrategies.FirstOrDefault(s => s.IsMatch(type));if (strategy == null)throw new ArgumentException($"No payment strategy found for {type}");await strategy.ProcessPaymentAsync(request);}
}// 扩展:新增支付宝支付(对扩展开放)
public class AlipayPaymentStrategy : IPaymentStrategy
{public bool IsMatch(PaymentType type) => type == PaymentType.Alipay;public async Task ProcessPaymentAsync(PaymentRequest request){Console.WriteLine("Processing Alipay payment..."); // 新的实现await Task.CompletedTask;}
}
在Program.cs中注册:
services.AddScoped<IPaymentStrategy, CreditCardPaymentStrategy>();
services.AddScoped<IPaymentStrategy, PayPalPaymentStrategy>();
services.AddScoped<IPaymentStrategy, AlipayPaymentStrategy>(); // 新增策略只需注册
services.AddScoped<PaymentProcessorOCP>();
好处:
①PaymentProcessorOCP的核心逻辑ProcessPaymentAsync方法不再需要修改。
②要支持新的支付方式,只需创建一个新的IPaymentStrategy实现并在DI容器中注册即可。系统功能得到了扩展,但核心模块保持关闭。
③这本质上是策略模式的应用。
4.L - 里氏替换原则
核心思想:子类必须能够替换掉它们的父类,而不破坏程序。即所有引用基类的地方必须能透明地使用其子类的对象。
4.1反面案例
子类修改了父类的预期行为,导致替换后程序出错。请看如下示例:
public virtual class Bird
{public virtual void Fly(){Console.WriteLine("Flying high!");}
}public class Duck : Bird { } // 鸭子会飞,符合预期public class Ostrich : Bird // 鸵鸟是鸟,但不会飞
{public override void Fly(){throw new NotImplementedException("Ostriches can't fly!");}
}// 使用的地方
public class BirdWatcher
{public void WatchBird(Bird bird){try{bird.Fly(); // 如果传入 Ostrich,这里会抛出异常,程序中断}catch (NotImplementedException ex){// 这破坏了程序的正确性}}
}
问题:Ostrich虽然继承自Bird,但它不能在不抛出异常的情况下完成Fly方法。这意味着它不能无缝替换Bird,违反了LSP。
4.2正面案例
重新设计继承体系,确保行为的一致性。通常意味着需要更精确的抽象。请看如下示例:
// 更精确的抽象
public abstract class Bird { }public interface IFlyable
{void Fly();
}// 只会飞的不是所有鸟的基类,而是实现了IFlyable的鸟
public class Sparrow : Bird, IFlyable
{public void Fly(){Console.WriteLine("Sparrow flying!");}
}public class Ostrich : Bird { } // 鸵鸟不会飞,它没有实现IFlyable// 使用的地方
public class BirdWatcherLSP
{// 这个方法只关心会飞的鸟public void WatchFlyableBird(IFlyable flyableBird){flyableBird.Fly(); // 可以安全调用,传入任何实现IFlyable的对象都不会出错}// 这个方法关心所有的鸟public void WatchAnyBird(Bird bird){Console.WriteLine("Watching a bird.");// 如果想知道它会不会飞,可以检查类型if (bird is IFlyable flyableBird){flyableBird.Fly();}}
}
好处:
①保证了继承层次结构的合理性。子类(Sparrow, Ostrich)都可以完美地替换父类(Bird)出现在期望是“鸟”的场合(WatchAnyBird方法)。
②对于需要“飞”这个特定行为的场合(WatchFlyableBird方法),我们依赖更窄的接口IFlyable,从而保证了行为的一致性,任何传递进来的对象都必定实现了Fly方法。
5.I - 接口隔离原则
核心思想:客户端不应该被迫依赖于它不使用的接口。将一个庞大的接口拆分成更小、更具体的接口。
5.1反面案例
创建一个“胖接口”,包含很多方法,导致实现类必须实现一些它根本不需要的方法。请看如下示例:
// 一个什么都做的“上帝接口”
public interface IMonolithicRepository
{// Order 相关Task<Order> GetOrderByIdAsync(int id);Task AddOrderAsync(Order order);Task UpdateOrderAsync(Order order);Task DeleteOrderAsync(int id);// Product 相关Task<Product> GetProductByIdAsync(int id);Task<List<Product>> GetAllProductsAsync();Task AddProductAsync(Product product);// ... 可能还有 User, Customer 等方法
}// 一个只关心订单的 Service
public class OrderService
{private readonly IMonolithicRepository _repository;public OrderService(IMonolithicRepository repository) // 被迫依赖整个大接口{_repository = repository;}// 它只使用 GetOrderByIdAsync 和 AddOrderAsync
}// 实现这个接口是场灾难
public class MonolithicRepository : IMonolithicRepository
{// Order 方法public Task AddOrderAsync(Order order) { /* ... */ }public Task<Order> GetOrderByIdAsync(int id) { /* ... */ }// Product 方法:OrderService 根本不需要这些,但它被迫实现了public Task AddProductAsync(Product product) => throw new NotImplementedException();public Task<List<Product>> GetAllProductsAsync() => throw new NotImplementedException();public Task<Product> GetProductByIdAsync(int id) => throw new NotImplementedException();// ... 其他不需要的方法全部抛异常
}
问题:
①OrderService依赖了一个它不需要的大接口。
②MonolithicRepository实现了许多冗余方法,仅仅是为了满足接口契约,这很容易导致NotImplementedException。
③接口和实现类之间的耦合度过高,任何对IMonolithicRepository的修改都会影响到所有实现类。
5.2正面案例
将大接口拆分成多个高度内聚的小接口。请看如下示例:
// 1. 定义多个特定于功能的接口
public interface IOrderRepository
{Task<Order> GetOrderByIdAsync(int id);Task AddOrderAsync(Order order);Task UpdateOrderAsync(Order order);Task DeleteOrderAsync(int id);
}public interface IProductRepository
{Task<Product> GetProductByIdAsync(int id);Task<List<Product>> GetAllProductsAsync();Task AddProductAsync(Product product);Task UpdateProductAsync(Product product);
}// 2. 服务只依赖于它真正需要的接口
public class OrderServiceISP
{private readonly IOrderRepository _orderRepository; // 只依赖订单仓储public OrderServiceISP(IOrderRepository orderRepository){_orderRepository = orderRepository;}// 使用 _orderRepository...
}// 3. 实现类可以只实现需要的接口,也可以实现多个
public class DapperOrderRepository : IOrderRepository
{// 只需要实现 IOrderRepository 的方法,干净利落public Task AddOrderAsync(Order order) { /* ... */ }public Task<Order> GetOrderByIdAsync(int id) { /* ... */ }// ... Update, Delete
}// 如果一个类确实需要实现多个功能
public class FullSqlRepository : IOrderRepository, IProductRepository
{// 实现 IOrderRepository 的方法...// 实现 IProductRepository 的方法...
}
好处:
①解耦:客户端和接口之间的依赖关系更清晰、更精确。
②可维护性:修改IProductRepository不会影响到任何只依赖IOrderRepository的代码(如OrderService)。
③避免污染:实现类不会再被强迫实现不需要的方法。
6.D - 依赖倒置原则
核心思想:
①高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
②抽象不应该依赖于细节,细节应该依赖于抽象。
这是 ASP.NET Core 架构的核心,其内置的依赖注入容器就是实现DIP的主要工具。
6.1反面案例
高层模块直接创建和依赖低层模块的具体实现。请看如下示例:
// 低层模块(细节)
public class SqlDatabaseService
{public void SaveData(string data){Console.WriteLine($"Saving {data} to SQL Database...");}
}// 高层模块
public class BusinessService // 高层模块
{// 直接依赖于低层模块的具体实现!private readonly SqlDatabaseService _databaseService = new SqlDatabaseService();public void PerformBusinessOperation(string data){// ... 一些业务逻辑_databaseService.SaveData(data); // tightly coupled 紧耦合}
}
问题:
①紧耦合:BusinessService与SqlDatabaseService紧密地耦合在一起。
②难以测试:无法在测试BusinessService时用一个Mock的数据库服务来替换真实的SqlDatabaseService。
③难以扩展:如果想换成NoSqlDatabaseService,必须修改BusinessService的代码。
6.2正面案例
依赖于抽象(接口),并通过构造函数注入依赖。这正是ASP.NET Core的标准做法。请看如下示例:
// 1. 定义抽象(接口)
public interface IRepository
{void SaveData(string data);
}// 2. 实现细节(低层模块依赖于抽象)
public class SqlDatabaseService : IRepository
{public void SaveData(string data){Console.WriteLine($"Saving {data} to SQL Database...");}
}public class NoSqlDatabaseService : IRepository
{public void SaveData(string data){Console.WriteLine($"Saving {data} to NoSQL Database...");}
}// 3. 高层模块依赖于抽象
public class BusinessServiceDIP
{private readonly IRepository _repository;// 依赖通过接口注入,而不是具体类public BusinessServiceDIP(IRepository repository){_repository = repository; // 这就是“依赖注入”}public void PerformBusinessOperation(string data){// ... 业务逻辑_repository.SaveData(data); // 它只知道接口,不知道具体实现}
}
好处:
①解耦:BusinessServiceDIP完全不关心数据是如何存储的,它只关心IRepository合约。
②可测试性:在单元测试中,你可以轻松地Mock一个IRepository并注入到BusinessServiceDIP中。
③可扩展性:更换数据库类型、添加缓存等,都只需注册不同的IRepository实现即可,无需修改任何业务逻辑代码。
7.总结
在ASP.NET Core项目中贯彻SOLID原则,尤其是结合其强大的依赖注入系统,可以带来巨大的好处:
原则 | 带来的主要好处 | ASP.NET Core 中的实践 |
SRP | 代码更清晰、更易维护 | 瘦控制器、专用服务(Service)、仓储(Repository) |
OCP | 系统易于扩展,稳定 | 策略模式、中间件模式、插件架构 |
LSP | 继承体系安全可靠 | 合理使用抽象类和接口,避免子类破坏契约 |
ISP | 接口设计精准,避免冗余 | 定义小而专一的接口(如 IOrderRepository,IEmailSender) |
DIP | 模块间解耦,易于测试 | 依赖注入是整个框架的核心,构造函数注入是标准方式 |
通过遵循这些原则,你的ASP.NET Core应用程序将变得更加灵活、健壮,并且能够从容应对不断变化的需求。
参考文献:
暂无