--- title: 'Stragety 策略 模式' disqus: kyleAlien --- Stragety 策略 模式 === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: > 定義一系列的演算法,並將每個一個**演算法封裝**,並 ==可**互相取代**== :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/) ::: [TOC] ## Stragety 使用場景 1. 對同一類型的問題有多種處理方式,多個 `if/else`,`switch/case` 判斷時 2. 需要**安全封裝多種 `同一類型` 的操作(介面)**,符合間接性、防變異原則(GRASP) 3. 策略模式的實作依附於 **語境物件**(`context object`),語境物件會將自身的引用傳遞給策略框架 ### Stragety 定義 & UML * **Stragety 定義**: 定義一組算法,並將其封裝起來,之後可以快速互換 * **Stragety UML**: 透過將共同方法定義在抽象,透過注入細節,實現相同方法 but 不同結果 | 角色 | 功能 | | -------- | -------- | | `Context` | 上下文角色,封裝 `IStragety`,屏蔽高層模塊對策略的直接訪問 | | `IStragety` | 共同方法的抽象接口 | | `ConstractStragetyA、B` | 實作類演算法類(細節實現) | > ![](https://hackmd.io/_uploads/HkeJoFoSn.png) :::success * 策略通常由工廠來創建,而這個工廠通常也是單例物件 ::: ### Stragety 設計 - 優缺點 * **Stragety 設計優點** * 結構清晰,簡單直觀,擴充方便 * 透過注入相對而言耦合度較低 * 不同演算法出現問題時,可以**分開維護**,也符合開閉原則 * **Stragety 設計缺點** * 隨著演算法的增加,實現子類會變多 * 另一個代價是,外部需要知道使用的策略!不符合迪米特原則(最少知識原則) ## Stragety 實現 :::info 假設去韓國旅行有三種方式,自助旅行、半自助旅行、旅行社,每種選擇方式所花費的金額也不一樣 ::: 最簡單直覺的寫法是使用 `if / else`、`switch / case` 去分類,但 **這種寫法不符合開閉原則**,每次新增一個旅遊金額算法,都需要修改 TravelCal 方法 ```java= public class TravelCal { public int getTravelCal(String type) { int result; if("Agency".equals(type)) { result = 300; } else if("SelfHelp".equals(type)) { result = 50; } else if("Backpacking".equals(type)) { result = 100; } else { result = -1; } return result; } } ``` ### Stragety 實現標準 1. **`IStragety` 介面**:宣告共同的抽象方法,以目前案例就是定義每一種旅行方式所需的花費 ```java= // IStragety.java public interface TravelCast { // 共同抽象 int calCast(); } ``` 2. **`ConstractStragety` 類**:定義細節,讓每一個方案去實作計算花費金額 ```java= // 旅行社 public class Agency implements TravelCast { @Override public int calCast() { return 300; } } // 自助 public class SelfHelp implements TravelCast { @Override public int calCast() { return 100; } } // 背包客 public class Backpacking implements TravelCast { @Override public int calCast() { return 50; } } ``` * User 使用:透過替換實作細節,來達到不同的金額 :::success * 開閉原則: 如果有新的旅行方案,同樣透過繼承 `TravelCast` 抽象來拓展,並不會觸碰到其他類 ::: ```java= public class TestStrategy { public static void main(String[] args) { TravelCast travelCast; travelCast = new Agency(); printCast(travelCast); travelCast = new SelfHelp(); printCast(travelCast); travelCast = new Backpacking(); printCast(travelCast); } public static void printCast(TravelCast travelCast) { System.out.println(travelCast.getClass().getSimpleName() + " Cast: " + travelCast.calCast()); } } ``` **--實作--** > ![](https://i.imgur.com/0Ymbbpm.png) ### 管理 Stragety 實作 - Context * 在最原始的 Stragety 實作,User 類會了解到細節 (TravelCast 的實作),這就不符合 `最少知識`、`依賴導致` 設計原則 * 這邊我們透過一個 `Manager 管理類`,透過外部注入 Option 選項,來決定最終要產生的花費 > 上面重複,沒有修改的類 (IStragety、ConstractStragety),這邊不會列出 > ![](https://hackmd.io/_uploads/ryBvatjBn.png) 1. **Manager**:新增一個管理類,聚合管理所有的 IStragety,並給予預設值 ```java= // Manager 類 public class TravelManager { enum Option { BACK_PACKING, SELF_HELP, AGENCY } // 聚合所有 TravelCast 抽象 private final Map<Option, TravelCast> map = new HashMap<>() { { put(Option.BACK_PACKING, new Backpacking()); put(Option.SELF_HELP, new SelfHelp()); put(Option.AGENCY, new Agency()); } }; private Option option; public TravelManager() { // 設定 default option = Option.AGENCY; } // 透過注入,來達到切換細節 public TravelManager setOption(Option option) { this.option = option; return this; } public void printCast() { TravelCast travelCast = map.get(option); if(travelCast == null) { throw new IllegalAccessError("setTravelCast first"); } System.out.println(travelCast.getClass().getSimpleName() + " Cast: " + travelCast.calCast()); } } ``` :::success * 這裡有另外一種方式叫做 **策略枚舉**,透過枚舉的封裝,可以加強可讀性 ::: 2. **User 使用**:**透過 Manager 來 ++管理細節++**,而 User 只需要知道 Manager 即可 ```java= // User 使用 public class TestStrategy { public static void main(String[] args) { TravelManager travelManager = new TravelManager(); travelManager.setOption(TravelManager.Option.AGENCY).printCast(); travelManager.setOption(TravelManager.Option.SELF_HELP).printCast(); travelManager.setOption(TravelManager.Option.BACK_PACKING).printCast(); } } ``` :::warning * **Manager(Context) 不符合開閉原則**: 雖然這裡透過 Manager 來管理 IStragety 所有細節,但如果有新增類,就需要手動修改 Manager 類 ::: :::success * 這裡還可以使用工廠、代理、享元... 等等模式來實做 Manager(依需求決定) ::: ## Android source 動畫 動畫透過人類的 **視覺暫留** 來達成,透過快速切換來達到動畫的效果 > 視覺暫留: 對於上個映像會暫時留在眼中 | 來源 | 每秒楨數 | | -------- | -------- | | 標準電影 | 24 楨/秒 | | Android 動畫 | 60 楨/秒 | ### 動畫 - [TimeInterpolator](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/animation/TimeInterpolator.java) 插值器 * 透過 `TimeInterpolator` 插值器,可以讓我們接收動畫的運作時間,透過時間的百分比來調整動畫的速度 * 下圖代表給予相同的時間,但不同插值器會返回不同結果,從這裡其實就可以看出 **Stragety 設計,不同的插值器代表了不同的算法** > ![](https://i.imgur.com/JHYACsq.png) * 常見 `Interpolator` 插值器如下 | 插值器 | 對動畫的功能 | | -------- | -------- | | [**LinearInterpolator**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/LinearInterpolator.java) | 線性加速 | | [**AccelerateDecelerateInterpolator**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/AccelerateDecelerateInterpolator.java) | 加速 & 減速 | | [**DecelerateInterpolator**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/DecelerateInterpolator.java) | 減速 | > ![](https://i.imgur.com/IXfzNa0.png) ### 動畫 - [TypeEvaluator](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/animation/TypeEvaluator.java) 類型估值器 * TypeEvaluator 的功能是,得到 TimeInterpolator 插值器的數值後,將得到的時間百分比、屬性起始值、目標值來運算最終結果,**將最終運算結果交給 View** ```java= // TypeEvaluator.java public interface TypeEvaluator<T> { // 泛型 public T evaluate(float fraction, T startValue, T endValue); } ``` * 常見的 `Evaluator` 類型估值器如下 | 插值器 | 對動畫的功能 | | -------- | -------- | | FloatEvaluator | 浮點數運算 | | IntEvaluator | 整數運算 | | ArgbEvaluator | 顏色運算 | ```java= // IntEvaluator.java public class IntEvaluator implements TypeEvaluator<Integer> { /** * fraction 是時間插值器運算出的值 (0 ~ 1 之間) */ public Integer evaluate(float fraction, Integer startValue, Integer endValue) { int startInt = startValue; return (int)(startInt + fraction * (endValue - startInt)); } } ``` > ![](https://i.imgur.com/yn4CBrl.png) ### Animation 基礎使用 1. 設定動畫 xml 檔案 ```xml= <!-- R.anim.click_btn --> <?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="300"> <scale android:fromXScale="0.5" android:fromYScale="0.5" android:pivotX="50%" android:pivotY="50%" android:toXScale="1.0" android:toYScale="1.0"/> </set> ``` 2. 使用 `AnimationUtils` 來讀取 xml 檔案 ```java= // 讀取 xml 資源檔 Animation animation = AnimationUtils.loadAnimation(this, R.anim.click_btn); ``` 3. 將 Animation 設定給 View ```java= Button btn = findViewById(R.id.btn); // Button 加載動畫 btn.startAnimation(animation); ``` :::info * startAnimation 是 View 的方法 ::: ### [View](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/View.java) - startAnimation 啟動動畫 ```java= // View.java public void startAnimation(Animation animation) { // 初始化動畫開始時間 animation.setStartTime(Animation.START_ON_FIRST_FRAME); // 對該 View 設定 Animation setAnimation(animation); // 刷新 Parent 緩存 invalidateParentCaches(); // 刷新 View 自身 invalidate(true); } // ------------------------------------------------------- // Animation.java public static final int START_ON_FIRST_FRAME = -1; ``` * 刷新後 ViewGroup 會透過 `dispatchDraw` 方法,對 View 的部分區域重繪,ViewGroup 重繪時會呼叫 ViewGroup#**drawChild** 方法 ```java= // ViewGroup.java protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // @ 追蹤 draw 方法 return child.draw(canvas, this, drawingTime); } // ------------------------------------------------------------- // View.java boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) { ... 省略部分 if (a != null) { // @ 追蹤 applyLegacyAnimation 方法 more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired); ... } else { ... 省略部分 } } ``` 1. applyLegacyAnimation 方法:初始化、標記 View 動畫啟動 ```java= // View.java @CallSuper protected void onAnimationStart() { // 標記該 View 已啟動 Animation mPrivateFlags |= PFLAG_ANIMATION_STARTED; } private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) { Transformation invalidationTransform; final int flags = parent.mGroupFlags; final boolean initialized = a.isInitialized(); // 檢查是否初始化 if (!initialized) { // 尚未初始化則初始化 a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight()); a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop); if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler); // 標記 View 啟動動畫 onAnimationStart(); } // 儲存動畫訊息 final Transformation t = parent.getChildTransformation(); // @ 追蹤 getTransformation 方法 // 獲取動畫相關值 boolean more = a.getTransformation(drawingTime, t, 1f); ... if (more) { if (!a.willChangeBounds()) { ... 省略部分 } else { if (parent.mInvalidateRegion == null) { parent.mInvalidateRegion = new RectF(); } final RectF region = parent.mInvalidateRegion; // 獲取重繪區域 a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region, invalidationTransform); // child 繪製的動畫可能螢幕之外,需要確保該刷新不會被取消 parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION; // 計算有效區域 final int left = mLeft + (int) region.left; final int top = mTop + (int) region.top; // ViewGroup 刷新指定區塊 parent.invalidate(left, top, left + (int) (region.width() + .5f), top + (int) (region.height() + .5f)); } } return more; } ``` 2. Animation#**getTransformation** 方法:**重點是計算動畫時間**,決定該動畫是否仍需要繪製,返回 true 代表要繪製動畫,如果 ```java= // Animation.java Interpolator mInterpolator; public boolean getTransformation(long currentTime, Transformation outTransformation, float scale) { mScaleFactor = scale; // @ 追蹤 getTransformation 方法 return getTransformation(currentTime, outTransformation); } public boolean getTransformation(long currentTime, Transformation outTransformation) { if (mStartTime == -1) { // 設定動畫啟動時間 mStartTime = currentTime; } // 該 Animation 總時間 final long startOffset = getStartOffset(); final long duration = mDuration; // 時間流逝的百分比 float normalizedTime; if (duration != 0) { normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / (float) duration; } else { // 動畫尚未啟動 (延遲) normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f; } // 動畫完成 or 取消,expired = true // 1.0 代表動畫時間已經過 final boolean expired = normalizedTime >= 1.0f || isCanceled(); mMore = !expired; ... // 動畫時間正在運作中 if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) { ... 省略部分 // 插值器 final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime); // @ 分析 applyTransformation 方法 applyTransformation(interpolatedTime, outTransformation); } ... if (!mMore && mOneMoreTime) { mOneMoreTime = false; return true; } return mMore; } ``` * 從這裡可以看到 [**Interpolator**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/Interpolator.java) 的身影,同時也可以發現 **使用者如果設定 ++不同 Interpolator 也會也不同的效果++** ```java= // Animation.java public Animation() { // 設定預設插值器 ensureInterpolator(); } public void setInterpolator(Interpolator i) { mInterpolator = i; } protected void ensureInterpolator() { if (mInterpolator == null) { // 預設插值器為 AccelerateDecelerateInterpolator mInterpolator = new AccelerateDecelerateInterpolator(); } } ``` > ![](https://i.imgur.com/4fcG8Sw.png) * 預設插值器為 [**AccelerateDecelerateInterpolator**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/AccelerateDecelerateInterpolator.java):這是加減速插值器 ```java= // AccelerateDecelerateInterpolator.java @HasNativeInterpolator public class AccelerateDecelerateInterpolator extends BaseInterpolator implements NativeInterpolator { public AccelerateDecelerateInterpolator() { } @SuppressWarnings({"UnusedDeclaration"}) public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) { } public float getInterpolation(float input) { // 加速插值器 return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } /** @hide */ @Override public long createNativeInterpolator() { // 減速插值器 return NativeInterpolatorFactory.createAccelerateDecelerateInterpolator(); } } ``` ### 里式原則應用 - applyTransformation * [**Animation**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/Animation.java)#applyTransformation 是一個空實現,給每一個繼承於它的子類實現細節,這就是理式原則的典型應用,父類可以任意地被子類替換 ```java= // Animation.java protected void applyTransformation(float interpolatedTime, Transformation t) { } ``` * 如過動畫 xml 有設定 alpha 值,則會對應到 [**AlphaAnimation 類**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/AlphaAnimation.java)、選轉則是對應到 [**RotateAnimation 類**](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/animation/RotateAnimation.java) ```java= // AlphaAnimation.java public class AlphaAnimation extends Animation { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { final float alpha = mFromAlpha; // 設定透明度 t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime)); } } // ----------------------------------------------------- // RotateAnimation.java public class RotateAnimation extends Animation { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float degrees = mFromDegrees + ((mToDegrees - mFromDegrees) * interpolatedTime); float scale = getScaleFactor(); if (mPivotX == 0.0f && mPivotY == 0.0f) { t.getMatrix().setRotate(degrees); } else { t.getMatrix().setRotate(degrees, mPivotX * scale, mPivotY * scale); } } } ``` ## 更多的物件導向設計 物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)! :::info * [**設計建模 2 大概念- UML 分類、使用**](https://devtechascendancy.com/introduction-to-uml-and-diagrams/) * [**物件導向設計原則 – 6 大原則(一)**](https://devtechascendancy.com/object-oriented-design-principles_1/) * [**物件導向設計原則 – 6 大原則(二)**](https://devtechascendancy.com/object-oriented-design-principles_2/) ::: ### 創建模式 - Creation Patterns * [**創建模式 PK**](https://devtechascendancy.com/pk-design-patterns-factory-builder-best/) * **創建模式 - `Creation Patterns`**: 創建模式用於「**物件的創建**」,它關注於如何更靈活、更有效地創建對象。這些模式可以隱藏創建對象的細節,並提供創建對象的機制,例如單例模式、工廠模式… 等等,詳細解說請點擊以下連結 :::success * [**Singleton 單例模式 | 解說實現 | Android Framework Context Service**](https://devtechascendancy.com/object-oriented_design_singleton/) * [**Abstract Factory 設計模式 | 實現解說 | Android MediaPlayer**](https://devtechascendancy.com/object-oriented_design_abstract-factory/) * [**Factory 工廠方法模式 | 解說實現 | Java 集合設計**](https://devtechascendancy.com/object-oriented_design_factory_framework/) * [**Builder 建構者模式 | 實現與解說 | Android Framwrok Dialog 視窗**](https://devtechascendancy.com/object-oriented_design_builder_dialog/) * [**Clone 原型模式 | 解說實現 | Android Framework Intent**](https://devtechascendancy.com/object-oriented_design_clone_framework/) * [**Object Pool 設計模式 | 實現與解說 | 利用 JVM**](https://devtechascendancy.com/object-oriented_design_object-pool/) * [**Flyweight 享元模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_flyweight/) ::: ### 行為模式 - Behavioral Patterns * [**行為模式 PK**](https://devtechascendancy.com/pk-design-patterns-cmd-strat-state-obs-chain/) * **行為模式 - `Behavioral Patterns`**: 行為模式關注物件之間的「**通信**」和「**職責分配**」。它們描述了一系列對象如何協作,以完成特定任務。這些模式專注於改進物件之間的通信,從而提高系統的靈活性。例如,策略模式、觀察者模式… 等等,詳細解說請點擊以下連結 :::warning * [**Stragety 策略模式 | 解說實現 | Android Framework 動畫**](https://devtechascendancy.com/object-oriented_design_stragety_framework/) * [**Interpreter 解譯器模式 | 解說實現 | Android Framework PackageManagerService**](https://devtechascendancy.com/object-oriented_design_interpreter_framework/) * [**Chain 責任鏈模式 | 解說實現 | Android Framework View 事件傳遞**](https://devtechascendancy.com/object-oriented_design_chain_framework/) * [**State 狀態模式 | 實現解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_state/) * [**Specification 規格模式 | 解說實現 | Query 語句實做**](https://devtechascendancy.com/object-oriented_design_specification-query/) * [**Command 命令、Servant 雇工模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_command_servant/) * [**Memo 備忘錄模式 | 實現與解說 | Android Framwrok Activity 保存**](https://devtechascendancy.com/object-oriented_design_memo_framework/) * [**Visitor 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_visitor_dispatch/) * [**Template 設計模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_template/) * [**Mediator 模式設計 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_mediator/) * [**Composite 組合模式 | 實現與解說 | 物件導向設計**](https://devtechascendancy.com/object-oriented_programming_composite/) ::: ### 結構模式 - Structural Patterns * [**結構模式 PK**](https://devtechascendancy.com/pk-design-patterns-proxy-decorate-adapter/) * **結構模式 - `Structural Patterns`**: 結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結 :::danger * [**Bridge 橋接模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_bridge/) * [**Decorate 裝飾模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_decorate/) * [**Proxy 代理模式 | 解說實現 | 分析動態代理**](https://devtechascendancy.com/object-oriented_design_proxy_dynamic-proxy/) * [**Iterator 迭代設計 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_iterator/) * [**Facade 外觀、門面模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_facade/) * [**Adapter 設計模式 | 解說實現 | 物件導向設計**](https://devtechascendancy.com/object-oriented_design_adapter/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Java 設計模式` `基礎進階`