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