如何用初学者语言描述观察者模式?

15

目前,我对网上有关观察者模式的所有编码示例的理解程度都不够。 我简单地将其理解为几乎是一种订阅,当委托注册所做的更改时,它会更新所有其他事件。 然而,我在真正理解其好处和用途方面非常不稳定。 我已经做了一些谷歌搜索,但大多数超出了我的理解范围。

我正在尝试在我的作业任务中实现这个模式,并确实需要更好地理解模式本身以及可能的示例来看看它的用途。 我不想强制将此模式推入某些东西以提交,我需要理解其目的并相应地开发我的方法,以使其真正具有良好的目的。 我的文本没有真正涉及它,只是在一句话中提到了它。 MSDN 对我来说很难理解,因为我是初学者,它似乎更是高级主题。

你如何向初学者描述C#中的观察者模式及其用途? 示例请保持代码非常简单,以便我更好地理解目的,而不是复杂的代码片段。 我正在尝试使用它有效地进行一些简单的文本框字符串操作,并使用委托来完成我的任务,所以需要指针!


如果我说“Event”,那是作弊吗? - boj
您对观察者模式有任何具体的问题吗? - Thomas Owens
只是一个大致的想法,可能带有简单的代码示例...我目前正在上的这门课程是一次可怕的经历,其中90%的内容都远超过了课程水平,我们几乎难以应对!我正在努力理解所学内容,并希望自己能够做到!到目前为止,多亏了像你这样的人的帮助,我的知识得到了更多的扩展,比教材提供的还要多。 - sheldonhull
我在我的回答中添加了一个简单的示例,我还将添加一些关于委托的内容。 - Daniel Brückner
16个回答

27
我能想到的最好例子是邮件列表(仅作为一个例子)。
您,观察者,订阅邮件列表并观察该列表。当您对该列表不再感兴趣时,您取消订阅。
这个概念就是观察者模式。涉及两个或多个类。一个或多个类订阅发布者类(有不同的名称),然后第一个类(以及每个订阅类)会在发布者希望通知他们时收到通知。
这就是我向我的妻子解释编程和设计理论的方式。她能够理解。我意识到这可能对您来说太简单了,但这是一个好的开始...
敬礼, Frank

不,实际上我需要它简单明了。我宁愿过度简化并在以后学习更多。我需要这个概念。谢谢! - sheldonhull
1
这是一个很棒的答案。它用简单明了、熟悉的例子给出了简洁的解释。 - Beska

5

请查看《Head First: 设计模式》,其中有一些非常易于理解的主要模式描述。

对于观察者模式,重要的是要了解它描述了一种一对多的关系,并使用订阅模型告诉其他类何时发生了更改。 RSS、Atom 和 Twitter 都沿用了这些思路。


提起这件事...我刚刚买了它,现在把它放在书架上了。由于我目前的家庭作业负担和购买的Headfirst C#版本,我把它忘记了。我也会在那里阅读相关内容!非常感谢你的推荐,并且谢谢你的提醒。 - sheldonhull

3
观察者模式需要订阅主题,以便在任何变化时得到通知。主题不知道观察者的存在,只需定义观察者需要提供的接口(或委托),并允许注册。
简而言之,观察者模式允许主题调用观察者,而主题不关心观察者是谁或是否存在。

但是该主题包含对观察者的引用,因此它必须以某种方式了解它。 - Chris Cudmore
1
要指出的是:主题不知道观察者是谁(这意味着:在哪个程序集中的哪个类,为什么它观察等等)。它有一个列表,其中观察者实际上正在添加和删除自身。主题不关心观察者是否已注册。 - Stefan Steinegger

2
有两个对象NOTIFIER和OBSERVER。 NOTIFIER不知道OBSERVER,而OBSERVER知道NOTIFIER实现了一个事件。
OBSERVER使用该事件通知其他对象发生了某些事情。简单地说,事件是一个方法列表。因为OBSERVER想在发生某些事情时得到通知,它将一个应该被调用的方法添加到NOTIFIER的事件中。
所以如果NOTIFIER使用此事件发布某些内容,NOTIFIER只需遍历方法列表并调用这些方法。当OBSERVER添加的方法被调用时,OBSERVER知道发生了某些事情,并且可以在这种情况下执行任何必要的操作。
这里是一个带有ValueChanged()事件的示例notifier类。
// Declare how a method must look in order to be used as an event handler.
public delegate void ValueChangedHandler(Notifier sender, Int32 oldValue, Int32 newValue);

public class Notifier
{
    // Constructor with an instance name.
    public Notifier(String name)
    {
        this.Name = name;
    }
    public String Name { get; private set; }

    // The event that is raised when ChangeValue() changes the
    // private field value.
    public event ValueChangedHandler ValueChanged;

    // A method that modifies the private field value and
    // notifies observers by raising the ValueChanged event.
    public void ChangeValue(Int32 newValue)
    {
        // Check if value really changes.
        if (this.value != newValue)
        {
            // Safe the old value.
            Int32 oldValue = this.value;

            // Change the value.
            this.value = newValue;

            // Raise the ValueChanged event.
            this.OnValueChanged(oldValue, newValue);
        }
    }

    private Int32 value = 0;

    // Raises the ValueChanged event.
    private void OnValueChanged(Int32 oldValue, Int32 newValue)
    {
        // Copy the event handlers - this is for thread safty to
        // avoid that somebody changes the handler to null after
        // we checked that it is not null but before we called
        // the handler.
        ValueChangedHandler valueChangedHandler = this.ValueChanged;

        // Check if we must notify anybody.
        if (valueChangedHandler != null)
        {
            // Call all methods added to this event.
            valueChangedHandler(this, oldValue, newValue);
        }
    }
}

这是一个观察者类的示例。
public class Observer
{
    // Constructor with an instance name.
    public Observer(String name)
    {
        this.Name = name;
    }
    public String Name { get; private set; }

    // The method to be registered as event handler.
    public void NotifierValueChanged(Notifier sender, Int32 oldValue, Int32 newValue)
    {
        Console.WriteLine(String.Format("{0}: The value of {1} changed from {2} to {3}.", this.Name, sender.Name, oldValue, newValue));
    }
}

一个小的测试应用程序。
class Program
{
    static void Main(string[] args)
    {
        // Create two notifiers - Notifier A and Notifier B.
        Notifier notifierA = new Notifier("Notifier A");
        Notifier notifierB = new Notifier("Notifier B");

        // Create two observers - Observer X and Observer Y.
        Observer observerX = new Observer("Observer X");
        Observer observerY = new Observer("Observer Y");

        // Observer X subscribes the ValueChanged() event of Notifier A.
        notifierA.ValueChanged += observerX.NotifierValueChanged;

        // Observer Y subscribes the ValueChanged() event of Notifier A and B.
        notifierA.ValueChanged += observerY.NotifierValueChanged;
        notifierB.ValueChanged += observerY.NotifierValueChanged;

        // Change the value of Notifier A - this will notify Observer X and Y.
        notifierA.ChangeValue(123);

        // Change the value of Notifier B - this will only notify Observer Y.
        notifierB.ChangeValue(999);

        // This will not notify anybody because the value is already 123.
        notifierA.ChangeValue(123);

        // This will not notify Observer X and Y again.
        notifierA.ChangeValue(1);
    }
}

输出结果如下:
观察者X:Notifier A的值从0变为123。
观察者Y:Notifier A的值从0变为123。
观察者Y:Notifier B的值从0变为999。
观察者X:Notifier A的值从123变为1。
观察者Y:Notifier A的值从123变为1。

为了理解委托类型,我将与类类型进行比较。
public class Example
{
   public void DoSomething(String text)
   {
      Console.WriteLine(
         "Doing something with '" + text + "'.");
   }

   public void DoSomethingElse(Int32 number)
   {
      Console.WriteLine(
         "Doing something with '" + number.ToString() + "'.");
   }
}

我们定义了一个简单的类 Example,它有两个方法。现在我们可以使用这个类类型。

Example example = new Example();

虽然这种方法可行,但以下方法不可行,因为类型不匹配。您会收到编译器错误提示。

Example example = new List<String>();

我们可以使用变量example

example.DoSomething("some text");

现在使用委托类型做同样的事情。首先,我们定义一个委托类型 - 这只是类定义之前的类型定义。

public delegate void MyDelegate(String text);

现在我们可以使用委托类型,但我们不能将普通数据存储在委托类型变量中,而是必须存储一个方法。

MyDelegate method = example.DoSomething;

我们现在已经存储了对象example的方法DoSomething()。下面的内容无法工作,因为我们将MyDelegate定义为一个只有一个字符串参数并返回void的委托。 DoSomethingElse返回void但需要一个整数参数,所以你会得到编译器错误。

MyDelegate method = example.DoSomethingElse;

最后,您可以使用变量method。由于该变量存储的是方法而不是数据,因此无法执行数据操作。但是您可以调用存储在变量中的方法。

method("Doing stuff with delegates.");

这会调用我们存储在变量中的方法 - example.DoSomething()


哇,这真的很多。我需要重新阅读几遍。感谢你的努力和代码示例。我很感激。 - sheldonhull

1

可能你遇到的问题是定义正确的接口。接口定义了订阅者和发布者之间的交互。

首先制作一个 C# Windows 窗体应用程序

将 Program.cs 设置如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new Form1());
        }
    }

    interface IObserver
    {
        void Refresh(List<string> DisplayList);
    }

    class ObserverList : List<IObserver>
    {
        public void Refresh(List<String> DisplayList)
        {
            foreach (IObserver tItem in this)
            {
                tItem.Refresh(DisplayList);
            }
        }

    }
}

我们在这里做两件事情。首先是让订阅者实现的接口。然后是为发布者创建一个包含所有订阅者的列表。

然后制作一个表单,其中包括两个按钮,一个标记为“表单2”,另一个标记为“表单3”。然后添加一个文本框,再添加一个标记为“添加”的按钮。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private List<string> DataList= new List<string>();
        private ObserverList MyObservers = new ObserverList();
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Form2 frmNewForm = new Form2();
            MyObservers.Add(frmNewForm);
            frmNewForm.Show();
            MyObservers.Refresh(DataList);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Form3 frmNewForm = new Form3();
            MyObservers.Add(frmNewForm);
            frmNewForm.Show();
            MyObservers.Refresh(DataList);

        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }

        private void button3_Click(object sender, EventArgs e)
        {
            DataList.Add(textBox1.Text);
            MyObservers.Refresh(DataList);
            textBox1.Text = "";
        }

    }
}

我故意设置了Form2按钮和FOrm3按钮,以制作每种类型的表单的多个副本。例如,您可以同时拥有十二个表单。

您会注意到,在创建每个表单后,我将其放入观察者列表中。我之所以能够这样做,是因为Form2和Form3都实现了IObserver。在显示表单后,我调用观察者列表上的refresh,以便新表单使用最新数据进行更新。请注意,我可以将其转换为IObserver变量并仅更新该表单。我试图尽可能简洁。

然后对于“Button3”添加按钮,我从文本框中提取文本,将其存储在我的DataList中,然后刷新所有观察者。

然后制作Form2。添加一个列表框和以下代码。

using System;

using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form2 : Form,IObserver
    {
        public Form2()
        {
            InitializeComponent();
        }

        private void Form2_Load(object sender, EventArgs e)
        {

        }

        void IObserver.Refresh(List<string> DisplayList)
        {
            this.listBox1.Items.Clear();
            foreach (string s in DisplayList)
            {
                this.listBox1.Items.Add(s);
            }
            this.listBox1.Refresh();
        }

    }
}

然后添加Form3,一个组合框,并添加以下代码。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form3 : Form,IObserver
    {
        public Form3()
        {
            InitializeComponent();
        }

        private void Form3_Load(object sender, EventArgs e)
        {

        }
        void IObserver.Refresh(List<string> DisplayList)
        {
            this.comboBox1.Items.Clear();
            foreach (string s in DisplayList)
            {
                this.comboBox1.Items.Add(s);
            }
            this.comboBox1.Refresh();
        }
    }
}

您会注意到每个表单都以稍微不同的方式实现IObserver接口的刷新方法。一个是用于列表框,另一个是用于组合框。在这里,接口的使用是关键要素。

在真实世界的应用程序中,此示例将更加复杂。例如,不是通过Refresh接口传递字符串列表。它将没有任何参数。相反,发布者(在此示例中为Form1)将实现发布者接口,并在初始化时向观察者注册自己。每个观察者将能够在其初始化例程中接受发布者。然后,在刷新时,它将通过接口公开的方法从发布者中提取字符串列表。

对于具有多种数据类型的更复杂应用程序,这允许您自定义实现IObserver的表单从发布者中提取的数据。

当然,如果您只想让观察者能够显示字符串列表或特定数据。那么将其作为参数的一部分传递。接口明确了每个层要做什么。这样,五年后,您可以查看代码并编写“哦,那就是它正在做的事情。”


谢谢!你在这方面付出了很多努力,我需要仔细研究一下。看起来是一些有用的信息! - sheldonhull

1

观察者模式就像它的名字一样 -

它是一种让某些对象观察另一个对象,以便观察其变化的方法。

在C#中,这变得相对简单,因为事件基本上是实现观察者模式的语言特定手段。如果您曾经使用过事件,那么您已经使用了观察者模式。

在其他语言中,这并不是内置的,因此有许多尝试规范处理此问题的方法。


1

观察者模式就像是一条直接的通信线路。与其让所有亲戚都给你打电话问候你的身体状况,当你生病时写一张卡片,所有关心你的人都能收到(或者复制)。当你康复时,你再发一张卡片。当你踢到脚趾头时,你也可以发一张卡片。当你得到A的好成绩时,你也可以发一张卡片。

任何关心你的人都可以加入你的群发名单,并根据自己的意愿做出回应。

这种依赖关系对于UI非常有用。如果我有一个进程很慢(例如),它可以在进度更新时触发事件。进度条元素可以观察到并更新其覆盖范围。OK按钮可以观察到并在100%时变为活动状态。光标可以观察到并在进度达到100%时进行动画处理。这些观察者中没有一个需要知道其他观察者的存在。此外,这些元素中没有一个严格需要知道驱动它们的内容。


太好了!那给了我一些很好的实际例子,并且恰好提到了简单的GUI开发。谢谢。 - sheldonhull

1

这个模式可能是最基本的模式之一,如果不是最基本的模式。

有两个“人”参与其中; 发布者和订阅者/观察者。

观察者只需请求发布者在有“新闻”时通知他。这里的新闻可以是任何重要的事情。它可以是空气温度,可以是网站上的新帖子,也可以是时间。


1

0
那些认为.NET中的事件确实是观察者模式实现的人并没有在忽悠你,这是真的。至于它是如何工作的,无论是从高层次的角度还是从更具体的实现细节方面,我将使用一个类比来解释。
想象一下一个报纸出版商。从面向对象编程的角度来看,我们可以把报纸看作是一个“可观察”的东西。但它是如何工作的呢?显然,报纸本身的实现细节(也就是在办公室里一起工作以制作报纸的记者、作家、编辑等)并不会公开暴露。人们(“观察者”)不会聚集在一起观看报纸出版商的员工工作。他们不能这样做,因为没有协议(或“接口”)来进行这样的操作。
这就是你如何观察(即阅读)一份报纸:你订阅它。你把自己的名字放在该报的订户列表上,然后出版商就知道每天早上要在你家门口送一份报纸。如果你不想再观察(阅读)它了,你就取消订阅;你的名字就会从列表中被删除。

现在,这可能看起来像是一个抽象的类比;但实际上几乎完美地类比了 .NET 事件的工作方式

假设有一个旨在被观察的类,它的实现通常不需要为公众所知。然而,它将向公众公开一种特定类型的接口,即事件。想要观察此事件的代码本质上会将自己注册为订阅者:

// please deliver this event to my doorstep
myObject.SomeEvent += myEventHandler;

当这段代码决定不再接收此事件的通知时,它会取消订阅:
// cancel my subscription
myObject.SomeEvent -= myEventHandler;

现在让我们快速讨论一下委托以及这段代码的实际工作原理。委托,可能你知道也可能不知道,本质上是一个存储方法地址的变量。通常,这个变量有一个类型--就像声明为intdoublestring等的变量都有类型一样。对于委托类型来说,这个类型是由方法的签名定义的;也就是说,它的参数和返回值。特定类型的委托可以指向执行任何操作的任何方法,只要该方法具有适当的签名。

所以回到报纸类比:为了成功订阅一份报纸,你必须遵循特定的模式。具体来说,你需要提供一个有效的地址,告诉出版商你想要报纸送到哪里。你不能只说,“是的,把它发送给丹。”你不能说,“我要一份培根芝士汉堡。”你必须提供出版商可以有意义地处理的信息。在.NET事件的世界中,这意味着需要提供正确签名的事件处理程序。

在大多数情况下,这个签名最终会成为像这样的一些方法:

public void SomeEventHandler(object sender, EventArgs e) {
    // anything could go in here
}

以上是一种可以存储在类型为EventHandler的委托变量中的方法。对于更具体的情况,有通用的EventHandler<TEventArgs>委托类型,它描述了一种类似于上述方法的方法,但其e参数是从EventArgs派生的某种类型。

记住,委托实际上是指向方法的变量,因此很容易将.NET事件与报纸订阅之间建立最终联系。事件的实现方式是通过一个委托列表,可以向其中添加和删除项目。这就像报纸出版商的订户列表一样,每个订户在每天早上分发报纸时都会收到一份副本。

无论如何,希望这有助于您在某种程度上理解观察者模式。当然,还有许多其他种类的此模式的实现,但.NET事件是大多数.NET开发人员熟悉的范例,因此我认为这是一个很好的起点,可以从中发展自己的理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接