JAVA七大设计原则总结(详解篇)

目录

什么是设计模式

概览

开闭原则

依赖倒置原则

单一职责原则

接口隔离原则

迪米特原则

里氏替换原则

合成复用原则

设计原则总结


什么是设计模式

  • 设计模式(Design Pattern)是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结。使用设计模式是为了可重用性代码,让代码更容易被他人理解,保证代码可靠性。

 

概览

  • 开闭原则:是指一个软件实体如类、模块和函数应该对扩展开放, 对修改关闭
  • 依赖倒置原则:是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象而不依赖于具体。
  • 单一职责原则:是指一 个 Class/Interface/Method 只负责一项职责。
  • 接口隔离原则:是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
  • 迪米特法原则(最少知道原则):是指一个对象应该对其他对象保持最少的了解
  • 里氏替换原则:是指一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变
  • 合成复用原则:是指尽量使用对象组合(has-a)或聚合(contanis-a),而不是继承关系达到软件复用的目的

 

开闭原则

 含义:

  •  开闭原则(Open-Closed Principle, OCP)是指一个软件实体 如类、模块和函数应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。强调的是用抽象构建框架,用实现扩展细节。开闭原则,是面向对象设计中最基础的设计原则。它指导我们如何建立稳定灵活的系统,例如:我们版本更新,我尽可能不修改源代码,但是可以增加新功能。

核心思想:

  • 面向抽象编程。

作用(优点):

  • 可以提高代码的可复用性:我们可以在软件完成以后,仍然可以对软件进行扩展,加入新的功能,非常灵活。因此,这个软件系统就可以通过不断地增加新的组件,来满足不断变化的需求。
  • 可以提高软件的可维护性:由于对于已有的软件系统的组件,特别是它的抽象底层不去修改,因此,我们不用担心软件系统中原有组件的稳定性,这就使变化中的软件系统有一定的稳定性和延续性。如:一人模块变化,会对其它的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化。

实现方式:

实现开闭原则的关键就在于“抽象”。把系统/软件的所有可能的行为抽象成一个抽象底层,这个抽象底层规定出所有的具体实现必须提供的方法的特征。作为系统设计的抽象层,要预见所有可能的扩展,从而使得在任何扩展情况下,系统的抽象底层不需修改;同时,由于可以从抽象底层导出一个或多个新的具体实现,可以改变系统的行为,因此系统设计对扩展是开放的。抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:

  • 通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。
  • 参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求。
  • 抽象层尽量保持稳定,一旦确定就不要修改。

举例说明:

以课程打折为例:

先看课程类接口ICourse:

public interface ICourse { 
    Integer getId(); 
    String getName(); 
    Double getPrice(); 
}
ICourse的实现类Java课程的类JavaCourse:
public class JavaCourse implements ICourse {
	private Integer Id;
	private String name;
	private Double price;

	public JavaCourse(Integer id, String name, Double price) {
		this.Id = id;
		this.name = name;
		this.price = price;
	}

	public Integer getId() {
		return this.Id;
	}

	public String getName() {
		return this.name;
	}

	public Double getPrice() {
		return this.price;
	}
}
现在我们要给Java课程做活动,价格优惠。如果修改 JavaCourse 中的 getPrice()方法,则会存在一定的风险,可能影响其他地方的调用结果。我们如何在不修改原有代码前提前下,实现价格优惠这个功能呢?
 
现在,我们增加一个处理优惠逻辑的类JavaDiscountCourse,提供一个方法去返回折扣价。这样就不用更改JavaCourse的getPrice()方法,降低了修改代码带来的风险,同时也不会影响到其他地方对JavaCourse的getPrice()方法的调用
public class JavaDiscountCourse extends JavaCourse {
	public JavaDiscountCourse(Integer id, String name, Double price) {
		super(id, name, price);
	}

	public Double getOriginPrice() {
		return super.getPrice();
	}

	public Double getDiscountPrice() {
		return super.getPrice() * 0.61;
	}
}

 

依赖倒置原则

含义:

  • 依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层(调用层)模块不应该依赖底层(被调用层)模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。即面向接口编程,不要面向实现编程。依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构要比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类。

核心思想:

  • 面向接口编程,不要面向实现编程。

作用(优点):

  • 减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并能够降低修改程序所造成的风险。

实现方式:

  • 传递依赖关系有三种方式:接口传递(方法注入)、构造方法传递和setter方法传递
  • 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
  • 低层模板尽量都要有抽象类或接口,或者两个都有,程序稳定性更好。
  • 变量的声明类型尽量都是抽象类或接口,这样我们的变量变量引用和实际对象间就存在一个缓冲层,有利于程序的扩展和优化。
  • 继承时遵循里氏替换原则。

举例说明:

以学习多个学科的课程为例

先看XiaoQiang类:

public class XiaoQiang {
	public void studyJavaCourse() {
		System.out.println("XiaoQiang 在学习 Java 的课程");
	}

	public void studyPythonCourse() {
		System.out.println("XiaoQiang 在学习 Python 的课程");
	}
}

来调用一下:

public static void main(String[] args) { 
	XiaoQiang xq = new XiaoQiang(); 
	xq.studyJavaCourse();
	xq.studyPythonCourse(); 
}
随着学习兴趣的暴涨,现在 XiaoQiang还想学习 AI 人工智能的课程。这个时候业务扩展,我们的代码要从底层到高层(调用层)一次修改代码。在 XiaoQiang类中增加 studyAICourse()的方法,在高层也要追加调用。 如此一来,系统发布以后,实际上是非常不稳定的,在修改代码的同时也会带来意想不到的风险。接下来我们优化代码
 
创建一个课程的抽象 ICourse 接口:
public interface ICourse { 
	void study(); 
}
然后写 JavaCourse 类:
 
public class JavaCourse implements ICourse {
	@Override
	public void study() {
		System.out.println("XiaoQiang 在学习 Java 课程");
	}
}
再实现 PythonCourse 类:
public class PythonCourse implements ICourse {
	@Override
	public void study() {
		System.out.println("XiaoQiang在学习Python课程");
	}
}

修改XiaoQiang类:

public class XiaoQiang { 
	public void study(ICourse course){ 
	course.study(); 
	} 
}
来看调用:
public static void main(String[] args) { 
	XiaoQiang xq = new XiaoQiang(); 
	xq.study(new JavaCourse()); 
	xq.study(new PythonCourse()); 
}
我们这时候再看来代码,XiaoQiang 的兴趣无论怎么暴涨,对于新的课程,我只需要新建一个类,通过传参的方式告诉 XiaoQiang ,而不需要修改底层代码。实际上这是一种大家非常熟悉的方式,叫依赖注入。注入的方式还有构造器方式和 setter 方式。
 
我们来看构造器注入方式:
public class XiaoQiang { 
	private ICourse course;
	
	public XiaoQiang(ICourse course){ 
		this.course = course; 
	}
	public void study(){ 
		course.study(); 
	} 
}
看调用代码:
public static void main(String[] args) { 
	XiaoQiang tom = new XiaoQiang(new JavaCourse()); 
	xq.study(); 
}
根据构造器方式注入,在调用时,每次都要创建实例。那么,如果 XiaoQiang 是全局单例,则我们就只能选择用 Setter 方式来注入,继续修改 XiaoQiang 类的代码:
 
public class XiaoQiang {
	private ICourse course;

	public void setCourse(ICourse course) {
		this.course = course;
	}

	public void study() {
		course.study();
	}
}
看调用代码:
public static void main(String[] args) { 
	XiaoQiang xq = new XiaoQiang(); 
	xq.setCourse(new JavaCourse()); 
	xq.study(); 
	xq.setCourse(new PythonCourse()); 
	xq.study(); 
}
切记:以抽象为基准比以细节为基准搭建起来的架构要稳定得多,因此大家在拿到需求之后,要面向接口编程,先顶层再细节来设计代码结构
 

 

单一职责原则

含义:

  • 单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。换一种说法,一个类只负责一项职责,应该仅有一个引起它变化的原因。总体来说就是一个Class/Interface/Method 只负责一项职责。

核心思想:

  • Class/Interface/Method 只负责一项职责

作用(优点):

  • 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
  • 提高类的可读性,提高系统的可维护性;
  • 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。

实现方式:

  • 单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。

举例说明:

以课程有直播课和录播课。直播课不能快进和快退,录播可以可以任意的反复观看,功能职责不一样为例说明。

先创建一个 Course 类:
public class Course {
	public void study(String courseName) {
		if ("直播课".equals(courseName)) {
			System.out.println("不能快进");
		} else {
			System.out.println("可以任意的来回播放");
		}
	}
}
看代码调用:
 
public static void main(String[] args) { 
	Course course = new Course(); 
	course.study("直播课"); 
	course.study("录播课"); 
}
从上面代码来看,Course 类承担了两种处理逻辑。假如,现在要对课程进行加密,那么直播课和录播课的加密逻辑都不一样,必须要修改代码。而修改代码逻辑势必会相互影响容易造成不可控的风险。我们对职责进行分离解耦,来看代码,分别创建两个类 ReplayCourse 和 LiveCourse:
 
LiveCourse 类:
public class LiveCourse {
	public void study(String courseName) {
		System.out.println(courseName + "不能快进看");
	}
}
ReplayCourse 类:
public class ReplayCourse {
	public void study(String courseName) {
		System.out.println("可以任意的来回播放");
	}
}
调用代码:
public static void main(String[] args) { 
	LiveCourse liveCourse = new LiveCourse(); 
	liveCourse.study("直播课"); 
	ReplayCourse replayCourse = new ReplayCourse(); 
	replayCourse.study("录播课"); 
}
业务继续发展,课程要做权限。没有付费的学员可以获取课程基本信息,已经付费的学员可以获得视频流,即学习权限。那么对于控制课程层面上至少有两个职责。我们可以把展示职责和管理职责分离开来,都实现同一个抽象依赖。
设计一个顶层接口,创建 ICourse 接口:
public interface ICourse { 
	//获得基本信息 
	String getCourseName(); 
	//获得视频流 
	byte[] getCourseVideo(); 
	
	//学习课程 
	void studyCourse(); 
	//退款 
	void refundCourse(); 
}
想想类层面的单一职责我们可以把这个接口拆成两个接口,创建一个接口 ICourseInfo 和 ICourseManager:
 
ICourseInfo 接口:
public interface ICourseInfo {
	String getCourseName();

	byte[] getCourseVideo();
}
ICourseManager 接口:
 
public interface ICourseManager {
	void studyCourse();

	void refundCourse();
}

来看一下类图:  

JAVA七大设计原则总结(详解篇)_第1张图片

 
下面我们来看一下方法层面的单一职责设计。有时候,我们为了偷懒,通常会把一个方法写成下面这样:
private void modifyUserInfo(String userName,String address){ 
	userName = "XiaoQiang"; 
	address = "Changsha";
}
还可能写成这样:
private void modifyUserInfo(String userName,String address,boolean bool){ 
	if(bool){ 
		userName = "XiaoQiang"; 
	}else{ 
		address = "Changsha";
	}
}
显然,上面的 modifyUserInfo()方法中都承担了多个职责,既可以修改 userName,也可以修改 address,甚至更多,明显不符合单一职责。那么我们做如下修改,把这个方法拆成两个:
 
private void modifyUserName(String userName){ 
	userName = "XiaoQiang"; 
}
private void modifyAddress(String address){ 
	address = "Changsha"; 
}

这修改之后,开发起来简单,维护起来也容易。所以编写代码的过程,尽可能地让接口和方法保持单一职责,便于我们项目后期的维护

 

接口隔离原则

含义:

  • 接口隔离原则(Interface Segregation Principle, ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。接口隔离原则符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性
    和可维护性

核心思想

  • 高内聚低耦合

作用(优点):

  • 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  • 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性、可扩展性和可维护性。

接口隔离原则跟单一职责原则区别:

  • 单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。
  • 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

实现方式:

  • 一个类对一类的依赖应该建立在最小的接口之上。
  • 建立单一接口,不要建立庞大臃肿的接口。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
  • 尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度,接口过小则会造成接口数量过多,使设计复杂化)。
  • 多花时间去思考、要考虑业务模型,包括以后有可能发生变更的地方还要做一些预判。所以对于抽象、对业务模型的理解是非常重要的

举例说明:

以动物行为的抽象为例:
 
IAnimal 接口:
public interface IAnimal {
	void eat();
	void fly();
	void swim();
}
Bird 类实现:
public class Bird implements IAnimal {
	@Override
	public void eat() {
	}

	@Override
	public void fly() {
	}

	@Override
	public void swim() {
	}
}
Dog 类实现: 
 
public class Dog implements IAnimal {
	@Override
	public void eat() {
	}

	@Override
	public void fly() {
	}

	@Override
	public void swim() {
	}
}
可以看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法显然不可能的。这时候,我们针对不同动物行为来设计不同的接口,分别设计 IEatAnimal,IFlyAnimal 和 ISwimAnimal 接口,来看代码:
 
IEatAnimal 接口:
public interface IEatAnimal { 
	void eat(); 
}

IFlyAnimal 接口:

public interface IFlyAnimal { 
	void fly(); 
}

ISwimAnimal 接口:

public interface ISwimAnimal { 
	void swim(); 
}
Dog 只实现 IEatAnimal 和 ISwimAnimal 接口:
public class Dog implements ISwimAnimal, IEatAnimal {
	@Override
	public void eat() {
	}

	@Override
	public void swim() {
	}
}

这样Dog 就可以依赖它不需要的接口,并且接口单一不会庞大臃肿,有很好的复用性和可维护性

最后来看下两种类图的对比,还是非常清晰明了的:

JAVA七大设计原则总结(详解篇)_第2张图片

迪米特原则

含义:

  • 迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP)尽量降低类与类之间的耦合。迪米特原则主要强调只和朋友交流,不和陌生人说话(出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。)

核心思想

  • 降低类之间的耦合度

作用(优点):

  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

举例说明:

以权限系统为例,Boss 需要查看目前发布到线上的课程数量。这时候,Boss 要找到 TeamLeader 去进行统计,TeamLeader 再把统计结果告诉 Boss。接下来我们看代码:
 
Course 类:
public class Course {

}
TeamLeader 类:
public class TeamLeader {
	public void checkNumberOfCourses(List courseList) {
		System.out.println("目前已发布的课程数量是:" + courseList.size());
	}
}
Boss 类:
public class Boss { 
	public void commandCheckNumber(TeamLeader teamLeader){ 
		//模拟 Boss 一页一页往下翻页,TeamLeader 实时统计
		List courseList = new ArrayList(); 
		for (int i= 0; i < 20 ;i ++) { 
			courseList.add(new Course()); 
		}
		teamLeader.checkNumberOfCourses(courseList); 
	}
}
测试代码:
public static void main(String[] args) {
	Boss boss = new Boss(); 
	TeamLeader teamLeader = new TeamLeader(); 
	boss.commandCheckNumber(teamLeader);
}
写到这里,其实功能已经都已经实现,代码看上去也没什么问题。根据迪米特原则,Boss 只想要结果,不需要跟 Course 产生直接的交流。而 TeamLeader 统计需要引用 Course 对象。Boss 和 Course 并不是朋友,从下面的类图就可以看出来
 

JAVA七大设计原则总结(详解篇)_第3张图片

下面来对代码进行改造:
 
TeamLeader 类:
public class TeamLeader {
	public void checkNumberOfCourses() {
		List courseList = new ArrayList();
		for (int i = 0; i < 20; i++) {
			courseList.add(new Course()); 
		}
		System.out.println("目前已发布的课程数量是:"+courseList.size()); 
	} 
}
Boss 类:
public class Boss {
	public void commandCheckNumber(TeamLeader teamLeader) {
		teamLeader.checkNumberOfCourses();
	}
}

由此设计Course 和 Boss 已经没有关联了,再来看下面的类图

JAVA七大设计原则总结(详解篇)_第4张图片

 
 
 

里氏替换原则

含义:

  • 里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为 T1 的对象 o1,都有 类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没 有发生变化,那么类型 T2 是类型 T1 的子类型。换一种理解方式,可以理解为一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,我们总结一下:(实现方式:
  1. 引申含义:子类可以扩展父类的功能,但不能改变父类原有的功能
  2. 子类可以实现父类的抽象方法,
  3. 不能覆盖父类的非抽象方法。
  4. 子类中可以增加自己特有的方法。
  5. 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入 参数更宽松。
  6. 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输 出/返回值)要比父类更严格或相等。
  • 里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

核心思想:

  • 子类中不应该重写父类的方法

作用(优点):

  • 约束继承泛滥,里氏替换原则是实现开闭原则的重要方式之一,是开闭原则的一种体现。
  • 克服了继承中重写父类造成的可复用性变差的缺点。
  • 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
  • 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性。降低需求变更时引入的风险

举例说明:

用正方形、矩形和四边形的关系说明里氏替换原则
 
我们都知道正方形是一个特殊的长方形,那么就可以创建一个长方形父类 Rectangle 类:
 
public class Rectangle {
	private long height;
	private long width;

	@Override
	public long getWidth() {
		return width;
	}

	@Override
	public long getLength() {
		return length;
	}

	public void setLength(long length) {
		this.length = length;
	}

	public void setWidth(long width) {
		this.width = width;
	}
}
创建正方形 Square 类继承长方形:
 
public class Square extends Rectangle {
	private long length;

	public long getLength() {
		return length;
	}

	public void setLength(long length) {
		this.length = length;
	}

	@Override
	public long getWidth() {
		return getLength();
	}

	@Override
	public long getHeight() {
		return getLength();
	}

	@Override
	public void setHeight(long height) {
		setLength(height);
	}

	@Override
	public void setWidth(long width) {
		setLength(width);
	}
}
在测试类中创建 resize()方法,根据逻辑长方形的宽应该大于等于高,我们让高一直自增,知道高等于宽变成正方形
public static void resize(Rectangle rectangle){ 
	while (rectangle.getWidth() >= rectangle.getHeight()){ 
		rectangle.setHeight(rectangle.getHeight() + 1); 
		System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight()); 
	}
	System.out.println("resize 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight()); 
}
测试代码:
public static void main(String[] args) {
	Rectangle rectangle = new Rectangle(); 
	rectangle.setWidth(20); 
	rectangle.setHeight(10); 
	resize(rectangle); 
}

运行结果: 

width:20,height:11
width:20,height:12
width:20,height:13
width:20,height:14
width:20,height:15
width:20,height:16
width:20,height:17
width:20,height:18
width:20,height:19
width:20,height:20
width:20,height:21
resize 方法结束
width:20,height:21
发现高比宽还大了,在长方形中是一种非常正常的情况。现在我们再来看下面的代码, 把长方形 Rectangle 替换成它的子类正方形 Square,修改测试代码:
public static void main(String[] args) {
	Square square = new Square(); 
	square.setLength(10);
	resize(rectangle); 
}
这时候我们运行的时候就出现了死循环,违背了里氏替换原则,将父类替换为子类后, 程序运行结果没有达到预期。因此,我们的代码设计是存在一定风险的。里氏替换原则只存在父类与子类之间约束继承泛滥。我们再来创建一个基于长方形与正方形共同的 抽象四边形 Quadrangle 接口:
 
public interface Quadrangle {
	long getWidth();
	long getHeight();
}
修改长方形 Rectangle 类:
public class Rectangle implements Quadrangle {
	private long height;
	private long width;

	@Override
	public long getWidth() {
		return width;
	}

	public long getHeight() {
		return height;
	}

	public void setHeight(long height) {
		this.height = height;
	}

	public void setWidth(long width) {
		this.width = width;
	}
}
修改正方形类 Square 类:
 
public class Square implements Quadrangle {
	private long length;

	public long getLength() {
		return length;
	}

	public void setLength(long length) {
		this.length = length;
	}

	@Override
	public long getWidth() {
		return length;
	}

	@Override
	public long getHeight() {
		return length;
	}
}
此时,如果我们把 resize()方法的参数换成四边形 Quadrangle 类,方法内部就会报错。 因为正方形 Square 已经没有了 setWidth()和 setHeight()方法了。因此,为了约束继承泛滥,resize()的方法参数只能用 Rectangle 长方形。

 

 

合成复用原则

含义:

  • 合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)、聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的。要根据具体的业务场景来做代码设计,其实也都需要遵循 OOP

核心思想:

  • 尽量使用聚合、组合的方式,而不是使用继承。

实现方式:

  • 合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

聚合与组合的区别:

  • 聚合是用来表示“拥有”关系或者整体与部分的关系。
  • 组合则是表示一种强得多的“拥有”关系,在组合里,部分与整体的生命周期是一样的。一个组合的新的对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。一个组合关系中的成分对象是不能与另一个组合关系共享的。一个组成部分在同一个时间内只能属于一个组合关系。

复用的基本方式有两种:

  • 组合/聚合
  • 继承

复用两种方式的区别:

  • 组合/聚合是将已有的对象纳入到新对象中,使之成为新对象的一部分,
  • 优点:
  1. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  2. 维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  3. 每个新的类可以将焦点集中在一个任务上。
  4. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
  • 缺点:
  1. 通过使用这种方式复用建造的系统会有较多的对象需要管理。
  • 继承是面向对象特有的复用工具,而且也最容易被滥用。继承复用通过扩展一个已有对象的实现来得到新的功能。
  • 优点:
  1. 较为容易,因为父类的大部分功能都可以通过继承关系自动进入子类。
  2. 修改或扩展继承而来的实现较为容易。
  • 缺点:
  1. 破坏了类的封装性,因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,修改父类就会形成链锁反映,这不利于类的扩展与维护。
  3. 限制了复用的灵活性,从父类继承而来的实现是静态的,在编译时已经定义,所以不可能在运行时间内发生改变,因此也没有足够的灵活性。

举例说明:

以数据库操作为例,先来创建 DBConnection 类:

public class DBConnection {
	public String getConnection() {
		return "MySQL 数据库连接";
	}
}
创建 ProductDao 类:
public class ProductDao {
	private DBConnection dbConnection;

	public void setDbConnection(DBConnection dbConnection) {
		this.dbConnection = dbConnection;
	}

	public void addProduct() {
		String conn = dbConnection.getConnection();
		System.out.println("使用" + conn + "增加产品");
	}
}
这就是一种非常典型的合成复用原则应用场景。但是,目前的设计来说,DBConnection 还不是一种抽象,不便于系统扩展。目前的系统支持 MySQL 数据库连接,假设业务发生变化,数据库操作层要支持 Oracle 数据库。当然,我们可以在 DBConnection 中增加对Oracle 数据库支持的方法。但是违背了开闭原则。其实,我们可以不必修改 Dao 的代码,将 DBConnection 修改为 abstract,来看代码:
public abstract class DBConnection {
	public abstract String getConnection();
}
然后,将 MySQL 的逻辑抽离:
public class MySQLConnection extends DBConnection {
	@Override
	public String getConnection() {
		return "MySQL 数据库连接";
	}
}
再创建 Oracle 支持的逻辑:
 
public class OracleConnection extends DBConnection {
	@Override
	public String getConnection() {
		return "Oracle 数据库连接";
	}
}
具体选择交给应用层,来看一下类图

JAVA七大设计原则总结(详解篇)_第5张图片

 
 
 
 
 

设计原则总结

这7种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同:

  • 开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭。
  • 依赖倒置原则告诉我们要面向接口编程。
  • 单一职责原则告诉我们实现类要职责单一。
  • 接口隔离原则告诉我们在设计接口的时候要精简单一。
  • 迪米特法则告诉我们要降低耦合度。
  • 里氏替换原则告诉我们不要破坏继承体系。
  • 合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。
学习设计原则,学习设计模式的基础。在实际开发过程中,千万不能形成强迫症。碰到业务复杂的场景,我们需要随机应变。而且不是一定要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要在适当的场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。

 

 

你可能感兴趣的:(java软件架构七大设计原则,23种设计模式)