使用面向对象的思想 用c#控制台代码模拟猫抓老鼠

我们先来分析一下猫抓老鼠的过程

1.猫叫了

2.所有老鼠听到叫声,知道是哪只猫来了

3.老鼠们逃跑,边逃边喊:"xx猫来了,快跑啊!我是老鼠xxx"

一  双向耦合的代码

首先需要一个猫类Cat 一个老鼠类Rat 和一个测试类Program

老鼠类的代码如下

//老鼠类
public class Rat
{
public string Name { get; set; } //老鼠的名字
public Cat MyCat { get; set; } //老鼠遇到的猫 //老鼠逃跑的方法
public void Run()
{
Console.WriteLine(MyCat.Name +
"猫来了,大家快跑!!我是" + Name);
//打印出信息 包含了猫的名字和老鼠本身的名字
} //带参和无参构造
public Rat() { }
public Rat(string name, Cat cat)
{
this.Name = name;
this.MyCat = cat;
}
}

要让猫叫的时候依次打印出老鼠的逃跑方法,需要在Cat类里添加一个存放Rat对象的集合

Cat类的代码如下

public class Cat
{
public string Name { get; set; } //猫的名字
List<Rat> list = new List<Rat>(); //存放Rat对象的集合 //为集合增加老鼠
public void Add(Rat rat)
{
list.Add(rat);
} //移除
public void Remove(Rat rat)
{
list.Remove(rat);
} //猫叫的方法
public void Shout()
{
Console.WriteLine("喵喵喵!");
//如果集合非空,循环执行每只老鼠的Run()方法
if (list != null)
{
foreach (Rat item in list)
{
item.Run();
}
}
} public Cat() { }
public Cat(string name)
{
this.Name = name;
}
}

在Main方法中,我们需要构建几个Rat对象和一个Cat对象,将Rat对象添加到Cat对象的集合中,调用Cat对象的Shout方法

代码如下

static void Main(string[] args)
{
//构建一个Cat对象和两个Rat对象 老鼠遇到的猫是构建的cat
Cat cat = new Cat("Tom");
Rat rat1 = new Rat("Jerry", cat);
Rat rat2 = new Rat("TaoQi", cat); //调用猫类的Add方法添加老鼠对象
cat.Add(rat1);
cat.Add(rat2); //调用猫的Shout方法
cat.Shout(); Console.ReadKey();
}

运行结果如下

这样的代码缺陷很明显,Cat类和Rat类紧密耦合

猫可能不止要抓老鼠 还要抓小鸟

当然不止是猫会抓 也可能狗拿耗子多管闲事

于是我们可以把猫和狗提出来 继承自一个抽象类Pet

抓捕的小动物老鼠和小鸟没有什么关系 但是都能(逃)跑

先不去管小鸟是飞,我们把它们称作 可以跑的 都实现一个IRunable接口

二  观察者 模式(发布-订阅模式)

修改后的代码如下

新增抽象类Pet ,猫类继承自Pet   (猫类的代码变化不大 略去不写

public abstract class Pet
{
public List<IRunable> list = new List<IRunable>();
public void Add(IRunable i)
{
list.Add(i);
} public void Remove(IRunable i)
{
list.Remove(i);
} public abstract void Shout();
}

接口IRubable 里面定义一个Run方法

public interface IRunable
{
void Run();
}

老鼠Rat和鸟Bird两个类都实现了这个接口

以Bird为例 代码如下

class Bird : IRunable
{
//鸟的名字和遇到的猫
public string Name { get; set; }
public Cat MyCat { get; set; } public void Run()
{
Console.WriteLine(MyCat.Name + "猫来了,快跑吧!我是小鸟" + Name);
} public Bird() { }
public Bird(string name, Cat cat)
{
this.Name = name;
this.MyCat = cat;
}
}

Rat类的代码几乎没有变化

那么在Main方法中也只需要稍作修改,增加一个Bird对象 略去不写 执行后的结果如下

以上猫抓老鼠的例子实际上就是用了一个设计模式:观察者模式

观察者模式又名发布-订阅模式(Publish-Subscribe)

1.Subject类 (通知者 主题)
//抽象类 里面有一个Observer类集合
把所有对观察者对象的引用保存在一个聚集里,每个主题都可以有多个观察者.抽象主题提供一个接口,可以增加和删除观察着对象

2.Observer类 (观察者)
//抽象类
抽象观察者,为所有的具体观察者定义一个接口,在得到主题的更新时提醒自己

3.ConcreteObserver类
//父类是Observer类
具体观察者,实现抽象观察者角色所需求的更新接口,以便使本身的状态与主题的状态相协调

4.ConcreteSubject类
//父类是Subject
具体主题,将有关状态存入具体观察者对象,在具体主题的内部状态改变时,给所有登记过的观察者发出通知

观察者模式的特点

1.一个主题可以有任意数量依赖他的观察者,一旦主题的状态发生改变,所有观察者都可以得到通知
2.主题发出通知不需要知道具体观察者
3.具体观察者也不需要知道其他观察者的存在

但是

将一个系统分割成一系列相互作用的类有一个很不好的副作用,就是需要维护相关对象间的一致性,使得各个类紧密耦合,这样会给维护,扩展和重用都带来不便

应用时机:

当一个对象的改变需要同时改变其他对象的时候使用观察模式
不知道具体有多少对象有待改变时,应考虑使用观察者模式
一个抽象模型有两个方面,其中一个方面依赖于另一个方面

三 委托

举个栗子 正如之前所说,老鼠会跑Run,小鸟会飞Fly,这根本是两个毫不相干的方法

但是的确有相同点--它们的返回值类型都是空,传进的参数列表也都为空

我们怎么样能把这两个不相关的类Bird和Rat的对象都装到Cat中去,再判断是哪个类依次调用它们的方法?

其实我们可以直接拿出它们的方法来装到Cat中去.

//委托
委托是什么?
和类一样是一种用户自定义类型
委托提供了方法的抽象

委托存储的是一系列具有相同签名和返回值类型的方法的地址,调用委托的时候,委托包含的所有方法将被执行

1.定义委托
访问修饰符 delegate 返回值类型 委托名(参数列表);

public delegate void MyDel(int x);

2.声明和初始化
委托名 委托对象名;
委托对象名=new 委托名(类名.方法名);

MyDel del;
del=new MyDel(Cat.Shout);

委托名=方法名
del=Cat.Shout;

3.委托的运算
委托可以使用额外的运算符来组合.这个运算最终会创建一个新的委托,其调用列表是两个操作数的委托调用列表的副本连接.委托是恒定的,操作数委托创建后不会被改变,委托组合拷贝的是操作数的副本
MyDel del2=Cat.Catch;
MyDel del3=del+del2;

使用+=为委托新增方法
使用-=为委托移除方法
del+=Cat.Catch;
del-=Cat.Shout;

4.委托调用

if(del!=null){
del();//委托调用
}

5.匿名方法
匿名方法是在初始化委托时内联声明的方法
delegate(参数){代码块}

delegate int MyDel(int x);
MyDel del=delegate(int x){
return x;
};

//匿名方法不声明返回值

Lambda表达式
用来简化匿名方法的语法

MyDel del1 = (int x) => {return x;};

在参数列表和匿名方法之间放置Lambda运算符=>
Lambda运算符读作 goes to

MyDel del1 = x => {return x};
MyDel del1 = x => x;

定义委托像定义枚举一样,可以定义在类的外部

然后在类中可以创建委托的对象

比如 修改后的我们的程序

(只是一个测试 不写的很详细了 还是拿猫和老鼠举例子 猫类和老鼠类 以及定义的委托的代码如下)

//定义一个委托 名叫CatShoutEventHandler 没有返回值 参数列表为空
public delegate void CatShoutEventHandler(); //猫类
public class Cat
{
//在猫类里定义一个该委托的对象CatShout
public CatShoutEventHandler CatShout; public void Shout()
{
Console.WriteLine("喵喵喵");
//判断委托内是否为空,若不为空,执行该委托
if (CatShout != null)
{
CatShout();
}
}
} //老鼠类
public class Rat
{
public string Name { get; set; } //老鼠的名字 //老鼠的逃跑方法
public void Run()
{
Console.WriteLine(Name + "跑了!");
} //无参和带参构造
public Rat() { }
public Rat(string name)
{
this.Name = name;
}
}

那么接下来就是我们的Main方法

static void Main(string[] args)
{
//构建一个猫对象和两个老鼠对象
Cat cat = new Cat();
Rat rat1 = new Rat("Jerry");
Rat rat2 = new Rat("TaoQi"); //向cat的委托对象CatShout中依次添加老鼠对象的Run方法
//注!!添加的是整个方法 不需要加括号
cat.CatShout += rat1.Run;
cat.CatShout += rat2.Run; //调用cat的Shout方法
cat.Shout();
Console.ReadKey();
}

运行结果是这样的

然而 然而 然而

比如说我来做一个很贱的操作

在Main方法中来一个 cat.CatShout=null;

好了 不管Cat类中的CatShout有没有初始值或者有没有赋值过都没了

我们知道面向对象的的三大特征 封装,继承,多态

我们一样可以把委托封装起来,以控制它的访问权限

这就是事件

四 事件

事件(Event)是一个用户操作,或是一些特定的出现情况.应用程序需要在事件发生时响应事件

事件在类中声明且生成,且通过使用同一个类或者其他类中的委托与时间处理程序关联

声明事件
在类的内部声明事件,首先必须声明该事件的委托类型

public delegate void CatShoutEventHandler();

然后声明事件本身,使用 event 关键字

public event CatShoutEventHandler CatShout;

上面的代码定义了一个名为CatShoutEventHandler的委托和一个CatShout的事件,该事件在生成时会调用委托

声明事件后可以实例化事件,注册函数到事件解除事件函数注册方法

CatShout+=new CatShoutEventHandler(rat.Run);
CatShout+=rat2.Run;//将函数Run注册到事件CatSHout上

在使用事件的时候,如果在封装类的外部,则该事件只能出现在+=或-=的左边

所以 用事件封装过的代码我们再来一遍

额..好像顺理成章的就写了下去

代码就这里变了一行 唯一的变化就是多了个event关键字

我们在这里使用了一个高大上的工具 reflector

将我们的代码反编译之后,可以看到CatShout这个事件其实做了两件事

里面有两个方法 一个是add_CatShout(CatShoutEventHandler)  另一个是remov_Cat(CatShoutEventHandler)

分别对应这个事件的运算符号 +=  和 -=

所以其实事件就是相当于对委托的封装

五 Object sender和EventArgs e

然而我又要找问题了,我们翻回去看要求

有一点是老鼠知道猫的名字,要调用猫对象的Name属性,我们现在试着给猫加上这个属性

public string Name { get; set; }

我们要排除Cat类与Rat类的耦合,所以不能在Rat类中存放一个Cat对象

当然我们可以在老鼠的Run方法中增加传进去一个Cat对象,但是这样需要定义一个这个程序自己使用的委托类型

系统已经有一些定义好的委托类型

public delegate void Action();
public delegate void EventHandler(object sender, EventArgs e);

第一个委托叫Action 它是没有参数 没有返回值类型的

第二个叫做EventHandler 它有两个参数 Object类型的sender 和 EventArgs类型的e

第一个还好,第二个,我去,这是什么东西啊?

其实第二个这个委托类型我们都十分熟悉 因为在winForm窗体应用程序中,控件生成的方法就带着这两个参数

我们一般把触发事件的整个对象封装成Object类型 做第一个参数

而第二个参数呢 我们首先需要知道什么是EventArgs

    [Serializable, ComVisible(true), __DynamicallyInvokable]
public class EventArgs
{
[__DynamicallyInvokable]
public static readonly EventArgs Empty = new EventArgs();
}

这是代码 首先我们知道了,这是个类

是系统定义的类,里面只有一个静态的readonly的自身变量 为空

也就是通过调用这个静态方法来返回一个新的(空的)EventArgs本身

要弄懂它,我们来先看一个例子

这是WinForm窗体中的TreeView的事件对应在窗体类中生成的方法

 private void tvList_AfterSelect(object sender, TreeViewEventArgs e)

我们可以看到 诶 诶 里面传进去的第一个参数没错是object sender 然而第二个变成了TreeViewEventArgs类型的e

那这个TreeViewEventArgs是什么东西呢

首先没错的是它继承自EventArgs

然后它加多了许多方法,我们知道的是它可以返回我们当前选中的那个节点(而sender则代表的是整个TreeView控件)

sa,我们可以把要用到的一些属性封装到EventArgs之中 来排除两个类的耦合

应用到猫和鼠的例子中去,我们最后的代码是这样子的

//自定义的CatShoutEventArgs类,继承自EventArgs
//用作保存Cat对象的Name属性,还可以扩展其他的功能
public class CatShoutEventArgs : EventArgs
{
public string CatName { get; set; }
public CatShoutEventArgs(string name)
{
this.CatName = name;
}
} //定义一个委托 名叫CatShoutEventHandler
public delegate void CatShoutEventHandler
(object sender,CatShoutEventArgs e); //猫类
public class Cat
{
public string Name { get; set; } //猫的名字 //在猫类里定义一个事件CatShout,返回值类型是定义的委托
public event CatShoutEventHandler CatShout; public void Shout()
{
Console.WriteLine("喵喵喵");
//判断委托内是否为空,若不为空,执行该委托
if (CatShout != null)
{
//new一个CatShoutEventArgs类,传入参数是自身的Name
CatShoutEventArgs e = new CatShoutEventArgs(Name);
//执行CatShout事件,传入自身和e
CatShout(this, e);
}
} //无参和带参构造
public Cat() { }
public Cat(string name)
{
this.Name = name;
}
} //老鼠类
public class Rat
{
public string Name { get; set; } //老鼠的名字 //老鼠的逃跑方法
public void Run(object sender, CatShoutEventArgs e)
{
//打出一句话,包括了猫的名字和老鼠的名字
Console.WriteLine(e.CatName + "来了! " + Name + "跑了!");
} //无参和带参构造
public Rat() { }
public Rat(string name)
{
this.Name = name;
}
}

嗯 然后Program类中的Main方法并不需要改动

为了视觉效果,我再放上来一遍

static void Main(string[] args)
{
//构建一个猫对象和两个老鼠对象
Cat cat = new Cat("Tom");
Rat rat1 = new Rat("Jerry");
Rat rat2 = new Rat("TaoQi"); //向cat的委托对象CatShout中依次添加老鼠对象的Run方法
//注!!添加的是整个方法 不需要加括号
cat.CatShout += rat1.Run;
cat.CatShout += rat2.Run; //调用cat的Shout方法
cat.Shout();
Console.ReadKey();
}

好了 好了 最后的最后 看看我们程序的运行效果吧

by天命 2016.11.9

C# 委托和事件 与 观察者模式(发布-订阅模式)讲解 by天命的更多相关文章

  1. SpringBoot事件监听机制及观察者模式/发布订阅模式

    目录 本篇要点 什么是观察者模式? 发布订阅模式是什么? Spring事件监听机制概述 SpringBoot事件监听 定义注册事件 注解方式 @EventListener定义监听器 实现Applica ...

  2. 设计模式 | 观察者模式/发布-订阅模式(observer/publish-subscribe)

    定义: 定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象.这个主题对象在状态发生变化时,会通知所有观察者对象,使他们能够自动更新自己. 结构:(书中图,侵删) 一个抽象的观察者接口, ...

  3. javascript设计模式——发布订阅模式

    前面的话 发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在javascript开发中,一般用事件模型来替代传统的发布—订阅模 ...

  4. 《JavaScript设计模式与开发实践》笔记第八章 发布-订阅模式

    第八章 发布-订阅模式 发布-订阅模式描述 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布-订阅模式可以广泛应用于 ...

  5. js设计模式-发布/订阅模式

    一.前言 发布订阅模式,基于一个主题/事件通道,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知. 就和用户 ...

  6. [转]js设计模式—发布订阅模式

    发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在javascript开发中,一般用事件模型来替代传统的发布—订阅模式.本文将 ...

  7. js 设计模式:观察者和发布订阅模式

    总是把这两个当作同一个模式,但其实是不太一样的,现在重温一下. 观察者模式 观察者直接订阅目标,当目标触发事件时,通知观察者进行更新 简单实现 class Observer { constructor ...

  8. RabbitMQ指南之三:发布/订阅模式(Publish/Subscribe)

    在上一章中,我们创建了一个工作队列,工作队列模式的设想是每一条消息只会被转发给一个消费者.本章将会讲解完全不一样的场景: 我们会把一个消息转发给多个消费者,这种模式称之为发布-订阅模式. 为了阐述这个 ...

  9. 3_06_MSSQL课程_Ado.Net_接口、委托、事件、观察者模式

    1.接口——实现接口 2.委托.事件(定义事件.注册事件.触发事件) 3.接口和事件的区别,怎么分情况用? 4.观察者模式作为设计模式的一种,也称发布订阅模式. 应对类型的变化和个数的变化. 中介设计 ...

随机推荐

  1. (Array)169. Majority Element

    Given an array of size n, find the majority element. The majority element is the element that appear ...

  2. Mono Fragment之间转换

    var newFragment = new mybacklogF (); var ft = FragmentManager.BeginTransaction (); ft.Replace (Resou ...

  3. mongoDB数据库

    1.mongoDB简介:mongoDB 为非关系数据库,集合(collection)关系数据库中的表,中存储的为json结构的文档,集合中的每一条记录都可以结构不同, 但必须都有_id字段(mongo ...

  4. 《从零开始做一个MEAN全栈项目》(2)

    欢迎关注本人的微信公众号"前端小填填",专注前端技术的基础和项目开发的学习.   上一节简单介绍了什么是MEAN全栈项目,这一节将简要介绍三个内容:(1)一个通用的MEAN项目的技 ...

  5. Tomcat回收连接

    最近公司一个JDK1.4的老项目升级了JDK1.6后BUG不断,最可恶的连接池被占满. 因为是使用tomcat的连接池所以在config下中添加 <Resource name="jdb ...

  6. Ajax请求数据

    后台使用数数组的形式存放数据(以键值对的形式存放).让后再Json转码. Map<String,String> map=new HashMap<String,String>() ...

  7. performSelector和respondsToSelector用法

    一.performSelector调用和直接调用区别 下面两段代码都在主线程中运行,我们在看别人代码时会发现有时会直接调用,有时会利用performSelector调用,今天看到有人在问这个问题,我便 ...

  8. php的数据循环 之li的3个类判断

    这种判断必须得保证后台数据的键值为数字 ,反正要能跟数字计算的数据才行 ts2.php <?php$array = array('0'=>'a1','1'=>'b1','2'=> ...

  9. 如何禁止 Mac OS X 在外接设备上生成 .DS_Store 文件?以及如何批量删除 .DS_Store 文件?

    如何禁止 Mac OS X 在外接设备上生成 .DS_Store 文件?以及如何批量删除 .DS_Store 文件?原文链接:http://www.java2class.net/bbs/viewthr ...

  10. linux小程序--cmatrix

    wget http://www.asty.org/cmatrix/dist/cmatrix-1.2a.tar.gz .2a.tar.gz cd cmatrix-.2a yum install ncur ...