《代码大全》第二版--第二部分

第二部分:创建高质量代码

    第五章:软件构建中的设计

     5.1 设计

        在编码前进行,比如画图,画xml,想好逻辑怎么做,新增哪些数据结构,命名;

        设计可能会考虑不周,并且设计过程是非常艰难的,会犯一些错误 ,但是在设计阶段犯错的代价远低于编码阶段;

        设计是易变的;

    5.2 设计的重要目标:管理复杂度

        复杂度是设计的重要指标之一,现代软件通常比较庞大,一个人的脑力远远不能装下这么多逻辑和设计。好的设计会将整个系统按照逻辑关系划分为子系统,在将子系统分解,最终划分为一个个小的在我们大脑记忆范围内的功能点,我们能够较容易的理解这些功能点,再将功能点联系起来,由小到大的理解整个系统。降低系统复杂度,每个最小集复杂度达到最小。

        目标:最小复杂度,易于维护扩展、耦合性, 重用,移植,层次性。

    5.3 分层设计

        横向和纵向上的设计,纵向上分层设计,从系统->子系统->模块->类->子程序。纵向上的设计有自上而下和自下而上,自上而下的好处是相对比较容易,从大局逐步剖析到细节,不好的地方是局部变化因素对上层影响较大,因为是固定好上层,如果下层变化需要影响到上层,会发生连锁反应,自上而下设计,通常自下而上的开发,先搭建基础组件。  自下而上的设计好处是先把影响最大并且最基础的部分设计好,在设计高层,高层可以随意更改而不至于影响下层,这样设计的难点在于要时刻和最终目标对齐,和最终整体目标对齐,否则容易走偏或者设计了一些多余的用不上的底层接口。

        横向模块之间的交互一定要注意接口之间的通信限制,如果没有限制,逻辑上其实就是一个接口,并没有分开,模块间要做到功能解耦,只通过小部分暴露的接口做交互,而且某一模块不能和太多模块打交道,比如数据库访问模块就只和数据库打交道,并且向外暴露接口,在数据库变化的时候只需要适配接口就行了。

    5.4 设计来源(启发式)

        将现实生活中的问题抽象成设计中的元素

        将生活中的相似实体抽象出共同的抽象特征

        采用信息隐藏、

        区分系统易变和不易变部分。找出一个最小的不易变部分,逐步扩大这个部分并检测这个部分的易变性。直到易变性突破了某个可接受范围,那么外边的更大范围就是易变的。

        适当采用现有的设计方法:常见的设计模式

    

        补充:几个代码设计的原则:

            单一职责原则SRP: 就一个类而言,应该仅有一个引起它变化的原因

            开放封闭原则ASD:类,模块,函数等等应该是可以扩展的,但是不可修改的

            里式替换原则LSP:所有引用基类的地方必须透明的使用其子类的对象

            依赖倒置原则DIP:高层模块不应该依赖低层模块,两个都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象    

            迪米特原理LOD:一个软件实体应当尽可能少地与其他实体发生相互作用

            接口隔离原则ISP:一个类对于另一个类的依赖应该建立在最小的接口上。

    

        总结:设计是个很重要的过程,并且需要大量的工作经验,不仅需要有全局观,还有关注具体模块功能的实现可行性,需要拆分目标系统,降低复杂度,需要关注的点很多,评估冲突妥协。这块后面有相关经验了再回来补充笔记。(普通开发人员在编码之前也要做se给出的设计进行思考,看有没有更优秀的方案,实现过程的逻辑伪代码也最好写一写,画一画逻辑流程图)

    第六章:可以工作的类

        6.1 类的基础:抽象数据类型ADTs

            抽象数据类型是指一些数据以及对这些数据所进行的操作的集合。

            类 = ADTs + 继承 + 多态

        6.2 良好的接口类型

            类的接口应该展现一致的抽象层次:一个类仅实现一个ADT(单一职责);创建类时明确要实现的抽象是什么,类中的服务也就是方法通常是成对的。

            类中的数据应该做好封装,只需要暴露部分以表达这个类的属性特征的接口(信息隐藏), 让调用方针对接口编程而不是去看接口中的具体实现来编程。

             《抽象单一,职责单一,服务单一》

        6.3 有关设计和实现的问题

            1. 包含:has a的关系, 类中的成员数据,可以声明也可以继承得来,最好不要超过7个成员变量

            2. 继承: is a 的关系;继承可以重用父类的数据和成员,继承体系不能过多,过多意味着复杂度过高,最好不超过6层。注意继承的权限:private,protected,public; 避免菱形继承。

                继承规则: 如果需要共享数据,则将这部分数据提取出来形成一个数据结构; 如果需要共享行为,将这些行为提取出来形成一个基类; 如果都要共享,提取基类并将数据结构填充到基类中去。

                基类控制接口:继承 

                自己控制接口:包含(包含字段,包含方法,包含对象)

            3.构造函数: 最好在构造函数里进行成员初始化动作,构造函数要注意深浅拷贝

        6.4 创建类的原因

            1. 为现实世界中的对象建模

            2. 为抽象的对象建模(比如物体的形状可以建立一个基类)

            3. 为了降低复杂度,建立多个类,类之间有相互交互; 类中可以隐藏信息,比如一个具体的复杂算法或者一个协议,隐藏在类中,对类外暴露接口即可。

            4. 减少参数的传递:对于多参数的函数,如果将这些参数封装成一个类,直接可以传递一个类的对象。

            5. 代码可重用,将重用的部分代码封装成一个类,其他个性化的类可以继承这个父类来获取那些公用的方法; 

            6. 首先做好系统分析工作,将动态的和非动态的数据分别放到不同的类中,动态的数据组成的类可以随时变化。

            应该避免的类:万能类(上帝类), 无关紧要的类(将这些类降级或者合并到其他类中), 类的命名不要使用动词(如果类中只包含动作而没有具体数据对象,这个类通常为一个工具类,名字一定要使用名词)

        

        补充总结:对类的思考,首先类的作用要单一,和函数一样,职责要单一,类中应该保存的是某一个ADT,对外暴露的接口也应该和这个ADT的抽象等级相同,对接口的暴露也要三思,最后类的名字要想好,用一个名字来表达。

    第七章:高质量的子程序

        子程序:routine, 方法:method, 过程:procedure, 宏:macro

        子程序是为实现一个特定目标而编写的一个可被调用的方法或者过程。函数有返回值,过程无返回值。

        7.1 创建子过程的正当理由: 降低系统的复杂度,避免代码重复, 封装一个复杂的算法或者不易懂的协议之类的过程, 简化复杂的布尔判断, 改善性能,确保每个子程序都比较小

        7.2 在子程序上设计: 着重内聚性,子程序内部操作紧密,子程序对外变现的功能单一;

        7.3 好的子程序名字:描述子程序所做的功能, 不要使用数字如part1, part2 ; 使用动宾短语。

        7.4 子程序可以写多长: 50-100 行, 最好在50行以内、函数越短小,功能越单一,可重用性越高

        7.5 如何使用子程序的参数:

                入参在前,出参在后; 使用宏定义来表明结构体中的出入参;参数最好不要超过5个,参数过多的时候可以考虑封装成数据结构或者对象的方式传入,参数命名也很重要; 形参名和实参名尽量保持一致。

 
#define  IN
#define  OUT    
struct {
    IN  int putIn;
    OUT  int putOut
}  
 

        7.6 使用函数时要特别注意的事情:什么时候用函数,什么时候用过程,函数有返回值,过程无返回值,有的时候表示过程的一个函数可以返回一个值用来表示这个过程是否执行成功。

        7.7 宏子函数与内联子函数:

            宏子函数中的参数和计算过程一定要用括号包起来,因为宏展开的时候各种符号的优先级可能会出现非定义情况,

            宏中多条语句的时候要用大括号包起来,防止在if后面紧跟宏时,宏展开只会有一条语句在for, if循环作用范围内;

            取代宏的手段:const , inline , template, enum, typedef

 
#define club(a)   ((a)*(a)*(a))   // a为x+1  -->   ((x+1)*(x+1)*(x+1))    
 

        总结:子程序,目的在于降低复杂度,提高可读性,可靠性,可修复性,可重用性,封装隐藏信息,对于子程序,要注意内部的内聚性,一个子程序只做一件事情,注意子程序行数尽量保持在50行内,最后,要给子程序取个好名字,达到从名字就能看出这个子程序完成的功能是啥。

    第八章:防御式编程

        子程序不因传入的错误数据而被破坏

    8.1 保护程序免遭非法输入数据的破坏:检查所有来源于外部的数据, 决定如何处理错误的输入数据。

    8.2 断言:开发和维护阶段使用,生产代码不要编译进去。断言检查的是不该发生的情况,错误码用来检查不太可能发生的异常场景。

    8.3 错误处理技术:返回中立值,换用下一个正确的数据, 返回与前一次相同的数据, 记录日志文件,返回一个错误码; 应该具体场景具体分析,对于正确性要求较高的场景应确保正确性,对于健壮性要求较高的时候应着重健壮性。

    8.4 异常:把代码中的错误或者异常事件传递给调用方代码的一种特殊手段。提出异常,交出控制权,或者自己处理异常。高健壮性要求对异常处理要全面。避免在构造函数和析构函数中抛出异常,比如在构造函数中抛出异常,异常之前分配的资源就得不到释放,这个时候就有内存泄漏问题; 了解库函数的异常情况。

    8.5 隔离程序,使之包容由错误造成的损害: 调用子程序前,先做preCheck动作

    8.6 辅助调试代码:目的是快速的检测错误

        进攻式编程:在开发阶段让异常显示出来,而在产品代码运行时让它能够自我恢复。

        对于辅助的调试代码,在正式产品中可以通过工具或者编译选项将他们剔除于生产代码中。

    8.7 确定在产品代码中保留多少防御式代码:如果每个子程序在正式代码之前都先运行防御代码,那么系统将会变得臃肿,首先第一点,去掉一些细微错误的检查代码,去掉硬件因素引起的异常代码,保留让程序稳妥的崩溃的代码; 第二,制定个产品规则,对于大多数的子程序,可以通过调用方来保证数据的正确性,有的子程序内部调用了几个子程序,这几个子程序的防御代码都是一致的,在这些子程序外部进行一次数据校验后就可以给多个子程序使用,如果每个子程序都去做一遍有点多余,个人觉得大多数子程序的数据正确性由调用方负责比较灵活。

    8.8 对防御式编程的态度:过多的防御式代码会让系统变得臃肿和缓慢,增加系统复杂度,可适当根据防御等级决定代码的编写。

    总结: 可以根据具体的项目要求来,有的项目要求调用方负责数据的正确性,有的要求被调用者负责数据的正确性,还是应该按需来。    

    第九章:伪代码编程过程

        9.1 关于伪代码:与具体语言无关,可以当成注释来写,尽量保持与逻辑一致

        9.2 通过伪代码创建子程序:检查先决条件;定义子程序要解决的问题:输入,输出,功能; 为子程序命名; 决定如何测试子程序; 搜索重用的代码; 代码效率性能考虑; 算法与数据结构的安排;编写伪代码;评审伪代码;编写代码;

        总结: 伪代码可以看成是具体代码的稍上层表现,包含了大概的实现过程,又通过简易的预览来描述实现过程,在写代码之前进行伪代码编写可以提前预估到困难点以及应对方法。

原文地址:https://www.cnblogs.com/Zhangyq-yard/p/10874862.html