设计您的足球引擎,并学习如何应用设计模式(观察者、装饰器、策略和生成器模式)- 第一部分和第二部分






4.76/5 (147投票s)
本文旨在 (1) 以简单、易于理解的方式向您介绍设计模式 (2) 训练您如何真正识别和应用设计模式 (3) 逐步演示使用设计模式解决设计问题的方法
目录
- 第一部分
- 第二部分
第一部分
解决方案架构师:“但是你可以使用设计模式”
小白开发者:“是的,但我能把它当作 ActiveX 控件来用吗?”
引言
本文简介
T本文旨在
- 以简单、易于理解(?)的方式向您介绍设计模式
- 训练您如何真正“应用”设计模式(您可以轻松学会设计模式,但要应用它们解决问题,您需要真正的设计技能)
- 为您提供有关应用以下模式的上下文的清晰概念 - 生成器、观察者、策略和装饰器(它们是一些流行的设计模式)
- 向您演示如何应用观察者模式来解决一个设计问题
在整篇文章中,您将经历以下步骤
- 您将为一个非常简单的足球游戏引擎建模
- 您将识别您的足球游戏引擎中的设计问题
- 您将决定使用哪些模式来解决您的设计问题
- 然后您将实际使用观察者模式来解决您的一个设计问题。
作为先决条件
- 您可能需要掌握一些阅读和理解 UML 图的技巧
使用代码
- 相关的 zip 文件包含代码、UML 设计(Visio 格式)等。阅读本文后,您可以使用 Winzip 等程序下载并解压缩 zip 文件,以体验源代码。
设计模式概述
即使对设计模式了解不多,设计师和开发者也倾向于重用类关系和对象协作来简化设计过程。简而言之,“设计模式由各种协作对象(类、关系等)组成”。它们为常见的设计问题提供解决方案。更重要的是,它们为设计师和程序员提供了一种讨论其设计的一致性语言。例如,您可以告诉朋友,您在项目中使用“生成器”模式来解决一些设计规范。
Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides [也称为四人帮 (GOF)] 提供了针对常见设计问题的模式的一致分类。四人帮 (GOF) 模式通常被认为是所有其他模式的基础。
使用模式的基本原则是可重用性。一旦某个问题以某种方式得到解决,如果您真正理解以模式为中心的软件工程概念,您就不需要再重新发明轮子了。以下是一些关于设计模式需要记住的要点。
- 设计模式不是代码。它实际上是可用于解决问题的一种方法或模型。
- 设计模式是关于对象的设计和交互,它们为解决常见的设计问题提供了可重用的解决方案。
- 设计模式通常借助 UML 图来表示。
一些真正的模式实践经验可能会给您带来更好的理解!
构建您的(简单)足球引擎
您在一家著名的电脑游戏开发公司工作,他们让您担任他们一个主要项目的解决方案架构师——一个足球游戏引擎(不错吧?)。现在,您正在领导整个足球游戏引擎的设计过程,并且突然之间,您立即面临许多设计上的考量。让我们来看看
- 您如何识别游戏系统中的实体,
- 您如何识别设计问题,以及
- 您如何应用模式来满足您的设计规范。
识别实体
首先,您需要识别在游戏引擎中使用的对象。为此,您应该设想最终用户将如何使用该系统。让我们假设最终用户将按以下顺序操作游戏(让我们保持简单)。
- 开始游戏
- 选择两支球队
- 向球队添加或移除球员
- 选择一个比赛场地
- 开始游戏
您的系统可能有多个场地、多支球队等。列出系统中的一些现实世界对象,您有
- 球员,踢足球的人
- 球队,拥有多名球员
- 球,由不同的球员控制。
- 场地,比赛进行的地方。
- 裁判,在场上控制比赛。
此外,您的游戏引擎中可能需要一些逻辑对象,例如
- 比赛,定义一场足球比赛,由球队、球、裁判、场地等组成
- 游戏引擎,用于同时模拟多场比赛。
- 球队策略,决定一支球队比赛时的策略
所以,这是系统的一个非常抽象的视图。方框代表系统中的类,连接器描述“拥有”关系及其多样性。箭头表示读取方向。即,一个游戏引擎拥有(可以模拟)多场比赛。一场比赛拥有(包含)三名裁判、一个球、两支球队和一个场地。一支球队可以有多名球员,并且在同一时间有一种策略。
图1 - 高层视图
识别设计问题
现在,您应该决定
- 这些对象是如何组织的
- 它们是如何被创建的
- 它们在相互交互时的行为,以制定设计规范。
首先,你必须写下你的足球引擎的最低限度描述,以确定设计问题。例如,这里有一些与我们之前确定的一些对象相关的设计问题。
- 球
- 当球的位置发生变化时,所有球员和裁判都应立即得到通知。
- 球队和球队策略
- 在比赛进行中,最终用户可以改变他的球队的策略(例如,从进攻转为防守)
- 玩家
- 球队中的球员应该有额外的职责,如前锋、后卫等,这些职责可以在运行时分配。
- 比赛场地
- 每个场地都由看台、场地表面、观众等组成——每个场地都有不同的外观。
那么现在,让我们看看如何识别模式,以解决这些设计问题。
确定要使用的模式
看看您上面识别出的设计问题(是的,再看一次)。现在,让我们看看如何使用设计模式来解决这些问题。
1:解决与“球”相关的设计问题
首先,我们来处理与球相关的规范。你需要设计一个框架,当球的状态(位置)改变时,所有球员和裁判都会收到关于球的新状态(位置)的通知。现在,让我们将这个问题泛化
具体设计问题:“当球的位置发生变化时,所有球员和裁判都应立即得到通知。”
问题泛化:“当一个主题(在这种情况下是球)发生变化时,它的所有依赖者(在这种情况下是球员)都会被自动通知和更新。”
一旦你遇到这样的设计问题,你参考 GOF 模式——然后你可能会突然发现你可以应用“观察者”模式来解决这个问题。
![]() |
在这种情况下,我们使用此模式是因为当球的位置改变时,我们需要通知所有球员。
2:解决与“球队”和“球队策略”相关的设计问题
接下来,我们必须解决与球队和球队策略相关的规范。正如我们之前讨论的,当比赛进行时,最终用户可以改变他球队的策略(例如,从进攻转为防守)。这清楚地意味着我们需要将球队的策略与其使用的球队分开。
具体设计问题:“当比赛进行中,最终用户可以改变他球队的策略(例如,从进攻改为防守)”
问题泛化:“我们需要让算法(球队策略)独立于使用它的客户端(在这种情况下是球队)而变化。”
然后,你可以选择“策略”模式来解决上述设计问题。
![]() |
3:解决与“球员”相关的设计问题
现在,让我们来解决与球员相关的设计规范。根据我们的问题定义,很明显我们需要在运行时为每个球员分配职责(如前锋、后卫等)。在这一点上,你可以考虑使用子类化(即继承)——通过创建一个球员类,然后从基类继承出前锋、后卫等类。但缺点是,当你进行子类化时,你无法将对象的职责与其实现分离开来。
也就是说,在我们的案例中,子类化不是合适的方法,因为我们需要将“前锋”、“中场”、“后卫”等职责与球员的实现分开。因为,一个球员有时可能是“前锋”,而在其他时候,同一个球员可能是“中场”。
具体设计问题:“球队中的球员应该有额外的职责,比如前锋、后卫等,这些职责可以在运行时分配。”
问题泛化:“我们需要在不使用子类化的情况下,动态地为对象(在这种情况下是球员)附加额外的职责(如前锋、中场等)。”
然后,你可以选择“装饰器”模式来解决上述设计问题。
![]() |
4:解决与“场地”相关的设计问题
如果你看一下场地的规范,我们发现一个场地的外观是由各种子单位决定的,比如看台、场地表面、观众等。场地的外观可能会根据这些子单位而变化。因此,我们需要以这样一种方式构建场地,即场地的构建过程可以创建场地的不同表示。也就是说,意大利的场地可能与英国的场地有不同的看台结构和表面。但是,游戏引擎可以通过调用同一组函数来创建这两个场地。
具体设计问题:“每个场地由看台、场地表面、观众等构成——并且每个场地都有不同的外观。”
问题泛化:“我们需要将一个对象(场地)的构建与其表示(场地的外观)分离开来,并且我们需要使用相同的构建过程来创建不同的表示。”
![]() |
现在,你可以选择“生成器”模式来解决上述设计问题。
第二部分
解决方案架构师:“我让你学习设计模式。”
小白开发者:“是的,现在我可以使用模式开发一个足球引擎了。”
解决方案架构师:“哈?你什么意思?!@@#!”
应用观察者模式
在本节中,我们将更仔细地研究观察者模式,然后我们将应用该模式来解决我们的第一个设计问题。如果你还记得,我们的第一个设计问题是,
- “当球的位置发生变化时,所有球员都应立即得到通知。”
理解观察者模式
观察者模式的 UML 类图如下所示。
图 2 - 观察者模式
该模式的参与者详情如下。
- 主题
此类提供了一个用于附加和分离观察者的接口。Subject 类还持有一个私有的观察者列表。Subject 类中的函数是
- Attach- 将一个新的观察者添加到观察该主题的观察者列表中
- Detach- 从观察该主题的观察者列表中移除一个观察者
- 通知- 当发生变化时,通过调用观察者中的
Update
函数来通知每个观察者。
- 具体主题
该类向观察者提供感兴趣的状态。它还通过调用其超类(即 Subject 类)中的 Notify 函数,向所有观察者发送通知。
ConcreteSubject
类中的函数是- GetState- 返回主题的状态
- 观察者
该类为所有观察者定义了一个更新接口,用于从主题接收更新通知。Observer 类被用作一个抽象类来实现具体的观察者。
- 更新- 此函数是一个抽象函数,具体观察者将重写此函数
- 具体观察者
该类维护一个对主题的引用,以便在收到通知时接收主题的状态。
- 更新- 这是具体类中被重写的方法。当主题调用此方法时,
ConcreteObserver
调用主题的GetState
方法来更新其关于主题状态的信息。
- 更新- 这是具体类中被重写的方法。当主题调用此方法时,
调整观察者模式
现在,让我们看看如何调整这个模式来解决我们的特定问题。这会让你有一个更好的理解。
图 3 - 解决我们的第一个设计问题
当我们调用球的 SetBallPosition
函数来设置新位置时,它会接着调用在 Ball 类中定义的 Notify
函数。Notify 函数会遍历列表中的所有观察者,并调用每个观察者的 Update
函数。当 Update 函数被调用时,观察者们将通过调用 Foot ball 类中的 GetBallPosition
函数来获取球的最新位置状态。
参与者详情如下。
球(主题)
Ball 类的实现如下所示。
' Subject : The Ball Class
Public Class Ball
'A private list of observers
Private observers As new System.Collections.ArrayList
'Routine to attach an observer
Public Sub AttachObserver(ByVal obj As IObserver)
observers.Add(obj)
End Sub
'Routine to remove an observer
Public Sub DetachObserver(ByVal obj As IObserver)
observers.Remove(obj)
End Sub
'Routine to notify all observers
Public Sub NotifyObservers()
Dim o As IObserver
For Each o In observers
o.Update()
Next
End Sub
End Class ' END CLASS DEFINITION Ball
足球(具体主题)
FootBall 类的实现如下所示。
' ConcreteSubject : The FootBall Class
Public Class FootBall
Inherits Ball
'State: The position of the ball
Private myPosition As Position
'This function will be called by observers to get current position
Public Function GetBallPosition() As Position
Return myPosition
End Function
'Some external client will call this to set the ball's position
Public Function SetBallPosition(ByVal p As Position)
myPosition = p
'Once the position is updated, we have to notify observers
NotifyObservers()
End Function
'Remarks: This can also be implemented as a get/set property
End Class ' END CLASS DEFINITION FootBall
IObserver (观察者)
IObserver
类的实现如下所示。该类为创建具体观察者提供了接口规范。
' Observer: The IObserver Class
'This class is an abstract (MustInherit) class
Public MustInherit Class IObserver
'This method is a mustoverride method
Public MustOverride Sub Update()
End Class ' END CLASS DEFINITION IObserver
球员 (具体观察者)
Player 类的实现如下所示。Player 继承自 IObserver
类
' ConcreteObserver: The Player Class
'Player inherits from IObserver, and overrides Update method
Public Class Player
Inherits IObserver
'This variable holds the current state(position) of the ball
Private ballPosition As Position
'A variable to store the name of the player
Private myName As String
'This is a pointer to the ball in the system
Private ball As FootBall
'Update() is called from Notify function, in Ball class
Public Overrides Sub Update ()
ballPosition = ball.GetBallPosition()
System.Console.WriteLine("Player {0} say that the ball is at {1},{2},{3} ", _
myName, ballPosition.X, ballPosition.Y, ballPosition.Z)
End Sub
'A constructor which allows creating a reference to a ball
Public Sub New(ByRef b As FootBall, ByVal playerName As String)
ball = b
myName = playerName
End Sub
End Class ' END CLASS DEFINITION Player
裁判(具体观察者)
Referee 类的实现如下所示。Referee 也继承自 IObserver
类
' ConcreteObserver : The Referee Clas
Public Class Referee
Inherits IObserver
'This variable holds the current state(position) of the ball
Private ballPosition As Position
'This is a pointer to the ball in the system
Private ball As FootBall
'A variable to store the name of the referee
Private myName As String
'Update() is called from Notify function in Ball class
Public Overrides Sub Update()
ballPosition = ball.GetBallPosition()
System.Console.WriteLine("Referee {0} say that the ball is at {1},{2},{3} ", _
myName, ballPosition.X, ballPosition.Y, ballPosition.Z)
End Sub
'A constructor which allows creating a reference to a ball
Public Sub New(ByRef b As FootBall, ByVal refereeName As String)
myName = refereeName
ball = b
End Sub
End Class ' END CLASS DEFINITION Referee
Position 类
此外,我们有一个 Position 类,用来保存球的位置。
'Position: This is a data structure to hold the position of the ball
Public Class Position
Public X As Integer
Public Y As Integer
Public Z As Integer
'This is the constructor
Public Sub New(Optional ByVal x As Integer = 0, _
Optional ByVal y As Integer = 0, _
Optional ByVal z As Integer = 0)
Me.X = x
Me.Y = y
Me.Z = Z
End Sub
End Class ' END CLASS DEFINITION Position
整合
现在,让我们创建一个球和几个观察者。我们还将这些观察者附加到球上,这样当球的位置发生变化时,它们就会被自动通知。代码本身就很容易理解。
'Let us create a ball and few observers
Public Class GameEngine
Public Shared Sub Main()
'Create our ball (i.e, the ConcreteSubject)
Dim ball As New FootBall()
'Create few players (i.e, ConcreteObservers)
Dim Owen As New Player(ball, "Owen")
Dim Ronaldo As New Player(ball, "Ronaldo")
Dim Rivaldo As New Player(ball, "Rivaldo")
'Create few referees (i.e, ConcreteObservers)
Dim Mike As New Referee(ball, "Mike")
Dim John As New Referee(ball, "John")
'Attach the observers with the ball
ball.AttachObserver(Owen)
ball.AttachObserver(Ronaldo)
ball.AttachObserver(Rivaldo)
ball.AttachObserver(Mike)
ball.AttachObserver(John)
System.Console.WriteLine("After attaching the observers...")
'Update the position of the ball.
'At this point, all the observers should be notified automatically
ball.SetBallPosition(New Position())
'Just write a blank line
System.Console.WriteLine()
'Remove some observers
ball.DetachObserver(Owen)
ball.DetachObserver(John)
System.Console.WriteLine("After detaching Owen and John...")
'Updating the position of ball again
'At this point, all the observers should be notified automatically
ball.SetBallPosition(New Position(10, 10, 30))
'Press any key to continue..
System.Console.Read()
End Sub
End Class
运行项目
运行项目后,您将得到如下输出
结论
模式可以分类
- 根据目的。
- 根据范围。
根据目的,模式可分为创建型、结构型和行为型。例如,
- 我们刚刚学习的观察者模式是一种行为模式(因为它帮助我们模拟对象的行为和交互)
- 生成器模式是一种创建型模式(因为它详细说明了如何以特定方式创建对象),依此类推。
这是完整的分类图。
我希望这篇文章
- 可以帮助您理解如何使用设计模式。
- 或许能在某种程度上帮助您在项目中应用设计模式
- 可能帮助你向朋友简要地介绍一下设计模式 :)
最后,如果你的脑子有点乱(这是伟大程序员的标志 ) - 我会为你推荐一个生活的艺术第一部分工作坊(见 http://www.artofliving.org/courses.html )。这是一个为期6天、共18小时的互动工作坊。就像它对我一样,我希望它能帮助你在工作和生活之间找到正确的平衡——提升你思维的清晰度,并改善你的生活质量。你可以在这里联系他们 - http://www.artofliving.org/centers/main.htm
我写的其他一些文章
另外,这是一篇关于应用提供者模式的文章 - https://codeproject.org.cn/useritems/providerframework.asp
这是我写的其他一些热门文章.. 你可以参考它们来看一些模式的实际应用。:)
BrainNet 神经网络库 - 第 1 至 3 部分 - 逐步学习神经网络编程并开发一个简单的手写检测系统
如果你想更好地理解 VB 代码,试试读我写的这篇文章..
VB.NET 教程 - 以一种非常简单的方式学习 VB.NET。
另外,您可以访问我自己的网站http://amazedsaint.blogspot.com/获取更多文章、项目和源代码
此外,您也可以查看我的技术点滴博客,下载我的开源项目,或者甚至看看我的直觉博客
非常感谢 :)
历史
- “历史可能让你意识到,生活只是一场戏”
- 2005年11月7日 - 准备本文用于发表