zoukankan      html  css  js  c++  java
  • 观察者模式,中介者模式,单例模式,手写事件总线原理(EventBus)

    观察者模式

    概念

    观察者模式是一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

    示例代码

    /* 需求:老师通过公告告诉学生考试
    *
    * 简单设计思路:观察者类提供接收公告类消息的方法=》学生类继承观察者类=》老师类循环观察者集合逐一进行公告
    *
    * 详细设计思路:
    * 1 公告类:提供公告消息属性
    * 2 观察者:抽象类,【接收】抽象方法参数是【公告类】
    * 3 学生类:
    * 3.1 继承【观察者类】
    * 3.2 构造函数参数是学生姓名
    * 3.3 重写观察者的【接收】抽象方法
    * 4 老师类(重点):
    * 4.1 依赖于【观察者】集合
    * 4.2 添加观察者方法,参数是【观察者类】
    * 4.3 【发送公告】方法:参数是【公告类】
    * 4.3.1 方法内循环【观察者类】集合
    * 4.3.1.1 循环内调用【观察者类】的接收方法,参数是4.3的公告类实例
    *
    * 调用方式:
    * 1 创建学生客户端:抽象【观察者类】 实例 学生类
    * 2 创建老师段:老师类实例,通过实例的(添加【观察者】方法)添加学生客户端
    * 3 发送通告:老师类实例的发送通告方法发送消息
    */

        // 公告类
        class Notice
        {
            public string Message { set; get; }//公告消息属性
        }
    
        // 观察者类
       abstract class IObserver
       {
            public abstract void Receive(Notice notice); //【接收】抽象方法
        }
    
        // 学生类:继承抽象类【观察者】
        class LStudentClient : IObserver
        {
            private string Name; // 学生姓名
    
            public LStudentClient(string name)
            {
                Name = name;
            }
    
            /// <summary>
            /// 接受公告
            /// </summary>
            public override void Receive(Notice notice)
            {
                Console.WriteLine($"{Name}收到通知:{notice.Message},开始准备考试");
            }
      }
    // 老师类 class Teacher { // 依赖抽象 public IList<IObserver> observers = new List<IObserver>(); public void AddObserver(IObserver observer) //构造函数 { observers.Add(observer); } public void SendNotice(Notice notice) //通过公告方法 { // 创建一个事件 foreach (IObserver observer in observers) { observer.Receive(notice); } //性能问题:可通过多线程、线程池来解决 }
      }
    //观察者使用方式: { // 1、创建学生客户端 IObserver lStudentClient = new LStudentClient("李学生"); IObserver zStudentClient = new ZStudentClient("张学生"); IObserver JStudentClient = new ZStudentClient("jack学生"); // 2、创建老师 Teacher teacher = new Teacher(); teacher.AddObserver(lStudentClient); teacher.AddObserver(zStudentClient); teacher.AddObserver(JStudentClient); // 3、发送通告 teacher.SendNotice(new Notice { Message = "考试" }); }

    场景

    存在着一对多关系的时候,通知场景

    总结

    1、目标角色
    2、观察者角色
    3、将目标角色和观察者角色解耦

    缺点

    1、会存在循环依赖的问题
    2、循环发送,会导致性能比价低,可通过多线程、线程池解决

    观察者模式与中介者模式区别

    观察者模式
    1、只有一个一个发起端,然后进行通知
    2、一对多的关系
    中介者
    1、所有的都是发起端,所有的都能通知
    2、多对多

    中介者模式

    概念

    软件工程领域,中介者模式定义了一个中介者对象,该对象封装了系统中对象间的交互方式。 由于它可以在运行时改变程序的行为,这种模式是一种行为型模式 。

    通常程序由大量的组成,这些类中包含程序的逻辑和运算。 然而,当开发者将更多的类加入到程序中之后,类间交互关系可能变得更为复杂,这会使得代码变得更加难以阅读和维护,尤其是在重构的时候。 此外,程序将会变得难以修改,因为对其所做的任何修改都有可能影响到其它几个类中的代码。

    中介者模式中,对象间的通信过程被封装在一个中介者(调解人)对象之中。 对象之间不再直接交互,而是通过调解人进行交互。 这么做可以减少可交互对象间的依赖,从而降低耦合

    示例代码 

    /* 需求:几个人在房间内玩金花,大家通过房间来发送/接收消息
    *
    * 简单设计思路:【客户端类】实现【群聊客户端接口】,且依赖【中介者房间类】进行消息通信
    *
    * 详细设计思路:
    * 1 群聊客户端接口:
    * 1.1 接受消息方法名
    * 1.2 发送消息方法名
    *
    * 2 中介者房间类:
    * 2.1 群聊接口集合
    * 2.2 注册客户端方法(参数是群聊接口)
    * 2.2.1 方法体:群聊接口集合添加群聊接口
    * 2.3 发送消息(群发)(参数是string消息)
    * 2.3.1 方法体:循环群聊接口集合,循环接口中的接收消息方法
    *
    * 3 客户端类:实现群聊客户端接口
    * 3.1 构造函数参数是客户端名,实例该类时传入的参数
    * 3.2 依赖中介者:创建中介者属性
    * 3.3 实现群聊客户端接口的接收消息、发送消息方法
    *
    * 使用方式:
    * 1 创建中介者房间类实例
    * 2 创建客户端类实例,传入参数为客户端名,且实例的中介者属性等于上一步的房间中介者类实例
    * 3 用房间中介者实例的注册客户端方法注册客户端实例
    * 4 客户端实例发送消息
    */

        /// <summary>
        /// 群聊客户端:接口,有两个方法名,接受消息和发送消息
        /// </summary>
        interface IClient
        {
            /// <summary>
            /// 接受消息
            /// </summary>
            public void Receive(string message);
    
            /// <summary>
            /// 发送消息
            /// </summary>
            public void Send(string message);
        }
    
        /// <summary>
        /// 房间中介者
        /// 作用:协调客户端
        /// </summary>
        class RoomMediator
        {
            /// <summary>
            /// 聊天客户端
            /// </summary>
            public IList<IClient> clients = new List<IClient>();
    
            /// <summary>
            /// 注册客户端
            /// </summary>
            public void RegistryClient(IClient client)
            {
                clients.Add(client);
            }
    
            /// <summary>
            /// 发送消息(群发),备注:还可以增加一个私聊的方法
            /// </summary>
            /// <param name="message"></param>
            public void SendMessage(string message)
            {
                foreach (IClient client in clients)
                {
                    // 1、所有客户端接收到消息 
                    client.Receive(message);
                }
            }
      }
    /// <summary> /// 张三客户端类:实现群聊接口,依赖中介者 /// </summary> class ZSClient : IClient { private string Name; public ZSClient(string name) { Name = name; } /// <summary> /// 依赖中介者 /// </summary> public RoomMediator roomMediator { set; get; } public void Receive(string message) { Console.WriteLine($"{Name}接受到消息:{message}"); } public void Send(string message) { // 将消息发送到房间 roomMediator.SendMessage(message); }
      }
    //中介者使用方式 // 1、创建房间中介者类 RoomMediator roomMediator = new RoomMediator(); //2、创建客户端 ZSClient clientZ = new ZSClient("张三"); clientZ.roomMediator = roomMediator; LSClient clientL = new LSClient("李四"); clientL.roomMediator = roomMediator; ZSClient clientW = new ZSClient("王五"); clientW.roomMediator = roomMediator; HHClient clientH = new HHClient("吼吼"); clientW.roomMediator = roomMediator; //3、注册客户端 roomMediator.RegistryClient(clientZ); roomMediator.RegistryClient(clientL); roomMediator.RegistryClient(clientW); roomMediator.RegistryClient(clientH); //4、客户端发送消息 clientZ.Send("搞金花"); clientL.Send("8点不见不散");

    条件

    1. 客户端角色
    2. 抽象客户端角色
    3. 房间角色

    总结

    优点:将原有多对多的关系,转换成1对一的关系,降低了复杂度
    符合迪米特原则
    缺点:存在复杂的应用关系,维护起来比价麻烦

    单例模式

    概念

    在应用这个模式时,单例对象的必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。 

    实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

    单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

    类型:

    1. 懒汉方式。指全局的单例实例在第一次被使用时构建。简单情景使用,启动快。
    2. 饿汉方式。指全局的单例实例在类装载时构建
      1. 线程安全式
      2. 线程不安全式
    3. 双检锁/双重校验锁式 (最安全的,推荐使用)

    破坏单例的两种方式:反射和序列化

    参考:破坏单例的两种方式 

    核心条件(必要条件)

    1. 构造函数私有化
    2. 自己创建自己

    优缺点

    优点

    1. 实例控制
      单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
    2. 灵活性
      因为类控制了实例化过程,所以类可以灵活更改实例化过程。

    缺点

    1. 开销
      虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。
    2. 可能的开发混淆
      使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用new关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
    3. 对象生存期
      不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用。

    示例代码

    懒汉方式、饿汉方式、双检锁/双重校验锁式

        /// <summary>
        /// 饿汉式单例
        /// </summary>
        class EagerSingleton
        {
            private static EagerSingleton instance = new EagerSingleton();
            private EagerSingleton() { }
            public static EagerSingleton getInstance()
            {
                return instance;
            }
        }
    
        /// <summary>
        /// 懒汉式--线程安全
        /// </summary>
        class SafetyLazySingleton
        {
            private static SafetyLazySingleton instance = null;
            private static readonly object padlock = new object();
            private SafetyLazySingleton() { }
            public static SafetyLazySingleton Instance
            {
                get
                {
                    lock (padlock)
                    {
                        if (instance == null)
                        {
                            instance = new SafetyLazySingleton();
                        }
                        return instance;
                    }
                }
            }
        }
    
        /// <summary>
        /// 懒汉式--线程不安全
        /// </summary>
        class NoSafetyLazySingleton
        {
            private static NoSafetyLazySingleton instance = null;
            private NoSafetyLazySingleton() { }
    
            public static NoSafetyLazySingleton Instance
            {
                get
                {
                    if (instance == null)
                    {
                        instance = new NoSafetyLazySingleton();
                    }
                    return instance;
                }
            }
        }
    
        /// <summary>
        /// 双检锁 / 双重校验锁式 单例 (最安全的,推荐使用)
        /// </summary>
        class DoubleCheckLockSingleton
        {
            //1、自己创建自己(静态):创建 SingleObject 的一个对象
            private volatile static DoubleCheckLockSingleton instance = null;
            private static readonly object padlock = new object();
    
            //2、构造函数私有化:让构造函数为 private,这样该类就不会被实例化(核心代码)
            private DoubleCheckLockSingleton() { }
    
            public static DoubleCheckLockSingleton Instance
            {
                get
                {
                    if (instance == null) //多线程时保证所有对象都初始化后,不需要等待
                    {
                        lock (padlock) //【锁】保证只有一个线程进去判断
                        {
                            if (instance == null) //保证对象为空再创建
                            {
                                instance = new DoubleCheckLockSingleton();
                            }
                        }
                    }
                    return instance;
                }
            }
        }

    单例模拟数据库连接池,提高复用性 

    /* 需求:模拟连接池连接数据库,复用可用连接,只需要创建几次连接就可以
    *
    * 设计思路:
    * 1 数据库连接类--模拟
    * 1.1 状态码属性:例如:0 创建,1 闲置 2 没使用
    * 1.2 构造函数进行连接 (这里只是模拟,实际情况可以传入连接数据库信息作为参数)
    * 1.3 关闭连接方法
    *
    * 2 连接池类--模拟
    * 2.1 用自己类创建自己
    * 2.2 获取唯一对象的方法,使用上一步自己创建自己的实例
    * 2.3 创建连接对象的集合
    * 2.4 私有构造函数中初始化连接:给连接集合中添加多个连接实例
    * 2.5 获取连接集合的方法
    *
    * 使用线程池方法:
    * 1 连接池实例=连接池的获取唯一可用对象
    * 2 实例调用获取连接对象集合方法
    */

    代码如下:

        /// <summary>
        /// 数据库连接类--模拟
        /// </summary>
        class Connection
        {
            private int status = 0;//状态码:例如:0 创建,1 闲置 2 没使用 
            public Connection()
            {
                Thread.Sleep(1000);
                Console.WriteLine($"创建Connection耗时1s");
            }
            public void Close()
            {
                Thread.Sleep(1000);
                Console.WriteLine($"关闭Connection耗时1s");
            }
        }
    
        /// <summary>
        /// 连接池(数据源)--模拟,为了简单测试,使用的是饿汉式单例,实际使用要改为双检锁模式的单例
        /// 1 用自己类创建自己
        /// 2 获取唯一对象的方法,使用上一步自己创建自己的实例
        /// 3 创建连接对象的集合
        /// 4 私有构造函数中初始化连接:给连接集合中添加多个连接实例
        /// 5 获取连接集合的方法
        /// </summary>
        class PoolDataSource
        {
            /// <summary>
            /// 自己创建自己(静态)
            /// </summary>
            private static PoolDataSource poolDataSource = new PoolDataSource();
            /// <summary>
            /// 构造函数私有化
            /// </summary>
            private PoolDataSource()
            {
                // 初始化连接数量:给连接集合中添加多个连接实例
                connections.Add(new Connection());
                connections.Add(new Connection());
                connections.Add(new Connection());
                connections.Add(new Connection());
                connections.Add(new Connection());
            }
            /// <summary>
            /// 获取唯一可用的对象
            /// </summary>
            /// <returns></returns>
            public static PoolDataSource GetInstance()
            {
                return poolDataSource;
            }
            /// <summary>
            /// connection集合(10个)
            /// </summary>
            private IList<Connection> connections = new List<Connection>();
    
            /// <summary>
            /// 获取连接集合的方法
            /// </summary>
            /// <returns></returns>
            public Connection GetConnection()
            {
                return connections[0];
            }
        }
    
    //连接池使用方式
                    //// 不使用连接池,每次都需要创建连接
                    //for (int i = 0; i < 100; i++)
                    //{
                    //    Connection connection = new Connection();
                    //    connection.Close();
                    //}
    
                    //// 使用连接池,但是没有使用单例模式,每次都需要new 一次
                    //for (int i = 0; i < 100; i++)
                    //{
                    //    PoolDataSource poolDataSource = new PoolDataSource();
                    //    Connection connection = poolDataSource.GetConnection();
                    //}
    
                    //使用连接池:把连接池改为单例模式:只要创建几次就行,后面直接复用,避免需要创建100次实例
                    for (int i = 0; i < 100; i++)
                    {
                        PoolDataSource poolDataSource = PoolDataSource.GetInstance(); //获取唯一可用对象
                        poolDataSource.GetConnection(); //获取连接对象的集合
                    }

    EFCore DbContextPool连接池探究

    参考EFCore的DbContextPool(连接池)

    发现DbContextPool不是使用单例模式的,单在public的够好函数中还是会判断在第一次访问时才创建连接

    手写事件总线EventBus原理

    参考

    事件总线--课件 

    事件总线知多少(1) - 「圣杰」 - 博客园

    EventBus--事件总线:观察者模式的拓展

    什么是事件总线

    事件总线就是用来简化应用中组件或者模块间的通信,从而实现模块间解耦的目的。基于观察者模式它使用发布订阅的方式支持组件和模块间的通信,摒弃了观察者模式需要显式注册回调的缺点。

    • 观察者:
      观察者模式是一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

    • 发布订阅模式:
      定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

    作用

    事件总线是一种机制,它允许不同的组件彼此通信而不彼此了解。 组件可以将事件发送到Eventbus,而无需知道是谁来接听或有多少其他人来接听。 组件也可以侦听Eventbus上的事件,而无需知道谁发送了事件。 这样,组件可以相互通信而无需相互依赖。 同样,很容易替换一个组件。 只要新组件了解正在发送和接收的事件,其他组件就永远不会知道.

    优点

    1、解耦
    2、提高重用性
    3、提高扩展性

    条件

    1、事件角色
    2、事件处理器角色
    3、事件总线角色

    事件总线4要素

    • 事件:所有的行为和动作都叫事件
    • 事件源:产生这些行为和动作的主要事件源
    • 事件处理器 (观察者):处理事件的角色
    • 消息总线:传播事件给事件处理器

    用老师发公告给学生举例说明:老师(事件源)通过类内的方法发起公告(事件)经过(事件总线)发给学生(事件处理器)处理

    用张三打车举例说明:张三(事件源)发送打车需求(事件)通过滴滴(事件总线)叫司机(事件处理器

    疑问

    按照概念理解:张三(事件源)发送打车需求(事件)通过滴滴(事件总线)叫司机(事件处理器)

    张三的需求是打车,不关心是那个司机,通过滴滴自动安排就行,张三和司机没有依赖关联

    疑问是:滴滴可以一对一的安排司机给乘客,单实际使用中,事件总线没有这么简单吧?会涉及到一堆一、一对多,多对多的关系吧?这些情况事件总线是如何分配的呢?

    是不是事件源发送事件的时候,事件就已经包含了具体的要求,例如要一对多,然后事件总线根据这些要求,发送给符合要求的事件总线

    扩展

    查看连接池、线程池、CAP源码

    如有错误,欢迎您指出。
    本文版权归作者和博客园共有,欢迎转载,但必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    英语俚语里的gotta和gonna
    如何设置Win XP远程登录如何远程控制电脑
    C#中as与is的用法(收藏)
    just用法
    even用法
    up to用法小结
    go out with用法
    realize与recognize辨析
    go through用法
    堆优先队列
  • 原文地址:https://www.cnblogs.com/qingyunye/p/13340769.html
Copyright © 2011-2022 走看看