精品日产一卡2卡三卡4卡在线_久久永久免费人妻精品_欧美日韩精品久久久免费观看_一女4P三黑人免费视频

當(dāng)前位置: 網(wǎng)站首頁  >> 知識庫  >> .Net  >> 查看詳情

NET編程之事件總線(Event Bus)

發(fā)布時(shí)間:2019-11-01 14:10:24  瀏覽次數(shù):4373 
  事件總線這個(gè)概念對你來說可能很陌生,但提到觀察者(發(fā)布-訂閱)模式,你也許就很熟悉。事件總線是對發(fā)布-訂閱模式的一種實(shí)現(xiàn)。它是一種集中式事件處理機(jī)制,允許不同的組件之間進(jìn)行彼此通信而又不需要相互依賴,達(dá)到一種解耦的目的。

  1. 引言

  事件總線這個(gè)概念對你來說可能很陌生,但提到觀察者(發(fā)布-訂閱)模式,你也許就很熟悉。事件總線是對發(fā)布-訂閱模式的一種實(shí)現(xiàn)。它是一種集中式事件處理機(jī)制,允許不同的組件之間進(jìn)行彼此通信而又不需要相互依賴,達(dá)到一種解耦的目的。

  我們來看看事件總線的處理流程:

  

http://www.lmwlove.com/ac/ID1187

 

  了解了事件總線的基本概念和處理流程,下面我們就來分析下如何去實(shí)現(xiàn)事件總線。

  2.回歸本質(zhì)

  在動(dòng)手實(shí)現(xiàn)事件總線之前,我們還是要追本溯源,探索一下事件的本質(zhì)和發(fā)布訂閱模式的實(shí)現(xiàn)機(jī)制。

  2.1.事件的本質(zhì)

  我們先來探討一下事件的概念。都是讀過書的,應(yīng)該都還記得記敘文的六要素:時(shí)間、地點(diǎn)、人物、事件(起因、經(jīng)過、結(jié)果)。

  我們拿注冊的案例,來解釋一下。

  用戶輸入用戶名、郵箱、密碼后,點(diǎn)擊注冊,輸入無誤校驗(yàn)通過后,注冊成功并發(fā)送郵件給用戶,要求用戶進(jìn)行郵箱驗(yàn)證激活。

  這里面就涉及了兩個(gè)主要事件:

  1,注冊事件:起因是用戶點(diǎn)擊了注冊按鈕,經(jīng)過是輸入校驗(yàn),結(jié)果是是否注冊成功。

  2,發(fā)送郵件事件:起因是用戶使用郵箱注冊成功需要驗(yàn)證郵箱,經(jīng)過是郵件發(fā)送,結(jié)果是郵件是否發(fā)送成功。

  其實(shí)這六要素也適用于我們程序中事件的處理過程。開發(fā)過WinForm程序的都知道,我們在做UI設(shè)計(jì)的時(shí)候,從工具箱拖入一個(gè)注冊按鈕(btnRegister),雙擊它,VS就會自動(dòng)幫我們生成如下代碼:

  void btnRegister_Click(object sender, EventArgs e)

  {

  // 事件的處理

  }

  其中object sender指代發(fā)出事件的對象,這里也就是button對象;EventArgs e 事件參數(shù),可以理解為對事件的描述 ,它們可以統(tǒng)稱為事件源。其中的代碼邏輯,就是對事件的處理。我們可以統(tǒng)稱為事件處理。

  說了這么多,無非是想透過現(xiàn)象看本質(zhì):事件是由事件源和事件處理組成。

  2.2. 發(fā)布訂閱模式

  定義對象間一種一對多的依賴關(guān)系,使得每當(dāng)一個(gè)對象改變狀態(tài),則所有依賴于它的對象都會得到通知并被自動(dòng)更新。 ——發(fā)布訂閱模式

  發(fā)布訂閱模式主要有兩個(gè)角色:

  1,發(fā)布方(Publisher):也稱為被觀察者,當(dāng)狀態(tài)改變時(shí)負(fù)責(zé)通知所有訂閱者。

  2,訂閱方(Subscriber):也稱為觀察者,訂閱事件并對接收到的事件進(jìn)行處理。

  發(fā)布訂閱模式有兩種實(shí)現(xiàn)方式:

  1,簡單的實(shí)現(xiàn)方式:由Publisher維護(hù)一個(gè)訂閱者列表,當(dāng)狀態(tài)改變時(shí)循環(huán)遍歷列表通知訂閱者。

  2,委托的實(shí)現(xiàn)方式:由Publisher定義事件委托,Subscriber實(shí)現(xiàn)委托。

  總的來說,發(fā)布訂閱模式中有兩個(gè)關(guān)鍵字,通知和更新。

  被觀察者狀態(tài)改變通知觀察者做出相應(yīng)更新。

  解決的是當(dāng)對象改變時(shí)需要通知其他對象做出相應(yīng)改變的問題。

  如果畫一個(gè)圖來表示這個(gè)流程的畫,圖形應(yīng)該是這樣的:

  

http://www.lmwlove.com/ac/ID1187

 

  3 實(shí)現(xiàn)發(fā)布訂閱模式

  相信通過上面的解釋,對事件和發(fā)布訂閱模式有了一個(gè)大概的印象。都說理論要與實(shí)踐相結(jié)合,所以我們還是動(dòng)動(dòng)手指敲敲代碼比較好。

  我將以『觀察者模式』來釣魚這個(gè)例子為基礎(chǔ),通過重構(gòu)的方式來完善一個(gè)更加通用的發(fā)布訂閱模式。

  先上代碼:

  ///

 

  /// 魚的品類枚舉

  ///

 

  public enum FishType

  {

  鯽魚,

  鯉魚,

  黑魚,

  青魚,

  草魚,

  鱸魚

  }

  釣魚竿的實(shí)現(xiàn):

  ///

 

  /// 魚竿(被觀察者)

  ///

 

  public class FishingRod

  {

  public delegate void FishingHandler(FishType type); //聲明委托

  public event FishingHandler FishingEvent; //聲明事件

  public void ThrowHook(FishingMan man)

  {

  Console.WriteLine("開始下鉤!");

  //用隨機(jī)數(shù)模擬魚咬鉤,若隨機(jī)數(shù)為偶數(shù),則為魚咬鉤

  if (new Random().Next() % 2 == 0)

  {

  var type = (FishType) new Random().Next(0, 5);

  Console.WriteLine("鈴鐺:叮叮叮,魚兒咬鉤了");

  if (FishingEvent != null)

  FishingEvent(type);

  }

  }

  }

  垂釣者:

  ///

 

  /// 垂釣者(觀察者)

  ///

 

  public class FishingMan

  {

  public FishingMan(string name)

  {

  Name = name;

  }

  public string Name { get; set; }

  public int FishCount { get; set; }

  ///

 

  /// 垂釣者自然要有魚竿啊

  ///

 

  public FishingRod FishingRod { get; set; }

  public void Fishing()

  {

  this.FishingRod.ThrowHook(this);

  }

  public void Update(FishType type)

  {

  FishCount++;

  Console.WriteLine("{0}:釣到一條[{2}],已經(jīng)釣到{1}條魚了!", Name, FishCount, type);

  }

  }

  場景類也很簡單:

  //1、初始化魚竿

  var fishingRod = new FishingRod();

  //2、聲明垂釣者

  var jeff = new FishingMan("圣杰");

  //3.分配魚竿

  jeff.FishingRod = fishingRod;

  //4、注冊觀察者

  fishingRod.FishingEvent += jeff.Update;

  //5、循環(huán)釣魚

  while (jeff.FishCount < 5)

  {

  jeff.Fishing();

  Console.WriteLine("-------------------");

  //睡眠5s

  Thread.Sleep(5000);

  }

  代碼很簡單,相信你一看就明白。但很顯然這個(gè)代碼實(shí)現(xiàn)僅適用于當(dāng)前這個(gè)釣魚場景,假如有其他場景也想使用這個(gè)模式,我們還需要重新定義委托,重新定義事件處理,豈不很累。本著”Don't repeat yourself“的原則,我們要對其進(jìn)行重構(gòu)。

  結(jié)合我們對事件本質(zhì)的探討,事件是由事件源和事件處理組成。針對我們上面的案例來說,public delegate void FishingHandler(FishType type);這句代碼就已經(jīng)說明了事件源和事件處理。事件源就是FishType type,事件處理自然是注冊到FishingHandler上面的委托實(shí)例。

  問題找到了,很顯然是我們的事件源和事件處理不夠抽象,所以不能通用,下面咱們就來動(dòng)手改造。

  3.1. 提取事件源

  事件源應(yīng)該至少包含事件發(fā)生的時(shí)間和觸發(fā)事件的對象。

  我們提取IEventData接口來封裝事件源:

  ///

 

  /// 定義事件源接口,所有的事件源都要實(shí)現(xiàn)該接口

  ///

 

  public interface IEventData

  {

  ///

 

  /// 事件發(fā)生的時(shí)間

  ///

 

  DateTime EventTime { get; set; }

  ///

 

  /// 觸發(fā)事件的對象

  ///

 

  object EventSource { get; set; }

  }

  自然我們應(yīng)該給一個(gè)默認(rèn)的實(shí)現(xiàn)EventData:

  ///

 

  /// 事件源:描述事件信息,用于參數(shù)傳遞

  ///

 

  public class EventData : IEventData

  {

  ///

 

  /// 事件發(fā)生的時(shí)間

  ///

 

  public DateTime EventTime { get; set; }

  ///

 

  /// 觸發(fā)事件的對象

  ///

 

  public Object EventSource { get; set; }

  public EventData()

  {

  EventTime = DateTime.Now;

  }

  }

  針對Demo,擴(kuò)展事件源如下:

  public class FishingEventData : EventData

  {

  public FishType FishType { get; set; }

  public FishingMan FisingMan { get; set; }

  }

  完成后,我們就可以去把在FishingRod聲明的委托參數(shù)類型改為FishingEventData類型了,即public delegate void FishingHandler(FishingEventData eventData); //聲明委托;

  然后修改FishingMan的Update方法按委托定義的參數(shù)類型修改即可,代碼我就不放了,大家自行腦補(bǔ)。

  到這一步我們就統(tǒng)一了事件源的定義方式。

  3.2.提取事件處理器

  事件源統(tǒng)一了,那事件處理也得加以限制。比如如果隨意命名事件處理方法名,那在進(jìn)行事件注冊的時(shí)候還要去按照委托定義的參數(shù)類型去匹配,豈不麻煩。

  我們提取一個(gè)IEventHandler接口:

  ///

 

  /// 定義事件處理器公共接口,所有的事件處理都要實(shí)現(xiàn)該接口

  ///

 

  public interface IEventHandler

  {

  }

  事件處理要與事件源進(jìn)行綁定,所以我們再來定義一個(gè)泛型接口:

  ///

 

  /// 泛型事件處理器接口

  ///

 

  ///

  public interface IEventHandler : IEventHandler where TEventData : IEventData

  {

  ///

 

  /// 事件處理器實(shí)現(xiàn)該方法來處理事件

  ///

 

  ///

  void HandleEvent(TEventData eventData);

  }

  你可能會納悶,為什么先定義了一個(gè)空接口?這里就留給自己思考吧。

  至此我們就完成了事件處理的抽象。我們再繼續(xù)去改造我們的Demo。我們讓FishingMan實(shí)現(xiàn)IEventHandler接口,然后修改場景類中將fishingRod.FishingEvent += jeff.Update;改為fishingRod.FishingEvent += jeff.HandleEvent;即可。代碼改動(dòng)很簡單,同樣在此略去。

  至此你可能覺得我們完成了對Demo的改造。但事實(shí)上呢,我們還要弄清一個(gè)問題——如果這個(gè)FishingMan訂閱的有其他的事件,我們該如何處理?

  聰穎如你,你立馬想到了可以通過事件源來進(jìn)行區(qū)分處理。

  public class FishingMan : IEventHandler

  {

  //省略其他代碼

  public void HandleEvent(IEventData eventData)

  {

  if (eventData is FishingEventData)

  {

  //do something

  }

  if(eventData is XxxEventData)

  {

  //do something else

  }

  }

  }

  至此,這個(gè)模式實(shí)現(xiàn)到這個(gè)地步基本已經(jīng)可以通用了。

  4. 實(shí)現(xiàn)事件總線

  通用的發(fā)布訂閱模式不是我們的目的,我們的目的是一個(gè)集中式的事件處理機(jī)制,且各個(gè)模塊之間相互不產(chǎn)生依賴。那我們?nèi)绾巫龅侥?同樣我們還是一步一步的進(jìn)行分析改造。

  4.1.分析問題

  思考一下,每次為了實(shí)現(xiàn)這個(gè)模式,都要完成以下三步:

  1,事件發(fā)布方定義事件委托

  2,事件訂閱方定義事件處理邏輯

  3,顯示的訂閱事件

  雖然只有三步,但這三步已經(jīng)很繁瑣了。而且事件發(fā)布方和事件訂閱方還存在著依賴(體現(xiàn)在訂閱者要顯示的進(jìn)行事件的注冊和注銷上)。而且當(dāng)事件過多時(shí),直接在訂閱者中實(shí)現(xiàn)IEventHandler接口處理多個(gè)事件邏輯顯然不太合適,違法單一職責(zé)原則。這里就暴露了三個(gè)問題:

  1,如何精簡步驟?

  2,如何解除發(fā)布方與訂閱方的依賴?

  3,如何避免在訂閱者中同時(shí)處理多個(gè)事件邏輯?

  帶著問題思考,我們就會更接近真相。

  想要精簡步驟,那我們需要尋找共性。共性就是事件的本質(zhì),也就是我們針對事件源和事件處理提取出來的兩個(gè)接口。

  想要解除依賴,那就要在發(fā)布方和訂閱方之間添加一個(gè)中介。

  想要避免訂閱者同時(shí)處理過多事件邏輯,那我們就把事件邏輯的處理提取到訂閱者外部。

  思路有了,下面我們就來實(shí)施吧。

  4.2.解決問題

  本著先易后難的思想,我們下面就來解決以上問題。

  4.2.1. 實(shí)現(xiàn)IEventHandler

  我們先解決上面的第三個(gè)問題:如何避免在訂閱者中同時(shí)處理多個(gè)事件邏輯?

  自然是針對不同的事件源IEventData實(shí)現(xiàn)不同的IEventHandler。改造后的釣魚事件處理邏輯如下:

  ///

 

  /// 釣魚事件處理

  ///

 

  public class FishingEventHandler : IEventHandler

  {

  public void HandleEvent(FishingEventData eventData)

  {

  eventData.FishingMan.FishCount++;

  Console.WriteLine("{0}:釣到一條[{2}],已經(jīng)釣到{1}條魚了!",

  eventData.FishingMan.Name, eventData.FishingMan.FishCount, eventData.FishType);

  }

  }

  這時(shí)我們就可以移除在FishingMan中實(shí)現(xiàn)的IEventHandler接口了。

  然后將事件注冊改為fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;即可。

  4.2.2. 統(tǒng)一注冊事件

  上一個(gè)問題的解決,有助于我們解決第一個(gè)問題:如何精簡流程?

  為什么呢,因?yàn)槲覀兪歉鶕?jù)事件源定義相應(yīng)的事件處理的。也就是我們之前說的可以根據(jù)事件源來區(qū)分事件。

  然后呢?反射,我們可以通過反射來進(jìn)行事件的統(tǒng)一注冊。

  在FishingRod的構(gòu)造函數(shù)中使用反射,統(tǒng)一注冊實(shí)現(xiàn)了IEventHandler類型的實(shí)例方法HandleEvent:

  public FishingRod()

  {

  Assembly assembly = Assembly.GetExecutingAssembly();

  foreach (var type in assembly.GetTypes())

  {

  if (typeof(IEventHandler).IsAssignableFrom(type))//判斷當(dāng)前類型是否實(shí)現(xiàn)了IEventHandler接口

  {

  Type handlerInterface = type.GetInterface("IEventHandler`1");//獲取該類實(shí)現(xiàn)的泛型接口

  Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 獲取泛型接口指定的參數(shù)類型

  //如果參數(shù)類型是FishingEventData,則說明事件源匹配

  if (eventDataType.Equals(typeof(FishingEventData)))

  {

  //創(chuàng)建實(shí)例

  var handler = Activator.CreateInstance(type) as IEventHandler;

  //注冊事件

  FishingEvent += handler.HandleEvent;

  }

  }

  }

  }

  這樣,我們就可以移出場景類中的顯示注冊代碼fishingRod.FishingEvent += new FishingEventHandler().HandleEvent;。

  4.2.3. 解除依賴

  如何解除依賴呢?其實(shí)答案就在本文的兩張圖上,仔細(xì)對比我們可以很直觀的看到,Event Bus就相當(dāng)于一個(gè)介于Publisher和Subscriber中間的橋梁。它隔離了Publlisher和Subscriber之間的直接依賴,接管了所有事件的發(fā)布和訂閱邏輯,并負(fù)責(zé)事件的中轉(zhuǎn)。

  Event Bus終于要粉墨登場了!!!

  分析一下,如果EventBus要接管所有事件的發(fā)布和訂閱,那它則需要有一個(gè)容器來記錄事件源和事件處理。那又如何觸發(fā)呢?有了事件源,我們就自然能找到綁定的事件處理邏輯,通過反射觸發(fā)。代碼如下:

  ///

 

  /// 事件總線

  ///

 

  public class EventBus

  {

  public static EventBus Default = > new EventBus();

  ///

 

  /// 定義線程安全集合

  ///

 

  private readonly ConcurrentDictionary > _eventAndHandlerMapping;

  public EventBus()

  {

  _eventAndHandlerMapping = new ConcurrentDictionary >();

  MapEventToHandler();

  }

  ///

 

  ///通過反射,將事件源與事件處理綁定

  ///

 

  private void MapEventToHandler()

  {

  Assembly assembly = Assembly.GetEntryAssembly();

  foreach (var type in assembly.GetTypes())

  {

  if (typeof(IEventHandler).IsAssignableFrom(type))//判斷當(dāng)前類型是否實(shí)現(xiàn)了IEventHandler接口

  {

  Type handlerInterface = type.GetInterface("IEventHandler`1");//獲取該類實(shí)現(xiàn)的泛型接口

  if (handlerInterface != null)

  {

  Type eventDataType = handlerInterface.GetGenericArguments()[0]; // 獲取泛型接口指定的參數(shù)類型

  if (_eventAndHandlerMapping.ContainsKey(eventDataType))

  {

  List handlerTypes = _eventAndHandlerMapping[eventDataType];

  handlerTypes.Add(type);

  _eventAndHandlerMapping[eventDataType] = handlerTypes;

  }

  else

  {

  var handlerTypes = new List { type };

  _eventAndHandlerMapping[eventDataType] = handlerTypes;

  }

  }

  }

  }

  }

  ///

 

  /// 手動(dòng)綁定事件源與事件處理

  ///

 

  ///

  ///

  public void Register(Type eventHandler)

  {

  List handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];

  if (!handlerTypes.Contains(eventHandler))

  {

  handlerTypes.Add(eventHandler);

  _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;

  }

  }

  ///

 

  /// 手動(dòng)解除事件源與事件處理的綁定

  ///

 

  ///

  ///

  public void UnRegister(Type eventHandler)

  {

  List handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];

  if (handlerTypes.Contains(eventHandler))

  {

  handlerTypes.Remove(eventHandler);

  _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;

  }

  }

  ///

 

  /// 根據(jù)事件源觸發(fā)綁定的事件處理

  ///

 

  ///

  ///

  public void Trigger(TEventData eventData) where TEventData : IEventData

  {

  List handlers = _eventAndHandlerMapping[eventData.GetType()];

  if (handlers != null && handlers.Count > 0)

  {

  foreach (var handler in handlers)

  {

  MethodInfo methodInfo = handler.GetMethod("HandleEvent");

  if (methodInfo != null)

  {

  object obj = Activator.CreateInstance(handler);

  methodInfo.Invoke(obj, new object[] { eventData });

  }

  }

  }

  }

  }

  事件總線主要定義三個(gè)方法,注冊、取消注冊、事件觸發(fā)。還有一點(diǎn)就是我們在構(gòu)造函數(shù)中通過反射去進(jìn)行事件源和事件處理的綁定。

  代碼注釋已經(jīng)很清楚了,這里就不過多解釋了。

  下面我們就來修改Demo,修改FishingRod的事件觸發(fā):

  ///

 

  /// 下鉤

  ///

 

  public void ThrowHook(FishingMan man)

  {

  Console.WriteLine("開始下鉤!");

  //用隨機(jī)數(shù)模擬魚咬鉤,若隨機(jī)數(shù)為偶數(shù),則為魚咬鉤

  if (new Random().Next() % 2 == 0)

  {

  var a = new Random(10).Next();

  var type = (FishType)new Random().Next(0, 5);

  Console.WriteLine("鈴鐺:叮叮叮,魚兒咬鉤了");

  if (FishingEvent != null)

  {

  var eventData = new FishingEventData() { FishType = type, FishingMan = man };

  //FishingEvent(eventData);//不再需要通過事件委托觸發(fā)

  EventBus.Default.Trigger(eventData);//直接通過事件總線觸發(fā)即可

  }

  }

  }

  至此,事件總線的雛形已經(jīng)形成!

  5.事件總線的總結(jié)

  通過上面一步一步的分析和實(shí)踐,發(fā)現(xiàn)事件總線也不是什么高深的概念,只要我們自己善于思考,勤于動(dòng)手,也能實(shí)現(xiàn)自己的事件總線。

  根據(jù)我們的實(shí)現(xiàn),大概總結(jié)出以下幾條:

  1,事件總線維護(hù)一個(gè)事件源與事件處理的映射字典;

  2,通過單例模式,確保事件總線的唯一入口;

  3,利用反射完成事件源與事件處理的初始化綁定;

  4,提供統(tǒng)一的事件注冊、取消注冊和觸發(fā)接口。

  最后,以上事件總線的實(shí)現(xiàn)只是一個(gè)雛形,還有很多潛在的問題。有興趣的不妨思考完善一下。

  來源:http://www.cnblogs.com/sheng-jie/p/6970091.html

標(biāo)簽:NET編程
聯(lián)系我們
在線咨詢 QQ客服 0731-88362910
地址:湖南省長沙市雷鋒大道1389號
如有問題,可在線提交表單