本文主要汇总了自己在软件设计方面的一些经验总结。
设计评审
对于一些基础组件的重构改进,在自己做完相关设计后,务必要请组内其他人一起评审下,不要以自我为中心,以为自己写的接口就是完美的。多与人讨论,不管讨论的对象是老手、还是小白,多个角度去审视已有设计,于人于己都是大有益处的。
基础类接口的设计务必要慎重,一旦成型后,再想改动,难度就很大。举个例子,我之前负责的基类代码,其中有些接口以及内部实现写的不合理,外部用起来很别扭,但因前期使用范围较小,别扭就别扭的用了。随着时间的流逝,该基类被越来越多的人使用,吐嘈声不断,自己有过重构接口的想法和具体方案,但碍于改动牵涉到的面较大,就没有推动改进下去了。
UI设计
UI显示逻辑,显示数据来自后台响应,在后台响应数据之前,“当前界面的原始数据是否要清空,有两种看法:
清空界面数据:优点每次不会看到旧数据,缺点是当网络不好时,显示界面会空白。
不清空界面数据:优点是在网络尚未返回数据时,用户体验好。缺点是,看到的数据是旧数据。
具体采用哪种措施,个人觉的取决于当前数据对用户的重要程度以及更新频率,如果数据很重要并且频繁更新,那最好在先清空界面数据,再往后台发送请求。如果不是那么重要,而且更新频率不高,那么不清空界面数据带来的体验较好。
从开发流程一致性上来看,统一清空界面数据的心智负担较小。
对于一个界面来说,进入和退出的处理,最好要有相互呼应的地方。怎么理解呢?比如说,打开的时候有清空、申请资源、发送请求、初始化操作的动作,那么退出也要有清空、释放资源、取消请求和卸载操作的动作。不管是某个界面,某个模块,子系统和整个系统,一致性操作要保证,否则就有潜在的泄露问题。
解决方案设计
在重新设计时,不要被已有的实现方案所束缚。假设这个模块是从0开始,你会怎么去组织和设计各种信息。
脑子里面首先想出来的方案,往往不是最佳的,肯定有改进的空间。
每一个设计方案,首先都有它的整体的设计思路,随后的铺陈展开,都围绕这个整体思路来做。
任何一个需求或者功能的实现,都可以至少定出两三个方案,在开始具体编码之前,要分析各个方案在实现基本功能之外的优缺点。由团队一起来讨论,看看在这种场景下,使用哪种方案较好。
将各个模块所使用到的全局数据可以集中汇总到全局类中,如要实现配置热更新功能,则可在配置改变时,只需要更新此类中的内容即可。外部赖该数据的模块,无需改变,只需在全局配置改变事件中重新初始化即可。
如能够动态计算的内容,不需要额外用变量来保存,对外提供接口函数,在每次获取时,重新计算或者发消息获取。
对于枚举类型和字符类型的相互转换,如果枚举类型只定义了4种,每一种有对应的数值,
enum ETest
{
TEST_1 = 1,
TEST_2 = 2
};
ETEst a = (ETEst)1;
a = (ETEst)3; // 这句可以编译通过
枚举类型的强制转换,如果cTemp
的值来自于网络流,网络流数据又可能不在已有枚举范围内,会存在无效情况,而这在编译阶段时不会报错不会报错,在运行阶段,该枚举值就有潜在的隐患,为了消除该隐患,建议枚举类型统一通过函数来转换,对于不在枚举值中的转换,提示错误,返回错误类型的枚举值。
高内聚低耦合
什么样的功能需要高内聚在一起,在今天的讨论中,比如断线重连提示框的控制和提示框本身实现。
就这两个功能来说,提示框本身作为一个较独立的功能,若要复用,那么提供的接口函数粒度就可以比较细,让外部使用者根据具体功能来调用各个接口,实现自己的功能。从提供方来说,是可以了。
但在有些场景下,外部使用者只想设置它关心的数据,其他类型的配置它不想关心,但这些设置有是有必要的,比如位置、Logo等,这个时候,有两种做法:
方法一:在提示框中,各类设置均有一个有意义的默认值,若外部不设置,则使用默认值。
方法二: 若外部不想关心与它无关的设置,可在提示框外再提供一个初始化函数,内部设置好默认配置下提示框,提供给外部使用。外部只需要调用者一个初始化函数即可,若有其他的需求,接着调用其他接口就行。
功能的提供者和功能的使用者关注点是不一样的,提供者提供的粒度可小可大,但从设计原则上来看,每个接口实现的功能要足够细化,而一些较复杂,需要组合各个接口实现的功能,可由使用者来安排,也增加一个中间层,由中间层来构造较高级的接口,高级接口的设计,按照接口一致性来,有create
就一定有对应的destory
。
高内聚的另一种角度的理解:
代码被阅读的次数要远远大于它被修改的次数,因此,代码可读性决定了可维护性,代码定义的关联性(相关代码定义和对应辅助函数)要大于分散性。
相同语意的语句要内聚到一起。具体表现为一组相关的数据类型定义,应该聚合在一起,方便一起阅读理解。比如和登录相关的,登录类型和认证类型就适合放在一起。相互关联的定义以及接口函数,应该或者说必须放在一起。
颜色的复用
在程序中涉及到颜色数值,肯定不会直接写死,而是用颜色ID映射具体颜色数值。这里有一个问题,相同的颜色数值,要不要对应同一个颜色ID呢?如果这两个相同的颜色数值用在两个不同的地方,那么他们就是两个不同的颜色ID,即使他们所映射的数值相同。假想未来某个地方颜色值有修改,那只修改对应颜色数值即可,不用影响其他界面。
为了减少颜色的适配工作量,同时为以后的切换颜色方案打好基础,开发需要限制设计能够允许的颜色取值范围,不能让设计无休止的提各种不同的颜色需求,例如,每一类颜色只提供可选的10左右的ID段范围,可以有多类(12类)颜色,设计提供的总的颜色数值总量控制,具体数值可以自定。
消息设计
消息一般有操作系统原生消息,系统级别请求响应消息,系统级别订阅发布消息,还有各个子模块内部使用的自定义消息。每种层级的消息需要定义好使用界限,什么层级的代码使用什么类型范围的消息,在架构设计时,就需要有严格的定义。
消息宏的定义要全局系统唯一,消息的定义唯一可避免不同模块定义了相同数值的不同功能的消息。要做成全局唯一,最佳实践是在同一层级目录下,定义全局消息,事先定义好各个业务的消息起始段和结束段。
每一种消息的定义要清楚明确,比如显示用户信息,清除显示这两个功能,
第一个功能可以定义 WM_SHOW_INFO
,带用户标识可。
第二个功能的话,有两种做法:
-
复用
WM_SHOW_INFO
,传空的用户标识代表清楚显示内容。 -
单独定义清空界面显示消息,
WM_CLEAR_INFO
这两种做法,推荐使用第二种,它符合消息功能的唯一性。
针对消息发送来说,不允许跨层消息传递,只允许向上一层发送消息,只响应本模块负责的消息,若不属于本模块的职责,向上继续传递消息。
消息的命名规范,与现有工程中的消息命名规则保持一致,若不一致,则需要修改使其一致,同时,将本次修改作为单独提交。
消息的使用场景、使用方法、入参说明以及正常的使用范例,至于消息负责人,可以从SVN的blame
功能中推导出来,需要随着消息的定义一同清晰明确的给出。
对于消息的定义和使用,一种消息带的参数,一定有明确定义,一般不建议传递可变长度的消息,例如如下这种:
struct NMClickNum
{
CString m_strName;
vector<CString> m_vInfo;
};
在消息中传上述结构体,是有安全隐患的。因为接受者有额外的心智负担,需要考虑传递的顺序和对应的一致性,很容易用错。比如A界面vector里面塞3个数据,B界面塞3个数据,但不同顺序的数据含义不同,在消息接收端,首先判断消息的个数是否符合要求,即使是符合要求,也不能确保特定索引位置上的消息一定是想要的,因为无法判断错误。这种送不定长的消息只适用于网络流层面,在业务层是禁止使用。
业务层面的消息的目的性要唯一,不能造一个通用层面的消息,携带各种数据,在各种组件之间穿梭。而是,针对特定的业务和界面,定义特定的消息,比如说
A界面需要3个数据
B界面需要另3个数据
C组件产生这3个数据是A界面需要的
D组件产生的3个数据是B界面需要的
那么,在定义消息时,A-C之间定义一个消息,D-B之间定义另一个消息,不同消息携带的参数要有明显的区分定义,以防用错。