如何编写出拥抱变化的代码
2013-02-28 09:16
127 查看
如何编写出拥抱变化的代码?
敏捷设计模型测试开发经验面向对象
摘要:编写高效优质的代码一直是程序员所追求的目标之一,那么什么样的代码才叫优质呢?其中最重要的莫过于易维护、易修改。本文作者从面向对象和SOLID两大方面,非常详细地总结了如何编写出易修改的代码,绝对让你受益匪浅。在实际的开发中,编写出易维护和易接受变化的代码并非易事,想要实现可能更加困难重重:源码难于理解、依赖关系指向不明、耦合也很令人头疼。难道就真的就没有办法了吗?本文中我们一起探讨几个技术原则和一些编码理念,让你的代码跟着需求走,而且易维护易拓展。
介绍些面向对象方法
面向对象编程(OOP)是一种很受欢迎的编程思想,它保证了代码的组织性和重用性。软件公司采用OOP思想编程已经好多年了,如今仍然在项目开发中使用这一思想。OOP拥有一系列非常好的编程原则,如果使用恰当,它会让你的代码更好、更整洁和更易维护。
1.内聚力
这里的内聚力是指拥有一些共同的特征的东西而逐渐凝聚到一起,而不能在一起的东西则会被移除出去。可以用一个类来说明内聚力:
这个类只负责各个独立的群体信息,显然,内聚力很低。重构上面的例子:
2.正交性
就简单而言,正交是指隔离或排除副作用。一个方法、类或者模块改变了其他无关的方法、类或模块就不是正交。例如,飞机的黑匣子就具有正交性,它自身就具备电源、麦克风和传感器等这些功能。而它对外在的其他东西没有任何影响,它只提供一种机制,用来保存和检索飞行数据。
一个典型的非正交系统例子就是汽车电子设备。提高汽车的速度也存在些负面影响,比如会增加无线电音量,然而对汽车来说,速度并不是正交。
3.依赖和耦合
大多数情况下,这两个单词是可以互换的,但是在某些情况下,又存在优先级关系。
那么,什么是依赖呢?当对象A需要使用对象B时,为了执行其规定的行为,我们说A依赖B。在OOP中,依赖是极其常见的。对象之间经常互相依赖才发挥功效。因此消除依赖是一项崇高的追求,这样做几乎是不可能的。控制依赖和减少依赖则是非常完美的。
就紧耦合(heavy-coupling)和松耦合(loose-coupling)而言,通常是指一个对象依赖于其他对象的程度。
在一个松耦合系统中,一个对象的变化会减少对其依赖对象的影响。在这样的系统中,类取决于接口而不是具体的实现(将会在下面提到)。这就是为什么松耦合系统对修改更加开放的原因。
Coupling in a Field
让我们看下面这个例子:
通过访问其他类方法进行耦合
大家可以先看下面的代码:
通过方法引用进行耦合
你也可以通过方法引用进行耦合:
利用多态进行耦合
遗传可能是依赖里的最强表现形式。
开发人员可以通过依赖注入来降低耦合度,例如:
利用接口降低耦合
例如:
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)
一个类只干一件事。听起来简单,但在实践中却可能相当难。
这样的类往往都是高内聚低耦合的。
2.Open-Closed原则(OCP)
类(和模块)应具备很好的功能扩展性,以及对现有功能具有一定的保护能力。让我们一起来看下典型的电风扇例子,你有一个开关来控制风扇:
这时,你的老板希望能利用该开关控制电风扇上的电灯,那么你就不得不修改Switch_这个类。
对现有代码进行修改存在一部分风险,很有可能对系统其他部分产生影响。所以在添加新功能时的最好的方法是避开现有功能。
在OOP中,你可以发现Switch_对Fan类有很强的依赖性。这正是我们的问题所在,基于此,做出如下修改:
这样做有哪些好处?
首先,该解决方案打破了Switch_和Fan之间的依赖关系。Switch_不知道它要开启风扇,并且也不关心。其次引进的Light类不会影响Switch_或Switchable。难道你想用Switch_类来控制Light对象吗?代码如下:
LSP是指子类永不打破父类的功能,这点是非常重要的。用户定义一个子类只是希望能实现其自有功能,而不是去影响原来的功能。
乍看有点困惑,还是让我们一起来看看代码吧:
正方形乃是矩形中的一个特殊图形,我们定义Square类来继承Rectangle:
然而从编程的角度来看又存在一个问题;如果Square是一个Rectangle,把它馈入到Geometry类是没有任何问题的,但这样做以后,Geometry的代码就显的多余,毫无意义可言。它设置了高度和宽度两个值,这也就是为什么square不是rectangle编程。LSP正很好是说明了这一点。
4.接口隔离原则(ISP)
该原则主要集中用在把大接口分成多个小接口和特殊的接口。基本思路是在同一个类中,不同的用户不应该知道不同的接口——除非该用户需要用到那个接口。即使一个用户不需要使用该类的所有方法,但它仍然依赖于这些方法。所以为什么不根据用户需要定义相应的接口呢?
想象下,如果我们要实现一个股票市场应用,我们要有一个经纪人(Broker)来购买和出售股票,并且报告每天的收益和损失。一个简单的实现方法是定义一个Broker接口,一个NYSEBroker类用来实现Broker和一些用户的接口类:创建交易(TransactionUI)和写报告(DailyReporter)。代码可以类似下面这样:
你怀疑Broker没有内聚力,因为它的一些方法没有任何相关性。也许你说的对,但是具体答案还得由Broker说了算;销售和购买可能与当前的盈余有相当大的关系。例如当亏本的时候有可能就不会执行购买操作。
此时,你可能会说Broker违反了SRP,因为有两个类以不同的方式在使用它,可能有两个不同的执行者。好吧,其实它并没有违反SRP。唯一的执行者就是Broker。他会根据当前的形式做出购买/出售操作,其最终的依赖对象是整个系统和业务。
毫无疑问,上述代码肯定是违反了ISP,两个UI类都依赖于整个Broker。这是很常见的问题,改变下观点,代码可以这样修改:
更复杂的例子你可以前往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个对象来测试一个单独方法时,你的代码很有可能是有问题的。
总结
俗话说,实践乃是检验真理的唯一标准,所以开发者只有在平时的工作中坚持使用这些原则才能编写出理想的代码。与此同时,不要轻易满足于自己所编写出的代码,要努力让你的代码易于维护、干净并且拥抱变化。
相关文章推荐
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?(转)
- 如何编写出拥抱变化的代码?
- 研发周报:如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写出拥抱变化的代码?
- 如何编写程序使得数据库服务器变化时代码不需要重新编译
- 程序员自我修炼(一) - 如何编写合格代码(上)