设计模式,即Design Patterns,是指在软件设计中,被反复使用的一种代码设计经验。 使用设计模式的目的是为了可重用代码,提高代码的可扩展性和可维护性。 设计模式这个术语是上个世纪90年代由Erich Gamma、Richard Helm、Raplh Johnson和Jonhn Vlissides四个人总结提炼出来的,并且写了一本 Design Patterns 的书。
项目地址: https://gitee.com/baichen9187...
一,开闭原则
开闭原则的定义
开闭原则是最基础的设计原则,它指导我们如何建立一个稳定,灵活的系统。开闭原则定义如下:
Software entities like classes,modules and functions should be open
for extension but closed for modifications.
OOP理论中5个原则之一(分别是S O L I D,单一职责原则、开闭原则、里氏替换、接口隔离、依赖反转)
一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
什么是开闭原则
开闭原则明确的告诉我们:实体实现应该对扩展开放,对修改关闭,其含义是说一个实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。那什么是实体呢?软件实体包括以下几个部分:
项目或软件产品中按照一定的逻辑规则划分的模块
抽象和类
方法
开闭原则的作用
开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体来说,其作用如下。
- 对软件测试的影响软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
- 可以提高代码的可复用性粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
- 可以提高软件的可维护性遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
开闭原则的优点
- 提高代码复用性
- 提高可维护性
- 提高灵活性
- 易于测试
#include
class Number {
protected:
double pi = 3.1415926;
public:
virtual double getCircularArea(int d) = 0;
virtual double getRectangularArea(int a, int b) = 0;
};
class NumberArea : public Number {
public:
double getCircularArea(int r) {
return ((double)r * r) * this->pi;
}
double getRectangularArea(int a, int b) {
return (double)a * b;
}
};
int main()
{
NumberArea num;
double cricular = num.getCircularArea(1);
std::cout << cricular << std::endl;
}
二,单一职责原则
单一职责定义
单一职责原则(SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式与实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。
所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
单一职责的作用
就一个类而言,应该仅有一个引起它变化的原因。应该只有一个职责。
每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。
单一职责的优点
单一职责原则的优点有以下几个方面:
- 降低类的复杂性;
- 提高类的可读性;
- 提高代码的可维护性和复用性;
- 降低因变更引起的风险。
#pragma once
#include
using namespace std;
class ProductFactory {
public:
virtual string product(string material) = 0;
};
class ConsumeFactory {
public:
virtual void consume(string product) = 0;
};
#include
#include"./factroy.h"
using namespace std;
class ProductFactoryimpl :ProductFactory{
public:
string product(string material) {
return material ;
}
};
class ConsumeFactoryimpl : ConsumeFactory {
public :
void consume(string product) {
cout << "消费了" << product << endl;
}
};
int main() {
ProductFactoryimpl p;
string data = p.product("goods");
ConsumeFactoryimpl c;
c.consume(data);
return 0;
}
单一职责的违背原则
一个接口实现多种行为方法
#pragma once
#include
using namespace std;
class ConsumeFactory {
public:
virtual string product(string material) = 0;
virtual void consume(string product) = 0;
};
class ConsumeFactory :ProductFactory{
public:
string product(string material) {
return material ;
}
void consume(string product) {
cout << "消费了" << product << endl;
}
};
int main() {
ConsumeFactoryimpl c;
string data = c.product("goods");
c.consume(data);
return 0;
}
三,依赖倒置原则
依赖倒置的定义
在依赖倒置原则中的倒置指的是和一般OO设计的思考方式完全相反。
面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
依赖倒置原则的核心思想是面向接口编程,依赖倒置原则是JavaBean、EJB和COM等组件设计模型背后的基本原则
高层模块不应该依赖低层模块,应该依赖抽象。
抽象不应该依赖细节;细节应该依赖抽象。
即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
依赖倒置原则的作用
依赖倒置原则可以有效减少类之间的耦合性,提高系统稳定性没降低并行开发中的风险,提高代码可维护性和可读性。
依赖倒置原则的本质就是通过抽象(接口)使得各个类或模块之间相互独立,互不影响,实习模块之间的松耦合
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生;
- 接口或抽象类不依赖于实现类;
- 实现类依赖于接口或抽象类。
- 尽量使用多态
- 任何类不应该从具体类派生
尽量不重写基类方法
案例分析
其中的生物包含依赖了动植物
场景问题: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
#pragma once
#include
class AnimalInterface
{
public:
virtual std::string get() = 0; //食物
};
#include".\Factroy.h"
#include
using std::string;
class AnimalImpl_1 : public AnimalInterface {
public:
string get() {
return "草";
}
};
class AnimalImpl_2 :public AnimalInterface {
public:
string get() {
return "水";
}
};
#include
#include
#include"abstraction.cpp"
using namespace std;
class pasture1 {
public:
void get(AnimalImpl_1 a) {
vector list{ "牛" ,"羊" ,"猪" ,"马" };
for (auto i : list) {
cout << i << "被喂食了 " << a.get() << endl;
}
}
};
class pasture2 {
public:
void get(AnimalImpl_2 a) {
vector list{ "牛" ,"羊" ,"猪" ,"马" };
for (auto i : list) {
cout << i << "被喂食了 " << a.get() << endl;
}
}
};
int main() {
pasture1 p;
p.get(AnimalImpl_1());
pasture2 p2;
p2.get(AnimalImpl_2());
return 0;
}
如果我还想加一些动物,或者做一些筛选,是不是需要修改代码
我们按照原则修改代码,原来的抽象层保持不变,修改高层代码
#pragma once
#include
class AnimalInterface
{
public:
virtual std::string get() = 0; //食物
};
#include".\Factroy.h"
#include
using std::string;
class AnimalImpl_1 : public AnimalInterface {
public:
string get() {
return "草";
}
};
class AnimalImpl_2 :public AnimalInterface {
public:
string get() {
return "水";
}
};
class pasture
{
public:
void get(AnimalInterface *a){
vector list{ "牛" ,"羊" ,"猪" ,"马" };
for (auto i : list) {
cout << i << "被喂食了 " << a->get() << endl;
}
}
};
int main() {
pasture p;
AnimalInterface* ai = new AnimalImpl_1();
AnimalInterface* ai2 = new AnimalImpl_2();
p.get(ai);
p.get(ai2);
return 0;
}
四,里氏替换原则
里氏替换原则的重点在不影响原功能,而不是不覆盖原方法
里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。
里氏替换原则的定义
里氏替换原则在1994年Barbara Liskov 和 Jeannette Wing发表论文中的描述是:
If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T
如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
简单来说就是类B继承A时,除了新的方法外,尽量不对父类的方法进行重写,否则会对原有功能发生变化
里氏替换原则的强制性
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。 - 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
解决方法有多态:新增特有方法
里氏替换原则的优缺点
优点:
- 代码贡献,减小创建工作
- 提高代码复用性
- 提高代码可扩展性
- 提高项目开放性
相应的缺点也有:
- 继承是入侵式的。
- 降低代码的灵活性。
增强了耦合性。
里氏替换原则的违背原则
class Math {
public:
virtual int sum(int a, int b) = 0;
virtual float halve(int a) = 0;
};
#include"Factroy.h"
#include
using namespace std;
class math :Math {
public:
int sum(int a, int b) {
return a + b;
}
float halve(int num) {
return num >> 1;
}
};
class math2 :math {
private:
int add = 100;
public:
int sum(int a, int b) {
return a + b + add;
}
float halve(int num) {
return num >> 1;
}
};
int main() {
math m;
int sum = m.sum(2, 2);
float halve = m.halve(sum);
cout << "sum(2,2)" << sum << endl;
cout << "halve(4)" << halve << endl;
math2 m2;
int sum2 = m2.sum(2, 2);
float halve2 = m2.halve(sum2);
cout << "sum(2,2)" << sum2 << endl;
cout << "halve(4)" << halve2 << endl;
return 0;
}
案例分析
class Math {
public:
virtual int sum(int a, int b) = 0;
virtual float halve(int a) = 0;
};
#include"Factroy.h"
#include
using namespace std;
class math :Math {
public:
int sum(int a, int b) {
return a + b;
}
float halve(int num) {
return num >> 1;
}
};
class math3 : public math {
private:
int add = 100;
public:
int sum(int a, int b) {
return a + b + add;
}
float halve(int num) {
return num >> 1;
}
};
int main() {
math* math;
math3 m3;
math = &m3;
int sum3 = math->sum(2, 2);
float halve3 = math->halve(sum2);
cout << "sum(2,2)" << sum3 << endl;
cout << "halve(4)" << halve3 << endl;
return 0;
}
五,接口隔离原则
接口隔离原则的定义
接口隔离原则是对接口的使用进行约束规范的一个原则,具体含义如下
- 客户端不应该依赖它不需要的接口,一个接口对应一个角色,不应该将不同的角色分配给同一个接口,会形成一个庞大的接口,这也导致了接口污染
- 不应该强迫接口依赖与他们不需要的接口
- 类间的依赖关系应该建立在最小的接口上
接口隔离原则的优点:
- 避免接口污染,防止无法预料的风险
- 提供类内部的逻辑的灵活性,维护性
- 提供了系统的内聚性,降低耦合度
- 减少代码冗余
代码实现
#pragma once
#include
#include
using std::string;
using std::vector;
/*封装了vector*/
template class Container :public vector {
};
template class Crud {
public:
virtual void insert(T t) = 0;
virtual T get( int index) = 0;
virtual void delete_(int index) = 0;
virtual int size() = 0;
};
//应用
template class Apply {
public:
virtual V apply(Crud* c) = 0;
};
#include
#include"Interface.h"
using std::cout;
using std::endl;
class Applylmp : public Apply, public Crud, public Container {
private:
Container list;
public:
void insert(string s) {
list.insert(list.end(),s);
cout << "插入了" << s << endl;
}
string get(int index) {
return list.operator[](index);
}
void delete_(int index) {
list.operator[](index) = "";
}
int size() {
return list.size();
}
string apply() {
string s;
for (int i = 0; i < this->size(); i++) {
s.append(this->get(i));
}
return s;
}
};
int main()
{
Applylmp apply;
string s[] = { "a","b","c" };
for (int i = 0; i < (sizeof(s)/ sizeof(string)); i++) {
apply.insert(s[i]);
}
string data = apply.apply();
cout << data;
return 0;
}
六,迪米特原则(最小知识原则)
迪米特原则的定义
迪米特法则(Law of Demeter,简称LOD),又称为“最少知识原则”。
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,在1987年由Ian
Holland在美国东北大学为一个叫迪米特的项目设计提出的,因此叫做迪米特法则。这条法则实际上是很多著名系统,例如火星登陆软件系统、木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则。
它的定义为:一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。迪米特法则是对软件实体之间通信的限制,它对软件实体之间通信的宽度和深度做出了要求。迪米特的其它表述方式为:
只与你直接的朋友们通信。
不要跟“陌生人”说话。
每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
“朋友”的条件为:
当前对象本身(this);
被当做当前对象的方法的参数传入进来的对象;
当前对象的方法所创建或者实例化的任何对象;
当前对象的任何组件(被当前对象的实例变量引用的任何对象)。
迪米特原则的优点
- 减小了类之间的关系
- 提高了代码的弱耦合
- 提高了代码的复用性
代码实现
#include
class Stranger {
public:
void call() {
std::cout << "我是陌生人" << std::endl;
}
};
class Friend {
private:
Stranger* stranger;
public:
void forward() {
std::cout << "我给你介绍一下" << std::endl;
stranger->call();
}
void call() {
std::cout << "我是朋友" << std::endl;
}
};
class Someone {
public:
void call(Friend* f) {
std::cout << "你好!" << std::endl;
f->forward();
}
};
int main()
{
Someone someone;
someone.call(new Friend());
}