OOP与FP
2017.01.11 17:26:35

看了阿当老师的微博:OOP与FP的一些事情,发现阿当老师的内容总是能点燃争议的爆点,引发大家的思考与论战。我就“OOP与FP哪个好、哪个坏”也来说说自己的看法。

一般来讲,我喜欢写一写自己喜欢的事情、自己喜欢的代码,一般不太喜欢扯一些“扯不清的事”、“虚无的话”。但是这个话题一阵子就会出现,一阵子又出现,然后我就觉得争论里面,基本上都在“扯不清”(我自己的感觉)。于是我就想谈谈我的认识。——这就是本文的出发点。

如上所讲,我不喜欢来一堆堆云里雾里虚无的论述,“Talk is cheap, show me the code”,先来一段代码(本来想用Java,但Java样板代码似乎多了些,所以还是用Python)。

比如你有一个鸡蛋,你想煎个鸡蛋,你用OO的思路,恐怕首先会想到定义个抽象“蛋”(父类或接口)方便日后扩展,这么写:

from abc import ABCMeta, abstractmethod


class IEgg():
    __metaclass__ = ABCMeta

    @abstractmethod
    def fry(self):
        pass

然后来个鸡蛋子类:

class HensEgg(IEgg):
    def __init__(self, egg):
        self.egg = egg

    def fry(self):  
        print('煎{}。'.format(self.egg))

如果你还想蒸蛋,那么要继续在IEgg中加个steam方法,或者如果你只对鸡蛋才用“蒸”的方式,那就直接加在HensEgg拉倒。接下来,你还有鸭蛋、鹅蛋、鸽子蛋、鹌鹑蛋等等等等,但处理都类似如上方式,你想想麻烦不?有点吧?而且,如果接口或父类的哪个方法的形式稍加改变,子类也得跟着调整,再如果你加个属性、删个属性啥的,子类是不是也可能要跟着调整呢?

进一步想一想问题出在哪儿呢?我们吃的蛋不管有几种类型,也都是属于“蛋”这一个类型。主要是,蛋的烹饪手法花样百出,煎、蒸、炒、烧汤、卤、茶叶蛋......数都数不过来,光是煎蛋就有单面煎、双面煎,还有煎三分熟、半熟、七分熟......ಥʖ̯ಥ。

那么,用OO的方式真的有点累。我不妨换个角度,既然蛋的烹饪手法如一百单八式少林长拳,我就针对蛋这个东西写函数就好了,需要一个写一个(而且不管什么蛋,煎蛋是一样的处理程序,炖蛋也是这样),高效而且改动只需针对每个函数!其实这个概念的运用大家也许见过。嗯,有些语言是提供了比较好的扩展支持的,如Kotlin、Python,它们都是为了解决继承或实现中的问题,变着法子来减少对既有结构的冲击,但又能添加额外特性,其实这时候骨子里都觉得:“我现在只想快捷地增加一个函数/方法,别让我费劲啊!”但这些方式背后的实现,很多时候的出发点也都是为了“用一时千辛万苦换得日后的一点点简单”、“用我的辛苦造轮子,让你得到解脱”。

来看看Kotlin的extension functions做法,如果我想把列表中的字符串都转换为大写,但列表对象本身不支持这个方法,我如果要去改这个列表类那就有点过了,所以,可以考虑扩展函数这个变通方式。但如前所述,其实我想要的只是一个处理函数!

fun MutableList<String>.toUpperCase() {
    this.mapIndexed { k, v -> this[k] = this[k].toUpperCase() }
}

Python中甚至也可以这样:

h = HensEgg('Egg')
h.hello = lambda x: print('x:{x},egg:{egg}。'.format(x=x, egg=h.egg))
h.hello("添加的参数")

接下来,换个例子继续我们前进的脚步。

你的工具箱有大大小小各式各样的镙丝刀、扳手,你需要为各种工具定义旋开、旋紧两种方法。

好了,这时候你按上述方式,定义个接口,放上tightenunscrew,然后在子类中具体实现个性功能,像大扳手有时候还能用作“锤子”来钉钉子,那如果你需要这个功能,就完全可以在扳手类中添加一个“pound”方法。这时候不会发生继承或实现链上的大范围修改,最常见的操作就是在现有抽象中增加实现方法。同时,借助OO,把代码梳理成了层次结构,显得井井有条。

总结:

  • 业务功能比较明确,如上面工具箱的例子,用OO不失为一种好选项,因为层次分明,抽象良好,且层次上的侵入性与不确定性较小。——难点就在于复杂业务场景中功能需求的不确定性。
  • 如果业务实体对象较确定,但是其行为属“野球拳”难以揣测,比如上面的“蛋”的例子,就是一个蛋,但就看你怎么各显烹饪的神通,则用函数思维会比较愉快,用OO则不容易保证结构的稳定性。这时候只需要为“蛋”添加各种实现函数。
  • 但系统设计与开发永远没有银弹。业务功能不确定,业务实体范围也模糊,这是常态——也就造成了各种争论,包括“是OO还是FP”。这只能深入分析了,但仍需按上述两点来分析脉络,一条线是功能的归类,一条线是业务实体的抽象。

注意,上面内容中说的函数,不是常规下所谓FP的纯函数、引用透明等等的概念,只是突出一种方法,从函数的角度来打通问题域->解决域,不要事事都从OO的角度来思考问题。而且在OOP的实践中,也是可以借助这种“函数”角度来解决问题的,比如上述提到Kotlin、Python的扩展函数。

所以“OO”与“函数”,本身是思维的两种模式,而且并非非此即彼的关系。所以我也认为,函数式编程不要纠结于它“函数式”纯度有多高才算是FP编程(如引用透明、纯函数、不可变性啥的),头脑里接受“‘函数’是一种思维模式和方法,而OO亦然”的概念,才是最重要的。

一个语言写个“Hello World”程序也要定义一堆类的样板代码,这是这个语言的选择。但如“蛋”的例子,在Java里,你可以封装成一个Utils工具库,你把这个工具库里定义的各种方法理解成一个个“函数”来运用就是了,接受这种概念,反过来也更有助你接受FP编程不是抽象的,不是在Java里就不能体现函数式编程风格的——比如Guava。

简而言之,FP编程,不是就看“函数式”纯度,你随时都可以用其理念来设计程序,这才是真正意义上我们需要的函数式编程。只不过,大多数FP语言都提供了一大堆“伴手礼”功能,什么高阶函数、模式匹配、甚至map/reduce(不可否认的是,纯函数、状态不变性等,确实能标榜出FP的更深特质);而OOP更提倡可变状态的封装。所以让开发者觉得它俩是如此迥异并加以严格区分,甚至觉得模式匹配啥的就是函数式的特性。不是这样的。你说Java不能实现模式匹配吗?Python和JS不能实现Currying化吗?都可以。但Python、JS的参数、变量不可变吗?当然不是。

理解了上述内容,就不会有微博里的疑问和大家的争论了。如果现代语言都在极力打通OOP与FP的融合,那么我们还在争论“OOP和FP哪个更好”,有何意义?


(0)

发表评论请先登录或注册