最近在学习李建忠老师的《设计模式》,遂开新坑。想在这个系列里记录一些我自己对这门课的理解和感悟。
本文通过一个画图板程序的案例来理解软件设计的目标和面向对象的设计原则。
画图板程序的设计
如果我们现在要编写一个画图板的程序,这个程序要实现画点和画直线的功能。我们将这个问题分解为在已知的画板上新增一个点或者新增一条直线的功能,和把更新显示到画板上的功能。
我们可以利用多态性设计基类Shape
,然后继承出两个子类Point
和Line
,然后在MainForm
类中,使用一个向量存Shape
子类的指针。
为什么要利用多态性设计基类呢?考虑如果我们不设计这个基类,MainForm
中就需要两个vector
来分别存取Point
和Line
的对象,这样如果我们需要新增一个Rectangle
类的话,需要在属性中加入一个向量,然后在paint
方法中用三个for
循环来实现绘图。但是如果利用多态性的话,既不需要增加vector
,也不需要多写循环,原来的MainForm
类是不需要改动的,只需要新增一个Rectangle
类即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
#include <vector> #include <iostream> #include <string> class Shape { public: virtual std::string toString() = 0; virtual void draw() { std::cout << toString() << std::endl; } virtual ~Shape() {} }; class Point : public Shape { private: int x; int y; public: Point(int x_ = 0, int y_ = 0) : x(x_), y(y_) {} std::string toString() { return std::string("Point Object") .append("(") .append(std::to_string(x)) .append(", ") .append(std::to_string(y)) .append(")"); } }; struct Line : public Shape { private: Point start; Point end; public: Line(Point start_ = Point(), Point end_ = Point()) : start(start_), end(end_) {} std::string toString() { return std::string("Line Object") .append("[") .append(start.toString()) .append(", ") .append(end.toString()) .append("]"); } }; class MainForm { private: Point pointStart; Point pointEnd; std::vector <Shape *> shapeObjects; public: MainForm() {} ~MainForm() { for (auto shape: shapeObjects) { delete shape; } } void paint() { for (auto shape: shapeObjects) { shape->draw(); } } void addObject(Shape *object) { shapeObjects.push_back(object); } }; int main() { MainForm form; form.addObject(new Point(1, 1)); form.addObject(new Line(Point(1, 1), Point(2, 2))); form.paint(); return 0; } |
设计模式要解决的问题
如上面的例子所示,设计模式需要解决件设计复杂性的问题,我们利用分解和抽象的方法来设计软件,实现软件的功能。在今天这个需求快速变化的开发时代,设计模式还需要解决应对需求变化的问题,即做到代码的可复用,也就是说如果我们新增了一个需求,我们只要新增很少的代码,而不是改动大部分模块的内部实现,这就要求我们在模块设计的时候要做到“松耦合”。
在上面的例程中,运用多态性之后新增的需求变化不会影响原来的代码,这就体现了“松耦合”的设计。
理解面向对象
从面向对象的三大机制来理解,“封装性”要求隐藏内部实现,“继承性”要求复用现有代码,“多态性”要求改写用户行为。
从抽象的思维层面理解,面向对象要求隔离变化和各司其职。从宏观层面来讲,软件的设计应该能够适应软件的变化,能够将变化带来的影响减为最小;从微观层面来讲,每一个部分都有自己的“责任”,一个模块的功能不能太多,要恰到好处,才能各司其职。
面向对象的设计原则
依赖倒置原则。稳定的高层模块不应该依赖于不稳定的低层模块,比如MainForm
不应该直接依赖于Point
或者Line
,而是应该借助Shape
这个稳定的抽象,来和Point
与Line
建立联系。而稳定的抽象不依赖于不稳定的实现细节,但是实现细节依赖于抽象。
开放封闭原则。对扩展开放,对修改封闭。这里体现为MainForm
模块应当是可拓展的,但是不可修改。
单一职责原则。一个类不要包含很多个功能,一个类仅有一个引起变化的原因,变化的方向隐含着类的责任。
Liskov替换原则。子类可以替换父类,子类可以使用父类中的方法。
接口隔离原则。不应该强迫用户程序依赖他们不用的方法,接口应该小而完备。
优先使用对象组合,而不是继承。
封装变化点。使用封装创建对象之间的分界层,使得一侧变化、一侧稳定,从而实现“松耦合”。
针对接口编程,而不是针对实现编程。不声明具体的类,而是声明接口;客户程序无需知道对象的具体类型,只需要知道对象的接口。这样可以减少系统各部分的依赖关系,实现“高内聚,低耦合”。
说点什么