面向对象设计原则实践:之一.开放封闭原则
常用的面向对象设计原则包括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是面向对象设计的核心所在。
遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。
然而,对于应用程序的每个部分都肆意地进行抽象并不是一个好主意。
应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要