【苦练基本功】代码整洁之道 pt4(第10章-第12章)

10 类

10.1 类的组织

  • 遵循标准的Java约定,类应该从一组变量列表开始。如果公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量。
  • 公共函数应该跟在变量列表之后,我们喜欢把有某个函数调用的私有工具函数跟在该公共函数后面。这符合自上而下原则,让程序读起来就像一篇报纸文章

封装

我们喜欢保持变量和工具函数的私有性,但并不执着于此。有时,我们也需要用到受保护的变量或工具函数,好让测试可以访问到。对我们来说,测试说了算。若同一个程序包内的某个测试需要调用一个函数或变量,我们就会将该函数或变量置为受保护或在整个程序包内可访问。然而,我们首先会想办法是指保有隐私。放松封装总是下策。

10.2 类应该短小

关于类的第一条规则是类应该短小。第二条规则是还要要更短小。就像函数一样,在设计类时,首要规则就是要更短小。

“多小合适呢?”,对于函数而言,通过计算代码行数衡量其大小。对于类,我们采用不同的衡量方法,即计算其权责(responsibility)。

类的名称应当描述其权责。命名是帮助判断类的长度的第一个手段。如果无法为某个类命以精确的名称,那么这个类大概就是太长了。类名越含糊,该类就越有可能拥有过多权责

例如:如果某类名中包含含义模糊的词,如Processor、Manager或Super,那么这种现象往往说明有不恰当的权责聚集情况存在。

我们应该能够用大概25个单词简要描述一个类,且不用“若”(if)“与”(and)“或”(or)或者“但是”(but)等词汇。

10.2.1 单一权责原则

单一权责原则(SRP) 认为:类或模块应该有且只有一条加以修改的理由。该原则既给出了权责的定义,又是关于类的长度的指导方针。

  • 类只应该有一个权责——只有一条修改的理由。

  • 鉴别权责(修改的理由)常常帮助我们在代码中认识到并创建出更好的抽象。

SRP是面向对象设计中最为重要的概念之一,也是较为容易理解和遵循的概念之一。奇怪的是SRP往往也是最容易被破坏的类设计原则,经常会遇到做太多事的类。

让软件能工作和让软件保持整洁,是两种截然不同的工作。我们中的大多数人脑力有限,只能更多地把精力放在让代码能工作上,而不是放在保持代码有组织和整洁上,这全然正确。分而治之在编程行为中的重要程度,等同于其在程序中的重要程度。
问题是太多人在程序能够做事就以为万事大吉了。我们没能把思维转向有关代码组织和整洁的部分。我们直接转向了下一个问题,而没能回过头将臃肿的类切分为只有单一权责的去耦式单元。

以此同时,许多开发者害怕数量巨大的短小单一目的类会导致难以一目了然抓住全局。他们认为,要搞清楚一件较大工作如何完成,就得在类与类之间找来找去。

然而,有大量短小类的系统并不比有少量庞大类的系统拥有更多移动部件,其数量大致相等。问题是:你想把工具归置到有许多抽屉、每个抽屉中装有定义和标记良好的组件的工具箱中?还是想要少数几个能够随便把所有东西扔进去的抽屉?

每个达到一定规模的系统都会包括大量逻辑和复杂性。管理这种复杂性的首要目标就是加以组织,以便开发者知道到哪儿能找到东西,并且在某个特定时间只需要理解直接相关的复杂性。反之,拥有巨大、多目的类的系统,总是让我们在目前并不需要了解的一大堆东西中艰难跋涉。

系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个权责,只有一个修改原因,并于少数与其他类一起协同达成期望的系统行为。

10.2.2 内聚

类应该只有少数实体变量。

类中的每个方法都应该操作一个或多个这种变量。方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用的,则该类具有最大的内聚性。

一方面,创造这种极大化内聚类是即不可取也不可能的;
另一方面,我们希望内聚性保持在较高位置。

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

保持函数和参数列表短小的策略,有时会导致为一组子集方法所用的实体变量数量增加。出现这种情况时,往往意味着至少有一个类要从大类中挣扎出来。应该尝试将这些变量和方法拆分到两个或多个类中,让新的类更为内聚。

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

在将较大函数切割为小函数时,将导致更多的类出现。但如果将一个有许多变量的大函数拆解为单独的函数,只需要将其中泳道的遍历提升为类的实体变量,完全无须传递任何变量即可完成拆解。很容易将函数拆分为小块。

但这样意味着类丧失了内聚性,因为堆积了越来越多的只为允许少量函数而存在的实体变量。因此可以考虑让这些需要函数共享的的变量拥有他们自己的类。当类丧失了内聚性,就拆分它!

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

10.3 为了修改而组织

  • 对于多数系统,修改将会一直持续。 每处修改都让我们冒着系统其他部分不能如期望般工作的风险。在整洁的系统中,我们对类加以组织,以降低修改的风险。

  • 当出现只与类一小部分有关的私有方法行为,就意味着存在改进空间。然而展开行动的基本动因却应该是系统的变动。

  • 开放闭合原则(OCP):类应当对扩展开放,对修改封闭。

  • 我们希望精心组织系统,从而在添加或修改特性时尽可能少惹麻烦。在理想系统中,我们通过扩展系统而非修改现有代码来添加新特性。

10.3.1 隔离修改

需求会改变,所以代码也会改变。

具体类包含实现细节(代码),而抽象类则只呈现概念。依赖具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。

同时对具体细节的依赖给对系统的测试带来了挑战。

对系统解耦,系统会变得更加灵活也更加可复用。部件之间的解耦代表着系统中的元素互相隔离得很好。隔离也让对系统每个元素的理解变得更加容易。

通过降低连接度,我们的类就遵循了另一条类设计原则——依赖倒置原则(DIP)。本质而言,DIP认为类应该依赖抽象而不是依赖具体细节

11 系统

复杂要人命,它消磨开发者的生命,让产品难以规划、构建和测试。

11.1 如何建造一个城市

即便是管理一个即存的城市,也是靠单人能力无法做到的。不过城市还是在运转。这是因为每个城市都有各种组织管理不同的部分,如供水系统、供电系统、交通、执法、立法,诸如此类。有些人负责全局,有些人负责细节。

城市能运转,还因为它演化出恰当的抽象等级和模块,好让个人和其所管理的“组件”即便在不了解全局时也能有效地运转。

尽管软件团队也是这样组织起来的,但所致力的工作往往没有同样的关注面切分及抽象层级,而整洁的代码可以帮助我们在较低的抽象层级上达成这一目标。

11.2 将系统的构造与使用分开

构造与使用是非常不一样的过程。

软件系统应将起始过程和起始过程之后的运行时逻辑分离开,在起始过程中构建应用对象,也会存在互相纠缠的依赖关系。

  • 每个应用程序都应该留意起始过程。将关注的方面分离开,是软件技能中古老也最重要的设计技能。
  • 延迟初始化/赋值。可以在真正用到对象前,无须操心这种架空构造,起始时间也会更短,而且还能保证永远不会返回null值。
  • 测试时会有问题,如果对象是一个中心重型对象,则我们必须确保在单元测试调用该方法之前,就给服务指派恰当的测试替身(TEST DOUBLE)或MOCK对象(MOCK OBJECT)。由于构造逻辑与运行过程相混杂,我们必须测试所有的执行路径。有了这些权责,说明方法不止做了一件事,就略微违反了单一权责原则。
  • 仅出现一次的延迟初始化不算是严重问题,全局设置策略在应用程序中四散分布,缺乏模块组织性,通常也会出现许多重复代码。

如果我们勤于打造有着良好格式并且强固的系统,就不该让这类就手小技巧破坏模块组织性,对象构造的初始化和设置过程也不例外,应当将这个过程从正常的运行时逻辑中分离出来。确保拥有解决主要依赖问题的全局性一贯策略。

11.2.1 分解main

将构造与使用分开的方法之一是将全部构造过程搬迁到main或被称之为main的模块中,设计系统的其余部分时,假设所有对象都已正确构造和设置。
在这里插入图片描述
控制流程。main函数创建系统所需的对象,再传递给应用程序,应用程序只管使用。如上图,看横贯main与应用程序之间隔离的依赖箭头的方向,他们都从main函数向外走,这表示应用程序对main或者构造过程一无所知,他只是简单地指望一切已齐备。

11.2.2 工厂

有时应用程序也要负责确定合适创建对象。

例如某个订单处理系统中,应用程序必须创建LineItem实体,添加到Order对象。在这种情况下,我们可以使用抽象工厂模式让应用自行控制何时创建LineItem,但构造的细节却隔离于应用程序代码之外。

在这里插入图片描述
如图所示,所有依赖都是从main指向OrderProcessing应用程序的,这表示应用程序与如何构建LineItem的细节是分离开来的。其中构建能力由LineItemFactoryImplementation持有,而LineItemFactoryImplementation又是在main这一边的,但应用程序能完全控制LineItem实体何时构建,甚至能传递应用程序特定的构造器参数。

11.2.3 依赖注入

依赖注入(Dependency Injection DI)能够实现分离构造与使用。
控制反转(Inversion of Control IoC)则是在依赖管理中的一种应用手段。
控制反转将第二权责从对象中拿出来,转移到另一个专注于此的对象中,从而遵循单一权责原则。

在依赖管理的情景中,对象不应负责实体化对自身的依赖,反之它应该将这份权责移交给其他“有权力”的机制,从而实现控制的反转。因为初始设置是一种全局问题,所以通常这种授权机制要么是main例程,要么是有特定的目的容器。

调用对象并不控制真正返回对象的类别(前提是它实现了恰当的接口),但调用对象仍然主动解决了依赖问题。

11.3 扩容

城市由城镇而来,城镇由乡村而来。

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

与物理系统相比,软件系统比较独特。软件系统的架构可以递增式地增长,只要我们持续将关注面恰当地切分。 软件系统短生命周期的本质使得这一切变得可行。

11.3.1 横贯式关注面

  • 持久化之类关注面倾向于横贯某个领域的天然对象边界。
  • 横贯式关注面——可以从模块、封装的角度推理持久化策略。但在实践上,不得不将实现了持久化策略的代码铺展到许多对象中。
  • 面向切面编程(Aspect-Oriented Programming AOP)是一种恢复横贯式关注面模块化的普适手段

在AOP中,被称为切面(aspect)的模块构造说明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明或编程机制来实现的。

可以声明哪些对象和属性(或模块)应当被持久化,然后将持久化任务委托给持久化框架。行为的修改由AOP框架以无损方式(无需手工修改源代码)在目标代码中进行。

在Java中的3种切面或类似切面的机制:

  • 1 Java代理
  • 2 纯Java AOP框架
  • 3 AspectJ的方面

11.4 Java代理

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

使用字节操作类库是具有挑战性的,代码量和复杂度是代理的两大弱点,创建整洁代码变得很难!另外,代理也没有提供在系统范围内指定执行点的机制,而那正是真正的AOP解决方案所必需的。

AOP有时会与实现他的技术相混淆,例如方法拦截和通过代理做的“封包”。AOP系统的真正价值在于用简洁的和模块化的方式指定系统行为。

11.5 纯Java AOP框架

编程工具能自动处理大多数代理模板代码。在数个Java框架中,代理都是内嵌的,如Spring AOP和JBoss AOP等,从而能够以纯Java代码实现面向切面编程。

旧式Java对象(Plain-Old Java Object POJO)自扫门前雪,并不依赖于企业框架,因此,它在概念上更为简单、更易于测试驱动,也较易于保证正确地实现相应的用户故事,并为未来的用户故事维护和改进代码。

通过使用描述性配置文件或API,你可以把需要的应用程序构架组合起来,包括持久化、事务、安全、缓存、恢复等横贯性问题。许多时候,只需要指定Spring或JBoss类库,框架以对用户透明的方式处理使用Java代理或字节代码库的机制。这些声明驱动了依赖注入容器,DI容器再实体化主要对象,并按需将对象连接起来。

11.6 AspectJ 的方面

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

在80%-90% 泳道切面特性的情况下,Spring AOP和JBoss AOP提供的纯Java实现手段足够使用。然而,AspectJ却提供了一套用以切分关注面的丰富而强有力的工具。AspectJ的弱势在于,需要采用几种新工具,学习新语言构造和使用方法。

11.7 测试驱动系统架构

通过切面式的手段切分关注面的威力不可低估。假使你能用POJO编写应用程序的领域逻辑,在代码层面与架构关注面分离开,就有可能真正地用测试来驱动架构。

采用一些新技术,就能将架构按需从简单演化到精细,没必要先做大设计(Big Design Up Front BDUF)。实际上BDUF甚至是有害的,它阻碍改进,因为心里上会抵制丢弃即成之事,也因为架构上的方案选择影响后续的设计思路。

最佳的系统架构由模块化的关注面领域组成,每个关注面均用纯Java(或其他语言)对象实现。不同的领域之间用最不具有侵害性的方面或类方面工具整合起来,这种架构能测试驱动,就像代码一样。

11.8 优化决策

模块化和关注面切分成就了分散化管理和决策。在巨大的系统中,不管是一座城市还是一个软件项目,无人能做所有决策。

延迟决策至最后一刻 也是一种好的手段,这不是懒惰或不负责,而是让我们能够给予最有可能的信息做出选择。提前决策是一种预备知识不足的决策。如果决策太早,就会缺少太多客户的反馈、关于项目的思考和实施经验。

拥有模块化关注面的POJO系统提供的敏捷能力,允许我们基于最新的知识做出优化的、时机刚好的决策。决策的复杂度也会随之降低。

11.9 明智使用添加了可论证价值的标准

建筑构造大有可观,既因为新建筑的构建过程(即便是在隆冬季节),也因为那些现今科技所能实现的超凡设计。

有了标准,就更易复用想法和组件、雇用拥有相关经验的人才、封装好点子,以及将组件连接起来。不过,创立标准的过程有时却漫长到行业等不及的程度,有些标准没能与他要服务的采用者的真实需求相结合。

11.10 系统需要领域特定语言

建筑,与大多数其他领域一样,发展出一套丰富的语言,有词汇、熟语和清晰而简洁地表达基础信息的句式。在软件领域,领域特定语言(Domain-Specific Language DSL)重受关注。

DSL是一种单独的小型脚本语言或以标准语言写就的API,领域专家可以用它编写读起来像是组织严谨的散文一般的代码。

DSL在有效使用时能提升代码惯用法和设计模式之上的抽象层次,它允许开发者在恰当的抽象层级上直指代码的初衷。

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

11.11 小结

  • 系统也应该是整洁的。

  • 侵害性架构会湮灭领域逻辑,冲击敏捷能力。 如果领域逻辑受到困扰,质量就会堪忧,因为缺陷会更易隐藏,用户故事更难实现。当敏杰能力受到损害时,生产力也会降低,TDD的好处遗失殆尽。

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

  • 只有在编写POJO并使用类方面的机制来无损地组合其他关注面时,这种事情才会发生。

  • 无论是设计系统还是单独的模块,别忘了使用大概可开展工作的最简单方案

12 迭进

12.1 通过迭进设计达到整洁

Kent Beck关于简单设计的4条规则:

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

遵循以上规则,设计就能变得“简单”。

12.2 简单设计规则1:运行所有测试

  • 设计必须制造出如预期一般工作的系统。
  • 设计也许有一套绝佳设计,但如果缺乏验证系统是否真按预期那样工作的简单方法,那就无异于纸上谈兵。
  • 全面测试并持续通过所有测试的系统,就是可测试的系统。
  • 只要系统可测试,就会导向保持类短小且单一的设计方案。
  • 遵循SRP的类,测试起来会比较简单
    测试编写的越多,就越能持续走向编写较易测试的代码。因此,确保系统完全可测试能帮助我们创造更好的设计。
  • 紧耦合的代码难以编写测试。
    同样,编写测试越多,就越会遵循DIP之类的规则,从而越会使用依赖注入、接口和抽象等工具尽可能减少耦合,如此一来,设计就会有长足进步。
  • 遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近面向对象低耦合度、高内聚度的目标,编写测试将会引致更好的设计。

12.3 简单设计规则2~4:重构

测试消除了对清理代码就会破坏代码的恐惧。
有了测试,就能保持代码和类的整洁,方法就是递增式地重构代码。添加几行代码后就要暂停,琢磨一下变化了的设计,保证设计没有退步且没有破坏任何东西。

重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称等等。即简单设计规则中的后3条规则:消除重复、保证表达力、尽可能减少类和方法的数量。

12.4 不可重复

重复是拥有良好设计的系统的大敌。它代表着额外的工作、额外的风险和额外的不必要的复杂度。

重复有很多种表现。
极其雷同的代码行是重复。类似的代码往往可以调整得更相似,这样就能更容易地进行重构。重复也包括实现上的重复。

tips:可以把一个新方法分拣到另外的类中从而提升其可见性,团队中其他成员也许会发现进一步抽象新方法的机会,并且在其他场景中复用之。“小规模复用”可大量降低系统复杂性。想要实现大规模复用,必须理解如何实现小规模复用。

模板方法模式(Template Method)是一种移除高层级重复的通用技巧。

12.5 表达力

软件项目的主要成本在于长期的维护。
为了在修改时尽量降低出现缺陷的可能性,很有必要理解系统是做什么的。

当系统变得越来越复杂,开发中就需要越来越多的时间来理解它,而且极有可能误解。所以代码应当清晰地表达其作者的意图。

  • 我们可以通过选用好名称来表达。
  • 也可以通过保持函数和类尺寸短小来表达。短小的类和函数通常易于命名,易于编写,易于理解。
  • 还可以通过采用标准命名法来表达。例如设计模式很大程度上关乎沟通和表达。通过实现这些模式的类的名称中采用标准模式名,例如COMMAND或VISITOR,就能充分表述你的设计。
  • 编写良好的单元测试也具有表达性。
  • 不过,要做到有表达力的最重要的方式却是尝试。很多时候,我们一旦写出能工作的代码,就转移到下一个问题上,而没有花功夫调整代码,让后者更易于阅读。记住,下一位读代码的人最有可能是你自己。

12.6 尽可能少的类和方法

即便是消除重复、代码表达力和和SRP等最基础的概念,也会被使用过度。为了保持类和函数短小,我们可能会造出太多的细小类和方法。

类和方法的数量太多,有时是由毫无意义的教条主义导致的。

我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍。不过要记住,这条是4条规则中优先级最低的一条,尽管使类和方法的数量尽可能少是很重要的,但更重要的却是测试、消除重复和表达力。

12.7 小结

不会有能替代经验的简单实践手段。遵循简单设计的实践手段,开发者不必经年学习就能掌握好的原则和模式。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值