设计模式之七大原则
1、单一职责原则
将不同的职责封装到不同的类或模块中,降低耦合性、增强内聚性
一个类应该只有一个职责(不代表只有一个方法), 如果一个类承担的职责过多,就等于把这些职责耦合在一起了。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。所以要尽可能的遵守单一职责原则。
Cast1:

1 public class Cast1 { 2 public static void main(String[] args) { 3 Vehicle vehicle = new Vehicle(); 4 vehicle.fun("摩托车"); 5 vehicle.fun("汽车"); 6 vehicle.fun("飞机"); 7 } 8 } 9 class Vehicle { 10 public void fun(String name) { 11 System.out.println(name + "在路上跑"); 12 } 13 }
fun 方法违反了单一职责原则
解决方案:根据交通工具功能不同,分解成不同类
Cast2:

1 public class Cast2 { 2 public static void main(String[] args) { 3 WaterVehicle vehicle1 = new WaterVehicle(); 4 vehicle1.fun("轮船"); 5 6 LandVehicle vehicle2 = new LandVehicle(); 7 vehicle2.fun("汽车"); 8 9 AirVehicle vehicle3 = new AirVehicle(); 10 vehicle3.fun("飞机"); 11 } 12 } 13 14 class WaterVehicle { 15 void fun(String name) { 16 System.out.println(name + "在水上游"); 17 } 18 } 19 20 class LandVehicle { 21 void fun(String name) { 22 System.out.println(name + "在路上跑"); 23 } 24 } 25 26 class AirVehicle { 27 void fun(String name) { 28 System.out.println(name + "在天上飞"); 29 } 30 }
遵守单一职责原则
但是这样做的改动很大,即将类分解,同时修改客户端
解决方案:直接修改 Vehicle 类,改动的代码会比较少
Cast3:

1 public class Cast3 { 2 public static void main(String[] args) { 3 Vehicles v = new Vehicles(); 4 v.funInWater("轮船"); 5 v.funInLand("汽车"); 6 v.funInAir("飞机"); 7 } 8 } 9 10 class Vehicles { 11 public void funInWater(String name) { 12 System.out.println(name + "在水上游"); 13 } 14 15 public void funInLand(String name) { 16 System.out.println(name + "在路上跑"); 17 } 18 19 public void funInAir(String name) { 20 System.out.println(name + "在天上飞"); 21 } 22 }
Cast3 没有对原来的类做大的修改,只是增加方法
虽然没有在类级别上遵守单一职责原则,但是在方法级别上,仍然遵守
单一职责不代表一个类只能有一个方法
总结:
1、降低类的复杂度,一个类只负责一项职责。.
2、提高类的可读性,可维护性
3、降低变更引起的风险
4、通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,可以在方法级别保持单一职责原则
2、接口隔离原则
使用多个隔离的接口,比使用单个接口要好,这样可以降低类之间的耦合度。
由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,强调降低依赖,降低耦合。
Cast1:
A类 通过 接口Inter 依赖 InterImplA,但只使用到 Inter的IFun1、2、3
B类 通过 接口Inter 依赖 InterImplB,但只使用到 Inter的IFun1、4、5

1 public class Cast1 { 2 public static void main(String[] args) { 3 A a = new A(); 4 a.depend1(new InterImplA()); 5 a.depend2(new InterImplA()); 6 a.depend3(new InterImplA()); 7 System.out.println(); 8 B b = new B(); 9 b.depend1(new InterImplB()); 10 b.depend4(new InterImplB()); 11 b.depend5(new InterImplB()); 12 } 13 } 14 15 interface Inter { 16 void IFun1(); 17 void IFun2(); 18 void IFun3(); 19 void IFun4(); 20 void IFun5(); 21 } 22 23 class InterImplA implements Inter { 24 public void IFun1() { 25 System.out.println("A Inter.IFun1"); 26 } 27 public void IFun2() { 28 System.out.println("A Inter.IFun2"); 29 } 30 public void IFun3() { 31 System.out.println("A Inter.IFun3"); 32 } 33 public void IFun4() { 34 System.out.println("A Inter.IFun4"); 35 } 36 public void IFun5() { 37 System.out.println("A Inter.IFun5"); 38 } 39 } 40 41 class InterImplB implements Inter { 42 public void IFun1() { 43 System.out.println("B Inter.IFun1"); 44 } 45 public void IFun2() { 46 System.out.println("B Inter.IFun2"); 47 } 48 public void IFun3() { 49 System.out.println("B Inter.IFun3"); 50 } 51 public void IFun4() { 52 System.out.println("B Inter.IFun4"); 53 } 54 public void IFun5() { 55 System.out.println("B Inter.IFun5"); 56 } 57 } 58 59 // A类通过接口依赖InterImplA类 60 class A { 61 void depend1(Inter i) { i.IFun1(); } 62 void depend2(Inter i) { i.IFun2(); } 63 void depend3(Inter i) { i.IFun3(); } 64 } 65 66 // B类通过接口依赖InterImplB类 67 class B { 68 void depend1(Inter i) { i.IFun1(); } 69 void depend4(Inter i) { i.IFun4(); } 70 void depend5(Inter i) { i.IFun5(); } 71 }
接口Inter对于A类、B类来说不是最小接口,InterImplA类、InterImplB类不需要实现接口的全部方法
按照接口隔离原则,可将接口Inter拆分为几个独立接口,类A、B分别与他们需要的接口建立依赖关系

1 public class Cast2 { 2 public static void main(String[] args) { 3 A a = new A(); 4 a.depend1(new InterImplA()); 5 a.depend2(new InterImplA()); 6 a.depend3(new InterImplA()); 7 System.out.println(); 8 B b = new B(); 9 b.depend1(new InterImplB()); 10 b.depend4(new InterImplB()); 11 b.depend5(new InterImplB()); 12 } 13 } 14 15 interface Inter1 { 16 void IFun1(); 17 } 18 interface Inter2 { 19 void IFun2(); 20 void IFun3(); 21 } 22 interface Inter3 { 23 void IFun4(); 24 void IFun5(); 25 } 26 27 28 class InterImplA implements Inter1, Inter2 { 29 public void IFun1() { 30 System.out.println("A Inter.IFun1"); 31 } 32 public void IFun2() { 33 System.out.println("A Inter.IFun2"); 34 } 35 public void IFun3() { 36 System.out.println("A Inter.IFun3"); 37 } 38 } 39 40 class InterImplB implements Inter1, Inter3 { 41 public void IFun1() { 42 System.out.println("B Inter.IFun1"); 43 } 44 public void IFun4() { 45 System.out.println("B Inter.IFun4"); 46 } 47 public void IFun5() { 48 System.out.println("B Inter.IFun5"); 49 } 50 } 51 52 // A类通过接口依赖InterImplA类 53 class A { 54 void depend1(Inter1 i) { i.IFun1(); } 55 void depend2(Inter2 i) { i.IFun2(); } 56 void depend3(Inter2 i) { i.IFun3(); } 57 } 58 59 // B类通过接口依赖InterImplB类 60 class B { 61 void depend1(Inter1 i) { i.IFun1(); } 62 void depend4(Inter3 i) { i.IFun4(); } 63 void depend5(Inter3 i) { i.IFun5(); } 64 }
3、依赖倒置原则
1、高层模块不应该依赖低层模块,二者都应该依赖其抽象
2、抽象不应该依赖细节,细节应该依赖抽象
3、依赖倒置的中心思想是面向接口编程
设计理念:
相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。(在java中,抽象指的是接口或抽象类,细节就是具体的实现类)
使用接口或者抽象类的目的是制定规范,而不涉及任何具体的操作,把具体的任务交给实现类完成
Cast1:
假设一个司机有一辆奔驰车,并且可以开

1 public interface DependInversion { 2 public static void main(String[] args) { 3 new Driver().drive(new Benz()); 4 } 5 } 6 7 class Benz { 8 public void run() { 9 System.out.println("开奔驰车"); 10 } 11 } 12 13 class Driver { 14 public void drive(Benz benz) { 15 benz.run(); 16 } 17 }
若干年后,司机买了一台宝马车

1 class BMW { 2 public void run() { 3 System.out.println("开宝马车"); 4 } 5 }
但是司机类的drive()方法只能开奔驰,最直接的办法就是添加driveBMW()方法

1 class Driver { 2 public void driveBenz(Benz benz) { 3 benz.run(); 4 } 5 public void driveBMW(BMW bmw) { 6 bmw.run(); 7 } 8 }
如果司机有几十台车,就要在Driver类中添加对应的方法,程序的耦合性过大
解决办法:
1、接口声明依赖:
定义两个接口表示 车 和 司机 的抽象

1 interface ICar { 2 public void run(); 3 } 4 5 interface IDriver { 6 public void drive(ICar iCar); 7 }
底层模块和高层模块分别依赖抽象

1 class Benz implements ICar { 2 public void run() { 3 System.out.println("开奔驰车"); 4 } 5 } 6 7 class BMW implements ICar { 8 public void run() { 9 System.out.println("开宝马车"); 10 } 11 } 12 13 class DriverImpl implements IDriver { 14 public void drive(ICar iCar) { 15 iCar.run(); 16 } 17 }
当需求变化时只需要创建ICar接口的实现类即可,保证程序的扩展性
2、构造函数传递依赖

1 interface IDriver { 2 public void drive(); 3 } 4 5 class DriverImpl implements IDriver { 6 private ICar iCar; 7 8 public DriverImpl(ICar iCar) { 9 this.iCar = iCar; 10 } 11 12 public void drive() { 13 this.iCar.run(); 14 } 15 }
3、setter方法传递依赖

1 interface IDriver { 2 public void setCar(ICar iCar); 3 public void drive(); 4 } 5 6 class DriverImpl implements IDriver { 7 private ICar iCar; 8 9 public void setCar(ICar iCar) { 10 this.iCar = iCar; 11 } 12 13 public void drive() { 14 this.iCar.run(); 15 } 16 }
4、里氏替换原则
如果对每个类型为T1的对象ol,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
通俗的理解,子类不要随便重写从父类继承来的方法。里氏替换原则所追求的是任何使用父类实例的地方都能使用子类实例替代。(子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法;子类中可以增加自己特有的方法。)
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误,在适当情况下,可以通过聚合、组合、依赖来解决问题。
Cast1:鸵鸟非鸟
定义鸟类,有飞行速度属性

1 class Bird { 2 double flySpeed; 3 4 public double getFlySpeed() { 5 return flySpeed; 6 } 7 8 public void setFlySpeed(double flySpeed) { 9 this.flySpeed = flySpeed; 10 } 11 }
鸵鸟类继承鸟类,由于鸵鸟不会飞,飞行速度设置为0

1 class Ostrich extends Bird { 2 public void setFlySpeed(double flySpeed) { 3 this.flySpeed = 0.0; 4 } 5 }
测试类 测试鸟内飞行时间

1 class TestFlyTime { 2 double distance = 1000.0; 3 4 void calcFlyTime(Bird bird) { 5 System.out.println(bird.getClass().getSimpleName()); 6 System.out.println("Fly time = " + distance / bird.getFlySpeed() + " s"); 7 } 8 }
Main方法

1 public static void main(String[] args) { 2 Bird bird = new Bird(); 3 bird.setFlySpeed(100); 4 Ostrich ostrich = new Ostrich(); 5 ostrich.setFlySpeed(0); 6 7 TestFlyTime t = new TestFlyTime(); 8 t.calcFlyTime(bird); 9 t.calcFlyTime(ostrich); 10 11 }
解决方案:
可以让鸟类和鸵鸟类继承于更基础的类如动物类
结论:在TestFlyTime类中 Bird 类型的参数不能被 Ostrich类型代替,所以鸵鸟类不能继承鸟类
面向对象的设计关注的是对象的行为
,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。
“Is-A”
关系,实际上指的是行为上的“Is-A”
关系,可以把它描述为“Act-As”

public class Cast2 { public static void main(String[] args) { System.out.println("父类运行结果:"); HashMap<Object,Object> map = new HashMap<>(); Father father = new Father(); father.fun(map); System.out.println("子类覆盖父方法后的结果"); Son son = new Son(); son.fun(map); } } class Father { public void fun(HashMap map) { System.out.println("Father.fun()"); } } class Son extends Father { public void fun(Map map) { System.out.println("Son.fun()"); } }
由于子类方法的参数范围比父类大,和预期结果不同
(其实加上@Override注解会发现编译通不过)
将子类方法的参数范围改成和父类相同时达到预期效果

1 class Son extends Father { 2 public void fun(HashMap map) { 3 System.out.println("Son.fun()"); 4 } 5 }
Cast3:
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格(或相等)。
否则编译不通过
5、开闭原则
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
即应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。
开闭原则的重要性:
1、开闭原则对测试的影响
开闭原则可是保持原有的测试代码仍然能够正常运行,我们只需要对扩展的代码进行测试就可以了。
2、开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。
3、开闭原则可以提高可维护性
如何使用开闭原则:
1、抽象约束
- 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
- 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
- 抽象层尽量保持稳定,一旦确定即不允许修改。
2、元数据(metadata)控制模块行为
元数据就是用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
Spring容器就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control)
3、制定项目章程
在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。
4.封装变化
- 将相同的变化封装到一个接口或者抽象类中;
- 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
(上述依赖倒置原则的代码案例就符合开闭原则)
6、迪米特法则
1、一个对象应该对其他对象保持最少的了解
2、类与类关系越密切,耦合度越大
3、迪米特法则又叫最少知道原则,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public方法,不对外泄露任何信息(高内聚)
4、迪米特法则还有个更简单的定义:只与直接朋友通信
5、直接朋友:
每个对象都会与其他对象有耦合关系,只要两个对象只有有耦合关系,我们就说这两个对象之间是朋友关系。
耦合的方式很多,依赖,关联,组合,聚合等。
其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,
而出现在局部变量中的类不是直接的朋友。陌生的类最好不要以局部变量的形式出现在类的内部。
Cast1:
领导询问员工某产品的ID列表

1 public class Cast1 { 2 public static void main(String[] args) { 3 Leader leader = new Leader(); 4 Employee employee = new Employee(); 5 leader.printAllProduct(employee); 6 } 7 } 8 9 class Product { 10 private String id; 11 12 public void setId(String id) { 13 this.id = id; 14 } 15 16 public String getId() { 17 return id; 18 } 19 } 20 21 class Employee { 22 public List<Product> getAllProduct() { 23 List<Product> productList = new ArrayList<>(); 24 for (int i = 0; i < 3; i++) { 25 Product p = new Product(); 26 p.setId(String.format("%03d", i)); 27 productList.add(p); 28 } 29 return productList; 30 } 31 } 32 33 class Leader { 34 35 public void printAllProduct(Employee employee) { 36 List<Product> productList = employee.getAllProduct(); 37 System.out.println("-- 产品ID --"); 38 productList.forEach(p -> System.out.println("id = " + p.getId())); 39 } 40 }
领导类和产品类不是直接朋友,违背了迪拉米特原则
为了避免非直接朋友关系的耦合,对代码进行改进
解决方案:

1 class Employee { 2 public List<Product> getAllProduct() { 3 List<Product> productList = new ArrayList<>(); 4 for (int i = 0; i < 3; i++) { 5 Product p = new Product(); 6 p.setId(String.format("%03d", i)); 7 productList.add(p); 8 } 9 return productList; 10 } 11 public void printAllProduct() { 12 List<Product> productList = getAllProduct(); 13 System.out.println("-- 产品ID --"); 14 productList.forEach(p -> System.out.println("id = " + p.getId())); 15 } 16 } 17 18 class Leader { 19 20 public void printAllProduct(Employee employee) { 21 employee.printAllProduct(); 22 } 23 }
迪拉米特原则的核心是降低类之间的耦合,每个类都减少了不必要的依赖,但只是降低了对象间的耦合关系,并不是完全没有依赖
7、合成复用原则
软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
如果我们只是想让B类使用A类的方法,使用继承会增强B、A之间的耦合性
解决方案:
找出应用中可能需要变化之处,把他们独立出来,面向接口编程,而不是针对实现编程
B 依赖 A(B ..> A)
A1、A2 聚合成 B(A --o B)
聚合:has-a关系,B类包含A类,A类可以独立于B类存在(B生命周期结束时,A的生命周期不一定结束),如班级和学生
A1、A2 组合成 B
组合:part-of关系,A类不能独立于B类存在,B类生命周期结束时,作为B的属性A对象也会消亡,如身体和细胞
PS:在实际开发当中不能为了遵守原则而遵守,在适当的场景中可能会打破原则(如简单工厂模式不符合开闭原则、透明组合模式就不符合接口隔离原则),要灵活变通
参考资料:
https://www.bilibili.com/video/BV1G4411c7N4?p=1 ——尚硅谷 韩顺平
https://www.runoob.com/design-pattern/design-pattern-tutorial.html ——菜鸟教程
https://zhuanlan.zhihu.com/p/120119210 ——cenmmy
https://www.jianshu.com/p/b8ddbe107126 ——GabrielPanda