用抽象构建架构,用实现扩展细节。
* 单一职责原则:实现类要职责单一;
* 接口隔离原则:在设计接口的时候要精简单一;
* 依赖倒转原则:面向接口编程;
* 里氏替换原则:不要破坏继承关系;
* 开闭原则原则:对扩展开发,对修改关闭;
* 迪米特法则:最少知道原则;
* 合成复用原则:先考虑组合或聚合,后考虑继承;
一、单一职责原则
1、目的
降低代码复杂度、降低系统耦合度、提高可读性
2、定义
对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。
3、具体实现
将不同的职责封装到不同的类或者模块中,当有新的需求将现有的职责分为颗粒度更小的职责的时候,应该及时对现有代码进行重构。
4、优点
(1)降低类的复杂度,一个类只负责一个职责。这样写出来的代码逻辑肯定要比负责多项职责简单得多。
(2)提高类的可读性,提高系统的可维护性。
(3)降低变更引起的风险。变更是必然的,如果单一职责原则遵守得好,当修改一个功能的时候可以显著降低对其他功能的影响。
5、注意事项和细节
(1)降低类的复杂度,一个类只负责一项职责;
(2)提高类的可读性,可维护性;
(3)降低变更引起的风险;
(4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单、才可以在代码级违反单一职责原则,只有类中的方法足够少,才可以在类中保持方法级别的单一职责原则。
二、接口隔离原则
1、目的
避免接口过于臃肿
2、定义
客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
3、具体实现
适度细化接口,将臃肿的接口拆分为独立的几个接口。
4、优点
(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
(3)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
(4)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
5、注意事项和细节
如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
三、依赖倒转原则
1、目的
避免需求变化导致过多的维护工作
2、定义
* 高层模块不应该依赖底层模块,二者都应该依赖其抽象;
* 抽象不应该依赖细节;
* 细节应该依赖抽象。
每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。在Java语言中,抽象就是指接口和或抽象类,两者都不能被直接实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化。
3、具体实现
面向接口编程,使用接口或者抽象类制定好规范和契约,而不去设计任何具体的操作,把展现细节的任务交给他们的实现类去完成。
4、DIP的好处
采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
5、DIP的几种写法
(1)接口声明依赖对象;
(2)构造函数传递依赖对象;
在类中通过构造函数声明依赖对象(好比spring中的构造器注入),采用构造器注入。
(3)Setter方法传递依赖对象
在抽象中设置setter方法声明依赖对象(spring中的方法注入)
6、深入理解
依赖倒转原则的本质就是通过抽象使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。
在项目中使用这个规则需要以下原则:
(1)每个类尽量都要有接口或抽象类:依赖倒转的基本要求,有抽象才能依赖倒转;
(2)变量的表明类型尽量是接口或者抽象类;
(3)任何类都不应该从具体类派生;
(4)尽量不要重写基类已经写好的方法(里氏替换原则);
(5)结合里氏替换原则来使用:
接口负责定义public属性和方法,并且声明与其他对象的依赖关系;
抽象类负责公共构造部分的实现;
实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化;
一句话,依赖倒转原则就是面向接口编程。
四、里氏替换原则
1、目的
避免系统继承体系被破坏
2、定义
所有引用基类的地方必须能透明地使用其子类的对象。
3、具体实现
(1)子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法;
(2)子类可以增加自己特有的方法;
(3)当子类覆盖或实现父类的抽象方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;方法的后置条件(即方法的返回值)要比父类更严格。
(4)如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸形,则建议断开继承关系,采用依赖,聚合,组合等关系继承。
4、代码实例
package designMode.advance.principle; public class Liskov { public static void
main(String[] args) { A a = new A(); System.out.println("11-3=" + a.func1(11,
3)); System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------"); B b = new B();
//因为B类不再继承A类,因此调用者,不会再func1是求减法 //调用完成的功能就会很明确 System.out.println("11+3=" +
b.func1(11, 3));//这里本意是求出11+3 System.out.println("1+8=" + b.func1(1, 8));// 1+8
System.out.println("11+3+9=" + b.func2(11, 3)); //使用组合仍然可以使用到A类相关方法
System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3 } } //创建一个更加基础的基类
class Base { //把更加基础的方法和成员写到Base类 } // A类 class A extends Base { // 返回两个数的差
public int func1(int num1, int num2) { return num1 - num2; } } // B类继承了A //
增加了一个新功能:完成两个数相加,然后和9求和 class B extends Base { //如果B需要使用A类的方法,使用组合关系 private A
a = new A(); //这里,重写了A类的方法, 可能是无意识 public int func1(int a, int b) { return a +
b; } public int func2(int a, int b) { return func1(a, b) + 9; } //我们仍然想使用A的方法
public int func3(int a, int b) { return this.a.func1(a, b); } }
五、开闭原则
1、目的
提高扩展性、便于维护
2、定义
对扩展开放(对提供方),对修改关闭(对使用方)。
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。
六、迪米特法则
1、目的
降低类与类之间的耦合度
2、定义
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好,对于依赖的类不管有多复杂,都尽量将逻辑封装在类的内部,对外除了提供public方法,不泄漏任何信息。
更简单的说法:只与直接朋友通信。
直接朋友:每个对象都会与其它对象有耦合关系,耦合的方式有很多,依赖、关联、组合、聚合等。我们称出现在成员变量,方法参数,方法返回值中的类称为直接朋友,而出现在局部变量中的类不能称为直接朋友,也就是说,陌生的类不要以局部变量的形式出现在类的内部。
3、注意事项和细节
(1)在类的结构设计上,尽量降低类成员的访问权限;
(2)在类的设计上,优先考虑将一个类设计成不变类;
(3)在类的引用上,将引起其他类的次数降到最低;
(4)不暴露类的属性成员,而应该提供相应的访问器(getter、setter);
(5)谨慎使用序列化(serializable)功能;
过分的使用迪米特原则,会产生大量这样的中介和传递类,类之间需要通信就通过第三方转发的方式,就会造成系统的不同模块之间的通信效率降低、使系统的不同模块之间不容易协调等缺点,同时大大增加了系统的复杂度。所以
在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
4、代码实例
下面代码违反了迪米特法则,我平时就这么写的。。。
package designMode.advance.principle; import java.util.ArrayList; import
java.util.List; public class Demeter { public static void main(String[] args) {
//创建了一个 SchoolManager 对象 SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息 schoolManager.printAllEmployee(new CollegeManager()); }
} //学校总部员工类 class Employee { private String id; public void setId(String id) {
this.id = id; } public String getId() { return id; } } //学院的员工类 class
CollegeEmployee { private String id; public void setId(String id) { this.id =
id; } public String getId() { return id; } } //管理学院员工的管理类 class CollegeManager
{ //返回学院的所有员工 public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>(); for (int i = 0;
i < 10; i++) { //这里我们增加了10个员工到 list CollegeEmployee emp = new
CollegeEmployee(); emp.setId("学院员工id= " + i); list.add(emp); } return list; } }
//学校管理类 //分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager //CollegeEmployee
不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则 class SchoolManager { //返回学校总部的员工 public
List<Employee> getAllEmployee() { List<Employee> list = new
ArrayList<Employee>(); for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee(); emp.setId("学校总部员工id= " + i); list.add(emp); }
return list; } //该方法完成输出学校总部和学院员工信息(id) void printAllEmployee(CollegeManager
sub) { //分析问题 //1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友 //2.
CollegeEmployee 是以局部变量方式出现在 SchoolManager //3. 违反了 迪米特法则 //获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------"); for (CollegeEmployee e :
list1) { System.out.println(e.getId()); } //获取到学校总部员工 List<Employee> list2 =
this.getAllEmployee(); System.out.println("------------学校总部员工------------");
for (Employee e : list2) { System.out.println(e.getId()); } } }
使用迪米特法则改进:
//该方法完成输出学校总部和学院员工信息(id) void printAllEmployee(CollegeManager sub) { //分析问题
//1. 将输出学院的员工方法,封装到CollegeManager sub.printEmployee(); //获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------"); for (Employee e : list2)
{ System.out.println(e.getId()); } } //管理学院员工的管理类 class CollegeManager2 {
//返回学院的所有员工 public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>(); for (int i = 0;
i < 10; i++) { //这里我们增加了10个员工到 list CollegeEmployee emp = new
CollegeEmployee(); emp.setId("学院员工id= " + i); list.add(emp); } return list; }
//输出学院员工的信息 public void printEmployee() { //获取到学院员工 List<CollegeEmployee> list1
= getAllEmployee(); System.out.println("------------学院员工------------"); for
(CollegeEmployee e : list1) { System.out.println(e.getId()); } } }
简单来说,就是将获取学院员工的方法写到学校员工类中。
七、合成复用原则
1、目的
防止类的体系庞大
2、定义
它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
3、注意事项和细节
(1)通常的复用分为继承复用和合成复用,继承复用虽然有简单和易实现的优点,但它也有如下的缺点:
* 继承复用破坏了类的封装性
因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
* 子类和父类的耦合度高
父类的改变会直接影响子类,不利于类的扩展和维护。
* 限制了复用的灵活性
从父类继承而来的实现是静态的,在编译时已经定义,运行时无法发生变化。
(2)采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
* 维护了类的封装性
因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
* 低耦合
这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
* 复用的灵活性高
这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。