在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的世界依然和谐美好。
再来一例。
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上达成一致,那咱往后退一层,来看看更基础的原则 单一职责 和 不要重复 。
对于订单一例,只有把订单项的数据和行为(开销)放在一起,才算系统里面对一个概念的解释只在一处存在,满足 不要重复 的原则。对于汽车一例,只有把易于变化的行为和稳定的数据结构分离,才能做到一个个独立的职责 汽车/工程师/洗车工/外星人,才能做到易于维护和扩展。
能够把上面这一点想通,其实只是个开始而已。一码个人觉得,对于代码层面的设计而言:
从代码设计的角度看,如果你会C#,那么不要再去学Java(反之亦然),而应该去学学Scheme的函数式编程,Ruby的元编程。只有掌握不同的术,才能让道逐渐丰满,也才能为具体问题找到最合适的设计方案。
消除过长方法
消除过长类
消除重复代码
答粉丝问
你的参数列表像蚯蚓一样让人厌恶吗
职责单一原则真的简单吗
防止“加个需求,到处改代码”