本章内容
本章内容就是在回答一个问题:好的API应该具备哪些特征?
下面就是从几个方面来解释应具备的特征:
1. 对于问题有良好的建模
API的目的是为了解决某一方面的问题。因此,首先应对问题有个清晰的认识,能塑造出这个问题里的核心对象。
1.1 抽象
设计API时应阐述有意义的深层概念,而非公开底层细节。
(可以让非技术人员看接口,如果连他们都能看懂这个接口会干什么,那就说明接口的抽象非常到位了)
1.2 对象建模
确定问题牵扯的主要对象有哪些,他们各自提供什么操作,他们之间的关系。
然而,不要太极端,不要创建比需求更加通用的对象模型。
2. 隐藏细节
任何内部实现细节(那些很可能变更的部分)都必须对API的客户隐藏。
手段上分为物理隐藏和逻辑隐藏。
对于C++来说,物理隐藏指将公开的接口(.h)和内部细节(.cpp)分开放于不同的文件。
逻辑隐藏指封装,即通过语言特性来限制访问内部。
对于C++来说有几个方面要注意:
2.1 隐藏成员变量
不要把成员变量设置为public。
如果要访问成员,应该使用getter和setter方法来间接地访问。这有以下几个好处:
- 可在访问时验证有效性,比如拒绝输入的错误值。
- 惰性求值。可仅在需要时再执行计算。
- 缓存。
- 额外的计算,比如提供一些附加操作。
- 可发布通知。
- 便于调试。
- 线程同步。
- 更精确的访问控制,比如只读。
- 维持和其他数值的关系。
也最好不要把成员变量设置为protected。正如AlanSnyder所说,继承损害了封装所带来的好处。
2.2 隐藏实现方法
类只应定义做什么而非如何做。
(很遗憾,C++由于语言限制,private成员也要出现在类的声明中。但是之后会介绍Pimpl技巧来解决这个问题)
2.3 隐藏实现类
并非所有类都应该公开。仅用于实现的类应该从API的公开接口中移除。
3. 最小完备性
优秀的API应该是最小完备的。
Occam剃刀原理:若无必要,勿增实体。
3.1 不要过度承诺
不确定某个接口是否需要时,就不要提供。
工程师希望解决方案更通用和灵活,但是要抵制这种诱惑,因为:
- 想要添加的通用性可能永远用不到。
- 当未来真的需要时再添加,那时你会拥有更多的知识。
- 当到了确实需要添加功能时,简单的API比复杂的API更容易添加。
3.2 谨慎添加虚函数
虽然继承十分强大,但是有潜在隐患:
- 基类变得脆弱,不好修改。
- 客户继承后的行为无法控制,可能会做出本来不允许的行为。
- override函数可能会破坏内部完整性。
因此要谨慎使用继承。仅当子类和基类形成一种 is-a 关系时,继承才是有意义的。
3.3 分离“核心API”和“便捷化API”
“减少API函数数目” 和 “使API易于各种客户使用” 有天然的矛盾。
API傻瓜时会影响灵活度,但是复杂时会影响使用难度。
因此,将“核心API”和“便捷化API”放在不同的地方。这样专家可以用其实现高级功能,但又不影响普通用户的使用。
4. 易用性
API应易于理解。
4.1 自说明性
指用户能够通过API自身明白如何使用它。
比如一个清晰的、描述性强的名字。
4.2 不易误用
反例如函数参数中前后两个bool,很容易混淆。这时可以使用枚举类作为参数。
4.3 一致性
采用一致的风格,更易于用户理解。简化了用户学习过程。
比如一致的命名风格、相同的词语应表达相同的概念。
4.4 正交性
东西坐标的变化不应影响南北坐标。
比如对水压设置的接口不应影响温度。
需要铭记两个重点:
- 减少冗余。确保每个信息都只有唯一权威的来源。
- 增加独立性。确保暴露的概念没有重叠。
4.5 健壮的资源分配
C++指针无法分辨自己是否引用合法内存,因此这交给了程序员自己判断。有些解决方案,比如共享指针。
4.6 平台无关
不要将平台相关的内容放在公开的接口中,这会使你的接口平台相关。
5. 松耦合
- 耦合:组件之间相互连接的强度,即系统中每个组件对其他组件的依赖程度。
- 内聚:组件内部各种方法相互关联的程度。
优秀API表现为:松耦合,高内聚
一种理解耦合的方式是:当A改变时需要改变B中多少代码。
5.1 仅依赖名字
在C++中,如果你只需要知道另一个类的名字,不需要知道它的大小或调用它的任何方法,那就可以使用前向声明。
5.2 降低类的耦合
如果可以,则优先使用非成员函数(static函数)。因为成员函数会访问所有成员,耦合度更高。
5.3 有时需要重复来降低依赖
如果依赖另一个组件只是因为需要一个微不足道的小功能,那么此时直接复制过来代码是可以接受的。
5.4 Manager类
可用一个Manager类来连接各个类,这样他们各自的耦合度就会降低。
5.5 回调、观察者、通知
假设A要告诉B事情,则A就要依赖B。但是如果之后A又要告诉C,则A还要依赖C。那么更好的方式是注册成监听器,这样A就不用直接依赖B和C了。
6. 稳定性、文档、自动测试
此部分在后面章节有更详细的介绍。