设计模式的引入

面向对象设计原则

  • [1. 软件设计固有的复杂性](#1. 软件设计固有的复杂性)
  • [2. 面向对象设计原则](#2. 面向对象设计原则)
    • [2.1 引入](#2.1 引入)
    • [2.2 依赖倒置原则](#2.2 依赖倒置原则)
    • [2.3 开放封闭原则](#2.3 开放封闭原则)
    • [2.4 单一职责原则](#2.4 单一职责原则)
    • [2.5 Liskov 替换原则( LSP)](#2.5 Liskov 替换原则( LSP))
    • [2.6 接口隔离原则( ISP)](#2.6 接口隔离原则( ISP))
    • [2.7 优先使用对象组合,而不是类继承](#2.7 优先使用对象组合,而不是类继承)
    • [2.8 封装变化点](#2.8 封装变化点)
    • [2.9 针对接口编程,而不是针对实现编程](#2.9 针对接口编程,而不是针对实现编程)

1. 软件设计固有的复杂性

复制代码
建筑商从来不会去想给一栋已建好的100层高的楼房底下再新修一个小地下室------这样做花费极大而且注定要失败。
然而令人惊奇的是,软件系统的用户在要求作出类似改变时却不会仔细考虑,而且他们认为这只是需要简单编程的事。
							      ------Object-Oriented Analysis and Designwith Applications

为什么软件设计复杂?本质原因是变化,包括客户需求的变化、技术平台的变化、开发团队的变化、市场环境的变化...

如何解决复杂性问题呢?

  1. 分解:人们面对复杂性有一个常见的做法:即分而治之,将大问题分解为多个小问题,将复杂问题分解为多个简单问题
  2. 抽象:更高层次来讲,人们处理复杂性有一个通用的技术,即抽象。由于不能掌握全部的复杂对象,我们选择忽视它的非本质细节,而去处理泛化和理想化了的对象模型。

2. 面向对象设计原则

2.1 引入

一个好的设计和差的设计之间有什么区别?好的设计是能够抵御变化的,是能够尽量复用代码的。

我猜你应该用过或者看到过画图软件 ,你有思考过画图软件的底层实现吗?

几乎每一个画图软件,都可以绘制常见图形,比如圆、正方形、长方形、矩形、三角形...思考一下,如果要你去实现绘图的功能,你会怎么实现?我在学习设计思想之前,第一个想到的办法是用大量的if else去判断是哪个图像,就调用哪个方法,就像下面这个代码一样

cpp 复制代码
class Point{
public:
	int x;
	int y;
};

class Line{
public:
	Point start;
    Point end;

	Line(const Point& start, const Point& end){
        this->start = start;
        this->end = end;
    }

};

class Rect{
public:
	Point leftUp;
    int width;
	int height;

	Rect(const Point& leftUp, int width, int height){
        this->leftUp = leftUp;
        this->width = width;
		this->height = height;
    }

};

//增加
class Circle{


};

class MainForm : public Form {
private:
	Point p1;
	Point p2;

	vector<Line> lineVector;
	vector<Rect> rectVector;
	//改变
	vector<Circle> circleVector;

public:
	MainForm(){
		//...
	}
protected:

	virtual void OnMouseDown(const MouseEventArgs& e);
	virtual void OnMouseUp(const MouseEventArgs& e);
	virtual void OnPaint(const PaintEventArgs& e);
};


void MainForm::OnMouseDown(const MouseEventArgs& e){
	p1.x = e.X;
	p1.y = e.Y;

	//...
	Form::OnMouseDown(e);
}

void MainForm::OnMouseUp(const MouseEventArgs& e){
	p2.x = e.X;
	p2.y = e.Y;

	if (rdoLine.Checked){
		Line line(p1, p2);
		lineVector.push_back(line);
	}
	else if (rdoRect.Checked){
		int width = abs(p2.x - p1.x);
		int height = abs(p2.y - p1.y);
		Rect rect(p1, width, height);
		rectVector.push_back(rect);
	}
	//改变
	else if (...){
		//...
		circleVector.push_back(circle);
	}

	//...
	this->Refresh();

	Form::OnMouseUp(e);
}

void MainForm::OnPaint(const PaintEventArgs& e){

	//针对直线
	for (int i = 0; i < lineVector.size(); i++){
		e.Graphics.DrawLine(Pens.Red,
			lineVector[i].start.x, 
			lineVector[i].start.y,
			lineVector[i].end.x,
			lineVector[i].end.y);
	}

	//针对矩形
	for (int i = 0; i < rectVector.size(); i++){
		e.Graphics.DrawRectangle(Pens.Red,
			rectVector[i].leftUp,
			rectVector[i].width,
			rectVector[i].height);
	}

	//改变
	//针对圆形
	for (int i = 0; i < circleVector.size(); i++){
		e.Graphics.DrawCircle(Pens.Red,
			circleVector[i]);
	}

	//...
	Form::OnPaint(e);
}

假如我们现在要新增一个图形,我们就得在后面增加if else判断,然后重新编译,重新运行...经过一系列的步骤完成更改。这看起来也没有多麻烦,因为代码量太少了,但如果是在大型项目里,是很麻烦的,而且增加if else也有可能会起冲突。假如后面要增加非常多的图形,if else就会非常多,逐渐会变成屎山代码

再看这个功能的另一种设计

cpp 复制代码
class Shape{
public:
	virtual void Draw(const Graphics& g)=0;
	virtual ~Shape() { }
};


class Point{
public:
	int x;
	int y;
};

class Line: public Shape{
public:
	Point start;
	Point end;

	Line(const Point& start, const Point& end){
		this->start = start;
		this->end = end;
	}

	//实现自己的Draw,负责画自己
	virtual void Draw(const Graphics& g){
		g.DrawLine(Pens.Red, 
			start.x, start.y,end.x, end.y);
	}

};

class Rect: public Shape{
public:
	Point leftUp;
	int width;
	int height;

	Rect(const Point& leftUp, int width, int height){
		this->leftUp = leftUp;
		this->width = width;
		this->height = height;
	}

	//实现自己的Draw,负责画自己
	virtual void Draw(const Graphics& g){
		g.DrawRectangle(Pens.Red,
			leftUp,width,height);
	}

};

//增加
class Circle : public Shape{
public:
	//实现自己的Draw,负责画自己
	virtual void Draw(const Graphics& g){
		g.DrawCircle(Pens.Red,
			...);
	}

};

class MainForm : public Form {
private:
	Point p1;
	Point p2;

	//针对所有形状
	vector<Shape*> shapeVector;

public:
	MainForm(){
		//...
	}
protected:

	virtual void OnMouseDown(const MouseEventArgs& e);
	virtual void OnMouseUp(const MouseEventArgs& e);
	virtual void OnPaint(const PaintEventArgs& e);
};


void MainForm::OnMouseDown(const MouseEventArgs& e){
	p1.x = e.X;
	p1.y = e.Y;

	//...
	Form::OnMouseDown(e);
}

void MainForm::OnMouseUp(const MouseEventArgs& e){
	p2.x = e.X;
	p2.y = e.Y;

	if (rdoLine.Checked){
		shapeVector.push_back(new Line(p1,p2));
	}
	else if (rdoRect.Checked){
		int width = abs(p2.x - p1.x);
		int height = abs(p2.y - p1.y);
		shapeVector.push_back(new Rect(p1, width, height));
	}
	//改变
	else if (...){
		//...
		shapeVector.push_back(circle);
	}

	//...
	this->Refresh();

	Form::OnMouseUp(e);
}

void MainForm::OnPaint(const PaintEventArgs& e){

	//针对所有形状
	for (int i = 0; i < shapeVector.size(); i++){

		shapeVector[i]->Draw(e.Graphics); //多态调用,各负其责
	}

	//...
	Form::OnPaint(e);
}

这个设计直接让所有的图形继承自了Shape基类,并重写基类虚函数Draw,利用运行时多态消除了代码变化,使得在调用绘图方法时,不需要关注是哪个图形,而统一调用Draw方法

如果此时新增了图形,只需要新增此图形的类,并继承自Shape类,重写Draw方法,就可以完成修改,不需要动任何代码,只需要新增一个类文件即可。

重新认识面向对象

  • 理解隔离变化
    • 从宏观层面来看,面向对象的构建方式更能适应软件的变化,能将变化所带来的影响减为最小
  • 各司其职
    • 从微观层面来看,面向对象的方式更强调各个类的"责任"
    • 由于需求变化导致的新增类型不应该影响原来类型的实现------是所谓各负其责
  • 对象是什么?
    • 从语言实现层面来看,对象封装了代码和数据。
    • 从规格层面讲,对象是一系列可被使用的公共接口。
    • 从概念层面讲,对象是某种拥有责任的抽象。

2.2 依赖倒置原则

  • 高层模块(稳定)不应该依赖于低层模块(变化),二者都应该依赖于抽象(稳定) 。
  • 抽象(稳定)不应该依赖于实现细节(变化) ,实现细节应该依赖于抽象(稳定)。

    上面的代码中,第一种设计让MainForm依赖了LineRectMainForm是高层稳定的模块,LineRect是底层变化的模块,让稳定的依赖变化的,稳定的也会被改变

    而第二种设计,让高层和底层都依赖了抽象,此时就解决了变化的问题

2.3 开放封闭原则

  • 对扩展开放,对更改封闭。
  • 类模块应该是可扩展的,但是不可修改

假如你是一个工匠,有一个桌子,现在你想提高它的防火能力,有两种解决办法:1. 换耐火材料重新打造一个新桌子; 2. 给桌子涂一层防火漆

相信大部分人都会选择给桌子涂一层防火漆,因为这是这简单高效解决此问题的方案,同理软件工程里,尽量让设计可扩展,而不是去修改他

2.4 单一职责原则

  • 一个类应该仅有一个引起它变化的原因。
  • 变化的方向隐含着类的责任。

2.5 Liskov 替换原则( LSP)

  • 子类必须能够替换它们的基类(IS-A)。
  • 继承表达类型抽象

确保父类的方法和属性被子类继承后依旧能够使用,如果父类的成员在子类不需要,则不应该是继承关系

2.6 接口隔离原则( ISP)

  • 不应该强迫客户程序依赖它们不用的方法。
  • 接口应该小而完备

2.7 优先使用对象组合,而不是类继承

  • 类继承通常为"白箱复用",对象组合通常为"黑箱复用" 。
  • 继承在某种程度上破坏了封装性,子类父类耦合度高。
  • 而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。

2.8 封装变化点

使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合

2.9 针对接口编程,而不是针对实现编程

  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口。
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
  • 减少系统中各部分的依赖关系,从而实现"高内聚、松耦合"的类型设计方案。
相关推荐
杨充20 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充20 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充20 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充20 小时前
2.面向对象的特性
设计模式
杨充20 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充20 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充20 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式