SOLID设计原则
一、SOLID设计原则简介
设计模式的六大原则有:
- S:单一职责原则(Single-responsibility principle)
- O:开放封闭原则(Open-closed principle)
- L:里氏替换原则(Liskov substitution principle))+迪米特法则(Law of Demeter)
- I:接口隔离原则(Interface segregation principle
- D:依赖倒置原则(Dependency inversion principle,DIP)
六个原则结合使用的好处:建立稳定、灵活、健壮的设计
二、单一职责原则
单一个类应该只有一个发生变化的原因
2.1 用抽象构建架构,用实现扩展细节
单一职责原则简称 SRP ,顾名思义,就是一个类只负责一个职责。那这个原则有什么用呢,它让类的职责更单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分得很清楚,那么代码维护起来也更加容易。
当然,这个原则不仅仅适用于类,对于接口和方法也适用,即一个接口/方法,只负责一件事,这样的话,接口就会变得简单,方法中的代码也会更少,易读,便于维护。
2.2 单一职责原则的好处
- 代码的粒度降低了,类的复杂度降低了。
- 可读性提高了,每个类的职责都很明确,可读性自然更好。
- 可维护性提高了,可读性提高了,一旦出现 bug ,自然更容易找到他问题所在。
- 改动代码所消耗的资源降低了,更改的风险也降低了。
三、开放封闭原则
一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
3.1 变化带来的问题
在软件的生命周期内,因为需求变更、升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
3.2 变化带来的问题
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。其实只要遵循SOLID中的另外5个原则,设计出来的软件就是符合开闭原则的。
3.3 用抽象构建架构,用实现扩展细节
用抽象构建架构,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
四、里氏替换原则
所有引用基类的地方必须能透明地使用其子类的对象
在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年的一次会议上,在名为“数据的抽象与层次”的演说中首次提出。
里氏替换原则的内容可以描述为:“派生类(子类)对象可以在程序中代替其基类(父类)对象。”
也就是说,程序中的对象不管出现在什么地方,都应该可以使用其派生类(子类)的对象进行替换,而不影响程序运行的正确性。
4.1 里氏替换原则对继承进行了规则上的约束
里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比- 父类方法的输入参数更宽松。(即只能重载不能重写)
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
4.2 代码示例
我们设计一个用例,例如一个超市有两种类型商品:存在折扣和没有折扣的商品。
abstract class Commodity
{
/// <summary>
/// 商品名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 商品原价
/// </summary>
public double Prince { get; set; }
/// <summary>
/// 计算商品优惠金额
/// </summary>
/// <returns></returns>
public abstract double Discount();
}
class DiscountCommodity: Commodity
{
public override double Discount()
{
return (1 - 0.95) * Prince;
}
}
class UnDiscountCommodity: Commodity
{
public override double Discount()
{
throw new NotImplementedException("不存在折扣");
}
}
接下来在 Main
方法中调用它们。
static void Main(string[] args)
{
Commodity e;
e = new DiscountCommodity() { Name = "牙膏", Prince = 15,};
Console.WriteLine($"{e.Name} 的折扣是 {e.Discount()} 元");
e = new UnDiscountCommodity() { Name = "巧克力", Prince = 22,};
Console.WriteLine($"{e.Name} 的折扣是 {e.Discount()} 元");
Console.ReadKey();
}
运行一下可以观察到(显而易见的),当使用 DiscountCommodity 类创建的对象替换基类型 Commodity 的变量 e
时,调用 Discount()
方法可以正常运行,但是使用 UnDiscountCommodity 类创建的对象替换变量 e
时,调用 Discount()
方法抛出了异常,导致程序无法正常运行。这就明显违反了里氏替换原则。
那么,应该如何改进一下呢?
interface ICommodity
{
/// <summary>
/// 计算商品优惠金额
/// </summary>
/// <returns></returns>
public abstract double Discount();
}
abstract class Commodity
{
/// <summary>
/// 商品名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 商品原价
/// </summary>
public double Prince { get; set; }
}
class DiscountCommodity: Commodity, ICommodity
{
public double Discount()
{
return (1 - 0.95) * Prince;
}
}
class UnDiscountCommodity: Commodity
{
}
在 Main
方法中,将调用它们的测试代码改为:
static void Main(string[] args)
{
Commodity e;
ICommodity ie;
var p = new DiscountCommodity() { Name = "牙膏", Prince = 15, };
e = p;
ie = p;
Console.WriteLine($"{e.Name} 的折扣是 {ie.Discount()} 元");
e = new UnDiscountCommodity() { Name = "巧克力", Prince = 22,};
Console.WriteLine($"{e.Name} 是无折扣商品,不存在折扣");
Console.ReadKey();
}
五、迪米特法则
只与你的直接朋友交谈,不跟“陌生人”说话
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。例如一个中介,客户只要找中介要满足的楼盘 ,而不必跟每个楼盘发生联系。无服务中的网关,前端都请求到网关,而不是直接请求具体的微服务。
5.1 迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
5.2 掌握使用迪米特法则的平衡
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
5.3 迪米特法则的实现方法
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
六、接口隔离原则
1、客户端不应该依赖它不需要的接口;2、类间的依赖关系应该建立在最小的接口上。
以上两个定义的含义是:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
6.1 接口隔离原则和单一职责的区别
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
6.2 接口隔离原则的优点
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
6.3 接口隔离原则的实现方法
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
- 根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
七、依赖倒置原则
1、上层模块不应该依赖底层模块,它们都应该依赖于抽象;2、抽象不应该依赖于细节,细节应该依赖于抽象。
首先,这个原则听起来很像是“针对接口编程,不针对现实编程”,不是吗?的确很相似,然而这里更强调“抽象”。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。
使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。
7.1 依赖、倒置原则的作用
依赖倒置原则的主要作用如下。
- 依赖倒置原则可以降低类间的耦合性。
- 依赖倒置原则可以提高系统的稳定性。
- 依赖倒置原则可以减少并行开发引起的风险。
- 依赖倒置原则可以提高代码的可读性和可维护性。
7.2 依赖倒置原则的实现方法
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。