文章目录
在软件开发过程中,开发者经常面临复杂系统的设计与实现。为了应对这一挑战,“设计模式”应运而生。设计模式是一种解决问题的“套路”,它既简化了软件开发的过程,又提高了代码的复用性和可维护性。本文将从设计模式的概念出发,逐步分析软件设计的复杂性以及如何通过面向对象和设计模式来降低复杂性,最终实现高效的代码复用。
什么是设计模式?
“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。
——Christopher Alexander
设计模式(Design Pattern)是对软件开发过程中反复出现的设计问题所提供的通用解决方案。它不是代码,而是经过验证的“最佳实践”,以一种结构化的方式记录了解决问题的思想。
- 核心理念:通过设计模式,开发者无需重复发明轮子,可以直接借用已有的设计经验。(复用!!!)
- 分类:设计模式主要分为三大类:
- 创建型模式:解决对象创建相关的问题,如单例模式(Singleton)、工厂模式(Factory Method)。
- 结构型模式:关注对象间的组合和结构,如适配器模式(Adapter)、装饰器模式(Decorator)。
- 行为型模式:处理对象间的职责分配和通信,如观察者模式(Observer)、状态模式(State)。
为什么需要设计模式?
- 解决常见问题
软件开发中,许多问题是重复出现的。设计模式总结了这些问题的通用解决方案,减少了开发中的试错。 - 提升代码质量
使用设计模式可以使代码更加易读、灵活和扩展性强,方便后期维护。 - 提高开发效率
借助设计模式,开发者可以专注于业务逻辑,而不是基础框架设计,从而加快开发进程。
GOF 设计模式
GOF(Gang of Four,四人帮)指的是设计模式的奠基者——Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides。这四位作者在 1994 年出版了经典著作 《Design Patterns: Elements of Reusable Object-Oriented Software》,首次系统化地总结了 23 种面向对象设计模式。
- 核心观点:
- 软件设计应遵循面向对象的基本原则(如单一职责、开放封闭等)。
- 模式之间可以组合使用,实现更强大的功能。
再次理解面向对象
对于C++程序猿来说,需要将底层思维与抽象思维都进行分析。
**底层思维:**向下,如何把握机器底层从微观理解对象构造
• 语言构造
• 编译转换
• 内存模型
• 运行时机制
**抽象思维:**向上,如何将我们的周围世界抽象为程序代码
• 面向对象
• 组件封装
• 设计模式
• 架构模式
良好的设计模式可以根据语言在底层的逻辑和在语言逻辑层面的设计进行相结合,以此来提高代码质量和开发效率。
所以,在讨论设计模式之前,必须再次理解面向对象的核心思想:
- 封装:将数据和操作封装到对象中,隐藏内部实现。
- 继承:通过继承重用已有的代码,减少重复。
- 多态:通过统一的接口实现不同的行为。
面向对象通过以上三大特性为设计模式奠定了基础,是理解设计模式的关键。
软件设计固有的复杂性
建筑商从来不会去想给一栋已建好的100层高的楼房底下再新修一个小地下室——这样做花费极大而且注定要败。然而令人惊奇的是,软件系统的用户在要求作出类似改变时却不会仔细考虑,而且他们认为这只是需要简单编程的事。
——Object-Oriented Analysis and Design with Applications
复杂性是软件设计中的一大挑战。软件的复杂性源于多个维度:
- 规模增长:软件系统的功能越多,模块之间的关系越复杂。
- 需求变化:用户需求总是在变化,如何适应这些变化是设计的难题。
- 技术多样性:系统可能需要集成多种技术,增加了设计和实现的难度。
软件设计复杂性的根本原因
- 模块之间的耦合:模块间的强耦合导致修改一个模块可能会影响其他模块。
- 缺乏抽象:没有抽象层,导致系统难以扩展和维护。
- 过度复杂的设计:为了应对变化,设计变得过于复杂,反而不利于实现。
- 跨平台的支持:多个版本设计。
原因在后期会进行功能的添加或者修改,但是在刚开始程序进行设计的时候的代码逻辑无法使得在基础上直接添加新功能,所以只能在原来的上面进行强行添加,也就是
if - else
屎山设计的由来~
如何解决复杂性?
分解
人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题。
抽象
更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。
- 遵循设计原则:
- 单一职责原则(SRP):每个模块只做一件事。
- 开放封闭原则(OCP):对扩展开放,对修改封闭。
- 依赖倒置原则(DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
- 采用面向对象思想:通过封装、继承和多态降低耦合、增强复用性。
- 使用设计模式:通过复用成熟的设计经验,将复杂的设计问题分解为可管理的模块。
结构化 VS 面向对象(封装)
我们以一个现实问题为例:计算矩形的面积和周长。
结构化设计代码示例:
在结构化设计中,数据和功能是分离的。函数主要以操作为中心,而数据仅作为参数传递。
#include <iostream>
using namespace std;
// 数据结构:存储矩形的长度和宽度
struct Rectangle {
double length;
double width;
};
// 函数:计算面积
double calculateArea(const Rectangle& rect) {
return rect.length * rect.width;
}
// 函数:计算周长
double calculatePerimeter(const Rectangle& rect) {
return 2 * (rect.length + rect.width);
}
int main() {
// 创建一个矩形
Rectangle rect = {5.0, 3.0};
// 调用函数进行操作
cout << "Area: " << calculateArea(rect) << endl;
cout << "Perimeter: " << calculatePerimeter(rect) << endl;
return 0;
}
特点:
- 数据 (
Rectangle
) 和操作(calculateArea
和calculatePerimeter
)是独立的。 - 如果新增功能(如计算对角线长度),需要添加新的函数,操作逻辑分散。
- 数据和逻辑间的耦合较弱**,但扩展性差**。
面向对象设计代码示例:
在面向对象设计中,数据和功能封装在一个类中,操作直接基于对象调用。
#include <iostream>
#include <cmath> // 为了计算对角线
using namespace std;
// 面向对象设计:定义矩形类
class Rectangle {
private:
double length;
double width;
public:
// 构造函数
Rectangle(double l, double w) : length(l), width(w) {}
// 方法:计算面积
double calculateArea() const {
return length * width;
}
// 方法:计算周长
double calculatePerimeter() const {
return 2 * (length + width);
}
// 方法:计算对角线长度
double calculateDiagonal() const {
return sqrt(length * length + width * width);
}
// 设置长度和宽度
void setDimensions(double l, double w) {
length = l;
width = w;
}
// 显示矩形信息
void display() const {
cout << "Rectangle [Length: " << length << ", Width: " << width << "]" << endl;
}
};
int main() {
// 创建一个矩形对象
Rectangle rect(5.0, 3.0);
// 调用对象方法进行操作
rect.display();
cout << "Area: " << rect.calculateArea() << endl;
cout << "Perimeter: " << rect.calculatePerimeter() << endl;
cout << "Diagonal: " << rect.calculateDiagonal() << endl;
return 0;
}
特点:
- 数据 (
length
,width
) 和操作(如calculateArea
,calculatePerimeter
)被封装在类中。 - 新增功能(如计算对角线长度)可以通过类方法轻松扩展。
- 高度封装,使对象的行为与数据直接相关,提高了模块化和复用性。
对比总结
特点 | 结构化设计 | 面向对象设计 |
---|---|---|
数据与操作的关系 | 数据和操作分离,函数作用于独立的数据 | 数据和操作封装在一个对象内,操作与数据密切相关 |
扩展性 | 新增功能需要新增函数,整体设计不易扩展 | 新增功能只需添加类方法,设计更具扩展性 |
维护性 | 操作逻辑分散在多个函数中,难以维护 | 操作封装在类中,逻辑清晰,易于维护 |
代码复用性 | 数据结构和操作复用性较差 | 类和方法具有较高的复用性 |
示例中的实现复杂度 | 数据操作较为简单,但难以应对复杂系统 | 初期实现略复杂,但适合复杂系统 |
通过以上对比,可以直观地看出,结构化设计更适合小型、单一功能的程序,而面向对象设计在解决复杂系统时更具优势。
结构化设计 | 面向对象设计 |
---|---|
以功能为中心,数据为次要目标 | 以对象为中心,功能服务于对象 |
数据和操作分离 | 数据和操作封装在对象内部 |
难以复用和扩展 | 具有较好的复用性和扩展性 |
面向对象设计通过关注对象及其交互,解决了结构化设计中模块间强耦合的问题,为设计模式的实施提供了基础。
软件设计的目标:复用!
复用是软件设计的核心目标之一。设计模式通过以下方式实现复用:
- 代码复用:减少重复代码,提高开发效率。
- 设计复用:使用模式模板应对类似的设计需求。
- 经验复用:将设计经验总结为模式,方便传递和共享。
总结
设计模式是应对软件复杂性的重要工具,是面向对象思想的升华。通过学习和实践设计模式,开发者可以设计出灵活、可扩展且易维护的系统。对于初学者来说,了解设计模式的基本分类和使用场景,是深入学习的第一步。
关于具体不同设计模式是如何设计的,请阅读笔者该专栏其他文章。