前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计原则:面向对象设计原则详解

设计原则:面向对象设计原则详解

作者头像
黄规速
发布2022-04-14 20:38:36
1.9K0
发布2022-04-14 20:38:36
举报

我们在应用程序开发中,一般要求尽量两做到可维护性和可复用性。

应用程序的复用可以提高应用程序的开发效率和质量,节约开发成本,恰当的复用还可以改善系统的可维护性。而在面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则。遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。 面向对象设计原则和设计模式也是对系统进行合理重构的指导方针。

代码语言:txt
复制
好代码的总体愿景指标是:

代码整洁易读:代码能让人容易阅读、跟踪和理解:代码简单、编码风格一致、代码意图表达明确、恰到好处的注视。

可维护性高:理解、改正、改动、改进软件的难易程度。因素有可理解性、可测试性和可修改性,包括编写和运行的维护性,比如强烈依赖底层系统的服务就不太好维护。

可扩展性强:方便增加新功能。

可靠性高性能:增加新的功能后,对原来的功能没有影响,

代码语言:txt
复制
   常用的面向对象设计原则包括7个,这些原则并不是孤立存在的,它们相互依赖,相互补充**。设计原则核心思想:**
  1. 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  2. 针对接口编程,而不是针对实现编程。
  3. 为了交互对象之间的松耦合设计而努力

前5个原则组合称为:SOLID 固定原则

分类总结就是:

一、解藕原则:解决耦合性问题,尽少依赖外部。

代码语言:txt
复制
1、单一职责
代码语言:txt
复制
2、开闭原则
代码语言:txt
复制
3、迪米特法则

二、接口原则

代码语言:txt
复制
4、依赖倒置:依赖接口编程。
代码语言:txt
复制
5、接口隔离:接口分类和专职。

三、继承父子原则:既有解藕又有接口。

代码语言:txt
复制
6、里氏替换原则

7、合成/聚合原则。

一.单一职责原则Single

1、定义:类的职责要单一,不能将太多的职责放在一个类中,应该只负责一类行为。(高内聚、低耦合)

一个类应该只包含单一的职责,并且该职责被完整地封装在一个类中。(Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class.),即又定义有且仅有一个原因使类变更。该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。

如:xxMapper,xxDAO只实现对某一个数据表的增删改查功能

2、原则分析:

1)一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

2)类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。一个模块或者函数只对某一类行为负责。 就是看谁用这个模块(函数)。使用者不同,其职责就可能不同,职责不同就要想办法分开,强行放在一起,就违反SRP

3)单一职责原则是 实现高内聚、低耦合的 指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。

例子:

最简单例子是:一个数据结构职责类和算法行为都放在一个类User。现使用单一职责原则对User类进行重构: 一个软件模块都应该只对某一类行为者负责, 我们应该把数据结构和行为分开。

3、优点:

1、降低类的复杂性,类的职责清晰明确,提高了类的内聚性 。比如数据职责和行为职责清晰明确。类依赖和被依赖的其他类也变少了,减少了代码的耦合性。

2、提高类的可读性和维护性,

4、变更引起的风险减低,变更是必不可少的,当需求变化时,只需要修改一个地方。 如果接口的单一职责做得好,一个接口修改只对相应的类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否合理,但是“职责”和“变化原因”都是没有具体标准的,一个类到底要负责那些职责?这些职责怎么细化?细化后是否都要有一个接口或类?这些都需从实际的情况考虑。因项目而异,因环境而异。 **如果类拆分过细,反而会降低内聚性,影响代码的可维护性。** 通常情况下, **我们应当遵守单一职责原则**, **只有逻辑足够简单**,才可以在代码级违反单一职责原则:只有类种方法数量足够少,可以在方法级别保持单一职责原则。

4、如何判断类职责是否单一

类是什么,两个角度来看

  • 组成派:是对一类事物的抽象,由成员属性和成员方法组成的数据结构,强调封装
  • 职责派:是为达成一种目标一组能力的集合,承担它所代表的抽象的职责,强调行为。

1)、贫血模型和充血模型的单一职责问题:

具体请看:架构师技能1:Java工程规范、浅析领域模型VO、DTO、DO、PO、优秀命名_hguisu的博客-CSDN博客

贫血模型是指对象只有属性(getter/setter),或者包含少量的CRUD方法,而业务逻辑都不包含在其中,而是放在单独的业务处理逻辑层。JavaBean就是最为典型的代表。该模型的确是不够面向对象,对象只是作为保存状态(如数据层的表映射)或者传递状态(如方法中的出入参数)使用,所以就说只有数据没有行为的对象不是真正的对象。

充血模型是指对象里即有数据和状态,也有行为,行为负责维持本身的数据和状态,具有内聚性,最符合面向对象的设计,满足单一职责原则。这也是我们是为常见的对象设计方式。

遵循充血模型的规范,出发点非常好,但对开发人员要求非高,随着变化与演进,最后可能一个类充满了乱七八糟的内容,反而忘记初心,违背单一原则。

有一个员工管理模块EmployeeManager类,EmployeeManager类有三个函数:

代码语言:javascript
复制
class Employee {

}
public class EmployeeManager {

    public float calculateWorkHours(Employee employee) {
        //996 工作制
        float x = 12 * 6;
        return x;
    }

    public float calculateSalary(Employee employee) {
        //计算工资一小时一百块
        float salary = 100 * calculateWorkHours(employee);
        return salary;
    }

    public float check(Employee employee) {
        //考核分数计算,工作时长*基本能力系数
        float check = 2 * calculateWorkHours(employee);
        return check;
    }

}

这三个函数分别对应的是三类不同的行为者,违反了SRP设计原则。

代码语言:txt
复制
calculateWorkHours() 工时计算
代码语言:txt
复制
calculateSalary()是计算工资函数
代码语言:txt
复制
check()是考核分数函数。

这三个函数被放在同一个源文件中,即同一个EmployeeManager类中,这样做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致计算工资的需求变更影响到考核分数所依赖的功能。例如,calculateSalary()函数和check()函数使用了同样逻辑来计算员工的工时情况。开发者为了避免重复代码,通常会将该算法单独实现为一个名为calculateWorkHours()的函数。

现在需求变动,加班没有工资。每天按照8小时来计算:

代码语言:javascript
复制
    public float calculateWorkHours(Employee employee) {
        //虽然996小时工作,但是工资只有5*8.
        float x = 8 * 5;
        return x;
    }

测试了一下,工资计算非常合理,就提交了代码。

​ 最后就出现,员工的考核结果也受到了影响。因为考核也按照8*5来计算了。

​ 虽然都是计算工作时长,但是因为给不通过的部门使用,calculateWorkHours 函数的职责在这种情况下其实不一样。

这类问题发生的根源就是因为我们将不同行为者所依赖的代码强凑合到了一起,对此,SRP强调这类代码一定要分开。

2)、多属性的单一职责问题

在社交产品中,需设计一个UserInfo类来记录用户心信息:

代码语言:javascript
复制
public class UserInfo{
    private long userId;
    private String userName;
    private String email;
    private String telephone;
    private long createTime;
    private long lastLoginTime;

    private String provinceOfAddress; // 省
    private String cityOfAddress;    // 市
    private String regionOfAddress;    // 区
    private String detailedAddress;    // 详细地址
    //... 其他属性
}

对于这个类,是否满足职责单一原则?有两种不同的观点:

一种观点认为,该类包含了跟用户相关的信息,满足单一职责原则;

另一种观点认为,地址信息在该类中占比较重,应该拆分出来UserAddress,UserInfo只保留除地址以外的信息,方便后续在扩展如电商物流等模块功能时,能更好的与UserInfo解耦。

至于哪种观点正确?实际上,应该结合实际的应用场景。如果该类中的地址信息跟其他信息一样,只是用来展示,那么UserInfo现在的设计就是合理的。但是如果现在这个社交产品发展得比较好,需要添加电商模块,那么在电商物流中,就会用到用户的地址信息,为了让电商模块更好的复用这部分信息,并且易于后期维护,就需要将地址信息从UserInfo中拆分处理,独立成用户地址信息。

这里没有一个具体的金科玉律,但从实际代码开发经验上,有一些可执行性的侧面判断指标,可供参考:

  • 中的代码行数、函数、或者属性过多:会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多:不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多:我们就要考虑是否将私有方法独立到新的类中,设置public方法,供更多的类使用,从而提高代码的复用性;
  • 类中大量的方法都是集中操作类中的几个属性:比如,在UserInfo例子中,如果一半的方法都是在操作address信息,那就可以考虑将这几个属性和对应的方法拆分出来。
  • 比较难给类起一个合适的名字:很难用一个业务名词概括,或者只能用一些笼统的Manager、Context之类的词语来命名,这就说明类的职责定义得可能不够清晰;

2.开闭原则( Open - ClosedPrinciple ,OCP )

1、定义 :对扩展开放,对修改关闭(设计模式的核心原则是

一个软件实体(如类、模块和函数)应该对扩展(提供方)开放,对修改(使用方)关闭.

意思是,在一个系统或者模块中,对于扩展是开放的,对于修改是关闭的,

一个好的系统:是在不修改源代码的情况下,可以扩展你的功能.

代码语言:txt
复制
  实现开闭原则的关键就**是抽象化:**用抽象构建框架,用实现扩展细节。

1、软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。 扩展代码也包括:新增模块、类、方法、属性。 2、开闭原则不是完全杜绝修改,而是以最小的修改代码代价来完成新功能开发。 3、同样的代码修改,载粗粒度下可以被认为修改,在细粒度下被认为扩展。 类添加新的方法,添加新方法对类来说是修改,但这个改动并没有修改已有的属性和方法,在方法和属性这层,它被认为是扩展。

2、开闭原则的背景

任何软件都需要面临一个很重要的问题:即它们的需求会随时间的推移而发生变化。

当软件系统需要面对新的需求时,我们应该尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,那么可以非常方便地对系统进行扩展,而且在扩展时无须修改现有代码,使得软件系统在拥有适应性和灵活性的同时具备较好的稳定性和延续性。随着软件规模越来越大,软件寿命越来越长,软件维护成本越来越高,设计满足开闭原则的软件系统也变得越来越重要。

3、开闭原则的作用

开闭原则是面向对象设计的终极目标,他使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下:

1.对软件测试的影响: 软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍能够正常运行

2.可以提高代码的可复用性: 粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。

3.提高软件的可维护性: 遵守开闭原则的软件,其稳定性高和延续性强,从而易于维护

4、原则分析 :

1)当软件实体因需求要变化时, 尽量通过扩展已有软件实体,可以提供新的行为,以满足对软件的新的需求,而不是修改已有的代码,使变化中的软件有一定的适应性和灵活性 。已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。

2)实现开闭原则的关键就是抽象化 :在"开-闭"原则中,不允许修改的是抽象的类或者接口,允许扩展的是具体的实现类,抽象类和接口在"开-闭"原则中扮演着极其重要的角色..即要预知可能变化的需求.又预见所有可能已知的扩展..所以在这里"抽象化"是关键!

3)可变性的封闭原则:找到系统的可变因素,将它封装起来. 这是对"开-闭"原则最好的实现. 不要把你的可变因素放在多个类中,或者散落在程序的各个角落. 你应该将可变的因素,封套起来..并且切忌不要把所用的可变因素封套在一起. 最好的解决办法是,分块封套你的可变因素!避免超大类,超长类,超长方法的出现!!给你的程序增加艺术气息,将程序艺术化是我们的目标!

例子:我们前面提到的模板方法模式和观察者模式都是开闭原则的极好体现。

5、如何做到开闭原则

1.抽象约束

1.通过接口或抽象类约束扩展,对扩展进行边界限定, 不允许出现在接口或抽象类中不存在的public方法

2.参数类型,引用对象(调用对象具体方法)尽量使用接口或者抽象类,而不是实现类

3.抽象层尽量保持稳定,一旦确定不允许修改接口或抽象类一旦定义,应立即执行,不能有修改接口的想法,除非是彻底的大返工

2.元数据控制模块行为

代码语言:txt
复制
  元数据:用来描述环境和数据的数据,通俗的说就是配置参数
代码语言:txt
复制
  通过扩展一个子类,修改配置文件,完成了业务的变化,也是框架的好处

3.制定项目章程

4.封装变化

代码语言:txt
复制
  对变化的封装包含两层含义:
代码语言:txt
复制
 1.将相同的变化封装到一个接口或抽象类中
代码语言:txt
复制
 2.将不同的变化封装到不同的接口或抽象类中

在Java程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。

Sunny软件公司开发的CRM系统可以显示各种类型的图表,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图1所示:

代码语言:javascript
复制
......
if (type.equals("pie")) {
    PieChart chart = new PieChart();
    chart.display();
}

else if (type.equals("bar")) {
    BarChart chart = new BarChart();
    chart.display();
}
......

在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。

现对该系统进行重构,使之符合开闭原则。

在本实例中,由于在ChartDisplay类的display()方法中针对每一个图表类编程,因此增加新的图表类不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之增加新的图表类时无须修改源代码,满足开闭原则。具体做法如下:

代码语言:txt
复制
增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
代码语言:txt
复制
ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。

我们引入了抽象图表类AbstractChart,且ChartDisplay针对抽象图表类进行编程,并通过setChart()方法由客户端来设置实例化的具体图表对象,在ChartDisplay的display()方法中调用chart对象的display()方法显示图表。如果需要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,在客户端向ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。

注意:因为xml和properties等格式的配置文件是纯文本文件,可以直接通过VI编辑器或记事本进行编辑,且无须编译,因此在软件开发中,一般不把对配置文件的修改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的Java代码或C#代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。

三.里氏代换原则( Liskov Substitution Principle ,LSP )

1、定义:任何基类可以出现的地方,子类也可以出现

第一种定义方式相对严格:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。

第二种更容易理解的定义方式: 所有引用基类(父类)的地方必须能透明地使用其子类的对象。即 子类能够必须能够替换基类能够从出现的地方。

通俗来讲就是: 子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。一旦重写父类方法,有可能破坏继承体系,导致子类不能够替换引用基类的地方。

里氏替换原则目的是指导继承中子类如何设计,子类的设计保证在替换父类是,不改变原有的逻辑和程序的正确性。

2、场景:

当使用继承时,遵循 里氏替换原则:子类继承基类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,也尽量不要重载父类的方法。

1、继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

代码语言:txt
复制
2、继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。   
代码语言:txt
复制
3、继承实际上让两个类耦合性增强了,在适当的情况下,可以通过     **聚合,组合,依赖**来解决耦合问题   

( 里氏代换原则由 2008 年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院教授 Barbara Liskov 和卡内基 . 梅隆大学 Jeannette Wing 教授于 1994 年提出。其原文如下: Let q(x) be a property provableabout objects x of type T. Then q(y) should be true for objects y of type Swhere S is a subtype of T. )

3、原则分析:

1)讲的是基类和子类的关系,只有这种关系存在时,里氏代换原则才存在。正方形是长方形是理解里氏代换原则的经典例子。

2)里氏代换原则可以通俗表述为:在 软件中如果能够使用基类对象,那么一定能够使用其子类对象 。把基类都替换成它的子类,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。

3) 里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此 在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象 。

例子:正方形不是长方形 在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统中,让正方形继承自长方形是顺利成章的事情。由于正方形的度和宽度必须相等,所以在方法setLength和setWidth中,对长度和宽度赋值相同。

代码语言:javascript
复制
/**
 * 长方形类Rectangle:
 *
 */
class Rectangle {
  double length;
  double width;
  public double getLength() { return length; } 
  public void setLength(double height) { this.length = length; }   
  public double getWidth() { return width; }
  public void setWidth(double width) { this.width = width; } 
}

正方形集成长方形:

代码语言:javascript
复制
/**
 * 正方形类Square:
 */
class Square extends Rectangle {
  public void setWidth(double width) {
    super.setLength(width);
    super.setWidth(width);   
 }
  public void setLength(double length) { 
    super.setLength(length);
    super.setWidth(length);   
  } 
}
代码语言:txt
复制
  由于正方形的度和宽度必须相等,所以在方法setLength和setWidth中,对长度和宽度赋值相同。类TestRectangle是我们的软件系统中的一个组件,它有一个resize方法要用到基类Rectangle,resize方法的功能是模拟长方形宽度逐步增长的效果 :

测试类TestRectangle:

代码语言:javascript
复制
class TestRectangle {
  public void resize(Rectangle objRect) {
    while(objRect.getWidth() <= objRect.getLength()  ) {
        objRect.setWidth(  objRect.getWidth () + 1 );
    }
  }
}

我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。

我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

代码语言:txt
复制
   原因就是类Square重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候。

我个人建议通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用 依赖,聚合,组合等关系代替。

代码语言:javascript
复制
 /**
 * 长方形类Rectangle:
 *
 */
class Base {
  double width;
  public double getWidth() { return width; }
  public void setWidth(double width) { this.width = width; }
}


/**
 * 长方形类Rectangle:
 *
 */
class Rectangle extends Base{
  double length;
  public double getLength() { return length; } 
  public void setLength(double height) { this.length = length; }   
}



/**
 * 正方形类Square:
 */
class Square extends Base {
  double length;
  public double getLength() { 
      return length; } 
  public void setLength(double height) { 
        this.length = length; 
        super.setWidth(length);   
  }   
}

4、优缺点:

在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

1)代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

2)提高代码的重用性;

3)子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;

4)提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口都是通过继承父类来完成的;

5)提高产品或项目的开放性。

自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:

1)继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

2)降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;

3)增强了耦合性。当父类的常量、变量和方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构

四.接口隔离原则(Interface Segregation Principle,ISL):

1、定义:客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)

客户端不应该依赖那些它不需要的接口。即一个类对另外一个类的依赖应该建立在最小的接口上。

另一种定义方法:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。

注意 ,在该定义中的接口指的是所定义的方法。例如外面调用某个类的public方法。这个方法对外就是接口。

2、原则分析:

1) 接口隔离原则是指使 用多个专门的接口,而不使用单一的总接口 。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。

• (1) 一个接口就 只代表一个角色 ,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。

• (2) 接口 仅仅提供客户端需要的行为 ,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。

2) 使用接口隔离原则拆分接口时,首先必须满足 单一职责原则 ,将一组相关的操作定义在一个接口中,且在满足高内聚的前提下,接口中的方法越少越好。

3) 可以在进行系统设计时采用 定制服务 的方式,即 为不同的客户端提供宽窄不同的接口 ,只提供用户需要的行为,而隐藏用户不需要的行为。

3、接口隔离和单一职责的区别:

范围区别:单一职责原则是针对模块、类、接口的设计,接口隔离原则相对单一职责更侧重接口的设计。

接口隔离主要面向调用者:调用者只使用部分接口或者接口的部分功能,那接口的设计就不够单一。

4、例子:

下图展示了一个拥有多个客户类的系统,在系统中定义了一个巨大的接口(胖接口) AbstractService 来服务所有的客户类。可以使用接口隔离原则对其进行重构。

重构后

五、依赖倒转原则( Dependence Inversion Principle ,DIP ):

1、定义:要依赖抽象,而不要依赖具体的实现.

高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。简单的说,依赖倒置原则要求客户端依赖于抽象耦合。原则表述:

1)抽象不应当依赖于细节;细节应当依赖于抽象;

2)要针对接口编程,不针对实现编程。

依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定得多。以抽象为基础搭建的架构比以细节为基础的架构要稳定得多。在Java中,抽象指的是接口或抽象类,细节就是具体的实现类。

使用接口或抽象类的目的就是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

2、原则分析:

1)如果说开闭原则是面向对象设计的目标,依赖倒转原则是到达面向设计"开闭"原则的手段..如果要达到最好的"开闭"原则,就要尽量的遵守依赖倒转原则. 可以说依赖倒转原则是对"抽象化"的最好规范! 我个人感觉,依赖倒转原则也是里氏代换原则的补充..你理解了里氏代换原则,再来理解依赖倒转原则应该是很容易的。

2)依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中。

3) 类之间的耦合: 零耦合 关系, 具体耦合 关系, 抽象耦合 关系。 依赖倒转原则要求客户端依赖于抽象耦合 ,以抽象方式耦合是依赖倒转原则的关键。

理解这个依赖倒置,首先我们需要明白依赖在面向对象设计的概念:

依赖关系(Dependency): 是一种 使用关系 ,特定事物的改变有可能会影响到使用该事物的其他事物,在需要表示一个事物使用另一个事物时使用依赖关系。( 假设 A 类的变化引起了 B 类的变化,则说名 B 类依赖于 A 类。 )大多数情况下, 依赖关系体现在某个类的方法使用另一个类的对象作为参数 。 在UML中,依赖关系用带箭头的虚线表示, 由依赖的一方指向被依赖的一方。

3、应用场景

依赖倒转和控制反转优点类似,更多的目的是指导框架层面的设计。

例子:某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。

由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类 MainClass 都需要修改源代码,以便使用新的类,但违背了开闭原则。现使用依赖倒转原则对其进行重构。

代码语言:javascript
复制
/**
 * Data抽象接口
 *
 */
public interface IDataSource {
    public  void getSource();
}

/**
 * Database数据源具体实现
 *
 */
public class DatabaseSource implements IDataSource{

    public  void getSource(){
        System.out.println("Get database data");
    }
}

/**
 * Text数据源具体实现
 *
 */
public class TextSource implements IDataSource{

    public  void getSource(){
        System.out.println("Get xml data");
    }
}



/**
 * 转换抽象接口
 *
 */

public  interface Itransformer {
   public  void transform();
}

/**
 * 依赖注入是依赖IDataSource接口抽象注入的,而不是具体
 * DatabaseSource
 *
 *
 */
public class XMLStransformer implements Itransformer{
    /**
     *
     */
    private IDataSource source;
    /**
     * 构造注入(Constructor Injection):通过构造函数注入实例变量。
     */
    public void XMLStransformer(IDataSource source){
        this.source = source;
    }

    /**
     * 设值注入(Setter Injection):通过Setter方法注入实例变量。
     * @param source : the sourceto set
     */
    public void setSource(IDataSource source) {
        this.source = source;
    }
   
    public void transform( ) {
        source.getSource();
        System.out.println("Stransforming ...");
    }
}

依赖注入的三种写法:

• 构造注入 (Constructor Injection) :通过 构造函数 注入实例变量。

• 设值注入 (Setter Injection) :通过 Setter 方法 注入实例变量。

代码语言:javascript
复制
public class XMLStransformer implements Itransformer{
    /**
     *
     */
    private IDataSource source;
    /**
     * 构造注入(Constructor Injection):通过构造函数注入实例变量。
     */
    public void XMLStransformer(IDataSource source){
        this.source = source;
    }

    /**
     * 设值注入(Setter Injection):通过Setter方法注入实例变量。
     * @param source : the sourceto set
     */
    public void setSource(IDataSource source) {
        this.source = source;
    }
   
    public void transform( ) {
        source.getSource();
        System.out.println("Stransforming ...");
    }
}

优点:采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

依赖正置就是类间的依赖是实实在在的实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑,而编写程序需要的是对现实世界的事物进行抽象,抽象的结构就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。

六 .合成/聚合复用原则(Composite/Aggregate ReusePrinciple ,CARP):

1、定义:要尽量使用对象组合,而不是继承关系达到软件复用的目的

经常又叫做合成复用原则(Composite ReusePrinciple或CRP),尽量使用对象组合,而不是继承来达到复用的目的。

就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。简而言之,要尽量使用合成/聚合,尽量不要使用继承。

2、原则分析:

1)在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过 组合 / 聚合关系 或通过 继承 。

继承复用 :实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。( “白箱”复用

组合/聚合复用 :耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。( “黑箱”复用

2)组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

3)此原则和里氏代换原则氏相辅相成的,两者都是具体实现"开-闭"原则的规范。违反这一原则,就无法实现"开-闭"原则,首先我们要明白合成和聚合的概念:

什么是合成 ?

合成(组合): 表示一个 整体与部分的关系, 指一个依托整体而存在的关系( 整体与部分 不可以分开 ),例如:一个人对他的房子和家具,其中他的房子和家具是不能被共享的,因为那些东西都是他自己的。并且人没了,这个也关系就没了。这个例子就好像,乌鸡百凤丸这个产品,它是有乌鸡和上等药材合成而来的一样。 也比如网络游戏中的武器装备合成一样,多种东西合并为一种超强的东西一样。

虽然组合表示的是一个整体与部分的关系,但是组合关系中部分和整体具有统一的生存期。一旦整体对象不存在,部分对象也将不存在,部分对象与整体对象之间具有同生共死的关系。

在组合关系中,成员类是整体类的一部分,而且整体类可以控制成员类的生命周期,即成员类的存在依赖于整体类。 在UML中,组合关系用带实心菱形的直线表示。

源码:

代码语言:javascript
复制
public class Head
{
    private Mouth mouth;
    public Head() {
    	mouth = new Mouth();
    }
}
public class Mouth
{
}

什么是聚合 ?

聚合: 聚合是比合成关系的一种更强的依赖关系,也表示整体与部分的关系(整体与部分可以分开),例如,一个奔驰S360汽车,对奔驰S360引擎,奔驰S360轮胎的关系..这些关系就是带有聚合性质的。因为奔驰S360引擎和奔驰S360轮胎他们只能被奔驰S360汽车所用,离开了奔驰S360汽车,它们就失去了存在的意义。在我们的设计中,这样的关系不应该频繁出现.这样会增大设计的耦合度。

在面向对象中的聚合: 通常在定义一个整体类后,再去分析这个整体类的组成结构,从而找出一些成员类,该整体类和成员类之间就形成了聚合关系。 在聚合关系中, 成员类是整体类的一部分 ,即成员对象是整体对象的一部分,但是成员对象可以脱离整体对象独立存在。 在UML中,聚合关系用带空心菱形的直线表示。

比如汽车和汽车引擎:

代码语言:javascript
复制
public class Car
{
    private Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public void setEngine(Engine engine) {
        this.engine = engine;
    }
}
public class Engine
{
}

明白了合成和聚合关系,再来理解合成/聚合原则应该就清楚了。要避免在系统设计中出现,一个类的继承层次超过3次。如果这样的话,可以考虑重构你的代码,或者重新设计结构. 当然最好的办法就是考虑使用合成/聚合原则。

七.迪米特法则(Law of Demeter,LoD:

1、定义:系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度

又叫最少知识原则(Least Knowledge Principle或简写为LKP)几种形式定义:

(1) 不要和“陌生人”说话。英文定义为:Don't talk to strangers.

(2) 只与你的直接朋友通信。英文定义为:Talk only to your immediatefriends. 一个软件实体应当尽可能少地与其他类发生相互作用

(3) 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

简单地说,也就是,一个对象应当对其它对象有尽可能少的了解。一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心。

2、法则分析:

朋友关系:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。

1)朋友类:

在迪米特法则中,对于一个对象,其朋友包括以下几类:

(1) 当前对象本身 (this) ;

(2) 以参数形式传入到当前对象方法中的对象;

(3) 当前对象的成员对象;

(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;

(5) 当前对象所创建的对象。

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。

直接朋友:成员变量,方法参数,方法返回值中的类

非直接朋友:局部变量中的对象, 也就是说,陌生的类最好不要以局部变量的形式是出现在类的内部。

代码语言:txt
复制
,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。    

2)狭义法则和广义法则:

在狭义的迪米特法则中,要求我们在设计系统时,应该尽量减少对象之间的交互。 如果两个类之间不必彼此直接通信 , 那么这两个类就不应当发生直接的相互作用 ,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过 第三者转发这个调用 。

狭义的迪米特法则 :可以 降低类之间的耦合 ,但是会在系统中增加大量的小方法并散落在系统的各个角落,它可以使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接的关联,但是也会 造成系统的不同模块之间的通信效率降低 ,使得系统的不同模块之间不容易协调。

广义的迪米特法则 : 指对对象之间的信息流量、流向以及信息的影响的控制 ,主要是 对信息隐藏的控制 。信息的隐藏可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用,由于每一个模块都不依赖于其他模块而存在,因此每一个模块都可以独立地在其他的地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。

3、迪米特法则的主要用途:在于控制信息的过载。

  1. 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用,一个处在松耦合中的类一旦被修改,不会对关联的类造成太大波及;
  2. 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
  3. 在类的设计上,只要有可能,一个类型应当设计成不变类;
  4. 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

例子: 外观模式

迪米特法则与设计模式Facade模式、Mediator模式使民无知

系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度,因为在你的系统中,扩展的时候,你可能需要修改这些类,而类与类之间的关系,决定了修改的复杂度,相互作用越多,则修改难度就越大,反之,如果相互作用的越小,则修改起来的难度就越小..例如A类依赖B类,则B类依赖C类,当你在修改A类的时候,你要考虑B类是否会受到影响,而B类的影响是否又会影响到C类. 如果此时C类再依赖D类的话,呵呵,我想这样的修改有的受了。

迪米特法则是目的,而接口隔离法则是对迪米特法则的规范. 为了做到尽可能小的耦合性,我们需要使用接口来规范类,用接口来约束类.要达到迪米特法则的要求,最好就是实现接口隔离法则,实现接口隔离法则,你也就满足了迪米特法则。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.单一职责原则Single
      • 1、定义:类的职责要单一,不能将太多的职责放在一个类中,应该只负责一类行为。(高内聚、低耦合)
        • 2、原则分析:
          • 3、优点:
            • 4、如何判断类职责是否单一
              • 1)、贫血模型和充血模型的单一职责问题:
              • 2)、多属性的单一职责问题
            • 1、定义 :对扩展开放,对修改关闭(设计模式的核心原则是)
              • 2、开闭原则的背景
                • 3、开闭原则的作用
                  • 4、原则分析 :
                    • 5、如何做到开闭原则
                    • 三.里氏代换原则( Liskov Substitution Principle ,LSP )
                      • 1、定义:任何基类可以出现的地方,子类也可以出现
                        • 2、场景:
                          • 3、原则分析:
                            • 4、优缺点:
                              • 四.接口隔离原则(Interface Segregation Principle,ISL):
                                • 1、定义:客户端不应该依赖那些它不需要的接口。(这个法则与迪米特法则是相通的)
                                  • 2、原则分析:
                                    • 3、接口隔离和单一职责的区别:
                                      • 4、例子:
                                      • 五、依赖倒转原则( Dependence Inversion Principle ,DIP ):
                                        • 1、定义:要依赖抽象,而不要依赖具体的实现.
                                          • 2、原则分析:
                                            • 3、应用场景
                                              • 1、定义:要尽量使用对象组合,而不是继承关系达到软件复用的目的
                                                • 2、原则分析:
                                                  • 1、定义:系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度
                                                    • 2、法则分析:
                                                      • 3、迪米特法则的主要用途:在于控制信息的过载。
                                                      领券
                                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档