OOP 七大原则

七大原则分别为 SOLID 原则、组合/聚合复用原则、迪米特原则,其中 SOLID 原则为五个原则的缩写,分别是:

  • 单一职责原则(Single Responsibility Principle, SRP)
    • 一个类应该只有一个引起它变化的原因。也就是说,一个类只负责一项职责。
  • 开闭原则(Open/Closed Principle, OCP)
    • 对扩展开放,对修改关闭。这意味着软件实体(类、模块、函数等) 应该可以通过扩展来实现新功能,而无需修改现有的代码。
  • 里氏替换原则(Liskov Substitution Principle, LSP)
    • 子类必须能够替换其基类,并且在替换后,程序的行为仍然正确。
  • 接口隔离原则(Interface Segregation Principle, ISP)
    • 指设计时不同的功能应定义在不同的接口上,避免实现类依赖不需要的接口,换个角度理解就是一个类应该依赖尽量少的接口或实现类,减少程序的冗余性和复杂性。
  • 依赖倒置原则(Dependency Inversion Principle, DIP)
    • 高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
  • 组合/聚合复用原则 (Composition/Aggregation Reuse Principle, CARP)
    • 一个类应通过组合(Containment)或聚合(Aggregation)来复用已有功能,而不是通过继承。也就是说,将需要的行为封装在独立的组件中,然后在使用方类中通过持有这些组件的实例来获得功能。这样可以降低类之间的耦合度,增强灵活性和可维护性。
  • 迪米特法则 (Law of Demeter, LOD)
    • Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

实际软件开发中也不要太过执着于这些开发规范,有时候你自己的个人小项目这抽离一下那解耦一下,最后发现抽象代码比业务逻辑代码还要多,得不偿失,根据项目体量而定,所以在开发前期,开发人员需要对整个项目进行预期的设计与评估,避免太过纠结规范从而被规范所限。

本文的示例代码均以C#例

单一职责原则

单一职责原则(Single Responsibility Principle, SRP)要求一个类(或模块)应该仅有一个引起其变化的原因。换句话说,每个类都只负责一项职责,职责变化时,只有这个类需要修改。这样做能提高代码可维护性、可测试性和可复用性。

单一职责原则概念很好理解,通俗来说就是:一辆汽车,车轮只负责滚动的功能,大灯只负责照明的功能,如果你想在车轮上装上发动机让它可以自驱动,那不是纯SB吗。

开发场景

假设正在开发一个电商系统,其中有一个 OrderService 类,负责:

  1. 订单创建、取消、修改等业务逻辑;
  2. 发送下单成功的邮件通知;
  3. 将订单信息写入日志;
  4. 生成发票 PDF 并保存。

如果把所有功能都写在同一个类里,一旦邮件模板变更、日志格式调整或发票生成方式更新,都要改动同一个文件,极易引入回归 bug。

错误示例

  • 该类同时承担 订单管理邮件发送日志记录发票生成 四大职责。
  • 任何一项功能改动都需要修改 OrderService,极易产生不必要的耦合。
public class OrderService
{
    public void CreateOrder(Order order)
    {
        // 1. 订单业务逻辑
        Validate(order);
        SaveToDatabase(order);

        // 2. 发送邮件通知
        var smtpClient = new SmtpClient("smtp.example.com");
        var mail = new MailMessage("no-reply@example.com", order.CustomerEmail) {
            Subject = "下单成功",
            Body = "您的订单已成功创建,订单号:" + order.Id
        };
        smtpClient.Send(mail);

        // 3. 写日志
        File.AppendAllText("orders.log", $"Order {order.Id} created at {DateTime.Now}\n");

        // 4. 生成发票
        var invoice = InvoiceGenerator.Generate(order);
        File.WriteAllBytes($"invoices/{order.Id}.pdf", invoice);
    }

    // 省略 Validate、SaveToDatabase 等方法…
}

正确示例

  • OrderService.cs
    订单管理邮件发送日志记录发票生成 通过构造函数注入
public
{
    private readonly IOrderRepository _repository;
    private readonly IEnumerable<INotifier> _notifiers;
    private readonly ILogger _logger;
    private readonly IInvoiceGenerator _invoiceGenerator;

    public OrderService(
        IOrderRepository repository,
        IEnumerable<INotifier> notifiers, 
        ILogger logger,
        IInvoiceGenerator invoiceGenerator)
    {
        _repository = repository;
        _notifiers = notifiers;
        _logger = logger;
        _invoiceGenerator = invoiceGenerator;
    }

    public void CreateOrder(Order order)
    {
        Validate(order);
        _repository.Save(order);
        foreach (var notifier in _notifiers)
        {
            notifier.Notify(order);
        }

        _logger.LogInfo($"订单 {order.Id} 已创建");
        _invoiceGenerator.GenerateAndSave(order);
    }
}
  • 其余四大职责实现
// 订单持久化接口与实现
public interface IOrderRepository
{
    void Save(Order order);
}

public class SqlOrderRepository : IOrderRepository
{
    public void Save(Order order)
    {
        // 执行数据库保存逻辑…
    }
}

// 通知接口与实现
public interface INotifier
{
    void Notify(Order order);
}

// 短信通知实现
public class SmsNotifier : INotifier
{
    public void Notify(Order order)
    {
        // 发送短信通知逻辑
    }
}

// 邮件通知实现
public class SmtpEmailNotifier : INotifier
{
    public void Notify(Order order)
    {
        var smtpClient = new SmtpClient("smtp.example.com");
        var mail = new MailMessage("no-reply@example.com", order.CustomerEmail)
        {
            Subject = "下单成功",
            Body = $"您的订单已成功创建,订单号:{order.Id}"
        };
        smtpClient.Send(mail);
    }
}

// 日志记录接口与实现
public interface ILogger
{
    void LogInfo(string message);
}

public class FileLogger : ILogger
{
    public void LogInfo(string message)
    {
        File.AppendAllText("orders.log", message + Environment.NewLine);
    }
}

// 发票生成接口与实现
public interface IInvoiceGenerator
{
    void GenerateAndSave(Order order);
}

public class PdfInvoiceGenerator : IInvoiceGenerator
{
    public void GenerateAndSave(Order order)
    {
        var pdf = InvoiceGenerator.Generate(order);
        File.WriteAllBytes($"invoices/{order.Id}.pdf", pdf);
    }
}

通过重新划分职责后通过构造函数进行依赖注入即可完全符合 单一职责原则,每一个职责都很清晰,会带来以下好处:

  • 高聚合、低耦合: 各模块职责单一,改动只影响对应实现类。
  • 单元测试性增强: 单元测试时可轻松 Mock 通知、日志、发票等模块。
  • 通过注入 IEnumerable<INotifier>,新增短信、微信、App 推送等只需新增实现并注册,不需要修改 OrderService

开闭原则

软件实体(类、模块、函数等)应对扩展开放,对修改关闭。也就是说,当需求变化时,应通过添加新代码来扩展功能,而不是修改已有的、已经经过测试的代码。

开闭原则就更好理解了,通俗来说就像电脑的USB接口:你不能改动接口本身,但可以插上U盘、键盘、摄像头等任何新设备来扩展功能,总不能你要插个U盘还需要把电脑拆了重新在主板上再设计一个U盘接口出来吧?那不纯NT吗。

开发场景

假设在电商系统中,需要为不同支付方式(如支付宝、微信、银联)提供统一的支付流程:

  1. 用户发起支付请求;
  2. 根据支付方式执行不同的接口调用;
  3. 返回支付结果。

如果每次新增一种支付方式都去改核心 PaymentService,会违反开闭原则。

错误示例

所有需要调用的支付方式通通写死在 PaymentServicePay 方法中,每次甲方要新增一个支付方式我都要再根据支付方式判断然后去调用相应的 SDK,最后变的又臭又长。

public class PaymentService
{
    public void Pay(Order order, string paymentType)
    {
        if (paymentType == "Alipay")
        {
            // 调用支付宝 SDK
            AlipaySdk.Pay(order.Amount);
        }
        else if (paymentType == "WeChat")
        {
            // 调用微信支付 SDK
            WeChatSdk.Pay(order.Amount);
        }
        else if (paymentType == "UnionPay")
        {
            // 调用银联 SDK
            UnionPaySdk.Pay(order.Amount);
        }
        // ……
    }
}

正确示例

  • 首先抽象支付接口
public interface IPaymentProcessor
{
    /// <summary>
    /// 标识当前支付方式(如 "Alipay", "WeChat"…)
    /// </summary>
    string PaymentType { get; }

    /// <summary>
    /// 执行支付
    /// </summary>
    void Pay(decimal amount);
}
  • 实现各个支付接口
public class AlipayProcessor : IPaymentProcessor
{
    public string PaymentType => "Alipay";
    public void Pay(decimal amount)
    {
        AlipaySdk.Pay(amount);
    }
}

public class WeChatProcessor : IPaymentProcessor
{
    public string PaymentType => "WeChat";
    public void Pay(decimal amount)
    {
        WeChatSdk.Pay(amount);
    }
}

public class UnionPayProcessor : IPaymentProcessor
{
    public string PaymentType => "UnionPay";
    public void Pay(decimal amount)
    {
        UnionPaySdk.Pay(amount);
    }
}
  • 修改PaymentService
public class PaymentService
{
    private readonly IEnumerable<IPaymentProcessor> _processors;

    public PaymentService(IEnumerable<IPaymentProcessor> processors)
    {
        _processors = processors;
    }

    public void Pay(Order order, string paymentType)
    {
        var processor = _processors
            .FirstOrDefault(p => p.PaymentType.Equals(paymentType, StringComparison.OrdinalIgnoreCase));
        if (processor == null)
            throw new ArgumentException($"不支持的支付方式:{paymentType}");

        processor.Pay(order.Amount);
    }
}

假设现在需要再新增一个 PayPal 的支付方式,只需要再实现一个 PayPalProcessor,然后注册后依赖注入即可。

public class PayPalProcessor : IPaymentProcessor
{
    public string PaymentType => "PayPal";
    public void Pay(decimal amount)
    {
        PayPalSdk.Pay(amount);
    }
}
// 1. 手动注册支付接口(你也可以通过容器注册)
var processors = new List<IPaymentProcessor>
{
    new AlipayProcessor(),
    new WeChatProcessor(),
    new UnionPayProcessor(),
    new PayPalProcessor()
};

// 2. 传入 PaymentService
var paymentService = new PaymentService(processors);

// 3. 调用支付
var order = new Order { Id = 123, Amount = 99.9m };
paymentService.Pay(order, "PayPal");

现在就完全遵循了开闭原则,实现了 高内聚、低耦合/可测试性强/维护成本低/扩展简单 的要求。

里氏替换原则

子类对象必须能够替换父类对象,并且程序行为保持正确。换句话说,父类能工作的地方,换成子类也必须能正常工作,不会抛异常、不破坏逻辑。

假设你有一个遥控器(父类),可以控制电视。现在你换了一个新的智能遥控器(子类),它不仅能控制电视,还能控制空调和电灯。这个新的智能遥控器(子类)完全可以替代旧遥控器(父类)来控制电视,而且不会出问题。这就是符合里氏替换原则的。但如果这个新的智能遥控器(子类)的“音量+”按钮变成了“换台”功能,那它就不能完全替代旧遥控器了,因为它改变了原有的行为,这就违反了里氏替换原则。

一句话总结就是:龙生龙,凤生凤,老鼠的儿子会打洞。你爹会的东西你也没问题。

开发场景

假设正在构建一个文档导出系统,支持导出为 PDF、Excel 和 HTML 等格式。设计了一个基类 Exporter,并为每种格式实现一个子类。

后来甲方要求你加了一个“纯文本导出”类 TextExporter,结果它不支持分页。但父类提供了分页相关方法,于是只好在子类里抛了个 NotSupportedException

这不完蛋了吗,隔壁老王生的,让你养了。

错误示例

public abstract class Exporter
{
    public abstract void Export(string content);

    public virtual void SetPageSize(int width, int height)
    {
        Console.WriteLine($"设置页面大小为 {width}x{height}");
    }
}

public class PdfExporter : Exporter
{
    public override void Export(string content)
    {
        Console.WriteLine("导出为 PDF");
    }
}

public class TextExporter : Exporter
{
    public override void Export(string content)
    {
        Console.WriteLine("导出为纯文本");
    }

    public override void SetPageSize(int width, int height)
    {
        // 文本不支持分页,抛异常
        throw new NotSupportedException("TextExporter 不支持分页设置");
    }
}
void ExportDocument(Exporter exporter)
{
    exporter.SetPageSize(800, 600); 
    exporter.Export("内容内容内容");
}

你在通过 ExportDocument(new TextExporter()); 调用的时候,程序直接抛出 NotSupportedException

正确示例

  • 将分页职责分离出来,用接口表达能力。
public interface IPageableExporter
{
    void SetPageSize(int width, int height);
}

public abstract class Exporter
{
    public abstract void Export(string content);
}

public class PdfExporter : Exporter, IPageableExporter
{
    public override void Export(string content)
    {
        Console.WriteLine("导出为 PDF");
    }

    public void SetPageSize(int width, int height)
    {
        Console.WriteLine($"PDF 分页设置:{width}x{height}");
    }
}

public class TextExporter : Exporter
{
    public override void Export(string content)
    {
        Console.WriteLine("导出为纯文本");
    }
}

调用时区分该类职责:

void ExportDocument(Exporter exporter)
{
    if (exporter is IPageableExporter pageable)
    {
        pageable.SetPageSize(800, 600);
    }

    exporter.Export("内容内容内容");
}

里氏替换原则就没啥好说的了,还是那句话:龙生龙,凤生凤,老鼠的儿子会打洞
只要父类能上的地方,子类都能无缝替换上,且不会出事,这才是健壮的继承。
遵循该原则很简单,当你写一个子类时,仔细想一想 "如果是一个完全不懂这个子类的人调用,会不会踩坑"。
如果“会”,那就要重构。

接口隔离原则

接口的使用者(即依赖该接口的类或模块)不应该被迫实现它们根本用不到的方法。
一个臃肿的接口会让依赖它的类不得不填充大量无用实现,增加维护成本。应该将大接口拆分成多个专用接口,使得每个使用者只关心自己真正需要的方法。

你只想剪下指甲,我应该给你一把指甲刀,而不是硬塞给你一把瑞士军刀。

开发场景

在一个订单处理系统中,存在一个 IOrderService 接口,包含下单、取消订单、打印订单、导出发票、发送通知等方法。不同客户端(如后台管理工具、打印机适配器、移动端推送服务)只需要部分功能,却被迫依赖全部方法。

错误示例

这样写有两个比较严重的问题,一是 PrinterAdapter 被迫实现了大量无用方法,用 throw 或空实现填充,明明它只需要打印功能;二是任何接口方法的变更,都要影响所有实现类,假设我现在修改了接口的 PrintOrder 方法,那 PrinterAdapter 就直接废了。

// 假设是几百行的综合接口
public interface IOrderService
{
    void CreateOrder(Order order);
    void CancelOrder(int orderId);
    void PrintOrder(int orderId);
    void ExportInvoice(int orderId, string path);
    void SendNotification(int orderId, string message);
}

// 打印机类,只关注 PrintOrder
public class PrinterAdapter : IOrderService
{
    public void CreateOrder(Order order)      => throw new NotImplementedException();
    public void CancelOrder(int orderId)      => throw new NotImplementedException();
    public void PrintOrder(int orderId)       => /* 打印逻辑 */ ;
    public void ExportInvoice(int orderId, string path) => throw new NotImplementedException();
    public void SendNotification(int orderId, string message) => throw new NotImplementedException();
}

正确示例

  • 根据职责拆分接口
public interface IOrderCreator
{
    void CreateOrder(Order order);
}

public interface IOrderCanceler
{
    void CancelOrder(int orderId);
}

public interface IOrderPrinter
{
    void PrintOrder(int orderId);
}

public interface IInvoiceExporter
{
    void ExportInvoice(int orderId, string path);
}

public interface IOrderNotifier
{
    void SendNotification(int orderId, string message);
}
  • 各端只依赖需要的接口
// 打印机适配器只实现 IOrderPrinter
public class PrinterAdapter : IOrderPrinter
{
    public void PrintOrder(int orderId)
    {
        // 执行打印逻辑
    }
}

// 邮件通知服务只实现 IOrderNotifier
public class EmailNotifier : IOrderNotifier
{
    public void SendNotification(int orderId, string message)
    {
        // 通知服务逻辑
    }
}

// 订单管理后台实现创建、取消和导出发票
public class AdminOrderService : IOrderCreator, IOrderCanceler, IInvoiceExporter
{
    public void CreateOrder(Order order)     { /* … */ }
    public void CancelOrder(int orderId)     { /* … */ }
    public void ExportInvoice(int orderId, string path) { /* … */ }
}

这个没啥好说的,就是实现 高内聚、低耦合,精简依赖,避免无用实现,可灵活扩展。

依赖倒置原则

高层模块不应该依赖低层模块,二者都应该依赖抽象(接口或抽象类),抽象不应该依赖细节,细节应该依赖抽象。

你(高层模块)别直接找我(底层模块),去找我的经纪人(接口)。

开发场景

假设有一个日志功能,业务层 OrderService 需要记录操作日志。若直接依赖 FileLogger,一旦要改用数据库或远程日志服务,就必须改 OrderService 代码。

错误示例

  • OrderServiceFileLogger 强耦合;
  • 若要切换到 DatabaseLogger 或第三方日志库,必须修改 OrderService
// 低层 —— 具体实现
public class FileLogger
{
    public void Log(string message)
    {
        File.AppendAllText("app.log", message + Environment.NewLine);
    }
}

// 高层 —— 业务逻辑直接依赖具体类
public class OrderService
{
    private readonly FileLogger _logger = new FileLogger();

    public void CreateOrder(Order order)
    {
        // … 业务处理 …
        _logger.Log($"订单 {order.Id} 已创建");
    }
}

正确示例

  • 定义抽象日志接口
public interface ILogger
{
    void Log(string message);
}
  • 低层依赖抽象,实现细节
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        File.AppendAllText("app.log", message + Environment.NewLine);
    }
}

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        // 写入数据库表 Logs
    }
}
  • 高层依赖抽象,不再依赖细节
public class OrderService
{
    private readonly ILogger _logger;

    // 通过构造函数注入 ILogger 抽象
    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void CreateOrder(Order order)
    {
        // 业务处理
        _logger.Log($"订单 {order.Id} 已创建");
    }
}
  • 业务调用
ILogger logger = new FileLogger();
var orderService = new OrderService(logger);
orderService.CreateOrder(new Order { Id = 1 });

若需要修改为其他日志类,比如 DatabaseLogger,仅需在注入的时候注入为 DatabaseLogger 实例对象即可。

ILogger logger = new DatabaseLogger();
var orderService = new OrderService(logger);

组合/聚合复用原则

在设计对象关系时,优先通过“组合”(一个对象包含另一个对象)或“聚合”(一个对象引用另一个对象)来复用已有功能,而不是通过继承。
继承会带来强耦合——子类与父类产生“is-a”关系,一旦父类发生变化,子类也会受到影响。组合/聚合则通过“has-a”关系,将依赖的变化隔离,占据更灵活、更低耦合的设计空间。

想开车,就去买一辆或者租一辆(聚合/组合),而不要让自己去造一辆车(继承)。

开发场景

一个消息发送系统,需要支持发送邮件和短信。若直接让 EmailSenderSmsSender 继承自一个 MessageSender 基类来共享“格式化消息”逻辑,可能会把不相关的行为强行拉进继承层级。更好的做法是将 消息格式 抽象为一个独立组件,通过组合复用给不同的发送器使用。

在消息发送系统中,需要发送两类消息:

  1. 验证码消息:“您的验证码是 123456,请在 5 分钟内使用。”
  2. 天气消息:“今日天气:晴,最高 30℃,最低 22℃。”

错误示例

继承关系把两种格式混在一个基类中,假设现在老板叫你加一个快递提示消息类型的时候需要去更改 MessageSender

// 将格式化逻辑放在基类,继承膨胀
public abstract class MessageSender
{
    public virtual string FormatVerification(string code)
        => $"[验证码] 您的验证码是 {code},请在 5 分钟内使用。";

    public virtual string FormatWeather(string weather)
        => $"[天气] 今日天气:{weather}。";

    public abstract void Send(string content);
}

public class SmsSender : MessageSender
{
    public override void Send(string content)
    {
        // 内容可能是验证码或天气,但 FormatXXX 写死在父类,耦合不清
        Console.WriteLine("短信发送:" + content);
    }
}

public class EmailSender : MessageSender
{
    public override void Send(string content)
        => Console.WriteLine("邮件发送:" + content);
}

正确示例

  • 定义格式化接口与实现
public interface IMessageFormatter
{
    string Format(string payload);
}

public class VerificationCodeFormatter : IMessageFormatter
{
    public string Format(string code)
        => $"[验证码] 您的验证码是 {code},请在 5 分钟内使用。";
}

public class WeatherMessageFormatter : IMessageFormatter
{
    public string Format(string weather)
        => $"[天气] 今日天气:{weather}。";
}

public class DeliveryMessageFormatter : IMessageFormatter
{
    public string Format(string trackingInfo)
        => $"[快递] 您的快递状态:{trackingInfo}。";
}
  • 定义发送接口与实现
public interface IMessageSender
{
    void Send(string formattedContent);
}

public class SmsSender : IMessageSender
{
    public void Send(string formattedContent)
    {
        Console.WriteLine("短信发送:" + formattedContent);
    }
}

public class EmailSender : IMessageSender
{
    public void Send(string formattedContent)
    {
        Console.WriteLine("邮件发送:" + formattedContent);
    }
}
  • 业务调用
// 准备格式化器
var codeFormatter    = new VerificationCodeFormatter();
var weatherFormatter = new WeatherMessageFormatter();
var deliveryFormatter = new DeliveryMessageFormatter();

// 准备发送器
var smsSender   = new SmsSender();
var emailSender = new EmailSender();

// 发送验证码短信
var codeMessage = codeFormatter.Format("123456");
smsSender.Send(codeMessage);

// 发送天气邮件
var weatherMessage = weatherFormatter.Format("晴,最高 30℃,最低 22℃");
emailSender.Send(weatherMessage);

// 发送快递通知短信
var deliveryMessage = deliveryFormatter.Format("您的快递已到达,请注意查收。");
smsSender.Send(deliveryMessage);

通过组合把“会干什么”(格式化)和“怎么干”(发送)拆开,任意组装,避免继承带来的僵化,让各组件只依赖接口,变化隔离更彻底,各司其职,互不干扰。
这样可以使得代码 职责分离、高复用、易扩展、低耦合。

迪米特法则

又称“最少知识原则”,即一个对象应当对其他对象有尽可能少的了解。具体来说,方法内只能调用:

  1. 自身的方法;
  2. 方法参数所引用对象的方法;
  3. 本类内部创建或持有的对象的方法;
  4. 直接组件(成员变量)的方法。
  5. 不应链式调用(避免 a.getB().getC().doSomething()

从而降低了类与类之间、模块间的耦合度,提高系统的可维护性,使系统更容易维护和修改。

只和你的朋友说话,不要和陌生人说话。

开发场景

在一个汽车租赁系统中,需要远程启动用户的车辆。后台服务 CarService 接收租赁平台的调用,负责将启动指令下发给车辆。为保证系统稳定,应遵循迪米特法则,让服务层只与自己直接认识的对象交互,避免因为内部对象结构变化而连带修改业务逻辑。

错误示例

// 火花塞(“陌生人的‘陌生人’”)
public class SparkPlug
{
    public void Ignite() => Console.WriteLine("火花塞点火");
}

// 引擎(火花塞的拥有者)
public class Engine
{
    public SparkPlug SparkPlug { get; } = new SparkPlug();
}

// 汽车(引擎的拥有者)
public class Car
{
    public Engine Engine { get; } = new Engine();
}

// 汽车启动服务直接“连跳”三层
public class CarService
{
    public void StartCar(Car car)
    {
        // 违背 LoD:CarService 不认识 SparkPlug,却直接调用它的方法
        car.Engine.SparkPlug.Ignite();
        Console.WriteLine("汽车启动完成");
    }
}

问题在于 CarService 不仅和 Car 交互,甚至还直接越级访问 EngineSparkPlug —— 这正是“陌生人的陌生人”(SparkPlug)不该直接接触其方法,违背迪米特法则。

正确示例

// 火花塞保持不变
public class SparkPlug
{
    public void Ignite() => Console.WriteLine("火花塞点火");
}

// 引擎对外只暴露“启动”能力
public class Engine
{
    private readonly SparkPlug _sparkPlug = new SparkPlug();

    public void Start()
    {
        _sparkPlug.Ignite();
    }
}

// 汽车对外只暴露“发动机启动”能力
public class Car
{
    private readonly Engine _engine = new Engine();

    public void StartEngine()
    {
        _engine.Start();
    }
}

// 启动服务只和 Car 互动
public class CarService
{
    public void StartCar(Car car)
    {
        // 只调用 Car 的公开方法,不再穿透内部结构
        car.StartEngine();
        Console.WriteLine("汽车启动完成");
    }
}

// 使用
var car = new Car();
new CarService().StartCar(car);

现在 Engine 不再把 SparkPlug 暴露给外部, Car 也不再把 Engine 的内部细节暴露给调用者。
CarService 只调用 Car.StartEngine(),完全不用关心内部实现。

这就像电影里面——"你知道的太多了"; 所以知道的越少越少,也就是最少知道法则(迪米特法则)

再举个例子:
假设你在超市里头买了东西,现在你需要付钱给收银员。

  • 违反迪米特法则:你直接从收银员的口袋里掏出钱包,再从钱包里拿出钱来塞到收银员的钱包,再把收银员的钱包放回去,完成付账行为。
    • 你和“收银员的朋友”(钱包)发生了直接的交互。
  • 符合迪米特法则:你把钱给收银员,收银员自己去操作她的钱包。
    • 你只和你的直接朋友“收银员”交互,不关心他的钱包是怎么回事。

结语

在开发过程中其实大家已经遵守了其中一个或多个原则,只不过对于概念不是很清晰。

当然,碰到傻逼写的代码还是容易让人生气,忍不住骂两句臭傻逼。