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

常用的面向对象设计原则包括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是面向对象设计的核心所在。

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

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

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

猜你喜欢

转载自blog.csdn.net/fireroll/article/details/82107008
今日推荐