原文地址:Chapter 1 - An Introduction to ASP.NET MVC
Avoiding Code Smells
如果你不是很小心,软件程序很快就会变得很难对应变更。我们都曾经有这种经历,从别人那里接管一个应用程序,然后被要求对其进行改动。回想一下你心中在作出第一个改动前的担心和恐惧。
在这个接力棒的游戏中,你必须依次将接力棒从一堆接力棒中移除,而不能影响其他接力棒。一个很微小的错误对于一堆接力棒来说,可能是很巨大的。
改动一个现存的软件应用程序和接力棒的游戏很像。在你修正一个错误的同时,又引入了一个bug。
不好的软件很难应对变化。Robert和Micah Martin将不好的软件的特点描述为代码味道。以下代码味道表明了糟糕的软件的代码味道:
· 僵化 – 僵化的软件是指当你改动一个地方时,同时还需要改动其他很多地方。
· 脆弱 – 脆弱的软件是指你作出一个改动后,软件会在多个地方崩溃。
· 没有必要的复杂度 – 没有必要的复杂度是指软件被过度设计成可以解决任何可能的变更。
· 没有必要的重复 – 没有必要的重复是指软件中包含重复代码。
· 晦涩 – 晦涩的软件很难被理解。
*** Begin Note ***
这些代码味道在Micah 和 Robert Martin的著作《Agile Principles, Patterns, and Practices in C#》的104页被提及,强烈推荐这本著作。
*** End Note ***
请注意这些代码味道都和变更相关,其中任何一个代码味道都是横在变更前的一个障碍。
Software Design Principles
软件不必写的很差,可以在一开始时,对软件进行设计,使得它更好的应对后面的变更。
使得软件容易应对变更的最好策略就是让软件的各个组件之间松耦合。在一个松耦合的应用程序中,你可以对程序的某一个组件进行改动,而不会影响其他组件。
多年来,为了编写好的软件,出现了一些原则。这些原则可以降低软件不同部分相互依赖的程度。这些 原则被Robert Martin 收集到他的著作中。
Robert Martin并没有发明这些原则,他是第一个人将这些原则收集整理到一个简单的列表中。以下就是他关于软件设计原则的列表:
· SRP – 单一职责原则
· OCP – 开-闭原则
· LSP – Liskov可替换原则
· ISP – 接口隔离原则
· DIP – 依赖倒置原则
这个由原则组成的集合被广泛的称为SOLID(没错,SOLID是每个原则的首字母的缩写)。
例如,根据单一职责原则,一个类应该有且只有一个原因来引发变化。下面是一个对于这个原则如何应用的实际示例:如果你知道你可能会更改程序的验证逻辑,这些验证逻辑是独立于它的数据访问逻辑的,那么,你就不应该将验证和数据访问的逻辑放在一个类中。
*** Begin Note ***
关于软件设计原则,还存在其他列表。例如《Head First Design Patterns》中也包含一个非常不错的列表。
*** End Note ***
Software Design Patterns
软件设计模式是应用软件设计原则的体现,换言之,软件设计原则是不错的想法,设计模式是实现这个想法的工具(它是一个鼓)。
设计模式的思想最初是由《Design Patterns: Elements of Reusable Object-Oriented Software》书中提出的,这本书也被称为“四人组”或“四人帮”,这本书影响了其他很多讲述软件设计模式的书籍。
和“四人帮”的书相比,《Head First Design Pattern》在设计模式方卖弄,提供了一个更加用户友好的介绍,这本书集中介绍了诸如观察者、门面、单件以及适配器等14个模式。
另外一本在软件设计模式领域有影响力的书籍是Martin Fowler 的《Patterns of Enterprise Application Architecture》。这本书有一个支持网站,在网站中提供了书中涉及的所有模式,网站地址:http://www.martinfowler.com/eaaCatalog/。
软件设计模式可以使你的代码更易于对应变更。例如,在这本书的很多地方,我们都会利用设计模式中的Repository模式。Eric Evans 在他的著作《Domain-Driven Design》中,对Repository 模式有以下描述:
一个Repository 代表了特定类型的所有对象,特定类型是一个概念上的集合(通常是模拟的)。它在操作上很像一个集合,但是包含了更加复杂的查询能力。特定类型的对象可以被添加和删除,这些插入和删除是通过操作数据库来实现的(参考151页)。
译注:本段文字翻译的可能不太准确,个人理解是Repository相当于在应用层和数据层之间的一层,向应用层提供持久化数据,向数据层负责管理持久化数据。附上原文:A REPOSITORY represents all objects of a certain type as a conceptual set (usually emulated). It acts like a collection, except with more elaborate querying capability. Objects of the appropriate type are added and removed, and the machinery behind the REPOSITORY inserts them or deletes them from the database. (see page 151),欢迎大家补充准确的含义。
按照Evans 的说法,使用Repository 模式的一个主要好处就是能够让你“将应用、领域设计和持久层技术、多数据库策略或者多数据源之间进行解耦合”(ibid),换言之,Repository 模式可以在数据库访问机制发生变化时,对你的应用程序进行保护,使应用程序独立于数据库访问方式。
例如,当我们在本书的最后编写论坛应用程序时,我们利用Repository 模式来隔离论坛应用程序和特定的持久层技术。通过这种设计方式,论坛应用程序可以在不同的访问救赎之间进行切换,例如从LINQ切换到SQL、Entity框架甚至是NHibernate 。
Writing Unit Tests for Your Code
通过利用软件设计原则和模式,你可以创建更易应对变更的软件。设计模式是架构层次的模式,它们着眼于应用程序总的结构。
如果你想在更细的粒度层次上表明你的程序可以应对变更,你可以为你的应用程序构建单元测试。单元测试能够让你确定程序中的方法是否按照你的意图运行。
你可以从为你的代码编写单元测试中得到很多好处:
1) 为你的代码编写测试可以向你提供一个应对变更的安全环境。
2) 为你的代码编写测试可以迫使你编写松耦合的代码。
3) 为你的代码编写测试可以迫使你站在用户的立场上看待代码。
首先,单元测试为你提供了一个应对变更的安全环境,这是Michael Feathers 在他的著作《Working Effectively with Legacy Code》中反复强调的,事实上,他将遗留代码定义为“没有带测试的简单代码”(参看xvi)。
当你的应用程序被单元测试所覆盖时,你可以改动代码而不用担心改动会破坏代码的功能。单元测试使得你的代码可以很安全的进行重构,如果你可以重构,那么你可以利用设计模式对代码进行改动,从而使得程序更易于应对变更。
*** Begin Note ***
重构是这样一个过程,它改动了代码,但是没有改变代码的功能。
*** End Note ***
第二,为你的代码编写单元测试可以迫使你以一种特别的方式编写代码。可测试的代码一般是松耦合的代码,单元测试是对一块代码进行隔离测试。为了使得你的程序是可测试的,你不得不以隔离组件的方式构建你的应用程序。
当一个类和另外一个类是松耦合时,你对第一个类进行改动时,不用影响到第二个类。测试驱动开发经常迫使你编写非常松耦合的代码,这种代码非常易于应对变更。
最后,编写单元测试可以迫使你站在用户的立场来看待代码。当编写一个单元测试时,你会采取和将来使用你的代码的人一样的立场,既然编写测试会迫使你思考将来开发人员(可能就是将来的你自己)如何使用你的代码,代码自然会被更好的设计。
Test Driven Development
在上一个章节中,我们讨论了为你的代码编写单元测试的重要性,测试驱动开发是一种软件设计方法学,它认为单元测试在整个软件开发过程中出于中心地位,当你实践测试区鄂东开发时,你首先编写测试,然后针对测试编写代码。
更细的说,当你实践测试驱动开发时,在你编写代码时,需要3个步骤(红/绿/重构):
· 编写一个失败的单元测试(红)。
· 编写代码,已通过单元测试(绿)。
· 重构你的代码(重构)。
首先,编写单元测试,单元测试应该表达你期望代码应该是如何运行的。当你第一次创建单元测试时,它是应该运行不通过的,之所以失败,是因为你还没有编写任何可以满足测试的代码。
接下来,你需要编写必要的代码来使得单元测试可以通过,要是以最懒、最草率、最快的方式来编写代码。你不应该浪费时间来思考程序的架构,你应该关注的是编写最小数量的代码来满足单元测试中表达的意图。
最后,在编写了足够的代码后,你可以退回去思考程序的整个架构,在这个阶段,你应该利用设计模式来重写(重构)你的代码--例如Repository 模式-- ,这样你的代码会变的更易于维护。在这个过程中你可以放心的重写代码,因为代码已经被单元测试覆盖了。
实践测试驱动开发可以带来很多好处。首先,测试驱动开发迫使你关注那些实际中确实需要的代码,因为你持续的关注只编写能够通过特定测试的代码,就可以避免编写那些庞大的、你永远都不会用到的代码。
其次,“测试优先”的设计方法学可以迫使你站在使用你的代码如何被使用的角度来编码。换言之,在实践测试驱动开发的时候,你是一直的站在用户的角度编写你测试,这样,测试驱动开发可以带来更加整洁和可理解的API。
最后,测试驱动开发迫使你将编写单元测试作为整个软件开发过程的一部分。在着眼于项目截止日期的方法论中,测试经常是第一个被取消的,但是在实践测试驱动开发时,由于单元测试出于整个开发过程的中心地位,你在编写单元测试时,可能会更加从容不迫。
Short Term Pain, Long Term Gain
构建用于应对变更的软件需要更多前期的努力,实现设计原则和模式需要思考和努力,编写测试需要时间。然而,与前期让软件以正确的方式进行构建所花费的力气相比,在将来会得到巨大的回报。
开发人员有两条路。你可以做一个牛仔,或者你可以做一个工匠。牛仔接到任务后马上跳进去开始编码,他可以迅速的构建一个软件应用程序,问题是随着时间迁移,软件必须要进行维护。
工匠就很有耐心。工匠在构建软件时很小心。他会很小心的构建单元测试来覆盖应用程序中的所有代码,这样他需要花费更长时间来完成一个应用程序。但是,在应用程序被创建后,修改bug和添加新特性都会变得很容易。
大部分开发者都是以牛仔的方式开始他们的编程生涯,然而,在某个时候,你必须挂起你的马鞍,来构建那些能够经得起时间考验的软件。