橋接模式(BridgePattern)

前言:

目的:

如果有兩大類模組是多對多的組合,如本次Smaple Code. Nick和Addidas 包包都有紅、藍、黃….或其他顏色

就可能呈現下面6種組合

  • Nick(紅)
  • Nick(藍)
  • Nick(黃)
  • Addidas(紅)
  • Addidas(藍)
  • Addidas(黃)

如果此建立類別的話 可能情況如下面的UNL圖

類別數量 = 顏色數量 * 包包品牌數量

這樣會有兩個問題

  1. 隨著品牌和顏色增多,包包類別數量急速增長 (X = m*n)個
  2. 顏色我們可看做一個抽象 不應當和包包合在一起

其中的第二點我覺得最重要

這時候就很適合帶入我們的主角BridgePattern


範例介紹

關係變成下圖UML

我們可以看到BagBsae去引用ColorBase 可以看到我們所需的子類別變成五個而已,重點是類別關係變得更有條理了,顏色和包包分開可調整性更大。

OOP有一個很重要的設計觀念

盡量用組合取代繼承,因為繼承耦合性遠大於組合!

因為子類別繼承父類別,子類別無條件都擁有protect已上的方法或成員資料.這就會造成一個耦合性(使用繼承須看情況),而A類別對於B類別進行組合就可達到繼承效果但不造成像繼承般的強耦合.

我們的背包一樣可擁有多種顏色,但耦合度跟類別關係變得更清晰了。


程式碼解說

建立 BagBase 類別並將 ColorBase 當建構傳入(因為Bag需要上顏色)

public abstract class BagBase
{
    protected ColorBase color{ get; set; }

    public BagBase(ColorBase color) {
        this.color = color;
    }
    public abstract void GetBag();
}

public abstract class ColorBase
{
    public abstract string Color();
}

這邊我只介紹一種顏色和包包來當作範例,因為其他概念都一樣

public class AdidasBag : BagBase
{
    public AdidasBag(ColorBase color) : base(color)
    {
    }

    public override void GetBag()
    {
        Console.WriteLine($"It is Addidas Bag,Color is {color.Color()}");
    }
}

class ColorBlue : ColorBase
{
    public override string Color()
    {
        return "Blue";
    }
}

建立

  • AdidasBag類別重載GetBag方法
  • ColorBlue類別重載Color方法

因為BagBase要傳入顏色GetBag就可幫包包上色.

使用如下外面看起來很合理乾淨.

class Program
{
    static void Main(string[] args)
    {
        AdidasBag nick = new AdidasBag(new ColorBlue());
        nick.GetBag();
        Console.ReadKey();
    }
}

實際案例

前陣子在做一個Unity2D遊戲,遇到一組遊戲邏輯

一個人物要移動有分兩種移動方式

  1. 自動移動
  2. 玩家手動點擊移動

因為是2D遊戲 有 上下左右 四個方位移動,四個方位配上兩個移動方式,人物會有不一樣的移動邏輯.

這邊我貼上部分程式碼

建立一個 RoadActionBase裡面有三個必要屬性需要給 上下左右 實現

  1. ArrowType 2D人物移動箭頭方向
  2. OffSetPos 移動距離
  3. PlayerDirction 這是一個Unity2D座標屬性

    public abstract class RoadActionBase
    {

    protected int _level;
    
    public RoadActionBase()
    {
        _level = SenceParamter.RoadCount;
    }
    
    public abstract ArrowType ArrowType { get; }
    
    public abstract int OffSetPos { get; }
    
    public abstract Vector2 PlayerDirction { get; }
    

    }

UpRoadAction類別對於往時的狀態做給值

public class UpRoadAction : RoadActionBase
{
    public override ArrowType ArrowType
    {
        get
        {
            return ArrowType.Up;
        }
    }

    public override int OffSetPos
    {
        get
        {
            return -_level;
        }
    }

    public override Vector2 PlayerDirction
    {
        get
        {
            return Vector2.up;
        }
    }
}

DownRoadAction類別對於往時的狀態做給值

public class DownRoadAction : RoadActionBase
{
    public override ArrowType ArrowType
    {
        get
        {
            return ArrowType.Down;
        }
    }

    public override int OffSetPos
    {
        get
        {
            return _level;
        }
    }

    public override Vector2 PlayerDirction
    {
        get
        {
            return Vector2.down;
        }
    }
}

RightRoadAction類別對於往時的狀態做給值

public class RightRoadAction : RoadActionBase
{
    public override ArrowType ArrowType
    {
        get
        {
            return ArrowType.Right;
        }
    }

    public override int OffSetPos
    {
        get
        {
            return 1;
        }
    }

    public override Vector2 PlayerDirction
    {
        get
        {
            return Vector2.right;
        }
    }
}

LeftRoadAction類別對於往時的狀態做給值

public class LeftRoadAction : RoadActionBase
{
    public override ArrowType ArrowType
    {
        get
        {
            return ArrowType.Left;
        }
    }

    public override int OffSetPos
    {
        get
        {
            return -1;
        }
    }

    public override Vector2 PlayerDirction
    {
        get
        {
            return Vector2.left;
        }
    }
}

建立一個 MoveBase 並將 RoadActionBase當作建構子傳入(內部邏輯有寫注解).

重點在於一個 IsWalkNext方法 提供Hock給子類別做實現,因為手動和自動移動邏輯不一樣.
/// <summary>
/// 橋接模式
/// </summary>
public abstract class MoveBase
{
    protected PlayerController _player;

    protected int _level;

    protected float _Scape;

    public RoadActionBase RoadAction { get; protected set; }

    public MoveBase(RoadActionBase roadAction)
    {
        _player = PlayerController.Instance;
        _level = SenceParamter.RoadCount;
        _Scape = SenceParamter.Scape + SenceParamter.RoadHeigh;
        RoadAction = roadAction;
    }

    public virtual void Move(RoadContext currentRoad, RoadContext nextRoad)
    {
        //取得下一個位置
        Vector2 nextPos = nextRoad.transform.localPosition;

        if (IsWalkNext(currentRoad, nextRoad, _player.targetPos, nextPos))
        {
            //將下一個資料塞給當前玩家
            _player.targetPos = nextPos;
            _player.RoadContext = nextRoad;
            _player.moveDirction = RoadAction.PlayerDirction;

            currentRoad.SetIsWalk(true);

            //加入等待轉換的地方
            ReloadRoadController.Instance.AddRoadContext(currentRoad);
        }
    }

    protected abstract bool IsWalkNext(RoadContext currentRoad, RoadContext nextRoad, Vector3 targetPos, Vector3 nextPos);
}

TouchMove 類別重載 IsWalkNext實現自己的邏輯

public class TouchMove : MoveBase
{
    public TouchMove(RoadActionBase roadAction) : base(roadAction)
    {
    }

    /// <summary>
    /// 判斷是否可以 前往下一個目標
    /// </summary>
    /// <param name="currentRoad"></param>
    /// <param name="nextRoad"></param>
    /// <param name="targetPos"></param>
    /// <param name="nextPos"></param>
    /// <returns></returns>
    protected override bool IsWalkNext(RoadContext currentRoad, RoadContext nextRoad, Vector3 targetPos, Vector3 nextPos)
    {
        ArrowType arrowType = RoadAction.ArrowType;
        //1.下一個道路要可以進去
        //2.當前道路要可以出來
        //3.必須為四周的道路
        return
            arrowType.CanWalk(currentRoad.CanWalkOut) &&
            arrowType.CanWalk(nextRoad.CanWalkIn) &&
            CanMoveNextPos(targetPos, nextPos);

    }

    private bool CanMoveNextPos(Vector3 targetPos, Vector3 nextPos)
    {
        return ((int)Vector2.Distance(targetPos, nextPos)) % 
            ((int)_Scape) == 0;
    }
}

AutoMove 類別重載 IsWalkNext實現自己的邏輯

public class AutoMove : MoveBase
{
    public AutoMove(RoadActionBase roadAction) : base(roadAction)
    {
    }

    /// <summary>
    /// 判斷是否可以 前往下一個目標
    /// </summary>
    /// <param name="currentRoad"></param>
    /// <param name="nextRoad"></param>
    /// <param name="targetPos"></param>
    /// <param name="nextPos"></param>
    /// <returns></returns>
    protected override bool IsWalkNext(RoadContext currentRoad, RoadContext nextRoad, Vector3 targetPos, Vector3 nextPos)
    {
        //1.下一個道路要可以進去
        //2.當前道路要可以出來
        //3.必須為四周的道路
        //4.步數必須大於0
        return
                currentRoad.CurrentArrow.CanWalk(nextRoad.CanWalkIn) &&
                currentRoad.CurrentArrow.CanWalk(currentRoad.CanWalkOut) &&
                CanMoveNextPos(targetPos, nextPos) &&
                !nextRoad.IsChangeState && 
                GameModel.Step >0;
    }

    private bool CanMoveNextPos(Vector3 targetPos, Vector3 nextPos)
    {
        return ((int)Vector2.Distance(targetPos, nextPos)) % 
            ((int)_Scape) == 0;
    }
}

上面程式碼最主要是跟大家分享移動方式和方位的關係,上下左右值和方位式固定,將此配上不同的移動方式有不一樣的邏輯.

SourceCode

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/bridge-pattern/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^