欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

面向对象设计原则实践:之一.开放封闭原则

程序员文章站 2022-03-21 21:43:38
...

常用的面向对象设计原则包括7个,这些原则并不是孤立存在的,它们相互依赖,相互补充。

名称

易记符

设计原则及简介

实现关键

关系

重要性

开放封闭原则

开放闭合

程序对扩展是开放的,对修改是封装的。

即在不修改原有功能的基础上扩展其功能

抽象化;将可变因素封装;

最重要的原则

5

单一职责

职责单一

一个对象应该只包含单一的职责,

并且该职责被完整地封装在一个类中

归纳与抽象类的不同职责,并将其分离

是实现高内聚、

低耦合的指导方针

4

接口隔离原则

接口单一交互

客户端不应该依赖那些它不需要的接口

组合多个专一的接口实现总的接口

 

2

迪米特原则

中间类交互

一个软件实体应当尽可能少的

与其他实体发生相互作用

使用 中间类 进行间接交互

 

3

合成复用

组合聚合

继承关系的是静态的;

关联、组合、聚合关系的动态的

尽量使用关联、组合、聚合关系,少用继承。

 

4

           

依赖倒转原则

抽象类依赖

所有模块都依赖于抽象类;

客户端与实现类之间以抽象类进行耦合

通过抽象类进行耦合

最主要的实现手段

5

里氏代换

基类定义,

子类运行

所有引用基类(父类)的地方,

必须能透明地使用其子类的对象

在程序定义中,

使用基类类型来对对象进行定义,

在程序运行时,

确定其子类类型,用子类对象来替换父类对象 

实现开闭原则的

重要方式之一

4

 

一、开放封闭原则(Open-Closed principle)

1.  开闭原则定义

一个软件实体应当对扩展开放,对修改关闭。

也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下进行扩展,

即软件实体应当在不修改的前提下扩展。 

 

2.  开闭原则分析

(1)开闭原则由Bertrand Meyer于1988年提出,它是面向对象设计中最重要的原则之一。

(2)在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。

(3)抽象化是开闭原则的关键。

(4)开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,

对可变性封装原则(Principle of Encapsulation of Variation,EVP)要求找到系统的可变因素并将其封装起来。

 

3.  实例一

某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,

用户可能会改变需求要求使用不同的按钮,原始设计方案如图所示:

开闭原则-图-1

面向对象设计原则实践:之一.开放封闭原则

现对该系统进行重构,使之满足开闭原则的要求。

开闭原则-图-2

面向对象设计原则实践:之一.开放封闭原则

 

对比分析

图(1):客户端的一个方法直接调用加法类,但是我想添加一个减法类,

你就会发现添加减法类就得改变加法类中代码(用switch语句实现),这就违背了“开闭原则”

图(2):在这个图中我们添加了一个运算类的父类,这样我们再添加减法类的时候就不用修改客户端类。

 

用【策略模式】实现这个方案的代码如下:

#include <stdio.h>

#include <stdlib.h>

#include <iostream>

#include <string>



using namespace std;



/* Step1: 使用vitual定义函数的功能模块,相当于实现公共界面设计 */

class IGetResult {

public:

//virtual double get_result(double numberA, double numberB) = 0;

virtual double get_result() = 0;



public:

/* 注意一定要不要忘记添加虚—析构函数 */

virtual ~IGetResult(){}

};



/* Step2 : 继承基类,定义具体的功能模块,并声明具体需要的私有成员变量 */

class Add : public IGetResult {

public:

Add(double a, double b){

std::cout << "Add" << endl;



m_numberA = a;

m_numberB = b;

};

~Add(void) {};



public:

double get_result();

private:

double m_numberA;

double m_numberB;

};



/* 实现具体的功能模块 */

double Add::get_result() {

std::cout << "Add::get_result()" << endl;



return m_numberA + m_numberB;

}



class Del : public IGetResult {

public:

Del(double a, double b){

std::cout << "Del" << endl;

m_numberA = a;

m_numberB = b;

};

virtual ~Del(void) {};

public:

double get_result();

private:

double m_numberA;

double m_numberB;

};



double Del::get_result(){

std::cout << "Del::get_result()" << endl;

return m_numberA - m_numberB;

}



int main(int argc, char* argv[]){

std::cout << "main()" << endl;

int a = 22;

int b = 10;

int ret = 0;

Add add = Add(a, b);

ret = add.get_result();

std::cout << "ret = " << ret << endl;





Del del = Del(a, b);

ret = del.get_result();

std::cout << "ret = " << ret << endl;

return 0;

}

编译:

g++ -Wall -g strategy-pattern.cpp -o strategy

 

运行结果:

main()

Add

Add::get_result()

ret = 32

Del

Del::get_result()

ret = 12

 

 

4. 实例二

假如,我们要写一个工资税类,工资税在不同国家有不同计算规则。

如果我们不坚持OCP,直接写一个类封装工资税的算税方法,而每个国家对工资税的具体实现细节是不尽相同的!

如果我们允许修改,即把现在系统需要的所有工资税(中国工资税、美国工资税等)都放在一个类里实现,

谁也不能保证未来系统不会被卖到日本,一旦出现新的工资税,而在软件中必须要实现这种工资税,

这个时候我们能做的只有找出这个类文件,在每个方法里加上日本税的实现细节并重新编译库。

虽然我们只要将新的库覆盖到原有的库即可,并不影响现有程序的正常运行,

但每次出现新情况都要找出类文件,添加新的实现细节,

这个类文件不断扩大,以后维护起来就变的越来越困难,也并不满足我们以前说的单一职责原则(SRP),

因为不同国家的工资税变化都会引起对这个类的改变动机!

 

如果我们在设计这个类的时候坚持了OCP的话,把工资税的公共方法抽象出来做成一个接口,封闭修改,

在客户端(使用该接口的类对象)只依赖这个接口来实现对自己所需要的工资税,

以后如果系统需要增加新的工资税,只要扩展一个具体国家的工资税实现我们先前定义的接口,就可以正常使用,

而不必重新修改原有类文件! 

 

5. 实例三

下面这个例子就是既不开放也不封闭的,

因为Client和Server都是具体类,

如果我要Client使用不同的一个Server类那就要修改Client类中所有使用Server类的地方为新的Server类。 

class Client{  

Server server;  

void GetMessage(){  

server.Message();  

 }  

}  

 

class Server{  

void Message();  

}

 

下面为修改后符合OCP原则的实现,

我们看到Server类是从ClientInterface继承的,不过ClientInterface却不叫ServerInterface,

原因是我们希望对Client来说ClientInterface是固定下来的,变化的只是Server。

这实际上就变成了一种策略模式(Gof Strategy) 

 

interface ClientInterface{  

public void Message();  

//Other functions  

}  

 

class Server:ClientInterface{  

public void Message();  

}  

 

class Client {  

ClientInterface ci;  

 public void GetMessage(){  

ci.Message();  

}  

public void Client(ClientInterface paramCi){  

ci=paramCi;  

}  

}  

 

//那么在主函数(或主控端)则  

public static void Main(){  

ClientInterface ci = new Server();  

//在上面如果有新的Server类只要替换Server()就行了.  

Client client = new Client(ci);  

client.GetMessage();  

}

 

6. 实例四

使用Template Method实现OCP:

abstract class Policy{

private int[] i ={ 1, 1234, 1234, 1234, 132 };

public bool Sort(){

SortImp();

}

protected virtual bool SortImp(){

 

}

}

 

class Bubbleimp : Policy{

protected override bool SortImp(){

//冒泡排序

}

}

class Bintreeimp : Policy{

protected override bool SortImp(){

//二分法排序

}

}

 

//主函数中实现

static void Main(string[] args){

//如果要使用冒泡排序,只要把下面的Bintreeimp改为Bubbleimp

Policy sort = new Bintreeimp();

sort.Sort();

}  

 

7. 开闭原则总结

面对需求,对程序的改动是通过增加新代码进行的,而不是改变原来的代码。

 

OCP优点: 

1)、降低程序各部分之间的耦合性,使程序模块互换成为可能; 

2)、使软件各部分便于单元测试,通过编制与接口一致的模拟类(Mock),可以很容易地实现软件各部分的单元测试; 

3)、利于实现软件的模块的互换,软件升级时可以只部署发生变化的部分,而不会影响其它部分; 

 

使用OCP注意点: 

1)、实现OCP原则的关键是抽象; 

2)、两种安全的实现开闭原则的设计模式是:Strategy pattern(策略模式),Template Methord(模版方法模式); 

3)、依据开闭原则,我们尽量不要修改类,只扩展类,

但在有些情况下会出现一些比较怪异的状况,这时可以采用几个类进行组合来完成; 

4)、将可能发生变化的部分封装成一个对象,如: 状态, 消息,,算法,数据结构等等 ,

封装变化是实现"开闭原则"的一个重要手段,如经常发生变化的状态值,如温度,气压,颜色,积分,排名等等,

可以将这些作为独立的属性,如果参数之间有关系,有必要进行抽象。

对于行为,如果是基本不变的,则可以直接作为对象的方法,否则考虑抽象或者封装这些行为; 

5)、在许多方面,OCP是面向对象设计的核心所在。

遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。

然而,对于应用程序的每个部分都肆意地进行抽象并不是一个好主意。

应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要

 

 

相关标签: c