这本书其实看完有一段时间了,今天来做个总结。在此之前,想为该作者Friedman的其他书籍打个广告,我目前看过《The Little Schemer》—-神书,看完(并实践完)能让你对递归有更深的理解,同时还能学习一门Lisp方言。

本书主要内容

如果你去豆瓣看看书评,有人会说,本书好评(客套话),全篇只讲了一个东西,就是 Visitor模式!!!(浅显的认知)另外,Visitor模式本身的价值可能也被大多数OO程序所忽略了,这可能也是作者为什么从那么多模式里面单挑了Visitor来讲的原因。

作者在序言中提到,OO编程语言可以让程序员构建可复用的代码组件,在理想的情况下,程序员不需要修改现有的代码,只需要在现有代码框架下面编写胶水代码和添加新功能的代码。但是这样设计良好的OO代码太难得到了,需要有很强的“编程自律”(discipline of programing)

为什么选择java来讲解这些模式,是因为java这门语言简单,OO but not like cpp,同时提供了GC,可以让程序员不用太担心内存问题,而只需要集中精力在程序设计上面。(我猜想也可能是java程序员多,用java这本书会好卖一点,毕竟javascript跟java没有一毛钱关系,也要趁个ip)

谈设计模式,只提Visitor,这显然不合理。因为我们知道GOF的设计模式可是有24个,分为创建型,行为型和结构型三大类,是OO编程语言的内功心法,易筋经,谈java,必谈设计模式。所以,这可能是本书书名的由来。Orz

但是,作者意图显然并不止于此,作者更多的是从编写可重用组件的角度来讲OO模式的。

OO领域有SOLID五大原则:单一职责原则,开闭原则,里氏替换原则,依赖倒转原则和接口隔离原则。作者把它对pattern的理解写成了一系列的advice。我这里把这些advice总结一下,作为本书的内容精要吧。

Advice 1: 使用抽象类进行数据类型建模

When specifying a collection of data, use abstract classes for data types and extended classes for variants.

(我的理解:下文同,故会省略)

OO是不喜欢函数指针的,继承和多态可以消除函数指针,实现动态Dispatch。同时抽象类可以让模块之间依赖减少,多种不同的派生类负责具体的业务逻辑。这里讲的应该是 datatype的可自由组合,你可以把datatype跟乐高作对比,每一个datatype就是乐高积木的一个形状。

Advice 2:使用派生类来扩展数据类型

When writing a function over a datatype, place a method in each of the variants that make up the datatype. If a field of a variant belongs to the same datatype, the method may call the corresponding method of the field in computing the function.

里氏替换原则里面要求,派生类必须完整实现父类定义的接口,否则父类和派生类就不是可以随意替换的。如果派生类的数据成员被其他派生类所共有,通常情况下会在实现子类方法的时候调用父类的实现。(即调用super.methodXXX())。这一层,讲的是datatype的可替换性,这个类似乐高里面的点槽,形状一样,点槽不一样的积木是没办法自由组合的。

Advice 3:函数返回值使用new创建的对象

When writing a function that returns values of a datatype, use new to create these values.

这里(敲黑板)就不再是简单的OO原则了,而是FP原则。尽量减少副作用(Side Effect),同时返回新的对象还可以实现链式调用,增强OO函数的可组合性。函数的参数是datatype,返回值也是datatype,这样就可以自由组合了,达到最大限度的灵活性(注意:datatype必须是抽象的)

Advice 4: 使用访问者模式来扩展类

When writing several functions for the same self-referential datatype, use visitor protocols so that all methods for a function can be found in a single class.

通常,我们会选择直接给一个类添加方法来进行扩展。如果业务有变化,我们就新建子类并在子类中覆盖父类的实现来完成功能扩展和定制。但是,如果我们把某一个方法抽象成一个类,然后该类型子类所有的实现都可以聚集在一个class中。如下图的代码所示:

因为类型是可以参数化的(还记得吗?多态),所以,原本的方法抽象成类以后,我们就拥有了更强大的可组合性。另外,再多提一点,把所有子类型判断是不是Vegetarian都放在一个类中,其实也有利于功能内聚,很多OO模式(状态模式,命令模式,策略模式)关注的也是用Class去封装行为,而不只是封装数据。

Advice 5 is mising

因为书里面没有写。

Advice 6: 构建具有类型结构的visitor

When the additional consumed value change for a self-referential use of a visitor, don't forget to create a new visitor.

每一组方法(这组方法是为该datatype所有的派生类准备的)抽象成一个类以后,这些类可以组成一个类的继承结构,每一个派生类都是一个Visitor。

这个就是我们的visitor模式的示意图了。到这里,我们基本上可以了解visitor模式是怎么演化过来的了。其实本来是没有什么模式的,大家都按照这样的设计原则去设计程序,最后就变成了我们熟悉的模式了。

Advice 7: 设计visitor协议时考虑共享同一个基类

When designing visitor protocols for many different types, create a unifying protocol using Object.

当为许多不同类型设计 Visitor时,尽可能统一协议,即采用Object作为每个Visitor方法的参数。因为Java里面所有的对象都是从Object派生过来的,这样子设计的visitor实际上扩展性是更强了。但是,这也会遇到一些向下转型(downcasting)的问题,需要使用运行时的类型信息做一些额外的处理。

Advice 8:设计子类时尽量使用重载,保证父子类型可以替换

When extending a class, use overriding to enchrich its functionality.

当扩展一个类的时候,使用覆盖(overriding) 的方式来增强其功能。这里其实也是呼应前面提到的抽象类和子类的概念,利用OO的多态实现功能扩展。OO hierarchy between datatypes 和 OO hierarchy between datatype’s method groups,这里visitor类,更像是一个个高度抽象化的函数,对比函数式编程(FP),函数是可以自由组合的,只要签名一样,函数还可以自由替换。

Advice 9: 设计扩展良好的visitor

If a datatype many have to be extended, be forward looking and use a constructor-like (overridable) method so that visitors can be extended, too.

如果一个数据类型需要被扩展,那么也需要同时考虑visitor是否可以兼容这种扩展。最好是使用可以被override的方法,这样visitor的子类型就可以覆盖这些方法。

Advice 10: 尽可能使用visitor模式来解耦代码

When modifications to objects are needed, use a class to insulate the operations that modify objects. Otherwise, beware the consequences of your actions.

当必须要对一个对象进行修改的时候,不要每次都直接在该对象的类型上添加一个方法和若干数据成员来解决。可以考虑,是否可以通过添加一个visitor class来解决。因为visitor 的方式可扩展性更强,如果直接添加方法可以解决问题,并且后续需求变化不需要频繁对该方法进行修改,那么就没有必要引入visitor,反之,则需要使用visitor来解耦代码。

总结

这9个advice,包含了visitor模式的演化思路和OO设计思想的基本原则,聚焦在可复用面向对象软件设计“套路”上,通过OO提供的多态,把原本类方法拆分成一组带有类型结构信息的 visitors,提高了代码的灵活性和可复用性。学会了这个模式,以后你的OO代码就不再是:几个巨无霸类 + 一堆单例 + 一个MVC框架的祖传代码了。

这样,我们离完美世界也就不远了:

添加新功能不需要修改现有代码,只需要编写一些新的class和胶水代码。而Reason代码的时候,也不会被复杂的Context所吓到,只需要Focus到某个具体业务场景下。