标签归档:代码

要写易删除,而不易扩展的代码

英文来源:Write code that is easy to delete, not easy to extend
作者:tef,拥有着犀利的演讲风格,简介请详见他在http://programmingisterrible.com/about上的自我介绍。

译者简介:张咏枫,硅谷创业公司 BloomSky Inc. 软件工程师,加州大学圣克鲁兹分校计算机科学硕士,方向为机器学习。硕士期间曾在 Palo Alto Networks 实习,从事平台开发。在目前的公司做全栈开发,包括后端 API 开发、分布式计算平台的架构、与其它应用平台的整合、数据库架构,以及前端网页应用的开发。涉及到的技术包括:Django、Celery、Nginx、Node.js、React.js、Rethinkdb 等。个人博客:http://blog.yongfengzhang.com/cn/

感谢译者张咏枫对本篇文章的翻译以及授权。

译者序


本文托管在 GitHub 上: https://github.com/freedombird9/code-easy-to-delete,欢迎 Star 或纠错。


好的文章总是见解独到,功底深厚而逻辑清晰。这是一篇关于如何设计、架构代码的文章。文章的观点新颖而有力。作者的观点是,我们所做的一切 —— 重构、模块化、分层,等等,都是为了让我们的代码易于被删改,都是为了让遗留代码不成为我们的负担,而不是为了代码复用。

作者认为,经过七个不同的开发阶段,最终便可以提炼出这样的代码。每个阶段都有详细的介绍和例子。

初读文章,可能会有抽象、晦涩之感。但多读几遍之后,其主旨就会变的清晰。

一个晚上的彻夜不眠,有了这篇中文翻译,与大家分享,希望对读者有所助益。

本文托管在 GitHub 上,水平有限,还望大家多多指点。

感谢

谢谢秋兄将这篇文章分享给我。

中文翻译如下

编程是一件很糟糕的事 —— 在荒废了自己的一生之后所学到的东西

要写容易删除,而不容易扩展的代码。


没有一行代码产生于理性、有很强的可维护性,且不会被偶然地删除掉 Jean-Paul Sartre’s Programming in ANSI C.


每写一行代码,都会有一个代价:维护。为了不在代码上花费太多,我们有了可复用的软件。但是代码复用有一个问题:当你以后想要修改的时候它就会成为一个障碍。

一个 API 的用户越多,为了引入修改而需要重写的代码就越多。相似的,你依赖第三方 API 越多,当其有任何改变时你的麻烦就越多。管理代码之间的兼容性,或者模块之间的依赖关系在大型系统中是一个很重要的问题。而且随着项目越来越久,这个问题就会变得越复杂。


今天我的观点是,如果我们要去计算一个程序有多少行代码,我们不应该将其看成是「产生了多少行」,而应该看成「耗费了多少行。」 EWD 1036


如果我们将「有多少行代码」看成是「耗费了多少行代码」的话,那么当我们删除这些代码的时候,我们就降低了维护成本。我们应该努力开发可丢弃的(disposable)软件,而不是可复用的软件。

我不需要告诉你删除代码比写代码更有趣吧。

为了写易于删除的代码:重复你自己以避免产生模块依赖性,但是不要重复管理这些代码。同时将你的代码分层:在易于实现但不易于使用的模块的基础上构建易于使用的 API。拆分你的代码:将很难于实现且很可能会改变的模块互相隔离,并同时和其他的模块隔离。不要将每一个选项都写死,容许在运行时做改变。不要试图同时去做上述所有的事情,或许你在一开始就不要写这么多代码。

阶段0:不写代码

代码有多少行本身并不能告诉我们什么,但是代码行数的数量级可以:50,500,5000,10000,25000等等。一个一百万行的庞然大物显然会比一个一万行的程序更折磨人。替代它也会显著花费更多的时间、金钱和努力。

虽然代码越多,摒弃起来就越困难,但是少写一行代码本身并不能省掉任何事情。

即使如此,最容易删除的代码是你一开始就避免写出来的代码。

阶段1:复制粘贴代码

写可复用的代码是一件在事后有了代码库中的使用示例后更容易做的事情,而不是在事前就能预料好的。往好的看,仅仅是利用文件系统你或许就已经在复用很多代码了,所以何必这么担心呢?一点点冗余是健康的。

复制粘贴代码若干次,而不是仅仅为了给这个用法取一个名字就去写一个库函数,是完全没有问题的。一旦把一个东西变成共享的 API,改变起来就会更困难。

调用你的函数的那段代码会依赖于其实现背后有意或无意的行为。使用你的函数的程序员不会根据你的文档去调用,而会根据他们观察到的函数行为去调用。

删除函数内的代码比删除一个函数更简单。

阶段2:不要复制粘贴代码

当你已经复制粘贴足够多次数时,或许就是该提炼出一个函数的时候了。这是「把我从标准库中拯救出来」的东西:「打开一个配置文件并返回一个哈希表」,「删除这个文件夹」。这些例子包括了无状态函数,或者有一些全局信息,如环境变量的函数。这些是最终会出现在一个叫做 “util” 文件中的东西。

旁白:建一个 util 文件夹,把不同的功用放在不同的文件里。单个 util 文件总是会不断变大直到大得来无法拆分。使用单个 util 文件是不简洁的做法。

对于应用或者项目而言通用性越强的代码,就越容易复用,被改变或者删除的可能性就越低。它们包括日志记录,第三方 API,文件柄(handle)或者进程相关的库。其他你不会删除掉的代码有列表、哈希表,以及其他集合。这不是因为它们的接口通常都很简单,而是因为它们的作用域不会随着时间的增长而变大。

我们要努力将代码中难以删除的部分与易于删除的部分分隔得尽可能开,而不是使所有代码都变得易于删除。

阶段3:写更多的模版

虽然我们通过库来避免复制粘贴,但是我们常常会需要复制粘贴来使用这些库,最后导致写了更多的代码。不过我们给这些代码另外一个名字:模版(boilerplate)。模版和复制粘贴在很大程度上很像,除了每次使用模版的时候都会在不同的地方做一些改变,而不是一次次重复完全一样的东西。

就像复制粘贴一样,我们会重复部分代码以避免引入依赖性,以获得灵活度,代价则是冗余。

需要模版的库通常有网络协议、有线格式(wire formats)、解析套件,或者很难将策略(一个程序应该做的)和协议(一个程序能做的)交织起来而又不限制可选项的东西。这种代码是很难被删除的:与其他的电脑通信或者处理不同的文件通常是一种必需,而我们永远不想让业务逻辑充斥其中。

写模版不是在练习代码复用:我们尽可能将变化频繁的部分和相对更稳定的部分分隔开。应最小化库的依赖性或责任,即使我们必须通过模版来使用它们。

你会写更多的代码,但是这些多出来的代码都是在易于删除的部分。

阶段4:不要写模版

当库需要迎合所有要求的时候,模版的作用最为明显。但是有时候重复的东西太多了。是时候将一个弹性很大的库用一个考虑到了策略、流程和状态的库打包起来了。开发易用的 API 就是将模版转换成一个库。

这比你想象中的要普遍:最为流行和倍受喜爱的 Python http 客户端模块 requests 就是一个很成功的例子,它将一个使用起来更为繁琐的库 urllib3 打包,为用户提供了一套更加简单的接口。当使用 http 的时候, requests 照顾到普遍的工作流,而对用户隐藏了许多实际的细节。相比而言, urllib3 处理流水线和连接管理,不对用户隐藏任何细节。

当把一个库包进另一个库的时候,与其说是为了隐藏细节,倒不如说是为了将不同的关切分开: requests 是关于http的冒险,urllib3 则是给你工具让你自己选择你自己的冒险。

我并不是主张让你去建一个 /protocol/ 和 /policy/ 文件夹,但是你确实应该尝试使 util 不受业务逻辑的干扰,并且在易于实现的库的基础上开发易于使用的库。你并不需要将一个库全部写完之后再在上面写另一个库。

将一个第三方库打包起来通常也是很好的实践,即使它们不是协议类的库。你可以写一个适合你的代码的库,而不是在整个项目中都锁定一个选择。开发一个好用的 API 和开发一个具有扩展性的 API 通常是互相冲突的。

像这样将不同的关切分开,能让我们在使一些用户很高兴的同时不会让其他用户想做的事情变得不可能。当你从一开始就有一个好的 API 的时候,分层是最简单的。但是在一个写得不好的 API 上开发出一个好的 API 则会很困难。好的 API 在设计之时就会站在使用者的位置上考虑问题,而分层则是我们意识到我们不能同时让所有人都高兴。

分层更多的是为了使那些很难删除的代码易于使用(在不让业务逻辑污染它们的情况下),而不仅仅是关于写以后可以删除的代码。

阶段5:写一大段代码

你已经复制粘贴了,你已经重构了,你已经分层了,你已经构建了,但是代码在最后还是需要做一些事情的。有时候最好的做法是放弃,然后写一大段垃圾代码将剩余部分弄在一起。

业务逻辑是那种有着无尽的边界情况和快速而肮脏的hack的代码。这是没问题的,我对此并不反对。其他的风格,如「游戏代码」,或者「创始人代码」,也是同一个东西:采用捷径来节省大量的时间。

原因?有时候删掉一个大的错误比删掉18个小的交错在一起的错误更为容易。大量的编程都是探索性的,犯几次错误然后去迭代比想着一开始就做对更快速。

这个对于更有趣味或者更有创造性的尝试来说更为正确。如果你正在写你的第一个游戏:不要写成一个游戏引擎。类似的,不要在写好一个应用之前就去写一个框架。第一次的时候尽管大胆的去写一堆乱七八糟的代码。你是不会知道怎样拆分成模块的,除非你是先知。

单一库有类似的取舍:你事先不会知道怎样拆分你的代码,而一个大的错误显然比20个紧密关联的错误更容易处理。

当你知道哪些代码将会被舍弃、删除,或者替换的时候,你就可以采用更多的捷径。特别是当你要写一个一次性的客户端网站,或关于一个活动的网页的时候。或者任何一个有模版、要删除复本、要填补框架所留下的缺口的地方。

我不是说你应该重复同一件事情十次来纠正错误。引用 Perlis 的话:「所有东西都应该从上到下建立,除了第一次的时候。」你应该在每一次尝试时都去犯新的错误,接纳新的风险,然后通过迭代慢慢的来完善。

成为一个专业的软件开发者的过程就是不断积累后悔和错误清单的过程。你从成功身上学不到任何东西。并不是你能知道好的代码是什么样的,而是你对坏的代码记忆犹新。

项目不管怎样最终都会失败或者成为遗留代码。失败比成功更频繁。写十个大的泥球,看它们能将你带向哪比尝试去给一个粪球抛光更快速。

一次行删掉所有的代码比一段一段的去删更容易。

阶段6:把你的代码拆分成小块

大段的代码是最容易写的,但同时维护起来也最为昂贵。一个看起来很简单的修改就会以特定的方式影响代码库的几乎每个部分。本来作为一个整体删除起来很简单的东西,现在变得不可能去一段一段地删除了。

就像我们根据相互独立的任务来将我们的代码分层一样,从特定平台的代码到特定领域的代码,我们同样需要找到一种方法来梳理出顶层逻辑。


从一系列很困难的或者很容易变的设计决定开始。然后去设计一个个模块,让每一个模块都能隐藏一个设计上的决定,使其对其他决定不可见。 D. Parnas


我们根据代码之间没有共享的部分来拆分代码,而不是将其拆分成有共同功能的模块。我们把写起来、维护起来,或者删除起来最让人沮丧的部分互相隔离开。

我们构建模块不是为了复用,而是为了易于修改。

不幸的是,有些问题相比其他的问题而言分割起来更加困难和复杂。虽然单一责任原则说「每一个模块都应该只去解决一个难题」,但更重要的是「每一个难题都只应该由一个模块去解决」。

当一个模块做两件事情的时候,通常都是因为改变一部分需要另外一部分的改变。一个写得很糟糕但是有着简单接口的组件,通常比需要互相协调的两个组件更容易使用。


我如今再也不会尝试用「松耦合」这种速记一样的描述来定义那种应该被认可与接受的材料了,或许我永远不可能以清晰易懂的方式来定义它。但是当我看到它的时候我能够认出来,而当前的代码不属于那种。 SCOTUS Justice Stewart


你如果可以在一个系统中删除某一模块而不用因此去重写其他模块的话,这个系统就通常被称为是松耦合的。但是解释松耦合是什么样的比在一开始就建立一个这样的系统要容易多了。

甚至于写死一个变量 一次,或者使用命令行标记一个变量都可以叫松耦合。松耦合能让你在改变想法的同时不需要改写太多的代码。

比如,微软 Windows 的内部 API 和外部 API 就是因为这个目的而存在的。外部 API 与桌面程序的生命周期捆绑在一起,内部 API 则和内核捆绑在一起。隐藏这些 API 在给了微软灵活性的同时又不会挂掉过多的软件。

HTTP 中也有松耦合的例子:在你的 HTTP 服务器前设置一个缓存。将图片移到 CDN 上,仅改变一下到它们的链接。这两者都不会挂掉你的浏览器。

HTTP 的错误码是另外一个关于松耦合的例子:服务器之间常见的问题都有自己独特的错误码。当你收到400的时候,再尝试一次还是会得到同样的结果。如果是500则可能会变。结果是,HTTP客户端可以替代程序员处理许多的错误。

当把一个软件分解成更小的部分时,必须要考虑到如何去处理错误。这件事说比做容易。


我勉强决定去使用LATEX。在有错误存在的情况下去实现可靠的分布式系统。 Armstrong, 2003


Erlang/OTP 在处理错误方面有独到之处:监督树(supervision trees)。大致来说,每一个 Erlang 进程都由一个监督进程发起并监视。当一个进程遇到了问题的时候,它就会退出。当进程退出的时候,其监督进程会将其重启。

(这些监督进程由一个引导进程(bootstrap process)发起,当监督进程遇到错误的时候,引导进程会将其重启)

其思想是,快速的失败然后重启比去处理错误要快。像这样的错误处理看起来跟直觉相反 —— 当错误发生的时候通过放弃处理来获得可靠性。但是重启是解决暂时性错误的灵丹妙药。

错误处理和恢复最好是在代码的外层进行。这被称为端对端(end-to-end)原则。端对端原则说在一个连接的远端处理错误比在中间处理要更容易。即使在中间层进行处理,最终顶层的检查也无法被省去。如果不管怎样都需要在顶层来处理错误,那么为什么还要在里层去处理它们呢?

错误处理是一个系统可以紧密结合在一起的方式之一。除此之外还有许多其他紧耦合(tight coupling)的例子,但是要找一个糟糕的设计出来有一点不公平。除了 IMAP。

IMAP 中的每一个操作都像雪花一样,都有自己独特的选择和处理。错误处理相当痛苦:错误可能因为其他操作产生的结果而半路杀出。

IMAP 使用独特的令牌,而不是 UUID,来识别每一条信息。这些令牌也可能因为一个操作而中途被改变。许多操作都不是原子操作。找到一种可靠的方式将一封email从一个文件夹移动到另一个文件夹花费了25年时间。它还采用了一种特别的 UTF-7 编码,和一种独特的 base64 编码。

以上这些都不是我编的。

相比而言,文件系统和数据库是远程储存中好得多的例子。在文件系统中,操作的种类是固定的,但是却有很多可操作的对象。

虽然 SQL 像是一个比文件系统要广得多的接口,它仍然遵循相同的模式。若干对 set 的操作,许许多多对行的操作。虽然不能总是用一个数据库去替换出另一个数据库,但是找到可以和 SQL 一起使用的东西比找到任何一种自制的查询语言都更容易。

其他松耦合的例子有具备中间件、过滤器(filter)和管道(pipeline)的系统。例如,Twitter Finagle 的服务都是使用共同的 API,这使得泛型的超时处理、重试机制,和身份验证都能被毫不费力的加进客户端和服务器端的代码中。

(我很确定如果我不在这提UNIX管道的话,肯定会有人向我抱怨)

首先我们将我们的代码分层,但现在其中的一些层要共享一个接口:一系列有着不同实现的相同行为和操作。好的松耦合通常就意味着一致的接口。

一个健康的代码库不一定要完美的呈现出模块化。模块化的部分使写代码变得很有趣,就像乐高玩具的趣味来自于它所有的零件都可以被拼在一起一样。一个健康的代码库会有一些赘言和冗余,但它们使得可移植的组件间的距离恰到好处,因此你不会把自己套在里面。

松耦合的代码不一定就是易于删除的代码,但是它们替代和修改起来都会容易得多。

阶段7:持续的写代码

如果在写新代码的时候不需要去考虑旧有的代码,那么测试新的想法就要容易很多。并不是说一定要写小的模块,避免庞大的程序,而是说你的系统在你正常开发的同时还需要能够支持一两个试验。

功能发布控制(feature flag)是能让你在以后改变主意的一种方法。虽然 feature flag 被视作一种测试不同功能的方法,但同时它能让你在不重新部署的情况下就应用修改。

Google Chrome 是一个很好的例子,能说明其带来的好处。他们发现维持固定发布周期最困难的就是要合并一个长期存在的功能分支的时候。

能够在不需要重新编译的情况下激活和关闭新的代码,大的修改就可以在不影响现存代码的情况下被分解为更小的合并。如果新功能在代码库中更早出现的话,当一个长期的功能开发影响到其他部分的时候就会表现得更加明显。

Feature flag 并不是命令行开关,它是一种分离功能发布与合并分支,分离功能发布与代码部署的方式。当软件更新需要花费数小时、数天、甚至数周的时候,能够在运行中改变功能就变得越来越重要了。随便问一个运维人员,你就会知道任何一个可能在半夜把你叫起来的系统都值得在运行时去控制。

你更多的是要有一个反馈回路,而不是不停的迭代。模块更多的是用来隔离不同组件以应对改变的,而不仅是用来做代码复用的。处理代码的更改不仅仅是开发新的功能,同时也是抛弃掉旧的功能。写具有扩展性的代码是寄希望于三个月后你能把所有事情都做对。写可以被删除的代码则是基于相反的假设。

我在上文中谈到的策略 —— 分层、隔离、共同的接口、构造 —— 并不是有关写出优秀的软件的,而是关于怎样开发一个可以随着时间而改变的软件。


因此,管理上的问题不是要不要建一个试验性的系统然后把它抛弃掉。你会这么做的。[……]所以做好抛弃它的打算吧;无论如何你都会的。 Fred Brooks


你不必要将它全部抛弃,但是你需要删除某些部分。好的代码并不是要第一次就做对一件事。好的代码是那些不会造成障碍的遗留代码(legacy code)。

好的代码总是易于删除的代码。

什么是整洁的代码(Clean Code)?

http://www.iteye.com/news/26838

什么样的代码才是真正好的、整洁的代码?来看看大牛们怎么说。 

Bjarne Stroustrup,C++之父:

引用
我喜欢优雅、高效的代码:

  • 逻辑应该是清晰的,bug难以隐藏;
  • 依赖最少,易于维护;
  • 错误处理完全根据一个明确的策略;
  • 性能接近最佳化,避免代码混乱和无原则的优化;
  • 整洁的代码只做一件事。

Grady Booch,《面向对象分析与设计》作者:

引用
  • 整洁的代码是简单、直接的;
  • 整洁的代码,读起来像是一篇写得很好的散文;
  • 整洁的代码永远不会掩盖设计者的意图,而是具有少量的抽象和清晰的控制行。

Dave Thomas,OTI公司创始人,Eclipse战略教父:

引用
  • 整洁的代码可以被除了原作者之外的其他开发者阅读和改善;
  • 具备单元测试和验收测试;
  • 有一个有意义的名字;
  • 使用一种方式来做一件事情;
  • 最少的依赖,并明确定义;
  • 提供了一个清晰的、最小的API;
  • 应该根据语言特性,在代码中单独显示必要的信息,而不是所有的信息。

Michael Feathers,《修改代码的艺术》作者:

引用
  • 整洁的代码看起来总是像很在乎代码质量的人写的;
  • 没有明显的需要改善的地方;
  • 代码的作者似乎考虑到了所有的事情。

Ward Cunningham,Wiki和Fit创始人,极限编程联合创始人,Smalltalk和面向对象的思想领袖:

引用
  • 当你读代码时,你发现每个程序都如你期待的那样
  • 你可以称之为漂亮的代码
  • 代码完美展现了该编程语言的设计目的

总之,整洁的代码的特点:

  • 容易与其他人协作(简单、意图明确、良好的抽象、不出意料、合适的名称)
  • 针对现实世界,比如,有一个清晰的错误处理策略
  • 代码作者显然很关心软件和其他开发者(针对双方的可读性和可维护性)
  • 最小化(做一件事,最小的依赖)
  • 以最合适的方式解决问题

如何编写出拥抱变化的代码?

http://www.csdn.net/article/2013-02-25/2814251-coding-change

发表于2013-02-26 09:06| 13321次阅读| 来源net.tutsplus| 70 条评论| 作者Patkos Csaba

摘要:编写高效优质的代码一直是程序员所追求的目标之一,那么什么样的代码才叫优质呢?其中最重要的莫过于易维护、易修改。本文作者从面向对象和SOLID两大方面,非常详细地总结了如何编写出易修改的代码,绝对让你受益匪浅。

在实际的开发中,编写出易维护和易接受变化的代码并非易事,想要实现可能更加困难重重:源码难于理解、依赖关系指向不明、耦合也很令人头疼。难道就真的就没有办法了吗?本文中我们一起探讨几个技术原则和一些编码理念,让你的代码跟着需求走,而且易维护易拓展。

介绍些面向对象方法

面向对象编程(OOP)是一种很受欢迎的编程思想,它保证了代码的组织性和重用性。软件公司采用OOP思想编程已经好多年了,如今仍然在项目开发中使用这一思想。OOP拥有一系列非常好的编程原则,如果使用恰当,它会让你的代码更好、更整洁和更易维护。

1.内聚力

这里的内聚力是指拥有一些共同的特征的东西而逐渐凝聚到一起,而不能在一起的东西则会被移除出去。可以用一个类来说明内聚力:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ANOTCohesiveClass {
   private $firstNumber;
   private $secondNumber;
   private $length;
   private $width;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function setLength($length) {
      $this->length = $length;
   }
   function setHeight($height) {
      $this->width = $height;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
   function area() {
      return $this->length * $this->width;
   }
}

 

该例定义了一个类以及一些表示数字和大小的字段。而这些属性通过他们的名称来判断是否应该在一起。add()和substract()方法来对两个number进行操作,此外还定义了area()来操作length和width这两个字段。

这个类只负责各个独立的群体信息,显然,内聚力很低。重构上面的例子:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ACohesiveClass {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}

重构以后,该类明显变成了高内聚特征的类。为什么?因为这个类里的每个部分都与另外一部分彼此联系。虽然在实际开发中编写出高内聚的类比较困难,但开发人员应该坚持这样做,坚持就是胜利。
2.正交性

就简单而言,正交是指隔离或排除副作用。一个方法、类或者模块改变了其他无关的方法、类或模块就不是正交。例如,飞机的黑匣子就具有正交性,它自身就具备电源、麦克风和传感器等这些功能。而它对外在的其他东西没有任何影响,它只提供一种机制,用来保存和检索飞行数据。

一个典型的非正交系统例子就是汽车电子设备。提高汽车的速度也存在些负面影响,比如会增加无线电音量,然而对汽车来说,速度并不是正交。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calculator {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      $sum $this->firstNumber + $this->secondNumber;
      if ($sum > 100) {
         (new AlertMechanism())->tooBigNumber($sum);
      }
      return $sum;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class AlertMechanism {
   function tooBigNumber($number) {
      echo $number 'is too big!';
   }
}

在这个例子中,Calculator类里的add()方法里列了几个意想不到的行为:它生成AlertMechanism对象并调用其中的一个方法。实际上,该库的使用者并不希望消息被打印到屏幕上,相反,他们则是要计算数字之和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Calculator {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class AlertMechanism {
   function checkLimits($firstNumber$secondNumber) {
      $sum = (new Calculator($firstNumber$secondNumber))->add();
      if ($sum > 100) {
         $this->tooBigNumber($sum);
      }
   }
   function tooBigNumber($number) {
      echo $number 'is too big!';
   }
}

这样明显好多了,AlertMechanish在Calculator中没有任何负面影响,相反,在任何需要弹出警告的地方都可以使用AlertMechanish。

3.依赖和耦合

大多数情况下,这两个单词是可以互换的,但是在某些情况下,又存在优先级关系。

那么,什么是依赖呢?当对象A需要使用对象B时,为了执行其规定的行为,我们说A依赖B。在OOP中,依赖是极其常见的。对象之间经常互相依赖才发挥功效。因此消除依赖是一项崇高的追求,这样做几乎是不可能的。控制依赖和减少依赖则是非常完美的。

就紧耦合(heavy-coupling)和松耦合(loose-coupling)而言,通常是指一个对象依赖于其他对象的程度。

在一个松耦合系统中,一个对象的变化会减少对其依赖对象的影响。在这样的系统中,类取决于接口而不是具体的实现(将会在下面提到)。这就是为什么松耦合系统对修改更加开放的原因。

Coupling in a Field

让我们看下面这个例子:

 

1
2
3
4
5
6
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = new Calculator(1,2);
   }
}

 

这段代码很常见,在该例中,Display类依赖Calculator类并直接引用该类。Display类里的 $calculator字段属于Calculator类型。该对象和字段直接调用Calculator的构造函数。

 通过访问其他类方法进行耦合

大家可以先看下面的代码:

 

1
2
3
4
5
6
7
8
9
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = new Calculator(1, 2);
   }
   function printSum() {
      echo $this->calculator->add();
   }
}

Display类调用Calculator对象的add()方法。这是另外一种耦合方式,一个类访问另外一个类的方法。

通过方法引用进行耦合

你也可以通过方法引用进行耦合:

 

1
2
3
4
5
6
7
8
9
10
11
12
class Display {
   private $calculator;
   function __construct() {
      $this->calculator = $this->makeCalculator();
   }
   function printSum() {
      echo $this->calculator->add();
   }
   function makeCalculator() {
      return new Calculator(1, 2);
   }
}

需引起注意的是,makeCalculator()方法返回一个Calculator对象,这也是一种依赖。

利用多态进行耦合

遗传可能是依赖里的最强表现形式。

 

1
2
3
4
5
class AdvancedCalculator extends Calculator {
   function sinus($value) {
      return sin($value);
   }
}

通过依赖注入降低耦合

开发人员可以通过依赖注入来降低耦合度,例如:

1
2
3
4
5
6
7
class Display {
   private $calculator;
   function __construct(Calculator $calculator = null) {
      $this->calculator = $calculator ? : $this->makeCalculator();
   }
// ... //
}

利用Display的构造函数对Calculator对象进行注入,从而减少了Display对Calculator类产生的依赖。

利用接口降低耦合

例如:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface CanCompute {
   function add();
   function subtract();
}
class Calculator implements CanCompute {
   private $firstNumber;
   private $secondNumber;
   function __construct($firstNumber$secondNumber) {
      $this->firstNumber = $firstNumber;
      $this->secondNumber = $secondNumber;
   }
   function add() {
      return $this->firstNumber + $this->secondNumber;
   }
   function subtract() {
      return $this->firstNumber - $this->secondNumber;
   }
}
class Display {
   private $calculator;
   function __construct(CanCompute $calculator = null) {
      $this->calculator = $calculator ? : $this->makeCalculator();
   }
   function printSum() {
      echo $this->calculator->add();
   }
   function makeCalculator() {
      return new Calculator(1, 2);
   }
}

 

该代码定义了一个CanCompute接口,在OOP中,接口可以看作一个抽象类型,它所定义的成员必须由类或结构来实现。在上述代码中,Calculator类来实现CanCompute接口。

Display构造函数期望有个对象来实现Cancompute接口,这时,Display的依赖对象Calculator被打破。然而,我们可以创建另一个类对象来实现Cancompute,并且传递一个对象到Display的构造函数中。Display现在只依赖于Cancompute接口,但即使这样依赖关系仍然是可选的。如果我们不传递任何参数给Display的构造函数,那么它将通过调用makeCalculator()方法来创建一个Calculator对象。这种技术经常被开发者们使用,尤其对驱动测试开发(TDD)极其有帮助。

SOLID原则

SOLID是一套代码编写守则,也就是大家常常说的敏捷开发原则,最初由Robert C. Martin所提出。使用它编写出来的代码不仅干净整洁,而且易维护、易修改和易扩展。实践表明,其在可维护性上有着非常积极的影响,更多资料大家可以阅读: Agile Software Development, Principles, Patterns, and Practices

SOLID所涵盖的话题非常广,下面我将会针对本文的主旨介绍一些简单易学的方法。

1.单一责任原则(SRP)

一个类只干一件事。听起来简单,但在实践中却可能相当难。

 

1
2
3
4
5
6
class Reporter {
   function generateIncomeReports();
   function generatePaymentsReports();
   function computeBalance();
   function printReport();
}

 

查看上面的代码,你认为该类的受益者会是哪个部门?会计部是用于收支平衡、财政部可能用来编写收入/支出报告,甚至归档部来打印和存档报告。然而每个部门都希望有属于自己的方法,并且根据自身需求来做些自定义的方法。

这样的类往往都是高内聚低耦合的。

2.Open-Closed原则(OCP)

类(和模块)应具备很好的功能扩展性,以及对现有功能具有一定的保护能力。让我们一起来看下典型的电风扇例子,你有一个开关来控制风扇:

 

1
2
3
4
5
6
7
8
9
10
11
12
class Switch_ {
   private $fan;
   function __construct() {
      $this->fan = new Fan();
   }
   function turnOn() {
      $this->fan->on();
   }
   function turnOff() {
      $this->fan->off();
   }
}

 

这段代码创建了Switch_类,用来创建和控制Fan对象。注意这里的下划线,在PHP中是不允许把类名定义为Switch的。

这时,你的老板希望能利用该开关控制电风扇上的电灯,那么你就不得不修改Switch_这个类。

对现有代码进行修改存在一部分风险,很有可能对系统其他部分产生影响。所以在添加新功能时的最好的方法是避开现有功能。

在OOP中,你可以发现Switch_对Fan类有很强的依赖性。这正是我们的问题所在,基于此,做出如下修改:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Switchable {
   function on();
   function off();
}
class Fan implements Switchable {
   public function on() {
      // code to start the fan
   }
   public function off() {
      // code to stop the fan
   }
}
class Switch_ {
   private $switchable;
   function __construct(Switchable $switchable) {
      $this->switchable = $switchable;
   }
   function turnOn() {
      $this->switchable->on();
   }
   function turnOff() {
      $this->switchable->off();
   }
}

 

该代码定义了一个Switchable接口,它里面所定义的方法需要开关启用选项来实现。Fan对象实现Switchable和Switch_并且接受一个参数到Switchable对象的构造函数里。

这样做有哪些好处?

首先,该解决方案打破了Switch_和Fan之间的依赖关系。Switch_不知道它要开启风扇,并且也不关心。其次引进的Light类不会影响Switch_或Switchable。难道你想用Switch_类来控制Light对象吗?代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Light implements Switchable {
   public function on() {
      // code to turn ligh on
   }
   public function off() {
      // code to turn light off
   }
}
class SomeWhereInYourCode {
   function controlLight() {
      $light new Light();
      $switch new Switch_($light);
      $switch->turnOn();
      $switch->turnOff();
   }
}

 

3.Liskov替换原则(LSP)

LSP是指子类永不打破父类的功能,这点是非常重要的。用户定义一个子类只是希望能实现其自有功能,而不是去影响原来的功能。

乍看有点困惑,还是让我们一起来看看代码吧:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle {
   private $width;
   private $height;
   function setWidth($width) {
      $this->width = $width;
   }
   function setHeigth($heigth) {
      $this->height = $heigth;
   }
   function area() {
      return $this->width * $this->height;
   }
}

 

定义一个简单的Rectangle类,我们可以设置它的高度和宽度,并且area()方法可以计算出该矩形的面积。再看下面例子:

 

1
2
3
4
5
6
7
class Geometry {
   function rectArea(Rectangle $rectangle) {
      $rectangle->setWidth(10);
      $rectangle->setHeigth(5);
      return $rectangle->area();
   }
}

 

rectArea()方法接受一个Rectangle对象作为一个参数,设置其高度和宽度并且返回该图形的面积。

正方形乃是矩形中的一个特殊图形,我们定义Square类来继承Rectangle:

 

1
2
3
class Square extends Rectangle {
   // What code to write here?
}

 

我们有好几种方法来重写area()方法并且返回该正方形的宽度:

 

1
2
3
4
5
6
7
8
9
10
class Rectangle {
   protected $width;
   protected $height;
   // ... //
}
class Square extends Rectangle {
   function area() {
      return $this->width ^ 2;
   }
}

 

把Rectangle的字段改为protected,好让Square有访问的权限。从几何的角度来看是非常合理的,因为正方形的边长是相等的,所以返回正方形的宽度是非常合理的。

然而从编程的角度来看又存在一个问题;如果Square是一个Rectangle,把它馈入到Geometry类是没有任何问题的,但这样做以后,Geometry的代码就显的多余,毫无意义可言。它设置了高度和宽度两个值,这也就是为什么square不是rectangle编程。LSP正很好是说明了这一点。

4.接口隔离原则(ISP)

该原则主要集中用在把大接口分成多个小接口和特殊的接口。基本思路是在同一个类中,不同的用户不应该知道不同的接口——除非该用户需要用到那个接口。即使一个用户不需要使用该类的所有方法,但它仍然依赖于这些方法。所以为什么不根据用户需要定义相应的接口呢?

想象下,如果我们要实现一个股票市场应用,我们要有一个经纪人(Broker)来购买和出售股票,并且报告每天的收益和损失。一个简单的实现方法是定义一个Broker接口,一个NYSEBroker类用来实现Broker和一些用户的接口类:创建交易(TransactionUI)和写报告(DailyReporter)。代码可以类似下面这样:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
interface Broker {
   function buy($symbol$volume);
   function sell($symbol$volume);
   function dailyLoss($date);
   function dailyEarnings($date);
}
class NYSEBroker implements Broker {
   public function buy($symbol$volume) {
      // implementsation goes here
   }
   public function currentBalance() {
      // implementsation goes here
   }
   public function dailyEarnings($date) {
      // implementsation goes here
   }
   public function dailyLoss($date) {
      // implementsation goes here
   }
   public function sell($symbol$volume) {
      // implementsation goes here
   }
}
class TransactionsUI {
   private $broker;
   function __construct(Broker $broker) {
      $this->broker = $broker;
   }
   function buyStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->buy($data['sybmol'], $data['volume']);
   }
   function sellStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->sell($data['sybmol'], $data['volume']);
   }
}
class DailyReporter {
   private $broker;
   function __construct(Broker $broker) {
      $this->broker = $broker;
   }
   function currentBalance() {
      echo 'Current balace for today ' date(time()) . "\n";
      echo 'Earnings: ' $this->broker->dailyEarnings(time()) . "\n";
      echo 'Losses: ' $this->broker->dailyLoss(time()) . "\n";
   }
}

 

虽然这段代码可以正常工作,但它违反了ISP。DailyReporter和TransactionUI都依赖Broker接口。然而,它们只使用接口的一部分。TransactionUI使用buy()和sell()方法,而DailyReporter只用到dailyEarnings()和dailyLoss()方法。

你怀疑Broker没有内聚力,因为它的一些方法没有任何相关性。也许你说的对,但是具体答案还得由Broker说了算;销售和购买可能与当前的盈余有相当大的关系。例如当亏本的时候有可能就不会执行购买操作。

此时,你可能会说Broker违反了SRP,因为有两个类以不同的方式在使用它,可能有两个不同的执行者。好吧,其实它并没有违反SRP。唯一的执行者就是Broker。他会根据当前的形式做出购买/出售操作,其最终的依赖对象是整个系统和业务。

毫无疑问,上述代码肯定是违反了ISP,两个UI类都依赖于整个Broker。这是很常见的问题,改变下观点,代码可以这样修改:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
interface BrokerTransactions {
   function buy($symbol$volume);
   function sell($symbol$volume);
}
interface BrokerStatistics {
   function dailyLoss($date);
   function dailyEarnings($date);
}
class NYSEBroker implements BrokerTransactions, BrokerStatistics {
   public function buy($symbol$volume) {
      // implementsation goes here
   }
   public function currentBalance() {
      // implementsation goes here
   }
   public function dailyEarnings($date) {
      // implementsation goes here
   }
   public function dailyLoss($date) {
      // implementsation goes here
   }
   public function sell($symbol$volume) {
      // implementsation goes here
   }
}
class TransactionsUI {
   private $broker;
   function __construct(BrokerTransactions $broker) {
      $this->broker = $broker;
   }
   function buyStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->buy($data['sybmol'], $data['volume']);
   }
   function sellStocks() {
      // UI logic here to obtain information from a form into $data
      $this->broker->sell($data['sybmol'], $data['volume']);
   }
}
class DailyReporter {
   private $broker;
   function __construct(BrokerStatistics $broker) {
      $this->broker = $broker;
   }
   function currentBalance() {
      echo 'Current balace for today ' date(time()) . "\n";
      echo 'Earnings: ' $this->broker->dailyEarnings(time()) . "\n";
      echo 'Losses: ' $this->broker->dailyLoss(time()) . "\n";
   }
}

 

修改后的代码明显变的有意义而且尊重了ISP。DailyReporter只依赖BrokerStatistics,它无需关心和知道出售和购买这两个操作。另一方面,TransactionUI只关心购买和出售。NYSEBroker和先前的定义是一样的,实现BrokerTransactions和BrokerStatistics接口。

更复杂的例子你可以前往Rober C.Martin博客上查看 The Interface Segregation Principle里的首篇论文。

5.依赖倒置原则(DIP)

这条原则指出高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖细节,细节反过来应依赖于抽象。简单地说,你应该尽可能的依赖于抽象而不是实现。

DIP的诀窍是你想反转依赖,但是又想一直保持着整个控制流。回顾下OCP(Switch和Light类),在原始实现中是直接利用开关来控制灯的。

 

你会看到整个依赖和控制流都是由Switch流向Light。当不想直接控制Light时,你可以引进接口这一概念。

 

非常神奇!引进接口后,代码同时满足了DIP和OCP两大原则。正如你上图所看到的,倒置了依赖,但整个控制流是不变的。

高级设计

关于代码的另一重要方面是高级设计和通用体系结构。一个混乱的架构所产生的代码往往是很难修改的,所以保持一个干净整洁的架构是必不可少的,第一步就是理解如何根据不同的内容分离代码。

 

在这张图中,最主要的部分是业务逻辑,它能够如预期那样正常有效的工作并且与其他部分不存在任何瓜葛。站在高级设计角度可以看作为正交性。

从右边的“main”开始看,箭头进入应用程序——创建对象工厂。一个理想的解决方案是从各个特定的工厂中得到相应的对象,但这有点不切实际。不过当有机会这样做的时候还是要使用,并且让它们保持在业务逻辑之外。

再看底部,定义持久层(数据库、文件访问、网络通信)用来保证信息的持久性。业务逻辑层是没有对象知道持久层是如何工作的。

左边则是交互机制。MVC比如Laravel、CakePHP,只能是交付机制而已。

当你看到应用程序架构或目录时,你应该注意其架构是说明程序将要做什么,而不是使用什么技术或数据库。

最后,为了确保所有的依赖项都指向业务逻辑层。用户接口、工厂、数据库则是具体的实现,而你永远不要只依赖于它们。依赖倒置指向业务逻辑模块,无需修改业务逻辑的依赖关系即可允许我们改变依赖。

关于设计模型

在使代码变得易于修改和理解的过程中,设计模型扮演着非常重要的角色。从结构的角度来看,设计模式显然是很有好处的,它们是行之有效并且深思熟虑的解决方案。更多关于设计模式内容,可以前往 Tuts+ Premium course 

测试的力量

测试驱动开发(TDD)所编写出来的代码是很容易测试的。TDD迫使你尊重以上原则来编写代码,从而使你的程序更易被测试。单元测试运行速度很快,应该非常快,当你在一个类里使用10个对象来测试一个单独方法时,你的代码很有可能是有问题的。

总结

俗话说,实践乃是检验真理的唯一标准,所以开发者只有在平时的工作中坚持使用这些原则才能编写出理想的代码。与此同时,不要轻易满足于自己所编写出的代码,要努力让你的代码易于维护、干净并且拥抱变化。(编译/张红月 责编/王然)

来自: net.tutsplus

自动生成代码工具

背景

很多业务很多功能对应的操作都很接近,很多代码都是共通的,只需改动一部分。如果新建一张表,共通的代码能自动生成就好,不需要拷贝原来的代码还要修改,能节省很多时间。

准备

1、MyBatis使用Generator自动生成代码,工具配置、数据库连接读取等通过它来完成。需要下载mybatis-generator-core-1.3.2

MyBatis生成的是固定格式的的几个模板,而且只能是java相关的代码,并且很多东西还不太灵活,不能满足很多业务。

2、通过velocity生成模板文件。需要下载velocity1.7

设计

1.一个表对应的数据通过MyBatis读取。

2.按一定规则自定义一套模板文件,通过velocity生成每个表对应的模板文件。自定义的模板文件可以满足PHP/JAVA等各种语言的代码生成。

开发语言:java

详细说明及源码

正准备开发中,待续!