我看了关于Gof23种设计模式的视频:(设计模式)。
有个问题:其中提到了OOP七大原则,都太过于陌生。
我查了资料,有这些说法:
设计原则之合成复用原则(含UML类图的完整例子)_atu1111的博客-CSDN博客_合成复用原则
设计模式七大原则——迪米特原则_张起灵-小哥的博客-CSDN博客
根据我的参看文章,我把他们做了汇总:
1.开闭原则
开闭原则是七大设计原则中最常见、最基本的,在spring的静态代理模块就有用到。
定义:软件实体对扩展是开放的,但对修改是关闭的。意思就是说在不修改软件实体的基础上去扩展其他功能。
问题的由来:在软件的生命周期的,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧的代码引入错误,也可能还是我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
解决办法: 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
有人总结,开闭原则想表达的就是,用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
为什么要遵循开闭原则?
1、只要是面向对象的编程,在开发过程中都会强调开闭原则
2、是最基础的设计原则,其他五个设计原则都是开闭原则的具体形态
3、可以提高代码的复用性
4、可以提高代码的可维护性
参考自https://www.cnblogs.com/az4215/p/11489712.html
2.单一职责原则
定义:如果一个类具有多个职责,应该把这多个职责分离出去,再分别创建一些类去一一完成这些职责
换句话说就是一个类的职责要单一,不能将太多的职责放在一个类中。
核心:高内聚、低耦合。
现状:在实际情况很难去做到单一职责原则,因为随着业务的不断变更,类的职责也在发生着变化,即职责扩散。如类A完成职责P的功能,但是随着后期业务细化,职责P分解成更小粒度的P1与P2,这时根据单一职责原则则需要拆分类A以分别满足细分后的职责P1和P2。但是实际开发环节,若类的逻辑足够简单,可以在代码上级别上违背单一职责原则;若类的方法足够少,可以在方法级别上违背单一职责原则。
优点:
1、降低类的功能复杂度
2、提高系统的可维护性
3、变更风险低
参考自https://www.cnblogs.com/az4215/p/11462818.html
3.里氏替换原则
是继承复用的基石,说白了就是继承与派生的规则。
核心:软件系统中,一个可以接受父类对象的地方必然可以接受子类对象。
注意:里氏替换原则是实现开闭原则的重要方法之一。
里氏替换至少包含一下两个含义:
1、里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
2、如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
优点:可以大大减少程序的bug以及增强代码的可读性。
4.依赖倒置原则
依赖倒置也叫依赖注入、依赖倒转
要针对抽象层编程,不要针对具体类编程
依赖倒置原则核心:要依赖于抽象,不要依赖于具体的实现。
分开来说:(注:抽象:接口或抽象类;细节:具体实现;如果把模块层次关系比作基础关系的话:高层模块和底层模块对应于父类和子类)
1、高层模块不应该依赖底层模块,二者都应该依赖抽象。
2、抽象不应该依赖细节,细节应该依赖抽象。
3、依赖倒置的中心思想是面向接口编程。
4、依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。
5、使用接口或抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类来完成。
依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。我们在项目中使用这个原则要遵循下面的规则:
1、每个类尽量都有接口或者抽象类,或者抽象类和接口两都具备
2、变量的表面类型尽量是接口或者抽象类
3、任何类都不应该从具体类派生
4、尽量不要覆写基类的方法
5、如果基类是一个抽象类,而这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会有一定的影响。
5.接口隔离原则
客户端不应该被迫依赖于它不使用的方法
一个类对另一个类的依赖应该建立在最小的接口上
接口隔离原则(Interface Segregation Principle,ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
接口隔离是为了高内聚、低耦合。在实际的业务开发中,通常会先定义好需要开发的接口,并由各个服务类实现。
但如果没有经过考虑和设计,就很可能造成一个接口中包括众多的接口方法,而这些接口并不一定在每一个类中都需要实现。这样的接口很难维护,也不易于扩展,每一次修改验证都有潜在的风险
在具体应用接口隔离原则时,应该根据以下几个规则衡量:
(1)接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
(2)为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
(3)了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同,要深入了解业务逻辑。
(4)提高内聚,减少对外交互。让接口用最少的方法完成最多的事情。
二、示例
《王者荣耀》里有很多英雄,可以分为射手、战士、刺客等,每个英雄有三种技能。
这些技能该如何定义,让每个英雄实现相应的技能效果呢?接下来就分别使用两种不同的方式实现,来体现设计原则的应用。
首先定义一个技能接口,实现的英雄都需要实现这个接口,进而实现自己的技能。
public interface ISkill {
// 射箭
void doArchery();
// 隐袭
void doInvisble();
// 沉默
void doSilent();
// 眩晕
void doVertigo();
}
这里提供了四个技能的接口,包括射箭、隐袭、沉默、眩晕,每个英雄都实现这个接口。
接下来实现两个英雄:后羿和廉颇。
当然,这里为了说明问题进行了简化,英雄技能只有三个,与真实游戏中有所差别。
英雄后裔:在英雄后羿的类中,实现了三个技能,最后一个眩晕的技能是不需要实现的。
public class HeroHouYi implements ISkill{
@Override
public void doArchery() {
System.out.println("后羿的灼日之矢");
}
@Override
public void doInvisble() {
System.out.println("后羿的隐身技能");
}
@Override
public void doSilent() {
System.out.println("后羿的沉默技能");
}
@Override
public void doVertigo() {
// 无此技能的实现
}
}
英雄廉颇:在英雄廉颇的类中,同样只实现了三个技能,有一个射箭的技能没有实现。
public class HeroLianpo implements ISkill {
@Override
public void doArchery() {
// 无此技能的实现
}
@Override
public void doInvisble() {
System.out.println("廉颇的隐身技能");
}
@Override
public void doSilent() {
System.out.println("廉颇的沉默技能");
}
@Override
public void doVertigo() {
System.out.println("廉颇的眩晕技能");
}
}
每个英雄的实现类里都有一个和自己无关的接口实现类,非常不符合设计模式,也不易于维护。
因为不仅无法控制外部的调用,还需要维护对应的文档,来说明这个接口不需要实现。
如果有更多这样的接口,就会变得非常麻烦。
三、问题改进
按照接口隔离原则的约定,应该在确保合理的情况下,把接口细分。保证一个松散的结构,也就是把技能拆分出来,每个英雄都可以按需继承实现。
接下来分别定义四个技能接口,包括射箭(ISkillArchery)、隐身(ISkillInvisible)、沉默(ISkillSilent)、眩晕(ISkillVertigo),如下所示。
public interface ISkillArchery {
// 射箭
void doArchery();
}
1
2
3
4
public interface ISkillInvisble {
// 隐袭
void doInvisble();
}
1
2
3
4
public interface ISkillSilent {
// 沉默
void doSilent();
}
1
2
3
4
public interface ISkillVertigo {
// 眩晕
void doVertigo();
}
有了四个技能细分的接口,英雄的类就可以自由地组合了。
英雄后羿的实现:
public class HeroHouYi implements ISkillArchery, ISkillArchery, ISkillSilent{
@Override
public void doArchery() {
System.out.println("后羿的灼日之矢");
}
@Override
public void doInvisble() {
System.out.println("后羿的隐身技能");
}
@Override
public void doSilent() {
System.out.println("后羿的沉默技能");
}
}
英雄廉颇的实现:
public class HeroLianpo implements ISkillArchery, ISkillSilent, ISkillVertigo {
@Override
public void doInvisble() {
System.out.println("廉颇的隐身技能");
}
@Override
public void doSilent() {
System.out.println("廉颇的沉默技能");
}
@Override
public void doVertigo() {
System.out.println("廉颇的眩晕技能");
}
}
现在可以看到这两个英雄的类都按需实现了自己需要的技能接口。
这样的实现方式就可以避免一些本身不属于自己的技能还需要不断地用文档的方式进行维护,同时提高了代码的可靠性,在别人接手或者修改时,可以降低开发成本和维护风险。
6.迪米特法则
1.什么是迪米特原则?
一个对象应该对其他对象保持最少的了解。
类与类关系越密切,耦合度越大。
迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息。
迪米特法则还有个更简单的定义:只与直接的朋友通信。
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
2.违反迪米特原则代码案例
代码中的注释我已经写的很详细了,大家有问题即可指出。
package com.szh.principle.demeter;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
//学校总部员工类
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<>();
for (int i = 1; i <= 5; i++) { //这里我们增加了5个员工到 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<>();
for (int i = 1; i <=3 ; i++) { //这里我们增加了3个员工到 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("------------学院员工------------");
list1.stream().map(CollegeEmployee::getId).forEach(System.out::println);
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
list2.stream().map(Employee::getId).forEach(System.out::println);
}
}
public class Demeter {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
3.遵守迪米特原则代码案例
前面设计的问题在于SchoolManager 中,CollegeEmployee 类并不是SchoolManager类的直接朋友。
按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。我们将上面的代码进行修改,使其遵守迪米特原则。
package com.szh.principle.demeter.improve;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
//学校总部员工类
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<>();
for (int i = 1; i <= 5; i++) { //这里我们增加了5个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
list1.stream().map(CollegeEmployee::getId).forEach(System.out::println);
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些: Employee、CollegeManager
//CollegeEmployee 不是 直接朋友, 而是一个陌生类, 这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<>();
for (int i = 1; i <= 3; i++) { //这里我们增加了3个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//改进代码:将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = getAllEmployee();
System.out.println("------------学校总部员工------------");
list2.stream().map(Employee::getId).forEach(System.out::println);
}
}
public class Demeter {
public static void main(String[] args) {
System.out.println("~~~使用迪米特法则的改进~~~");
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
4.迪米特原则的注意事项
迪米特法则的核心是降低类之间的耦合。
但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。
7.合成复用原则
1.1 定义
合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
合成复用原则的重要性
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
1).继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
2).子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
3).它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
1).它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
2).新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
3).复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
1.2 UML类图
(图1 未遵循合成复用原则的设计)
(图2 遵循合成复用原则的设计)
1.3 问题由来
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就有6种。将上述业务用代码实现,如果只用继承复用,那么最后有6个子类白色油车、黑色油车、红色油车、白色电动汽车、黑色电动汽车、红色电动汽车。这种情况实现方式,会导致子类过多的情况出现。 并且当增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。
public abstract class Car
{
abstract void run();
}
public class ElectricCar extends Car
{
@Override
void run()
{
System.out.println("电动汽车");
}}
public class PetrolCar extends Car {
@Override
void run()
{
System.out.println("汽油汽车");
}
}
public class BlackElectricCar extends ElectricCar
{
public void appearance()
{
System.out.print("黑色");
super.run();
}
}
public class BlackPetrolCar extends PetrolCar
{
public void appearance()
{
System.out.print("黑色");
super.run();
}
}
public class RedElectricCar extends ElectricCar
{
public void appearance()
{
System.out.print("红色");
super.run();
}
}
public class RedPetrolCar extends PetrolCar
{
public void appearance()
{
System.out.print("红色");
super.run();
}
}
public class WhiteElectricCar extends ElectricCar
{
public void appearance()
{
System.out.print("白色");
super.run();
}
}
public class WhitePetrolCar extends PetrolCar
{
public void appearance()
{
System.out.print("白色");
super.run();
}
}
public class Test {
public static void main(String[] args) {
RedElectricCar redElectricCar = new RedElectricCar();
redElectricCar.appearance();//红色电动汽车
}
}
1.4 实现思路
采用组合或聚合复用方式,第一步先将将颜色Color抽象为接口,有白色,黑色,红色三个颜色实现类,第二步将Color对象组合在汽车Car类中,最终我们只需要生成5个类,就可以实现上诉功能。同时当增加新的“动力源”或者增加新的“颜色,都不要修改源代码,只要增加实现类就可以。
1.5 解决方案
代码修改如下:
public abstract class Car
{
abstract void run();
Color color;
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
public interface Color
{
void colorKind();
}
public class ElectricCar extends Car
{
@Override
void run()
{
System.out.println("电动汽车");
}}
public class PetrolCar extends Car {
@Override
void run()
{
System.out.println("汽油汽车");
}}
public class White implements Color{
@Override
public void colorKind()
{
System.out.println("白色");
}
}
public class Black implements Color{
@Override
public void colorKind()
{
System.out.println("黑色");
}}
public class Red implements Color{
@Override
public void colorKind()
{
System.out.println("红色");
}
}
public class Test
{
public static void main(String[] args)
{
ElectricCar electricCar = new ElectricCar();
White color = new White();
electricCar.setColor(color);
electricCar.getColor().colorKind();//白色
electricCar.run();//电动汽车
}
}
看过之后,缺失留下来一点印象,记入笔记。