文章目录
- 设计模式
- 七大设计原则
- 开闭原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则-最少知道原则
- 单一职责原则
- 合成复用原则
设计模式
面向对象的三个基本特征:
- 继承
- 封装
- 多态
设计模式体现了代码的耦合性、内聚性、可维护性、可扩展性、重用性、灵活性。
- 代码重用性:相同功能代码不用多次编写
- 可读性
- 可扩展性:添加新功能非常方便,可维护
- 可靠性:当我们添加新功能不影响原有的功能
- 使程序呈现高内聚、低耦合的特性
七大设计原则
开闭原则
原则:一个软件实体如类、模块、函数应该对拓展开发、对修改关闭
在程序需要拓展的时候,不能去修改原有代码,为了易于维护我们应该更多的学会使用接口和抽象类,在设计的时候尽量适应变化,提高系统的稳定性、灵活性。
场景:书店销售书籍
public class OpenClosePrinciple {interface Book{String getName();double getPrice();String getAuthor();}class NovelBook implements Book{private String name;private String author;private double price;public NovelBook(String name, String author, double price) {this.name = name;this.author = author;this.price = price;}@Overridepublic String getName() {return this.name;}@Overridepublic double getPrice() {return this.price;}@Overridepublic String getAuthor() {return this.author;}}@Testvoid client(){NovelBook novelBook = new NovelBook("斗罗大陆", "唐家三少", 55.8);System.out.println(novelBook.getName() + " " + novelBook.getAuthor() + " " + novelBook.getPrice());}}
假如我们想在双十一对书籍进行打折。
实现方案:
-
修改接口:在Book类中新增一个打折接口;但是这样做其他所有实现类都需要改动。
-
修改实现类方法:NovelBook类修改原有的getPrice()方法,实现打折逻辑,但是这样做的话违背了开闭原则,如果双十一过去了是不是还有再把代码回退??
-
实现类新增方法:NovelBook类新增getDiscountPrice()方法,这个看起来不错,但是一个类中提供两个获取价格的接口对调用者不是很友好,而且随着业务需求越来越多这个类不断添加新的方法,本来我们设计这个类的目的只是想获取书籍的基本信息,但是现在又混合了很多业务逻辑,那么这个类就违背了单一职责原则。
-
派生出一个打折类:该类继承NovelBook基类,用来专门处理打折逻辑,这样不用修改原有实现类不会对上层调用者产生影响,利用拓展实现功能,其实这种对基类已实现的方法进行覆盖违背了里氏替换原则,如果想不违背里氏替换原则只能在派生类中添加新的方法,并且在基类的方法中添加final保证不会被重写,这里我们只是讲解开闭原则,就不考虑那么多。
class DiscountNovelBook extends NovelBook{public DiscountNovelBook(String name, String author, double price) {super(name, author, price);}@Overridepublic double getPrice() {System.out.println("打折后价格");return super.getPrice() * 0.88;}}@Testvoid client(){//老的业务逻辑正常调用NovelBook novelBook = new NovelBook("斗罗大陆", "唐家三少", 55.8);System.out.println(novelBook.getName() + " " + novelBook.getAuthor() + " " + novelBook.getPrice());System.out.println("----------------");//新促销业务调用打折代码DiscountNovelBook discountNovelBook = new DiscountNovelBook("斗罗大陆", "唐家三少", 55.8);System.out.println(discountNovelBook.getName() + " " + discountNovelBook.getAuthor() + " " + discountNovelBook.getPrice());}
引用:https://www.jianshu.com/p/d36da4f136c4
里氏替换原则
原则:所有基类在的地方,都可以换成子类,程序还可以正常运行。这个原则与继承特性密切相关。
相信我们经常用到继承,那你知道继承有哪些优点?
- 子类拥有父类的所有方法和属性,从而减少创建类的工作量。
- 提高了代码的重用性。
- 提高了代码的拓展性,子类不但拥有父类的所有功能,还可以添加自己的功能。
缺点:
- 继承是有侵入性的,只要继承,就必须拥有父类的所有属性和方法。
- 降低了代码的灵活性。因为继承时,父类对子类有一种约束。
- 增强了耦合性,当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。
里氏替换原则对继承进行了规则上的约束
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的已实现的方法。
- 子类可以增加自己特有的方法。
- 当子类重载父类的方法时,方法的形参要比父类的输入参数更宽松。(只能重载不能重写)
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
约束详细解释:
-
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的已实现的方法。
首先子类必须实现父类的抽象方法,不然编译无法通过;
如果重写父类的方法,当用子类替代父类后会出现意想不到的错误,如果想避免这种问题我们可以将父类不得重写的方法添加final关键字,这样在语法层面就可以避免违反里氏替换原则;
-
子类可以增加自己特有的方法
子类可以对父类功能进行拓展。
-
当子类重载父类的方法时,方法的形参要比父类的输入参数更宽松
定义一个父类
Father#run(Map map)
利用子类重载run方法Son#run(HashMap map)
如果此时HashMap map = new HashMap()
作为参数传入son.run(map)
public class LiskovSubPrinciple {public class Parent {void run(Map map){System.out.println("父类执行...");};}public class Son extends Parent{void run(HashMap map){System.out.println("子类执行...");}}@Testvoid run(){HashMap<Object, Object> map = new HashMap<>();Parent parent = new Parent();parent.run(map);Son son = new Son();son.run(map);}//结果//父类执行...//子类执行... }
结果会发现用同一个参数子类和父类调用结果却不同,会出现程序调用混乱,由于子类形参为HashMap导致父类方法没有被重写的情况下调用了子类。
如果我们把子类的形参范围设置的比父类更大就不会出现上述问题。
public class LiskovSubPrinciple2 {public class Parent {void run(HashMap map){System.out.println("父类执行...");};}public class Son extends Parent{void run(Map map){System.out.println("子类执行...");}}@Testvoid run(){HashMap<Object, Object> map = new HashMap<>();Parent parent = new Parent();parent.run(map);Son son = new Son();son.run(map);}//父类执行...//父类执行... }
这次会发现两次调用结果一致。
-
当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格
如果子类的返回值类型比父类范围更广编译无法通过。
依赖倒置原则
原则:面向接口编程,不要面向实现。
场景:小明去不同的商店购物电器,但是商店有很多种
public class DependenceInversionPrinciple {public interface Shop{void sell();}public class GomeShop implements Shop{@Overridepublic void sell() {System.out.println("国美商店...");}}public class SuningShop implements Shop{@Overridepublic void sell() {System.out.println("苏宁易购...");}}public class People{String name;public People(String name) {this.name = name;}void shopping(Shop shop){System.out.print(this.name);shop.sell();}}@Testvoid testMain(){People people = new People("小明购物:");people.shopping(new GomeShop());people.shopping(new SuningShop());/*小明购物:国美商店...小明购物:苏宁易购...*/}
}
如果将方法形参的Shop接口改成实现类需要新增很多种方法不符合依赖倒置原则。
接口隔离原则
原则:使用多个隔离的接口比使用单一接口更好。
场景:现在有一个接口声明了1、2、3、4这四个方法,A类需要用到1、2方法,B类需要用到3、4方法,C类需要用到1、2、3、4方法。
public class InterfaceSegregationPrinciple {public interface I{void method1();void method2();void method3();void method4();}/*** A类需要用到方法1 2*/public class A implements I{@Overridepublic void method1() {System.out.println("实现方法1");}@Overridepublic void method2() {System.out.println("实现方法2");}@Overridepublic void method3() {//不实现}@Overridepublic void method4() {//不实现}}public class B implements I{@Overridepublic void method1() {//不实现}@Overridepublic void method2() {//不实现}@Overridepublic void method3() {System.out.println("实现方法3");}@Overridepublic void method4() {System.out.println("实现方法4");}}public class C implements I{@Overridepublic void method1() {System.out.println("实现方法1");}@Overridepublic void method2() {System.out.println("实现方法2");}@Overridepublic void method3() {System.out.println("实现方法3");}@Overridepublic void method4() {System.out.println("实现方法4");}}
}
可以看出A和B类存在很多空方法,这种方式对调用者来说不是很友好,并且接口很臃肿,不符合接口隔离原则。
让我们重新设计一下
public class InterfaceSegregationPrinciple2 {public interface I{void method1();void method2();}public interface K{void method3();void method4();}/*** A类需要用到方法1 2*/public class A implements I{@Overridepublic void method1() {System.out.println("实现方法1");}@Overridepublic void method2() {System.out.println("实现方法2");}}public class B implements K{@Overridepublic void method3() {System.out.println("实现方法3");}@Overridepublic void method4() {System.out.println("实现方法4");}}public class C implements I,K{@Overridepublic void method1() {System.out.println("实现方法1");}@Overridepublic void method2() {System.out.println("实现方法2");}@Overridepublic void method3() {System.out.println("实现方法3");}@Overridepublic void method4() {System.out.println("实现方法4");}}
}
将接口拆分为两个接口,这样每个类只需要实现方法不需要空方法,符合接口隔离原则。
迪米特法则-最少知道原则
一个对象应该对其他对象有最少的了解。
原则:只和直接的朋友交流、减少对朋友的了解
概念:
直接朋友:成员变量、方法入参、返回参数;例如A类方法参数是B类对象、返回值是C类对象则A与B、C是直接朋友。
朋友:两个对象耦合就会成为朋友;例如A类中方法代码块中使用了B类对象,AB是朋友。
只和直接的朋友交流
应用场景:
现在有三个角色老师Teacher、班长GroupLeader、学生Student,老师让班长去统计全班人数。
实现一:
public class DemeterPrinciple {private static class Teacher{/*** 发号命令* @param groupLeader*/int command(GroupLeader groupLeader){//创建学生数组Student[] students = new Student[20];for (int i = 0; i < 20; i++){students[i] = new Student(i);}//让班长统计人数return groupLeader.count(students);}}private static class GroupLeader{int count(Student[] students){return students.length;}}private static class Student{int id;public Student(int id) {this.id = id;}}public static void main(String[] args) {Teacher teacher = new Teacher();int total = teacher.command(new GroupLeader());System.out.println("人数: " + total);}
}
让我们来分析一下上面代码:
Teacher类朋友关系:GroupLeader(直接朋友)、Student(朋友)
由于Teacher类中和非直接朋友有关系,这就违背了demeter法则。方法是类中的一个行为,类竟然不知道自己的行为与其他类产生依赖关系,这严重违反了迪米特法则。
让我们做如下修改:
public class DemeterPrinciple2 {private static class Teacher{/*** 发号命令* @param groupLeader*/int command(GroupLeader groupLeader){//让班长统计人数return groupLeader.count();}}private static class GroupLeader{Student[] students;public GroupLeader(Student[] students) {this.students = students;}int count(){return students.length;}}private static class Student{int id;public Student(int id) {this.id = id;}}public static void main(String[] args) {Teacher teacher = new Teacher();//创建学生数组Student[] students = new Student[20];for (int i = 0; i < 20; i++){students[i] = new Student(i);}int total = teacher.command(new GroupLeader(students));System.out.println("人数: " + total);}
}
再看看Teacher、GroupLeader类中不依赖朋友而只有直接朋友,类之间解耦了。
减少对朋友的了解
尽量减少一个类对外暴露的方法。
场景:人使用咖啡机获得咖啡。
public class DemeterPrinciple3 {private static class People{private CoffeeMachine coffeeMachine;public People(CoffeeMachine coffeeMachine) {this.coffeeMachine = coffeeMachine;}void takeCoffee(){coffeeMachine.addCoffeeBean();coffeeMachine.addWater();coffeeMachine.makeCoffee();}}private static class CoffeeMachine{public void addCoffeeBean(){System.out.println("添加咖啡豆");}public void addWater(){System.out.println("加水");}public void makeCoffee(){System.out.println("制作咖啡");}}public static void main(String[] args) {CoffeeMachine coffeeMachine = new CoffeeMachine();People people = new People(coffeeMachine);people.takeCoffee();}}
上述代码中People类虽然只有一个CoffeeMachine类的直接朋友,但是他对这个朋友过多的了解,其实我们不用关系怎么去制作咖啡过程,结合现实中的例子拿到我们使用咖啡机需要按三个按钮才能出咖啡?如果咖啡机底层逻辑发生改变我们人还要去按更多的按钮?
让我们做下优化
public class DemeterPrinciple4 {private static class People{private CoffeeMachine coffeeMachine;public People(CoffeeMachine coffeeMachine) {this.coffeeMachine = coffeeMachine;}void takeCoffee(){coffeeMachine.makeCoffee();}}private static class CoffeeMachine{private void addCoffeeBean(){System.out.println("添加咖啡豆");}private void addWater(){System.out.println("加水");}private void doMakeCoffee(){System.out.println("制作咖啡");}public void makeCoffee(){addCoffeeBean();addWater();doMakeCoffee();}}public static void main(String[] args) {CoffeeMachine coffeeMachine = new CoffeeMachine();People people = new People(coffeeMachine);people.takeCoffee();}}
这样修改以后人不用去关系咖啡机的制作过程,只需要咖啡机提供的一个按钮就可以喝到美美的咖啡啦!
引用:https://www.jianshu.com/p/4b244f132439
单一职责原则
原则:一个类只负责一项职责,同样适用于方法
通常情况下,我们应该遵守单一职责原则,只有当逻辑足够简单,可以通过在类中添加不同方法违反单一职责原则。
public class SingleResponsibilityPrinciple {/*** 交通工具类* 逻辑简单直接在类中添加方法即可*/class Vehicle{void run(String car){System.out.println(car + " run");}void fly(String airplane){System.out.println(airplane + " fly");}}@Testvoid testRun(){Vehicle vehicle = new Vehicle();vehicle.run("奔驰");vehicle.fly("播音747");}
}
合成复用原则
原则:将已有的对象加入新对象中,作为新对象的成员对象来实现,新对象可以调用已有对象的功能,达到复用。
在java中优先使用组合而不是继承。如果要使用继承关系必须严格遵循里氏替换原则,而合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现。
复用分为:继承复用、合成复用
继承复用缺点:
- 会破坏类的封装性,继承会将父类的实现细节暴露给子类,又称“白箱复用”。
- 父类与子类耦合度高,父类发生修改会影响子类功能,不利于类的拓展与维护。
- 限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,运行时不能发生变化。
采用组合复用优点:将已经有对象作为成员对象成为新对象的一部分
- 维护了类封装性,成员对象不会暴露实现细节,这种方式称为“黑箱复用”
- 新旧类耦合度低,只需要利用成员对象接口访问。
- 复用的灵活性,这种复用可以在运行时动态进行,新对象可以动态地引用于成员对象类型相同的对象。
场景:汽车按照动力分为汽油汽车、电动汽车等,颜色有红色、黑色、白色等,如果同时考虑这两种组合我们应该怎么实现?
如果利用继承方式:
利用继承方式会出现多个子类,随着汽车种类越多子类会膨胀,并且每添加一个颜色都得继承汽油、电动这两个类都要修改源代码,如果利用组合可以避免这种问题。
引用:http://c.biancheng.net/view/1333.html