一、开闭原则 顾名思义,在软件设计中应当遵循对扩展开放,而对修改关闭。也即在实际开发过程中,当需求变动业务调整时,在不改动源码的情况下可以扩展以支撑新的功能;这也要求了在设计之初制定技术方案时应有前瞻性。遵循开闭原则的好处:提高代码的复用性、可维护性、有利于单元测试。实现:在面向对象的设计中,通常可以通过定义接口或者抽象类来约束相同属性或者一般通用的实现(抽象),这样具体派生实现类可以将具体的实现封装在内部。即使业务变化,我们只需要相应的派生出一个实现类就可以实现扩展。不过在实际中,这种对业务的抽象能力要求还是比较高的。如果抽象的粒度太小,那么会伴随着繁杂的实现类;如果粒度太大却不利于扩展。经验的积累与思考很重要。1.实际问题 商品价格变动模拟,如打折促销、涨价等定义顶层的商品接口(仅仅包含ID、名称、价格)public interface Product { long getId(); long getPrice(); String getName(); }新建水果中香蕉的实现类public class Banana implements Product { private long id; private long price; private String name; public Banana(long id, long price, String name) { this.id = id; this.price = price; this.name = name; } public void setId(long id) { this.id = id; } public void setPrice(long price) { this.price = price; } public void setName(String name) { this.name = name; } @Override public long getId() { return this.id; } @Override public long getPrice() { return this.price; } @Override public String getName() { return this.name; } }香蕉不易保存的特性决定了,如果库存较多只能打折进行处理。 如果直接修改Banana实现类中价格getPrice()势必会对其他的地方的调用产生影响,违背了开闭原则。因此增加BananaDiscountImp折扣类,当然这其实也是不合理的,仅仅作为举例,如果都是这种,会增加很多不必要的实现类,使得项目膨胀冗杂。public class BananaDiscountImp extends Banana { public BananaDiscountImp(long id, long price, String name) { super(id, price, name); } /** * 原始价格 */ @Override public long getPrice() { return super.getPrice(); } /** * 折后价格 * (需借助BigDecimal转换,包括保留小数位等,80相当于8折) */ public long getOriginalPrice() { return getPrice() * 80L; } }二、里氏替换原则含义:通俗地讲在继承过程中子类可以对基类的功能进行扩展,但不能改变基类原有的功能。在面向对象的程序设计中,继承作为三大特性之一。虽然带来了很大的便利性,但同时也增加了耦合性,侵入性。里氏替换原则实际上更是对继承过程中的一种规范与约束:1.子类可以增加自身特有的方法;2.子类可以实现基类的抽象方法,但不能覆盖基类的非抽象方法;3.当子类重载基类的方法时,方法的入参应该比基类更宽松;4.当子类实现基类的抽象方法时,方法的返回值应该比基类更严格;5.如果子类必须重写基类的方法时,应该考虑替换当前的继承关系,同时继承更加一般的基类,或者使用组合、聚合、依赖等其他方式替代。1.实际问题 比较经典的"正方形非长方形问题",另外我们知道鸵鸟是不会飞的,但是奔跑的速度很快。顶层的抽象动物类public class Animal { /** * 米每秒 */ private long moveSpeed; public long getMoveTime(long distance) { return distance / moveSpeed; } public void setMoveSpeed(long moveSpeed) { this.moveSpeed = moveSpeed; } }较为一般的鸟类public class Bird extends Animal { private long flySpeed; public void setFlySpeed(long flySpeed) { this.flySpeed = flySpeed; } public long getFlyTime(long distance) { return distance / flySpeed; } } 在定义的过程,无非就是根据一些鸟类的特性,比如有羽毛,会飞,有喙等等;但是往往会存在特例。鸵鸟除了没有飞的能力其他都是包含的,如果继承Bird类,当求导飞行速度时势必会出现错误,因为鸵鸟的飞行速度为0。具体到某一种鸟类-麻雀public class Sparrow extends Bird { @Override public void setFlySpeed(long flySpeed) { super.setFlySpeed(flySpeed); } }鸵鸟类(错误的继承) public class Ostrich extends Bird { @Override public void setFlySpeed(long flySpeed) { //鸵鸟的飞行速度为零,重写了 flySpeed = 0; super.setFlySpeed(flySpeed); } } 当测试时,肯定是会出现系统异常的情况,这里违背了里氏替换的原则-不能覆盖基类的非抽象方法;从而导致了错误的结果,此时应该考虑取消继承关系,改为更加通用的基类,也即继承Animal,动物都有移动的速度。鸵鸟类继承Animalpublic class Ostrich extends Animal { public Ostrich() {} @Override public void setMoveSpeed(long moveSpeed) { super.setMoveSpeed(moveSpeed); } public static void main(String[] args) { //测试 Animal ostrich = new Ostrich(); ostrich.setMoveSpeed(90); } }实际开发的过程中应避免对滥用继承,实现子类时遵循里氏替换的原则能够帮助我们对子类更好地约束,建立起更健壮、易维护的系统。当然不遵循程序也能跑,随着项目的复杂度增加,出现问题的概率也大大增加。三、依赖倒置原则 高层结构的模块不应该依赖低层结构的模块,二者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。1.一般含义通俗的解释,依赖倒置的核心思想-面向接口编程。面向接口编程的好处不言而喻,相对于实现细节的多变性,抽象的概念则稳定得多,很多同学包括自己在实际开发中有时候也会陷入到实现的细节中,试想以具体的实现类来构建系统自然是不够稳定的,同样不利于扩展。对于这种,首先考虑的是制定抽象的接口、抽象类层,以接口来约束和规范实现,而不关心具体的实现细节。2.作用既然都面向了接口,类与类之间的耦合度降低了(依赖倒置原则降低了类之间的耦合度)。耦合度低,提高了系统的稳定性(稳定性)。抽象的规范与约束作用,提高了代码的可维护性,可读性,当然既然存在继承,那么在设计与实现的过程中应遵循里氏替换原则(可维护性、可读性)。3.如何设计面向接口-尽量使用使用接口或者抽象类,或者两者都包含来代替类传递。对于变量的申明类型尽量使用接口或者抽象类,而不是具体的实现类。继承遵循里氏替换原则4.实际问题 以大学生学习课程为例定义课程的接口/** * Created by Sai * on: 05/01/2022 23:54. */ public interface ICourse { void selected(); }具体课程类-物理课/** * Created by Sai * on: 05/01/2022 23:58. */ public class PhysicsCourse implements ICourse { @Override public void selected() { System.out.println("物理课被选修了"); } }具体课程类-英语课/** * Created by Sai * on: 06/01/2022 00:00. */ public class EnglishCourse implements ICourse { @Override public void selected() { System.out.println("英语课被选修了"); } }学生类/** * on: 06/01/2022 00:01. * Description: */ public class Student { //依赖注入 private ICourse course; public Student() {} public ICourse getCourse() { return course; } public void setCourse(ICourse course) { this.course = course; } public void study() { if (null != course) { course.selected(); } } public static void main(String[] args) { Student stu = new Student(); stu.setCourse(new EnglishCourse()); stu.study(); stu.setCourse(new PhysicsCourse()); stu.study(); } } //英语课被选修了 //物理课被选修了 //Process finished with exit code 0前面提到依赖倒置的核心-面向接口编程,理解了面向接口编程的含义与运用,依赖倒置原则自然而然就掌握了,当然这离不开实践过程中的积累与思考。