Refactoring to Functions

horance發表於2019-02-16

OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathers

Conditional Deferred Execution

日誌Logger

if (logger.isLoggable(Level.INFO)) {
  logger.info("problem:" + getDiagnostic());
}

這個實現存在如下一些壞味道:

  • 重複的樣板程式碼,並且散亂到使用者的各個角落;

  • logger.debug之前,首先要logger.isLoggablelogger暴露了太多的狀態邏輯,違反了LoD(Law of Demeter)

Eliminate Effects Between Unrelated Things.

Apply LoD

logger.info("problem:" + getDiagnostic());
public void info(String msg) {
  if (isLoggable(Level.INFO)) {
    log(msg)
  }
}

這樣的設計雖然將狀態的查詢進行了封裝,遵循了LoD原則,但依然存在一個嚴重的效能問題。無論如何,getDiagnostic都將得到呼叫,如果它是一個耗時、昂貴的操作,可能成為系統的瓶頸。

Apply Lambda

靈活地應用Lambda惰性求值的特性,可以很漂亮地解決這個問題。

public void log(Level level, Supplier<String> supplier) {
  if (isLoggable(level)) {
    log(supplier.get());
  }
}

public void debug(Supplier<String> supplier) {
  log(Level.DEBUG, supplier);
}

public void info(Supplier<String> supplier) {
  log(Level.INFO, supplier);
}

...

使用者的程式碼也更加簡潔,省略了那些重複的樣板程式碼。

logger.info(() -> "problem:" + getDiagnostic());

Apply Scala: Call by Name

在使用lambda時多餘的()顯得有點冗餘,可以使用by-name引數進一步提高表達力。

def log(level: Level, msg: => String) {
  if (isLoggable(level)) {
    log(msg)
  }
}

def debug(msg: => String) {
  log(DEBUG, msg)
}

def info(msg: => String) {
  log(INFO, msg)
}
logger.info("problem:" + getDiagnostic());

"problem:" + getDiagnostic()語句並非在logger.info展開計算,它被延遲計算直至被apply的時候才真正地被評估和計算。

Execute Around

我們經常會遇到一個場景,在執行操作之前,先準備環境,之後再拆除環境。例如XUnit中的setUp/tearDown;運算元據庫時,先取得資料庫的連線,運算元據後確保釋放連線;當操作檔案時,先開啟檔案流,操作檔案後確保關閉檔案流。

Apply try-finally

為了保證異常安全性,在Java7之前,常常使用try-finally的實現模式解決這樣的問題。

public static String process(File file) throws IOException {
  BufferedReader bf = new BufferedReader(new FileReader(file));
  try {
    return bf.readLine();
  } finally {
    if (bf != null) 
      bf.close();
  }
}

這樣的設計和實現存在幾個問題:

  • if (bf != null)是必須的,但常常被人遺忘;

  • try-finally的樣板程式碼遍佈在使用者程式中,造成大量的重複設計;

Apply try-with-resources

Java7,只要實現了AutoCloseable的資源類,可以使用try-with-resources的實現模式,進一步簡化上例的樣板程式碼。

public String process(File file) throws IOException {
  try(BufferedReader bf = new BufferedReader(new FileReader(file))) {
    return bf.readLine();
  }
}

但是,在某些場景下很難最大化地複用程式碼,這使得實現中存在大量的重複程式碼。例如遍歷檔案中所有行,並替換制定模式為其他的字串。

public String replace(File file, String regex, String i) throws IOException {
  try(BufferedReader bf = new BufferedReader(new FileReader(file))) {
    return bf.readLine().replaceAll(regex, replace);
  }
}

Apply Lambda

為了最大化地複用程式碼,最小化使用者樣板程式碼,將資源操作前後的程式碼保持封閉,使用lambda定製與具體問題相關的處理邏輯。

process使用BufferedProcessor實現行為的引數化。

public static String process(File file, BufferedProcessor p) throws IOException {
  try(BufferedReader bf = new BufferedReader(new FileReader(file))) {
    return p.process(bf);
  }
}

其中,BufferedProcessor是一個函式式介面,用於描述lambda的原型資訊。

@FunctionalInterface
public interface BufferedProcessor {
  String process(BufferedReader bf) throws IOException;
}

使用者使用lambda表示式,使得程式碼更加簡單、漂亮。

process(file, bf -> bf.readLine());

如果使用Method Reference,可增強表達力。

process(file, BufferedReader::readLine);

Apply Scala: Structural Type, Call by Name, Currying

為了最大化地複用資源釋放的實現,使用Scala可以神奇地構造一個簡單的DSL,讓使用者更好地實現複用。

Make it Easy to Reuse.

import scala.language.reflectiveCalls

object using {
  def apply[R <: { def close(): Unit }, T](resource: => R)(f: R => T) = {
    var res: Option[R] = None
    try {
      res = Some(resource)
      f(res.get)
    } finally {
      if (res != None) res.get.close
    }
  }
}

R <: { def close(): Unit }中泛型引數R是一個擁有close方法的型別;resource: => Rresource宣告為Call by Name,可延遲計算;apply使用了兩個引數,並進行了Currying化。

受益於Currying,使用者的定製的函式可以使用大括號來增強表達力,using猶如內建的語言特性,得到抽象了的控制結構。

using(Source.fromFile(file)) { source =>
  source.getLines 
}

因為引數source僅僅使用了一次,可以通過佔位符進一步增強表達力。

using(Source.fromFile(file)) { _.getLines }

相關文章