本科阶段学过设计模式,那时对设计模式的五大原则——SOLID原则的概念与理解还是比较模糊,此时过去了2年时间,在学习《高级软件工程》课程中老师又提到了设计模式,课程中还详细讨论了五大原则的过程,这次SOLID原则再回首作者提出了一些更通俗的理解吧~
单一职责原则(Single Responsibility Principle,SRP)思想:就一个类而言,应仅有一个引起它变化的原因(一个类只有一个职责);每一个引起类变化的原因就是一个职责,当类具有多职责时,应该把多余职责分离出去,分别创建类来完成.
每一个职责都是一个变化的轴线,当需求变化时会反映为类的职责的变化.
例如Modem可以链接dial(拨号连接)\hangup(挂断拨号)和send(发送数据)\recv(接收数据).
interface Modem {
public void dial(String pno);
public void hangup();
public void send(char c);
public char recv();
}
Modem类有两个职责,链接管理和数据通信,应该将它们分离.一个类中可以有多个方法,但是一个类只干一件事.
开闭原则(Open Closed Principle,OCP)思想:对功能扩展是开放的(增加新功能),但对修改原功能代码是关闭的.在进行扩展(类\模块\函数都可以扩展)时,不需要对原来的程序进行修改,即已有东西不变,可添加新功能.
它的好处是灵活可用,可加入新功能,新模块满足不断变化的新需求,由于不修改软件原来的模块,不用担心软件的稳定性.主要原则包括:
1.抽象原则
把系统的所有可能的行为抽象成一个底层,由于可从抽象层导出多个具体类来改变系统行为,因此对于可变部分系统设计对扩展是开放的.
2.可变性封装原则
对系统所有可能发生变化的部分进行评估和分类,每个可变的因素都单独进行封装.
正如《大话设计模式》中所说:“我们在做任何系统时,需求都会存在一定的变化,那么如何面对需求变化时,设计软件可以相对容易修改,而不是把整个项目推倒重来.开闭原则可以让设计面对需求变化时保持相对稳定,从而使得系统在第一个版本后不断推出新的版本.”
但是无论模块怎么封装,都会存在一些无法对之封装的变化,既然不可能完全封装,设计人员必须对设计的模块应该对哪种变化封装做出选择,他需要通过猜测最有可能发生的变化种类,然后构造抽象来隔离那些变化.通常设计人员会在发生小变化时,就及早去想办法应对发生更大变化的可能,即等到变化时立即采取行动.当变化发生时就创建抽象来隔离以后发生同类变化.
例子理解:(源自<<大话设计模式>>)
香港回归采用"一国两制"的体制就是开闭原则的代表例子,社会主义制度是不可更改,但可以增加新的制度实现共和.再如一个计算器的例子,原来客户端只有加法运算,现在需要添加新的功能减法,此时如果你去修改原来类就会违背"开放-封闭原则",而且当添加很多新的运算时,这样的代码很难维护.
此时你需要重构程序,增加一个抽象类的运算类,通过继承或多态等隔离具体加法、减法与client耦合,面对需求变化,对程序的改动是通过增加新代码进行的,而不是更改现有的代码,这就是开闭原则的精髓.
里氏替换原则(Liskov Substitution Principle,LSP)思想:继承必须确保父类所拥有的性质在子类中仍然成立,当一个子类的实例能够替换任何其父类的实例时,它们之间才具有is-a-kind-of-A关系.只有当一个子类(派生类)可以替换掉其父类(基类)而软件功能不受影响时,基类才能被复用,派生类也才能在基类的基础上增加新的行为.
其本质是在同一个继承体系中的对象应该有共同的行为特征.
换句话说,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且觉察不出父类对象和子类对象的区别,在软件中把父类都替换成它的子类,程序的行为没有变化,子类必须能够替换掉它们的父类型.
例子理解:
父类为猫,子类有黑猫和白猫,如果一个方法使用于猫如捉老鼠,则必然使用于黑猫和白猫.再如<<大话设计模式>>中的问题“企鹅是鸟吗?”
生物学:企鹅属于鸟类
LSP原则:企鹅不属于鸟类,因为企鹅不会"飞",所以企鹅不能继承鸟类
//司机类 public class Driver { //司机主要职责驾驶汽车 public void drive(Benz benz) { benz.run(); } } //奔驰类 public class Benz { public void run() { System.out.printIn("奔驰汽车跑"); } } //场景类 public class Client { public static void main(String[] args) { //eastmount开奔驰车 Driver eastmount = new Driver(); Benz benz = new Benz(); eastmount.drive(benz); } }现在司机eastmount不仅要开奔驰车,还要开宝马车,又该怎么实现呢?自定义宝马汽车类BMW,但是不能开,因为eastmount没有开宝马车的方法,出现的问题就是:
//司机接口 驾驶汽车 抽象 public interface IDriver { public void drive(ICar car); } //司机类 具体实现 public class Driver implements IDriver{ //司机的主要职责就是驾驶汽车 public void drive(ICar car){ car.run(); } } //汽车接口 public interface ICar { public void run(); } //奔驰车类 public class Benz implements ICar{ public void run(){ System.out.println("奔驰汽车跑"); } } //宝马车类 public class BMW implements ICar{ public void run(){ System.out.println("宝马汽车跑"); } } //实现eastmount开宝马车 public class Client { public static void main(String[] args) { IDriver eastmount = new Driver(); ICar bmw = new BMW(); eastmount.drive(bmw); } }由于"抽象不应该依赖细节",所以我们认为抽象(ICar接口)不依赖BMW和Benz两个实现类(细节),在高层次的模块中应用都是抽象,Client就属于高层次业务逻辑,它对于低层次模块的依赖都是建立在抽象上,eastmount都是以IDriver接口类型进行操作,屏蔽了细节对抽象的影响,现在开不同类型的车,只需要修改业务场景类即可实现:
接口隔离原则(Interface Segregation Principle,ISP)思想:多个和客户相关的接口要好于一个通用接口.如果一个类有几个使用者,与其让这个类再如所有使用这需要使用的所有方法,还不如为每个使用者创建一个特定接口,并让该类分别实现这些接口.
例子理解:(推荐&引用zhengzhb博客——接口隔离原则)
如下图所示,类A依赖接口I中方法1\方法2\方法3,类B是对类A依赖的实现,类C依赖于接口I中的方法方法1\方法4\方法5,类D是对类C依赖的实现.对于类B和类D中都存在用不到的方法,但是由于实现了接口I,所以需要实现这些不用的方法.(代码见原文链接)