一、建模
1. 抽象
2. 关键对象的建模
二、隐藏实现细节
1. 物理隐藏:声明与定义
将内部细节(.cpp)和公有接口(.h)分离,存储在不同文件中
2. 逻辑隐藏:封装
使用C++中语言受保护的和私有的访问控制特性,从而限制访问内部细节
- 隐藏成员变量:成员变量始终声明为私有的,而不是受保护的和公有的
- 隐藏不必要的实现方法
-
- 永远不要返回私有数据成员的非const指针或引用,这会破坏封装性
- 将私有函数声明为.cpp文件中的静态函数,而不要将其作为私有方法暴露在公开的头文件中(更好的做法是使用Pimpl惯用法)
- 隐藏实现类:隐藏那些存粹是为了实现的类
三、最小完备
1. 不要过度承诺
在发布了某个API并且已经有用户使用之后,增加新功能很容易,而删除功能就很困难,当不确定是否需要某个接口时,就不要提供此接口(疑惑之时,果断弃之!精简API中公有的类和函数)
2. 谨慎添加虚函数
- 避免将函数声明为可重写的函数(虚函数),除非你有合理且迫切的需求
- 一般来说,如果API没有在其内部调用某个特定的方法,那么此方法很可能不应该声明为虚方法,当潜在的子类与基类之间形成一种“is-a”的关系时,继承才有意义
- 使用虚函数的注意点:
-
- 如果基类中包含任意虚函数,那么需要将析构函数声明为虚函数(确保子类可以释放其申请的额外资源)
- 编写文档,说明虚函数内部实现中需要调用哪些方法以维持对象内部的完整性,子类继承时需要按照文档实现
- 不要在构造函数或析构函数中调用虚函数,这些调用不会指向子类
3. 便捷API
基于最小化的核心API,以独立的模块或库的形式构建便捷API
四、易用性
1. 可发现性
可发现的API要求用户可以通过API自身明白如何使用它,而不需要参阅任何解释或文档
2. 不易误用
- 最常见的误用API的方式是向方法传递错误的参数或非法值。当方法拥有相同的类型的参数,但用户忘记参数顺序或者使用int而不是enum类型时,误用情况很可能发生
//容易发生误用的写法
std::string FindString(const std::string& text,bool bSearchForward,bool bCaseSensitive);
//不易发生误用的写法
enum SEARCHFORWARD
{
FORWARD,
BACKWARD
}
enum CASESENSITIVE
{
CASE_SENSITIVE,
CASE_INSENSITIVE
}
std::string FindString(const std::string& text,SEARCHFORWARD direction,bool aseSensitive);
- 对于enum不能解决的复杂情况,为了确保每个参数都有唯一的类型,可以引入新的类
//容易发生误用的写法
class Date
{
public:
Date(int year,int month,int day);
}
//不易发生误用的写法
class Year
{
public:
explicit Year(int y) : m_year(y) {}
int GetYear() const { return m_year; }
private:
int m_year;
};
class Month
{
public:
explicit Month(int m) : m_month(m){}
int GetMonth() const { return m_month; }
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
static Month Mar() { return Month(3); }
static Month Apr() { return Month(4); }
static Month May() { return Month(5); }
static Month Jun() { return Month(6); }
static Month Jul() { return Month(7); }
static Month Aug() { return Month(8); }
static Month Sep() { return Month(9); }
static Month Oct() { return Month(10); }
static Month Nov() { return Month(11); }
static Month Dec() { return Month(12); }
private:
int m_month;
};
class Day
{
public:
explicit Dat(int d) : m_day(d){}
int GetDay() const { return m_day; }
private:
int m_day;
}
class Date
{
public:
Date(const Year& y, const Month& m, const Day& d);
};
Date BirthDay(Year(1992),Month::Sep,Day(23));
知识点:
1.使用枚举类型代替bool或者int类型,提高代码的可读性
2.避免使用拥有多个相同类型参数的函数
3. 一致性
- 使用一致的函数命名和参数顺序
- 拥有相似角色的类应该提供相似的接口,比如:std::map std::vector 都有接口size()
4. 正交
- 正交的API意味着函数没有副作用
- 调用设置特定属性的方法应该仅改变那个属性,而不能额外改变其他可以公共访问的属性
//不好的设计
class ShowerController
{
public:
float GetTemperature() const { return m_temperature; };
float GetPower() const { return m_power; };
void SetPower(float p)
{
if (p < 0) p = 0;
if (p > 100) p = 100;
m_power = p;
m_temperature = 42.0f + sin(p / 38.0f) * 45.0f;
}
private:
float m_temperature; //温度
float m_power; //水压
};
//正交设计
class ShowerController
{
public:
float GetTemperature() const { return m_temperature; };
float GetPower() const { return m_power; };
void SetPower(float p)
{
if (p < 0) p = 0;
if (p > 100) p = 100;
m_power = p;
}
void SetTemperature(float t)
{
if (t < 42) t = 42;
if (t > 85) t = 85;
m_temperature = t;
}
private:
float m_temperature; //温度
float m_power; //水压
};
5. 健壮的资源分配
常见的误用指针引发的错误:
对NULL解引用
:尝试对NULL指针使用->或*操作二次释放
:对同一块内存调用两次delete或free()访问非法内存区域
:对尚未分配或已经释放的内存使用->或*操作混用内存分配器
:用delete释放由malloc()分配的内存,或用free()释放由new分配的内存数组释放不正确
:使用delete操作符而非delete[] 释放数组内存泄漏
:内存使用后没有释放
知识点:
1.使用智能指针:
- 如果函数返回了需要用户释放的指针时
- 用户需要的指针的生命周期比对象的生命周期更长
#include <memory>
using MyObjectPtr = std::shared_ptr<class MyObject>
class MyObject
{
public:
static MyObjectPtr CreateInstance();
~MyObject();
private:
//用工厂方法创建实例
MyObject();
};
MyObjectPtr MyObject::CreateInstance()
{
return MyObjectPtr(new MyObject());
}
int main(int argc, int* argv[])
{
//当ptr 变量超出作用域时 两个实例都会销毁(智能指针)
//如果此处返回 MyObject* 类型 需要手动释放两个对象
MyObjectPtr ptr = MyObject::CreateInstance();
ptr = MyObject::CreateInstance();
return 0;
}
2.将资源的申请和释放当作对象的构造和析构
- 资源分配是对象构造,资源释放是对象析构
#include <string>
#include <mutex>
std::mutex mutex;
//错误写法
void SetName(const std::string& name)
{
mutex.lock();
if (name.empty())
return;
mutex.unlock();
}
//正确写法
void SetName(const std::string& name)
{
std::lock_guard<std::mutex> guard(mutex);
if (name.empty())
return;
}
6. 独立平台
不要将平台相关的#if或#ifdef语句放到公共API中,因为这些语句暴露了实现细节,并使API因平台而异
#include <string>
//不好的设计
//导致在不同的平台上创建不同的API,进而强迫API的用户为其应用程序引入同样的平台相关特征
//用户在使用GetGPSLocation 时也需要进行平台判断
class MobilePhone
{
public:
bool StartCall(const std::string& number);
bool EndCall();
#if defined TARGET_OS_IPHONE
bool GetGPSLocation(double* lat, double* lon);
#endif // defined TARGET_OS_IPHONE
};
//好的设计
class MobilePhone
{
public:
bool StartCall(const std::string& number);
bool EndCall();
bool HasGPS() const;
bool GetGPSLocation(double* lat, double* lon);
};
bool MobilePhone::HasGPS() const
{
#if defined TARGET_OS_IPHONE
return true;
#else
return false;
#endif // defined TARGET_OS_IPHONE
}
五、松耦合
- 耦合:软件组件之间相互连接的强度的度量,即系统中每个组件对其他组件的依赖程度
- 内聚:单个软件组件内的各种方法相互关联或者聚合强度的度量
知识点:
优秀的API表现为松耦合和高内聚
1. 仅通过名字耦合
如果类A中仅需要知道类B的名字,即它不需要知道类B的大小或者调用类B的任何方法,那么类A就不需要依赖类B的完整声明(可以为类B使用前置声明)
class MyObject; //只需知道MyObject 的名字
class MyObjectHolder
{
public:
MyObjectHolder();
void SetObject(MyObject* obj);
MyObject* GetObject() const;
private:
MyObject* obj;
};
//如果相关联的从cpp 文件仅仅存储并返回MyObject的指针,同时限制任何除指针比较外
//的与该指针的交互,就不需要#include "MyObject.h"
知识点:
除非确实需要#include类的完整定义,否则应该为类使用前置声明
2. 降低类耦合
知识点:
与成员函数相比,使用非成员、非友元的方法能降低耦合度
//耦合度高
class MyObject
{
public:
std::string GetName() const;
//类的成员函数,可以访问MyObject所有私有和受保护的成员函数以及数据成员
//如果MyObject 有基类,则也可以访问基类中受保护的成员
void PrintName() const;
private:
std::string m_name;
};
//低耦合设计
class MyObject
{
public:
std::string GetName() const;
private:
std::string m_name;
};
void PrintName(const MyObject& obj);
//为了更好的传达MyObject和PrintName在概念上的关联性
//1.可以将它们放到同一个命名空间中
//2.可以创建一个新的帮助类的静态方法
//1
namespace MyObjectHelper
{
void PrintName(const MyObject& obj);
} // namespace MyObjectHelper
//2
class MyObjectHelper
{
public:
static void PrintName(const MyObject& obj);
};
3. 刻意的数据冗余
#include "ChatUser.h"
#include <string>
#include <vector>
//此类中 TextChatLog 与 ChatUser 耦合 但是此处实际上只使用了 userName(引入ChatUser实际上只是用来获取username)
class TextChatLog
{
public:
bool AddMessage(const ChatUser& user, const std::string& msg);
//获取聊天数量
int GetCount()const;
//获取聊天信息 类似 dujinwei[10:00] hello
std::string GetMessage(int index);
private:
struct ChatEvent
{
ChatUser m_user; //用户信息
std::string m_message; //消息
size_t m_timeStamp; //时间戳
};
std::vector<ChatEvent> m_chatEvents;
};
//去除TextChatLog 与 ChatUser 耦合
//这种做法的弊端是在TextChatLog和ChatUser中都保留了用户名这个属性
//如果当前有个需求,修改用户名,并在历史信息中同步更新新用户名,就必须使用上面紧耦合版本,
//或者更改用户名的时候同步更改历史消息中的用户名
#include <string>
#include <vector>
class TextChatLog
{
public:
bool AddMessage(const std::string& user, const std::string& msg);
//获取聊天数量
int GetCount()const;
//获取聊天信息 类似 dujinwei[10:00] hello
std::string GetMessage(int index);
private:
struct ChatEvent
{
std::string m_userName; //用户名
std::string m_message; //消息
size_t m_timeStamp; //时间戳
};
std::vector<ChatEvent> m_chatEvents;
};
4. 管理器类
知识点:
管理器类可以通过封装几个低层次的类降低耦合
5. 回调、观察者和通知
回调函数、观察者模式、信号槽通知