神奇SELF-TYPE:让你的类更精简的一种方式

2022-07-21 13:45:30 浏览数 (1)

本来标题名想取 神奇SELF-TYPE:继承,Mixin和对象组合之外的类交互方式的,但是发现不容易理解,找了半天,觉得还是现在的标题好

我们经常会把一个类写的很大,因为我们要完成的任务非常多。但是一个类过于庞大,往往会有巨大的维护成本。 所以面向对象编程引入多个类来将单个类拆解,从而使得代码的组织变得更加优雅,但这也引入了一个新的问题,就是,如何让这些类进行协作交互。我们首先会想到如下两个:

  1. 继承
  2. Mixin(拥有方法的Trait)

接着,我们还会有下面的办法:

  1. 静态工具类(本质是以函数为粒度封装,然后通过一个object/static class 来进行管理)
  2. 工具对象,在使用时new出来,然后调用里面的逻辑。

其中,继承和mixin可以将被继承的类和被mixin的类的成员(变量以及方法)引入到继承者身上,好处是可以方便的在主类里访问到这些方法,而静态工具类和工具对象,则更加独立,复用程度也更好,缺点是成员可见性问题,会使得方法签名或者对象实例属性变得很复杂。

下面我们一个一个来看。

代码语言:javascript复制
class A(v1:String,v2:String) {
   def complexFun()={
     val v11 = process1(v1)
     val v12 = process2(v1,v2)
     compose(v11,v12)
   }
}

process1/process2/compose 三个方法里的逻辑都可以放到A里,不过假设他们逻辑其实非常复杂,而且其他地方也会需要用到,所以这个时候,我们可以将其抽取为静态工具类

代码语言:javascript复制
object Process1 {
  def process1(v1:String) .... 
}
object Process2 {
  def process2(v1:String) .... 
}

class A 需要改成如下的样子了:

代码语言:javascript复制
class A(v1:String,v2:String) {
   def complexFun()={
     val v11 = Process1.process1(v1)
     val v12 = Process2.process2(v1,v2)
     compose(v11,v12)
   }
}

现在看起来一切都很好,但是如果process2需要很多东西,事情就会变得复杂了,参数变得很多就会很难受。

代码语言:javascript复制
class A(v1:String,v2:String) {
   val v2 = ...
   private def v3 = ...
   .....
   def complexFun()={
     ....
     val v12 = Process2.process2(v1,v2,v3,v4,v5,v6)
     ....
   }
}

这个时候,我们可以抽象成对象,部分变成实例变量,部分变成参数来减少这种难受:

代码语言:javascript复制
class A(v1:String,v2:String) {
   val v2 = ...
   private def v3 = ...
   .....
   def complexFun()={
     ....
    val p2 = new Process2(v1,v2)
     val v12 = p2.process2(v3,v4,v5,v6)
     ....
   }
}

但是,这个时候A又加了一个变量v7,我们也需要在Process2里访问这个变量,你会觉得太麻烦了,还要纠结放到实例变量还是方法参数里。索性将A作为变量传递进去:

代码语言:javascript复制
class A(v1:String,v2:String) {
   val v2 = ...
   private def v3 = ...
   .....
   def complexFun()={
     ....
    val p2 = new Process2(this)
     val v12 = p2.process2()
     ....
   }
}

舒服很多了,但是你会发现在Process2里,没办法访问A的private 函数v3,于是你不得不修改他的范围,就是为了能够在Process2里去访问v3。而且,你的Process2不再变得那么复用了,他被绑定到了A中,为了使用Process2,你必须实例化一个A,并且确保A里的东西都能被Process2所访问到。 如果我们直接继承Process2,则避免了这个麻烦。

代码语言:javascript复制
class A(v1:String,v2:String) extends  Process2{
   val v2 = ...
   private def v3 = ...
   .....
   def complexFun()={
     ....
    process2(v1...v7)
     ....
   }
}

但是问题来了,我们没办法在Process2的方法里访问A的变量,因为Process2对A 一无所知,于是我们又回到了通过参数传递变量的方法里去了。

这个时候,我们希望能够找到一种更好的类组织方式,我们希望能够把代码分门别类的放到不同的类里面,但是他们能够自由的访问住类的变量,使用起来看起来就像一个类一样,避免复杂方法或者实例调用。 Scala 提供这种问题的解决方案,叫Self-Type,极大的简化了代码的组织。

代码语言:javascript复制
class DeltaLog private(
    val logPath: Path,
    val dataPath: Path,
    val clock: Clock)
  extends Checkpoints
  with MetadataCleanup
  with LogStoreProvider
  with VerifyChecksum {

我们看这个例子,我们定义了三个实例变量,然后这是一个日志操作类,他的核心功能是将日志记录按一定的逻辑记录下来,但是不可避免,你需要有元数据清理功能,你需要一些类支持将我们的数据写到对应的存储上,你还需要校验一些校验码,甚至做一个checkpoint,这些逻辑都需要访问到DeltaLog主类的一些方法和变量(比如logPath,dataPath等等),而DeltaLog主类也需要访问到这些额外功能里的方法和变量。我们知道继承只能满足单向”可见“,也就是deltaLog可以看到如MetadataCleanUp的所有方法和变量,反之MetadataCleanUp 则看不到deltaLog的变量和方法。Scala 通过一个神奇的语法让这个变得可能:

代码语言:javascript复制
trait MetadataCleanup {
  self: DeltaLog =>

这里,我们在trait的第一行,添加了self:DetaLog => ,表示MetadataCleanup其实就是一个DeltaLog实例的一部分。现在,在MetadataCleanUp里就可以访问DeltaLog里的变量和方法了:

代码语言:javascript复制
trait MetadataCleanup {
  self: DeltaLog =>

   def doLogCleanup(): Unit = {
     delelte(logPath)//访问logPath变量 
  }
}

Scala的这种模式,可以很好的将一个大类拆分成N个小类(trait),并且还非常好的解决了他们之间的双向可见性。非常优秀的设计。

0 人点赞