1. 程式人生 > >StrangIOC框架一

StrangIOC框架一

正式之前先說說基礎的概念:

首先是MVC框架,MVC框架的思路是將業務邏輯、資料、介面顯示分離,網上很多關於MVC的資料,這裡不再過多的描述了。

下面截圖來自wikipedia的mvc框架:

然後是控制反轉(包括依賴注入和依賴查詢),控制反轉也是著名框架spring框架的核心。

對於控制反轉(Inversion of Control,縮寫IOC)的依賴注入,先用一個C#示例程式碼演示一下。比如設計一個玩家攻擊敵人的遊戲,玩家可以裝備手槍,步槍。最簡單的程式碼設計實現如下:

public class Player

{

    public string gunName = "ShouQiang";

    public void Attack(Enemy enemy)

    {

        if(gunName == "ShouQiang")

            enemy.hp -= 10;

        else if(gunName = ''BuQiang")

        ememy.hp -= 20;

    }

}

如上述程式碼,假如專案中多了一個(機槍),需要重寫player類,重寫攻擊邏輯(if..else)設計不合理。重新設計,程式碼可以用下面的方實現:

public interface IGun {

    voidAttack(Enemy eneny);

}

public class ShouQiang : IGun

{

    public void Attack(Enemy eneny)

    {

        enemy.hp -=10;

    }

}

public class BuQiang : IGun

{

    public void Attack(Enemy enemy)

    {

        enemy.hp -= 20;

    }

}

public class Player

{

    public IGun gun = newShouQiang(); 

    public

 void Attack(Enemy enemy)

    {

        gun. Attack(enemy);

    }

}

public class Eneny

{

    public int hp;

}

如果再增加機槍,直接增加一個機槍類繼承IGun介面就可以了 playe類中

public IGun gun = new ShouQiang();簡單實現了一個注入.

下面正式介紹strangeioc.

正如strangeioc描述的一樣,總體上說它是根據控制反轉和解耦和原理設計的,支援依賴注入. 繼續引入一個示例,如果寫一個單玩家遊戲,死亡後將最高分數提交到facebook ,程式碼如下:

public class MyShip: MonoBehaviour {

    private FacebookService facebook;

    private int myScore,

    void Awake()

    {

        facebook = FacebookService.getlnstance(),

    }

    void onDie()

    {

        if (score > highScore) 

            facebook.PostScore(myScore),

    }

}

這樣做有兩個問題:

1.ship作為控制角色,但是除了要通知facebook上傳分數還要處理程式碼邏輯.將自身與遊戲邏輯與 facebook邏輯關聯(髙耦合)

2.FacebookService是單鍵,如果我們改為要提交到Twitter..or Google+... 怎麼辦?或者FacebookService裡面的方法有變更怎麼辦?

如果程式碼如上,需要重構很多程式碼才能完成相應的功能,但是通過引入ioc,重構將會方便很多。

public class MyShip : MonoBehavior

{

    void OnDie()

    {

        dispatcher.Dispatch(GameEvent.SHIP_DEAD);

    }

}

檢視模組出發了死亡邏輯,它(View)需要知道的僅僅是這些就夠了,然後將死亡訊息傳送出去。

某個模組的程式碼用來接收死亡訊息,然後做出處理,如下:

public class OnShipDeathCommand: EventCommand

{

    [Inject]

    ISocialService socialService {getset,}

    [Inject]

    IScoreModel scoreModel {get set,}

    public override void Execute()

    {

        if (scoreModel.score > scoreModel.highScore) 

            socialService.PostScore(scoreModel.score);

    }

}

[Inject]標籤實現介面,而不是例項化類 

#if UNITY_ANDROID

injectionBinder.Bind<ISocialService>().To<GoogleService>().AsSingleton();

#else

injectionBinder.Bind<ISocialService>().To<FacebookService>().AsSingleton(); 

#endif

//...or...

//injectionBinder.Bind<ISocialService>().To<MultiServiceResolver>().AsSingleton();

injectionBinder.Bind<IScoreModel>() To<ScoreMcxiel>() AsSIngleton();

commandBinder.Bind(GameEvent.SHIP_DEAD , OnShipDeathCommand);

用IOC(控制反轉)我們不用關注具體邏輯,也不同自己再建立單鍵。

ship觸發Command來執行邏輯,每個模組之間都是獨立的。

1>commandBinder.Bind(GameEvent.SHIP_DEAD , OnShipDeathCommand);

將事件和對應邏輯繫結在了一起。

2>dispatcher.Dispatch(GameEvent.SHIP_DEAD)

就是發出事件,但是具體做什麼事情不需要了解。

Unity StrangeIoC(二)

strangeioc第一大部分:1.Binding(繫結)

strange的核心是繫結,我們可以將一個或者多個物件與另外一個或者多個物件繫結(連線)在一起。將介面與類繫結來實現介面,將事件與事件接受繫結在一起。或者繫結兩個類,一個類被建立的時候另一個自動建立。如果你再Unity裡面用過SendMessage方法,或者一個類引用另一個類,再或者式if...else語句,從某種形式上來說也是一種繫結。

但是直接繫結是有問題的,導致程式碼不易修改(不再是高聚合低耦合).比如一個spaceship類,裡面包含gun發射和鍵盤控制功能,當需求變更為使用滑鼠控制時,你就需要重新修改spaceship方法,但是僅僅是控制功能的需求改變了,為什麼要重寫spaceship方法呢?

如果你自定義一個滑鼠控制類(mousecontrol)。然後spaceship裡面呼叫滑鼠控制類,你還是採用的直接繫結的方法。當需求變為鍵盤控制的時候,你又要去重新修改spaceship去呼叫鍵盤控制的class。

strange可以實現間接繫結從而使你的程式碼減輕對其他模組的依賴(低耦合)。這是面向物件程式設計(Object-Oriented Programming)的根基和宗旨。通過strangeioc繫結你的程式碼會變得更加靈活。

The structure of a binding

strange的binding由兩個必要部分和一個可選部分組成,必要部分是a key and a value, key觸發value,因此一個事件可以觸發回撥,一個類的例項化可以觸發另一個類的例項化。可選部分是name,它可以區分使用相同key的兩個binding。下面的三種繫結方法其實都是一樣的,語法不同而已:

1.Bind<IDrive>().To<WarpDrive>();

2.Bind(typeof(IDrive)).To(typeof(WarpDrive));

3.IBinding binding = Bind<IDrive>();

bingding.To<WarpDrive>();

bind是key,to是value。

當使用非必要部分Name時,如下:

Bind<IComputer>().To<SuperComputer>().ToName("DeepThought");

MVCSContext式推薦的版本,它包含下面介紹的所有拓展,是使用strange最簡單的途徑

strangeioc第二大部分:2.Extensions

看strange的介紹,它是一個依賴注入框架,雖然它具有依賴注入的功能,但是核心思想還是繫結。

接下來講介個關於binder(繫結)的擴充套件。

The injection extension(注入)

在binder擴充套件中最接近Inversion-of-Control(IoC)控制反轉思想的是注入。

大家應該很熟悉介面的使用方法,介面本身沒有是實現方法,只是定義類中的規則:

interface ISpaceship

{

    void input(float angle,float velocity);

    IWeapon weapon{ getset; }

}

使用另一個類來實現這個介面:

class Spaceship : ISpaceship

{

    public void input(float angle,float velocity)

    {

        //do stuff here

    }

    IWeapon weapon{ getset; }

}

如果採用上面的寫法,Spaceship類裡面不用再寫檢測輸入(input)的功能了,只需要處理輸入就可以了,不再需要武器邏輯,僅僅式注入武器例項就可以了。到現在已經邁出一大步了。但是出現了一個問題,誰告訴spaceship,我們使用何種武器?我們可以採用工廠模式給weapon賦值具體的武器例項,簡單的工廠模式如下:

public interface IWeapon

{    

    void Attack();

}

public class BuQiang : IWeapon

{

    public void Attack(){//敵人掉血邏輯

    }

}

public class ShouQiang : IWeapon

{

    public void Attack() {//敵人掉血邏輯

    }

}

上面建立好了武器的約定介面和兩種武器,下面建立武器工廠:

public class MeaponFactory 

{

    public IWeapon GetWeapon(string name)

    {

        if(name == "BuQiang") 

            return new BuQiang(); 

        else if (name == "ShouQiang") 

            return new ShouQiang(); 

        return null;

    }

}

取武器直接使用

IWeapon weapon = new WeaponFactory().GetWeapon("BuQiang");

(ps. 也有人說,取武器直接IWeapon weapon = new BuQiang();使用工廠反而麻煩,但是如果我們可以從美國軍械庫去到步槍,手槍。中國軍械庫也可以去到步槍,手槍。使用工廠模式就方便了許多:

IWeapon weapon = new AmericaWeaponFactory().GetWeapon("BuQiang");

IWeapon weapon = new ChinaWeaponFactory().GetWeapon("BuQiang");

但是這樣做並沒有解決根本問題,factorymanager需要包括/返回各種武器的例項。現在我們引入一個全新的模式:Dependency Injection(DI)依賴注入:

DI模式中一個類對另一個類的引用,通過發射的方式實現(比如我們的ISpaceship中的weapon不再直接複製類的例項/或者通過工廠賦值類的例項(這樣它們之間仍舊存在引用關係))而是通過反射(Reflection)的方式實現。

首先我將IWeapon和具體的手槍例項繫結:

injectionBinder.Bind<IWeapon>().To<ShouQiang>();

然後ISpaceship中weapon賦值如下:

[Inject]

public IWeapon weapon { get; set; }

當然如果有多個實現方式可以使用如下方式:

injectionBinder.Bind<IWeapon>().To<ShouQiang>().ToName("ShouQiang");

injectionBinder.Bind<IWeapon>().To<BuQiang>().ToName("BuQiang");

然後ISpaceship中weapon賦值如下:

[Inject("BuQiang")]

public IWeapon weapon { getset; }

或者:

[Inject("ShouQiang")]

public IWeapon weapon { getset; }

Unity StrangeIoC(三)

第二個擴充套件部分:

The reflector extension(反射)

List<Type> list = new List<Type>();

list.Add(tpyeof(Borg));

list.Add(typeof(DeathStar));

list.Add(typeof(Galactus));

list.Add(typeof(Berserker));

//count should equal 4, verifying that all four classes were reflected

int count = injectionBinder.Reflect(list);

看官網介紹,可以批量反射,全部反射等待.. injectionBinder.ReflectAll();

下面接著說擴充套件部分中的第三個擴充套件(排程器)

The dispatcher extension

從原理上來說dispatcher相當於觀察者模式中的公告板,允許客戶監聽他,並且告知當前發生的事件。但在strangeioc中,通過EventDispatcher方式實現,EventDispatcher繫結觸發器來觸發帶引數/不帶引數的方法。

觸發器通常是string或者是列舉型別(觸發器你可以理解為key,或者事件的名稱,名稱對應著觸發的方法).

如果事件有返回值,他將存在IEvent,編輯邏輯實現這個介面就能得到返回值,標註的strangeioc事件叫做TmEvent.如果你在用MVCSContext,有個全域性的EventDispatcher叫:contextDispatcher會自動注入,你可以用它來傳遞事件。

There's also acrossContextDispatcher for communicating between Contexts。

同樣有一個accossContextDispatcher可以在contexts傳遞事件。

使用EventDispatcher需要做兩個事情就可以,排程/設定事件和監聽事件。

dispatcher.AddListener("FIRE_MISSILE",onMissleFire);

然後事件就會處於監聽狀態,直到FIRE_MISSLE事件被觸發,然後執行相應的onMissleFire方法。

當然Key也可以不適用字串而是列舉型別。

相關的幾個Api:

dispatcher.AddListener("FIRE_MISSILE",onMissleFire);

dispatcher.RemoveListener("FIRE_MISSILE",onMissleFire);

dispatcher.UpdateListener("FIRE_MISSILE",onMissleFire);

派發訊息:

dispatcher.Dispatch("FIRE_MISSLE")。

Unity StrangeIoC(四)

繼續擴充套件部分的第四個擴充套件(命令)

The command extension

除了將事件繫結到方法以外,還可以將他們與命令繫結。命令式mvc框架的指揮者。

在strangeioc的MVCSContext中,CommandBinder監聽所有來自dispatcher分發的事件.

Signals(上文提到的訊息(signal)與事件(event))也可以繫結到command.

using strange.extensions.command.impl; 

using com.example.spacebattle.utils; 

namespace com.example.spacebattle.controller

{

    class StartGameCommand : EventCommand

    {

        [Inject]

        public ITimer ganeTimer { getset; }

        overridepublic void Execute()

        {

            gameTimer.start();

            dispatcher.dispatch(GameEvent.STARTED);

        }

    }

}

如果是有網路請求,有返回,可以採用這樣的方法實現:

using strange.extensions.command.impl; 

using com.example.spacebattle.service;

namespace com.example.spacebattle.controller

{

    class PostScoreCommand : EventCommand

    {

        [Inject]

        IServer gameServe { getset ;} 

        override public void Execute()

        {

            Retain();

            int score = (int)evt.data;

            gameServer.dispatcher.AddListener(

                        ServerEvent.SUCCESS, onSuccess);               

            gameServer.dispatcher.AddListener(

                        ServerEvent.FAILURE, onFailure);    

            gaweServer.send(score);

        }

        private void onSuccess()

        {

            gameServer.dispatcher.RemoveListener(

                        ServerEvent.SUCCESS, onSuccess);     

            gameServer.dispatcher.RemoveListener(

                        ServerEvent.FAILURE, onFailure); 

            //…do something to report success…

            Release();

        }

        private void onFailure(object payload)

        {

            gameServer.dispatcher.RemoveListener(

                        ServerEvent.SUCCESS, onSuccess);     

            gameServer.dispatcher.RemoveListener(

                        ServerEvent.FAIlURE, onFailure);

            //...do sowething to report failure...

            Release();

        }

    }

}

將命令和該類繫結起來

commandBinder.Bind(ServerEvent.POST_SCORE).To<PostScoreCommand>();

命令繫結多個事件:

commandBinder.Bind(GameEvent.HIT).To<DestroyEnemyCommand>()

.To<UpdateScoreCommand>();

解除繫結:

commandBinder.Unbind(GameEvent.POST_SCORE);

按順序觸發命令繫結事件:

commandBinder.Bind(GameEvent.HIT).InSequence()

.To<CheckLevelClearedCommand>()

.To<EndLevelCommand>()

.To<GameOverCommand>();

Unity StrangeIoC(五)

擴充套件部分的第五個擴充套件

The signal extension(訊息)

訊息是一種代替EventDispatcher的分發機制,與EventDispatcher相比,訊息1.分發結果不再建立例項,因此也不需要回收更多的垃圾。2.更安全,當訊息與回撥不匹配時,就會斷開執行。官網也推薦使用signal來相容後續版本。

Signal<int> signalDispatchesInt = new Signal<int>(); 

Signal<string> signalDispatchesString = new Signal<string>();

//Add a callback with an int parameter

signalDispatchesInt AddListener(callbacklnt);

//Add a callback with a string parameter  

signalDispatchesString AddListener(callbackString); 

signalDispatchesInt.Oispatch(42); //dispatch an int 

signalDispatchesString.Oispatch("Ender Wiggin"); //dispatch a string

void callbacklnt(int value)

{

//Do something with this int

}

void callbackString(string value)

{

//Do something with this string

}

訊息最多可以使用四個引數:

//works

Signal signal0 = new Signal();

//works

Signal<SomeValueObject> signal1 =  new Signal<SomeValueObject>();

//works

Signal<int,string> signal2 =  new Signal<int,string>();

//works

Signal<int,int> signal3 =  new Signal<int,int>();

//works

Signal<SomeValueObject,int,string,MonoBehavior> signal4 =  new 

Signal<SomeValueObject,int,string,MonoBehavior>();

//FAILS!!!!  Too many params.

Signal<int,string,Vector2,Rect> signal5 =  new 

Signal<int,string,Vector2,Rect>();

訊息繫結:

commandBinder.Bind<SomeSignal>().To<SomeCommand>();

看下一個示例:

commandBinder.Bind<ShipDestroyedSignal>().To<ShipDestroyedCommand>();

然後:

[Inject]

public ShipDestroyedSignal shipDestroyedSignal{ get; set; }

// imagining that the Mediator holds avalue for this ship

private int basePointValue;

//Something happened that resulted in destruction

private void OnShipDestroyed()

{

    shipDestroyedSignal.Dispatch(view,basePointValue);

}

再然後:

using System;

using strange.extensions.command.impl;

using UnityEngine;

namespace mynamespace

{

    //Note how we extend Command,not EventCommand

    public class ShipDestroyedCommand: Command

    {

        [inject]

        public MonoBehavior view{ getset; }

        [inject]

        public int basePointValue{ getset; }

        public override void Execute()

        {

            //Do unspeakable things to the destroyed ship

        }

    }

}

再給出一個strangeios的demo裡面的程式碼:

using System;

using UnityEngine;

using strange.extensions.context.api;

using strange.extensions.context.impl;

using strange.extensions.dispatcher.eventdispatcher.api;

using strange.extensions.dispatcher.eventdispatcher.impl;

using strange.extensions.command.api;

using strange.extensions.command.impl;

namespace strange.examples.signals

{

  public class SignalsContext : MVCSContext

  {

      public SignalsContext (MonoBehaviour view) : base(view)

      {

      }

      public SignalsContext (MonoBehaviour view, 

ContextStartupFlags flags)

  : base(view, flags)

      {

      }

      // Unbind the default EventCommandBinder and rebind the SignalCommandBinder

      protected override void addCoreComponents()

      {

         base.addCoreComponents();

         injectionBinder.Unbind<ICommandBinder>();

         injectionBinder.Bind<ICommandBinder>()

.To<SignalCommandBinder>().ToSingleton();

      }

      // Override Start so that we can fire the StartSignal 

      override public IContext Start()

      {

         base.Start();

         StartSignal startSignal= (StartSignal)injectionBinder

.GetInstance<StartSignal>()

         startSignal.Dispatch();

         return this;

      }

      protected override void mapBindings()

      {

         injectionBinder.Bind<IExampleModel>()

.To<ExampleModel>().ToSingleton();

         injectionBinder.Bind<IExampleService>()

.To<ExampleService>().ToSingleton();

         mediationBinder.Bind<ExampleView>().To<ExampleMediator>();

         commandBinder.Bind<CallWebServiceSignal>()

.To<CallWebServiceCommand>();

         //StartSignal is now fired instead of the START event.

         //Note how we've bound it "Once". This means that the mapping goes away as soon as the command fires.

         commandBinder.Bind<StartSignal>().To<StartCommand>().Once();

         //In MyFirstProject, there's are SCORE_CHANGE and FULFILL_SERVICE_REQUEST Events.

         //Here we change that to a Signal. The Signal isn't bound to any Command,

         //so we map it as an injection so a Command can fire it, and a Mediator can receive it

         injectionBinder.Bind<ScoreChangedSignal>().ToSingleton();

         injectionBinder.Bind<FulfillWebServiceRequestSignal>()

.ToSingleton();

        }

    }

}

signal 既可以通過command新增繫結監聽也可以通過AddLister

Unity StrangeIoC(六)

繼續擴充套件部分的下一個擴充套件

The mediation extension(調節器(中介模式))

mediationContext是唯一一個專為Unity設計的部分,因為mediation關係的式對view(GameObject)的操作,由於view部分天生的不確定性,我們推薦view由兩種不同的monobehavior組成:View and Mediator.

view就是mvc中的v,一個view就是一個你可以編寫邏輯,控制可見部分的monobehavior.

這個類可以附加(拖拽)到Unity編輯器來關聯GameObject.但是不建議吧mvc的models和controller寫在view中。

Mediator類的職責是知曉view和整個應用的執行。它也會獲取整個app中分發和接受的時間和訊息,但是因為mediator的設計,建議使用命令模式(command)來做這部分的功能。

mediator示例:

using strange.extensions.mediation.ipml; 

using com.example.spacebattle.events; 

using com.example.spacebattle.model; 

namespace com.example.spacebattle.view 

{

    class DashboardMediator : EventMediator

    {

        [Inject]

        public DashboardView view { getset; } 

        override public void OnRegister()

        {

            view.init();

            dispatcher.Addlistener

                    (ServiceEvent.FULFILL_ONLINE_PLAYERS, onPlayers); 

            dispatchers Dispatch

                    (ServiceEvent.REQUEST_ONLINE_PLAYERS);

        }

        override public void OnRemove()

        {

            dispatcher.RemoveListener

                    (ServiceEvent.FULFILl_ONLINE_PLAYERS, onPlayers);

        }

        private void onPlayers(IEvent evt)

        {

            IPlayers[] playerList = evt.data as IPlayers[];

            view.updatePlayeCount(playerlist. Length);

        }

    }

}

示例說明:

1.當完成注入之後,DashboardView變為當前的場景所掛載的view.

2.當完成注入之後(可以理解為結構體的方式) OnRegister()方法會立即執行。可以用這個方法來做初始化。

3.contextDispatcher可以擴充套件任何的EventMediator。

4.OnRemove()清理使用,當一個view在被銷燬前呼叫,並且要移除你所新增的所有監聽器。

5.示例中的view暴力ultimate兩個介面,但是在程式設計的時候你可能會更多,但是有一條理念:中介者不處理GameObject相關屬性更新。

可以這樣來繫結:

mediationBinder.Binder<DashboardView>().To<DashboardMediator>();

注意:

1.並不是所有MonoBehavior都需要限定成view.

2.中介者繫結是例項對例項的,也就是說一個view一定是對應一個mediator,如果有多個view那麼一定有多個mediator。

繼續另外一個擴充套件:

The context extension

MVCSContext包括

EventDispatcher(事件分發),

InjectionBinder(注入繫結),

MediationBinder(中介繫結),

CommandBinder(命令繫結)。

可以重新將CommandBinder 繫結到SinalCommandBinder。 命令和中介依託注入,context以為命令和中介的繫結提供關聯。

建立一個專案需要從重新定義MVCSContext或者Context,一個app也可以包含多個Contexts,這樣可以使你的app更高的模組化,因此,一個app可以獨立的設計為聊天模組,社交模組,最終他們會整合到一起成為一個完整的app。

Unity StrangeIoC(七)

strangeioc第三大部分:3.MVCSContext

這一部分講的是如何使用MVCSContext建立一個app.

MVCSContext式通過使用strangeioc的方式,將所有的小的模組/架構,整合到一個易用的包中。它是以mvcs(s meaning service)的架構來設計的。

1.app的入口是一個monobehavior來擴充套件的mvcscontext。

2.用mvcs的子類來執行各種繫結。

3.dispatcher(分發器)可以讓你在整個app中傳送時間,在mvcscontext中它們傳送的是TmEvents,你可以重寫context來使用signal(訊息)的方式傳送。

4.commands(命令)會被IEvents或者Signal觸發,觸發之後會執行一段邏輯。

5.model儲存狀態。

6.services與app之外的部分通訊(比如接入facebook)。