设计模式七大原则
一、设计模式介绍
设计模式是针对软件存在问题而提出的解决方案,设计模式的本质是面向对象设计原则的实际运用,是对类与类关系的充分理解。
二、类图
(一)、类图概述
类图是描述系统中的类及类与类之间关系的静态视图,通过分析类图能够让我们在编码之前对系统中的类、接口和它们之间的协作关系有一个充分了解。
(二)、类的表示方式
通过类名、属性和方法且带有分割线的矩形来表示一个类
属性的完整表示方式是: 可见性+名称 :类型 +[ = 缺省值]
方法的完整表示方式是: 可见性 +名称(参数列表) +[ : 返回类型]
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有四种:
注意:中括号中的内容表示是可选的,也有将类型放在变量名前面,返回值类型放在方法名前面
限定修饰符 | 意义 |
---|---|
+ | 表示public |
- | 表示private |
# | 表示protected |
表示default |
例:有个员工实体类Employee
类名:Employee
属性:name,age和address这3个,
方法:work
(三)、类与类之间的多种关系(必要的多种耦合)
1、关联关系(引用关系)
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,根据联系的程度以及各个联系的成分之间的特点,又分为一般关联关系、聚合关系和组合关系。
(1)、一般关联关系
①单向关联
定义:类A中引用了类B作为成员变量,而类B没有引用类A作为成员变量,即引用关系是单向的
画法:单向关联用一个带箭头的实线表示,箭头头部表示被引用类。
下图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。
②双向关联
定义:类A中引用了类B作为成员变量,而类B也引用了类A作为成员变量,即双方各自持有对方类型的成员变量
画法:双向关联用一个不带箭头的直线表示。
下图中在Customer类中维护一个List<Product>,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。
③自关联
定义:类A包含自身类的成员变量,即自身引用自身
画法:自关联用一个带有箭头且指向自身的线表示。
下图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。
(2)、聚合关系
定义:聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
画法:聚合关系可以用带空心菱形的实线来表示,菱形所在一端为整体。
下图所示是大学和教师的关系图:
(3)、组合关系
定义:组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在*,部分对象不能脱离整体对象而存在*。例如,头和嘴的关系,没有了头,嘴也就不存在了。
画图:组合关系用带实心菱形的实线来表示,菱形所在一端为整体。
下图所示是头和嘴的关系图:
2、依赖关系(使用关系)
定义:依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
画法:在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。
下图所示是司机和汽车的关系图,司机驾驶汽车:
3、继承关系(泛化关系)
定义:继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
画法:泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。
例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:
4、实现关系
定义:实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
画法:实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。
例如,汽车和船实现了交通工具,其类图如图 9 所示。
5、总结各大关系
类之间的关系 | 关系的实现 |
---|---|
关联关系(引用关系) | 关联关系是通过将其他类作为成员变量来实现的 |
依赖关系(使用关系) | 依赖关系是通过使用其他类来实现的 |
继承关系(泛化关系) | 是通过子类继承父类的属性和方法实现 |
实现关系 | 是通过实现类实现接口类实现的 |
(四)、不必要的耦合
不必要的耦合是指,除了上面必要的多种耦合关系外,其它的耦合关系。比如一个类创建了没有非必要耦合关系类的对象并作为局部变量使用,这会导致该类增加不必要的耦合度。详情见迪米特法则
三、设计模式七大原则
(一)、开闭原则(Open-Close Principle)
1、什么是开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
2、如何实现开闭原则
想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
3、开闭原则案例
(1)、案例描述
下面以 搜狗输入法的皮肤为例介绍开闭原则的应用。
分析:搜狗输入法的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,将其共同特点抽取出来可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin和HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。
(2)、代码演示
①、搜狗输入法类
搜狗输入法有皮肤,搜狗输入法与它的皮肤构成整体和部分的关系,但是皮肤类可以脱离搜狗输入法单独存在,所以搜狗输入法与它的皮肤是聚合关系
public class SouGouInput {
// 聚合皮肤类
private AbstractSkin skin;
// 展示皮肤
public void display(){
// 调用皮肤的展示方法,不同皮肤有不同展示方式
skin.display();
}
}
②、抽象皮肤类
抽象皮肤类,是对皮肤共同特征的抽取而创建的,屏蔽了细节,而让具体类去实现细节,当需要扩展皮肤时,仅仅需要再添加一个派生类就可,这样就实现了对扩展开放对修改关闭
public abstract class AbstractSkin {
public abstract void display();
}
③、经典皮肤类
继承于抽象皮肤类
public class ClassicSkin extends AbstractSkin{
@Override
public void display() {
System.out.println("展示经典皮肤");
}
}
④、清新皮肤类
继承于抽象皮肤类
public class FreshSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("展示清新的皮肤");
}
}
⑤、用户类
模拟用户使用皮肤
public class Client {
// 用户使用皮肤模拟
public static void main(String[] args) {
// 创建搜狗
SouGouInput souGouInput = new SouGouInput();
// 设置清晰款皮肤
souGouInput.setSkin(new FreshSkin());
// 展示皮肤
souGouInput.display();
// 设置为经典款皮肤
souGouInput.setSkin(new ClassicSkin());
// 展示皮肤
souGouInput.display();
}
}
(二)、里氏代换原则(Liskov Substitution Principle)
1、什么是里氏代换原则
任何基类可以出现的地方,子类一定可以出现。
通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法(除了抽象方法以外)。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
2、非里氏代换案例
(1)、案例描述
【例】正方形不是长方形。
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。
类图如下:
(2)、代码演示
①、长方形类RecTangle
public class RecTangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
②、正方形类Square
正方形类继承长方形类,并重写set方法
public class Square extends RecTangle{
@Override
public void setLength(double length) {
// 调用父类的setter方法将长和宽设置为一样高
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
}
③、测试类RectangleDemo
public class RectangleDemo {
public static void main(String[] args) {
// 创建一个长方形
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setLength(30);
resize(rectangle);
printLengthAndWidth(rectangle);
// 创建一个正方形
Square square = new Square();
square.setWidth(20);
square.setLength(30);
resize(square);
printLengthAndWidth(square);
}
// 让宽不断增长大于长
public static void resize(Rectangle r){
while (r.getWidth()<= r.getLength()){
r.setWidth(r.getWidth()+1);
}
}
// 打印长宽
public static void printLengthAndWidth(Rectangle r) {
System.out.println(r.getLength());
System.out.println(r.getWidth());
}
}
(3)、运行结果
30.0
31.0
(4)、问题分析
问题:我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
(5)、得出结论
结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。
3、里氏代换案例(对以上案例改进)
(1)、改进方案
如何改进呢?
由于Square类和Rectangle类之间的继承关系违反了里氏代换原则,因此不应该再使用继承方式
此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口,这样他们就并不是继承关系了
类图如下:
(2)、代码演示
① 、四边形接口
这个接口定义了获取长度宽度的方法
public interface Quadilateral {
// 获取长度
void getLength();
// 获取宽度
void getWidth();
}
②、正方形类
正方形类实现了四边形接口
public class Square implements Quadilateral{
// 正方形边长
private double side;
public void setSide(double side) {
this.side = side;
}
@Override
public double getLength() {
return side;
}
@Override
public double getWidth() {
return side;
}
}
③、长方形类
长方形类实现了四边形接口
public class Rectangle implements Quadilateral{
// 长方形长
private double length;
// 长方形宽
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() {
return length;
}
@Override
public double getWidth() {
return width;
}
}
④、测试类
public class RectangleDemo {
public static void main(String[] args) {
// 创建一个长方形
Rectangle rectangle = new Rectangle();
rectangle.setLength(30);
rectangle.setWidth(20);
resize(rectangle);
printLengthAndWidth(rectangle);
// 创建一个正方形
Square square = new Square();
square.setSide(20);
printLengthAndWidth(square);
}
// 让宽不断增长大于长
public static void resize(Rectangle r){
while (r.getWidth()<= r.getLength()){
r.setWidth(r.getWidth()+1);
}
}
// 打印长宽
public static void printLengthAndWidth(Quadilateral q) {
System.out.println(q.getLength());
System.out.println(q.getWidth());
}
}
4、里氏代换原则的启示
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖 来解决问题。
(三)、依赖倒转原则(Dependency Inversion Principle)
1、什么是依赖倒转原则
定义:将对具体的依赖转变为对抽象的依赖,即依赖尽量抽象化
原理:相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多,且扩展性更强。
原则:
- 高层模块不应该依赖低层模块
- 抽象不应该依赖细节,细节应该依赖抽象
在java中,抽象指的是接口或抽象类,细节就是具体的实现类,使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
依赖倒转是对开闭原则的很好的应用
2、非依赖倒转案例
(1)、案例描述
现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。
类图如下:
(2)、代码实现
①、金士顿内存条类
public class KingstonMemory {
public void run(){
System.out.println("金士顿内存条正在运行");
}
}
②、因特尔cpu类
public class IntelCpu {
public void run(){
System.out.println("使用intelcpu");
}
}
③、希捷硬盘类
public class XiJIeHardDisk {
public void run(){
System.out.println("希捷硬盘正在运行");
}
}
④、计算机类
Computer类通过使用KingstonMemory类,IntelCpu类,XiJIeHardDisk类的运行,实现了整个计算机的运行
Computer类与KingstonMemory类,IntelCpu类,XiJIeHardDisk类是依赖关系
public class Computer {
// 依赖于细节
public void run(KingstonMemory memory, IntelCpu cpu, XiJIeHardDisk disk) {
System.out.println("计算机开启");
memory.run();
cpu.run();
disk.run();
}
}
(3)、代码测试
public class ComputerDemo {
public static void main(String[] args) {
XiJIeHardDisk disk = new XiJIeHardDisk();
IntelCpu cpu = new IntelCpu();
KingstonMemory memory = new KingstonMemory();
// 创建计算机对象
Computer computer = new Computer();
// 启动计算机
computer.run(memory,cpu,disk);
}
}
(4)、运行结果
计算机开启
金士顿内存条正在运行
使用intelcpu
希捷硬盘正在运行
(5)、问题分析
计算机所依赖的是具体的类,这使得抽象依赖了具体,不满足依赖倒转原则
该电脑只能够运行因特尔的cpu和金士顿的内存以及希捷的磁盘,当用户想要使用其它品牌的硬件时只能更换电脑,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。
3、依赖倒转案例(对上面方案的改进)
(1)、改进方案
抽取KingstonMemory类,InterCpu类,XiJIeHardDisk类的共同特征创建Cpu、Memory和Disk抽象类,将Computer类对具体的KingstonMemory类,IntelCpu类,XiJIeHardDisk类的依赖,转化为对抽象类Cpu、Memory和Disk的依赖,这样就满足细节依赖于抽象,符合依赖倒转
类图如下:
(2)、代码演示
①、抽象Cpu类
public abstract class Cpu {
public abstract void run();
}
②、抽象Memory类
public abstract class Memory {
public abstract void run();
}
③、抽象Disk类
public abstract class Disk {
public abstract void run();
}
④、Computer类
public class Computer {
public void run(Memory memory, Cpu cpu, Disk disk) {
System.out.println("计算机开启");
memory.run();
cpu.run();
disk.run();
}
}
4、依赖倒转给我们的启示
- 低层模块(被引用的类)尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 面向抽象和接口编程,避免面向具体编程
(四)、接口隔离原则(Interface Segregation Principle)
1、什么是接口隔离
一个类不应该被迫依赖了(使用)它不使用的方法,即一个类对另一个类的依赖应该建立在最小的接口上。
2、非接口隔离案例
(1)、案例描述
类A需要用到方法dosome1 和 dosome2并通过接口DoSome依赖类C。
类B需要用到方法dosome2 和 dosome3并通过接口DoSome依赖类D。
类图如下:
(2)、代码实现
①、A类
public class Aclass {
public void run(DoSome doSome) {
doSome.dosome2();
doSome.dosome3();
}
}
②、B类
public class Bclass {
public void run(DoSome doSome) {
doSome.dosome1();
doSome.dosome2();
}
}
③、C类
public class Cclass implements DoSome {
@Override
public void dosome1() {
System.out.println("C类实现了dosome1");
}
@Override
public void dosome2() {
System.out.println("C类实现了dosome2");
}
@Override
public void dosome3() {
System.out.println("C类实现了dosome3");
}
}
④、D类
public class Dclass implements DoSome {
@Override
public void dosome1() {
System.out.println("D类实现了dosome1");
}
@Override
public void dosome2() {
System.out.println("D类实现了dosome2");
}
@Override
public void dosome3() {
System.out.println("D类实现了dosome3");
}
}
⑤、DoSome接口
public interface DoSome {
void dosome1();
void dosome2();
void dosome3();
}
(3)、代码测试
public class IspTest {
public static void main(String[] args) {
Aclass aclass = new Aclass();
Bclass bclass = new Bclass();
aclass.run(new Cclass());
bclass.run(new Dclass());
}
}
(4)、运行结果
C类实现了dosome2
C类实现了dosome3
D类实现了dosome1
D类实现了dosome2
(5)、问题分析
类A通过接口DoSome接口依赖于类C,但是此时类A用的到方法dosome1 和 dosome2
类B通过接口DoSome接口依赖于类D,但是此时类B只用得到方法dosome2,dosome3
类A类B都被迫依赖了他们不用的方法
类A被迫依赖dosome3方法
类B被迫依赖dosome1方法
一个类被迫依赖了(使用)它不使用的方法,即一个类对另一个类的依赖没有建立在最小的接口上,所以违背了接口隔离原则。
3、接口隔离案例(对上面案例的改进)
(1)、改进方案
将接口DoSome拆分为独立的粒度适当的几个接口
类图如下:
(2)、代码演示
①、接口DoSome1,2,3
public interface DoSome1 {
void dosome1();
}
public interface DoSome2 {
void dosome2();
}
public interface DoSome3 {
void dosome3();
}
②、C类
public class Cclass implements DoSome1, DoSome2 {
@Override
public void dosome1() {
System.out.println("C类实现了dosome1");
}
@Override
public void dosome2() {
System.out.println("C类实现了dosome2");
}
}
③、D类
public class Dclass implements DoSome2, DoSome3 {
@Override
public void dosome2() {
System.out.println("D类实现了dosome2");
}
@Override
public void dosome3() {
System.out.println("D类实现了dosome3");
}
}
④、A类
public class Aclass {
public void run1(DoSome1 doSome1) {
doSome1.dosome1();
}
public void run2(DoSome2 doSome2) {
doSome2.dosome2();
}
}
⑤、B类
public class Bclass {
public void run2(DoSome2 doSome2) {
doSome2.dosome2();
}
public void run3(DoSome3 doSome3) {
doSome3.dosome3();
}
}
(五)、迪米特法则(Law of Demeter)
1、什么是迪米特法则
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。从依赖的角度来说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部,对外除了提供的public 方法,不对外泄露任何信息。对于依赖者来说只依赖应该依赖的对象。
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
说白了就是要将系统中类与类的耦合度降到最低
迪米特法则还有几种定义形式,包括:*不要和“陌生人”说话、只与你的直接朋友(有耦合关系的类)通信等,在迪米特法则中,对于一个对象,其朋友*包括以下几类:
- 当前对象本身(this)(关联关系);
- 以参数形式传入到当前对象方法中的对象(依赖关系);
- 当前对象的成员对象(关联关系);
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友(关联关系);
- 当前对象所创建的非必要耦合关系类的对象。(非直接朋友)
说白了除了直接朋友其它全是陌生人,类中尽量减少陌生人
2、非迪米特法则案例
(1)、案例描述
有一个学校,下属有各个学院,现要求打印出学校员工id和学院员工的id。
(2)、代码实现
①、学校总部员工类
public class CollegeEmployee {
private Long cId;
public Long getcId() {
return cId;
}
public void setcId(Long cId) {
this.cId = cId;
}
}
②、学院员工类
public class AcademicEmployee {
private Long aId;
public Long getaId() {
return aId;
}
public void setaId(Long aId) {
this.aId = aId;
}
}
③、学院管理者类
能够返回全部的学院的员工列表
public class AcademicManager {
private List<AcademicEmployee> employees = new LinkedList<>();
// 返回学院所有员工
public List<AcademicEmployee> getAcademicEmployee() {
for (Long i = 1L; i <= 5L; i++) {
AcademicEmployee employee = new AcademicEmployee();
employee.setaId(1L);
employees.add(employee);
}
return employees;
}
}
④、学校管理者类
能够返回全部学校员工
能够打印全部学校总部员工和学院员工
public class CollegeManager {
private List<CollegeEmployee> employees = new LinkedList<>();
// 返回学校总部所有员工
public List<CollegeEmployee> getCollegeEmployee() {
for (Long i = 1L; i <= 5L; i++) {
CollegeEmployee employee = new CollegeEmployee();
employee.setcId(i);
employees.add(employee);
}
return employees;
}
// 打印所有员工
public void printAllEmployees(AcademicManager academicManager) {
System.out.println("学校总部员工");
// 打印学校员工
this.getCollegeEmployee().forEach(System.out::println);
System.out.println("学院员工");
// 打印学院员工
academicManager.getAcademicEmployee().forEach(System.out::println);
}
}
(3)、代码测试
public class IodDemo {
public static void main(String[] args) {
// 创建学校管理者
CollegeManager collegeManager = new CollegeManager();
// 打印全体员工
collegeManager.printAllEmployees(new AcademicManager());
}
}
(4)、运行结果
学校总部员工
com.bloom.lod.CollegeEmployee@34c45dca
com.bloom.lod.CollegeEmployee@52cc8049
com.bloom.lod.CollegeEmployee@5b6f7412
com.bloom.lod.CollegeEmployee@27973e9b
com.bloom.lod.CollegeEmployee@312b1dae
学院员工
com.bloom.lod.AcademicEmployee@3941a79c
com.bloom.lod.AcademicEmployee@506e1b77
com.bloom.lod.AcademicEmployee@4fca772d
com.bloom.lod.AcademicEmployee@9807454
com.bloom.lod.AcademicEmployee@3d494fbfProcess finished with exit code 0
(5)、问题分析
在CollegeManager类中,直接朋友有:CollegeEmployee类、AcademicManager类
然而AcademicEmployee类不是直接朋友却以局部变量的形式出现在了CollegeManager中
所以在CollegeManager类中出现了“陌生人”,不满足迪米特法则
即CollegeManager类与AcademicEmployee类之间产生了不必要的耦合,CollegeManager类与其它类的耦合度之间没有达到最低,应该减少这个不必要的耦合,并且AcademicManager类的成员变量不应该可以被其它类直接访问,说明AcademicManager类的封装性不够好,应该对打印方法进一步封装。
3、迪米特法则案例(对上面案例的改进)
(1)、改进方案
由于CollegeManager类与AcademicEmployee类不是真正的朋友,所以在CollegeManager类中不应该出现AcademicEmployee类对象,所以应该对AcademicManager打印方法进行封装,CollegeManager直接调用即可
(2)、代码演示
①、学院管理类
封装了打印员工的方法
public class AcademicManager {
private List<AcademicEmployee> employees = new LinkedList<>();
// 返回学院所有员工
public List<AcademicEmployee> getAcademicEmployee() {
for (Long i = 1L; i <= 5L; i++) {
AcademicEmployee employee = new AcademicEmployee();
employee.setaId(1L);
employees.add(employee);
}
return employees;
}
// 打印学校员工
public void printAllEmployees() {
this.getAcademicEmployee().forEach(System.out::println);
}
}
②、学校管理类
不用再自己遍历打印学员员工,直接调用AcademicManager类提供的printAllEmployees()方法即可打印学院员工
// 打印所有员工
public void printAllEmployees(AcademicManager academicManager) {
System.out.println("学校总部员工");
// 打印学校员工
this.getCollegeEmployee().forEach(System.out::println);
System.out.println("学院员工");
// 打印学院员工
academicManager.printAllEmployees();
}
4、迪米特法则给我们的启示
- 迪米特法则的核心是将类与类之间的耦合降到最低,减少不必要的耦合关系。
- 在一个类中要与直接朋友通信,而尽量减少与陌生人通信,,即避免出现将非耦合(非直接朋友)的类创建为局部变量进行使用,这会增加不必要的耦合。
注意:由于每个类都减少了不必要的耦合,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系
(六)、合成复用原则(Composite Reuse Principle)
1、什么是合成复用原则
尽量先使用组合或者聚合等关联关系来实现,而不是使用继承。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为*“白箱”复用*。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用合成复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
- 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为*“黑箱”复用*。
- 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
2、非合成复用案例
(1)、案例描述
【例】汽车分类管理程序
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。
类图如下:
(2)、代码实现
①、汽车类
汽车类是一个抽象类,具有运行的方法
public abstract class Car {
public abstract void move();
}
②、电力汽车类/柴油汽车类
继承自汽车类,并重写了移动方法
public abstract class ElectricCar extends Car {
@Override
public void move() {
System.out.println("电动汽车在运行");
}
}
public abstract class PetrolCar extends Car {
@Override
public void move() {
System.out.println("柴油汽车在运行");
}
}
③、红色/白色的电力汽车类
public class RedElectricCar extends ElectricCar {
private final static String COLOR = "RED";
@Override
public void move() {
System.out.println(COLOR + "的");
super.move();
}
}
public class WhiteElectricCar extends ElectricCar {
private final static String COLOR = "White";
@Override
public void move() {
System.out.println(COLOR + "的");
super.move();
}
}
④、红色/白色的柴油汽车类
public class RedPetrolCar extends PetrolCar {
private final static String COLOR = "RED";
@Override
public void move() {
System.out.println(COLOR + "的");
super.move();
}
}
public class WhitePetrolCar extends PetrolCar {
private final static String COLOR = "White";
@Override
public void move() {
System.out.println(COLOR + "的");
super.move();
}
}
(3)、代码测试
public class CrpDemo {
public static void main(String[] args) {
// 红色柴油汽车
Car petrolCar = new RedPetrolCar();
petrolCar.move();
// 红色柴油汽车
Car electricCar = new RedElectricCar();
electricCar.move();
}
}
(4)、运行结果
RED的
柴油汽车在运行
RED的
电动汽车在运行
(5)、问题分析
从上面类图和代码我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类,这回导致类的数量急剧增加,让工程变得臃肿。同时父类与子类的耦合度很高
3、合成复用案例(对上面案例的改进)
(1)、改进方案
将颜色单独抽取出来作为一个接口,不同颜色实现这个接口,汽车与颜色就构成了整体与部分的聚合关系,这样就形成了动力源车与颜色的组合,不用再创建多个颜色的不同动力源的车,大大减少了类的数量
(2)、代码演示
①、颜色接口类
public interface Color {
void show();
}
②、具体颜色(蓝色/红色)类
实现了颜色类
public class BlueColor implements Color {
private final static String COLOR = "BLUE";
@Override
public void show() {
System.out.println(COLOR);
}
}
public class RedColor implements Color {
private final static String COLOR = "RED";
@Override
public void show() {
System.out.println(COLOR);
}
}
③、汽车抽象类
实现了对颜色类的聚合
public abstract class Car {
protected Color color;
public Car(Color color) {
this.color = color;
}
public abstract void move();
}
④、具体动力源汽车类
public class PetrolCar extends Car {
public PetrolCar(Color color) {
super(color);
}
@Override
public void move() {
color.show();
System.out.println("的柴油车正在运行");
}
}
public class ElectricCar extends Car {
public ElectricCar(Color color) {
super(color);
}
@Override
public void move() {
color.show();
System.out.println("的电车正在运行");
}
}
4、合成复用原则给我们的启示
尽量采用合成复用减少继承
(七)、单一职责原则(Single responsibility principle)
就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。 每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。