美文网首页
[Common] 代码大全

[Common] 代码大全

作者: 木小易Ying | 来源:发表于2020-06-12 22:02 被阅读0次
    • 开发前要进行好准备工作,这能减少后面写代码的时候折返跑的概率。(架构设计、需求完整、考虑每个需求点的商业价值...)

    • 架构设计:(为了降低风险)
      ※ 对实现80%功能的20%的类所说明
      ※ 数据库设计(为啥用单个/多个数据库、表设计、view设计、为啥要用数据库而非文件)
      ※ 对资源的使用率例如线程带宽以及极端情况处理
      ※ 安全性、性能以及风险
      ※ 可伸缩性(用户增长应对)
      ※ 国际化
      ※ 错误以及容错
      ※ 指明对哪些类必须健壮,而其他类可以勉强健壮,防止过度工程

    • 主要的构建:代码规范 & 测试用例单元测试 & merge前的统一工序

    • 子系统之前的通信要避免过多,这样容易牵一发而动全身,避免形成环:


      乱七八糟的系统
    简化后的系统
    • 隐藏业务规则(例如税率)& 用户界面 & 数据库访问 & 系统相关的接口(例如Windows)的实现,尽量通过接口访问,这样可以很快的对他们做修改替换

    • 信息隐藏:如果有个变量是int的,你可以通过 int id = newID() 生成它,但是如果它变成string了呢,你要到程序里面所有地方把类型改掉。如果你typedef了一个idType,然后程序里都用idType就可以很轻松的改id的实际type了~ 所以要多思考我要隐藏些什么?

    • 封装变化:把容易变化的搞成一个类,然后让内部的变化对外不可见。例如sdk的操作啥的,这样如果以后要换一个sdk就很方便。(例如业务规则、对硬件or系统的依赖、输入输出的文件格式、非标准的语言特性、困难设计区域、状态变量、常量定义)

    如何找变化
    • 为测试而设计


      设计check list
    • 不要太多注释说明的事儿,可以直接assert解决,如果错误使用就crash了

    • 保持接口抽象一致性、内聚性。不要暴露过多接口,注意封装,封装如果做不好接口抽象也一般很难做好,随之而来的可能就是耦合严重

    • 如果你调用某个函数必须知道它的内部实现逻辑,那么就是有问题的,这不是面向接口编程而是面向实现了

    • 7-9个成员变量是一个类的合理大小,如果有更多的话最好拆成单独的类

    • 继承的适用场景是,如果有个基类A和子类BCD,你在使用BCD的时候是无差别的,不用考虑它具体是哪个类才是对的。如果C的某个方法和BD不一致,使用实例的时候还得先判断是不是C,如果是就do something,继承就是不好的。

    • 只有一个派生类的基类是没啥用的可以不用提早过度设计做继承;而有很多只覆盖了几个方法还不干活的派生类的基类也是有问题的。

    派生误区1 派生误区2
    • 减少继承层数,最多2-3层以内

    • 多重继承


      屏幕快照 2020-05-30 上午8.09.26.png
    • 减少实例化对象的种类、减少在实例化对象上调用不同子程序的数量、减少调用由子程序返回对象的子程序例如new A().createB().getC().doSth()

    • 避免只有数据或者只有行为的类。
      (这里其实有点迷,model不是有一些就只有数据么,以及util一般都只有行为吖)

    • 继承会增加复杂度,所以用组合好于继承,因为我们编程的目的是降低复杂度。

    • 创建子程序(函数)的原因:
      降低复杂度让调用者可以不必思考细节直接用、
      引入中间易懂的抽象让代码更可读、
      避免代码重复、
      支持子类化、
      隐藏顺序如先A后B可以把AB都分为两个子程序、
      隐藏指针操作、
      提高可移植性隔离有语言特性的代码、
      简化复杂的bool判断、
      改善性能集中调用可以集中发现修改问题、
      限制变化带来的影响、

    • 子程序应该提高内聚性做好一个事儿例如求cos,避免干两个事儿例如cos以及tan。高内聚性的bug会更少。

    • 子程序命名:
      (1)有的时候会起解决的问题&副作用的名字例如calculateReportTotalsAndOpenOutputFile。这样就太长了,可以只保留解决的问题即可
      (2)避免无意义的动词看不明白干了啥,例如handleOutput可以改成formatAndPrintOutput,但有时候不好起名是这个子程序没有很好地内聚,需要修改代码
      (3)避免仅数字形成不同名字,例如outputUser1/outputUser2
      (4)要对返回值有所描述,例如currentColor()
      (5)面向对象语言里面如果都document.print()了就可以不用命名为printDocument
      (6)对仗,open/close、create/destory
      (7)为常见操作统一命名。例如项目里面好几个类都有获取id的方法,那么就尽量不要一个类叫getId(),另一个类叫id(),还要一个类叫id().get()

    • 参数顺序,可以先是input不修改值的、最后是output会改变值的。(类似swift)

    • 用assert对参数说明,例如数值范围、枚举、数量的参数的单位

    • 参数限制在7个以内;

    • 如果一个有10个参数外露的对象,子程序之需要3个,那么是传入对象还是3个参数呢?看子程序需要的抽象究竟是什么,避免装包拆包也就是避免创建一个新的对象给子程序,然后子程序再拿自己要的三个属性,如果是这样就直接给三个属性即可。如果经常需要修改子程序参数,每次都是加这个对象的其他参数,那么就可以传入对象。

    • 显式声明参数名,防止错位

    • 返回值应该是函数名所指定的,如果函数名没有指定,则不应该有返回值。不要为了方便随意加返回值给外面暗示成功or失败哦。

    • define宏注意用括号 & 花括号包起来避免执行错误:例如Cube(a) ((a) * (a) * (a)),这样就避免了传入的是 x + 1 这种。所以尽量不要用宏,可以用inline、template、typedef来替代

    • 断言是保证代码正确性的,只有debug环境监测,避免绝不应出现的问题;错误处理是处理可能出现的问题,让程序不崩溃;异常是严重问题,外面拿到异常如果不catch就crash了

    • 不要抛出低层级exception暴露内部实现:


      错误的异常
    正确的异常
    • 可以先写伪代码,从高到低,直到你觉得已经没有意义了,然后作为注释,填充代码。(注意去除冗余的注释,虽然其实现在的互联网没有给写伪代码的时间..)

    • 变量存活时间是从最后一次使用到第一次声明的行数;平均跨度是相邻使用之间的行数距离的平均值。我们追求的应该是短存活时间 + 短跨度,可以让读者更好的理解代码,并且减少中间使用的可能性来减少错误。其实也就是让你思考和控制的范围变小,就更不容易出错。(所以应该避免全局变量)

    变量名命名
    • 把计算的量放在名字最后的这条规则也有例外,那就是Num限定词的位置已经是约定俗成的。Num放在变量的开始位置代表一个总数:numCustomers表示的是员工的总数。Num放在变量名的结束位置代表一个下标:customerNum表示的是当前员工的序号。通过numCustomers最后代表复数的s也能够看出这两种应用之间的区别。
      然而,由于这样使用Num常常会带来麻烦,因此可能最好的办法是避开这些问题,用Count或者Total来代表员工的总数,用Index来指代某个特定的员工。这样,customerCount就代表员工的总数,customerIndex代表某个特定的员工。

    • 循环中可以用i,j,k命名,但是如果是循环嵌套,其实这样容易出问题,或者这个变量不仅仅在循环内用,这些情况最好起一个有意义的名字类似 score[teamIndex][eventIndex]

    • BOOL的命名可以用foundsuccess等,自己就包含了真假的含义。如果有的人习惯在前面加一个is例如isFound也可以,可以避免非bool含义的变量被如此命名例如isStatus就很奇怪。但是if(found)就比if(isFound)好很多。如果是反义的可以取名notFound,如果经常判断的是if(!notFound)就应该替换为if(found)

    • 在同一段中不要出现含义类似的两个变量,很难理解。也不要那种拼写类似的哦,容易看混。

    • 在精度要求很高的环境下,如果你不确定自己要用double还是float,其实可以typedef一个新的类型,然后计算时候都用自己定义的type,如果需要改精度,只要改这个type的定义就可以改掉所有地方比较方便~ 例如typedef Coordinate double,而且这样可以对使用者隐藏细节。

    • 内存中的一个位置就是一个地址,常用16进制形式表示。在32bit的处理器上面,地址用32bit表示,指针本身只包含地址

    • 如何解释内存中某个位置的内容,是由指针的base type决定的,如果某个指针指向整数,那么意味着编译器会把指向的内存位置的数据解释为整数。所以内存并没有绑定一个指定的type,是你用的指针才指定了解析内存的方式。你可以让一个整数、字符串、浮点数都指向同一块内存,同样的内存可以解释为不同的内容,只是取决于指针的base type,例如:


      指针type
    • 如果子程序传入参数是个指针,需要*parameter = some_value才是改了指针指向的内容哦。
      但是注意int *p; *p = 1不能用,因为int *p;定义了一个指针p,然而p并没有指向任何地址,所以当使用*p时是没有任何地址空间对应的,所以*p=1就会导致,不知道把这个1赋值给哪个地址空间了。
      int *p; p = 1;能用是因为int *p;定义了一个指针p, p = 1;意思是将一个内存地址为1的地址赋值给p,所以这个是可行的。但是这个操作是不安全的。

    • 即使全局变量,也不要让程序里各个地方直接使用和修改,需要创建setter和getter控制访问。这样比较方便之后拓展和调试。

    全局变量访问
    • 虽然应该避免全局变量,但如果真的需要那么就光明正大的用,命名尽量和别的区分开,不要搞一个大的杂乱无章的对象来回传递反而不好。(类应该具有抽象一致性,而非单纯为了减少参数传递个数之类的,除非本身就是一致的)

    • 如何标识一系列需要顺序执行的子程序:

    通常我们对写程序对数据操作都是取数,对数据进行计算,再打印。一段java演示代码如下:

    data = ReadData();
    results = CalculateResultsFromData( data );
    PrintResults( results );
    

    这三个步骤思路非常清晰,说明了这几个函数之间的耦合程度,和相互依赖性。除非什么特殊情况发生,否则都会按照这个顺序来执行。尤其在ABAP中,似乎都是这样的顺序。

    再看一个例子:

    revenue.ComputeMonthly();
    revenue.ComputeQuarterly();
    revenue.ComputeAnnual();
    

    在以上代码中先计算的是月份,接着是季度,最后是年份。这是一个常识,但是光从代码中我们无法得出他们是否有耦合,是否应该按照这个特定顺序执行。

    再看一个VB例子:

    ComputeMarketingExpense
    ComputeSalesExpense
    ComputeTravelExpense
    ComputePersonnelExpense
    DisplayExpenseSummary
    

    从以上例子中也看不处这段代码的执行顺序有什么特殊之处,也不知道如果改变语句的执行顺序会怎样。没有任何的注释,程序也没有参数,可能都是直接对全局变量进行操作。所以这段代码写的并不好。

    => Solution 1: 组织代码让依赖关系更明显
    在上面的VB代码中,应该有一个初始化函数,如InitializeExpenseData()。写这个函数的目的是程序的结构更清晰,让读代码的人知道了一个隐含的信息,在调用其它函数之前,必须调用初始化函数

    => Solution 2: 使子程序名凸显依赖关系
    如果ComputeMarketingExpense必须最先执行,它做的不仅仅是现在名字里面的事情,还InitializeMemberData它应该命名为ComputeMarketingExpenseAnd InitializeMemberData

    => Solution 3: 利用子程序参数明确显示依赖关系
    如果子程序没有传参,你就看不出来哪些子程序依赖了一样的数据,通过显式写参数可以暗示使用者顺序是很重要的。

    ComputeMarketingExpense( marketingData )
    ComputeSalesExpense( salesData )
    ComputeTravelExpense( travelData )
    ComputePersonnelExpense( personnelData )
    DisplayExpenseSummary( marketingData, salesData, travelData, personnelData )
    

    有一种更明确依赖关系的方式就是加入输入和输出

    expenseData = InitializeExpenseData( expenseData )
    expenseData = ComputeMarketingExpense( expenseData )
    expenseData = ComputeSalesExpense( expenseData )
    expenseData = ComputeTravelExpense( expenseData )
    expenseData = ComputePersonnelExpense( expenseData )
    DisplayExpenseSummary( expenseData )
    

    这样你是不是就可以明确的感受到,后面的结果依赖于前面的步骤了

    => Solution 4: 注释

    => Solution 5: 断言
    如果这段非常重要,可以在前置步骤里面加一个bool isXXXExceuted,然后再后面的步骤判断之前的isXXXExceuted都是true才执行后面的,否则就assert。但这样增加了变量哦,所以要权衡利弊啦是不是足够重要。


    • 顺序无关的语句也要遵循就近原则,相关的放在一起,降低读代码的时候的复杂度。

    • if-else、switch-case的时候,先写正常的再写异常情况。这个也类似likely可以提高执行效率。

    • 如果condition过于复杂,可以抽出函数来得到BOOL值,不要把if(xxx)写的过长很难理解。

    • 简化case里面要做的事儿,如果很长就提成函数。尽量避免case里面不break越到下个case,如果真的要这么做就注释一下,但也尽量不要这么做。

    • 递归要注意有可能内存溢出,可以加入安全计数器限制执行次数。(不要用递归计算阶乘 & 斐波那契,明明循环更可读并性能好)

    • 各种方式找代码问题的有效率:


      缺陷检测率

    所以没有任一种是可以直接把问题都找出来的,需要混合各种方法来提高检测率。代码复查的找到错误的效率要高于测试的,所以要多review吖还可以顺便改bug

    • 测试先行。先写测试用例,能明确需求也能提高开发效率。

    • 一个不能经验论的例子:性能优化的时候我们可能为了性能而写了晦涩的高级代码,例如二维数组求和,为了避免用循环以及下标计算,可以用指针递减的方式做。但是当你真的测试性能就会发现没有区别两者,因为编译器早就做了优化,循环的实现就是指针递减。所以真的有性能问题的时候要找到影响性能的20%代码然后优化,不要再细枝末节上面纠结太多。

    • C#中switch-case的效率要高于if-else,然而java中相反。不用语言是不一样的哈~ 只能通过测试确定这个事情。

    • 用表查询替代逻辑判断:


      image1.png
    image2.png
    • 当性能和可读性矛盾的时候需要看场合,不是一定为了性能牺牲可读性。例如循环展开求和比for快,但很难理解。但我们应该尽量简化for循环里面做的事情

    • 把最忙的循环放在内层:


      把最忙的循环放在内层
    • 削弱运算强度来提高性能:


      运算强度
    • 用低级语言重写代码:


      image
    • 性能改善:


      代码调整方法
    提高代码执行速度
    • 项目越大,写代码的时间占比会越小,架构、需求、测试的b比例会更大。并且项目的大小成倍增加,总的时间可能不止一倍增加,因为相应其他的部分是非线性增长的。

    • 编译器与链接器:


      编译器与链接器

    根据C++标准,一个编译单元(Translation Unit)是指一个.cpp文件以及这所include的所有.h文件,.h文件里面的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE(Portable Executable,即Windows可执行文件)文件格式,并且本身包含的就是二进制代码,但是不一定能执行,因为并不能保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由链接器进行链接成为一个.exe或.dll文件。

    举个例子,编译器负责把每一页或节或章翻译成等价的中文,链接器负责把翻译好的章节整理成完整说明书。


    Finally,这本书真的好长。。涉及的很广还会有一些管理心理学之类的,虽然年代久远但也值得一看吧。

    相关文章

      网友评论

          本文标题:[Common] 代码大全

          本文链接:https://www.haomeiwen.com/subject/mdfpahtx.html