概念
在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,
指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得
更加可能。SOLID所包含的原则是通过引发编程者进行软件源代码的代码重构进行软件的代码异味清扫,从而使得软件清晰可读以及可扩展时可以应用
的指南。SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。
首字母 指代 概念 S 单一功能原则 认为对象应该仅具有一种单一功能的概念 O 开闭原则 认为软件体应该是对于扩展开放的,但是对于修改封闭的概念 L 里氏替换原则 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”概念 I 接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口” D 依赖反转原则 认为一个方法应该遵从“依赖于抽象而不是一个实例”的概念
依赖注入所以该原则的一种实现方式
以上源自Wiki百科:SOLID(面向对象设计)
在我们的实际工作中,这五个原则互相关联和支持,可能实现一个业务功能把五个原则都用到了,也可能只用到了其中的三个
单一功能原则(S)
按照字面意思不难理解,一个对象有且仅有一种功能,同时也仅有一种功能需求的变更能够引发该对象实现的修改。
- 试想以下场景,如果我们有一个对象,里面大而全的实现了某业务领域内的全部功能,那么,当该领域内的需求发生变更时,我们是不是要对该对象内的代码进行修改?
- 再试想一下,如果我们将该业务领域内的需求进行细分,针对细分后的功能实现对应的功能(业务)对象,那么当该领域内的需求发生变更时,我们仅仅需要修改有变更需求对应的功能(业务)对象就可以?
可能有人会说,可是两种方式都是修改代码了呀?!是的,两种方式都对我们的实现进行了代码修改,这是不可避免的。
但换个角度想,此时我们修改的代码,对其他需求没有变更的功能(业务)对象来说,是不是已经没有了太大的关联?
因为实现了“单一功能原则(S)”,顺带也满足了“接口隔离(I)”原则,当然严格来说是面向接口,但面向实现也未尝不可。而此时,我们对特定业务/功能的修改不会影响到该领域内的其他实现,是不是也捎带手实现了“开闭原则(O)”?
其实,在开发过程中,有一个百试不爽的窍门就是“拆”,大拆小,直到不能再拆(原子级),遇到复杂的业务如是,遇到复杂的功能同样适用。不但有利于实现,同时也有利于测试。
延伸一下思路,无论是当下流行的微服务,还是一个方法只完成一个功能的普遍认知,是不是和单一功能原则有着相似的理念?
理论讲完,来点具体的代码实现验证一下
// C# 示例
// 做饭接口
public interface ICooking
{
//做饭接口方法
bool Cook();
}
//做中餐
public class ChineseCook: ICooking
{
public bool Cook()
{
Console.WriteLine("开始做鱼香肉丝")
Console.WriteLine("鱼香肉丝做好了,快来米西吧")
}
}
//做西餐
public class WesternCook: ICooking
{
public bool Cook()
{
Console.WriteLine("开始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快来eat吧")
}
}
例子写的可能不太恰当,但足够说明吃货脑子里不想别的。
如果哪天鱼香肉丝吃腻味了换成宫保鸡丁,是不是就只要修改ChineseCook的Cook实现就可以了?
又或者说如果哪天新增加了墨西哥菜,泰国菜,日本料理等,是不是只需要实现ICooking接口就可以了?
可能这个例子还不能对“拆”这个行为提供足够的支持,因为好处不够明显,但别忘了五个原则是互相关联和支持的,个原则是互相关联和支持的,原则是互相关联和支持的,是互相关联和支持的,互相关联和支持的,相关联和支持的,关联和支持的,联和支持的,相和支持的,支持的,持的,的...
上面的示例中,实现了单一功能原则(S)的同时,也实现了开闭原则(O),接口隔离原则(I)。
依赖反转原则(D)
请看如下代码示例:
// 厨师 - 依赖反转示例
public class Cooker
{
//这里只需要ICooking接口对象,不关心接口实现
public void Cook(ICooking cook)
{
cook.Cook();
}
}
// 食客 - 消费者
public class Diner
{
public static void main(string[] args)
{
Cooker cooker = new Cooker();
ICooking chineseCook = new ChineseCook(); //做中餐实例
ICooking westernCook = new WesternCook(); //做西餐实例
// 或许哪天增加日本料理
// ICooking japaneseCook = new JapaneseCook(); //日本料理实例
cooker.Cook(chineseCook); // 做中餐
cooker.Cook(westernCook); // 做西餐
cooker.Cook(japaneseCook); // 日本料理
}
}
代码说明:
以上示例,在ICooking接口的两个实现的基础上,实现了依赖反转/倒置(DIP)。
我们来看一下定义:
- 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
- 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口
在此示例中:
- 高层次的模块 Cooker 不依赖于 ChineseCook 或 WesternCook, 而是依赖于 ICooking 接口
- ICooking接口不依赖于具体的实现。而 ChineseCook 和 WesternCook 则依赖于 ICooking 接口
- 如果新增加了墨西哥菜,日本料理,只需要实现接口ICooking即可(开闭原则:面向修改关闭,面向扩展开放)
依赖倒置原则基于这样一个事实:
相对于实现的多变性,抽象的东西(接口)要稳定的多。以抽象为基础搭建起来的架构要比以实现为基础搭建起来的架构要稳定的多。
在不同的语言领域中,抽象和实现可能有着不同的实现。不一而论。在C#或者Java中,抽象指的是接口或者抽象类,实现则是具体的实现类。
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
出自设计模式六大原则(3):依赖倒置原则
里氏替换原则(L)
定义:
派生类(子类)对象能够替换其基类(超类)对象被使用。
出自里氏替换原则
这个概念比较好理解,结合上面的示例,如果我们把接口替换成抽象类,该抽象类实现了默认的Cook方法(默认做一份老北京鸡肉卷):
// 父类,实现了一个默认的Cook方法
public class Cooking
{
public virtual Cook()
{
Console.WriteLine("开始做老北京鸡肉卷");
Console.WriteLine("老北京鸡肉卷做完了,快来啃吧!");
}
}
//做中餐(重载Cook方法)
public class ChineseCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("开始做鱼香肉丝")
Console.WriteLine("鱼香肉丝做好了,快来米西吧")
}
}
//做西餐(重载Cook方法)
public class WesternCook: Cooking
{
public override bool Cook()
{
Console.WriteLine("开始做蔬菜沙拉")
Console.WriteLine("蔬菜沙拉做好了, 快来eat吧")
}
}
代码说明:
上例中,父类Cooking实现了Cook的默认方法(做一份老北京鸡肉卷),所以如果不继承该父类的话,服务员端上来的就是鸡肉卷,很显然,该类可以被子类替换。
本文完。