Jumping with Try

horance發表於2019-02-16

場景

以一個簡化了的使用者登入的鑑權流程,流程大體如下:

  • 首先嚐試本站鑑權,如果失敗,再嘗試twiter的方式恢復;

  • 之後再進行Two Factor認證;

快速實現

按照流程定義,可以快速實現第一個版本。這段程式碼充滿了很多的壞味道,職責不單一,複雜的異常分支處理,流程的脈絡不夠清晰等等,接下來我們嘗試一種很特別的重構方式來改善設計。

public boolean authenticate(String id, String passwd) {
  User user = null;    
  try {
   user = login(id, passwd);
  } catch (AuthenticationException e) {
    try {
      user = twiterLogin(id, passwd);
    } catch (AuthenticationException et) {
      return false;
    }
  }
    
  return twoFactor(user);
}

解決思路

logintwiterLogin生產User物件,扮演生產者的角色;而twoFactor消費User物件,扮演消費者的角色。

最難處理的就是logintwiterLogin可能會存在異常處理。正常情況下它們生產User物件,而異常情況下,則丟擲異常。

重構的思路在於將異常處理更加明晰化,讓生產者與消費者之間的關係流水化。為此,可以將User物件,可能丟擲的異常進行容器化,它們形成互斥關係,不能共生於這個世界。

容器化

public interface Try<T> {
  static <T, E extends Exception> 
  Try<T> trying(ExceptionalSupplier<T, E> s) {
    try {
      return new Success<>(s.get());
    } catch (Exception e) {
      return new Failure<>(e);
    }
  }
  
  boolean isFailure();
  
  default boolean isSuccess() {
    return !isFailure();
  }
  
  T get();
}

其中,SuccessFailure包內私有,對外不公開。

final class Success<T> implements Try<T> {
  private final T value;
  
  Success(T value) {
    this.value = value;
  }
  
  @Override
  public boolean isFailure() {
    return false;
  }

  @Override
  public T get() {
    return value;
  }
}
import java.util.NoSuchElementException;

final class Failure<T> implements Try<T> {
  private final Exception e;
  
  Failure(Exception e) {
    this.e = e;
  }
  
  @Override
  public boolean isFailure() {
    return true;
  }

  @Override
  public T get() {
    throw new NoSuchElementException("Failure.get");
  }
}

生產者

JDK8標準庫中不一樣,它能處理受檢異常。

@FunctionalInterface
public interface ExceptionalSupplier<T, E extends Exception> {
  T get() throws E;
}

第一次重構

import static Try.trying;

public boolean authenticate(String id, String passwd) {
  Try<User> user = trying(() -> login(id, passwd));
  if (user.isFailure()) {
    user = trying(() -> twiterLogin(id, passwd));
  }
 
  return user.isSuccess() && twoFactor(user.get());
}

鏈式DSL

上述trying的應用,使用狀態查詢Try.isFailure/isSuccess方法顯得有些笨拙,可以通過構造鏈式的DSL改善設計。

public interface Try<T> {
  ......

  <U> Try<U> recover(Function<Exception, Try<U>> f);
}
final class Success<T> implements Try<T> {
  ......
   
  @Override
  @SuppressWarnings("unchecked")
  public <U> Try<U> recover(Function<Exception, Try<U>> f) {
    return (Try<U>)this;
  }
}
final class Failure<T> implements Try<T> {
  private final Exception e;
  
  Failure(Exception e) {
    this.e = e;
  }
   
  @Override
  public <U> Try<U> recover(Function<Exception, Try<U>> f) {
    try {
      return f.apply(e);
    } catch (Exception e) {
      return new Failure<U>(e);
    }
  }
}

第二次重構

使用recover關鍵字,進一步地改善表達力。首先試圖login生產一個User,如果失敗從twiterLogin中恢復;最後由twoFactor消費User物件。

public boolean authenticate(String id, String passwd) {
  Try<User> user = trying(() -> login(id, passwd))
      .recover(e -> trying(() -> twiterLogin(id, passwd)));    
    
  return user.isSuccess() && twoFactor(user.get());
}

徹底鏈化

public interface Try<T> {
  ......

  default T getOrElse(T defaultValue) {
    return isSuccess() ? get() : defaultValue;
  }
    
  <U> Try<U> map(Function<T, U> f);
}
final class Success<T> implements Try<T> {
  ......
  
  @Override
  public <U> Try<U> map(Function<T, U> f) {
    try {
      return new Success<U>(f.apply(value));  
    } catch (Exception e) {
      return new Failure<U>(e);
    }
  }
}
final class Failure<T> implements Try<T> {
  ......

  @Override
  @SuppressWarnings("unchecked")
  public <U> Try<U> map(Function<T, U> f) {
    return (Try<U>)this;
  }
}

第三次重構

public boolean authenticate(String id, String passwd) {
  return trying(() -> login(id, passwd))
    .recover(e -> trying(() -> twiterLogin(id, passwd)))
    .map(user -> twoFactor(user))
    .getOrElse(false);
}

應用Scala

Java8 Lambda表示式() -> login(id, passwd)中空的引數列表頗讓人費解,但這是Java8語言本身限制的,應用Scala表達力可進一步提高。

import scala.util.Try

def authenticate(id: String, passwd: String): Boolean = {
  Try(login(id, passwd))
    .recover{ case e: => twiterLogin(id, passwd) }
    .map(twoFactor)
    .getOrElse(false)
}

Try的本質

TryMonad的一個應用,使得異常的處理可以在流水線上傳遞。使用ScalaTry簡化實現版本是這樣的。

sealed abstract class Try[+T] {
  def isSuccess: Boolean
  def get: T
}

final case class Failure[+T](val exception: Throwable) extends Try[T] {
  def isSuccess: Boolean = false
  def get: T = throw exception
}


final case class Success[+T](value: T) extends Try[T] {
  def isSuccess: Boolean = true
  def get = value
}

object Try {
  def apply[T](r: => T): Try[T] = {
    try Success(r) catch {
      case NonFatal(e) => Failure(e)
    }
  }
}

相關文章