SOLID 在 C# 中 第1部分:单一职责

发布: (2026年2月22日 GMT+8 07:10)
9 分钟阅读
原文: Dev.to

Source: Dev.to

这是我关于 C# 中 SOLID 原则系列的第 1 部分。每篇文章都会通过真实代码逐步讲解一个原则,这类代码正是你在生产代码库中会看到的。

什么是单一职责原则?

简易版: 一个类应该只有一个修改的理由。

不是一个方法。不是一行代码。一个 修改的理由 —— 这意味着一个职责,一个责任范围。

当团队成员说“我需要更新邮件发送方式”时,这个更改应该只影响一个类,而不是一个同时处理订单计算和数据库查询的类。

问题:一个做所有事情的类

我在几乎所有参与过的代码库中都见过这种模式。某个时刻,有人创建了一个 OrderService,然后不断往里添加功能:

public class OrderService {
    private readonly string connectionString;

    public OrderService(string connectionString) {
        this.connectionString = connectionString;
    }

    public void PlaceOrder(string customerEmail, string product, int quantity, decimal price) {
        // 1. Calculate the total
        decimal total = quantity * price;
        if (quantity > 10)
            total *= 0.9m; // 10% bulk discount

        // 2. Save to database
        using var connection = new SqlConnection(connectionString);
        connection.Open();
        using var command = new SqlCommand(
            "INSERT INTO Orders (Email, Product, Qty, Total) VALUES (@e, @p, @q, @t)",
            connection);
        command.Parameters.Add("@e", SqlDbType.NVarChar).Value = customerEmail;
        command.Parameters.Add("@p", SqlDbType.NVarChar).Value = product;
        command.Parameters.Add("@q", SqlDbType.Int).Value = quantity;
        command.Parameters.Add("@t", SqlDbType.Decimal).Value = total;
        command.ExecuteNonQuery();

        // 3. Send confirmation email
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Orders", "orders@company.com"));
        message.To.Add(new MailboxAddress("", customerEmail));
        message.Subject = "Order Confirmed";
        message.Body = new TextPart("plain") {
            Text = $"Thanks for ordering {quantity}x {product}. Total: ${total}"
        };
        using var smtp = new SmtpClient();
        smtp.Connect("smtp.company.com", 587);
        smtp.Send(message);
        smtp.Disconnect(true);

        // 4. Log it
        File.AppendAllText("orders.log",
            $"{DateTime.Now}: Order placed - {product} x{quantity} = ${total}\n");
    }
}

为什么这是个问题

这个类有 四个修改动因

动因需要修改的内容
折扣规则修改计算逻辑
数据库结构修改 SQL 语句
邮件格式修改消息构造方式
日志方式修改文件写入代码

团队中可能有四个人需要同时修改这个文件,而几乎不可能在隔离的情况下对其中任意一种行为进行测试。这通常被称为 “上帝类” —— 它了解太多、做的事太多。

Source:

修复方案:每个类只承担一个职责

让我们把它拆分开来。每个类只负责一个单一的原因,并且我们将面向接口编程,以便这些组件可以被单独测试。

合约(接口)

// Each interface represents one responsibility
public interface IOrderCalculator {
    decimal CalculateTotal(int quantity, decimal unitPrice);
}

public interface IOrderRepository {
    void Save(Order order);
}

public interface IOrderNotifier {
    void SendConfirmation(string customerEmail, string product, int quantity, decimal total);
}

public interface IOrderLogger {
    void Log(string product, int quantity, decimal total);
}

实现

订单计算器 – 仅负责定价规则

public class OrderCalculator : IOrderCalculator {
    public decimal CalculateTotal(int quantity, decimal unitPrice) {
        decimal total = quantity * unitPrice;

        if (quantity > 10)
            total *= 0.9m; // bulk discount

        return total;
    }
}

订单仓储 – 仅负责持久化

public class OrderRepository : IOrderRepository {
    private readonly string connectionString;

    public OrderRepository(string connectionString) {
        this.connectionString = connectionString;
    }

    public void Save(Order order) {
        using var connection = new SqlConnection(connectionString);
        connection.Open();
        using var command = new SqlCommand(
            "INSERT INTO Orders (Email, Product, Qty, Total) VALUES (@e, @p, @q, @t)",
            connection);
        command.Parameters.Add("@e", SqlDbType.NVarChar).Value = order.CustomerEmail;
        command.Parameters.Add("@p", SqlDbType.NVarChar).Value = order.Product;
        command.Parameters.Add("@q", SqlDbType.Int).Value = order.Quantity;
        command.Parameters.Add("@t", SqlDbType.Decimal).Value = order.Total;
        command.ExecuteNonQuery();
    }
}

邮件订单通知器 – 仅负责通知

public class EmailOrderNotifier : IOrderNotifier {
    public void SendConfirmation(string customerEmail, string product, int quantity, decimal total) {
        // Using MailKit (the modern replacement for System.Net.Mail.SmtpClient,
        // which has been deprecated since .NET 6)
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Orders", "orders@company.com"));
        message.To.Add(new MailboxAddress("", customerEmail));
        message.Subject = "Order Confirmed";
        message.Body = new TextPart("plain") {
            Text = $"Thanks for ordering {quantity}x {product}. Total: ${total}"
        };
        using var smtp = new SmtpClient();
        smtp.Connect("smtp.company.com", 587);
        smtp.Send(message);
        smtp.Disconnect(true);
    }
}

订单日志记录器 – 仅负责日志

public class FileOrderLogger : IOrderLogger {
    public void Log(string product, int quantity, decimal total) {
        File.AppendAllText("orders.log",
            $"{DateTime.Now}: Order placed - {product} x{quantity} = ${total}\n");
    }
}

编排流程

public class OrderProcessor {
    private readonly IOrderCalculator calculator;
    private readonly IOrderRepository repository;
    private readonly IOrderNotifier notifier;
    private readonly IOrderLogger logger;

    public OrderProcessor(IOrderCalculator calculator,
                          IOrderRepository repository,
                          IOrderNotifier notifier,
                          IOrderLogger logger) {
        this.calculator = calculator;
        this.repository = repository;
        this.notifier = notifier;
        this.logger = logger;
    }

    public void PlaceOrder(string customerEmail, string product, int quantity, decimal unitPrice) {
        var total = calculator.CalculateTotal(quantity, unitPrice);
        var order = new Order {
            CustomerEmail = customerEmail,
            Product = product,
            Quantity = quantity,
      Total = total
        };

        repository.Save(order);
        notifier.SendConfirmation(customerEmail, product, quantity, total);
        logger.Log(product, quantity, total);
    }
}

现在每个类只有 一个变化的理由,这使得代码库更易于理解、测试和维护。

我们到底获得了什么?

你可以对每个组件单独进行测试。 想验证批量折扣吗?直接测试 OrderCalculator。无需数据库、邮件服务器或文件系统:

[Test]
public void CalculateTotal_BulkOrder_AppliesTenPercentDiscount() {
    var calculator = new OrderCalculator();

    var total = calculator.CalculateTotal(quantity: 15, unitPrice: 10m);

    Assert.That(total, Is.EqualTo(135m)); // 150 * 0.9
}

尝试在原来的上帝类上编写同样的测试。仅仅为了测试算术,你就需要数据库连接、SMTP 服务器以及可写的文件系统。

更改是有范围的。 当你的老板说“我们要从 SMTP 换到 SendGrid”时,你只需编写一个新的 IOrderNotifier 实现。计算器、仓库、日志记录器:它们都不受影响,也不会意外被破坏。

新开发者能更快找到东西。 “定价逻辑在哪里?” → OrderCalculator.cs。不再埋在 200 行方法的第 47 行里。

如何发现 SRP 违规

在代码审查时我会留意的红旗:

  • 类名中包含 “And”(或应该包含):OrderAndEmailServiceUserValidationAndStorage
  • 描述时使用 “also” 这个词: “它计算总额 also 发送邮件 also 保存到数据库。”
  • 依赖来自完全不同的领域:一个类同时使用 SmtpClientSqlConnection 表明它承担了太多职责。
  • 方法长度超过约 30 行:这不是硬性规定,但长方法往往是多个职责被塞在一起的表现。
  • 团队中多个人因无关原因频繁编辑同一个文件

最常见的反对意见

“但是现在我有太多文件了!”

是的。五个小而专注的文件比一个庞大的文件更容易理解、测试和修改。你的 IDE 有 Ctrl+T。好好利用它。

增加文件的成本几乎为零。神类(God Class)的成本是没人愿意触碰的代码以及跨不相关功能的连锁 bug。

快速规则

  • 问:“我能用一句话描述这个类的职责吗,且不使用 and 吗?”
  • 如果一个类有超过 3‑4 个依赖,它可能做得太多了。
  • 协调者是可以的。 OrderService 上面协调工作,但它本身并不 执行 工作。
  • 不要走极端。你不需要为每个方法都创建一个类。将因同一原因而改变的内容归为一组。

接下来: 第 2 部分 – 开闭原则,“扩展,而非修改”。 我们将看看如何在不触碰已有类的情况下添加新行为。

这是一系列文章的一部分,内容涵盖 SOLID → 单元测试 → 清洁架构 → 集成测试。 关注我 以获取下一篇。

0 浏览
Back to Blog

相关文章

阅读更多 »