[翻牆轉]Using Scala Implicits to Replace Mindless Delegation

海興發表於2013-02-12

http://jackcoughonsoftware.blogspot.com/2008/11/using-scala-implicits-to-replace.html

I'm still refactoring my Scala CPU Simulator code, as I keep finding ways to just lop off piles of unneeded code. In this latest example, I was using pretty standard Java style delegation - having my class implement an interface, having a member variable of that interface, and delegating all method calls on that interface to the member variable.

There are several problems with this:

  1. The main problem: if I add a method to the interface I then have to add it to everyone implementing the interface. But, what If I'm not actually writing the implementation of the interface? Client code won't compile. Adding an implicit doesn't remove this problem entirely, but it certainly works for plain old delegates.
  2. It's error prone. Maybe the method was a void, and my IDE added the signature for the method, so the code compiles, but I forgot to actually delegate to the member.
  3. It's just plain wordy and ugly.

To remove the boilerplate, all I needed to do was add an implicit conversion from my class to the interface, using the member variable. I'll show this below.

First, here is an example of the old, more wordy style:

trait PowerSource{
  def connect( p: PowerSource ): Unit
  def disconnect( p: PowerSource ): Unit
  def reconnect( p: PowerSource ): Unit
}

class DelegateToPowerSource( in: PowerSource ) extends PowerSource {
  def connect( p: PowerSource ) = in connect p
  def disconnect( p: PowerSource ) = in disconnect p
  def reconnect( p: PowerSource ) = in reconnect p
}

DelegateToPowerSource has a member, p, and implements the PowerSource interface by delegating to that member for each of the methods on the PowerSource interface. The more methods that PowerSource has, the longer DelegateToPowerSource gets, and yet the code is just boilerplate. Even if the IDE does this for you, its still wordy and potentially error prone.

Now for the new version:

trait PowerSource{
  def connect( p: PowerSource ): Unit
  def disconnect( p: PowerSource ): Unit
  def reconnect( p: PowerSource ): Unit
}

object DelegateToPowerSource{
  implicit def delegateToPowerSource( d: DelegateToPowerSource ) = d.p
}

class DelegateToPowerSource( p: PowerSource )

That's it. Now, any time I add a method to the PowerSource interface (I should probably start calling it trait), DelegateToPowerSource simply has that method; via the conversion. I never have to change DelegateToPowerSource because of a change to PowerSource. DelegateToPowerSource can simply have whatever code in it that it was originally intended to have, obviously augmenting PowerSource in some way.

For completeness, I'll post the actual code where I did exactly this. But, it's pretty much the same, so if you get the point, no need to keep reading.

trait LogicGate extends PowerSource{
  val inputA: PowerSource
  val inputB: PowerSource
  val output: PowerSource
}

abstract class BaseLogicGate(val inputA: PowerSource, inputB: PowerSource)
               extends LogicGate {

  def state = output.state

  def -->( p: PowerSource ): PowerSource = output --> p 
  def <--( p: PowerSource ): PowerSource = output <-- p 

  def disconnectFrom( p: PowerSource ): PowerSource = output disconnectFrom p 
  def disconnectedFrom( p: PowerSource ): PowerSource = output disconnectedFrom p 

  def handleStateChanged( p: PowerSource ) = {}
  def notifyConnections = {} 
}

class AndGate(a: PowerSource, b: PowerSource) 
  extends BaseLogicGate(a: PowerSource, b: PowerSource){
  val output = new Relay(a, new Relay(b))
}

Of course there were several other LogicGates (or, nor, nand, etc). All of this was condensed down to:

object LogicGate{
  implicit def logicGateToPowerSource( lg: LogicGate ): PowerSource = lg.output
}

trait LogicGate{
  val inputA: PowerSource
  val inputB: PowerSource
  val output: PowerSource
}

class AndGate(val inputA: PowerSource, val inputB: PowerSource) extends LogicGate{
  val output = new Relay(inputA, new Relay(inputB))
}

BaseLogicGate is now removed entirely. This is great because BaseLogicGate was really weird, it didn't even use its inputs. The only reason it was there was to decouple the delegation logic from the actual trait.

Now things have become MUCH more clear. AndGate is a LogicGate with inputs A and B, and one output, made from Relays.

相關文章