場景
以一個簡化了的使用者登入的鑑權流程,流程大體如下:
-
首先嚐試本站鑑權,如果失敗,再嘗試
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);
}
解決思路
login
或twiterLogin
生產User
物件,扮演生產者的角色;而twoFactor
消費User
物件,扮演消費者的角色。
最難處理的就是login
或twiterLogin
可能會存在異常處理。正常情況下它們生產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();
}
其中,Success
與Failure
包內私有,對外不公開。
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的本質
Try
是Monad
的一個應用,使得異常的處理可以在流水線上傳遞。使用Scala
的Try
簡化實現版本是這樣的。
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)
}
}
}