JavaWeb可擴充套件性之模板方法的運用
服務編織時用模板方法模式是一種非常實用技巧,通過模板方法定義出服務基本操作、日誌、異常處理等,也方便做限流、報警、流量統計等。這裡的可擴充套件性體現在,當需要實現新新增的服務時,只需要套用模板,實現差異點就可以了。當然模板對可擴充套件點的定義和粒度都會影響具體的效果。
以API服務的實現為例,實現一個簡單模板,有基本的日誌、異常處理,程式碼如下:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; // API服務實現模板 public abstract class ApiServiceTemplate<R, P> { protected Logger logger = LoggerFactory.getLogger(getClass()); // 業務邏輯入口 protected abstract R process() throws BizException; // 介面引數 protected abstract P getParam(); // 模板執行入口 public Response<R> execute() { long startTime = System.currentTimeMillis(); logger.info("Enter service, params:{}", getParam().toString()); try { R result = process(); return new Response<R>(SystemCode.BIZ_SUCCESS, result); } catch (BizException e) { logger.error("Api sevice execute Bizexception", e); return new Response<R>(SystemCode.BIZ_FAILED, null); } catch (Throwable e) { logger.error("Api sevice execute error", e); return new Response<R>(SystemCode.BIZ_FAILED, null); } finally { long totalTime = System.currentTimeMillis() - startTime; if (totalTime > 200) { logger.warn("This method used too long time, please check and optimize it:{}ms", totalTime); } } } } // 其中Response、SystemCode和BizException的程式碼我刪掉了,只突出重點
針對一些具體的業務流程,模板方法的作用更明顯,業務的具體實現是應用的核心程式碼,這部分可複用的價值很高,但相應地,問題也很多。主要有兩點:
問題一:合作開發成本高。別人實現的帶有強烈主觀意願的設計思路,往往都不那麼好理解。且不好的設計尤其是各種“專屬設定”往往會隱藏著坑!我曾經接手一個應用,服務實現對流程和操作抽象度很高,甚至BO都是繼承了N層的介面,結果來來回回看好幾遍都不清楚層次結構,每層的定位是什麼。
問題二:盲目複用和多層複用會導致程式碼後期維護非常複雜。看下面這個吃牛肉的例子:
/** * Created by song on 2019/1/5. */ // 錯誤示範 public interface Eat { // 吃牛肉 public void eatBeaf(); } // 一般吃法,作為基本實現 abstract class NormalEat implements Eat{ public void eatBeaf() { // 第一步,做熟 cookBeaf(); // 第二步,吃牛肉 eatTheBeaf(); } protected void cookBeaf() { System.out.println("roast beaf"); } protected void eatTheBeaf() { System.out.println("eat with hand"); } } // 西式吃法 class WestEat extends NormalEat{ @Override protected void eatTheBeaf() { System.out.println("eat with knife & folk"); } } // 東方吃法 class EestEat extends NormalEat{ @Override protected void cookBeaf() { System.out.println("hot pot beaf"); } @Override protected void eatTheBeaf() { System.out.println("eat with chopsticks"); } } // 中式吃法 class ChineseEat extends NormalEat{ @Override protected void cookBeaf() { super.cookBeaf(); // 中式吃法加很多辣椒 System.out.println(" add plenty of chilli"); } } // 日式吃法 class JapaneseEat extends EastEat{ @Override protected void eatTheBeaf() { talkBeforeEat(); super.eatTheBeaf(); } protected void talkBeforeEat() { // 日本人吃之前先說:"我開動了" System.out.print("いただきます"); } } // 日本北海道吃法 class JapaneseBhdEat extends JapaneseEat{ @Override protected void talkBeforeEat() { // 北海道人吃之前不說"我開動了"(我胡謅的) } } // 日本北海道札幌市吃法 class JapaneseBhdZhEat extends JapaneseBhdEat{ @Override protected void cookBeaf() { super.cookBeaf(); // 煮牛肉火鍋加鹹魚 System.out.println("add salt fish"); } }
例子中講牛肉的吃法,最基礎實現NormalEat中定義了基本實現分兩步:做牛肉cookBeaf和吃牛肉eatTheBeaf,到“日本北海道”吃法中,由於複用了父類JapaneseEat的eatTheBeaf和祖父類EastEat的cookBeaf,想知道北海道是怎麼吃的,就需要一層層查上去。這是個非常簡單的例子,實際應用中的業務場景會複雜的多,層數如果很多,而中間層又有很多差異化的“共性實現”,那麼將是一個災難,這樣一層層整合下去,結果就是北海道札幌市鬼子村的鬆尾桑在做牛肉和吃牛肉之間做了什麼,沒有人能說清楚,也不知道他有沒有說“我開動了”!
那麼怎麼解決這個問題呢?
1)不能分層太多,最多不能超過3層,比如北海道札幌市鬼子村的鬆尾桑吃牛肉的方法,就用一個和北海道平級的實現,雖然這一層的實現變多了,但很清晰,最多找兩層就清楚所有實現了。
2)一些共用實現方法從模板裡拆出來,作為獨立完整的小服務,調用出只關心出入口。這樣既使程式碼結構清晰,又使得服務的實現更加穩定且便於維護擴充套件。
對上例做修改如下:
// 再來一頓,改進版
public interface EatAgain {
// 吃牛肉
public void eatBeaf();
}
// 服務模板抽象
abstract class AbsEatAgain implements EatAgain{
// 做牛肉方法
public abstract CookBeafHandler getCookBeafHandler();
// 吃牛肉方法
public abstract EatTheBeafHandler getEatTheBeafHandler();
public void eatBeaf() {
// 第一步,做熟
getCookBeafHandler().doIt();
// 第二步,吃牛肉
getEatTheBeafHandler().doIt();
}
}
// 做牛肉方法
interface CookBeafHandler {
// 做牛肉
public void doIt();
}
// 吃牛肉方法
interface EatTheBeafHandler {
// 吃牛肉方法
public void doIt();
}
// 一般吃法,作為基本實現
class NormalEat extends AbsEatAgain{
public CookBeafHandler getCookBeafHandler() {
return new BaseCookBeafHandler();
}
public EatTheBeafHandler getEatTheBeafHandler() {
return new BaseEatTheBeafHandler();
}
}
// 西式吃法
class WestEat extends NormalEat{
public EatTheBeafHandler getEatTheBeaf() {
return new KnifeFolkEatTheBeafHandler();
}
}
// 東方吃法
class EestEat extends NormalEat{
public CookBeafHandler getCookBeafHandler() {
return new HotPotCookBeafHandler();
}
public EatTheBeafHandler getEatTheBeafHandler() {
return new EatWithChopsticksHandler();
}
}
// 中式吃法
class ChineseEat extends NormalEat {
public CookBeafHandler getCookBeafHandler() {
return new ChilliCookBeafHandler();
}
}
// 日式吃法
class JapaneseEat extends NormalEat {
public EatTheBeafHandler getEatTheBeafHandler() {
return new TalkAndEatTheBeafHandler();
}
}
// 日本北海道吃法
class JapaneseBhdEat extends NormalEat {
// 可以直接用NormalEat,因為跟基本吃法是一樣的
}
// 日本北海道札幌市吃法
class JapaneseBhdZhEat extends NormalEat {
public CookBeafHandler getCookBeafHandler() {
return new SaltFishCookBeafHandler();
}
}
所有的Handler:
// 基本做牛肉方法
class BaseCookBeafHandler implements CookBeafHandler{
public void doIt() {
System.out.println("roast beaf");
}
}
// 基本吃牛肉方法
class BaseEatTheBeafHandler implements EatTheBeafHandler{
// 吃牛肉
public void doIt() {
System.out.println("eat with hand");
}
}
// 吃牛肉方法:刀叉吃法
class KnifeFolkEatTheBeafHandler extends BaseEatTheBeafHandler{
// 吃牛肉方法
public void doIt() {
System.out.println("eat with knife & folk");
}
}
// 做法:火鍋牛肉
class HotPotCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
System.out.println("hot pot beaf");
}
}
// 吃牛肉方法:用筷子吃
class EatWithChopsticksHandler extends BaseEatTheBeafHandler {
public void doId() {
System.out.println("eat with chopsticks");
}
}
// 做法:加辣椒的做法
class ChilliCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
super.doIt();
System.out.println("add plenty of chilli");
}
}
// 吃牛肉方法:說完再吃
class TalkAndEatTheBeafHandler extends BaseEatTheBeafHandler {
public void doId() {
System.out.print("いただきます");
super.doIt();
}
}
// 做法:加鹹魚做法
class SaltFishCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
super.doIt();
System.out.println("add salt fish");
}
}
上述程式碼中,有兩項重要調整:一是將模板中的兩個重要節點“做牛肉”和“吃牛肉”方法拆出來,建立專門介面和實現,這樣做的好處是這些功能點完全獨立,可複用性更好,並且作為獨立完整的小模組功能可以更強大和穩定;二是不再出現超多層繼承,服務的每個實現都很清晰做了哪些事。相對來說,雖然比之前的實現多了很多Handler,並且沒有做到“只要能複用就複用”,但層次會清晰很多,擴充套件和管理更方便。
現在如果北海道札幌市鬼子村的鬆尾桑想要吃牛肉,那麼只需要新增一個SongWeiEat繼承AbsEatAgain,並實現繼承BaseCookBeafHandler和BaseEatTheBeafHandler的Handler就可以了。