大话重构 之 原来反OO天天见
在OO(面向对象)时代长大的小伙伴们一定记得:
面向对象的基石:把数据和依赖该数据的行为封装在一起。
但我们经常遇到一个类依赖其它类的数据的情况。不多的话,正常,对象间势必存在交互,毕竟完全独立的类无法构建出复杂的业务系统。
太多依赖外部数据的话,可能是问题,也可能不是问题,而是故意为之。嗯?这不是反OO吗?莫急,先来看看两个例子,然后分析隐藏在后面的东西。
特性依恋
先看太多外部数据依赖是问题的情况,重构里面管这叫 特性依恋 。顾名思义,太过迷恋别人的东西。
case class Product(name: String, price: Float)
case class OrderItem(count: Int, product: Product)
case class Order(items: List[OrderItem]) {
def cost: Float = {
items.sum(item => item.count * item.product.price)
}
}
每个订单项的花销之和,就是订单的花销。问题异常明显,订单项的花销是在订单层次计算的,导致订单过度依赖订单项的数据。
case class OrderItem(count: Int, product: Product) {
def cost = count * product.price
}
case class Order(items: List[OrderItem]) {
def cost = items.sum(_.cost)
}
订单项的花销,订单项自己计算,订单的花销是所有订单项花销之和。代码比说明书清楚多了,OK。
行为构建在数据之上,对象作为载体封装二者。从上面的例子可以看出,不能错位,属于订单项的行为就不要放在订单里面,如此才能提高代码的可维护性和可重用性。
到目前为止,OO的世界依然和谐美好。
如此熟悉的反OO:访问者模式
再来一例。
case class Car(engine: Engine, body: Body, wheels: List[Wheel]) {
def engineerCheck() {
check(enigne)
check(body)
wheels.foreach(check(_))
}
def washerWash() {
wash(body)
wheels.foreach(wash(_))
}
}
一辆车有一个引擎,一个车身,几个轮子。出厂/维修/保养的时候都需要找工程师检查,洗车的时候需要找洗车工清洗。工程师检查的行为一定是针对汽车的各组件,洗车工也是清洗的各汽车组件,行为和数据在一起组成对象,从OO的角度看,没啥问题。
如果来了一个外星人,以前没见过地球的汽车,觉得新奇,准备自己反向工程一辆,那简单:
case class Car(engine: Engine, body: Body, wheels: List[Wheel]) {
...
def alienReverseEngineering() {
reverseEngineering(enigne)
reverseEngineering(body)
wheels.foreach(reverseEngineering(_))
}
}
小伙伴们发现没?汽车已经无辜到要关心外星人,职责太特么不单一了,即使它没有违反OO。重构的解决方案就是 访问者模式 ,把工程师/洗车工/外星人干的事情从汽车里面剥离出来。
trait Element {
def accept(v: Visitor)
}
class Engine extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
class Body extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
class Wheel extends Element {
def accept(v: Visitor) {
v.visit(this)
}
}
case class Car(engine: Engine, body: Body,
wheels: List[Wheel]) {
def accept(v: Visitor) {
engine.accept(v)
body.accept(v)
wheels.foreach(accept)
}
}
Elment代表的是需要被访问的元素,本例中就是汽车的各组件。Car容纳了所有组件,并隐藏组件间的结构。
trait Visitor {
def visit(engine: Engine)
def visit(body: Body)
def visit(wheel: Wheel)
}
class Engineer extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
class Washer extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
class Alien extends Visitor {
def visit(engine: Engine) = { ... }
def visit(body: Body) = { ... }
def visit(wheel: Wheel) = { ... }
}
Visitor是所有对Car感兴趣的人,以及他们会对Car发生的行为。
Element/Car是数据,而Visitor是行为,访问者模式使得你可以在不修改Car的组件及结构的情况下,通过Visitor的方式定义新的行为。
细心的小伙伴们已经发现了,其实访问者模式分离了数据和行为,反OO了。
反不反OO呢?
一会支持OO,一会反OO,以后咋做设计呢?
如果一码说设计是门艺术,需要根据实际情况仔细权衡,小伙伴们一定会在心里使劲骂,说了句废话。
那一码不说虚的,来分析点实在的东西。既然两个例子无法在OO上达成一致,那咱往后退一层,来看看更基础的原则 单一职责 和 不要重复 。
对于订单一例,只有把订单项的数据和行为(开销)放在一起,才算系统里面对一个概念的解释只在一处存在,满足 不要重复 的原则。对于汽车一例,只有把易于变化的行为和稳定的数据结构分离,才能做到一个个独立的职责 汽车/工程师/洗车工/外星人,才能做到易于维护和扩展。
能够把上面这一点想通,其实只是个开始而已。一码个人觉得,对于代码层面的设计而言:
- 软件设计的基本原则是道,如:单一职责,不要重复,依赖倒置等
- 范式及其背后的模式是术,如:面向对象及设计模式,函数式编程及Monads,泛型编程,元编程等
从代码设计的角度看,如果你会C#,那么不要再去学Java(反之亦然),而应该去学学Scheme的函数式编程,Ruby的元编程。只有掌握不同的术,才能让道逐渐丰满,也才能为具体问题找到最合适的设计方案。