Java中的设计模式基本上都是围绕着这六大原则进行设计,设计模式在我们开发过程中时必不可少的,它能够让我们更好的设计一个系统,使得这个系统具有良好的扩展性,可维护性等。下面我们开始了解六大原则!
单一职责原则(Single Responsibility Principle,SRP)
单一职责原则我们可以从名字上了解,就是指一个类的功能要单一,不能包罗万象。正如一个人,分配的工作如果太多,并不会提高工作的效率,有时候甚至会顾此失彼。单一职责原则并非面向对象编程思想所特有,只要是模块化 程序设计,都适用单一职责原则。
实现单一职责原则的方法很简单,加入类A有两个职责P1和P2,那么只需要分别创建两个类C1和C2,让他们分别负责P1和P2。这样就实现了单一职责原则,当我们需要修改P1职责时就只需要修改C1,而不用去修改C2,同时如果C1发生故障也不会影响到C2。
虽然单一职责原则十分简单,但是却十分重要。在实际开发中,随着程序功能的开发,程序的职责随之增加,为了避免程序中某个职责故障导致影响其他的职责运行,单一职责原则必不可少。
使用单一职责原则的优点:
- 可以降低类的复杂度,因为一个类只负责一个职责,逻辑肯定比负责多项职责的简单。
- 提高了类的可读性,提高系统的可维护性。
- 降低系统耦合性,当你修改一个职责时对其他职责的影响能显著降低。
单一职责的缺点是会增加类的数量,但是对于系统的稳定性和可维护性来说,这个缺点几乎可以忽略。
里式替换原则(the Liskov Substitution Principle,LSP)
里式替换原则简单来说就是:子类应当可以替换父类并且出现在父类能够出现的所有地方。例如,一个公司开年会举行员工抽奖活动,那么无论是老员工还是新员工,无论员工的岗位是否一样,只要是这个公司的员工,就可以参加。
里式替换原则依赖于面向对象的继承和多态。继承给我们带来巨大便利的同时,也带来了弊端,例如类的耦合性降低,打破了类的封装等。继承其实还包含了一层含义,父类中凡是实现好的方法,虽然不强制子类必须遵循父类中方法的实现方式,但是如果这些子类对这些非抽象方法任意修改,就失去了继承的意义(如果继承一个类,可是子类完全修改了父类所有的方法,使得子类与父类的相似性近乎于零,那么这样与没有继承有什么区别呢),会对整个继承体系造成破坏。
因此里式替换原则其实还包含了以下几层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。(可能造成功能实现方式错误)
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的形参应该比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更加严格。
接口隔离原则(the Interface Segregation Principle,ISP)
接口隔离原则就是模块间通过抽象接口隔离开,而不是通过具体的类强偶合起来,即面向接口编程。
类间的依赖关系应该建立在最小的接口上,建立单一的接口,而不是建立庞大臃肿的接口,尽量细化接口,减少接口中的方法。我们举例来说明接口隔离原则。
这个图的意思是:类B依赖接口C中的方法1、方法2、方法3,类D是对类B依赖的实现。类A依赖接口I中的方法4、方法5,类C是对类A依赖的实现。但是我们可以很容易的发现一个问题,类C和类D都存在他们不用到的方法。显然这不是一个好的设计,如果要将这个设计修改为复合接口隔离原则,就必须对接口进行拆分,如下图所示:
通过这个例子,我们可以很容易的理解接口隔离原则,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。很多人会觉得接口隔离原则和单一职责原则很相似,但单一职责原则关注的重点是职责,而接口隔离原则关注的重点是接口依赖的隔离。单一职责原则主要是用来约束类,其次才是接口和方法,它关注程序中的实现和细节。而接口隔离原则主要约束接口,针对的是抽象,是对程序整体框架的构建。
采用接口隔离原则有以下几点需要注意:
- 接口尽量小,但要有一定限度,过小的话会造成接口数量过多,使得设计复杂化。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。
依赖倒置原则(the Dependency Inversion Principle,DIP)
依赖倒置是最难以实现的原则,它是实现开闭原则的重要途径,DIP没有实现就不可能实现开闭原则。首先我们要先对依赖倒置原则中的一些概念进行了解:
- 低层模块:不可分割的原子逻辑,可能会根据业务逻辑经常变化。
- 高层模块:低层模块的再组合,对低层模块的抽象。
- 抽象: 接口或抽象类(是底层模块的抽象,特点:不能直接被实例化)
- 与接口或抽象类对应的实现类:低层模块的具体实现(特点:可以直拉被实例化)
依赖倒置原则可以概括成两句话:
- 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
通俗来讲,依赖倒置的本质就是通过抽象(接口或抽象类)使各个类或模块实现彼此相互独立,互不影响,实现模块的松耦合。
依赖倒置的优点
- 可以通过抽象使各个类或模块彼此独立,不互相影响,实现模块间的松耦合(本质)
- 可以规避一些非技术因素引起的问题,可以减少需求变化的工作量剧增的情况。
- 可以促进并行开发。
有一个最常见的例子,就是数据访问抽象层。
在我们编写业务代码时,我们不应该直接在某个业务代码处直接用jdbc操作数据库,业务层方法的参数应该是通过数据访问层的抽象接口进行访问。这个数据访问抽象层就体现了依赖倒置原则。
迪米特法则(Law of Demeter,LoD)
迪米特法则的核心思想就是类间解耦合,简单来说就是一个类对自己依赖的类知道的越少越好。什么是耦合,耦合就是对象之间的依赖性,依赖性越强耦合度越高。耦合度高意味着当一个类发生改变时,对与其耦合度高的另一个类产生的影响越大。这会大大提高我们的维护成本。因此对象的设计应该使类和构件之间的耦合最小。
我们通过一个违反迪米特法则的例子来加深我们对其的理解:
//总公司员工
class Employee{
private String id;
public void setId(String id){
this.id = id;
}
public String getId(){
return id;
}
}
//分公司员工
class SubEmployee{
private String id;
public void setId(String id){
this.id = id;
}
public String getId(){
return id;
}
}
class SubCompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
}
class CompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId("总公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
List list1 = sub.getAllEmployee();
for(SubEmployee e:list1){
System.out.println(e.getId());
}
List list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
public class Client{
public static void main(String[] args){
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
}
}
这个设计的问题主要在于CompanyManager中,从逻辑上总公司只与它的分公司耦合即可,与分公司的员工并无联系,而CompanyManager中却与分公司的员工耦合,这是不必要的耦合,按照迪米特法则我们应该把它消除。
class SubCompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<100; i++){
SubEmployee emp = new SubEmployee();
//为分公司人员按顺序分配一个ID
emp.setId("分公司"+i);
list.add(emp);
}
return list;
}
public void printEmployee(){
List list = this.getAllEmployee();
for(SubEmployee e:list){
System.out.println(e.getId());
}
}
}
class CompanyManager{
public List getAllEmployee(){
List list = new ArrayList();
for(int i=0; i<30; i++){
Employee emp = new Employee();
//为总公司人员按顺序分配一个ID
emp.setId("总公司"+i);
list.add(emp);
}
return list;
}
public void printAllEmployee(SubCompanyManager sub){
sub.printEmployee();
List list2 = this.getAllEmployee();
for(Employee e:list2){
System.out.println(e.getId());
}
}
}
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。
迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个"中介"来发生联系。但是过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
开放封闭原则(Open-Close Principle,OCP)
开放封闭原则是面向对象中最重要的设计原则,它不针对具体,而是针对一个思想,一个方向。在任何一个软件项目中,需求是不可能不变的,而面向对象软件框架的设计,就是为了将功能模块封装、降低耦合。而开闭原则就是这个目标的直接体现。其他的设计原则,都是在这一大的原则下进行的。
开放封闭就是一个模块在扩展性方面应该是开放的,而在更改性方面应该是封闭的。也就是在设计一个模块时,应当使这个模块可以在不被修改的前提下被扩展,即可以在不必修改代码的情况下修改这个模块的行为。开放封闭原则目的在于面对需求的改变而能够保持系统的相对稳定。
开放封闭原则其实很难实现,又很容易实现。为什么说他难又说他容易呢?要实现开放封闭原则,意味着你必须实现了其他五个设计原则。当你实现了五个设计原则后,开放封闭原则则自然实现了。