设计模式概念与作用:
设计模式是一套被反复使用的、多数人知晓、经过分类编目的优秀代码设计经验的总结。特定环境下特定问题的处理方法。
1)重用设计和代码 重用设计比重用代码更有意义,自动带来代码重用
2)提高扩展性 大量使用面向接口编程,预留扩展插槽,新的功能或特性很容易加入到系统中来
3)提高灵活性 通过组合提高灵活性,可允许代码修改平稳发生,对一处修改不会波及到其他模块
4) 提高开发效率 正确使用设计模式,可以节省大量的时间
设计模式六大原则:
1.单一职责原则
2.里氏替换原则
3.依赖倒置原则
4.接口隔离原则
5.迪米特法则
6.开闭原则
里氏替换原则:
里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
里氏替换原则的作用:
- 里氏替换原则是实现开闭原则的重要方式之一
- 它克服了继承中重写父类造成的可复用性变差的缺点
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新错误,
- 强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
里氏替换原则有以下几点要求
- 子类必须实现父类的抽象方法,但不得覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法
- 当子类覆盖父类的方法时,子类方法的前置条件(即参数)要比父类方法的参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即返回值)要比父类更严格。
案例说明:
案例1:子类必须实现父类的抽象方法,但不得重写父类的非抽象方法。如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
package com.design.principle.demo02;
public class ParentClazz {
public void output(){
System.out.println("我是ParentClazz父类的方法,继承我就要调用我");
}
}
package com.design.principle.demo02;
public class ChildClazz extends ParentClazz {
@Override
public void output(){
System.out.println("这是ChildClazz子类的方法,我重写了父类方法");
}
}
package com.design.principle.demo02;
public class Test {
public static void main(String[] args) {
ChildClazz child = new ChildClazz();
child.output();
}
}
执行结果
这是ChildClazz子类的方法,我重写了父类方法
结果:ChildClazz继承ParentClazz的本意就是可以使用ParentClazz的output方法,但是我们重写了父类的方法,结果执行了子类的方法。这违背了里氏替换原则, 造成调用方法错误。使得系统继承体系混乱。
案例二:子类中可以增加自己特有的方法。当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,
package com.design.principle.demo02;
public class ParentClazz {
public void output(){
System.out.println("我是ParentClazz父类的方法,继承我就要调用我");
}
}
package com.design.principle.demo02;
public class ChildClazz extends ParentClazz {
public void newOutput(){
System.out.println("这是ChildClazz子类的方法,我增加了新的功能");
}
}
package com.design.principle.demo02;
public class Test {
public static void main(String[] args) {
ChildClazz child = new ChildClazz();
child.output();
child.newOutput();
}
}
执行结果
我是ParentClazz父类的方法,继承我就要调用我
这是ChildClazz子类的方法,我增加了新的功能
这样是符合里氏替换原则的。这样ChildClazz既可以继承了父类的功能,也增加了自己的功能拓展。
案例三:当子类重载父类的方法时,子类方法的参数要比父类方法的参数更宽松。
package com.design.principle.demo02;
import java.util.ArrayList;
public class ParentClazz {
public void fun(ArrayList list){
System.out.println("我是父类方法fun,我的参数是ArrayList");
}
}
package com.design.principle.demo02;
import java.util.List;
public class ChildClazz extends ParentClazz {
public void fun(List list){
System.out.println("我是子类方法fun,我的参数是List");
}
}
package com.design.principle.demo02;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ChildClazz child = new ChildClazz();
ArrayList list = new ArrayList();
child.fun(list);
}
}
执行结果
我是父类方法fun,我的参数是ArrayList
ChildClazz类继承了ParentClazz类,ParentClazz的方法fun的参数是ArrayList, 而子类ChildClazz的fun方法参数是List,作为参数List比ArrayList更宽松,子类调用父类的方法执行的还是父类的方法,这是符合里氏替换原则。
如果我们修改调用方法的参数
package com.design.principle.demo02;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
ChildClazz child = new ChildClazz();
List list = new ArrayList();
child.fun(list);
}
}
执行结果
我是子类方法fun,我的参数是List
子类在没有重写父类的方法的前提下,子类方法被执行了,这个会引起以后的业务逻辑混乱。所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。
案例四:当子类的方法实现父类的抽象方法时,方法的后置条件(即返回值)要比父类更严格。
package com.design.principle.demo02;
import java.util.ArrayList;
import java.util.Map;
public abstract class ParentClazz {
public abstract Map myFun();
}
package com.design.principle.demo02;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ChildClazz extends ParentClazz {
@Override
public Map myFun() {
HashMap map = new HashMap();
System.out.println("这是ChildClazz子类的myFun方法,我重写了ParentClazz抽象方法");
return map;
}
}
package com.design.principle.demo02;
public class Test {
public static void main(String[] args) {
ChildClazz child = new ChildClazz();
child.myFun();
}
}
执行结果
这是ChildClazz子类的myFun方法,我重写了ParentClazz抽象方法
该案例父类ParentClazz类的myFun方法返回值为Map,子类ChildClazz重写方法myFun的返回值为HashMap,执行结果也是正常的。这符合里氏替换原则。
如果我们把父类ParentClazz类的myFun方法返回值为HashMap,子类ChildClazz重写方法myFun的返回值为Map,这样是编译不能通过的。
总结:
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
里氏替换原则的目的就是增强程序健壮性,版本升级时也可以保持非常好的兼容性。
为了使用代码更加健壮,降低程序的出错概率。我们一定要遵循里氏替换原则。