大话重构 之 原来反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的元编程。只有掌握不同的术,才能让道逐渐丰满,也才能为具体问题找到最合适的设计方案。

推荐

消除过长方法

消除过长类

消除重复代码

答粉丝问

你的参数列表像蚯蚓一样让人厌恶吗

职责单一原则真的简单吗

防止“加个需求,到处改代码”

图片二维码

posted @ 2015-07-23 01:02  一码  阅读(1747)  评论(2编辑  收藏  举报