架構演化思考總結(2)

畅知發表於2024-07-31

架構演化思考總結(2)

—-–從命令模式中來探索處理依賴關係

在正式引入命令模式的概念之前,我們先從簡單的案例來逐步演化大家在書面上常見到的內容。

public interface ICommand
{
    void Execute();
}

public class PlayMusicCommand : ICommand
{
     public void Execute()
     {
        Debug.Log("你說家是唯一的城堡,隨著稻香一路奔跑~");
     }
}

var Start()
{
    var command = new PlayMusicCommand();
    command.Execute();
}

這裡我們定義一個命令介面,如果是命令,必定要實現的一個執行方法。

PlayMusicCommand 實現介面,此命令的作用就是播放Jay的《稻香》,如果想實現播放音樂功能,直接執行對應命令的方法即可!

體現出命令的本質,我們把要所作的內容或者控制邏輯封裝到一起,當我們需要執行它時候,下達執行命令的方法即可!

來看AI對命令模式的介紹:
image

上面小案例正對應對“操作邏輯”進行封裝,提煉成命令,那麼這樣對操作邏輯進行封裝有什麼好處呢?

顯而易見的好處之一就是方便管理,邏輯清晰。進行復雜邏輯開發時候,我們正式把它儘可能提煉封裝成方法,為的就是方便管理,而命令模式是對邏輯程式碼再高一個層次的封裝,也就是說從方法抽象成類,顯然更加便於管理和複用。

使用命令模式降低複雜邏輯的開發除錯難度,你排查一個幾百行的大函式的bug肯定比封裝拆分成幾個函式或者是幾個對應的命令的狀況要麻煩。比如我們需要進行某個複雜操作,但是我們對它進行拆分封裝,分成幾個Command來執行,這樣既可以分發給幾個同事一起協作複雜邏輯開發,沒有與核心邏輯控制指令碼產生過多的耦合

當然另一方面分擔了控制指令碼Controller的控制壓力,使其沒那麼臃腫。

public interface ICommand
{
    void Execute();
}

public class ACommand
{
    public void Execute()
    {
        Debug.Log("Execute A Command");
    }
}

public class BCommand
{
    public void Execute()
    {
        Debug.Log("Execute B Command");
    }
}

public class CCommand
{
    public void Execute()
    {
        Debug.Log("Execute C Command");
    }
}

void Start()
{
    var commands = new List<ICommand>();
    commands.Add(new ACommand());
    commands.Add(new BCommand());
    commands.Add(new CCommand());
  
    commands.ForEach(c=>c.Execute());
}

命令模式–攜帶引數

如果我們要執行需要引數才能進行的命令呢?

好,接下來我們實現可以攜帶引數的命令,非常簡單,只需要給執行的命令中宣告引數即可!

interface ICommand
{
    void Execute();
}
public class BuyGoodsCommand : ICommand
{
    private int goodsId;
    private int goodsCount;
    public BuyGoodsCommand(int id,int count)
    {
        goodsId = id;
        goodsCount = count;
    }
    public void Execute()
    {
        Debug.Log($"購買了id為{goodsId}的商品{goodsCount}個");
        //執行相關的購買邏輯
        //......
    }
}

public class Test : MonoBehaviour
{
    private void Start()
    {
        var buyGoodsCommand = new BuyGoodsCommand(1, 15);
        buyGoodsCommand.Execute();
    }
}

命令模式–撤銷功能

接下來接著向命令模式的功能實現邁進,在剛接觸命令模式的時候,會好奇的想到,既然把命令都封裝好一步步執行了,那能不能撤銷已經執行好的行為呢?筆者也是在學習到命令模式之後才聯想到各種編輯工具的Ctrl+Z的效果的實現思路。那我們往命令模式中新增一個撤銷功能。

當然要執行撤銷命令,要有個容器來儲存已經執行的命令,這裡使用的是List,也可以用Stack

和Queue,當然使用棧就可以實現Ctrl + Z的逐步撤銷功能了!

interface ICommand
{
    void Execute();
    void Undo();
}
public class BuyGoodsCommand : ICommand
{
    private int goodsId;
    private int goodsCount;
    public BuyGoodsCommand(int id,int count)
    {
        goodsId = id;
        goodsCount = count;
    }
    public void Execute()
    {
        Debug.Log($"購買了id為{goodsId}的商品{goodsCount}個");
        //執行相關的購買邏輯
        //......
    }

    public void Undo()
    {
        Debug.Log($"剛才購買的id為{goodsId}的商品{goodsCount}個,已經全部退貨!");
        //執行相關的退貨操作
        //如庫存++
        //玩家金幣++
    }
}

public class Test : MonoBehaviour
{
    private void Start()
    {
        var commands = new List<BuyGoodsCommand>();
        commands.Add(new BuyGoodsCommand(1, 15));
        commands.Add(new BuyGoodsCommand(5, 2));

        //執行購買
        commands.ForEach(command => command.Execute());

        //5號物品不想要了 退貨
        commands[1].Undo();
    }
}

命令模式–命令和執行分離

這裡和上一篇所陳述的依賴關係大致相同,我們把命令從一個物件降級成方法來看。

我們常常進行的方法呼叫這種行為,就是命令和執行未分離的一個例子。即方法呼叫必然方法中的邏輯執行。

void DoSomethingCommand()
{
    Debug.Log("命令執行了!");
}
void Start()
{
    DoSomethingCommand();
}

那麼命令和執行分開是怎麼樣的呢?

我們可以使用委託來實現,時間和空間上的分離。

 public class A : MonoBehaviour
 {
     B b;
     void Start()
     {
         b = transform.Find("Animation").GetComponent<B>();

         // 註冊完成的事件
         b.OnDoSomethingDone += DoSomethingCommand;
     }
     void DoSomethingCommand()
     {
         Debug.Log("命令執行了!");
     }
 }

public class B : MonoBehaviour
{
    // 定義委託
    public Action OnDoSomethingDone = ()=>{};

    //當動畫播放完畢後呼叫
    public void DoSomething()
    {
        //觸發委託中的函式執行
        OnDoSomethingDone();
    }
}

這樣將要執行的命令DoSomethingCommand,會在特定時機(時間上分離)由另外一個指令碼(空間上分離)呼叫執行,實現時空分離。

好,我們已經在方法層面表述出命令的分離,現在我們回到類這個層面,將Command的宣告和執行進行分離。

這就需要一個對委託進行另一層的封裝使用,這裡是用委託(可以簡單理解為函式容器),儲存的是函式(command簡化為方法層面),可以使用。對應的將命令升級升級成物件,為此也要對

委託進行“升級”,這裡參考QFramWork的自定義的事件機制。

自定義事件機制

我們希望它事件機制擁有功能:傳送事件功能和自動登出功能。

傳送事件是必須的,而自動登出功能要的是當註冊事件監聽的GameObject的物件Destroy之後,要登出對事件的監聽功能。

現在按照這樣的要求來實現介面:

 public interface ITypeEventSystem
 {
     /// <summary>
     /// 傳送事件
     /// </summary>
     /// <typeparam name="T"></typeparam>
     void Send<T>() where T : new ();

     void Send<T>(T e);

     IUnRegister Register<T>(Action<T> onEvent);

     /// <summary>
     /// 登出事件
     /// </summary>
     /// <param name="onEvent"></param>
     /// <typeparam name="T"></typeparam>
     void UnRegister<T>(Action<T> onEvent);
 }
//登出機制
 public interface IUnRegister
 {
     void UnRegister();
 }

來著重實現自動登出機制:

我們來宣告一個類,來具體執行登出事件的功能:

public class TypeEventSystemUnRegister<T> : IUnRegister
{
    //持有事件機制引用
    public ITypeEventSystem TypeEventSystem { get; set; }
    
    //持有待登出的委託
    public Action<T> OnEvent {get;set;}
    
    //具體的登出機方法
    public void UnRegister()
    {
        //具體就是呼叫事件機制(系統)對應的方法,登出掉指定的函式 (OnEvent)
        TypeEventSystem.UnRegister(OnEvent);
        
        TypeEventSystem = null;
            OnEvent = null;
    }
}

當然登出時機是在當GameObjet銷燬時候,為此需要一個“觸發器”,其掛載在註冊事件的GameObject上,當檢測到Destroy時候進行觸發。

來實現對應的觸發器:

/// <summary>
/// 登出事件的觸發器
/// </summary>
public class UnRegisterOnDestroyTrigger : MonoBehaviour
{
    private HashSet<IUnRegister> mUnRegisters = new HashSet<IUnRegister>();

    public void AddUnRegister(IUnRegister unRegister)
    {
        mUnRegisters.Add(unRegister);
    }

    private void OnDestroy()
    {
        foreach (var unRegister in mUnRegisters)
        {
            unRegister.UnRegister();
        }

        mUnRegisters.Clear();
    }
}

來對登出機制的介面擴充功能,方便在註冊事件時候呼叫一個方法,透過這個方法呼叫直接將上段程式碼所示的登出機制的觸發器掛載在GameObject上。

public static class UnRegisterExtension
{
    public static void UnRegisterWhenGameObjectDestroyed(this IUnRegister unRegister, GameObject gameObject)
    {
        var trigger = gameObject.GetComponent<UnRegisterOnDestroyTrigger>();

        if (!trigger)
        {
            trigger = gameObject.AddComponent<UnRegisterOnDestroyTrigger>();
        }

        trigger.AddUnRegister(unRegister);
    }
}

至此,當我們在使用時候呼叫一下’UnRegisterWhenGameObjectDestroyed‘方法,將會掛載Tirgger,當物體銷燬時候會觸發,實現自動登出事件,有效的保證了在使用Unity中委託的註冊和登出成對出現的特徵,防止委託中出現空指標。

好,實現完成自動登出事件機制,繼續實現事件的註冊和呼叫機制。

public class TypeEventSystem : ITypeEventSystem
{
    //使用依賴倒轉原則
    interface IRegistrations
    {

    }

    class Registrations<T> : IRegistrations
    {
        public Action<T> OnEvent = obj => { };
    }
//根據事件的型別來儲存 對應的事件Action<T> 被封裝成類 以介面型別儲存 
    private Dictionary<Type, IRegistrations> mEventRegistrations = new Dictionary<Type, IRegistrations>();
    
    public void Send<T>() where T : new()
    {
        var e = new T();
        Send<T>(e);
    }
    //具體傳送機制 呼叫機制
    public void Send<T>(T e)
    {
        var type = typeof(T);
        IRegistrations eventRegistrations;

        if (mEventRegistrations.TryGetValue(type, out eventRegistrations))
        {
            //具體呼叫 “解壓 降維” 呼叫委託
            (eventRegistrations as Registrations<T>)?.OnEvent.Invoke(e);
        }
    }
    
    //註冊實現
     public IUnRegister Register<T>(Action<T> onEvent)
     {
         var type = typeof(T);
         //具體儲存 “加壓 升維” 向委託中新增函式
         IRegistrations eventRegistrations;

         //判斷儲存的事件型別存在否
         if (mEventRegistrations.TryGetValue(type, out eventRegistrations))
         {

         }
         else
         {
             //不存在就新增一個
             eventRegistrations = new Registrations<T>();
             mEventRegistrations.Add(type,eventRegistrations);
         }

         //如果存在就 解壓 新增到“解壓”好的事件機制中
         (eventRegistrations as Registrations<T>).OnEvent += onEvent;

         //返回登出物件需要的資料(引用)例項
         // 可以不透過建構函式來對共有訪問物件初始化賦值
         return new TypeEventSystemUnRegister<T>()
         {
             OnEvent = onEvent,
             TypeEventSystem = this
         };
     }
    //登出方法的具體實現
    public void UnRegister<T>(Action<T> onEvent)
    {
        var type = typeof(T);
        IRegistrations eventRegistrations;

        if (mEventRegistrations.TryGetValue(type,out eventRegistrations))
        {
            (eventRegistrations as Registrations<T>).OnEvent -= onEvent;
        }
    }
    
}

至此自定義的事件機制實現完畢!

如果感興趣,關注其對應的測試案例展示,(將在單獨一篇部落格介紹此事件機制)

繼續推進,使用此機制來實現Command的時空分離:

public interface ICommand
{
    void Execute();
}

public class SayHelloCommand
{
    public void Execute()
    {
        // 執行
        Debug.Log("Say Hello");
    }
}

void Start()
{
    // 命令
    var command = new SayHelloCommand();

    command.Execute();


    mTypeEventSystem = new TypeEventSystem();
    
   mTypeEventSystem.Register<ICommand>(Execute).UnRegisterWhenGameObjectDestroyed(gameObject);

    // 命令 使用Command物件註冊
   mTypeEventSystem.Send<Icommand>(new SayHelloCommand());
}

那麼對比三種實現方式發現什麼?

  • 方法:呼叫即執行!沒有分離
  • 事件機制:執行在事件註冊中實現 有分離
  • Command:執行在Command內部實現 有分離

顯然,Command對命令和執行的分離程度介於方法和事件機制之間。

重點對比事件機制,在實現自定義方法之前,筆者已經點到委託儲存的方法,和在使用封裝後委託(自定義事件)可以儲存類(命令例項),雖然都是透過委託來儲存執行方法,使用Command更為自由一些,可以在自定義的位置和時機執行,而事件機制一般至少需要透過兩個物件才能完整使用。

先寫到這裡吧!

下面我們繼續探索命令模式在架構演化中的作用,繼續接近我們學習中接觸到的Command模式!

謝謝各位能和我一起來探索專案架構設計演化!

相關文章