1. 单一职责原则(Single Responsibility Principle)
2. 里氏替换原则(Liskov Substitution Principle)
3. 依赖倒置原则(Dependence Inversion Principle)
4. 接口隔离原则(Interface Segregation Principle)
5. 迪米特法则(Law Of Demeter)
6. 开闭原则(Open Close Principle)
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
缩写 | 原则名称 | 概述 |
---|---|---|
SPR | 单一职责原则 | 每一个类应该专注于做一件事情。 |
LSP | 里氏替换原则 | 基类存在的地方,子类是可以替换的 |
DIP | 依赖倒置原则 | 实现尽量依赖抽象,不依赖具体实现。高层模块不应该直接依赖于低层模块,高层模块和低层模块应该同时依赖一个抽象层。 |
ISP | 接口隔离原则 | 应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。 |
LOD | 迪米特法则 | 又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。 |
OCP | 开闭原则 | 面向扩展开放,面向修改关闭。 |
CARP | 组合/聚合复用原则 | 尽量使用合成/聚合达到复用,少用继承。原则: 一个类中有另一个类的对象。 |
解析
1. 单一职责原则(Single Responsibility Principle):
见名知意,这个条职责的潜台词的就是,专注做一个事。单一职责原则可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用此原则。
2. 里氏替换原则(Liskov Substitution Principle):
将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常。反之则不成立,因为如果使用的是一个子类对象的话,那么它不一定能够使用基类对象(子类拥有父类未拥有的函数)。
此原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来定义对象,而在运行时再确定其子类类型,用子类对象来替换父类对象。
使用里氏替换原则时需要注意,子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法。尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加新的子类来实现。从大局看多态就属于这个原则。
示例代码
public abstract class Phone
{
public abstract void Call();
}
interface Android{ }
interface IOS{ }
public class OnePlus : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(OnePlus)}进行通话。。。。。");
}
}
public class Pixel : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(Pixel)}进行通话。。。。。");
}
}
public class XiaoMi : Phone, Android
{
public override void Call()
{
Debug.Log($"{nameof(XiaoMi)}进行通话。。。。。");
}
}
public class Apple : Phone, IOS
{
public override void Call()
{
Debug.Log($"{nameof(Apple)}进行通话。。。。。");
}
}
不使用里氏替换,调用call函数需要为每个类型的手机写一个函数
public void WantToCall_0(OnePlus phone)
{
phone.Call();
}
public void WantToCall_1(Pixel phone)
{
phone.Call();
}
public void WantToCall_2(XiaoMi phone)
{
phone.Call();
}
public void WantToCall_3(Apple phone)
{
phone.Call();
}
使用里氏替换,只需要一个函数全部搞定,在后面的【桥接模式】会广泛用到的
public void WantToCall_4(Phone phone)
{
phone.Call();
}
3. 依赖倒置原则(Dependence Inversion Principle):
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 依赖倒置原则的核心思想是面向接口编程。
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
什么是高层模块,什么是低层模块?
在项目中我们经常会有一些数学函数库,或者工具类(Log日志工具),这些封装好的工具会被我们业务逻辑模块经常调用,那么这些工具函数库就是高层模块,业务逻辑模块就是低层模块。
什么是细节,什么是抽象?
细节的意思就是具体的实现,例如上面里氏替换中打电话的例子,在不使用里氏替换的时候需要定义4种打电话函数,这就是依赖细节,其中的细节就是“OnePlus ”、“Pixel ”、“XiaoMi”、“Apple ”,反之抽象就是Phone 。
简例:华硕和微型都可使用不同型号的显卡,反之不同型号的显卡也可以使用在不同品牌的主板上。利用依赖倒置原则,这种规则的实现变得很简单也很灵活~
示例代码
//显卡
public interface IGraphicsCard
{
void BeginWork(IMainboard mainboard);
}
//主板
public interface IMainboard
{
void GetElectricity();
void DrawPicture(IGraphicsCard graphicsCard);
}
public class NVIDIA_2018Ti : IGraphicsCard
{
public NVIDIA_2018Ti(IMainboard mainboard)
{
BeginWork(mainboard);
}
public void BeginWork(IMainboard mainboard)
{
mainboard.GetElectricity();
Debug.Log($"NVIDIA_2018Ti获取{mainboard.GetType()}电量后开始工作");
}
}
public class NVIDIA_2018 : IGraphicsCard
{
public NVIDIA_2018(IMainboard mainboard)
{
BeginWork(mainboard);
}
public void BeginWork(IMainboard mainboard)
{
mainboard.GetElectricity();
Debug.Log($"NVIDIA_2018获取{mainboard.GetType()}电量后开始工作");
}
}
//华硕主板
public class Asus : IMainboard
{
public void DrawPicture(IGraphicsCard graphicsCard) { }
public void GetElectricity() { }
}
//微型主板
public class MSI : IMainboard
{
public void DrawPicture(IGraphicsCard graphicsCard) { }
public void GetElectricity() { }
}
实现代码,这种2*2种模式的实现非常简单~
public void DrawPicture()
{
IMainboard aSus = new Asus();
aSus.DrawPicture(new NVIDIA_2018Ti(aSus));
aSus.DrawPicture(new NVIDIA_2018(aSus));
IMainboard mSI = new MSI();
mSI.DrawPicture(new NVIDIA_2018Ti(mSI));
mSI.DrawPicture(new NVIDIA_2018(mSI));
}
4. 接口隔离原则(Interface Segregation Principle):
提供尽可能小的单独接口,而不要提供大的总接口。具体行为让实现类了解越少越好。
尽量细化接口,接口中的方法尽量少。也就是要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的约定,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
通俗的讲就是定义的接口尽量按照功能细分,比如打电话功能一个接口,上网一个接口,发短信一个接口。接口粒度小不仅职能明确,也不会因为使用某种职能而必须实现一些不必要的功能。
示例代码
5. 迪米特法则(Law Of Demeter)又称【最少知识原则】:
类与类之间的关系越密切,耦合度越大,只有降低类与类之间的耦合才符合设计模式;对于被依赖的类来说,无论逻辑多复杂都要尽量封装在类的内部。
每个对象都会与其他对象有耦合关系,我们称出现成员变量、方法参数、方法返回值中的类为直接的耦合依赖,而出现在局部变量中的类则不是直接耦合依赖,也就是说,不是直接耦合依赖的类最好不要作为局部变量的形式出现在类的内部。
一个对象对另一个对象知道的越少越好,即一个实体应当尽可能少的与其他实体发生相互作用,在一个类里降低引用其他类,尤其是局部变量的依赖类,能省则省。同时两个类不必彼此直接通信,那么这两个类就不必发生直接的相互作用。如果其中一个类需要调用另一个类的某一方法的话,可以通过第三者转发这个调用。
表达的意思是能用 private、protected的就不要用public,不要过多的暴露自己的内容,而且对应类与类之间的关系,尽量越少越好。后面讲到的门面模式和中介者模式想表达的也是这个意思。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,一个处于弱耦合的类被修改,对有关类造成波及的影响越小。
示例代码
注:这种情况就违背了迪米特法则。因为其他的类不需要这个Log扩展,这也就破坏了原来的结构,侵入性太强了。如果所有的扩展都是以Object为基准,那么调用函数的时候就会造成下拉函数条目过多。
public static class Exted
{
public static void CustomerLog_Obj0(this GameObject Obj) { }
public static void CustomerLog_Obj1(this object Obj) { }
public static void CustomerLog_Obj2(this object Obj) { }
public static void CustomerLog_Obj3(this object Obj) { }
public static void CustomerLog_Obj4(this object Obj) { }
}
6. 开闭原则(Open Close Principle):
主要体现对扩展开放、对修改封闭,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。软件需求总是变化的,世界上没有一个软件是不变的,因此对软件设计人员来说,在不需要对原有系统进行修改的情况下,实现灵活的系统扩展。
例如通过模板方法模式和策略模式进行重构,实现对修改封闭,对扩展开放的设计思路。
封装变化,是实现开闭原则的重要手段,对于经常发生变化的状态,将其封装为一个抽象,但拒绝滥用抽象,只将经常变化的部分进行抽象。
通俗的讲在功能变动的时候,尽量以增量补丁的形式更改,也就是原来代码保持不变的同时进行更改。
7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP):
整个设计模式就是在讲如何合理安排类与类之间的组合/聚合。在一个新的对象里面通过关联关系,使一些已有的对象成为新对象的一部分,新对象通过委派调用已有对象的方法,达到复用其已有功能的目的。也就是,要尽量使用类的合成复用,不要使用继承。
继承复用破坏数据封装性,将基类的实现细节全部暴露给了派生类,基类的内部细节常常对派生类是透明的。白箱复用,虽然简单,但不安全,不能在程序的运行过程中随便改变。基类的实现发生了改变,派生类的实现也不得不改变;从基类继承而来的派生类是固定的,不能在运行时发生改变,因此没有足够的灵活性。
组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
核心思想:组合优于继承