如何解决复杂的问题?
- 分解
- 抽象
下面两段代码是写一个画图工具分别采用分解和抽象的处理方法.
可以看出抽象的处理方法更易于扩展(增加类, 而非在之前的函数中进行添加或修改),改变的地方更少. 随着设计模式的深入学习, 我们可以将抽象中函数的改变也去掉
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{
public:
Point p;
int r;
Circle(const Point & p, int r)
{
//...
}
};
class From;
class MainFrom : public Form {
private:
Point p1, p2;
vector<Line> lineVector;
vector<Rect> rectVector;
vector<Circle> rectVector; //改变
MainFrom()
{
//...
}
protected:
virtual void onMouseDown(const MouseEventArgs& e);
virtual void onMouseUp(const MouseEventArgs& e);
virtual void onPoint(const MouseEventArgs& e);
};
void MainFrom::onMouseDown(const MouseEventArgs& e)
{
p1.x = e.x;
p1.y = e.y;
From::onMouseDown(e);
}
void MainFrom::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();
From::onMouseUp(e);
}
void MainFrom::onPoint(const PointEventArgs& e)
{
for (int i = 0; i < lineVectror.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 < rectVectror.size(); i++) {
e.Graphics.DrawRectangle(Pens.red, RectVector[i].leftUp,
RectVector[i].width,
RectVector[i].height);
}
//改变
for (int i = 0; i < circleVectror.size(); i++) {
e.Graphics.DrawCircle(Pens.red, circleVector[i]);
}
From::onPoint(e);
}
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;
}
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;
}
virtual void Draw (const Graphics& g)
{
g.DrawReactangle(Pens.red, leftUp, width, height);
}
};
//增加
class Circle : public Shape{
public:
//实现自己的Draw,负责画自己
virtual void Draw(const Graphics& g){
g.DrawCircle(Pens.Red,
...);
}
};
class MainFrom : public From {
private:
Point p1;
Point p2;
vector<Shape*> shapeVector;
public:
MainFrom() { }
protected:
virtual void onMouseDown(const MouseEventArgs& e);
virtual void onMouseUp(const MouseEventArgs& e);
virtual void onPaint(const MouseEventArgs& e);
};
void MainFrom:: OnMouseUP(const MouseEventArgs& e)
{
p2.x = e.x;
p2.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);
}
什么是好的软件设计? 软件设计的金科玉律:
复用
重新认识面向对象
我们之前认识的面向对象就是封装 继承 多态, 但这是底层逻辑, 我们应该建立更高级别的抽象
理解隔离变化
- 从宏观层面来看, 面向对象的构建方式更能适应软件的变化, 能将变化所带来的影响减为最小
各司其职
- 从微观层面来看, 面向对象的方式更强调各个类的"责任"
- 由于需求变化导致的新增类型不应该影响原来类型的实现–是所谓各负其责
对象是什么?
- 从语言实现层面来讲, 对象封装了代码和数据
- 从规格层面来讲, 对象是一系列可被使用的公共接口
- 从概念层面来讲, 对象是拥有某种责任的抽象
面向对象设计原则
-
依赖倒置原则
- 高层模型(稳定)不应该依赖于底层模块(变化), 二者都应该依赖于抽象(稳定)
- 抽象(稳定)不应该依赖于实现细节(变化), 实现细节应该依赖于抽象(稳定)
-
开放封闭原则
- 对扩展开放, 对更改封闭
- 类模块应该是可扩展的, 但是不可修改的
-
单一职责原则(SPR)
- 一个类应该仅有一个引起它变化的原因
- 变化的方向隐含着类的责任
-
LisKov替换原则(LSP)
- 子类必须能够替换他们的基类(IS-A)
- 继承表达类型抽象
-
接口隔离原则(ISP)
- 不应该强迫客户程序依赖他们不用的方法
- 接口应该小而完备
-
优先使用对象组合, 而不是类继承
- 类继承通常为"白箱复用", 对象继承通常为"黑箱复用"
- 继承在某种程度上破坏了封装性, 子类父类耦合度高
- 而对象组合则只要求被组合的对象具有良好定义的接口, 耦合度低
-
封装变化点
- 使用封装来创建对象之间的分界层, 让设计者可以在分界层一侧进行修改, 而不会对一侧产生不良影响, 从而实现松耦合
-
针对接口编程, 而不是针对实现编程
- 不将变量类型声明为某个特定的具体类, 而是声明为某个接口
- 客户程序无需获知对象的具体类型, 只需要知道对象所具有的接口
- 减少系统中各部分的依赖关系, 从而实现"高内聚, 松耦合"的类型设计方案
产业强盛的标志—接口标准化
GOF-23模式分类
从目的来看:
- 创建型(Creational)模式: 将对象的部分创建工作延迟到子类或者其他对象, 从而应对需求变化为对象创建时具体类型实现引来的冲击.
- 结构型(Structural)模式 : 通过类继承或者对象组合获得更灵活的结构, 从而应对需求变化为对象的结构带来的冲击
- 行为型(Behavioral)模式: 通过类继承或者对象组合来划分类与对象间的职责, 从而应对需求变化为多个交互的对象带来冲击.
从范围来看:
- 类模式: 处理类与子类的静态关系, 继承
- 对象模式: 处理对象间的动态关系, 组合
好的面向对象设计: 应对变化, 提高复用
现代软件设计的特征是需求的频繁变化, 设计模式就是要寻找变化点, 然后在变化点出应用设计模式
敏捷软件开发提倡的是"Refactoring to Patterns"
重构关键技法:
1. 静态 -> 动态
2. 早绑定 -> 晚绑定
3. 继承 -> 组合
4. 编译时依赖 -> 运行时依赖
5. 紧耦合 -> 松耦合
下面的博客陆续更新各个设计模式, 敬请期待…