(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的案例是"肥胖控制器"或"肥胖服务"。这个类承担了过多的职责。请看如下示例:

cs 复制代码
// 违反 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请求和响应。请看如下示例:

cs 复制代码
// 负责业务逻辑的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;

    // 依赖通过接口注入,符合 DIP
    public 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反面案例

当需要添加新功能时,通过修改已有的、已经测试好的类来实现。请看如下示例:

cs 复制代码
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正面案例

使用抽象(接口或抽象类)来实现开闭原则。通过添加新的实现来扩展功能,而不是修改旧代码。请看如下示例:

cs 复制代码
// 抽象策略
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中注册:

cs 复制代码
services.AddScoped<IPaymentStrategy, CreditCardPaymentStrategy>();
services.AddScoped<IPaymentStrategy, PayPalPaymentStrategy>();
services.AddScoped<IPaymentStrategy, AlipayPaymentStrategy>(); // 新增策略只需注册
services.AddScoped<PaymentProcessorOCP>();

好处:

①PaymentProcessorOCP的核心逻辑ProcessPaymentAsync方法不再需要修改。

②要支持新的支付方式,只需创建一个新的IPaymentStrategy实现并在DI容器中注册即可。系统功能得到了扩展,但核心模块保持关闭。

③这本质上是策略模式的应用。

4.L - 里氏替换原则

核心思想:子类必须能够替换掉它们的父类,而不破坏程序。即所有引用基类的地方必须能透明地使用其子类的对象。

4.1反面案例

子类修改了父类的预期行为,导致替换后程序出错。请看如下示例:

cs 复制代码
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正面案例

重新设计继承体系,确保行为的一致性。通常意味着需要更精确的抽象。请看如下示例:

cs 复制代码
// 更精确的抽象
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反面案例

创建一个"胖接口",包含很多方法,导致实现类必须实现一些它根本不需要的方法。请看如下示例:

cs 复制代码
// 一个什么都做的"上帝接口"
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正面案例

将大接口拆分成多个高度内聚的小接口。请看如下示例:

cs 复制代码
// 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反面案例

高层模块直接创建和依赖低层模块的具体实现。请看如下示例:

cs 复制代码
// 低层模块(细节)
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的标准做法。请看如下示例:

cs 复制代码
// 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应用程序将变得更加灵活、健壮,并且能够从容应对不断变化的需求。

参考文献:

暂无

相关推荐
拾忆,想起3 小时前
AMQP协议深度解析:消息队列背后的通信魔法
java·开发语言·spring boot·后端·spring cloud
PH = 73 小时前
Spring Ai Alibaba开发指南
java·后端·spring
不会吃萝卜的兔子4 小时前
springboot websocket 原理
spring boot·后端·websocket
Fency咖啡4 小时前
Spring Boot 内置日志框架 Logback - 以及 lombok 介绍
spring boot·后端·logback
karry_k6 小时前
什么是Fork/Join?
java·后端
karry_k6 小时前
四大函数式接口与Stream流式计算
后端
王家视频教程图书馆7 小时前
C# asp.net模板代码简单API请求
开发语言·c#·asp.net
Cosolar7 小时前
什么是 ONNX Runtime?
后端·架构
Cosolar7 小时前
榨干每一滴算力:ONNX Runtime 多维优化实战指南
后端·架构