天天看點

神奇SELF-TYPE:讓你的類更精簡的一種方式

本來标題名想取 神奇SELF-TYPE:繼承,Mixin和對象組合之外的類互動方式的,但是發現不容易了解,找了半天,覺得還是現在的标題好

我們經常會把一個類寫的很大,因為我們要完成的任務非常多。但是一個類過于龐大,往往會有巨大的維護成本。 是以面向對象程式設計引入多個類來将單個類拆解,進而使得代碼的組織變得更加優雅,但這也引入了一個新的問題,就是,如何讓這些類進行協作互動。我們首先會想到如下兩個:

  1. 繼承
  2. Mixin(擁有方法的Trait)

接着,我們還會有下面的辦法:

  1. 靜态工具類(本質是以函數為粒度封裝,然後通過一個object/static class 來進行管理)
  2. 工具對象,在使用時new出來,然後調用裡面的邏輯。

其中,繼承和mixin可以将被繼承的類和被mixin的類的成員(變量以及方法)引入到繼承者身上,好處是可以友善的在主類裡通路到這些方法,而靜态工具類和工具對象,則更加獨立,複用程度也更好,缺點是成員可見性問題,會使得方法簽名或者對象執行個體屬性變得很複雜。

下面我們一個一個來看。

class A(v1:String,v2:String) {
   def complexFun()={
     val v11 = process1(v1)
     val v12 = process2(v1,v2)
     compose(v11,v12)
   }
}           

複制

process1/process2/compose 三個方法裡的邏輯都可以放到A裡,不過假設他們邏輯其實非常複雜,而且其他地方也會需要用到,是以這個時候,我們可以将其抽取為靜态工具類

object Process1 {
  def process1(v1:String) .... 
}
object Process2 {
  def process2(v1:String) .... 
}           

複制

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

class A(v1:String,v2:String) {
   def complexFun()={
     val v11 = Process1.process1(v1)
     val v12 = Process2.process2(v1,v2)
     compose(v11,v12)
   }
}           

複制

現在看起來一切都很好,但是如果process2需要很多東西,事情就會變得複雜了,參數變得很多就會很難受。

class A(v1:String,v2:String) {
   val v2 = ...
   private def v3 = ...
   .....
   def complexFun()={
     ....
     val v12 = Process2.process2(v1,v2,v3,v4,v5,v6)
     ....
   }
}           

複制

這個時候,我們可以抽象成對象,部分變成執行個體變量,部分變成參數來減少這種難受:

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作為變量傳遞進去:

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,則避免了這個麻煩。

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,極大的簡化了代碼的組織。

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 通過一個神奇的文法讓這個變得可能:

trait MetadataCleanup {
  self: DeltaLog =>           

複制

這裡,我們在trait的第一行,添加了self:DetaLog => ,表示MetadataCleanup其實就是一個DeltaLog執行個體的一部分。現在,在MetadataCleanUp裡就可以通路DeltaLog裡的變量和方法了:

trait MetadataCleanup {
  self: DeltaLog =>

   def doLogCleanup(): Unit = {
     delelte(logPath)//通路logPath變量 
  }
}           

複制

Scala的這種模式,可以很好的将一個大類拆分成N個小類(trait),并且還非常好的解決了他們之間的雙向可見性。非常優秀的設計。