假如有一个需求,求一段字符串中单词的数量(为了简单起见,不考虑是不是真的正确的求出到底包含几个单词,我们的目的是增加一个功能),怎么做?
先来一个最简单的 javascript 的实现方式:
var wordCount = function(words){
return words.split(" ").length;
};
上面这种方式很容易想得到,也超级简单,但是这就完了吗?
进阶版,我们还可以在 javascript 的 prototype 上新增方法
String.prototype.wordCount = function(){
return this.split(" ").length;
};
得益于动态语言的特性,向已有对象添加方法比较容易, Python 也有类似做法(直接修改str
的话会报can't set attributes of built-in/extension type 'str'
)
class MyString(str):
pass
def word_count(self):
return len(self.split())
MyString.word_count = word_count
MyString("Are you OK?").word_count()
那么, javascript 和 python 哪个方式更好呢?看起来 python 实现的代码冗长的多,也有更多限制, javascript则没有限制,任何类型都可以增加方法. 但是,在团队开发过程中,如果没有事先沟通可能会有两个人先后增加了同名但功能不同的两个方法,结果就是其中一个人的实现被覆盖了. 或者同时使用了两个库,但是他们都增加了同名的方法,这样程序的行为就是不可知的. 因为这种增加方法的方式是作用于全局的,也可以说是污染
了全局.所以这两种方法还不如第一种方法,故不用也罢. 我们最想以什么样的方式来实现这个功能呢? “在不修改源代码的情况下为所有类型增加方法,并且不会污染全局”.
静态语言如何解决这个问题呢?
java项目一般都有个名叫utils
的 package, 里面放了各种StringUtils
,TimeUtils
等等,都是在XXXUtils
类中编写各种静态方法,在需要的时候 import 就可以了.(可以,这很java)
C#或许有更好的实现方式(LINQ就是通过这个方式实现的)
namespace StringExtensionMethods
{
public static class WordCountExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
重点来了: 必须 using 才可以使用这个扩展方法(扩展方法最终会转变成静态方法调用,所以跟 java 相比没有任何性能损失)
using StringExtensionMethods;
...
"Are you OK?".WordCount();
也就是说这个实现并没有作用于全局,这个方式看起来满足了我们的需求: 既可以随心所欲的增加方法,又不需要担心污染全局. 然而这还不够, 我们来看看 scala 是怎么做的:
object StringExtension {
implicit class StringWordCount(words: String) {
def wordCount: Int = {
words.split(" ").length
}
}
}
跟 C#类似,需要 import这个StringWordCount
才可以使用其中的功能
import StringExtension.StringWordCount
"Are you OK?".wordCount
好像跟C#的实现方式没什么不同啊!从上面代码来看, StringWordCount
是一个中间类型,这显然会造成性能开销,不过编译器会内联这些方法调用,JVM 也会进行一些优化,所以实际上性能开销可以忽略.(只是implicit转换越多,编译器越需要花时间寻找implicit转换,所以 scala 编译很慢implicit是有责任的).那么 C#的实现方式和 scala 相比哪种更好呢?我认为 scala 的更好,而且是上面所有实现方式中最好的.为什么呢?单单在实现这个功能上讲, C#的方式和 scala 没有太大的差别,C#的方式简单粗暴,是标准的静态语言套路.但是 scala 实现方式的可扩展性要强得多,因为implicit可以用在更多的其他地方,比如 隐式参数
和Type Class Pattern
.
有一个设计模式是开放-封闭原则
,其含义是’对于扩展是开放的,对于修改是封闭的’.实现方式是构造合理抽象以隔离变化,这就是基本的面向对象方式,抽象出一个接口或者类然后通过继承来实现新功能.这样来说或许继承一个普通的 trait 然后再实现方法就好了(子类型多态),为什么要费这么大劲呢?
trait ToJSON {
def toJSON(level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String,String) =
(INDENTATION * level, INDENTATION * (level+1))
}
case class Address(street: String, city: String) extends ToJSON {
override def toJSON(level: Int): String = ???
}
case class Person(name: String, address: Address) extends ToJSON {
override def toJSON(level: Int): String = ???
}
val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)
println(a.toJSON())
println(p.toJSON())
但是toJSON
一定要是Person
和Address
的成员吗? 通常我们的Model
都包含了太多方法,有的方法只在特定情况下使用,但是却作用于该Model
出现的所有范围,或者某几个子类需要一些功能,为了抽象将这些功能提取到公共父类或者接口中,但是一些子类可能从始至终都没有使用这些方法,因此在代码中添加了一些空实现之类的,这些无用的代码会增加系统负担.而且父类作出修改时子类就必须要做出修改,即便该子类从来不会使用这个方法. 这样就违背了单一职责原则
.子类型多态在这些情况下会使我们的 model
负担加重,成为充血模型
.
Type Class Pattern (代码修改自 Programming Scala)
case class Address(street: String, city: String)
case class Person(name: String, address: Address)
object ToJSONExtension{
trait ToJSON {
def toJSON(level: Int = 0): String
val INDENTATION = " "
def indentation(level: Int = 0): (String, String) = ???
}
implicit class AddressToJSON(address: Address) extends ToJSON {
def toJSON(level: Int = 0): String = ???
}
implicit class PersonToJSON(person: Person) extends ToJSON {
def toJSON(level: Int = 0): String = ???
}
}
import ToJSONExtension.AddressToJSON
import ToJSONExtension.PersonToJSON
val a = Address("1 Scala Lane", "Anytown")
val p = Person("Buck Trends", a)
println(a.toJSON())
println(p.toJSON())
有人会说,这不是很像父类和子类的关系吗?先声明一个trait,然后其他类继承这个trait, 再override需要的方法. 但是面向对象中父类和子类的关系是”is a”的关系,子类在语义上讲需要”is a” 父类. 在这个情况下,Person
和Address
可能有公共父类吗? 再次注意: 需要import对应的type class才可以只用toJSON
方法.所以在需要使用toJSON
时才需要 import, 不需要的时候完全没有任何负担要轻量许多,性能显而易见的更好,复杂度也低得多,扩展性最好的(随意扩展).这样只需要编写贫血模型的Model
就足够使用了.
Original link:https://izhangzhihao.github.io//2017/04/15/浅谈可扩展性/