面向对象分析、设计及开发

本书由六大部分构成:开发概述、设计原则、设计常识、UML建模、设计模式与极限编程,系统性地总结并归纳了笔者在软件工程和面向对象程序设计、研发等领域里的实践经验和深入思考。这本书目前尚未完稿,本次公布的只是一个大体的框架与提纲。尽管如此,内容也已经很丰富了……

  · 开发概述
    · 简单归纳
    · 一般过程
    · 管理与分解复杂度
    · 类和对象的定位与关系
    · 模块化、封装与隐藏
  · 设计原则
    · 指导思想
    · 总的原则
    · 设计目标
    · 七大原则
  · 设计常识
    · 主要流程与步骤
    · 一些基本常识
  · UML建模
    · 泛化关系(Generalization
    · 实现关系(Realization
    · 一般关联(Association
    · 依赖关系(Dependency
    · 聚合关系(Aggregation
    · 组合关系(Composition
    · 常用图例
  · 设计模式
    · 组合模式(Composite Pattern
    · 观察者模式(Observer Pattern
    · 命令模式(Command pattern
    · 享元模式(Flyweight Pattern
    · 工厂模式(Factory Pattern
    · 备忘模式(Memento Pattern
    · 适配器模式(Adapter Pattern
    · 外观模式(Facade Pattern
    · 单例模式(Singleton Pattern
    · 桥接模式(Bridge Pattern
    · 迭代器模式(Iterate Pattern
  · 极限编程

开发概述


简单归纳

一般过程

管理与分解复杂度

类和对象的定位与关系

模块化、封装与隐藏

增强独立性,减少耦合度。独立性体现在两个方面,一是隐藏信息,二是减少外部依赖。就隐藏性而言,能够隐藏的信息越多,同一时间所需考虑和照顾的信息就越少;所需考虑和照顾的信息越少,则忘记某项信息而犯错误的几率就越小,越能营造轻松编程,各司其责,互不影响的良好环境,越容易调试与集成,也容易维护和扩展(升级)。这些是编程方式由机器码进化到高级语言,由语句进化到过程,由过程进化到结构(函数),由结构进化到面向对象(类与对象)的本质主线。也是尽量避免使用全局数据的最根本由来(破坏模块与类的独立性)。

设计原则


指导思想

总的原则

设计目标

七大原则

  1. 单一职责:不要将太多的职责(功能)放在一个类中。这是实现高内聚、低耦合的指导方针。
  2. 开闭原则:软件实体应对扩展开放,对修改关闭。软件实体可以是一个系统模块,一组类或一个独立的类。具体实践中,通常是确定基类,通过派生子类来扩展或修改基类的功能。使用继承关系完成抽象到具体的转换,针对基类接口进行编程。
  3. 里氏代换:使用继承方式创建新类时,要严格遵守此原则,即:所有使用基类对象之处,皆可以子类对象代之。将基类对象替换为子类对象,程序不会产生任何错误和异常,但反之不成立。编程时,尽量使用基类类型进行定义,运行时再绑定具体的子类。这些属于面向对象多态性的一种灵活运用。使用里氏代换需注意两点:一是子类的所有方法必须在基类中声明,否则,需借助强制类型转换来调用基类中没有的方法,严格来说,这样并不符合里氏代换。二是尽量将基类设计为抽象类。
  4. 依赖倒转:系统抽象化的具体实现,即尽量针对抽象类的方法编程,而不要针对具体类的方法进行编程。里氏代换是依赖倒转的基础。在以类的对象进行传参时(即参数是类的对象),尽量将该参数声明为抽象类的指针或引用,而不要声明为具体类的对象。这种方式也可称为“依赖式注入”,具体有3种情况:构造注入(B类对象作为A类的构造参数),设置注入(A类中有一个数据成员为B类对象,而后在A类中专门写一个设置函数,该函数的参数为B类对象,将该参数赋值给数据成员,这样A类即可长期持有B类的对象),接口注入(A类中某个函数的参数为B类对象,仅在该函数中使用此对象)。
  5. 接口隔离:单一职责的另一种体现。如果某个类的功能和接口太多太大,则将其分割成多个小类。也适用于某个具体的函数。实际上是为了实现单一职责。
  6. 合成复用:创建新类时尽量使用组合或聚合的方式(其他类的对象作为新类的数据成员或临时调用),而少用或不用继承的方式。这也是为了实现低耦合与高内聚,减少外部依赖性(继承是最强的依赖)。
  7. 迪米特法则:不要与陌生人说话,只与最密切相关的类直接通信。即每个类都要尽量少的与其他类发生直接交互。这也是为了提高封装内聚,减少耦合依赖。如果必须与其他关系不紧密的类通信,则通过组合的方式用第三者来转发调用。

设计常识


使用C++进行软件开发,设计远比编码复杂而困难。无论项目大小,在开始编码之前,务必要进行全面的分析与思考。设计要以明确、清晰的文档来呈现,文字说明、注解、备忘、图示、表格、流程图、UML图、思维导图等等都是必不可少的文档元素。

主要流程与步骤

一些基本常识

UML建模


UML:统一建模语言。UML并非计算机编程语言,而是一种专用于软件系统的分析与设计的可视化图形文字表示方法,尤其适合于OOAD(面向对象分析与设计)和系统建模。UML 2.0定义了13种图示(Diagram),最常用的有3种:类图,用例图,时序图。类图中,类之间的关系有6种:泛化、实现、一般关联、依赖、聚合、组合。前两种为类的继承(is-a),后4种为类的包含(has-a)。

泛化关系(Generalization

子类继承非抽象基类,符号为直线和空心箭头。箭头指向基类。

实现关系(Realization

子类继承并实现抽象基类,符号为虚线和空心箭头。箭头指向基类。

一般关联(Association

类的对象之间在概念上有连接关系,但是两个类各自独立,不存在包含关系和依赖性。一般关联的符号为直线和实心箭头。双向关联为一条直线,无箭头。注意:关联关系中有一对多(B中有多个A的对象)和多对一(多个B中皆有A的对象)等多种模式,还包括本类的自关联(B中有本类的指针)。


class ClassA; // ClassB类的头文件中前向声明ClassA类

// ClassB类的类定义,数据成员中虽有ClassA的指针,但该对象并不重要
class ClassB 
{
    // ClassA类的指针可为nullptr,两个类的关系很松散
    void doSomething (ClassA* classA);
};

依赖关系(Dependency

B类的函数需使用A类的对象或B类的函数创建并返回A类对象,则修改A类会直接影响到B类,即B类依赖于A类对象。一个类的对象向另一个类的对象发送消息,使该类能正常工作,或者一个类的成员函数需要另一个类的对象作为其参数,或者成员函数中调用另一个类的成员函数来实现某个功能等等,这些都是典型的依赖关系。依赖关系的符号为虚线和实心箭头。B依赖于A,则箭头由B指向A。注意,B类的数据成员中并无A类的对象,A类的数据成员中也无B类的对象。

聚合关系(Aggregation

两个类的对象分别代表整体和部分,但是这两者各自独立,不存在一方消失,另一方也随之消失的问题,“部分”可以隶属于多个“整体”。通常为构造参数或以外部传参的方式获得赋值的数据成员。或者这样理解:B类的成员函数参数中需要A类的对象(接口需要)。聚合关系的符号为空心菱形、直线和实心箭头。B聚合了A,则箭头指向A。


class ClassA; // ClassB类的头文件中前向声明ClassA类

// ClassB类的类定义,数据成员中有ClassA的指针,该对象由构造函数传入
class ClassB 
{
    // ClassB的构造参数为ClassA的指针
    ClassB (ClassA* a) : classA (a) { } 
    ClassA* classA;  // B聚合了A。或者说:B中有A,各自独立
};

组合关系(Composition

最强烈的关联关系(属于部分与整体的关系,但“部分”只能隶属于“整体”),“整体”不存在了,“部分”也随之消失。比如内容组件中的控件,内容组件消失,控件也随之消失。通常,组合而来的对象为:需由本类负责创建、管理并销毁的数据成员。或者这样理解:B类创建、持有并管理A类的对象(实现需要,并且往往是构造函数中创建,析构函数中销毁之)。组合关系的符号为实心菱形、直线和实心箭头(双向可达性无箭头)。B组合了A,则箭头指向A。

 // ClassB类的头(源)文件中需包含ClassA类的类定义,否则无法创建该类的对象
#include “ClassA”

// ClassB类的数据成员中有ClassA的指针,该对象由本类创建和管理
class ClassB 
{
public:
    // ClassB的构造函数中创建ClassA的堆中对象
    ClassB () : classA ( new ClassA() ) { }

    // ClassB的析构函数中销毁ClassA的堆中对象
    ~ClassB () { deleteAndZero (classA); }

private:
    ClassA* classA;  // B组合了A。或者说:B中有A,同生共死
};

常用图例

无论是研究源码,还是前期设计,画出类图和时序图都是非常重要的一步。可使用EA、PowerDesigner等UML工具软件这件事。常用的3种图例见下:

用例图(Use Case Diagram

表示用户操作的一种图例。下图中guest可以访问query模块,而Admin则可以访问所有模块。

类图(Class Diagram

比较重要和常用的UML图例,表示整个系统或系统某个模块中类与类之间的关系。

时序图(Sequence Diagram

表示某个函数中的调用过程,特别是该函数中调用了其他类的函数。函数的返回线可不标注。

类图中成员标识符(数据成员的名称和函数名)在左侧,之后是“:”,冒号右侧是函数的返回类型或对象的类型。这么做的意义是:先看看有什么,名字是什么,能做什么,而后是该函数返回什么类型,或者该对象是什么类型的。这种方式虽然与C++中的声明对象、变量和函数的语法顺序相反,但更适合于阅读和理解。成员名字左侧的“+”表示该成员为public属性,“-”为private,“#”为protected。

比如:上图对应的C++语句为:


class MyClass
{
public:
    void setValue(const int value);
    const int getValue();

protected:
    void timerCallback();

private:
    AudioSource* audioSource;
};

设计模式


软件开发的前两个步骤总是需求分析和架构各个功能框架,功能框架可称为“架构模式”。架构之后,需使用一到多个设计模式进一步细化和完成之。设计模式精炼了众多现成的解决方案,可用于解决软件开发中最常见的设计问题。与完全自定义的设计方案相比,设计模式的特点有:

除了“四人帮”罗列的那一堆设计模式之外,实际上在现代面向对象的软件工程中还有一些常用、常见的模式,或者“模式变体”。此处仅列出部分比较大众和常用的。

组合模式(Composite Pattern

结构性模式,组合多个对象形成树型结构以表示“整体-部分”的结构层次。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性(相同的接口)。叶子对象和容器对象均继承自同一个基类(因此具有相同的接口),客户端直接针对基类编程。文件和目录浏览遍历、XML遍历、复杂的GUI界面之组成均属于典型的组合模式(以JUCE GUI为例:控件和控件容器具有相同的接口,均继承自Component类。客户端仅针对基类Component类编程即可)。

观察者模式(Observer Pattern

行为型模式,又称为“发布-订阅”、“模型-视图”、“来源-监听”模式。该模式定义了对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象均得到通知并被自动更新。观察者模式有两类角色,一是被动改变的观察者(视图、监听),二是产生变化的被观察者(数据来源、模型、观察目标)。数据来源和模型只有一个,而观察者却可以有多个。如果数据来源和模型调用成员函数后发生改变或产生事件,观察者就会及时响应,做出处理。基于消息循环的GUI程序,其事件驱动机制就采用了观察者模式。经典的MVC(模型-视图-控制器)框架也应用了观察者模式。JUCE类库的消息机制同样大量采用了这种模式。比如:“发布类”嵌套的Listener抽象基类,“订阅类”继承该抽象基类,实现纯虚函数,“发布类”在“订阅类”中注册捕获器,从而使“订阅类”能够处理“发布类”所产生的消息。

命令模式(Command pattern

行为型模式,在发送者和接收者之间增加一个命令层(抽象命令及其派生的各个具体命令),将发送者的请求封装在命令类的对象中,再通过命令对象来调用接收者的方法。

该模式用于处理对象之间的调用,将发送者和接收者解耦,两者之间互不知情、各司其职,通过命令对象来中转,从而增加灵活性。该模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开来,并引入抽象的命令接口,发送者针对该接口进行编程,而实现了抽象基类的具体命令则与接收者相关联,从而使接收者能够执行发送者的命令。

如果在命令类中同时提供撤销请求的方法,则该模式可实现应用程序中的“撤销/重做”功能。GUI程序中的“快捷键”、点击按钮后执行某个操作处理等功能也往往采用了命令模式。即:大多数GUI类库中,提供了命令的发送者、接收者和抽象命令类。实际编程中,仅需自定义具体命令类或实现该基类的某些功能性纯虚函数即可(比如JUCE类库中实现Undo/Redo功能和ButtonListener类的buttonClicked()等等),而增加新命令,无需修改已有的三个类(发送者、抽象命令、接收者),在“单一职责”的基础上满足了程序设计中的“开闭原则”。此外,还可实现多个命令的批量处理。

享元模式(Flyweight Pattern

结构型模式,通过共享技术实现相同或相似的细粒度对象的复用,节省内存,提高性能。现代C++编程中常用的引用计数(隐式共享、写时复制)等技术就是基于享元模式的(比如JUCE类库中的ReferenceCountedObject、Value、String、var等类)。

工厂模式(Factory Pattern

创建型模式。工厂模式有简单工厂、抽象工厂、工厂方法等多个变形。其核心思想是:将对象的创建与对象的使用分割开来,互不影响。创建对象由工厂类负责,其中,抽象工厂模式中,工厂类有一个抽象基类,其派生类负责创建具体的对象。所创建的对象则为另一个类系。客户端使用对象时,直接与工厂类打交道,让工厂类提供所需的对象,而不是直接创建之。

备忘模式(Memento Pattern

行为型模式,将A对象在某一时刻的状态(属性、数据)保存到B类对象中(此时,A对象的某个成员函数创建B类对象,return new B();返回指针即可,使之保存自己当前的状态。B类往往是带参构造,构造参数即A类的属性数据。所创建的B对象由客户端负责管理)。需要恢复状态时,A对象调用自己的另一个成员函数(该函数的参数即为B类对象,该对象由客户端负责提供),读取B对象中保存的数据并重新设置自己的属性。该模式有三个角色:① 需要备忘的A对象。② A对象所创建的B类对象的堆中对象,该对象的地址由客户端负责管理并赋值给B类的指针,每个对象保存A对象在某一时刻的状态。③ 客户端。

适配器模式(Adapter Pattern

结构型模式。将一个类的接口转化为客户希望的另一个接口,使得原本由于接口不兼容的类可以一起工作。该模式有两种情况:类适配器(采用多重继承的思路)和对象适配器(对象组合的思路)。该模式堪称跨平台类库的设计之基。深入研究JUCE等优秀的开源类库,即可体会这种模式的比比皆是。

外观模式(Facade Pattern

多个类或子系统协同实现某个功能,客户端并不直接与这些类打交道,而是将这些类的对象组合到一个外观类中,客户端使用外观类的对象完成所需的功能。

单例模式(Singleton Pattern

确保某个类最多只能实例化一个对象,提供一个访问该对象的全局访问点(静态函数)。其核心思想是:让该类负责创建并保存自己的唯一实例(类中声明一个静态数据成员为本类指针,而后写一个public静态函数创建并返回之。如果尚未创建,则创建,已经创建,则直接返回),用户无法通过正常方式实例化其对象(将构造函数和拷构函数声明为非public,也因此,单例类丧失了派生子类和多态的能力)。需注意线程安全和可重入的问题(因为类中有静态数据)。

桥接模式(Bridge Pattern

结构型模式,将继承关系转换为组合关系,即:将A类继承自B类,转换为A类中组合B类的对象(指针),A类对象调用某个函数时,该函数的参数为B类对象(指针),该函数所完成的具体功能,其实是B类对象(指针)调用其具体的功能性函数。A类和B类可为抽象基类。

迭代器模式(Iterate Pattern

提供了一个机制,使得操作或算法能够顺序访问容器(数据对象)的元素,将容器的数据与操作(属性和行为)进行解耦。C++ STL和JUCE类库的底层数据容器(及算法实现)大量使用了这种模式。

极限编程


又称“XP编程”,软件工程的最佳实践之一,有12个主要做法,体现在整个开发过程中的各个阶段:

作者:SwingCoder


如果本文对您有所启发或助益,请微信打赏

Email: underwaySoft@126.com 微信公众号: UnderwaySoft