我在大学时学的Java。OOP(即面向对象编程)模型深植在我的思维中。我想分享一些我从Clojure中学到的东西。
Clojure当然从Java借鉴了很多。如果能同时学习这两门语言一定会很酷。下面是一些通用原则。事实上,这些原则在OOP的世界里众所周知。你很可能已经了解它们,所以本文不要求你学习Clojure,但是我推荐你去这么做。
1、使用不变值
Clojure 得以闻名的一个特性是它的不可变的数据结构(immutable data structures)。甚至在Java的早期,不变值也是一种很受欢迎的做法。String是不可变的,这点在Java刚发布那会备受争议。在那时,C 和C++的字符串仅仅是可以改变的数组。不可变的String被认为是低效的。但是,回头再看,不可变的String似乎是一个正确的选择。Java中的许多可变类现在被认为是设计失误。拿java.util.Date来说,改变一个日期的月份值有什么意思呢?
让我们更深入地分析下。假设我是一个对象。你询问我的生日。我给你一张纸,上面写着我的生日是1981.7.18。你把这张纸带回家,存在某个地方,甚至让其他人看到这张纸。
其中有一个人看到这张纸上的日期后说“cool,a date!”,并且修改为他自己的生日:通过调用setTime方法修改为1976.4.2。这样下一个问我生日的人得到的实际上是这个家伙的生日。这将是多么糟糕的一件事!我将后悔我将那张可以改变我生日的魔术纸给了别人。
让值可变的导致这种magic-changing-at-a-distance行为常常可能发生。它之所以不当的一个原因是它违反了信息隐藏原则。我的生日是我这个对象的部分状态。如果我让生日的月份、日期和年份可以直接被修改,那么我实际上是让任何一个其他类都能够直接访问我的内部状态。
答案当然不是使用setters。而是保证对象一旦构建后不可变。这样,我这个对象的内部状态就一直处于封装状态。
这也适用于集合。你读过Iterator的文档吗?你能告诉我当底部的list改变时将发生什么?我也不能。一个不可变的list不应该有这么一个复杂的接口。
解决方案:不要写setter方法。对于集合,你有几个可选方案。有一个简单方案是使用Google Guava不可变类库。如果不使用Guava,那么任何时候你需要返回一个集合时,先将集合拷贝一份,然后用java.util.Collections。unmodifiable()包装一下这份拷贝,再扔掉对拷贝的引用。
- public static Map immutableMap(Map m) {
- return Collections.unmodifiableMap(new HashMap(m));
- }
2、不要在构造函数中做多余的事情
设想这个场景:你的Person类有一个构造函数接受一大堆信息(first name, last name,address等)并且将它们存为对象的状态。你团队中的某个人需要将这些数据存到文件中,比如存为JSON。为了方便创建Person对象,你增加了一个构造函数,接收inputStream参数并将其解析成JSON,然后设置对象状态。你还增加了一个构造函数接收aFile参数,读取文件并解析。之后又有一个人想从指定URL的web请求中读取内容,你又增加了一个构造函数。非常棒!你现在有了一个非常方便的类。
但是稍等一下!Person类的职责是什么?最初它用来表示某个人的个人信息。现在它还负责:
解析JSON
构造Web请求
读取文件
处理错误
而且现在Person类很难测试。我们如何才能测试File构造函数?首先,我们必须向文件系统中写入一个临时文件。不算太坏。那么我们如何测试Web请求呢?设置一个Web服务器,配置Web服务器,然后调用构造函数。
问题在于Person类违反了单一职责原则。Person类被用来保存状态信息,而不是用来持久化存储或者序列化的。它应该是一个数据对象,而不应该做更多的。
解决方案:避免让构造函数包含多余的逻辑。将“便利构造函数”(比如上面解析JSON的构造函数)分离到静态工厂方法。
3、针对小接口编程
Clojure做得非常好的一点是定义了一些功能强大的小接口,它们抽象出访问模式。任何使用这个接口的函数可以使用实现这个接口的任何类型。任何新类型可以利用已有的功能。
拿Iterable接口来说,它泛化(或者抽象)了任何可以被顺序访问的对象(比如一个list或者一个set)。如果一个方法需要在某对象上顺序操作,那么这个方法只需要了解那对象实现了Iterable接口。这就意味着,当程序员写程序时可以不必关注这个方法实际操作的对象的类型。
这符合依赖倒置原则,依赖倒置原则声称高层逻辑必需依赖于抽象而不是底层逻辑细节。接口很好的吻合了这条原则。高层逻辑应该对接口操作,而底层逻辑实现接口。
解决方案:仔细思考类的访问模式,看看能否抽象出小接口。然后针对接口编程。记住,有两个地方会用到接口:实现接口者和调用者。
4、表达计算过程,而不仅仅是世界(Represent computation, not the world)
当我读大学时,老师告诉我们你们应该用类来为现实世界的对象建模。典型的建模问题是学生选课问题。
一个课程可以有很多学生选,一个学生可以注册很多课程。多对多的关系。
显而易见地建一个Student类和一个Course类。每个类都包含一个对方的list。list表达了课程注册关系。类似register和listCourses这样的方法让学生注册课程或者列出他注册的课程。
教授用这个问题来探讨不同设计方案的折中问题。学生和课程的配置都不合理。一个聪明的数据建模者将能提炼出多对多关系模式。我们可以创建一个叫 ManyToMany<X, Y>的类来管理多对多关系。然后可以创建一个ManyToMany<CourseID, StudentID>对象来解决选课问题。
唯一的问题在于这样做直接违背了教师课程中的意思。关系不是现实世界的对象,它最适合被表述为一种抽象概念。
而且它也可以用来解决泛化的抽象问题。ManyToMany类可以在任何合适的地方被复用。甚至可以让ManyToMany作为一个有很多不同实现的接口。
我认为我的教授是错的。Java标准库也包含了很多单纯运算的类。为什么应用程序员不可以也自己写类似的类呢?更多内容参考GOF设计模式。大部分模式都与抽象运算有关,而不是现实世界的对象。比如职责链模式,在维基百科中被描述为“通过给予多个对象处理请求的机会,而避免调用请求与请求处理者耦合”。
解决方案:寻找代码中的重复模式,构建类来表示这些模式。使用这些类而不是在代码中一再重复。