当前位置: 首页 > news >正文

SOLID Principle基础入门

(Robert C. Martin (Uncle Bob))

什么是SOLID原则?

SOLID原则是面向对象编程(OOP)中编写高质量代码的指导方针。实际上,即使不使用SOLID原则,仅通过类、继承、封装和多态性,也可以让程序正常运行。那么为什么会出现SOLID原则呢?

SOLID原则是为了提高代码的可维护性 可扩展性 以及管理耦合度 而设计的一种指导方针。换句话说,SOLID原则是编写高质量OOP代码的指南。

Agile Software Development: Principles, Patterns, and Practices (2002)

SOLID原则的起源

SOLID原则最早出现在罗伯特·C·马丁(Robert C. Martin,也被称为Uncle Bob)于2002年出版的《敏捷软件开发:原则、模式与实践》一书中。虽然每个具体的原则在此之前已经存在,但罗伯特·C·马丁将它们整合在一起,并由迈克尔·C·费瑟斯(Michael C. Feathers)建议将其命名为“SOLID”,形成了我们今天熟知的形式。

罗伯特·C·马丁是敏捷开发和清洁代码领域的传奇人物,他对全球程序员社区产生了深远的影响。

在了解这些原则之前,我们需要先了解 敏捷(Agile)的概念。只有理解了敏捷,才能明白这本书为何如此具有革命性,甚至被视为程序员的“圣经”。

敏捷宣言与SOLID的关系

2001年,程序员们发表了《敏捷宣言》。这份宣言强调了编程中的哲学价值,但并没有提供具体的执行方法。罗伯特·C·马丁在《敏捷软件开发:原则、模式与实践》中提供了具体的实施方案,将敏捷的“哲学”转化为“方法论”。这一成果帮助敏捷从理论走向了实际标准。

在初版中,SRP、OCP、LSP、ISP、DIP是分别介绍的,之后在这本书出版后,根据迈克尔·C·费瑟斯(Michael C. Feathers)的建议,这些原则被命名为“SOLID”。

(具体时间线推测大约在2004年左右正式命名为“SOLID”)

这些原则被描述为敏捷设计的方法论。

敏捷原则并不仅仅是快速编码,而是以具备长期可维护性和适应变化能力的设计为目标进行说明的。

因此,遵循SOLID原则对内化敏捷设计方法论有很大的帮助。

之后,罗伯特·马丁(Uncle Bob)根据迈克尔·C·费瑟斯的建议,在博客和演讲中将其命名为“SOLID”,形成了我们熟知的“SOLID原则”。

(“SOLID”有固体、坚固的意思,所以这里也包含了一种文字游戏的趣味)

第七章总结

通过代码中的设计异味(Smells) ,如僵化性(Rigidity)、脆弱性(Fragility)、不流动性(Immobility)、粘滞性(Viscosity)等问题识别问题,并通过渐进式的改进逐步完善设计。这种改进并非一开始就追求完美,而是通过反复重构来优化,而SOLID原则正是这一过程中的实际方法论。

书中提到的“有异味的代码”的特征如下:
僵化性(Rigidity)

定义

  • 软件难以修改。也就是说,代码变得僵硬,当试图修改某一部分时,
  • 修改会对系统的其他多个部分产生影响,导致需要比预期更多的工作量。

特点

  • 代码高度耦合(Tightly Coupled),小的改动会引发连锁修改需求。
  • 难以根据需求变化进行调整。

示例

  • 修改一个类的方法时,发现需要同时修改调用它的数十个其他类。
  • 更改数据库模式时,需要对UI、业务逻辑、数据访问层进行全面修改。

问题原因 :高耦合度(High Coupling)和低内聚性(Low Cohesion)。

解决方法 :通过SOLID中的SRP(单一职责原则)和DIP(依赖倒置原则)降低模块间的耦合度,减少修改对其他部分的影响。

脆弱性(Fragility)

定义

  • 软件容易崩溃或出现错误的状态。
  • 修改某一部分时,意想不到的其他部分出现问题,或者系统变得不稳定。

特点

  • 修改代码后频繁出现Bug。
  • 在与修改部分无直接关联的区域出现错误。

示例

  • 修改支付模块后,登录系统突然无法正常工作。
  • 改进某个方法的逻辑后,使用该方法的其他模块发生运行时错误。

问题原因 :不恰当的继承使用(例如违反LSP)或依赖管理失败。

解决方法 :遵守LSP(里氏替换原则)确保继承结构的稳定性,并通过ISP(接口隔离原则)移除不必要的依赖。

不流动性(Immobility)

定义

  • 软件组件难以复用或移动的状态。
  • 当尝试将特定模块或代码用于其他项目或上下文时,由于过度依赖,难以分离或无法复用。

特点

  • 代码过于紧密地绑定到特定环境。
  • 如果要复用,需要进行大量修改。

示例

  • 数据库查询逻辑与UI代码纠缠在一起,导致无法在其他项目中单独复用查询逻辑。
  • 依赖特定硬件的代码无法在其他平台上运行。

问题原因 :模块间高耦合度和缺乏抽象。

解决方法 :通过DIP(依赖倒置原则)设计为依赖抽象而非具体实现,并通过OCP(开闭原则)创建可复用的结构。

粘滞性(Viscosity)

定义

  • 软件开发环境或代码使任务变得困难且缓慢的状态。
  • 粘滞性高意味着“正确的方式”比“错误的方式”更难操作。

特点

  • 主要分为两种形式:
    • 软件粘滞性(Viscosity of the Software) :代码本身难以维护或难以应用良好设计的情况。
    • 环境粘滞性(Viscosity of the Environment) :构建、测试、部署等开发环境效率低下,导致工作速度变慢。

示例

  • 复制粘贴添加代码比重构更容易(软件粘滞性)。
  • 构建时间过长,导致代码修改后验证变慢(环境粘滞性)。

问题原因 :未遵守设计原则,开发流程复杂。

解决方法 :通过SRP保持代码简洁性,并通过持续重构和自动化测试/构建环境降低粘滞性。

不过,仅靠SOLID原则并不能消除所有设计异味。

它只是一个高质量的指导方针,这些代码异味还取决于个人的架构哲学、主观判断以及实际需求。

例如,典型的“霰弹枪手术(Shotgun Surgery)”问题仅靠SOLID原则很难解决。

“霰弹枪手术”的原因通常是责任分配过多。

“霰弹枪手术”:这是一种反模式(Anti-Pattern),虽然属于错误代码的案例,但经常出现。通常,“霰弹枪手术”是因为责任分散过多而导致的问题。

SOLID与敏捷(Agile)如何关联?
  • SRP 减少代码的僵化性(Rigidity)。
  • OCP 在不修改现有代码的情况下扩展功能,从而降低代码间的粘滞性(Viscosity)。
  • LSP 保证继承结构的安全性,从而减少脆弱性(Fragility)。
  • DIP ISP 降低耦合度,从而减少不流动性(Immobility)。

(img ref:https://www.instagram.com/techwithisha/reel/C1Ws1ZDt8_j/)

SOLID原则?

那么,我们已经了解了上述问题的原因,现在让我们来详细探讨一下SOLID原则。

我对所有智能思维特征的理解

这指的是深入研究某一主题特定方面的态度。

这种研究不仅是为了保持该方面的一致性,同时也意识到自己所处理的内容只是整体的一部分。

我们知道程序必须正确运行,

因此我们可以从这个角度研究程序。

此外,我们也知道程序必须高效,

这一分析可以在其他时间单独进行。

有时,我们会思考程序是否真的必要,如果是,为什么。

然而,同时处理这些不同方面并不会带来任何好处,反而只会造成干扰。

我称之为“关注点分离(Separation of Concerns)”,

虽然无法完全实现,但这是有效整理思路的唯一方法。

当我说“专注于某一方面”时,

这并不意味着忽略其他方面。

相反,这意味着从某个特定方面的角度来看,其他方面暂时并不重要。

也就是说,这是一种既能专注于一件事,又能同时考虑多件事的思维方式。

—Edsger Wybe Dijkstra(On the role of scientific thought, 1982)

1. 单一职责原则(SRP, Single Responsibility Principle)

提出者 : 罗伯特·C·马丁(Robert C. Martin)

定义

  • “一个类应该只有一个职责。” 也就是说,类应该只有一个修改的理由。

意义

  • 如果一个类承担多个角色,一个角色的变化会影响其他角色,导致僵化性和脆弱性增加。
  • 分离职责可以使代码更简单,维护更容易。

示例代码

using System;
using System.Data.SqlClient;
public class Employee_BadExample // 通过类名标明 BadExample
{public string Name { get; set; }public double BaseSalary { get; set; }public string Department { get; set; }private SqlConnection _dbConnection; // Employee 类竟然还负责数据库连接!public Employee_BadExample(string name, double baseSalary, string department, SqlConnection dbConnection){Name = name;BaseSalary = baseSalary;Department = department;_dbConnection = dbConnection; // Employee 类接受数据库连接对象}public double CalculateSalary(){"""计算薪资的方法 (假设不同部门的奖金比例不同)"""double bonusRate = 0;if (Department == "Sales"){bonusRate = 0.1;}else if (Department == "Marketing"){bonusRate = 0.05;}return BaseSalary * (1 + bonusRate);}public void SaveToDatabase(){"""将员工信息存储到数据库的方法"""double salary = CalculateSalary(); // 计算薪资逻辑竟然也在 Employee 内!try{_dbConnection.Open();SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection);command.Parameters.AddWithValue("@Name", Name);command.Parameters.AddWithValue("@Salary", salary);command.Parameters.AddWithValue("@Department", Department);command.ExecuteNonQuery();}catch (Exception ex){Console.WriteLine("数据库存储错误: " + ex.Message);}finally{_dbConnection.Close();}}
}
public class BadExample_Program // 使用 BadExample 的主程序类
{public static void Main(string[] args){// 错误的示例:Employee_BadExample 类承担了太多职责!SqlConnection dbConn = null; // 需替换为实际数据库连接对象 (此处用 null 代替)Employee_BadExample employee = new Employee_BadExample("홍길동", 3000000, "Sales", dbConn);employee.SaveToDatabase(); // Employee_BadExample 既计算薪资,又保存数据库!}
}

错误示例 : Employee类同时处理薪资计算和数据库存储,薪资逻辑变化会影响数据库代码。

using System;
using System.Data.SqlClient;
public class Employee // Employee 类仅负责数据
{public string Name { get; set; }public double BaseSalary { get; set; }public string Department { get; set; }public Employee(string name, double baseSalary, string department){Name = name;BaseSalary = baseSalary;Department = department;}
}
public class SalaryCalculator // 负责薪资计算的类
{public double CalculateSalary(Employee employee) // 接收 Employee 对象作为参数{"""计算薪资的方法"""double bonusRate = 0;if (employee.Department == "Sales"){bonusRate = 0.1;}else if (employee.Department == "Marketing"){bonusRate = 0.05;}return employee.BaseSalary * (1 + bonusRate);}
}
public class EmployeeRepository // 负责数据库存储的类
{private SqlConnection _dbConnection;public EmployeeRepository(SqlConnection dbConnection){_dbConnection = dbConnection;}public void Save(Employee employee, double salary) // 接收 Employee 对象和计算后的薪资{"""将员工信息存储到数据库的方法"""try{_dbConnection.Open();SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection);command.Parameters.AddWithValue("@Name", employee.Name);command.Parameters.AddWithValue("@Salary", salary);command.Parameters.AddWithValue("@Department", employee.Department);command.ExecuteNonQuery();}catch (Exception ex){Console.WriteLine("数据库存储错误: " + ex.Message);}finally{_dbConnection.Close();}}
}
public class GoodExample_Program // 使用 GoodExample 的主程序类
{public static void Main(string[] args){// 正确的示例:每个类只承担单一职责!SqlConnection dbConn = null; // 需替换为实际数据库连接对象Employee employee = new Employee("김철수", 3500000, "Marketing");SalaryCalculator calculator = new SalaryCalculator(); // 创建负责薪资计算的对象double salary = calculator.CalculateSalary(employee);EmployeeRepository repository = new EmployeeRepository(dbConn); // 创建负责数据库存储的对象repository.Save(employee, salary);}
}

正确示例 : 将其分为SalaryCalculatorEmployeeRepository

优点 : 提高代码的内聚性(Cohesion),降低耦合度(Coupling)。
相关设计异味 : 僵化性、粘滞性缓解。

(Clean Coder Blog)

历史背景 : 根据罗伯特·C·马丁博客所述,SRP起源于大卫·L·帕纳斯的模块分解和戴克斯特拉的关注点分离概念。
结合当时编程社区中流行的耦合与内聚概念,最终形成了SRP。

正如罗伯特·C·马丁在博客中所说,SRP是关于人的。
现实中,软件会随着企业或组织的需求而变化,因此每个模块只负责单一业务功能,以便于明确哪个团队负责修改该功能。

2. 开闭原则(OCP, Open/Closed Principle)

提出者 : 贝特朗·迈耶(Bertrand Meyer)

定义

  • “软件实体(类、模块等)应对扩展开放,对修改关闭。”

意义

  • 在不修改现有代码的情况下添加新功能。
  • 使用抽象(接口、抽象类)和多态性来实现。

示例

using System;
public class PaymentProcessor_BadExample // 通过类名标明 BadExample
{public void ProcessPayment(string paymentMethod, double amount){"""根据支付方式处理支付的方法 (大量使用 if-else 语句)"""if (paymentMethod == "Card"){// 处理信用卡支付逻辑Console.WriteLine($"使用信用卡支付 {amount} 元");}else if (paymentMethod == "Cash"){// 处理现金支付逻辑Console.WriteLine($"使用现金支付 {amount} 元");}else if (paymentMethod == "MobilePay") // 新增支付方式!必须修改代码!{// 处理移动支付逻辑Console.WriteLine($"使用移动支付 {amount} 元");}else{Console.WriteLine("不支持的支付方式");}}
}
public class BadExample_Program // 使用 BadExample 的主程序类
{public static void Main(string[] args){// 错误的示例: PaymentProcessor_BadExample 违反开放-封闭原则 (OCP),无法轻易扩展!PaymentProcessor_BadExample processor = new PaymentProcessor_BadExample();processor.ProcessPayment("Card", 10000);processor.ProcessPayment("Cash", 5000);processor.ProcessPayment("MobilePay", 7000); // 使用新的支付方式}
}

错误示例 : 每次向PaymentProcessor类添加新的支付方式(如卡支付、现金支付)时,都需要修改if-else条件。

using System;
//  IPayment 接口: 适用于各种支付方式的通用接口
public interface IPayment
{void ProcessPayment(double amount);
}
//  信用卡支付类 (实现 IPayment)
public class CardPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[信用卡支付] 付款 {amount:N0} 元 完成");}
}
//  现金支付类 (实现 IPayment)
public class CashPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[现金支付] 付款 {amount:N0} 元 完成");}
}
//  移动支付类 (实现 IPayment)
public class MobilePayPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[移动支付] 付款 {amount:N0} 元 完成");}
}
//  支付处理类: 符合开放-封闭原则 (OCP)
public class PaymentProcessor
{private readonly IPayment _paymentMethod;//  通过构造函数注入支付方式 (可应用依赖注入 DI)public PaymentProcessor(IPayment paymentMethod){_paymentMethod = paymentMethod ?? throw new ArgumentNullException(nameof(paymentMethod));}public void Process(double amount){_paymentMethod.ProcessPayment(amount);}
}
public class Program
{public static void Main(){// 创建各种支付方式对象var cardPayment = new CardPayment();var cashPayment = new CashPayment();var mobilePayPayment = new MobilePayPayment();// 符合 OCP: 新增支付方式时,无需修改 PaymentProcessor 代码var processor1 = new PaymentProcessor(cardPayment);processor1.Process(10000);var processor2 = new PaymentProcessor(cashPayment);processor2.Process(5000);var processor3 = new PaymentProcessor(mobilePayPayment);processor3.Process(7000);}
}

正确示例 : 创建IPayment接口,并通过CardPaymentCashPayment类进行扩展。

优点 : 维持现有代码的稳定性,灵活应对新需求。
相关设计异味 : 僵化性、不流动性缓解。

出处 : 出自《面向对象软件构造》(Object-Oriented Software Construction, 1988),第2章“模块化”部分。

(Data Abstraction and Hierarchy) (1987, OOPSLA )

3. 里氏替换原则(LSP, Liskov Substitution Principle)

提出者 : 芭芭拉·利斯科夫(Barbara Liskov)

定义

  • “子类应能在不干扰父类行为的情况下替代父类。”
  • 也就是说,在程序中用子类型替换父类型时,程序仍能正常运行。

意义

  • 在继承关系中,子类不应违反父类的契约(Contract)。
  • 这是安全使用多态性的原则。

示例

public class Bird
{public virtual void Fly() => Console.WriteLine("鸟在飞翔。");
}
public class Penguin : Bird
{public override void Fly() //  企鹅不能飞!{throw new NotImplementedException("企鹅无法飞行。");}
}
public class Program
{public static void MakeBirdFly(Bird bird){bird.Fly(); //  如果传入的是 Penguin 对象,则会抛出异常!}static void Main(){Bird myBird = new Penguin();MakeBirdFly(myBird); //  可能导致程序崩溃}
}

错误示例 : Bird类有Fly()方法,而Penguin子类忽略或抛出异常。


//  将 Bird 抽象化,不允许直接使用
public abstract class Bird { } 
//  定义飞行接口
public interface IFlyable
{void Fly();
}
//  麻雀类 (实现 IFlyable 接口)
public class Sparrow : Bird, IFlyable
{public void Fly() => Console.WriteLine("麻雀在飞翔。");
}
//  企鹅类 (不实现 IFlyable 接口,表示不能飞)
public class Penguin : Bird { } 
public class Program
{public static void MakeBirdFly(IFlyable bird){bird.Fly();}static void Main(){IFlyable sparrow = new Sparrow();MakeBirdFly(sparrow); //  正常运行}
}

正确示例 : 将Bird分为FlyingBirdWalkingBird,使Penguin不需要实现Fly()

优点 : 确保继承结构的稳定性和可预测性。
相关设计异味 : 脆弱性缓解。

背景 : 该原则源自1987年OOPSLA会议论文,论文讨论了数据抽象和层次结构,为面向对象中的“继承(Inheritance)”提供了哲学和实用的指导方针。

数据抽象是指隐藏程序中数据的内部实现,仅通过接口访问。

归根结底,这是一个如何更好地抽象现实问题的问题。例如,如果将哺乳动物定义为“有腿的生物”,那么鲸鱼就难以被称为哺乳动物。因此,如何恰当地进行抽象才是关键。

4. 接口隔离原则(ISP, Interface Segregation Principle)

提出者 : 罗伯特·C·马丁(Robert C. Martin)

定义

  • “客户端不应依赖于它不需要的接口。”
  • 也就是说,接口应尽可能小且具体。

意义

  • 设计只提供客户端所需功能的接口,而不是大型通用接口。
  • 移除不必要的依赖以降低耦合度。

示例

using System;
// IWorker_BadExample 接口: 包含了太多功能 (违反 ISP)
public interface IWorker_BadExample
{void Work();   // 工作功能void Eat();    // 进食功能 - 但对 Robot 来说是不必要的!
}
// Robot_BadExample 类: 实现 IWorker_BadExample,必须强制实现不必要的 Eat() 方法
public class Robot_BadExample : IWorker_BadExample
{public void Work(){Console.WriteLine("机器人正在努力工作。");}public void Eat() //  机器人不需要进食,但仍然必须实现{// 机器人不吃饭,因此只能什么都不做,或者抛出异常Console.WriteLine("机器人无法进食。"); // 或者 throw new NotImplementedException();}
}
// HumanWorker_BadExample 类: 实现 IWorker_BadExample,正确地实现 Work() 和 Eat()
public class HumanWorker_BadExample : IWorker_BadExample
{public void Work(){Console.WriteLine("人类正在努力工作。");}public void Eat(){Console.WriteLine("人类正在吃午饭。");}
}
public class BadExample_Program // 使用 BadExample 的程序类
{public static void Main(string[] args){//  错误示例: Robot_BadExample 不应该有 Eat() 方法!IWorker_BadExample robot = new Robot_BadExample();robot.Work();robot.Eat(); // 机器人调用 Eat() 方法显得很奇怪IWorker_BadExample human = new HumanWorker_BadExample();human.Work();human.Eat();}
}

错误示例 : IWorker接口包含Work()Eat()方法,导致Robot类需要实现不必要的Eat()方法。

using System;
// IWorkable 接口: 仅包含工作功能 (遵循 ISP)
public interface IWorkable
{void Work(); // 工作功能
}
//  IEatable 接口: 仅包含进食功能 (遵循 ISP)
public interface IEatable
{void Eat();  // 进食功能
}
// Robot_GoodExample 类: 仅实现 IWorkable 接口 (仅实现必要的功能)
public class Robot_GoodExample : IWorkable // 机器人只能工作
{public void Work(){Console.WriteLine("机器人高效地执行任务。");}// Eat() 方法未实现: 机器人不需要进食
}
//  HumanWorker_GoodExample 类: 实现 IWorkable 和 IEatable 接口 (拥有所有必要功能)
public class HumanWorker_GoodExample : IWorkable, IEatable // 人类既能工作,也能进食
{public void Work(){Console.WriteLine("人类创造性地工作。");}public void Eat(){Console.WriteLine("人类正在享受美味的午餐。");}
}
public class GoodExample_Program // 使用 GoodExample 的程序类
{public static void Main(string[] args){//  正确示例: Robot_GoodExample 只需要实现 IWorkable!IWorkable robot = new Robot_GoodExample(); // 机器人仅用作 IWorkable 类型robot.Work();// robot.Eat(); // Robot 未实现 IEatable,因此无法调用 Eat() 方法 (编译错误)IWorkable humanWorker = new HumanWorker_GoodExample(); // HumanWorker 可用作 IWorkable 类型humanWorker.Work();IEatable humanEater = new HumanWorker_GoodExample(); // HumanWorker 也可用作 IEatable 类型humanEater.Eat();}
}

正确示例 : 将接口拆分为IWorkableIEatable,使Robot只需实现IWorkable

优点 : 提高代码的灵活性和可重用性。
相关设计异味 : 脆弱性、粘滞性缓解。

出处 : 出自罗伯特·C·马丁1996年的文章。

(1996 Robert C. Martin Essay)

5. 依赖倒置原则(DIP, Dependency Inversion Principle)

提出者 : 罗伯特·C·马丁(Robert C. Martin)

定义

  • “高层模块不应依赖于低层模块,二者都应依赖于抽象。”
  • 此外,“不要依赖具体实现,而是依赖抽象。”

意义

  • 通过接口或抽象类减少模块间的依赖。
  • 通过依赖注入(Dependency Injection)实现。

示例

using System;
// SqlDatabase 类: 具体数据库的实现 (低级模块,直接依赖于某个数据库)
public class SqlDatabase_BadExample
{public void Save(string data){// 实际将数据存入 SqlDatabase 的逻辑 (省略实现)Console.WriteLine($"数据已存入 SqlDatabase: {data}");}
}
// OrderService_BadExample 类: 直接依赖 SqlDatabase (违反 DIP,属于高级模块)
public class OrderService_BadExample
{private SqlDatabase_BadExample _database; // 直接依赖于具体的 SqlDatabase 类!public OrderService_BadExample(){_database = new SqlDatabase_BadExample(); // OrderService 直接创建 SqlDatabase 实例}public void PlaceOrder(string orderData){// 订单处理逻辑 (这里只是简单地存储数据)Console.WriteLine($"订单处理中: {orderData}");_database.Save(orderData); // OrderService 直接调用 SqlDatabase 的 Save() 方法Console.WriteLine("订单处理完成");}
}
public class BadExample_Program // 使用 BadExample 的程序类
{public static void Main(string[] args){//  错误示例: OrderService_BadExample 与 SqlDatabase 强耦合,难以扩展!OrderService_BadExample service = new OrderService_BadExample();service.PlaceOrder("客户: 张三, 商品: 笔记本电脑");}
}

错误示例 : OrderService直接依赖于SqlDatabase,更换数据库时需要修改代码。

using System;
// 遵循 DIP (依赖倒置原则): OrderService 仅依赖 IDatabase 接口
public interface IDatabase
{void SaveOrder(string orderDetails);
}
// SqlDatabase 实现 IDatabase 接口
public class SqlDatabase : IDatabase
{public void SaveOrder(string orderDetails){Console.WriteLine($"[SqlDatabase] 订单已保存: {orderDetails}");}
}
// MongoDatabase 实现 IDatabase 接口 (可以添加新的数据库类型)
public class MongoDatabase : IDatabase
{public void SaveOrder(string orderDetails){Console.WriteLine($"[MongoDatabase] 订单已保存: {orderDetails}");}
}
// OrderService 依赖于接口 (IDatabase),不依赖具体实现
public class OrderService
{private readonly IDatabase _database;// 依赖倒置原则 (DIP): OrderService 依赖接口,而不是具体实现public OrderService(IDatabase database){_database = database; // 依赖注入 (Dependency Injection)}public void PlaceOrder(string orderDetails){_database.SaveOrder(orderDetails); // 通过接口存储订单}
}
// OrderService 不再依赖特定数据库 → 可以轻松切换数据库
public class Program
{public static void Main(){// 在不使用 DI 容器的情况下,直接创建对象并注入依赖IDatabase sqlDatabase = new SqlDatabase();IDatabase mongoDatabase = new MongoDatabase();// 使用 SqlDatabase 的 OrderServiceOrderService orderService1 = new OrderService(sqlDatabase);orderService1.PlaceOrder("商品 A 订单");// 使用 MongoDatabase 的 OrderServiceOrderService orderService2 = new OrderService(mongoDatabase);orderService2.PlaceOrder("商品 B 订单");}
}

正确示例 : 创建IDatabase接口,OrderService依赖于接口,SqlDatabase作为具体实现。

优点 : 提高系统的灵活性和测试便利性。
相关设计异味 : 僵化性、不流动性缓解。

SOLID原则的整体意义

提高可维护性和扩展性,从而构建能够灵活应对变化的软件。

原则

定义

提出者

S - 单一职责原则 (SRP)

一个类应该只有一个职责。

罗伯特·C·马丁 (2002)

O - 开闭原则 (OCP)

在不修改现有代码的情况下扩展功能。

贝特朗·迈耶 (1988)

L - 里氏替换原则 (LSP)

子类应能替代父类。

芭芭拉·利斯科夫 (1987)

I - 接口隔离原则 (ISP)

客户端不应依赖于它不需要的接口。

罗伯特·C·马丁 (2002)

D - 依赖倒置原则 (DIP)

高层模块不应依赖于低层模块,而是依赖于抽象。

罗伯特·C·马丁 (1996)

SOLID原则真的是绝对正确的答案吗?

当然不是。SOLID只是一个指导方针。

示例:虚幻引擎中的Actor

虚幻引擎的Actor负责物理、碰撞、光照、网格渲染等大量任务。

也就是说,单个类承担了太多的任务。那么,如果将这些任务分开,真的会变得方便吗?完全不会。相反,分离的成本可能远远高于收益。

Actor是虚幻引擎的核心基础类。如果为了遵循SRP(单一职责原则)而将其拆分,会对整个引擎产生影响。

那么,这是不是一个糟糕的设计呢?并不是。因为游戏是以对象为中心设计的,在这个单位下,这种设计是有充分理由的。

如果强行拆分,反而会导致在对象级别重新组合时需要付出巨大的成本。

LSP的情况

同样地,LSP(里氏替换原则)也有例外。例如,在GUI框架中,当Button继承自Widget时,是否必须保证父类的按钮行为?

如果是这样,反而会导致实际问题。在这种情况下,如果需要多样化地设计按钮样式,可能会破坏设计的创造性。

因此,许多GUI框架(如C++的QT框架和GTK等)允许这样的例外。

此外,由于接口带来的开销,有时选择DOP(数据导向编程)而非OOP可能是更合适的。

虽然SOLID是面向对象编程(OOP)设计的一个很好的指导方针,但也需要了解OOP可能存在的性能限制和设计背景,并明确何时可以不遵守这些原则。

那么,什么时候可以违背SOLID原则?
  • SRP(单一职责原则) :如果功能具有很强的内聚性,严格遵守SRP可能导致类之间频繁调用方法,从而引发性能开销。
  • OCP(开闭原则) :虽然扩展频繁是好事,但如果性能优先,修改可能比扩展更好。
  • LSP(里氏替换原则) :如果继承结构简单,则不需要强制遵守。
  • ISP(接口隔离原则) :如果接口过多,反而会导致混乱。
  • DIP(依赖倒置原则) :如果抽象化带来了额外开销,具体依赖可能是更好的选择。

“假设维护你代码的人是一个知道你住址的暴力精神病患者,那么你就应该以这种方式编写代码。”-John F. Woods(1991年)

SOLID的本质与实用性平衡

SOLID原则诞生于敏捷哲学,作为提升代码可维护性和扩展性的强大工具,已经占据了重要地位。

然而,它并不是适用于所有情况的“银弹”。

对于像虚幻引擎的Actor这样具有强烈领域特性的场景,或者在GUI框架中需要发挥创造力时,又或者在性能至关重要的系统中,与其勉强遵循SOLID原则,不如选择适合上下文的设计更为重要。

正如John F. Woods所说,“假设维护你代码的人是一个知道你住址的暴力精神病患者”,这不仅仅是在提醒我们写易读的代码。

其背后的真正含义是,无论在什么情况下,都要让处理代码的人能够轻松理解并适应代码。

SOLID只是实现这一目标的一种方式,而不是唯一的方式。有时,违反SRP保持一个整合的类,或者忽略DIP选择具体的依赖,可能是防止“精神病患者”采取极端行动的实用选择。

相关文章:

SOLID Principle基础入门

(Robert C. Martin (Uncle Bob)) 什么是SOLID原则? SOLID原则是面向对象编程(OOP)中编写高质量代码的指导方针。实际上,即使不使用SOLID原则,仅通过类、继承、封装和多态性,也可以让程序正常运行。那么为…...

keil主题(vscode风格)

#修改global.prop文件,重新打开keil即可 # Keil uVision Global Properties File # This file is used to customize the appearance of the editor# Editor Font editor.font.nameConsolas editor.font.size10 editor.font.style0# Editor Colors editor.backgro…...

微信小程序读取写入NFC文本,以及NFC直接启动小程序指定页面

一、微信小程序读取NFC文本(yyy优译小程序实现),网上有很多通过wx.getNFCAdapter方法来监听读取NFC卡信息,但怎么处理读取的message文本比较难找,现用下面方法来实现,同时还解决几个问题,1、在回调方法中this.setData不更新信息,因为this的指向问题,2、在退出页面时,…...

大模型使用

prompt生成bot 角色:你扮演一个帮助用户生成大模型prompt内容的角色,不要直接回答问题,而是帮助用户生成prompt 任务:根据用户的输入,分析用户意图,与用户进行多轮沟通,最后根据对话形成最终的prompt 指令:最终形成的prompt必须包含以下6个方面: 1.所有三个引号之间的内容原样输…...

ISP 常见流程

1.sensor输出:一般为raw-OBpedestal。加pedestal避免减OB出现负值,同时保证信号超过ADC最小电压阈值,使信号落在ADC正常工作范围。 2. pedestal correction:移除sensor加的基底,确保后续处理信号起点正确。 3. Linea…...

SpringBoot原理-02.自动配置-概述

一.自动配置 所谓自动配置,就是Spring容器启动后,一些配置类、bean对象就自动存入了IOC容器当中,而不需要我们手动声明,直接从IOC容器中引入即可。省去了繁琐的配置操作。 我们可以首先将spring项目启动起来,里面有一…...

小红书自动评论

现在越来越多的人做起来小红书,为了保证自己的粉丝和数据好看,需要定期养号。 那么养号除了发视频外,还需要积极在社区互动,比如点赞、评论等等,为了节省时间,我做了一个自动化评论工具。 先看效果 那这个是…...

CosyVoice2整合包 特殊声音标记,声音克隆更逼真,新增批量生成

新增批量生成,可用于制作直播话术音频 特殊声音标记 符号示例1_语气加强<strong> </strong>每天都<strong>付出</strong>和<strong>精进</strong>&#xff0c;才能达到巅峰。2_呼吸声[breath][breath] 吸气,[breath] 呼气! [breath] 吸,[b…...

每天一个Flutter开发小项目 (8) : 掌握Flutter网络请求 - 构建每日名言应用

引言 欢迎再次回到 每天一个Flutter开发小项目 系列博客!在之前的七篇博客中,我们已经掌握了 Flutter UI 构建、状态管理、路由导航、表单处理,甚至数据持久化等一系列核心技能。您已经能够构建功能相对完善的本地应用。 然而,在互联网时代,绝大多数应用都需要与服务器进…...

C++Primer学习(4.8位运算符)

4.8位运算符 位运算符作用于整数类型的运算对象&#xff0c;并把运算对象看成是二进制位的集合。位运算符提供检查和设置二进制位的功能&#xff0c;如17.2节(第640页)将要介绍的&#xff0c;一种名为bitset的标准库类型也可以表示任意大小的二进制位集合,所以位运算符同样能用…...

在VSCode中使用MarsCode AI最新版本详解

如何在VSCode中使用MarsCode AI&#xff1a;最新版本详解与使用场景 在当今快速发展的软件开发领域&#xff0c;人工智能&#xff08;AI&#xff09;技术的应用已经变得越来越普遍。ByteDance推出的MarsCode AI是一款强大的AI编程助手&#xff0c;旨在帮助开发者更高效地编写代…...

可观测之Tracing-eBPF生态和发展

eBPF生态系统 eBPF已经不仅仅是一个内核技术&#xff0c;而是一个蓬勃发展的生态系统&#xff0c;涵盖了各种工具、库和项目&#xff0c;为可观测性、网络、安全等领域提供了强大的支持。 1. 核心工具与库 bcc (BPF Compiler Collection): 定位&#xff1a; 提供了更底层的e…...

linux 后台执行并输出日志

在Linux系统中&#xff0c;后台执行程序并输出日志通常有多种方法&#xff0c;这里列出几种常见的方法&#xff1a; 1. 使用&将命令放入后台 可以在命令的末尾加上&符号&#xff0c;将命令放入后台执行。例如&#xff1a; your_command > output.log 2>&1…...

C++ primer plus 第五节 循环

系列文章目录 C primer plus 第一节 步入C-CSDN博客 C primer plus 第二节 hello world刨析-CSDN博客 C primer plus 第三节 数据处理-CSDN博客 C primer plus 第四节 复合类型-CSDN博客 文章目录 系列文章目录 文章目录 前言 一 for循环 总结 前言 由于作者看了后面的内容&…...

使用Hydra进行AI项目的动态配置管理

引言:机器学习中的超参数调优挑战 在机器学习领域,超参数调优是决定模型性能的关键环节。不同的模型架构,如神经网络中的层数、节点数,决策树中的最大深度、最小样本分割数等;以及各种训练相关的超参数,像学习率、优化器类型、批量大小等,其取值的选择对最终模型的效果…...

.bash_profile一些笔记

下方ffmpeg目录为/Users/sin/Downloads/kakaaaaa/bin/ffmpeg 第一种方法冒号后拼接路径 第二种方法冒号后拼接变量 第三种方法,依旧用PATH变量拼接,更清晰美观而已 export的作用 权限问题&#xff1a; 确保 /Users/sin/Downloads/kaka/bin/ffmpeg 有可执行权限&#xff08;通…...

数据虚拟化的中阶实践:从概念到实现

数据虚拟化的中阶实践:从概念到实现 在大数据时代,数据的数量、种类和来源呈现爆炸式增长,如何高效、灵活地访问和利用这些数据成为了企业面临的重要问题。数据虚拟化作为一种创新的技术,正逐渐成为解决这一难题的关键。它通过抽象化层将底层数据源与应用程序隔离,使得数…...

MongoDB安全管理

MongoDB如何鉴权 保证数据的安全性是数据库的重大职责之一。与大多数数据库一样&#xff0c;MongoDB内部提供了一套完整的权限防护机制。如下例所示&#xff1a; mongo --host 127.0.0.1 --port 27017 --username someone --password errorpass --authenticationDatabasestor…...

[STM32]从零开始的STM32 DEBUG问题讲解及解决办法

一、前言 最近也是重装了一次keil&#xff0c;想着也是重装了&#xff0c;也是去官网下载了一个5.41的最新版&#xff0c;在安装和配置编译器和别的版本keil都没太大的区别&#xff0c;但是在调试时&#xff0c;遇到问题了&#xff0c;在我Debug的System Viewer窗口中没有GPIO&…...

创建Order项目实现Clean Hexagonal架构

创建Order项目实现Clean & Hexagonal架构 前言 在上一节中&#xff0c;讲到了Clean & Hexagonal架构的理论部分&#xff0c;并且通过图形解释了从MVC架构到清洁架构到演变。下面我们通过创建项目的方式来进一步理解Clean & Hexagonal架构。 1.项目创建 1. 项目…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

ES6从入门到精通:前言

ES6简介 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript语言的重大更新&#xff0c;引入了许多新特性&#xff0c;包括语法糖、新数据类型、模块化支持等&#xff0c;显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var&#xf…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)

0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述&#xff0c;后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作&#xff0c;其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

【解密LSTM、GRU如何解决传统RNN梯度消失问题】

解密LSTM与GRU&#xff1a;如何让RNN变得更聪明&#xff1f; 在深度学习的世界里&#xff0c;循环神经网络&#xff08;RNN&#xff09;以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而&#xff0c;传统RNN存在的一个严重问题——梯度消失&#…...

python报错No module named ‘tensorflow.keras‘

是由于不同版本的tensorflow下的keras所在的路径不同&#xff0c;结合所安装的tensorflow的目录结构修改from语句即可。 原语句&#xff1a; from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后&#xff1a; from tensorflow.python.keras.lay…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...