设计模式(一)概述和六大设计原则
概述
设计模式通常用于软件开发过程中,它们提供了软件开发过程中面临的一般问题的最佳解决方案。
设计模式的6大原则
1 单一职责原则
(Single Responsibility Principle,简称SRP)
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,否则就应该把类拆分。
2 接口隔离原则
Interface Segregation Principle,简称ISP;类间的依赖关系应该建立在最小的接口上,不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
- 核心思想:类间的依赖关系应该建立在最小的接口上
- 通俗来讲: 建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
- 问题描述: 类A通过接口interface依赖类B,类C通过接口interface依赖类D,如果接口interface对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
- 需注意:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性,但是如果过小,则会造成接口数量过多,使设计复杂化。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情所以一定要适度提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
- 为依赖接口的类定制服务。只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
3 开放封闭原则(总原则)
Open Close Principle,简称OCP;尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化;
对扩展开放,对修改封闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等。
- 核心思想: 尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
- 通俗来讲: 一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
4 里氏替换
(Liskov Substitution Principle,简称LSP): 子类可以替换父类。
继承可以提高代码的重用性(子类拥有父类的方法和属性)和可扩展性,但是增加了耦合(一旦父类有了变动,可能后果造成非常糟糕,要重构大量的代码)。为解决这个问题,引入了LSP:
a.子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
b.子类中可以增加自己特有的方法。
c.当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
public class LSP { class A { public void fun(HashMap map){ System.out.println("父类被执行..."); } } class B extends A{ public void fun(Map map){ System.out.println("子类被执行..."); } } public static void main(String[] args){ System.out.print("父类的运行结果:"); LSP lsp =new LSP(); LSP.A a= lsp.new A(); HashMap<Object, Object> map=new HashMap<Object, Object>(); a.fun(map); //父类存在的地方,可以用子类替代 //子类B替代父类A System.out.print("子类替代父类后的运行结果:"); LSP.B b=lsp.new B(); b.fun(map); } } /* 运行结果: 父类的运行结果:父类被执行... 子类替代父类后的运行结果:父类被执行... 符合条件 我们应当注意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。 子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行父类的重载方法。这符合里氏替换原则 */
//将子类方法的参数范围缩小会怎样? import java.util.Map; public class A { public void fun(Map map){ System.out.println("父类被执行..."); } } import java.util.HashMap; public class B extends A{ public void fun(HashMap map){ System.out.println("子类被执行..."); } } import java.util.HashMap; public class demo { static void main(String[] args){ System.out.print("父类的运行结果:"); A a=new A(); HashMap map=new HashMap(); a.fun(map); //父类存在的地方,都可以用子类替代 //子类B替代父类A System.out.print("子类替代父类后的运行结果:"); B b=new B(); b.fun(map); } } /* 运行结果: 父类的运行结果:父类被执行... 子类替代父类后的运行结果:子类被执行... 在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。 所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。不符合里式替换*/
不符合原则c的例子
d.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
public class LSP1 { abstract class A { public abstract Map fun(); } class B extends A{ @Override public HashMap fun(){ HashMap b=new HashMap(); b.put("b","子类被执行..."); return b; } } public static void main(String[] args){ LSP1 lsp =new LSP1(); LSP1.A a=lsp.new B(); System.out.println(a.fun()); } } /* 运行结果: {b=子类被执行...}*/
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?
后果就是:你写的代码出问题的几率将会大大增加。
5 依赖倒置原则
Dependence Inversion Principle,简称DIP
面向接口编程,多态(接口类或者抽象类
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖实现类;实现类应该依赖抽象。
对象的依赖关系可以通过三种方法来实现:接口注入、构造函数注入、setter方法注入,这里只说最常用的接口注入
package UML; /*在接口处就声明了依赖的对象。司机接口IDriver,其方法drive()的形参是ICar类型的,那么我们可以说IDrive与ICar发生了依赖关系,Dazhong,baoma依赖ICar注入,依赖倒置了。接口声明依赖的方法也叫接口注入。*/ //车子接口 interface ICar { public void run(); } class DaZhong implements ICar{ public void run(){ System.out.println("开大众汽车"); } } //宝马车类 class BaoMa implements ICar{ public void run(){ System.out.println("开宝马车"); } } //司机接口 interface IDriver { //接口声明依赖对象,接口注入ICar,这里变成了car依赖ICar public void drive(ICar car); } //司机类 class Driver implements IDriver{ //依赖接口 public void drive(ICar car){ car.run(); } } public class Client { public static void main(String[] args){ IDriver Tom=new Driver(); ICar daZhong=new DaZhong(); Tom.drive(daZhong);//Tom开大众汽车 ICar baoMa=new BaoMa(); Tom.drive(baoMa);//Tom开宝马 } }
1.接口声明依赖对象
6 迪米特法则
Law of Demeter,简称LoD, 低耦合,一个对象应该对其他对象保持最少的了解(中介者模式就是这个的应用)
- 核心思想: 类间解耦。
- 通俗来讲: 一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。
总结:
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。
总结:对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。
图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。
在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。
(附)本文参考了以下资源:
https://www.cnblogs.com/o-andy-o/p/10299953.html
https://github.com/youlookwhat/DesignPattern
《设计模式》