目录
九:总结&提升
一:前言
SOLID是五大设计原则的首字母简写,最早出现于出自Robert Martin(罗伯特. 马丁)的《架构整洁之道》第三章设计原则。他们分别是
single Responsibility Principle:单一职责原则
Open Closed Principle:开闭原则
Liskov Substitution Principle:里氏替换原则
Interface Segregation Principle:接口隔离原则
Dependence Inversion Principle:依赖倒置原则
再加上后来的组合聚合、迪米特法则,就构成了我们软件设计的七大原则。
二:单一职责
2.1 概念
单一职责原则(SRP:Single responsibility principle)又称单一功能原则,它规定一个类应该只有一个发生变化的原因。对于一个类来说,应该只有一个引起它变化的原因。单一职责降低了类功能的耦合程度,如果一个类有两个或者更多的职责,一个职责的改变往往会引起连锁反应,出现意想不到的错误。
2.2 具体代码
下面我们用具体的代码来进行分析。
业务:假设现在我们有很多种交通工具,这些交通工具都要支持运行。
2.2.1不符合单一职责的代码:
internal class Program { //程序开始时的main方法 static void Main(string[] args) { //声明对象进行方法的调用 Vehicle vehicle = new Vehicle(); vehicle.run("直升飞机"); vehicle.run("航天飞机"); //代码到这里是没有问题的,两种飞机在天空上进行行驶,可如果我们来了新的需求新增另外类型的交通工具 vehicle.run("出租车"); vehicle.run("轮船"); //很显然出租车和轮船是不在天空上进行行驶的。Vehicle类的run方法不适合。其原因就是run包含的职责 //太多了,实际上它包含了多种运行的职责。 } } //创建一个交通工具类 public class Vehicle { //实现一个交通工具运行的方法 public void run(string vehicleType) { Console.WriteLine(vehicleType+"在天空中运行"); } }
针对上面存在的问题对我们的代码进行改进
2.2.2 符合单一职责的代码
internal class Program { static void Main(string[] args) { //使用接口进行调用 IVehicle airVehicle = new AirVehicle(); airVehicle.run("直升飞机"); airVehicle.run("航天飞机"); //将职责分成三个类,每个类负责不同的run的实现,调用的时候可以直接根据对应对象进行调用 IVehicle roadVehicle = new RoadVehicle(); roadVehicle.run("出租车"); IVehicle waterVehicle = new WaterVehicle(); waterVehicle.run("轮船"); //此时不会因为职责的冗余而产生错误。并且当我们想要添加新的交通工具时,让它实现跑的接口。直接调用就可以了,也更好的支持了开闭原则 } } //声明一个交通工具的接口,里面包含一个跑的方法 internal interface IVehicle { void run(string vehicle); } //下面的类分别实现跑的方法 class RoadVehicle :IVehicle { public void run(String vehicle) { Console.WriteLine(vehicle + " 在公路运行..."); } } class AirVehicle : IVehicle { public void run(String vehicle) { Console.WriteLine(vehicle + " 在天空运行..."); } } class WaterVehicle :IVehicle { public void run(String vehicle) { Console.WriteLine(vehicle + " 在水上运行..."); } }
为了方便读者阅读,将类图附在下方。三:开闭原则
3.1 概念
开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它源代码的前提下变更它的行为。
下面我们用具体代码来进行实现
业务:假设有一个水果仓库,对苹果、香蕉、两种水果进行存储,现在新增第三种水果梨,进行入库。
3.1.1 错误代码:
internal class FruitStorage { //进行入库的方法,将具体的水果进行传入 public void Warehousing(Fruits fruits) { if (fruits.FruitsType =="1") //如果是第1种类型就调用苹果入库方法 { AppleWarehousing(); } else if (fruits.FruitsType =="2") //如果是第2种类型就调用香蕉入库方法 { BananWarehousing(); } PearWarehousing(); //如果是第3种类型就调用香蕉入库方法 } //具体进行苹果入库的方法 public void AppleWarehousing() { Console.WriteLine("苹果入库了"); } //具体进行香蕉入库的方法 public void BananWarehousing() { Console.WriteLine("香蕉入库了"); } //上面两个是原本写的方法,后面为新增的方法 public void PearWarehousing() { Console.WriteLine("梨入库了"); } } public class Fruits { //水果类型属性 public string FruitsType; } //苹果类 public class Apple :Fruits { //构造函数给水果类型赋值 public Apple() { FruitsType = "1"; } } //香蕉类 public class Banana : Fruits { public Banana() { FruitsType= "2"; } } //梨类 public class Pear : Fruits { public Pear() { FruitsType = "3"; } }
为了方便观看,附上类图 在上述代码中,我们增加需求,添加了Pear类时,需要更改FruitStorage类的Warehousing方法,添加新的分支,不符合开闭原则。
那么我们应该如何改造成符合开闭原则的代码呢?
3.2.2 符合开闭原则
internal class FruitStorage { //进行入库的方法,将具体的水果进行传入 public void Warehousing(Fruits fruits) { fruits.FruitsWarehousing(); //调用具体的水果类 } } public interface IFruits { void FruitsWarehousing(); } //苹果类 public class Apple : IFruits { public void FruitsWarehousing() { Console.WriteLine("苹果入库了"); } } //香蕉类 public class Banana : IFruits { public void FruitsWarehousing() { Console.WriteLine("香蕉入库了"); } } //梨类 public class Pear : IFruits { public void FruitsWarehousing() { Console.WriteLine("梨入库了"); } }
· 新的代码中,我们将具体入库方法的实现放在了水果类,当新增加一个水果类的时候,要实现相应的入库方法,通过添加子类的方式新增类型,不需要进行代码修改。
四: 里氏替换
4.1 概念
派生类(子类)对象可以在程式中代替其基类(超类)对象。要求子类不能对父类的方法进行重写,因为子类很可能改变父类的意思而引入未知错误。
举一个例子,假设有一对唱京剧的父子,儿子会父亲唱的所有的戏,并且还会父亲不会的新式戏曲。某天父亲无法上台,但是戏要必须唱。根据里氏替换,儿子可以代替父亲上台,但是只能唱父亲会的戏,不能唱自己会的。并且不可对父亲的戏进行改编。(如果不遵守,则子类无法完全扮演父类的角色。)
4.2 代码
//唱京剧的父亲 public abstract class Father { //父亲会的戏 public void BeijingOpera1() { Console.WriteLine("京剧1的实现"); } //父亲会,可以被儿子改编的戏 public virtual void BeijingOpera2() { Console.WriteLine("京剧2的实现"); } } //唱京剧的儿子 public class son : Father { public override void BeijingOpera2() { // base.BeijingOpera2(); Console.WriteLine("京剧2子类特殊的实现"); } }
这里子类就更改了父类的方法,使得子类不能代替父类上台演唱,在里氏替换里,这是不被允许的。
五:接口隔离原则
5.1 概念
不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次接口。
接口隔离有两个方面的重点。第一个是一个类对另外一个类的依赖性应当是建立在最小的接口上的,要求类之间通过最小接口通信。第二是这个接口一定要足够小,不让实现这个接口的类去实现不使用的方法。
业务:
定义一个支付接口,某用户要通过支付接口,进行支付。一共有三种支付方式,分别是微信支付、支付宝支付、还有现金支付。
5.1.1 错误的代码:
//支付接口 internal interface IPay { //微信支付 void WeChatPay(); //支付宝支付 void AliPay(); //现金支付 void CashPay(); } //普通顾客 public class OrdinaryCustomers : IPay { public void AliPay() { Console.WriteLine("支付宝支付的具体实现"); } public void CashPay() { Console.WriteLine("现金支付的具体实现"); } public void WeChatPay() { Console.WriteLine("微信支付的具体实现"); } } //互联网顾客,无法使用现金支付 public class InternetCustomers : IPay { public void AliPay() { Console.WriteLine("支付宝支付的具体实现"); } public void CashPay() //虽然无法使用现金支付,但是还是要给出现金支付的实现。 { throw new NotImplementedException(); } public void WeChatPay() { Console.WriteLine("微信支付的具体实现"); } }
上述代码中,明明互联网用户无法进行现金支付,可是还是要给出具体的现金接口的实现,不符合接口隔离。我们应当将接口拆开,让其粒度最小。
5.1.2 正确的代码
//微信支付接口 internal interface IWeChatPay { void WeChatPay(); } //支付宝支付接口 internal interface IAliPay { void AliPay(); } //现金支付接口 internal interface ICashPay { void CashPay(); } //普通顾客 public class OrdinaryCustomers : IAliPay,ICashPay,IWeChatPay { public void AliPay() { Console.WriteLine("支付宝支付的具体实现"); } public void CashPay() { Console.WriteLine("现金支付的具体实现"); } public void WeChatPay() { Console.WriteLine("微信支付的具体实现"); } } //互联网顾客,无法使用现金支付 public class InternetCustomers : IAliPay,IWeChatPay { public void AliPay() { Console.WriteLine("支付宝支付的具体实现"); } public void WeChatPay() { Console.WriteLine("微信支付的具体实现"); } }
为了方便观看,附上类图: 将接口粒度分小之后,下面的类就可以根据自己的业务需求(支付方式)进行支付。
六: 依赖倒置原则
6.1 概念
程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
什么是抽象呢?抽象可以理解为抽“像”,就是将业务里像的东西抽出来,放在一起。把握住事物的本质,将本质抽出来。
这里的业务可以于开闭原则结合,读者可向上翻看开闭原则代码。在开闭原则的代码中,实际上,上层的FruitStorage(商品入库)类是依赖于下面的Apple等水果类的,下方各个水果类的改变将会直接影响上层FruitStorage(商品入库)类的调用。
符合依赖倒置的编写方式,是应该让两者都依赖于抽象,也就是下面的正确的写法,依赖于抽象的IFruits接口,降低客户和模块间的耦合。让代码符合开闭原则。
6.2 类图
正确版本类图:七:组合聚合原则
7.1 概念
要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
利用原有的类来产生新的类的方式有两种,一种是继承,另一种是组合聚合。在六大关系中,继承是耦合性最强的类,耦合性越强,当代码发生变化时产生的影响就越大。
7.2 代码
public class Person { public void sayHello() { Console.WriteLine("打招呼方法的实现"); } } public class User : Person { } public class Operator : Person { }
这里使用继承,让两种用户继承了人类,如果我们继续使用继承的话,每一个用户只能继承一种角色,但是实际上一个用户可以即是普通用户又是管理员,与业务不符所以可以新增角色,这个角色由原有的的类组合/聚合而成。
八:迪米特原则
迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解。
迪米特原则规定两个类尽量不要发生关系如果非要发生关系也使用友元类进行通信,以此来降低类之间的耦合。
九:总结&提升
这篇博客介绍了软件设计中的五大设计原则:单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、接口隔离原则(ISP)和依赖倒置原则(DIP)。此外,还提到了后来补充的两个原则:组合聚合原则和迪米特法则。
1.博客首先详细介绍了单一职责原则,指出一个类应该只有一个变化的原因,通过示例代码展示了不符合单一职责原则和符合单一职责原则的实现方式。
2.接下来,博客讨论了开闭原则,该原则要求软件实体应对扩展开放,对修改封闭。通过一个水果仓库的例子,展示了不符合开闭原则和符合开闭原则的代码实现方式。
3.然后,博客介绍了里氏替换原则,该原则指出派生类对象可以替换其基类对象而不影响程序的正确性。通过唱京剧的父子的例子,说明了不符合里氏替换原则和符合里氏替换原则的代码实现方式。
4.接口隔离原则是下一个讨论的主题,该原则要求客户端不应该强迫依赖它们不需要的接口。通过支付接口的例子,展示了不符合接口隔离原则和符合接口隔离原则的实现方式。
5.最后,博客总结了这些设计原则的重要性,并强调它们在软件设计中的应用价值。
希望大家可以通过这篇博客,了解到什么7大设计原则,学会使用。