开放封闭原则(Open Closed Principle)

2023-11-03

在面向对象的设计中有很多流行的思想,比如说 “所有的成员变量都应该设置为私有(Private)”,“要避免使用全局变量(Global Variables)”,“使用运行时类型识别(RTTI:Run Time Type Identification,例如 dynamic_cast)是危险的” 等等。那么,这些思想的源泉是什么?为什么它们要这样定义?这些思想总是正确的吗?本篇文章将介绍这些思想的基础:开放封闭原则(Open Closed Principle)

Ivar Jacobson 曾说过 “所有系统在其生命周期中都会进行变化,只要系统要开发一个版本以上这一点就需时刻记住。”。

All systems change during their life cycles. This must be borne in mind when developing systems expected to last longer than the first version.

那么我们到底如何才能构建一个稳定的设计来面对这些变化,以使软件生命周期持续的更长呢?

早在 1988 年 Bertrand Meyer 就给出了指导建议,他创造了当下非常著名的开放封闭原则。套用他的原话:“软件实体(类、模块、函数等)应对扩展开放,但对修改封闭。”。

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

当一个需求变化导致程序中多个依赖模块都发生了级联的改动,那么这个程序就展现出了我们所说的 “坏设计(bad design)” 的特质。应用程序也相应地变得脆弱、僵化、无法预期和无法重用。开放封闭原则(Open Closed Principle)即为解决这些问题而产生,它强调的是你设计的模块应该从不改变。当需求变化时,你可以通过添加新的代码来扩展这个模块的行为,而不去更改那些已经存在的可以工作的代码。

开放封闭原则(Open Closed Principle)描述

符合开放封闭原则的模块都有两个主要特性:

1. 它们 “面向扩展开放(Open For Extension)”。

也就是说模块的行为是能够被扩展的。当应用程序的需求变化时,我们可以使模块表现出全新的或与以往不同的行为,以满足新的需求。

2. 它们 “面向修改封闭(Closed For Modification)”。

模块的源代码是不能被侵犯的,任何人都不允许修改已有源代码。

看起来上述两个特性是互相冲突的,因为通常扩展模块行为的常规方式就是修改该模块。一个不能被修改的模块通常被认为其拥有着固定的行为。那么如何使这两个相反的特性共存呢?

抽象是关键。

Abstraction is the Key.

在使用面向对象设计技术时,可以创建固定的抽象和一组无限界的可能行为来表述。这里的抽象指的是抽象基类,而无限界的可能行为则由诸多可能衍生出的子类来表示。为了一个模块而篡改一个抽象类是有可能的,而这样的模块则可以对修改封闭,因为它依赖于一个固定的抽象。然后这个模块的行为可以通过创建抽象的衍生类来扩展。

示例:Client/Server 引用

图 1 展示了一个简单的且不符合开放封闭原则的设计。

(图 1: 封闭的 Client)

Client 和 Server 类都是具体类(Concrete Class),所以无法保证 Server 的成员函数是虚函数。 这里 Client 类使用了 Server 类。如果我们想让 Client 对象使用一个不同的 Server 对象,那么必须修改 Client 类以使用新的 Server 类和对象。

图 2 中展示了符合开放封闭原则的相应设计。

(图 2: 开放的 Client)

在这个示例中,AbstractServer 类是一个抽象类,并包含一个纯虚成员函数。Client 类依赖了这个抽象,但 Client 类将使用衍生的 Server 类的对象实例。如果我们需要 Client 对象使用一个不同的 Server 类,则可以从 AbstractServer 类衍生出一个新的子类,而 Client 类则依然保持不变。

示例:Shape 抽象

考虑下面这个例子。我们有一个应用程序需要在标准 GUI 窗口上绘制圆形(Circle)和方形(Square)。圆形和方形必须以特定的顺序进行绘制。圆形和方形会被创建在同一个列表中,并保持适当的顺序,而程序必须能够顺序遍历列表并绘制所有的圆形和方形。

在 C 语言中,使用过程化技术是无法满足开放封闭原则的。我们可能会通过下面代码显示的方式来解决该问题。

1 enum ShapeType {circle, square};
 3 struct Shape 
 4 {
 5     ShapeType itsType;
 6 };
 8 struct Square 
 9 {
10     ShapeType itsType;
11     double itsSide; 
12     Point itsTopLeft;
13 };
15 struct Circle 
16 {
17     ShapeType itsType;
18     double itsRadius; 
19     Point itsCenter;
20 };
22 void DrawSquare(struct Square\*);
23 void DrawCircle(struct Circle\*);
24 typedef struct Shape \*ShapePointer;
26 void DrawAllShapes(ShapePointer list\[\], int n)
27 {
28     int i;
29     for (i=0; i<n; i++)
30     {
31         struct Shape\* s = list\[i\];
32         switch (s->itsType)
33         {
34             case square:
35                 DrawSquare((struct Square\*)s);
36                 break;
37             case circle:
38                 DrawCircle((struct Circle\*)s);
39                 break;
40         }
41     }
42 }

在这里我们看到了一组数据结构定义,这些结构中除了第一元素相同外,其他都不同。通过第一个元素的类型码来识别该结构是在表示一个圆形(Circle)还是一个方形(Square)。函数 DrawAllShapes 遍历了数组中的结构指针,检查类型码然后调用相匹配的函数(DrawCircle 或 DrawSquare)。

这里函数 DrawAllShapes 不符合开放封闭原则,因为它无法保证对新的 Shape 种类保持封闭。如果我们想要扩展这个函数,使其能够支持一个图形列表并且包含三角形(Triangle)定义,则我们将不得不修改这个函数。事实上,每当我们需要绘制新的图形种类时,我们都不得不修改这个函数。

当然这个程序仅仅是一个例子。在实践中 DrawAllShapes 函数中的 switch 语句将不断地在应用程序内的各种函数间不断的调用,而每个函数只是少许有些不同。在这样的应用中增加一个新的 Shape 意味着需要搜寻所有类似的 switch 语句(或者是 if/else 链)存在的地方,然后增加新的 Shape 功能。此外,要让所有的 switch 语句(或者是 if/else 链)都有类似 DrawAllShapes 函数这样较好的结构也是不太可能的。而更有可能的则是 if 语句将和一些逻辑运算符绑定到了一起,或者 switch 语句中的 case 子句的堆叠。因此要在所有的位置找到和理解这些问题,然后添加新的图形定义可不是件简单的事情。

下面这段代码展示了符合开放封闭原则的 Cicle/Square 问题的一个解决方案。

1   public abstract class Shape
 2   {
 3     public abstract void Draw();
 4   }
 6   public class Circle : Shape
 7   {
 8     public override void Draw()
 9     {
10       // draw circle on GUI
11     }
12   }
14   public class Square : Shape
15   {
16     public override void Draw()
17     {
18       // draw square on GUI
19     }
20   }
22   public class Client
23   {
24     public void DrawAllShapes(List<Shape> shapes)
25     {
26       foreach (var shape in shapes)
27       {
28         shape.Draw();
29       }
30     }
31   }

在这个例子中,我们创建了一个 Shape 抽象类,这个抽象类包含一个纯虚函数 Draw。而 Circle 和 Square 都衍生自 Shape 类。

注意在这里如果我们想扩展 DrawAllShapes 函数的行为来绘制一个新的图形种类,我们所需要做的就是增加一个从 Shape 类衍生的子类。而 DrawAllShapes 函数则无需进行修改。因此 DrawAllShapes 符合了开放封闭原则,它的行为可以不通过对其修改而扩展。

在比较现实的情况中,Shape 类可能包含很多个方法。但是在应用程序中增加一个新的图形仍然是非常简单的,因为所需要做的仅是创建一个衍生类来实现这些函数。同时,我们也不再需要在应用程序内查找所有需要修改的位置了。

因为更改符合开放封闭原则的程序是通过增加新的代码,而不是修改已存在的代码,之前描述的那种级联式的更改也就不存在了。

策略性的闭合(Strategic Closure)

要明白程序是不可能 100% 完全封闭的。例如,试想上面的 Shape 示例,如果我们现在决定所有的 Circle 都应该在 Square 之前先进行绘制,则 DrawAllShapes 函数将会发生什么呢?DrawAllShapes 函数是不可能对这样的变化保持封闭的。通常来说,无论模块的设计有多封闭,总是有各种各样的变化会打破这种封闭。

因此,完全闭合是不现实的,所以必须讲究策略。也就是说,程序设计师必须甄别其设计对哪些变化封闭。这需要一些基于经验的预测。有经验的设计师会很好的了解用户和所在的行业,以判断各种变化的可能性。然后可以确定对最有可能的变化保持开放封闭原则。

使用抽象来获取显示地闭合

那我们该如何使 DrawAllShapes 函数对绘制逻辑中的排序的变化保持闭合呢?要记住闭合是基于抽象的。因此,为了使 DrawAllShapes 对排序闭合,则我们需要对排序进行某种程度的抽象。上述例子中关于排序的一个特例就是某种类别的图形需要在其他类别的图像之前进行绘制。

一个排序策略就是,给定任意两个对象,可以发现哪一个应当被先绘制。因此,我们可以在 Shape 中定义一个名为 Precedes 的方法,它可以接受另一个 Shape 作为参数并返回一个 bool 类型的结果。如果结果为 true 则表示接收调用的 Shape 对象应排在被作为参数的 Shape 对象的前面。

我们可以使用重载操作符技术来实现这样的比较功能。这样通过比较我们就可以得到两个 Shape 对象的相对顺序,然后排序后就可以按照顺序进行绘制。

下面显示了简单实现的代码。

1   public abstract class Shape
 2   {
 3     public abstract void Draw();
 5     public bool Precedes(Shape another)
 6     {
 7       if (another is Circle)
 8         return true;
 9       else
10         return false;
11     }
12   }
14   public class Circle : Shape
15   {
16     public override void Draw()
17     {
18       // draw circle on GUI
19     }
20   }
22   public class Square : Shape
23   {
24     public override void Draw()
25     {
26       // draw square on GUI
27     }
28   }
30   public class ShapeComparer : IComparer<Shape>
31   {
32     public int Compare(Shape x, Shape y)
33     {
34       return x.Precedes(y) ? 1 : 0;
35     }
36   }
38   public class Client
39   {
40     public void DrawAllShapes(List<Shape> shapes)
41     {
42       SortedSet<Shape> orderedList =
43         new SortedSet<Shape>(shapes, new ShapeComparer());
45       foreach (var shape in orderedList)
46       {
47         shape.Draw();
48       }
49     }
50   }

这达成了排序 Shape 对象的目的,并可按照适当的顺序进行排序。但我们仍然还没有一个合适的排序抽象。以现在这种情况,单独的 Shape 对象将不得不覆写 Precedes 方法来指定顺序。这将如何工作呢?我们需要在 Precedes 中写什么样的代码才能确保 Circle 能够在 Square 之前绘制呢?

1     public bool Precedes(Shape another)
2     {
3       if (another is Circle)
4         return true;
5       else
6         return false;
7     }

可以看出,这个函数不符合开放封闭原则。无法使其对新衍生出的 Shape 子类保持封闭。每次当一个新的 Shape 衍生类被创建时,这个方法将总是被修改。

使用 “数据驱动(Data Driven)” 的方法来达成闭合

使用表驱动(Table Driven)方法能够达成对 Shape 衍生类的闭合,而不会强制修改每个衍生类。

下面展示了一种可能的设计。

1     private Dictionary<Type, int> \_typeOrderTable = new Dictionary<Type, int>();
 3     private void Initialize()
 4     {
 5       \_typeOrderTable.Add(typeof(Circle), 2);
 6       \_typeOrderTable.Add(typeof(Square), 1);
 7     }
 9     public bool Precedes(Shape another)
10     {
11       return \_typeOrderTable\[this.GetType()\] > \_typeOrderTable\[another.GetType()\];
12     }

通过使用这种方法我们已经成功地使 DrawAllShapes 函数在一般情况下对排序问题保持封闭,并且每个 Shape 的衍生类都对新的 Shape 子类或者排序策略的修改(例如修改排序规则以使先绘制 Square)等保持封闭。

这里仍然无法对多种 Shape 的顺序保持封闭的就是表(Table)本身。但我们可以将这个表定义放置在单独的模块中,使表与其他模块隔离,这样对表的更改则不再会对任何其他模块产生影响。

进一步的扩展闭合

这并不是故事的尾声。

我们可以掌控 Shape 的层级结构和 DrawAllShapes 函数对依据不同 Shape 类型的排序规则的闭合。尽管如此,Shape 的衍生类对不判断图形类型的排序规则是非闭合的。看起来可能我们希望可以根据更高级别的结构来对 Shape 进行排序。对这个问题的一个完整的研究已经超出了这篇文章的范围,但是感兴趣的读者可以考虑如何实现。例如让一个 OrderedShape 类来持有一个抽象的 OrderedObject 类,而其自身同时继承自 Shape 和 OrderedObject 类的实现。

总结

关于开放封闭原则(Open Closed Principle)还有很多可以讲的。在很多方面这个原则都是面向对象设计的核心。始终遵循该原则才能从面向对象技术中持续地获得最大的益处,例如:可重用性和可维护性。同时,对该原则的遵循也不是通过使用一种面向对象的编程语言就能够达成的。更确切的说,它需要程序设计师更专注于将抽象技术应用到程序中那些趋于变化的部分上。

面向对象设计的原则

 SRP

 单一职责原则

 Single Responsibility Principle

 OCP

 开放封闭原则

 Open Closed Principle

 LSP

 里氏替换原则

 Liskov Substitution Principle

 ISP

 接口分离原则

 Interface Segregation Principle

 DIP

 依赖倒置原则

 Dependency Inversion Principle

 LKP

 最少知识原则

 Least Knowledge Principle

参考资料

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

开放封闭原则(Open Closed Principle) 的相关文章

  • 数据挖掘入门

    目录 前言 简介 挖掘对象 挖掘步骤 分析方法 常用算法 神经网络法 决策树法 遗传算法 粗糙集法 模糊集法 关联规则法 面对的挑战 前言 不知不觉就步入了研究生的大门 蓦然回首 大学四年时光如流水 正如歌词唱的那样 转眼就各奔东西 工作的
  • R语言grep函数用法

    通常被用来进行数值计算比较多 字符串处理相对较少 而且关于字符串的函数也不多 用得多的就是substr strsplit paste regexpr这几个了 实际上R关于字符串处理的功能是非常强大的 因为它甚至可以直接使用Perl的正则表达
  • PYTHON自动化框架总结:SELENIUM+PYTEST+ALLURE

    文章目录 一 PYTEST 1 使用pytest执行测试需要遵循的规则 2 数据驱动 3 pytest fixture 4 结合ALLURE报告 5 pytest assume 二 接口自动化内容 1 接口自动化重要性 2 接口自动化平台的
  • CTFshow web入门——文件上传

    目录 Web 151 考点 后端无验证 前端校验 Web 152 考点 绕过前端校验 Web 153 考点 文件后缀名 黑名单 Web 154 155 考点 文件内容过滤 过滤关键字是php Web 156 158 Web159 Web 1
  • 解决:Hbuilder工具点击发行打包,一直报尚未完成社区身份验证,请点击链接xxxxx,项目xxx发布H5失败的错误。[Error]尚未完成社区身份验证

    全世界任何漂亮有魅力的女生 都不会因为你送她汉堡或奶茶而对你说我爱你 明白吗 你应该带她做一些特别的事情 让她感动 日常笔记 解决 Hbuilder工具点击发行打包 一直报尚未完成社区身份验证 请点击链接xxxxx 项目xxx发布H5失败的
  • QT之运行exe文件时缺少xxx.dll文件

    使用QT最大的好处和方便的可能就是他的输出为exe文件 将这个文件直接复制到其他人的电脑上便可以直接运行 十分的便捷 但是我们有时候我们发现我们自己写的QT生成的exe文件没有办法直接在自己的电脑上打开 或者有时候copy别人的项目时 发现
  • java中的几种加密方式

    第一种 DES加解密 import java security Key import java security SecureRandom import javax crypto Cipher import javax crypto Key
  • React-resize窗口监听

    import React useState useEffect useCallback from react 创建自定义函数来写入方法 function UseWillSize const size setSize useState wid
  • [CVPR-21] Scale-aware Automatic Augmentation for Object Detection

    Scale aware Automatic Augmentation for Object Detection code GitHub dvlab research SA AutoAug Scale aware Automatic Augm
  • Qt概述和项目文件介绍

    注意 学习本专栏的内容需要了解C 相关知识 会涉及到C 基础语法和相关特性 可以参考我的另一个专栏 c 从零开始 小梁今天敲代码了吗的博客 CSDN博客 目录 一 什么是Qt 二 Qt的案例 三 项目文件介绍 一 什么是Qt 是一个跨平台的
  • gRPC的介绍、安装与使用

    1 gRPC是什么 在 gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法 使得您能够更容易地创建分布式应用和服务 与许多 RPC系统类似 gRPC也是基于以下理念 定义一个服务 指定其能够被远程调用的方法
  • OpenCV(四)——图像特征与目标检测

    课程一览 目录 1 图像特征的理解 2 形状特征描述 2 1 HOG原理 2 2 Harris 2 3 SIFT
  • android上java.util.ConcurrentModificationException错误解决

    今天在运行Android代码 发现程序崩溃了 发现错误为 java util ConcurrentModificationException 错误栈在list里面 然后 看异常变量 为list本身错误 笔者马上想起来了 list和vecto
  • 计算机网络--IP数据报格式

    解析 1 版本 占4位 指IP协议的版本 通信双方使用的IP协议版本必须一致 目前广泛使用的IP协议版本号为4 即IPv4 2 首部长度 占4位 可表示的最大十进制数值是15 请注意 这个字段所表示数的单位是32位字长 1个32位字长是4字
  • WIN10上UG10.0安装步骤及出错解决

    UG10 0安装步骤及出错解决 准备安装包UG10 0 安装软件 安装java虚拟机 运行launch 安装授权 安装主文件 破解 准备安装包UG10 0 下载链接 提取码 s4dh 安装软件 安装java虚拟机 以管理员身份运行 运行la
  • yaml文件的数据

    com alipay function test base TestData caseName 1 查询店铺详情 queryStoreDetailV2 description normal dataItems loginid 1342708
  • 使用JPA根据实体类生成数据库表

    springboot数据库 一 springboot JPA JPA springboot jpa 数据库的一系列的定义数据持久化的标准的体系 学习的目的是 利用springboot实现对数据库的操作 第一步 添加springboot da
  • 获取JavaScript时间戳函数的方法和js时间戳转时间方法

    文章目录 一 JavasCRIPT时间转时间戳 方法一 Date now 方法三 valueOf 方法四 getTime 方法五 Number 二 js时间戳转时间 方法一 生成 yyyy MM dd 上 下 午hh mm ss 格式 方法
  • java字符串

    java字符串 java字符串 一 String类 一 特点 二 构造方法 String str abc 与 String str2 new String abc 的区别 三 常用方法 intern String类拼接 字符串转数字 字符串

随机推荐

  • linux qt目录查看,QT遍历目录获取文件信息

    QFileInfo获取文件信息 文件名称 路径 大小 创建时间 修改时间 权限等使用路径 UNIX home dipper file1Windows C dipper file1 构造函数 QFileInfo fileInfo path Q
  • 【翻译 + 整理】Qt样式表详解(10):伪状态

    1 active 部件处于活动的状态 2 adjoins item 当QTreeView的 branch与某个item相邻时 将设置此状态 QTreeView branch background red QTreeView branch a
  • 蓝桥杯流转呼吸灯

    include STC15F2K60S2 h include
  • Python直接使用plot()函数画图

    目录 一 plot 函数的认识 二 plot 函数基本运用 三 plot 函数数据可视化画图以及图元基本参数设置 一 plot 函数的认识 在使用Python进行数据可视化编程中matplotlib库是我们用来对数据进行画图常用的第三方库
  • 时间戳转换成字符串,返回Invalid Date(自己遇到的坑)

    今天在开发的过程中 遇到一个比较坑自己的问题 将时间戳转换成正常日期的时候 总是会返回Invalid Date 排查了好久 在想为什么是这个结果 在控制台里面测试都是ok的呀 于是乎 想到了自己再后端定义的时候 时间戳定义的是字符串格式的数
  • Python 使用 Thrift 连接 HBASE 进行操作

    在工作中想要使用Python对HBASE进行操作 主要用来获取数据进行分析 HBASE提供了 Thrift 借口 通过查看API 进行了一些的尝试 下面就是使用Python的相关代码 在使用之前需要启动 HBASE的Thrift和安装pyt
  • 分布式系统设计的求生之路

    作者 作者 Simon 腾讯后台开发高级工程师 链接 http wetest qq com lab view id 105 著作权归作者所有 商业转载请联系WeTest获得授权 非商业转载请注明出处 分布式系统理念渐渐成为了后台架构技术的重
  • 嵌入式入门教学——C51(中)

    嵌入式入门教学汇总 嵌入式入门教学 C51 上 嵌入式入门教学 C51 中 嵌入式入门教学 C51 下 文章中所使用到的所有代码模块 免费 基于STC89C52RC的代码模块资源 CSDN文库 目录 七 矩阵键盘 八 定时器和中断 九 串口
  • win10常用操作集合 - vhd/wsl/等等

    文章目录 wsl常用操作 cli操作 vhd常用操作 UI操作 扩容 缩容 方法一 常规方法 方法二 碎片整理 常见问题1 win10 UI 基本配置 win10网络配置 防火墙配置 wsl常用操作 cli操作 前提 BIOS要使能虚拟化相
  • MATLAB搜索路径的查看和设置方法

    MATLAB搜索路径的查看和设置方法 1 查看matlab的搜索路径 单击matlab主界面菜单工具栏中的 设置路径 按钮 打开 设置路径 对话框 左侧的几个按钮用来添加目录到搜索路径 还可以从当前的搜索路径中移除选择的目录 右侧的列表框列
  • 静态代码检查-Sonar-环境安装(一)

    1 前提 1 安装mysql数据库 5 6以上版本 本人数据库版本5 7 2 安装jdk1 8 本人jdk版本1 8 2 官网下载 https www sonarqube org downloads 最新版本6 7稳定版 选择 Show a
  • 密码学 / 哈希算法

    一 诞生原因 在日常生活中 每个人去银行 坐火车都需要身份证证明自己的身份 身份证存在的目的就是要证明我真的是我 同样在网络中 一个文件是否被改过 更改之后就是新的文件 需要一个 身份证 证明 这里就需要了 hash 算法了 二 特点 为了
  • 黑马并发笔记

    参考这个就好 https www yuque com gaohanghang sgrbwh wng754 这个也不错 https blog csdn net weixin 50280576 article details 113033975
  • 开放加速规范AI服务器设计指南

    近日 在2023年开放计算社区中国峰会 OCP China Day 2023 上 开放加速规范AI服务器设计指南 以下简称 指南 发布 指南 面向生成式AI应用场景 进一步发展和完善了开放加速规范AI服务器的设计理论和设计方法 将助力社区成
  • Linux内存管理:ARM Memory Layout以及mmu配置

    http blog csdn net hongzg1982 article details 47341881 在内核进行page初始化以及mmu配置之前 首先需要知道整个memory map 1 ARM Memory Layout PAGE
  • Adobe Photoshop 2022版 功能介绍及使用技巧

    目录 版本介绍 使用技巧 截图展示 分享 版本介绍 Adobe Photoshop 2022是Adobe公司的一款专业的图像处理软件 它提供了强大的图像处理功能 从色彩调整 图层处理到高级合成等功能 新版本带来的一些更新包括 1 人工智能辅
  • Angular的自动化测试

    当Angular项目的规模到达一定的程度 就需要进行测试工作了 本文着重介绍关于ng的测试部分 主要包括以下三个方面 框架的选择 Karma Jasmine 测试的分类和选择 单元测试 端到端测试 在ng中各个模块如何编写测试用例 下面各部
  • Unity的C#编程教程_36_while循环语句

    do while 循环 首先执行用于循环的程序块 再进行条件判断 判断为真则再次运行程序块 直到判定为假 跳出循环 比如数数程序 using System Collections using System Collections Gener
  • Shell文本处理三剑客之awk

    本章大纲 8 3 awk awk是一个处理文本的编程语言工具 能用简短的程序处理标准输入或文件 数据排序 计算以及生成报表等等 在Linux系统下默认awk是gawk 它是awk的GNU版本 可以通过命令查看应用的版本 ls l bin a
  • 开放封闭原则(Open Closed Principle)

    在面向对象的设计中有很多流行的思想 比如说 所有的成员变量都应该设置为私有 Private 要避免使用全局变量 Global Variables 使用运行时类型识别 RTTI Run Time Type Identification 例如