SOLID 设计原则
大约 11 分钟约 3365 字
SOLID 设计原则
简介
SOLID 是面向对象设计的五大基本原则的首字母缩写,由 Robert C. Martin(Uncle Bob)提出。这五个原则分别是单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。掌握 SOLID 原则是编写高内聚、低耦合、易于维护和扩展的 C# 代码的基础,对于企业级应用开发尤为重要。
特点
单一职责原则(SRP)
单一职责原则指出,一个类应该只有一个引起它变化的原因。换句话说,每个类只负责一项职责。
违反 SRP 的示例
// 违反 SRP:一个类同时处理用户数据和邮件发送
public class UserService
{
public void CreateUser(string name, string email)
{
// 数据库操作
using var connection = new SqlConnection("...");
connection.Execute("INSERT INTO Users ...", new { Name = name, Email = email });
// 发送邮件
var smtpClient = new SmtpClient("smtp.example.com");
smtpClient.SendMailAsync(
new MailMessage("admin@example.com", email, "欢迎", $"你好 {name}"));
}
}符合 SRP 的重构
// 数据访问职责
public class UserRepository
{
private readonly IDbConnection _connection;
public UserRepository(IDbConnection connection)
{
_connection = connection;
}
public void Add(string name, string email)
{
_connection.Execute(
"INSERT INTO Users (Name, Email) VALUES (@Name, @Email)",
new { Name = name, Email = email });
}
}
// 邮件发送职责
public class EmailService
{
private readonly SmtpClient _smtpClient;
public EmailService(SmtpClient smtpClient)
{
_smtpClient = smtpClient;
}
public Task SendWelcomeAsync(string email, string name)
{
return _smtpClient.SendMailAsync(
new MailMessage("admin@example.com", email, "欢迎注册", $"你好 {name}"));
}
}
// 协调服务
public class UserRegistrationService
{
private readonly UserRepository _userRepository;
private readonly EmailService _emailService;
public UserRegistrationService(UserRepository userRepository, EmailService emailService)
{
_userRepository = userRepository;
_emailService = emailService;
}
public async Task RegisterAsync(string name, string email)
{
_userRepository.Add(name, email);
await _emailService.SendWelcomeAsync(email, name);
}
}开闭原则(OCP)
开闭原则指出,软件实体(类、模块、函数)应该对扩展开放,对修改关闭。通过抽象和多态实现这一目标。
// 违反 OCP:每增加一种折扣都要修改原有代码
public class DiscountCalculator
{
public decimal Calculate(string type, decimal price)
{
if (type == "VIP") return price * 0.8m;
if (type == "SVIP") return price * 0.7m;
if (type == "NewUser") return price * 0.9m;
return price;
}
}
// 符合 OCP:通过抽象和策略模式扩展
public interface IDiscountStrategy
{
decimal ApplyDiscount(decimal price);
}
public class VipDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price) => price * 0.8m;
}
public class SvipDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price) => price * 0.7m;
}
public class NewUserDiscount : IDiscountStrategy
{
public decimal ApplyDiscount(decimal price) => price * 0.9m;
}
public class DiscountCalculator
{
private readonly Dictionary<string, IDiscountStrategy> _strategies;
public DiscountCalculator(IEnumerable<IDiscountStrategy> strategies)
{
_strategies = strategies.ToDictionary(
s => s.GetType().Name.Replace("Discount", "").ToLower());
}
public decimal Calculate(string type, decimal price)
{
return _strategies.TryGetValue(type.ToLower(), out var strategy)
? strategy.ApplyDiscount(price)
: price;
}
}里氏替换原则(LSP)
里氏替换原则指出,子类型必须能够替换其基类型而不影响程序的正确性。
// 违反 LSP:正方形继承矩形,但修改宽高行为不一致
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
get => base.Width;
set { base.Width = value; base.Height = value; }
}
public override int Height
{
get => base.Height;
set { base.Width = value; base.Height = value; }
}
}
// 测试方法 - 对 Rectangle 成立但对 Square 不成立
public void TestRectangle(Rectangle rect)
{
rect.Width = 5;
rect.Height = 4;
Debug.Assert(rect.Area == 20); // Square 会失败!
}
// 正确做法:使用接口抽象
public interface IShape
{
int Area { get; }
}
public class RectangleShape : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area => Width * Height;
}
public class SquareShape : IShape
{
public int Side { get; set; }
public int Area => Side * Side;
}接口隔离原则(ISP)
接口隔离原则指出,客户端不应该被迫依赖它不使用的方法。应该将臃肿的接口拆分为更小、更具体的接口。
// 违反 ISP:一个庞大的接口
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
// 机器人不需要 Eat 和 Sleep
public class Robot : IWorker
{
public void Work() => Console.WriteLine("工作中...");
public void Eat() => throw new NotSupportedException(); // 不需要
public void Sleep() => throw new NotSupportedException(); // 不需要
}
// 符合 ISP:拆分接口
public interface IWorkable
{
void Work();
}
public interface IFeedable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
public class HumanWorker : IWorkable, IFeedable, ISleepable
{
public void Work() => Console.WriteLine("人类工作中...");
public void Eat() => Console.WriteLine("吃饭中...");
public void Sleep() => Console.WriteLine("睡觉中...");
}
public class RobotWorker : IWorkable
{
public void Work() => Console.WriteLine("机器人工作中...");
}依赖倒置原则(DIP)
依赖倒置原则指出,高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
// 违反 DIP:高层直接依赖低层具体实现
public class NotificationService
{
private readonly EmailSender _emailSender = new();
public void Notify(string message)
{
_emailSender.Send(message);
}
}
// 符合 DIP:依赖抽象
public interface IMessageSender
{
Task SendAsync(string message);
}
public class EmailSender : IMessageSender
{
public Task SendAsync(string message)
{
Console.WriteLine($"通过邮件发送: {message}");
return Task.CompletedTask;
}
}
public class SmsSender : IMessageSender
{
public Task SendAsync(string message)
{
Console.WriteLine($"通过短信发送: {message}");
return Task.CompletedTask;
}
}
public class WeChatSender : IMessageSender
{
public Task SendAsync(string message)
{
Console.WriteLine($"通过微信发送: {message}");
return Task.CompletedTask;
}
}
public class NotificationService
{
private readonly IEnumerable<IMessageSender> _senders;
public NotificationService(IEnumerable<IMessageSender> senders)
{
_senders = senders;
}
public async Task NotifyAllAsync(string message)
{
foreach (var sender in _senders)
{
await sender.SendAsync(message);
}
}
}
// 在 DI 容器中注册
// builder.Services.AddTransient<IMessageSender, EmailSender>();
// builder.Services.AddTransient<IMessageSender, SmsSender>();
// builder.Services.AddTransient<IMessageSender, WeChatSender>();DIP 在 .NET 中的实际应用
// .NET Core 依赖注入天然遵循 DIP
var builder = WebApplication.CreateBuilder(args);
// 依赖抽象而非具体实现
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentService, StripePaymentService>();
builder.Services.AddScoped<IInventoryService, WarehouseService>();
// 控制器通过构造函数注入获取依赖
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderRepository _orderRepo;
private readonly IPaymentService _payment;
private readonly ILogger<OrdersController> _logger;
public OrdersController(
IOrderRepository orderRepo,
IPaymentService payment,
ILogger<OrdersController> logger)
{
_orderRepo = orderRepo;
_payment = payment;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> Create(CreateOrderRequest request)
{
var order = new Order(request.ProductId, request.Quantity);
await _payment.ChargeAsync(order.Total);
await _orderRepo.SaveAsync(order);
_logger.LogInformation("订单 {Id} 创建成功", order.Id);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
}SOLID 原则间的关联
五大原则的协作关系
// SOLID 不是孤立的原则,它们相互配合
// SRP → 职责单一,为其他原则奠定基础
// OCP → 通过抽象扩展,依赖 SRP 划分好的职责
// LSP → 保证继承体系的正确替换
// ISP → 接口粒度控制,为 DIP 提供合适的抽象
// DIP → 最终实现解耦,依赖 ISP 定义的精简接口
// 示例:一个完整的 SOLID 实践
// 1. SRP — 分离订单处理、支付和通知
// 2. OCP — 新增支付方式不需要修改现有代码
// 3. LSP — 所有支付方式可以互换使用
// 4. ISP — 支付接口和退款接口分离
// 5. DIP — 高层订单服务依赖支付抽象
// 支付接口(ISP — 按功能拆分接口)
public interface IPaymentProcessor
{
Task<PaymentResult> ChargeAsync(decimal amount, string currency);
}
public interface IRefundProcessor
{
Task<RefundResult> RefundAsync(string transactionId, decimal amount);
}
// 支付方式实现(OCP + LSP)
public class StripePayment : IPaymentProcessor, IRefundProcessor
{
public Task<PaymentResult> ChargeAsync(decimal amount, string currency)
{
// Stripe 扣款逻辑
return Task.FromResult(new PaymentResult(true, "stripe_tx_001"));
}
public Task<RefundResult> RefundAsync(string transactionId, decimal amount)
{
// Stripe 退款逻辑
return Task.FromResult(new RefundResult(true));
}
}
public class AliPayPayment : IPaymentProcessor
{
public Task<PaymentResult> ChargeAsync(decimal amount, string currency)
{
// 支付宝扣款逻辑
return Task.FromResult(new PaymentResult(true, "alipay_tx_001"));
}
}
// 订单服务(DIP — 依赖抽象而非具体实现)
public class OrderService
{
private readonly IPaymentProcessor _payment;
private readonly INotificationService _notification;
private readonly IOrderRepository _repository;
private readonly ILogger<OrderService> _logger;
public OrderService(
IPaymentProcessor payment,
INotificationService notification,
IOrderRepository repository,
ILogger<OrderService> logger)
{
_payment = payment;
_notification = notification;
_repository = repository;
_logger = logger;
}
public async Task<OrderResult> PlaceOrderAsync(OrderRequest request)
{
// 1. 创建订单
var order = Order.Create(request.Items, request.UserId);
_logger.LogInformation("订单创建:{OrderId}", order.Id);
// 2. 扣款
var paymentResult = await _payment.ChargeAsync(order.Total, "CNY");
if (!paymentResult.Success)
{
_logger.LogWarning("支付失败:{OrderId}", order.Id);
return OrderResult.Failed("支付失败");
}
// 3. 保存
order.MarkAsPaid(paymentResult.TransactionId);
await _repository.SaveAsync(order);
// 4. 通知
await _notification.NotifyAsync(order.UserId, "订单已确认");
return OrderResult.Success(order.Id);
}
}常见违反 SOLID 的代码味道
识别与重构
// 代码味道 1:方法过长(违反 SRP)
// 一个方法超过 50 行,通常意味着职责过多
// 重构:提取方法,每个方法只做一件事
// 代码味道 2:过多的 switch/if-else(违反 OCP)
// 每次新增类型都要修改已有代码
// 重构:使用策略模式或工厂模式
// 代码味道 3:子类抛出 NotSupportedException(违反 LSP)
// 子类不能履行基类的契约
// 重构:重新设计继承关系,使用接口替代
// 代码味道 4:胖接口(违反 ISP)
// 接口方法超过 5 个,或有客户端不需要的方法
// 重构:拆分为多个小接口
// 代码味道 5:new 关键字直接创建依赖(违反 DIP)
// public class Service {
// private readonly Repository _repo = new Repository(); // 违反 DIP
// }
// 重构:通过构造函数注入依赖
// 代码味道 6:God Class(上帝类)
// 一个类承担太多职责,引用太多依赖
// 重构:识别变化轴线,拆分为多个专注的类
// 重构安全检查清单:
// 1. 重构前是否有足够的单元测试?
// 2. 重构后所有测试是否通过?
// 3. 是否引入了不必要的抽象层?
// 4. 重构是否真的改善了代码的清晰度?SOLID 在 .NET 框架中的体现
框架级的设计实践
// ASP.NET Core 本身大量遵循 SOLID 原则
// 1. SRP — 中间件管道,每个中间件只做一件事
app.UseAuthentication(); // 只负责认证
app.UseAuthorization(); // 只负责授权
app.UseCors(); // 只负责跨域
// 2. OCP — 通过扩展方法扩展功能,不修改框架源码
builder.Services.AddControllers()
.AddJsonOptions(options => { /* 自定义 JSON */ })
.AddXmlSerializerFormatters();
// 3. LSP — ILogger 的所有实现都可以替换使用
builder.Services.AddSingleton<ILogger>(sp =>
new ConsoleLogger("MyApp")); // 可以替换为任何 ILogger 实现
// 4. ISP — IHostBuilder 和 IHost 分离关注点
// IHostBuilder 负责构建
// IHost 负责运行
// 两者接口方法互不干扰
// 5. DIP — 整个依赖注入系统就是 DIP 的实现
// 高层模块(Controller)依赖抽象(IRepository)
// 低层模块(SqlRepository)实现抽象
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
// .NET 6+ 的 Minimal API 也体现了 SOLID
// 短小精悍的终结点处理器符合 SRP
app.MapGet("/api/products", async (IProductRepository repo) =>
await repo.GetAllAsync());优点
缺点
总结
SOLID 原则是面向对象设计的基石,在 C# 和 .NET 开发中有着广泛的应用。单一职责让类更加聚焦,开闭原则让系统易于扩展,里氏替换保证继承体系的正确性,接口隔离让接口更加精简,依赖倒置让高层模块与低层模块解耦。在实际开发中,应当根据项目规模和团队能力灵活运用这些原则,避免过度设计,同时通过 Code Review 和重构逐步改善代码质量。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来“高级”而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《SOLID 设计原则》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕“为了模式而模式”,尤其是在简单业务里。
复盘问题
- 如果把《SOLID 设计原则》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《SOLID 设计原则》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《SOLID 设计原则》最大的收益和代价分别是什么?
