CleanCode---代码整洁之道笔记

一、整洁代码

糟糕 的代码想做太多事,它意图混乱、目的含混。整洁的代码只做好一件事,力求每个函数、每个类和每个模块都全神贯注于一事,完全不受四周细节的干扰和污染。如果对象功能太多,最好是切分为两个或者多个对象。如果方法功能太多,可以使用抽取手段进行重构,从而得到一个能较为清晰地说明自身功能的方法,以及另外数个说明如何实现这些功能的方法。

整洁的代码从 不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。

简单的代码有以下几点要求:

  1. 能通过所有测试。高效可用的代码是第一位。
  2. 没有重复的代码。如果同一段代码反复出现,就表示某种想法未在代码中得到良好的体现。
  3. 体现系统中的全部设计理念。
  4. 包括尽量少的实体,比如类、方法、函数等。

二、有意义的命名

  1. 名副其实

变量、函数或者类的名称应该告诉你,它为啥会存在、做什么事,该怎么用。如果名称需要注释来补充,就不算为一个好的命名。

日期类型的变量应该指明计量对象和计量单位的名称,如elapsedTimeInDays,daysSineCreation。

  1. 避免误导

程序员必须避免留下掩藏代码本意的错误线索。应当避免使用与本意相悖的词。例如,hp、aix和sco都不该用做变量名,因为它们都是UNIX平台或类UNIX平台的专有名称。

别用accountList来指称一组账号,除非它真的是List类型。List —词对程序员有特殊意义。如果包纳账号的容器并非List,就会引起错误的判断,所以用accountGroup或bunchOfAccounts。甚至直接用accounts都会好一些。

提防使用不同之处较小的名称。想区分模块中某处XYZControllerFor
EfficientflandlingOfStrings 和另一处的XYZControllerForEfficientStorageOfStrings,会花多长时
间呢?这两个词外形实在太相似了。

以同样的方式拼写出同样的概念才是信息。拼写前后不一致就是误导。

误导性名称真正可怕的例子是用小写字母l和大写字母O作为变量名,尤其是在两者组合使用的时候。主要是由于他们看起来完全像是常量’壹‘和‘零’。

  1. 做有意义的区分

如果程序员只是为满足编译器或者解释器的要求而编写代码,就会制造出很多麻烦。在变量中只是添加数字系列或是废话远远不够,即便这能够编译通过。如果名称必须相异,那其意思也应该不同才对。

以数字系列命名(al、a2,……aN)是依义命名的对立面。这样的名称纯属误导——完全没有提供正确信息:没有提供导向作者意图的线索。

废话是另一种没意义的区分。假设你有一个Product类。如果还有一个ProductInfo 或ProductData类, 那它们的名称虽然不同,意思却无区别 。Info和Data就像a、an和the—样,是意义含混的废话。废话都是冗余。Variable -词永远不应当出现在变量名中。Table -词永远不应当出现在表名中。NameString会比Name好吗?难道Name会是一个浮点数不成?

如果缺少明确约定,变量moneyAmount就与money没区别,customerlnfo与customer没区别 ,account Data 与account 没区另 ij ,theMessage也与message没区别 。要区分名称, 就要以读者能鉴别不同之处的方式来区分。

  1. 使用读的出来的名称

名称如果读不出来,和同事讨论的时候就会像个傻鸟。“哎,这儿,鼻涕阿三喜摁踢(bee cee am three cee enn tee)上头,有个皮挨死极翘(pee ess zee kyew) 整数,看见没?”这不是小事,因为编程本就是一种社会活动。

  1. 使用可搜索的名称

单字母名称和数字常量有个问题,就是很难在一大篇文字中找出来。找 MAX_CLASSES_PER_ STUDENT 很容易,但想找数字 7 就麻烦了,它可能是某些文件名或其他常量定义的一部分。同样,e 也不是个便于搜索的好变量名。它是英文中最常用的字母,在每个程序、每段代中都有可能出现。由此而见,长名称胜于短名称,搜得到的名称胜于用自造编码代写就的名称。

单字母名称仅用于短方法中的本地变量。名称长短应与其作用域大小相对应。若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。

  1. 避免使用编码

编码已经太多,无谓再自找麻烦。把类型或作用域编进名称里面,徒然增加了解码的负担。没理由要求每位新人都在弄清要应付的代码之外(那算是正常的),还要再搞另种编码“语言”。这对于解决问题而言,纯属多余的负担。带编码的名称通常也不便发音,容易打错。

现代编程语言具有更丰富的类型系统,编译器也记得并强制使用类型。而且,人们趋向于使用更小的类、更短的方法,好让每个变量的定义都在视野范围之内。

Java 程序员不需要类型编码。对象是强类型的,代码编辑环境已经先进到在编译开始前就侦测到类型错误的程度!所以,如今编码纯属多余,它们增加了修改变量、函数或类的名称或类型的难度,它们增加了阅读代码的难度,它们制造了让编码系统误导读者的可能性。

有时也会出现采用编码的特殊情形。比如,你在做一个创建形状用的抽象工厂(Abstract Factory)该工厂是个接口,要用具体类来实现。你怎么来命名工厂和具体类呢?Ishapefactory 和 Shapefactory 吗?我认为前导字母Ⅰ被滥用到了说好听点是干扰,说难听点根本就是废话的程度。我不想让用户知道我给他们的是接口。我就想让他们知道那是个 Shapefactory。如果接口和实现必须选一个来编码的话,我宁肯选择Shapefactorylmp,甚至是丑陋的 Cshapefactory,都比对接口名称编码来得好。

不必用 m_前缀来标明成员变量。应当把类和函数做得足够小,消除对成员前缀的需要,你应当使用某种可以高亮或用颜色标出成员的编辑环境。

  1. 避免思维映射

不应当让读者在脑中把你的名称翻译为他们熟知的名称。这种问题经常出现在选择是使用问题领域术语还是解决方案领域术语时

单字母变量名就是个问题。在作用域较小、也没有名称冲突时,循环计数器自然有可能被命名为 i 或 j 或 k。(但千万别用字母 1! )这是因为传统上惯用单字母名称做循环计数器。然而,在多数其他情况下,单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。

  1. 类名

类名和对象名应该是名词或名词短语,如 Customer、Wikipage、Account 和 Addressparser 避免使用 Manager、Processor、Data 或 nfo 这样的类名。类名不应当是动词。

  1. 方法名

方法名应当是动词或动词短语,如 postPayment、deletePage 或 save。属性访问器、修改器和断言应该根据其值命名,并依 Javabean 标准加上 get、set 和 is 前缀。

  1. 别伴可爱

伴可爱的做法在代码中经常体现为使用俗话或俚语。这种俗话或者俚语通常与文化和场景紧密相关,对读者理解代码不够友好,宁可明确,勿为好玩。言到意到,意到言到。

  1. 每个概念对应一个词

给每个抽象概念选一个词,并且一以贯之。例如,使用 feth、retrieve 和 get 来给在多个类中的同种方法命名。你怎么记得住哪个类中是哪个方法呢?很悲哀,你总得记住编写库或类的公司、机构或个人,能想得起来用的是哪个术语。否则,就得耗费大把时间浏览各个文件头及前面的代码。

Eclipse 和 Intellij 之类现代编程环境提供了与环境相关的线索,比如某个对象能调用的方法列表。不过要注意,列表中通常不会给出你为函数名和参数列表编写的注释。函数名称应当独一无二,而且要保持一致,这样你才能不借助多余的浏览就找到正确的方法。

12.别用双关语

避免将同一单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。如果遵循“一词一义”规则,可能在好多个类里面都会有 add 方法。只要这些add方法的参数列表和返回值在语义上等价即可。

但是,可能会有人决定为“保持一致”而使用 add 这个词来命名,即便并非真的想表示这种意思。比如,在多个类中都有 add 方法,该方法通过增加或连接两个现存值来获得新值。假设要写个新类,该类中有一个方法,把单个参数放到集合(collection)中。该把这个方法叫做 add 吗?这样做貌似和其他 add 方法保持了一致,但实际上语义却不同,应该用 insert 或 append 之类词来命名才对。把该方法命名为 add,就是双关语了。

13.使用解决方案领域名称

记住,只有程序员オ会读你的代码。所以,尽管用那些计算机科学(Computer Science, CS)术语、算法名、模式名、数学术语。依据问题所涉领域来命名不是聪明的做法,因为不该让协作者老是跑去问客户每个名称的含义。程序员要做太多技术性工作,给这些事取个技术性的名称,通常是最靠谱的做法。

  1. 使用源自所涉及问题领域的名称

如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。至少,负责维护代码的程序员就能去请教领域专家了。

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

  1. 添加有意义的语境

很少有名称是能自我说明的。反之,你需要用有良好命名的类、函数或名称空间来放置名称,给读者提供语境。如果没这么做,给名称添加前缀就是最后一招了。

设想你有名为 firstName、lastName、street, houseNumber、city、state 和 zipcode 的变量。当它们搁一块儿的时候,很明确是构成了一个地址。不过,假使只是在某个方法中看见孤零零一个 state 变量呢?你会理所当然推断那是某个地址的一部分吗?可以添加前缀 addrstate 等,以此提供语境。至少,读者会明白这些变量是某个更大结构的一部分。当然,更好的方案是创建名为 Address 的类。这,即便是编译器也会知道这些变量隶属某个更大的概念了。

16.不要添加没用的语境

只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。精确正是命名的要点

设若有一个名为GSD的应用,在其中给每个类添加 GSD 前缀就不是什么好点子。说白了,你是在和自己在用的工具过不去。输入G,按下自动完成键,结果会得到系统中全部类的列表,列表恨不得有一英里那么长。这样做聪明吗?为什么要搞得 IDE 没法帮助你?

三、函数

  1. 短小

函数的第一规则是要短小。第二条规则就是还要更短小。

if语句、else 语句、while 语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。函数的缩进层级不应该多于一层或两层。

  1. 只做一件事

函数应该做一件事。做好这件事。只做这一件事。如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。

要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。

  1. 一个函数一个抽象层级

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。

我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。

  1. switch 语句

写出短小的 switch语句很难。即便是只有两种条件的 switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的 switch语句也很难。 Switch天生要做N件事。不幸我们总无法避开 switch语句,不过还是能够确保每个 switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。

 public Money calculatePay(Employee e) throws InvalidEmployeeType{
		 switch (e.type){
				 case COMMISSIONED:
						 return calculateCommissionedPay(e);
				 case hourly:
						 return calculateHourlypay(e);
				 case SALARIED:
             return calculateSalariedPay(e);
				 default:
             throw new InvalidEmployee Type(e.type); 
			}
} 

该函数有好几个问题。首先,它太长,当出现新的雇员类型时,还会变得更长。其次,它明显做了不止一件事。第三,它违反了单一权责原则( Single Responsibility Principle,SRP)因为有好几个修改它的理由。第四,它违反了开放闭合原则( Open Closed Principle,OCP),因为每当添加新类型时,就必须修改之。不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有: isPayday(Employee e, Date date), 或 deliveryPay(Employee e, Money pay) 该问题的解决方案(如代码清单3-5所示)是将 switch语句埋到抽象工厂底下,不让任何人看到。该工厂使用 switch语句为 Employee的派生物创建造当的实体,而不同的函数,如 calculatePay、 isPayday和 deliverPay等,则藉由 Employee接口多态地接受派遣。对于 switch语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍.当然也要就事论事,有时我也会部分或全部违反这条规矩。
在这里插入图片描述

  1. 使用描述性的名称

函数越短小、功能越集中,就越便于取个好名字。函数的名字要能够很好的描述该函数所做的事儿。不要害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。

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

  1. 函数参数

最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由オ能用三个以上参数(多参数函数)一一所以无论如何也不要这么做。

尽量不要有输出参数,而是将输出设置为返回值。 如果参数较多的时候可以考虑使用类进行封装。

函数将布尔值作为参数违反了函数只做一件事的规则。在这种情况应该将函数一分为二,分别对应两种情况。

  1. 无副作用

副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。

应当避免使用输出参数,如果函数必须要修改某种状态,就修改所属对象的状态吧。

  1. 分隔指令与询问

函数要么是做什么事儿,要么回答什么事儿,但二者不可兼得。函数应该修改某对象的状态或是返回该队形的有关信息。

  1. 使用异常代替返回错误码

当函数返回错误码,就是在要求调用者立刻处理错误。会导致更深层次的嵌套结构。如果使用异常代理返回错误码,错误处理代码就能够从主路径代码中分离出来,从而简化了代码。

Try/Catch代码块把错误处理与正常流程混在一块,最好是吧try和catch代码块的主体部分抽离出来形成函数。使得错误处理与业务流程分隔开来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WSkqC0Qo-1618646322742)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2d8e8652-9bfd-4e37-81f3-0c7b4348f849/Untitled.png)]

函数应该只做一件事。错误处理就是一件事,所以错误处理函数不应该做其他事。

  1. 不要重复

如果你发现某两个函数用到了相同甚至相近的代码块应该迅速思考是不是可以将其抽取成单独的函数。 重复就是万恶之源。

  1. 结构化编程

Dijkstra认为,每个函数、每个代码块都应该只有一个入口一个出口。这意味着每个函数只能有一个return语句,循环中不能有break或continue,而且永远不能出现goto。 事实上,当代码相对短小的时候,适当多几个return、break、continue无伤大雅。当代码冗长时,这样的规则才能够发挥出其效力来。

12.总结

写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。

四、注释

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

带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多

  1. 好注释

**法律信息:**公司代码规范要求编写与法律有关的注释,例如版权和著作申明。

提供信息的注释: 不过作者认为 将函数名 重新命名为 responderBeingTested 注释就是多余的。

// returen an instance of the Responder being tested
protected abstract Responder responderInstance();

对意图的理解:有时注释不仅提供了有关实现的有用信息,而且还提供了某个决定后面的意图。

**阐释:**把某些晦涩难明的参数或返回值翻译为某种可读形式。特别是参数或者返回值是某个标准库的一部分,或者你不能修改代码,那帮助阐释其含义的代码就会有用。

警告: 对后续维护代码的程序员提出警告。

**TODO注释:**标识一些目前还没做但是下阶段需要做的一些工作。

**放大:**注释可以用来发达某种看来不合理之物的重要性。

  1. 坏注释

不要直接将代码注释掉。

右括号后面的类似//end while等的注释在长函数汇中可能有意义,但是对短小、封装的函数没有什么通,只是增加了混乱程度。如果发现自己想要标记右括号,其实应该做的是缩短函数。

尽量不要出现归属与署名的注释。

写的注释没有出现所标识代码最近的地方。

五、格式

  1. 垂直格式

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

在封包声明、导入声明和每个函数之间,都有空白行隔开。

变量声明应该尽可能的靠近其使用的位置。本地变量应该在函数的顶部出现。实体变量应该在类的顶部声明。循环中的控制比纳灵应该总在循环语句中声明。

相关函数:如果某个函数调用了另外一个,就应该把他们两个放在一起,而且调用这尽可能放在被调用者的上面。

概念相关:概念相关的代码应该放到一起。相关行越强,彼此的距离就该越短。

  1. 横向格式

一行代码的长度,作者建议是上限是120个字符。越短越好。平时工作时主要根据显示器的大小来具体确定。

赋值语句两端留空格

不在函数名和左括号间加空格。因为函数与其参数密切相关。

函数括号的参数之间要用空格隔开

乘法因子之间不加空格,因为它们具有较高的优先级。加减法运算项之间用空格隔开,因为加减法优先级较低。

public static double root1(double a, double b, double c)

缩进:在文件顶层的语句,例如大多数的类声明,根本不缩进。类中的方法相对该类缩进一个层级。方法的实现相对方法声明缩进一个层级。代码块的实现相对于其容器代码块缩进一个层级,以此类推。

六、对象和数据结构

要想隐藏具体实现并不是简单地使用取值器和赋值器将其变量推向外间,而是应该使用暴露接口的方式,使得用户无需了解数据的实现就能操作数据本体。

过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新的函数。面向对象代码便于在不改动既有函数的前提下添加新类。反过来讲也说的通,过程式代码难以添加新数据结构,因为必须修改所有函数,面向对象代码难以添加新函数,因为必须修改所有类。

得墨忒耳律(The Law of Demeter) 认为,模块不应了解它所操作对象的内部情形。得墨忒耳律认为,类 C 的方法 f 只应该调用以下对象的方法

  • C
  • 由 f 创建的对象;
  • 作为参数传递给 f 的对象;
  • 由 C 的实体变量持有的对象。

方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话。下面的代码就是违法了得墨忒耳律,这种连串的方法调用通常呗被认为是肮脏的风格,应该极力避免。可以进行单个的切分。切分之后的代码是否违反德墨忒尔律,在于切分之后的不同对象是对象还是数据结构,如果是对象则违反。

Final String outputdir =ctxt.Getoptions().Getscratchdir ().Getabsolutepath();

只有公共变量、没有函数的类。这种数据结构被称为数据传输对象(DTO). DTO在数据库通信或者解析套接字传递的消息等场景中应用较多。

七、错误处理

使用异常而非返回码

先写Try-Catch-Finally语句。异常的妙处之一是,它们在程序中定义了一个范围。执行try-catch-finally语句中try部分的代码时,你是在表明可随时取消执行,并在catch语句中接续。

给出异常发生的环境说明。应创建信息充分的错误消息,并和异常一起传递出去。

依调用者需要定义异常类。对错误分类有很多方式。可以依其来源分类:是来自组件还是其他地方?或依其类型分类:是设备错误、网络错误还是编程错误?不过,当我们在应用程序中定义异常类时,最重要的考虑应该是它们如何被捕获。

别返回null值,别传递null值。程序中不断的看到检测null值的代码,一处漏掉检测就可能会失控。作者认为这种代码很糟糕,建议抛出异常或者返回特定对象(默认值)。

八、边界

我们在使用第三方代码时,都应该干净利落地整合进自己的代码中,如果强依赖他们的代码,当他们升级之后没有考虑兼容性,比如方法移除、功能变更等等,那么程序可能连编译都不通过。所以下面介绍保持软件边界整洁的实践手段和技巧:

  • 学习性测试(通过编写测试来遍览和理解第三方代码)。在学习性测试中,我们如在应用中那样调用第三方代码。我们基本上是在通过核对试验来检测自己对那个API的理解程度。测试聚焦于我们想从API得到的东西。
  • 学习性测试的好处不只是免费。当第三方程序包发布了新版本,我们可以运行学习性测试,看看程序包的行为有没有改变。
  • 使用尚不存在的代码。有时候我们的第三方API还没设计出来。为了不受阻碍,编写我们想得到的接口,使它在我们的控制之下。有助于保持客户代码更可读,且集中于它该完成的工作。
  • 适配器模式管理边界。通过接口管理第三方边界,使用适配器模式将第三方提供的接口转换为我们的接口。

九、单元测试

TDD三定律:

  1. 在编写不能通过的单元测试前,不可编写生产代码。(先把单元测试写好了,再去写生产代码。)
  2. 只可编写刚好无法通过的单元测试,不能编译也算不通过。(单元测试至少要能编译通过。)
  3. 只可编写刚好足以通过当前失败测试的生产代码。(单元测试写好之后,生产代码要使失败的测试用例刚好能通过。)

保持测试整洁。测试代码和生产代码一样重要。它需要被思考、被设计和被照料。它该像生产代码一样保持整洁。有了测试,你就不必担心对代码的修改!没有测试,每次修改都可能带来新的缺陷。

整洁的测试三要素:可读性,可读性和可读性。使用构造-操作-检验模式提高可读性。每个测试都清晰地拆分为三个环节。第一个环节构造测试数据,第二个环节操作测试数据,第三个环节检验操作是否得到期望的结果。

每个测试一个断言。每个测试函数都应该有且只有一个断言语句。这条规则看似过于苛求,但其好处是可快速方便地理解测试用例。

整洁的测试还应遵循以下5条规则——F.I.R.S.T: 快速(Fast)。测试应该能快速运行。测试运行缓慢,你就不会想要频繁地运行它。 独立(Independent)。测试应该相互独立,某个测试不应为下一个测试设定条件。 可重复(Repeatable)。测试应当可在任何环境中重复通过。 自足验证(Self-Validating)。测试应该有布尔值输出,无论通过或失败,你不应该查看日志文件来确认测试是否通过。 及时(Timely)。测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。

十、类

类应该从一组变量列表开始。应该先出现公共静态常量。然后是私有静态变量,以及私有实体变量。很少会有公共变量。公共函数应跟在变量列表之后。

类应该短小。如何确保短小,作者提供了两个思路。单一权责原则,类或模块应有且只有一条加以修改的理由;保持高度内聚,如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。

为了修改而组织。对于多数系统,修改将一直持续。每处修改都使得系统其他部分不能如期望般工作的风险,我们应该对类加以组织,以降低修改的风险。组织的过程可以参考两个原则:开闭原则,类应当对扩展开放,对修改关闭;依赖倒置原则,类应当依赖于抽象而不是依赖于具体细节。主要是借助接口和抽象类来隔离需求改变所引入的风险。

十一、系统

作者用建造一个城市来比喻构造和使用是不一样的过程

三种方法可以构造和使用隔离开来。第一种分解main函数,main函数创建系统所需的对象,再传递给应用程序,应用程序只管使用;第二种抽象工厂模式,让应用程序自行控制何时创建对象,但构造的细节却隔离于应用程序之外;第三种依赖注入,对象不负责实体化对自身的依赖,反之,它将这份权责移交给其他“有权力”的机制,可参考Spring框架。

扩容。我们应该只实现今天的用户的需求。然后重构,明天再扩容系统,实现新用户的需求。这就是迭代和增量敏捷的精髓所在。

面向切面编程。在AOP中,被称为切面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。

十二、迭进

简单设计的四条规则:

  • 运行所有测试
  • 不可重复
  • 表达了程序员的意图
  • 尽可能减少类和方法的数量
  1. 运行所有测试

遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近OO低耦合度、高内聚度的目标。编写测试引致更好的设计。

  1. 简单设计规则2~4:重构。通过测试消除了对清理代码就会破坏代码的恐惧。在重构过程中,可以应用有关优秀软件设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。

  2. 不可重复

重复是拥有良好设计系统的大敌。它代表着额外的工作、额外的风险和额外且不必要的复杂度。模板方法模式可以消除明显的重复。

  1. 表达力

可以通过选用好名称来表达,也可以通过保持函数和类尺寸短小来表达。短小的类和函数通常易于命名,易于编写,易于理解。还可以通过采用标准命名法来表达。例如,设计模式很大程度上就关乎沟通和表达。通过在实现这些模式的类的名称中采用标准模式名,就能充分地向其他开发者描述你的设计。

编写良好的单元测试也具有表达性。测试的主要目的之一就是通过实例起到文档的作用。读到测试的人应该能很快理解某个类是做什么的。

5.尽可能少的类和方法

类和方法的数量太多,有时是由毫无意义的教条主义导致的。目标是在保持函数和类短小的同时,保持整个系统短小精悍。这在关于简单设计的四条规则里面是优先级最低的一条。所以,尽管使类和函数的数量尽量少是很重要的,但更重要的却是测试、消除重复和表达力。

十三、并发编程

并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开。在单线程应用中,目的与时机紧密耦合,很多时候只要查看堆栈追踪即可断定应用程序的状态。

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

并发的误解:

(1) 并发总能改进性能

并发没那么简单。并发有时能改进性能,但只在多个线程或处之间能分享大量等待时间的时候管用。

(2) 编写并发程序无需修改设计

并发算法的设计有可能与单线程系统的设计极不相同。目的与时机的解耦往往对系统结构产生巨大影响。

(3) 在采用 Web 或 EB 容器的时候,理解并发问题并不重要

所以下面是一些有关编写并发软件的中肯说法:

  1. 并发会在性能和编写额外代码上增加一些开销;
  2. 正确的并发是复杂的,即便对于简单的问题也是如此;
  3. 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,未被当做真的缺陷看待;
  4. 并发常常需要对设计策略的根本性修改。

并发防御原则

  • 单一权责原则

单一权责原则(SRP)认为,方法类组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由,所以也该从其他代码中分离出来。并发相关代码有自己的开发、修改和调优生命周期;

  • 推论:限制数据作用域

主要是利用同步代码的方式进行限制。

  • 推论:使用数据复本

避免共享数据的方法之一就是一开始就避免共享数据。在某些情形下,有可能复制对象并以只读方式对待。在另外的情况下,有可能复制对象,从多个线程收集所有复本的结果,并在单个线程中合并这些结果。

  • 推论:线程应尽可能地独立

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

** 警惕同步方法之间的依赖**。同步方法之间的依赖会导致并发代码中的狡猾缺陷。避免使用一个共享对象的多个方法。
保持同步区域微小。尽可能少地设计临界区,尽可能减小同步区域。
很难编写正确的关闭代码。尽早考虑关闭问题,尽早令其工作正常。这会花费比你预期更多的时间。检视既有算法,因为这可能会比想象中难得多。
测试线程代码。下面是一些精炼的建议:

  1. 将伪失败看作可能的线程问题;
  2. 先使非线程代码可工作;
  3. 编写可插拔的线程代码;
  4. 编写可调整的线程代码;
  5. 运行多于处理器数量的线程;
  6. 在不同平台上运行;
  7. 调整代码并强迫错误发生。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值