编写高质量的代码(1)

对于不能肯定的,就需要去判断

 

专业性和技艺来自于驱动规程的价值观。——>整洁的代码

 

首先要从宏观上想清楚要做哪几件事

添加只需要做少量修改,且修改是隔离的——>需要使用继承和多态

优秀的软件设计,大都关乎分隔——创建合适的空间放置不同种类的代码。对关注面的分隔让代码更易于理解和维护!

仅仅让代码能工作是不专业的。要改进代码的结构和设计。

糟糕的代码可以清理。不能让模块之间互相渗透,出现大量隐藏纠结的依赖关系

保持代码持续整洁和简单

 

尽可能消除重复:

每次看到重复代码,都代表遗漏了抽象。重复的代码可能成为子程序或干脆是另一个类。将重复代码叠放进类似的抽象,增加了你的设计语言的词汇量,提升了抽象层级。

重复最明显的形态就是不断看到明显一样的代码,不断在复制粘贴代码,可以用单一方法来替代之

较隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的switch/case或if/else链。可以用多态来替代之。

更隐蔽的形态是采用类似算法但具体代码行不同的模块,这也是重复,可以使用模板方法模式或策略模式来修正。

 

设计模式是消除重复的有效手段。 范式是消除数据库设计中的重复的策略。oo自身也是组织模块和消除重复的策略。

 

创建分离较高层级一般性概念与较低层级细节概念的抽象模型,分离要完整;基类对派生类应该一无所知。

将派生类和基类部署到不同的jar文件中,确保基类jar文件对派生类jar文件的内容一无所知。这样就能将系统部署为分散和独立的组件。这样,修改产生的影响就降低了,维护系统变得更简单。

孤立抽象是开发者很难做到的事之一;

 

耦合==依赖,耦合度低,就是依赖少;

优秀的软件开发人员要学会限制类或模块中暴露的接口数量类中的方法越少越好函数知道的变量越少越好类拥有的成员变量越少越好

隐藏你的数据。隐藏你的工具函数。隐藏你的常量。

 

在一个函数中不要有选择性参数,选择性参数可能是boolean类型,可能是枚举元素、整数或任何一种用于选择函数行为的参数。

使用多个函数,通常优于向单个函数传递某些代码来选择函数行为

 

让程序可读的最有力方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。解释性变量多比少好

函数名称应该表达其行为。若必须查看函数的实现才知道它是做什么的,就该换个更好的函数名

 

花时间理解算法。在你认为自己完成某个函数之前,确认自己理解了它是怎么工作的

 

位置错误的职责

 

用多态代替if/else或switch/case。 

命名常量替代没有意义的符号,如原始形态数字、字母

 

在代码中要足够准确明确自己为何要这么做,若遇到异常情况如何处理。若调用可能返回null的函数,确认自己检查了null值。

 

结构甚于约定。不能随意。

封装条件。因为布尔逻辑难以理解。

函数只做一件事。

每个函数都产生下一个函数所需的结果——>时序性;

 

在较高层级放置可配置数据。

不要继承常量,而是用静态导入

 

避免传递浏览

让协作者提供所需的全部服务,不要让模块了解太多其协作者的信息。即若A与B协作,B与C协作,不要让使用A的模块了解C的信息,如不要写类似a.getB().getC().doSomething()的代码。

 

使用枚举来替代常量。别再用public static final int,因为enum可以拥有方法和字段,从而成为能比int提供更多表达力和灵活性的强有力工具。

 

采用描述性名称。事物的意义随着软件的演化而变化,因此要经常性重新估量名称是否恰当

名称要与抽象层级相符。

尽可能使用标准命名法。

具有与项目有关的特定意义的名称用得越多,读者就越容易明白你的代码是做什么的。

 

原则:

a、稍后等于永不!

b、离开时要比发现时更整洁!持续改进!如只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套if语句。

c、沉迷测试!——>写出整洁代码。

d、具有表达力且短小精悍。 

e、命名——首先描述其作用,是干什么的,然后再定名字! 

f、测试使得改动变为可能

g、若有些函数想要共享某些变量,就让它们拥有自己的类

h、将大函数拆分为小函数,往往也是将类拆分为多个小类的时机

i、对类的任何修改(包括打开类)都有可能破坏类中的其它代码。 但对类进行扩展(也是有修改),就不会破坏类中的其它代码。

j、类应当对扩展开放,对修改关闭。通过子类化手段,对添加新功能是开放的,而且不影响其它类。

k、希望将系统打造成在添加或修改特性时尽可能少惹麻烦。系统通过扩展系统而非修改现有代码来添加新特性。(多态

l、使用依赖注入、接口和抽象尽可能减少耦合,如此设计就会进步。

m、重复有多种表现,极其雷同的代码行也是重复类似的代码往往可以调整得更相似。重复也有实现上的重复。 

n、做了一点点共性抽取,此时已经违反了SRP原则,因此可以把一个新方法分解到另外的类中。小规模复用可降低系统复杂性。

o、模板方法模式是一种移除高层级重复的通用技巧。

p、要编写整洁代码,必须先写肮脏的代码,然后再清理它。

q、尽量减少函数参数

r、放进拿出是重构过程中常见的事

s、if条件判断应该封装成一个方法,用来解释这个条件判断,这样可以更清晰表达代码意图;

t、以重用为原则,若不能完全重用,就部分重用,不能重用部分就分出去;重用的一定要具有原子性,即不可再分

u、严格分层,划分每层职责,将自己这层做好,然后再去调用下一层

v、每次修改都要问自己为什么要这样改,把修改的原因写下来;

w、设计良好的模块有着非常小的接口,不提供许多需要依靠的函数,因此耦合度也较低

x、不相互依赖的东西不该耦合。如普通的enum不应在特殊类中包括,因为这样应用程序就要了解这些更为特殊的类。根源是将变量、常量或函数不恰当放在临时方便的位置。应该要花点时间研究应该在什么地方声明函数、常量和变量。

y、开发者最重要的决定之一就是在哪里放代码——代码应该放在读者自然而然期待它所在的地方。先想想为什么要放在这不放在这行不行

z、通常倾向于选用非静态方法,若的确需要静态函数,要确保没机会打算让它有多态行为。

 

 

知和行:知就是熟悉有关原则、模式和实践的知识;只有通过实践才能叫掌握。

阅读大量代码,琢磨某段好在什么地方、坏在什么地方。

编写阅读清理代码时思维方式的知识库

以作者的思维路径考虑问题。

 

写代码要循序渐进,即写一段代码就运行检验一下;

忽略掉的代码部分就是缺陷藏身之地; 

必须要保证每个输入,都有输出;要有健壮性,即输入为null时,也要保证输出正常!

 

对于程序员来说,(逻辑)简单、易读、高可维护性和(业务方法)复用性,是最重要的!

a、恰当命名标识至关重要。

b、每段代码都该在你希望它所在的地方,若不在那里,就需要重构了。

c、对于四处遗弃的带注释的代码及反映过往或期望的无注释代码——除之而后快。

d、标准化——一贯的代码风格

e、自律——在实践中贯彻规程。


100W行代码,质的变化!——还得练!!!!

 

《敏捷软件开发:原则、模式与实践》:关注面向对象设计的原则以及开发者的实践方法

《代码整洁之道》
 

学习好程序员的思维过程,使用的技巧、技术和工具!

 

影响软件质量的因素:架构、项目管理、代码质量;

无论是架构还是代码都不强求完美,只求竭诚尽力而已。

 

代码质量和整洁度成正比。

干净的代码,在质量上可靠,为后期维护、升级奠定了良好的基础。

 

1、In Action

命名、函数、注释、代码格式、对象和数据结构、错误处理、边界问题、单元测试、类、系统、并发编程等做到整洁的最佳实践。 

 

(1) 整洁代码:

如何编写好代码?如何将糟糕的代码改写成好代码?——消除重复、只做一件事、提高代码表达力、提早构建简单抽象(小规模抽象),时时保持代码整洁;

 

关注模型和需求!需求能作为代码的可执行测试来使用。细节(代码)必须明确

 

代码:就是用特定语言编写的规约,是最终用来表达需求的语言。它必须严谨、精确、规范、详细,好让机器理解和执行!

对代码的每次修改都影响到其它两三处代码。

要熟悉系统设计,这样修改才能符合设计意图。

花时间保持代码整洁关乎生存

 

用户指望需求、功能是否都在系统中实现,PM从我们这里得到必须的信息,我们不应该羞于告知自己的想法!PM奋力卫护进度和需求,coder奋力卫护code!

程序员不能遵从不了解混乱风险的PM的意愿

 

做得快的唯一方法——始终尽可能保持代码整洁!之前的混乱会拖自己的后腿!

 

写整洁代码,需要遵守大量的小技巧!只有实践,才能得到这种‘代码感’。有了这种代码感,就能从混乱中看到其它的可能与变化。

 

什么是整洁代码?——整洁的代码只做一件事!糟糕的代码想做太多事。整洁的代码力求集中。每个函数、每个类和每个模块都全神贯注于一事,不受周围的污染!

a、代码逻辑简单、直接(只提供一种而非多种做一件事的途径),充满干净的抽象和直接的控制语句。代码应讲述事实。——缺陷难以隐藏;

b 、尽量减少依赖关系——便于维护;

c、完善错误处理代码——健壮,如内存泄露、命名方式等;

d、性能最优——速度,被浪费掉的运算周期并不雅观;

e、有单元测试,使用有意义的命名。明确定义和提供清晰、尽量少的API,代码应通过其字面表达含义

f、写小块的代码,越小越好! 

g、没有重复代码;若同一段代码反复出现,就表示某种想法未在代码中得到良好的体现。就要尽力去找出到底那是什么,然后再尽力更清晰表达出来。

h、包括尽量少的实体,如类、方法、函数等;检查对象或方法是否想做的事太多,若对象功能太多,最好是切分为两个或多个对象。若方法功能太多,就需要重构,得到一个能较为清晰说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。

i、有意义的命名是体现表达力的一种方式,往往需要修改好几次才会定下名字来。

j、构建简单抽象:即当发现所有程序都由极为相似的元素构成时,可以把实现手段封装到更抽象的方法或类中

k、代码专为解决那个问题而存在;每个模块都为下一个模块做好准备。每个模块都告诉你下一个模块会是怎样的。

l、无论是设计系统或单独的模块,使用大概可工作的最简单方案。

 

读优雅的代码——令人愉悦;

整洁的代码总是看起来像是某位特别在意它的人写的!

 

修改糟糕的代码,往往越改越烂;

易读的代码 != 易修改的代码

 

读代码与写代码的时间比例超过10:1。写新代码时,一直都在读旧代码。不读周边代码就没法写代码。编写代码的难度,取决于读周边代码的难度。

 

代码随时间流逝而腐坏

 

(2)有意义的命名

取好名字的简单规则:精确是命名的要点;

a、见名知意

取好名字要花时间。一旦发现有更好的名称,就换掉旧的。

变量、函数或类的名称应已经答复了所有的大问题:为什么会存在?它做什么事?应该怎么用?若名称还需要注释补充,就不算是名副其实!

体现本意的名称让人更容易理解和修改代码!

使用读得出来的名称

b、少用前缀

c、做有意义的区分:要区分名称,就要以读者能鉴别不同之处的方式来区分

d、专业程序员明确,编写其他人能理解的代码;不要让读者在脑中把你的名称翻译为他们熟知的名称

优秀的程序员和设计师,其工作之一就是分离解决方案领域和问题领域的概念。与所涉问题领域更为贴近的代码,应当采用源自问题领域的名称

e、类名:名词或名词短语;

f、方法名:动词或动词短语;

g、每个抽象概念对应一个词,并且一以贯之;

h、给名称添加有意义的语境,但不要添加没用的语境;只要短名称足够清楚,就要比长名称好。

大多数名称都不能自我说明的。因此,需要用有良好命名的函数或名称空间来放置名称给读者提供语境。若没这么做,给名称添加前缀是最后一招!

 

当函数内容较长时,可以分解此函数——可以创建一个类语境也增强了,同时算法能够通过分解为更小的函数而变得更为干净利落。

 

i、重构时,将名称改得更好;

 

(3) 函数

函数是所有程序中的第一组代码;

如何让函数易于阅读和理解:保持函数短小、确保只做一件事,让代码读起来像是一系列自顶向下的!

a、第一原则:短小;20行封顶最佳。

b、函数要一目了然,每个函数只做一件事(函数只做该函数名下同一抽象层上的步骤)

c、if、else、while语句等,其中的代码块应该只有一行,该行是一个函数调用语句;

d、函数的缩进层级不该多于一层或两层,这样的函数易于阅读和理解;

e、编写函数就是把大一些的概念(即函数的名称)拆分为另一抽象层上的一系列步骤

f、每个函数一个抽象层级:基础概念和细节不要混在一起

g、函数名使用描述性、说明性的名称:描述函数做的事

若每个程序都让你感到深合己意,那就是整洁代码;——为只做一件事的小函数取个好名字函数越短小、功能越集中,就越便于取个好名字

长而具有描述性的名称,比短而令人费解的名称好!

让函数名称中的多个单词容易阅读,使用这些单词给函数取个能说清其功用的名称。 

选择描述性的名称能理清你关于模块的设计思路,并帮你改进之。追索好名称,往往导致对代码的改善重构。

命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。

 

h、函数要尽量避免三个参数!最理想的参数数量是0,其次是1,再次是2;从测试的角度看,参数过多,要编写能确保参数的各种组合运行正常的测试用例很困难。

我们习惯认为信息通过参数输入函数,通过返回值从函数中输出,不太期望信息通过参数输出应避免使用输出参数

若仅向函数传入布尔值,就表示本函数不止做一件事——应把该函数一分为二

利用一些机制将二元函数转换为一元函数;如:可以把writeField(uutputStream, name)方法写成outputStream的成员之一,从而这样用:outputStream.writeField(name)。

若函数需要三个或三个以上参数,就说明其中一些参数应该封装为类了。也可以使用有可变参数的函数

对于一元函数,函数和参数应当形成一个良好的动词/名词对形式

给函数取个好名字,能较好解释函数的意图,以及参数的顺序和意图。也可以把参数的名称写入到函数名中

 

i、函数应该修改某对象的状态,或是返回该对象的有关信息,但不要同时做!函数要么做什么事,要么回答什么事。

 

j、抽离try/catch代码块

它搞乱了代码结构,把错误处理和正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数

若try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面不该有其它内容。因为错误处理就是一件事。处理错误的函数只做一件事。 

使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。如:

if(deletePage(page) == E_OK) {

           if(registry.deleteReference(page.name) == E_OK) {

                    logger.log("aa");

           }else {

                    logger.log("cc");

          }

}else {

          logger.log("bb");

}

可以改为:

try{

        deletePage(page);

        registry.deleteReference(page.name);

}catch(Exception e) {

        logger.log("aaa");

}

 

返回错误码通常暗示某处有个类或是枚举,定义了所有错误码。

使用异常代替错误码,新异常可以从异常类派生出来。

 

k、重复是软件中一切邪恶的根源; 软件开发领域的所有创新都是在不断尝试从源代码中消灭重复;数据库范式就是为了消灭数据重复。

 

l、只要函数保持短小(这个是前提),偶尔出现的return、break或continue语句没有坏处,甚至比单入单出更具有表达力;goto只在大函数中才有道理,应尽量避免使用。

 

m、switch语句

switch天生做N件事。并且当出现新的条件时,会变得更长。违反了单一职责原则,因为有好几个修改它的理由。违反了开闭原则,因为每当添加新的条件时,就必须修改之。

 解决该问题的方案:将switch语句放到抽象工厂底下,不让任何人看到。

 

n、函数要无副作用。即承诺只做一件事,但还是会做其他被藏起来的事。因此,一定要在函数名称中说明

 

如何写出这样的函数:

刚开始时,先想什么就写什么,然后再打磨它。初稿也许粗陋无序,通过斟酌推敲,直到遵循以上列出的规则。

刚写函数,都冗长而复杂,然后分解函数、修改名称、消除重复,缩短和重新安置方法,拆散类等,但要保证单元测试通过,遵循以上列出的规则。

 

每个系统都是使用某种领域特定语言搭建,这种语言是程序员设计来描述那个系统的。

领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级

 

真正的目标在于把系统当做故事来讲,而不是当做程序来写。你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事!

 

(4)注释

若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释。

注释是弥补我们在用代码表达意图时遭遇的失败

注释存在的时间越久,就离其所描述的代码越远。

 

只有代码能忠实告诉你它做的事,那是唯一真正准确的信息来源。

用代码来解释。只需要创建一个描述与注释所言同一事物的函数即可。

 

对于注释的代码,因为有源代码控制系统能记住这些不要的代码,因此直接删掉即可,无需注释。

 

注释的作用是解释未能自行解释的代码。

 

短函数不需要太多描述为只做一件事的短函数选个好名字比写注释要好

 

(5)格式(包括直垂方向和水平方向

短文件比长文件易于理解。一个文件最多500行。 

 

源文件名称本身应该足够告诉我们是否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。

 

每个空白行都是一条线索,标识出新的独立概念

关系密切的概念应该相互靠近。如变量声明应尽可能靠近其使用位置;成员变量放在类的顶部声明;若某个函数调用了另外一个,就应该将其放在一起,且调用者应放在被调用者上面;

概念相关的代码应该放在一起。

自上而下展示函数调用顺序,建立自顶向下贯穿源代码模块的良好信息流。 最重要的概念先出来,以最少的细节表述它们,底层细节最后出来。

 

短代码行——一行最多120个字符

用不对齐的声明和赋值。

若类中属性的声明的列表长度过长,就表示该类应该被拆分了

缩进:表示层级。

 

在团队中工作就是团队说了算。软件要有一以贯之的风格。

软件系统由一系列代码文件组成。

 

(6)对象和数据结构

不愿暴露数据细节,更愿意以抽象形态表述数据要以更好的方式呈现某个对象包含的数据

 

对象:把数据隐藏在抽象之后,暴露操作数据的函数。

数据结构:暴露其数据,没有提供有意义的函数。

 

当需要添加新数据类型而不是新函数时,此时对象和面向对象比较适合;当需要添加新函数而不是数据类型时,此时过程式代码和数据结构更合适。

 

连串的调用被认为是肮脏的代码,最好做切分!

 

模块不应了解它所操作对象的内部情形。

DTO(数据传输对象):在一系列将原始数据转换为数据库的过程中常使用。不要在这样的数据结构中添加业务方法

 

(7)错误处理(遇到错误时,最好抛出一个异常调用代码很整洁,其逻辑不会被错误处理搞乱

错误处理是编程时必须要做的事之一。因为输入可能出现异常,可能会出错。当错误发生时,程序员有责任确保代码照常工作

 

错误处理很重要,若它搞乱了代码逻辑,就是错误的做法。

 

处理错误代码的技巧:

a、使用异常而非返回码。遇到错误时,最好抛出一个异常调用代码很整洁,其逻辑不会被错误处理搞乱

b、先写try-catch-finally语句。无论try代码块中执行的代码出什么错,能帮你定义代码的用户应该期待什么。

c、在catch中,要记录日志。

d、依调用者需要定义异常类。在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。

将第三方API打包是一个很好的最佳实践。

e、区隔业务逻辑和错误处理代码,会变得整洁!——还可以写得更简洁,即创建一个类或配置一个对象,用来处理特例。你来处理特例,客户代码就不用应付异常行为了。异常行为被封装到特例对象中。

f、别返回null值。——避免NullPointerException的出现,代码就更整洁!

若打算在方法中返回null值,不如抛出异常,或是返回特例对象。若在调用某个第三方API中可能返回null值的方法,可以考虑用新方法打包这个方法,在新方法中抛出异常或返回特例对象。

返回null值就是在给自己增加工作量。只要有一处没检查null值,应用程序就会失控

java有Collections.emptyList()方法,该方法返回一个预定义不可变列表。

g、别传递null值:禁止传入null值。

除非API要求你向它传递null值,否则就要尽可能避免传递null值。

定义了异常,就得定义处理器!

传入null值,会得到运行时错误。在大多数编程语言中,没有良好的方法能对付由调用者意外传入的null值。恰当的做法就是禁止传入null值。这样,编码时传入null值就意味着出问题了。

 

整洁代码是可读的,但也要强固。 若将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。

 

(8)边界?

我们都需要将外来代码干净地整合进自己的代码中。 

 

8.2节没有阅读完!

 

保持软件边界整洁的方法:

a、编写测试是学习API最好途径。需要对第三方程序包写单元测试,了解其API的行为是否发生了改变。

可以使用这些边界测试来减轻迁移的劳力。

b、建议不要将Map(或在边界上的其他接口)在系统中传递。若要使用类似Map这样的边界接口,就把它保留在类中。因为Map需要进行类型转换,不是整洁的代码。

避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。

 

良好的软件设计,无需巨大投入和重写即可进行修改。在使用我们控制不了的代码时,要确保未来的修改不至于代价太大。

边界上的代码需要清晰的分割和定义了期望的测试。可以对第三方接口进行包装,或者使用adapter模式

 

(9)单元测试

测试代码和生产代码一样重要。它需要被思考、被设计,它该像生产代码一样保持整洁

 

覆盖了生产代码的自动化单元测试程序能尽可能保持设计和架构的整洁

测试使得改动变为可能

丢失了测试,代码也开始腐坏!

 

整洁的测试:可读性——明确、简洁、足够的表达力。在测试中,要以尽可能少的文字表达大量内容。

 

每个测试都可以清晰的拆分为三个环节:

第一个环节是构造测试数据,第二个环节是操作测试数据,第三个部分是检验操作是否得到期望的结果。

 

将细节作封装。读测试的人一读就能搞清楚状况,不至于被细节误导或吓到

 

每个测试一个断言!

每个测试一个概念,即只做一件事!

 

对于重复代码——可以使用模板模式,将不变的放到基类中,将变的放到派生类中!

 

整洁的测试遵循以下5个原则:

a、快速。测试应该够快。

b、独立。测试应该相互独立。

c、可重复

d、自我验证。测试应该有布尔值输出

e、及时。测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写

 

测试保证和增强了生产代码的可扩展性、可维护性和可复用性。因此,要保持测试代码整洁,让测试具有表达力并短小精悍

 

发明作为面向特定语言的测试API,帮助自己编写测试!

 

如果你坐视测试腐坏,那么代码也会跟着腐坏

 

(10)

代码语句以及由代码语句构成的函数的表达力

 

a、首先会想实质保有隐私,放松封装总是下策。

b、类应该短小精悍衡量的方法为职责

类的名称应当描述其职责。命名是帮助判断类的长度的第一个手段,若无法为某个类命以精确的名称,这个类就太长了。

c、类或模块应有且有一条加以修改的理由——单一职责原则。

鉴别职责可以帮助我们在代码中认识到并创建出更好的抽象。

代码组织和整洁

每个达到一定规模的系统都会包括大量逻辑和复杂性。管理这种复杂性的首要目标是加以组织,以便开发者知道在哪儿能找到东西。

系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个职责,只有一个修改的原因。

d、内聚

类应该只有少量成员变量。若一个类中的每个成员变量都被每个方法所使用,则该类具有最大的内聚性。一般来说,这是不可能的。但是是追求的目标

内聚性高,意味着类中的方法和变量相互依赖、互相结合成一个逻辑整体。

 

保持函数和参数列表短小,有时会导致一组子集方法所用的成员变量数量增加。此时意味着至少有一个类要从大类中分拆,应尝试将这些变量和方法分拆到两个或多个类中,让新的类更为内聚。

e、保持内聚性就会得到许多短小的类

堆积了越来越多只为允许少量函数共享而存在的成员变量——>类丧失了内聚性。当类丧失了内聚性,就拆分它若有些函数想要共享某些变量,就让它们拥有自己的类

将大函数拆分为小函数,往往也是将类拆分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构。

 

f、为了修改而组织

修改一直持续。

每次修改都让我们冒着系统其它部分不能如期望般工作的风险。因此,对类加以组织,以降低修改的风险

 

出现了只与类的一小部分有关的私有方法行为,意味着存在改进空间。

打开类,风险也随之而来

对类的任何修改(包括打开类)都有可能破坏类中的其它代码。 但对类进行扩展(也是有修改),就不会破坏类中的其它代码。——多态

一旦打开了类,就应当修正设计方案

私有方法,放到需要它的类中。

公共的方法,放到独立的工具类中。

 

g、隔离修改:通过接口和抽象类来实现。

抽象类只呈现概念。

 

面向接口编程——>系统解耦,会更加灵活、更加可复用。部件之间的解耦代表着系统中的元素相互隔离得很好。隔离让对系统每个元素的理解更容易。

 

依赖倒置原则:本质就是应当依赖于抽象而不是依赖于具体的实现。

 

(11)系统

 有些人负责全局,其它人负责细节

恰当的抽象等级和模块,能让组件在不了解全局时也能有效运转。

 

在较高的抽象层级(系统层级)上保持整洁。

 

a、将系统的构造与使用分开

软件系统应将启始过程和启始过程之后的运行时逻辑分离开

每个应用程序都该留意启始过程。将关注的方面分离开,是很重要的设计技巧

缺乏模块组织性,通常会有许多重复代码。

 

main函数创建系统所需的对象,再传递给应用程序,应用程序只管使用。

有时应用程序也要负责确定何时创建对象,此时可用抽象工厂模式让应用自行控制何时创建实体,构造的细节隔离于应用程序代码之外

依赖注入可以实现分离构造和使用。这种授权机制通常要么是main例程,要么是有特定目的的容器。如JNDI查找就是依赖注入的一种部分实现。在JNDI中,对象请求目录服务器提供一种符合某个特定名称的“服务”。

 

延后初始化:多数DI容器在需要对象之前并不构造对象,这类容器提供调用工厂或构造代理的机制。

 

b、扩容

一开始就做对系统是神话。我们应该只去实现今天的用户故事,然后重构,明天再扩展系统、实现新的用户故事。这就是迭代和增量敏捷的精髓。测试驱动开发、重构以及它们打造出的整洁代码,在代码层面保证了这个过程的实现。

 

软件系统的架构可以递增式增长,只要我们持续将关注面恰当切分

 

AOP是一种恢复横贯式关注面模块化的普适手段。

java代理适用于简单的情况,如在单独的对象或类中包装方法调用。JDK提供的动态代理仅能与接口协同工作。对于代理类,得使用字节码操作库,如CGLIB、ASM或Javassist。

Proxy API需要一个InvocationHandler对象,用来实现对代理的全部接口的方法调用

 

使用描述性配置文件或API,把需要的应用程序构架组合起来,包括持久化、事务、安全、缓存、恢复等横贯性问题。——实际情况是框架以对用户透明的方式处理使用java代理或字节码库的机制。

 

通过方面来实现关注面切分的功能最全的工具是AspectJ语言。它将方面作为模块构造处理支持Java扩展。

spring AOP和JBoss AOP提供纯java实现手段。

 

AspectJ引入了使用Java5 annotation定义纯java代码的方面。

 

c、没必要先做设计。将架构按需从简单演化到精细。

 

只要软件的构架有效切分了各个关注面,还是有可能做根本性改动的。可以从简单自然但切分良好的架构开始做软件项目,随着规模的增长添加更多基础架构。

网站采用了精密的数据缓存、安全、虚拟化等技术,获得了极高的可用性和性能。在每个抽象层和范围之内,那些最小化耦合的设计都简单到位,效率和灵活性也随之而来。

 

对总的覆盖范围、目标、项目进度和最终系统的总体架构,要有所预期。必须有能力随机应变。

 

最佳的系统架构是由模块化的关注面领域组成,每个关注面均用纯java对象实现。

 

d、模块化和关注面切分成就了分散化管理和决策

 

e、系统需要领域特定语言

领域特定语言是一种单独的小型脚步语言或以标准语言写就的API,领域专家用它编写代码。

 

优秀的领域特定语言填平了领域概念和实现领域概念的代码之间的鸿沟。

敏捷实践优化了开发团队和甲方之间的沟通。

若用和领域专家使用的同一种语言来实现领域逻辑,就会降低不正确将领域翻译为实现的风险。

 

领域特定语言在有效使用时,能提升代码和设计模式之上的抽象层次。

领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用POJO来表达。

  

系统应该是整洁的。侵害性架构会湮灭领域逻辑。当领域逻辑受到困扰,质量就会堪忧。

 

在所有的抽象层级上,意图都应该清晰可辨

 

无论是设计系统或单独的模块,使用大概可工作的最简单方案。

 

(12)迭

通过跌进设计达到整洁目的。

 

简单设计的4条规则:(以下规则按其重要程度排列)

a、运行所有测试;

b、不可重复

c、表达了程序员的意图

d、尽可能减少类和方法的数量优先级最低

 

只要系统可测试,就会导向保持类短小且目的单一的设计方案。

确保系统完全可测试能帮助我们更好的设计

紧耦合的代码难以编写测试。编写测试越多,就越会遵循DIP之类的规则。

使用依赖注入、接口和抽象尽可能减少耦合,如此设计就会进步。

 

递增式地重构代码

测试消除了对清理代码就会破坏代码的恐惧。

 

在重构过程中,提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称等。

 

重复是拥有良好设计系统的大敌。

重复有多种表现,极其雷同的代码行也是重复类似的代码往往可以调整得更相似

重复也有实现上的重复。 

 

要创建整洁的系统,需要有消除重复的意愿。即便对于短短几行也是如此。

 

软件项目的主要成本在于长期维护。

为了在修改时尽量降低出现bug的可能性,要理解系统是做什么。

代码要清晰表达作者的意图。

短小的类和函数易于命名,易于理解。

通过在实现模式的类的名称中采用标准模式名,能充分向其他开发者描述你的设计。

测试的主要目的之一是通过实例起到文档的作用,读到测试的人能很快理解某个类是做什么的

 

做到有表达力的最重要方式是尝试。——花一点时间在每个函数和类上。

 

(13)并发编程

编写整洁的并发程序非常难。编写在单线程中执行的代码简单得多。系统一旦遭受压力,代码就扛不住了。

 

并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开。

解耦目的与时机能明显改进应用程序的吞吐量和结构。从结构的角度看,应用程序更像是许多台协同工作的计算机。

 

要了解容器在做什么。了解如何对付并发更新、死锁等问题。

 

编写并发软件:

a、并发会在性能和编写额外代码上增加一些开销;

b、正确的并发是复杂的

c、并发缺陷并非总能重现

d、并发常常需要对设计策略的根本性修改。 

 

两个线程会相互影响。因为线程在执行一行代码时有许多可能路径。

有多少种不同路径?——需要理解just-in-time编译器如何对待生成的字节码,还要理解java内存模型认为什么东西具有原子性。

 

并发防御原则:

a、单一职责原则(SRP)认为,方法、类、组件应当只有一个修改的理由。

建议分离并发代码和其他代码

b、限制数据作用域。

谨记数据封装;严格限制对可能被共享的数据的访问

c、使用数据复本

d、线程应尽可能独立。

让每个线程在自己的世界中存在,不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量。这样每个线程都像是世界中的唯一线程,没有同步需要。

尝试将数据分解到可被独立线程(可能在不同处理器上)操作的独立子集。

 

在用java5编写线程代码,要注意以下几点:

a、使用类库提供的线程安全群集;如java.util.concurrent包,该代码包中的群集对于多线程解决方案是安全的,执行良好。

b、使用executor框架执行无关任务;

c、尽可能使用非锁定解决方案;

d、有几个类并不是线程安全的;

 

还有几个支持高级并发设计的类,如ReentrantLock类,可在一个方法中获取,在另一个方法中释放的锁;Semaphore类,经典的‘信号’的一种实现,有计数器的锁;CountDownLatch类,在释放所有等待的线程之前,等待指定数量事件发生的锁。这样,所有线程都平等的几乎同时启动。

 

对于Java,掌握java.util.concurrent、java.util.concurrent.atomic、java.util.concurrent.locks。

 

限定资源:并发环境中有着固定尺寸或数量的资源。如数据库连接和固定尺寸读、写缓存等;

互斥:每一时刻仅有一个线程能访问共享数据或共享资源;

线程饥饿:一个或一组线程在很长时间内或永久被禁止。如,总是让执行快的线程先运行,假如执行快的线程没完没了,则执行时间长得线程就会挨饿。

死锁:两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源,得不到其他线程拥有的资源,就无法终止。

活锁:执行次序一致的线程,每个都想要起步,但发现其它线程已经在路上。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动。

 

在并发编程中用到的几种执行模型:

a、生产者-消费者模型

一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。

b、读者-作者模型?

当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的累积。更新会影响吞吐量。协调读者线程,不去读作者线程正在更新的信息(反之亦然),是一种辛苦的平衡工作。作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。

挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。

c、 宴席哲学家

用线程代替哲学家,用资源代替叉子,就变成了进程竞争资源的情形。若没有用心设计,这种竞争式系统会遭遇死锁、活锁、吞吐量和效率降低等问题。

 

可能遇到的并发问题,大多数都是这三个问题的变种。请研究并使用这些算法,这样,遇到并发问题时你就能有解决问题的准备了。

 

警惕同步方法之间的依赖:避免使用一个共享对象的多个方法

若必须要使用一个共享对象的多个方法,在这种情况发生时,有3种写对代码的手段:

1、基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码; 

2、基于服务端的锁定——在服务端创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法。

3、适配服务端——创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码。

 

保持同步区域微小

synchronized制造了锁。同一个锁维护的所有代码区域在任一时刻保证只有一个线程执行。锁是昂贵的,因为它带来了延迟和额外开销。

 

很难编写正确的关闭代码:

建议尽早考虑关闭问题,尽早令其工作正常。这会花费比你预期更多的时间。

 

将伪失败看作可能的线程问题;

先使非线程代码可工作;偶发事件被忽略得越久,代码就越有可能搭建于不完善的基础之上。

编写可插拔的线程代码;这意味着创建由线程调用的POJO。能放进POJO中的代码越多越好。

编写可调整的线程代码;

运行多于处理器数量的线程;这是为了促使任务交换的发生。因为系统在切换任务时会发生一些事。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。

在不同平台上运行;不同操作系统有着不同线程策略,不同的线程策略影响了代码的执行。在不同环境中,多线程代码的行为也不一样。尽早并经常地在所有目标平台上运行线程代码。

调整代码并强迫错误发生;

 

装置调试代码:增加对Object.wait()、Object.sleep()、Object.yield()和Object.priority()等方法的调用,改变代码执行顺序。因为这些方法都会影响执行顺序,从而增加了检查到bug的可能性。

有两种装置代码的方法:

硬编码:即手工向代码中加入。若将系统分解为对线程及控制线程的类一无所知的POJO,就能更容易找到装置代码的位置。

自动化:使用CGLIB或ASM等之类工具通过编程来装置代码。

要点是让代码‘异动’,从而使线程以不同次序执行。编写良好的测试与异动相结合,能有效增加发现bug的机会

 

并发代码很难写正确。加入多线程和共享数据后,简单的代码也会出错。要编写并发代码,就得严格编写整洁的代码。

第一要诀就是遵循单一职责原则。将系统切分为分离了线程相关代码和线程无关代码的POJO。确保在测试线程相关代码时只是在测试,没有做其他事情。线程相关代码应保持短小和目的集中

 

了解并发问题的可能原因,对共享数据的多线程操作,或使用了公共资源池。类似平静关闭或停止循环之类边界情况尤其棘手。

学习类库,了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。

学习如何找到必须锁定的代码区域并锁定之。不要锁定不必锁定的代码。避免从锁定区域中调用其他锁定区域。

花点时间装置代码,就能极大提升发现bug的机会

 

(14)逐步改进

编程是一种技能。

要编写整洁代码,必须先写肮脏的代码,然后再清理它

 

若希望代码结构一直可维护,就需要调整了。

许多种不同类型,类似的方法——>类。

毁坏程序最好的方法之一就是以改进之名大动其结构

采用TDD是为了保持系统始终能运行

每次修改一个地方,持续运行测试。若测试出错,在做下一个修改前确保通过

 

一个类最好就抛出唯一一个异常。如Args类就抛出一个ArgsException。并且将大量的错误支持代码从Args类转移到ArgsException类中。

 

重构就是一种不停试错的迭代过程。

模块都能再改进,每个人都有责任把模块改进得比发现时更整洁

 

(15)

a、首先,让它能工作;

b、让它做对;

 

用Clover检查单元测试覆盖了哪些代码。

 

serialVersionUID变量用于控制序列号——自动控制序列号

 

使用解释临时变量模式使函数中的算法更为透明;

将抽象方法移到顶层类中;

 

 若两个方法中存在一些重复,可以抽离一个新方法来消除重复

 

每次修改都要问自己为什么要这样改,把修改的原因写下来

 

(16)总结

注释:只应该描述有关代码和设计的技术性信息;注释会很快过时,若发现废弃的注释,最好尽快更新或删除;注释应提及代码自身没提到的东西

看到注释掉的代码,就删除它!因为源代码控制系统会记得它。

 

应当能够使用单个命令签出系统,并用单个指令构建它。

应当能够发出单个指令就可以运行全部单元测试。

 

函数的参数量应该少;

布尔值参数宣告了函数不止做一件事,应消灭掉;

永不被调用的方法应该删除;

 

尽力减少源文件中额外语言的数量和范围;

代码应该有正确行为——开发者常常写出他们以为能工作的函数,而不是去证明代码在所有的角落和边界情形下真能工作追索每种边界条件,并编写测试

 

每次看到重复代码,都代表遗漏了抽象。重复的代码可能成为子程序或干脆是另一个类。将重复代码叠放进类似的抽象,增加了你的设计语言的词汇量,提升了抽象层级。

重复最明显的形态就是不断看到明显一样的代码,不断在复制粘贴代码,可以用单一方法来替代之

较隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的switch/case或if/else链。可以用多态来替代之。

更隐蔽的形态是采用类似算法但具体代码行不同的模块,这也是重复,可以使用模板方法模式或策略模式来修正。

 

设计模式是消除重复的有效手段。 范式是消除数据库设计中的重复的策略。oo自身也是组织模块和消除重复的策略。

 

创建分离较高层级一般性概念与较低层级细节概念的抽象模型,分离要完整,这很重要。通过创建抽象类来容纳较高层级概念,创建派生类来容纳较低层次概念。

与细节实现有关的常量、变量或工具函数不应该在基类中出现。

良好的软件设计要求分离位于不同层级的概念。 

 

死代码就是不执行的代码。可以在检查不会发生的条件的if语句体中找到。可以在从不抛出异常的try语句的catch块中找到。可以在永不会发生的switch/case条件中找到。

找到死代码,就要删除它。

 

变量和函数应该在靠近被使用的地方定义。

 

取名字要前后一致,这样让代码更加易读。

 

保持源文件整洁,良好的组织,不被搞乱。删除不用的

 

类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其它类中的变量和函数。(类总持有另一个对象的引用属于例外) 

代码要尽可能具有表达力

 

(17)


 

2、TIPS

(1)什么是好的代码?如何提高代码质量?如何通过代码评审,提高设计能力?

a、运行起来实现功能;

b、要应对变化,易扩展(对修改关闭,对扩展打开!)

c、易读、可维护性。

d、性能

 

(2)测试

只要还有没被测试探测过的条件,或是有没被验证的,测试就还不够。

 

使用覆盖率工具。覆盖率工具能汇报你测试策略中的缺口。

 

测试边界条件。算法的中间部分正确但边界判断错误的情形很常见

 

bug趋向于扎堆。在某个函数中发现一个bug时,最好全面测试那个函数,可能会发现bug不止一个。

 

测试应该快速。

 

(3) 

 

(4)

 

(5)

 

(6)自顶向下读代码:向下规则

每个函数后面都跟着位于下一抽象层级的函数。

 

(7)违反单一职责原则:因为不只做了一件事,有好几个修改它的理由;

违反开闭原则:因为每当添加新类型时,就必须修改之;

 

(8)21种代码坏味道

a、重复的代码

b、过长的函数

c、过大的类

d、过长的参数列表

e、发散式变化

f、分散的修改

g、伪面向对象的调用

h、数据泥团

i、基本类型的误用

j、switch-case结构的误用

k、平行继承体系

l、过薄的类

m、只有局部意义的成员变量

n、过度耦合的消息链

o、过薄的中间对象

p、紧耦合类

q、相似的类

r、只有数据的类

s、滥用类的继承关系


(3)代码命名

类名:通过名字表示;描述功能;描述存储的对象;

 

(4)

 

(5)


(6)


3、PS

(1)一个大访问量和高负载的网站,性能好坏取决于架构,而不取决于编程语言!

 

(2)软件生命周期:设计、编码、重构、单元测试、持续集成等。

重构是实现优秀设计的一种重要手段。

 

(3)代码静态分析工具:方便在编码阶段就能找出可能的编码缺陷,如java中的findbugs、checkstyle、PMD、javancss等。

checkstyle:用于编码标准;

PMD的CPD:帮助发现代码质量;

coverlipse:测量代码覆盖率;

JDepend:提供依赖项分析;

metric:有效查出复杂度;

 

代码分析工具:Similarity Analyzer、Jester;

 

(4)软件应对需求变化的能力越来越差——>架构重构。

 

(5)建议将算法分成多个函数!

 

(6)维护别人写的代码,使用重构工具解决问题,效果好!

 

(7)类的结构层次分明!

 

(8)code需要经历的考验:

a、单元测试

b、冒烟测试

c、build verification test

d、系统测试

e、故障演习

f、测试机群

g、生产机群

 

(9)怎样review别人的代码?

需要从各个截面去理解。

用户接口:

出错代码:

内部状态:这些状态如如何相互转换的;

测试用例:每种转换的情况是否有case?性能如何?

某个方法:变量的使用;检查多线程问题;检查逻辑错误


保证可读性:

若3分钟之内读不懂,让被review的人修改;

 

(10)合适的地方打合适级别的log

五级:debug、info、warn、error、fatal。

系统内在的一些逻辑(如每个函数的进入、退出):debug;

正常的逻辑(如正常的执行了用户的一个请求):info;

非正常的逻辑(如用户试图删除不存在的文件):warn;

系统错误(如发现有一块磁盘坏了):error;

不可恢复的错误:fatal;

 

(11)

 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章