当前位置: > 财经>正文

面向对象设计的七大设计原则详解 保险金信托方案设计原则有哪些

2023-08-31 21:03:41 互联网 未知 财经

面向对象设计的七大设计原则详解

面向对象的七大设计原则

文章目录 面向对象的七大设计原则简述七大原则之间的关系 一、开闭原则(The Open-Closed Principle ,OCP)概念理解系统设计需要遵循开闭原则的原因开闭原则的实现方法一个符合开闭原则的设计开闭原则的相对性 二、 里氏替换原则(Liskov Substitution Principle ,LSP)概念理解里式替换原则的优点重构违反LSP的设计 三、 迪米特原则(最少知道原则)(Law of Demeter ,LoD)概念理解迪米特原则的优缺点违反迪米特原则的设计与重构使用迪米特原则时要考虑的 四、单一职责原则为什么一个类不能有多于一个以上的职责?职责的划分使用单一职责原则的理由 五、 接口分隔原则(Interface Segregation Principle ,ISP)概念理解违反ISP原则的设计与重构接口分隔原则的优点和适度原则单一职责原则和接口分隔原则的区别 六、 依赖倒置原则(Dependency Inversion Principle ,DIP)概念理解依赖倒置原则的违反例和重构怎么使用依赖倒置原则依赖倒置原则的优点 七、 组合/聚合复用原则(Composite/Aggregate Reuse Principle ,CARP)概念理解什么时候才应该使用继承通过组合/聚合复用的优缺点通过继承来进行复用的优缺点

简述

类的设计原则有七个,包括:开闭原则、里氏代换原则、迪米特原则(最少知道原则)、单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则。

七大原则之间的关系

七大原则之间并不是相互孤立的,彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。

开闭原则是面向对象的可复用设计的基石。其他设计原则是实现开闭原则的手段和工具。

一般地,可以把这七个原则分成了以下两个部分:

设计目标:开闭原则、里氏代换原则、迪米特原则 设计方法:单一职责原则、接口分隔原则、依赖倒置原则、组合/聚合复用原则

一、开闭原则(The Open-Closed Principle ,OCP)

软件实体(模块,类,方法等)应该对扩展开放,对修改关闭。

概念理解

开闭原则是指在进行面向对象设计中,设计类或其他程序单位时,应该遵循:

对扩展开放(open)对修改关闭(closed) 的设计原则。

开闭原则是判断面向对象设计是否正确的最基本的原理之一。

根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的。

通过下边的例子理解什么是扩展开放和修改关闭:

左边的设计是直接依赖实际的类,不是对扩展开放的。

右边的设计是良好的设计:

Client对于Server提供的接口是封闭的;Client对于Server的新的接口实现方法的扩展是开放的。 系统设计需要遵循开闭原则的原因 稳定性。开闭原则要求扩展功能不修改原来的代码,这可以让软件系统在变化中保持稳定。扩展性。开闭原则要求对扩展开放,通过扩展提供新的或改变原有的功能,让软件系统具有灵活的可扩展性。 遵循开闭原则的系统设计,可以让软件系统可复用,并且易于维护。 开闭原则的实现方法

为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中,

可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

接口可以被复用,但接口的实现却不一定能被复用。 接口是稳定的,关闭的,但接口的实现是可变的,开放的。 可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

好处:提高系统的可复用性和可维护性。

简单地说,软件系统是否有良好的接口(抽象)设计是判断软件系统是否满足开闭原则的一种重要的判断基准。现在多把开闭原则等同于面向接口的软件设计。

一个符合开闭原则的设计

需求:创建一系列多边形。 首先,下面是不满足开闭原则的设计方法:

Shape.h

enumShapeType{ isCircle, isSquare};typedef struct Shape {enumShapeType type} shape;

Circle.h

typedef struct Circle {enumShapeType type;double radius;Point center;} circle;void drawCircle( circle* );

Square.h

typedef struct Square {enumShapeType type;double side;Point topleft;} square;void drawSquare( square* );

drawShapes.cpp

#include "Shape.h"#include "Circle.h"#include "Square.h"void drawShapes( shape* list[], intn ) {int i;for( int i=0; itype ) {case isSquare:drawSquare( (square*)s );break;case isCircle:drawCircle( (circle*)s );break;}}}

该设计不是对扩展开放的,当增加一个新的图形时:

Shape不是扩展的,需要修改源码来增加枚举类型drawShapes不是封闭的,当其被其他模块调用时,如果要增加一个新的图形需要修改switch/case

此外,该设计逻辑复杂,总的来说是一个僵化的、脆弱的、具有很高的牢固性的设计。

用开闭原则重构该设计如下图:

此时,在该设计中,新增一个图形只需要实现Shape接口,满足对扩展开放;也不需要修改drawShapes()方法,对修改关闭。

开闭原则的相对性

软件系统的构建是一个需要不断重构的过程,在这个过程中,模块的功能抽象,模块与模块间的关系,都不会从一开始就非常清晰明了,所以构建100%满足开闭原则的软件系统是相当困难的,这就是开闭原则的相对性。

但在设计过程中,通过对模块功能的抽象(接口定义),模块之间的关系的抽象(通过接口调用),抽象与实现的分离(面向接口的程序设计)等,可以尽量接近满足开闭原则。

二、 里氏替换原则(Liskov Substitution Principle ,LSP)

所有引用基类的地方必须能透明地使用其派生类的对象。

概念理解

也就是说,只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

不应该在代码中出现if/else之类对派生类类型进行判断的条件。

派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

以下代码就违反了LSP定义。

if (obj typeof Class1) { do something} else if (obj typeof Class2) { do something else}

里氏替换原则(LSP)是使代码符合开闭原则的一个重要保证。

同时LSP体现了:

类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。

动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。 示例:

里式替换原则为我们是否应该使用继承提供了判断的依据,不再是简单地根据两者之间是否有相同之处来说使用继承。

里式替换原则的引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。

具体来说:

子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。子类中可以增加自己特有的方法。当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。当子类的方法实现父类的方法时(重载/重写或实现抽象方法)的后置条件(即方法的输出/返回值)要比父类更严格或相等。

下面举几个例子帮助更进一步理解LSP: 例:1:

Rectangle是矩形,Square是正方形,Square继承于Rectangle,这样一看似乎没有问题。

假如已有的系统中存在以下既有的业务逻辑代码:

void g(Rectangle r){r.SetWidth(5);r.SetHeight(4);assert(r.GetWidth() * r.GetHeight()) == 20);}

则对应于扩展类Square,在调用既有业务逻辑时:

Rectangle square = new Square(); g(square);

时会抛出一个异常。这显然违反了LSP原则。说明这样的继承关系在这种业务逻辑下不应该使用。

例2:鲸鱼和鱼,应该属于什么关系?从生物学的角度看,鲸鱼应该属于哺乳动物,而不是鱼类。没错,在程序世界中我们可以得出同样的结论。如果让鲸鱼类去继承鱼类,就完全违背了Liskov替换原则。因为鱼作为基类,很多特性是鲸鱼所不具备的,例如通过腮呼吸,以及卵生繁殖。那么,二者是否具有共性呢? 有,那就是它们都可以在水中"游泳",从程序设计的角度来说,它们都共同实现了一个支持"游泳"行为的接口。

例:3:运动员和自行车例子,每个运动员都有一辆自行车,如果按照下面设计,很显然违反了LSP原则。

class Bike {public: void Move( ); void Stop( ); void Repair( );protected: int ChangeColor(int );private: int mColor;};class Player : private Bike{public: void StartRace( ); void EndRace( ); protected: int CurStrength ( ); private: int mMaxStrength; int mAge;}; 里式替换原则的优点 约束继承泛滥,是开闭原则的一种体现。加强程序的健壮性,同时变更时也可以做到非常好地提高程序的维护性、扩展性。降低需求变更时引入的风险。 重构违反LSP的设计

如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。

从B到A的继承关系改为关联关系。

对于矩形和正方形例子,可以构造一个抽象的四边形类,把矩形和正方形共同的行为放到这个四边形类里面,让矩形和正方形都是它的派生类,问题就OK了。对于矩形和正方形,取width 和height 是它们共同的行为,但是给width 和height 赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。

对于运动员和自行车例子,可以采用关联关系来重构:

class Player {public: void StartRace( ); void EndRace( ); protected: int CurStrength ( ); private: int mMaxStrength; int mAge;Bike * abike;};

在进行设计的时候,我们尽量从抽象类继承,而不是从具体类继承。

如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这只是一个一般性的指导原则,使用的时候还要具体情况具体分析。

在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。

三、 迪米特原则(最少知道原则)(Law of Demeter ,LoD)

迪米特原则(Law of Demeter)又叫最少知道原则(Least Knowledge Principle),可以简单说成:talk only to your immediate friends,只与你直接的朋友们通信,不要跟“陌生人”说话。

概念理解

对于面向OOD来说,又被解释为下面两种方式:

1)一个软件实体应当尽可能少地与其他实体发生相互作用。

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

朋友圈的确定 “朋友”条件:

当前对象本身(this)以参量形式传入到当前对象方法中的对象当前对象的实例变量直接引用的对象当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友当前对象所创建的对象

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

迪米特原则的优缺点

迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

迪米特原则不希望类直接建立直接的接触。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特原则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系,这在一定程度上增加了系统的复杂度。

例如,购房者要购买楼盘A、B、C中的楼,他不必直接到楼盘去买楼,而是可以通过一个售楼处去了解情况,这样就减少了购房者与楼盘之间的耦合,如图所示。

违反迪米特原则的设计与重构

下面的代码在方法体内部依赖了其他类,这严重违反迪米特原则

class Teacher { public: void command(GroupLeader groupLeader) { list listStudents = new list; for (int i = 0; i < 20; i++) { listStudents.add(new Student()); } groupLeader.countStudents(listStudents); } }

方法是类的一个行为,类竟然不知道自己的行为与其他类产生了依赖关系(Teacher类中依赖了Student类,然而Student类并不在Teacher类的朋友圈中,一旦Student类被修改了,Teacher类是根本不知道的),这是不允许的。

正确的做法是:

class Teacher { public:void command(GroupLeader groupLeader) { groupLeader.countStudents(); } }class GroupLeader { private:list listStudents; public:GroupLeader(list _listStudents) { this.listStudents = _listStudents; } void countStudents() { cout

版权声明: 本站仅提供信息存储空间服务,旨在传递更多信息,不拥有所有权,不承担相关法律责任,不代表本网赞同其观点和对其真实性负责。如因作品内容、版权和其它问题需要同本网联系的,请发送邮件至 举报,一经查实,本站将立刻删除。