一、概述
软件设计原则是指在开发和设计软件时应遵循的一些基本原则和准则。为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,要尽量遵循软件设计七大原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
遵循软件设计原则的优点如下:
遵循软件设计原则有以下几个重要原因:
-
提高代码质量:软件设计原则是一些经过验证的最佳实践,能够帮助开发人员以一种结构良好且可扩展的方式编写代码。可以提高代码的可读性、可维护性和可测试性,从而提高代码质量。
-
提升系统可扩展性:良好的软件设计原则可以使系统更易于扩展。通过组织代码结构、定义清晰的接口和模块化的设计,可以使新功能的添加和现有功能的修改更加简单和安全。这可以确保系统能够适应未来的需求变化,提高系统的可扩展性。
-
降低软件维护成本:合理的软件设计可以减少系统的复杂性和耦合度,使得对系统的修改和维护更加容易。在日常维护中,如果遵循设计原则,开发人员可以更快地理解和修改代码,从而降低了维护成本。
-
提高团队协作效率:软件设计原则提供了一种通用的设计语言和指导原则,可以促使开发团队成员之间更好地共享和理解设计思路。一个遵循设计原则的项目可以减少沟通和理解上的困惑,从而提高团队协作的效率。
-
提升软件系统的可靠性和性能:良好的软件设计原则可以降低系统中出现错误的可能性,并提高系统的稳定性和性能。通过合理地设计类之间的关系和划分模块,可以减少bug的产生,增强系统的可靠性和健壮性。
各种原则要求的侧重点不同,大致介绍如下:
1. 开闭原则:要对扩展开放,对修改关闭。
2. 里氏替换原则:不要破坏继承体系。
3. 依赖倒置原则:要依赖抽象接口编程。
4. 单一职责原则:实现类要职责单一。
5. 接口隔离原则:设计接口的时候要精简单一。
6. 迪米特法则:要降低耦合度。
7. 合成复用原则:优先使用组合或者聚合关系复用,少用继承关系复用。
二、开闭原则
2.1 介绍
当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
一个常见的做法是使用抽象和接口来实现开放封闭原则:我们定义接口和抽象类来表示一组功能,在具体实现类中实现具体的细节。当需要添加新的功能时,我们可以通过实现接口或者扩展抽象类的方式来添加新的代码,而不需要修改原有的代码。
2.2 作用
- 提高代码可复用性:在面向对象程序设计中,根据原子和抽象编程提高代码可复用性。在拓展时,只需要实现现有的接口和抽象类就可以实现拓展
- 提高可维护性:稳定性和延续性高,易于拓展和维护
- 便于软件测试:只需要对拓展的代码进行测试,不用对原有的代码进行测试
2.3 演示
场景介绍
在”第五人格“游戏中,有很多个人物角色。此时可以抽象出一个角色类,每新增一个角色信息,就让它继承这个抽象类。
代码演示:
Step1:定义抽象角色类
/**
* [1]定义抽象的角色类,每新增一个角色时,只需要继承该方法并重写抽象方法
*/
public abstract class Role {
public abstract void display();
}
Step2:定义具体角色类
2.1 医生类
/**
* [2.1] 医生实现类
*/
public class Doctor extends Role {
@Override
public void display() {
System.out.println("医生:可以在受到攻击时自疗");
}
}
2.2 机械师类
/**
* [2.2] 机械师实现类
*/
public class Mechanician extends Role {
@Override
public void display() {
System.out.println("机械师:可以操控一个机器人,有时间和限制");
}
}
Step3:定义游戏总体控制类
/**
* [3] 游戏控制类:
* 以后每新增一个类,都只用给该类的成员方法进行设置,而不用修改该类的成员方法
*/
public class FifthPersonalityGame {
//注入实现了Role类的角色
private Role role;
public void setRole(Role role) {
this.role = role;
}
//调用角色的方法
public void display() {
role.display();
}
}
Step4:游戏测试
@Test
public void test(){
//1. 创建第五人格游戏对象
FifthPersonalityGame game = new FifthPersonalityGame();
//2. 创建角色
//如果还有其他可以选择的角色,可以直接创建后直接设置到游戏对象的成员方法中
Doctor doctor = new Doctor();
//c_Mechanician mechanician = new c_Mechanician();
//3. 选择当前角色
game.setRole(doctor);
//4. 显示角色信息
game.display();
}
如果后续又新增了“律师”、“魔术师”等角色,只需要让这个类继承Role这个抽象类即可。而不用修改现有的源码。
这种方式提高了代码的可复用性和可维护性。同时,在进行测试时,也只需要测试新增的类。
2.4 优秀框架中的开闭原则
在Spring框架中,就实现了对修改关闭但对扩展开放的设计理念,这使得开发者可以通过扩展和配置的方式来增加新功能,而不需要修改或破坏原有的框架代码。从而使得Spring 框架变得更加灵活、可扩展和易于维护。
以下是Spring使用开闭原则的一些举例:
-
依赖注入(DI):Spring 使用依赖注入将各个组件解耦,并通过配置文件或注解的方式动态地注入依赖关系。这使得在不修改已有代码的情况下,可以轻松地替换、添加或删除组件。
-
扩展点机制:Spring 提供了多个扩展点,如 BeanPostProcessor、BeanFactoryPostProcessor 等。这些扩展点允许开发者在不修改框架源码的情况下,通过编写自定义的扩展类来添加额外的功能。
-
面向接口编程:Spring 通过接口和抽象类定义了许多核心组件,如 ApplicationContext、BeanFactory 等。这样一来,开发者可以基于这些接口和抽象类进行扩展实现,而不需要修改框架的源码。
-
AOP(面向切面编程):Spring 的 AOP 功能通过动态代理机制实现,可以在不修改源代码的情况下,添加切面逻辑来实现横切关注点的功能。这使得系统的核心业务逻辑和横切关注点能够分离,符合开闭原则。
三、里氏替换原则
3.1 介绍
子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
3.2 作用
里氏替换原则是实现开闭原则的重要方式之一。 它克服了继承中重写父类造成的可复用性变差的缺点。 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
3.3 演示
场景介绍
关于里氏替换原则的例子有很多,最有名的是“正方形不是长方形”问题。生活中的例子也有许多,例如:玩具狗不会叫,不能定义为”狗“的子类;玩具炸弹伤害不了敌人,不能定义为”武器炮弹“的子类;”气球鱼“不会游泳,不能定义为”鱼类“的子类等等……
在生物学上划分鸟类包括了麻雀、燕子、企鹅等。但是从类继承关系上看,”企鹅“不会飞,不能继承分类的”飞行“方法,否则会破坏类的继承关系。
代码演示
错误示范
Step1:定义“鸟类”
//鸟类的父类
public class Bird {
//飞行速度
private double speed;
//飞行速度设置
public void setSpeed(double speed) {
this.speed = speed;
}
/**
* 计算飞行时间
* @param distance 飞行距离
* @return 飞行时间
*/
public double getFlyTime(double distance){
double time = distance / speed;
return time;
}
}
Step2:定义“麻雀类”
2.1 “麻雀类”继承“鸟类”这个父类
// 麻雀类,会继承父类的方法
public class Sparrow extends Bird{
}
2.2 对麻雀类进行测试
@Test
public void testSparrow(){
double distance = 100;//飞行距离
//麻雀飞行
Bird sparrow = new Sparrow();
sparrow.setSpeed(10); // 继承父类方法
double sparrowFlyTime = sparrow.getFlyTime(distance);
System.out.println(sparrowFlyTime);
}
以上测试中,输出该麻雀飞行时间为10.0。运行结果正确。
Step3:定义“企鹅类”
3.1 “企鹅类”继承“鸟类”这个父类并重写父类方法
由于企鹅不会飞,所以对企鹅所继承的父类方法进行重写,设置飞行速度为0。
// 企鹅类,继承鸟类方法后,对飞行速度方法进行了重写,设置了飞行速度为0
public class Penguin extends Bird {
//企鹅不会飞,重写父类方法,设置飞行速度为0
@Override
public void setSpeed(double speed) {
super.setSpeed(0);
}
}
3.2 对企鹅类进行测试
@Test
public void testPenguin(){
double distance = 100;//飞行距离
//企鹅飞行
//由于企鹅不会飞,其子类重写父类方法,即飞行速度为0,导致计算飞行时间时出现Infinity
Bird penguin = new Penguin();
penguin.setSpeed(10); // 已被企鹅的子类重写为速度 = 0
double penguinFlyTime = penguin.getFlyTime(distance);
System.out.println(penguinFlyTime);
}
以上测试中,输出该企鹅飞行时间为Infinity。此时出现:距离100 / 速度0 (数学计算错误)
以上模式存在一个问题:企鹅类虽然是鸟类,但是企鹅不会飞,被迫继承了鸟类的飞行方法。企鹅类对该飞行方法进行了重写,设置了飞行速度为0,导致在计算飞行时间时出现了错误。说明此时企鹅类重写父类方法导致类的继承关系被破坏,即违背了里氏替换原则。
正确示范
对以上方式进行改进。
Step1:定义“动物类”
//动物类
public class Animal {
//所有动物都会吃
public void eat(){
System.out.println("动物会吃饭");
}
}
Step2:定义“鸟类”
// 鸟类,继承动物类
public class Bird extends Animal {
private double speed; //飞行速度
//设置飞行速度
public void setSpeed(double speed) {
this.speed = speed;
}
//计算飞行时间
public double getFlyTime(double distance){
double time = distance / speed;
return time;
}
}
Step3:定义“麻雀类”
// 麻雀类,会继承父类的方法
public class Sparrow extends Bird {
}
Step4:定义“企鹅类”
// 企鹅类,由于没有鸟类的飞行方法,因此不继承鸟类,直接继承动物类
public class Penguin extends Animal {
}
Step5:测试
@Test
public void test(){
double distance = 100;
//麻雀
Bird sparrow = new Sparrow();
sparrow.setSpeed(10); // 继承父类方法
double sparrowFlyTime = sparrow.getFlyTime(distance);
System.out.println(sparrowFlyTime);
sparrow.eat();
//企鹅
//由于企鹅不会飞,直接继承动物类,从而无法调用飞行方法
Animal penguin = new Penguin();
penguin.eat();
}
四、依赖倒置原则
4.1 介绍
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。即要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。简单来说:要面向接口编程,不要面向实现编程。
实现方式:
-
每个类进行提供接口或抽象类,或两者都提供
-
变量的声明类型尽量使用接口或抽象类
-
任何类都不应该从具体类派生
-
继承时注意要尽量遵循里氏替换原则(即不要重写父类方法导致破坏类的继承结构)
4.2 作用
-
降低类间的耦合性
-
提高系统稳定性
-
提高代码可读性及可维护性
4.3 演示
以组装电脑为例。组装一台电脑,需要配件CPU、硬盘、内存条。CPU有Intel、AMD等品牌,硬盘有希捷、西数等品牌,内存条有金士顿、海盗船等品牌。
这种方式导致一个问题,即每次更换配置品牌,如想换成AMD牌的CPU,我需要修改Computer类的代码。
故修改为以下方式:
Step1:定义电脑的各个组件接口
//CPU
public interface Cpu {
void run();
}
//硬盘
public interface HardDisk {
void save();
}
//内存条
public interface Memory {
void save();
}
Step2:定义一台电脑
//电脑需要配件CPU、硬盘、内存条配置。
// CPU有Intel、AMD等品牌,硬盘有希捷、西数等品牌。内存条有金士顿、海盗船等品牌。
public class Computer {
private Cpu cpu; //CPU
private HardDisk hardDisk; //硬盘
private Memory memory; // 内存条
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
public void run(){
System.out.println("=====电脑开机=====");
cpu.run();
memory.save();
hardDisk.save();
}
}
Step3:定义具体的组件类
3.1 定义CPU
// Intel品牌CPU
public class IntelCpu implements Cpu{
public void run() {
System.out.println("使用Intel牌的CPU处理器");
}
}
3.2 定义硬盘
// 希捷品牌硬盘
public class XiJieHardDisk implements HardDisk {
public void save() {
System.out.println("使用希捷硬盘");
}
}
3.3 定义内存条
// 金士顿品牌内存条
public class KingstonMemory implements Memory{
public void save() {
System.out.println("使用金士顿内存条");
}
}
Step4:测试
@Test
public void test(){
//组装计算机
IntelCpu cpu = new IntelCpu(); //CPU
XiJieHardDisk hardDisk = new XiJieHardDisk(); //硬盘
KingstonMemory memory = new KingstonMemory(); //内存
Computer computer = new Computer();
computer.setCpu(cpu);
computer.setHardDisk(hardDisk);
computer.setMemory(memory);
computer.run();
}
优点:
* 新增一个品牌,直接重写配置类
* 此时如果想要换一个配置品牌,只需要重新new一个配置再装配到Computer类中即可
以上方式符合以下实现条件:
- 为每个配置类提供接口
- Computer类变量声明使用接口
- 类又接口进行派生
- 不破坏类的继承结构
五、单一职责原则
5.1 介绍
单一职责原则的核心思想是:一个类或者模块应该只负责完成一个单一的职责或功能。
通俗地说,就是一个类或者模块应该只做一件事情,不涉及其它不相关的功能。
该原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下缺点:
-
一个职责的变化可能会削弱或抑制这个类其他能力
-
客户端需要该对象的某一个职责,被迫将其他不需要的职责都包含进来,造成了代码冗余
-
一个类或模块功能较多,代码杂乱,导致代码的复杂度增加、可读性下降,难以阅读和维护
5.2 作用
这样设计的代码更加清晰、可读性更好,并且易于维护和扩展。每个类或者模块的职责清晰明确,容易理解和维护。
当有需求变更或者功能扩展时,只需要关注相关的类或者模块,而不需要影响其它不相关的功能。
其次,单一职责原则有助于降低代码之间的耦合度,提高代码的灵活性和可测试性。
5.3 演示
公司工作内容主要包括人员管理和项目管理两个方面的工作。人员管理主要包括人员招聘、薪资发放等工作。项目管理主要包括技术培训、开发运营等工作。如果将这些工作交给一个人负责容易造成混乱。正确的做法是人员管理分给人事部门,项目管理分给项目部门。
Step1:定义“公司类”
//公司
public class Company {
private PersonnelDepartment personnel; //人事部门
private ProjectDepartment project; //项目部门
public PersonnelDepartment getPersonnel() {
return personnel;
}
public void setPersonnel(PersonnelDepartment personnel) {
this.personnel = personnel;
}
public ProjectDepartment getProject() {
return project;
}
public void setProject(ProjectDepartment project) {
this.project = project;
}
}
Step2:定义“人事部门”
//人事部门:人员招聘、薪资发放
public class PersonnelDepartment {
public void recruitment(){
System.out.println("人员招聘");
}
public void payroll(){
System.out.println("薪资发放");
}
}
Step3:定义“项目部门”
//项目部门:技术培训、开发运营
public class ProjectDepartment {
public void technicalTraining(){
System.out.println("技术培训");
}
public void developmentAndOperation(){
System.out.println("开发运营");
}
}
Step4:测试
@Test
public void test(){
Company company = new Company();
PersonnelDepartment personnel = new PersonnelDepartment(); //人事部门
ProjectDepartment project = new ProjectDepartment(); //项目部门
company.setPersonnel(personnel);
company.setProject(project);
}
六、接口隔离原则
6.1 介绍
接口隔离原则要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法,不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。简单来说,就是要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是类的职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建
区别:
⚪单一职责原则强调一个类或模块的职责单一,一个类或模块只负责一项功能。
⚪接口隔离原则强调合理定义接口,确保接口粒度适当,不强迫实现类去实现其不需要的接口成员。
⚪两个原则都有助于提高代码的可维护性、可扩展性和可测试性,但侧重点略有不同。
6.2 作用
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下优点:
1. 避免接口臃肿和依赖冗余
接口隔离原则要求将接口拆分成更小的、更具体的接口,每个接口只包含其实现类所需的最小接口成员。这样,实现类就不会被强迫实现其不需要的接口成员,从而避免了接口冗余和依赖关系过于复杂的问题。
2. 降低耦合度
通过精确定义接口,让不同的模块和类之间的依赖关系变得清晰明了。这样能够使得系统更加灵活,易于维护和修改。
3. 容易重构和扩展
遵循接口隔离原则的设计能够更好地支持系统的重构和扩展。当需要增加新的功能或类时,只需要实现其需要的接口,而不必去修改原有的接口和实现类,这样能够减少修改的范围和影响。同时,这也能够使得系统更加模块化,方便模块的组合和重用。
4. 提高代码质量
接口隔离原则能够提高代码的可读性、可维护性和可测试性。实现类只需要实现其需要的接口成员,让代码变得更加简洁明了,并且方便单元测试和代码复用。
6.3 演示
安全门案例:创建一个XX品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。
此时,如果又加了一个YY品牌的安全门,但是YY牌安全门只有防火、防盗功能,如果实现了以上安全门的接口显然不合理,因为YY品牌安全门并没有防水功能,但是被迫接受了该功能。很显然如果实现SafetyDoor接口就违背了接口隔离原则。
因此,可以采用以下方式:
将防火、防水、防盗三个功能拆成三个接口,每个安全门具有什么功能,就实现什么接口。
Step1:定义功能接口
分别定义“防盗”接口、“防火”接口、“防水”接口
/**
* 防盗接口
*/
public interface AntiTheft {
void antiTheft();
}
/**
* 防火接口
*/
public interface Fireproof {
void fireProof();
}
/**
* 防水接口
*/
public interface Waterproof {
void waterProof();
}
Step2:定义不同品牌的安全门
//XX牌防盗门,能够防盗、防火、防水
public class XXDoor implements AntiTheft,Fireproof,Waterproof{
public void antiTheft() {
System.out.println("防盗");
}
public void fireProof() {
System.out.println("防火");
}
public void waterProof() {
System.out.println("防水");
}
}
//YY牌防盗门,能够防盗、防火
public class YYDoor implements AntiTheft,Fireproof{
public void antiTheft() {
System.out.println("防盗");
}
public void fireProof() {
System.out.println("防火");
}
}
Step3:测试
@Test
public void test(){
XXDoor xxDoor = new XXDoor();
xxDoor.antiTheft();
xxDoor.fireProof();
xxDoor.waterProof();
YYDoor yyDoor = new YYDoor();
yyDoor.antiTheft();
yyDoor.fireProof();
}
七、迪米特法则
7.1 介绍
迪米特法则又叫最少知识原则。只和你的直接朋友交谈,不跟“陌生人”说话。
“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
从迪米特法则的定义和特点可知,它强调以下两点:
从依赖者的角度来说,只依赖应该依赖的对象。
从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点:
在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
在类的结构设计上,尽量降低类成员的访问权限。
在类的设计上,优先考虑将一个类设置成不变类。
在对其他类的引用上,将引用其他对象的次数降到最低。
不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
谨慎使用序列化(Serializable)功能。
迪米特法则和单一职责原则的区别:
单一职责原则(Single Responsibility Principle)和迪米特原则(Law of Demeter)是面向对象设计原则中的两个重要原则,它们的区别如下:
单一职责原则:单一职责原则指的是一个类或模块应该有且只有一个责任。它强调一个类或模块应该只负责一件事情,并且只有一个引起它变化的原因。这样做有助于降低类或模块的复杂性、提高可维护性和可重用性。单一职责原则主要关注的是类或模块的内聚性,要求类或模块的功能要高度集中和独立。
迪米特原则:迪米特原则,也称为最少知识原则,强调一个对象应该对其他对象有尽可能少的了解。迪米特原则要求在设计和交互过程中,对象之间应该尽量减少彼此的依赖关系,只与直接的朋友进行通信。直接的朋友指的是当前对象本身、被当作方法参数传入的对象、当前对象所创建的对象以及当前对象的组件对象。迪米特原则主要关注的是类和对象之间的关系,要求减少类之间的耦合度,提高系统的灵活性和扩展性。
总结起来,单一职责原则关注的是类或模块自身的内聚性,强调一个类或模块应该只负责一项职责;而迪米特原则关注的是对象之间的依赖关系,强调一个对象应该尽可能少地了解其他对象。它们都是为了提高软件设计的质量和可维护性,但侧重点和考虑的因素略有不同。
7.2 作用
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点:
降低了类之间的耦合度,提高了模块的相对独立性。
由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
7.3 演示
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会、和媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
Step1:定义“明星类”
// 明星类
public class Star {
private String name;
public Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Step2:定义“粉丝类”
// 粉丝类
public class Fans {
private String name;
public String getName() {
return name;
}
public Fans(String name) {
this.name = name;
}
}
Step3:定义“公司类”
// 公司类
public class Company {
private String name;
public String getName() {
return name;
}
public Company(String name) {
this.name = name;
}
}
Step4:定义“经纪人”类
// 经纪人类
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company;
}
//和粉丝见面的方法
public void meeting() {
System.out.println(star.getName() + "和粉丝" + fans.getName() + "见面");
}
//和媒体公司洽谈的方法
public void business() {
System.out.println(star.getName() + "和" + company.getName() + "洽谈");
}
}
Step5:测试
@Test
public void test(){
//创建经纪人类
Agent agent = new Agent();
//创建明星对象
Star star = new Star("林青霞");
agent.setStar(star);
//创建粉丝对象
Fans fans = new Fans("李四");
agent.setFans(fans);
//创建媒体公司对象
Company company = new Company("黑马媒体公司");
agent.setCompany(company);
agent.meeting();//和粉丝见面
agent.business();//和媒体公司洽谈业务
}
八、合成复用原则
8.1 介绍
通常,类的复用分为继承复用和合成复用两种。
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
合成复用原则又叫组合/聚合复用原则。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
通俗地解释,合成复用原则就像是用积木搭建模型一样。当我们需要创建一个新的模型时,不必从头开始制作每一个构件,而是可以选择现有的积木来组合。每个积木都有自己的特性和功能,我们可以根据需求选择适合的积木进行组合,从而形成新的模型。这样做的好处是,我们可以灵活地搭建出各种不同的模型,而无需重复造轮子。
在软件设计中,同样的道理适用。合成复用原则告诉我们,当我们需要创建一个新的对象时,可以利用现有的对象来构建,而不是通过继承关系来实现。通过合成复用,我们可以将现有对象的不同部分组合起来,形成新的对象,达到代码复用的目的。这样做有助于降低耦合度,增加系统的灵活性和可维护性。
8.2 作用
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
8.3 演示
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。
从上图可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如下图所示。
Step1:定义“颜色接口”
/**
* 颜色接口
*/
public interface Color {
String tintage();//涂色
}
Step2:定义具体颜色类
/**
* 白色车漆
*/
public class White implements Color {
@Override
public String tintage() {
return "白色";
}
}
/**
* 红色车漆
*/
public class Red implements Color{
@Override
public String tintage() {
return "红色";
}
}
/**
* 黑色车漆
*/
public class Black implements Color{
@Override
public String tintage() {
return "黑色";
}
}
Step3:定义“汽车父类”
public abstract class Car {
protected Color color;
public Car(Color color) {
this.color = color;
}
public abstract void move();
}
Step4:定义具体汽车类
/**
* 汽油汽车
*/
public class GasolineCar extends Car {
public GasolineCar(Color color) {
super(color);
}
@Override
public void move() {
System.out.println(color.tintage() + "汽油汽车在行驶");
}
}
/**
* 电动汽车
*/
public class ElectricCar extends Car {
public ElectricCar(Color color) {
super(color);
}
@Override
public void move() {
System.out.println(color.tintage() + "电动汽车在行驶");
}
}
Step5:测试
@Test
public void test(){
Car gasolineCar = new GasolineCar(new White());
gasolineCar.move();
Car electricCar = new ElectricCar(new Red());
electricCar.move();
}