良好的 OO 设计 - 单例设计模式
突然之间,我遇到了一点面向对象的危机。在过去的几年里,我很好地利用了 Singleton 对象。我在很多地方都使用过它们。
例如,在设计 MVC Java 应用程序时,我会创建一个 Singleton“SystemRegistry”类来存储模型和视图类(我只开发过简单的应用程序,并且从未出现过对多个视图的需求)。
当我创建模型和视图对象(不是单例,只是普通对象)时,我会执行以下操作:
SystemRegistry.getInstance().setModel(model);
在我的控制器类(几乎是不同 GUI 项目的事件处理程序)中,我可以访问视图或模型如下:
SystemRegistry.getInstance().getView();
我永远不会在应用程序的模型部分中使用 SystemRegistry 类,但有时会在我的视图中使用它来访问(但很少(如果有的话)修改)来自模型的信息。
从我读过的内容来看(尤其是Steve Yegge 的文章),这似乎就像设计我的应用程序的糟糕方法一样。关于构建我的代码的更好方法的任何想法。
另外,我设计类的另一个方面(可能与单例相关,也可能不相关)是“管理器类型”类的使用。一个例子是我用 C++ 创建的(非常简单)基于 OpenGL 的游戏引擎。
主要类是GameEngine。它是一个过度的类,存储了一堆管理器并处理主循环等等。此类中存储的一些管理器如下所示: ObjectManager、RenderingManager、LightingManager、EventManager(包括输入)、HUDManager、FrameRateManager、WindowManager 等。可能还有更多。
基本上这些类处理游戏引擎的不同方面。这些名称非常简单,因此您应该能够很好地了解它们的使用方式。
现在,这是一个可重复使用的基础,我可以在不同的项目中使用它,并且需要对其进行理想的更改。
在每个新游戏中,我将创建一个 GameEngine 实例作为类范围变量(大部分游戏逻辑存储在单个类中)并设置不同的管理器(例如,加载窗口坐标或照明)文件中的详细信息、设置 FPS 等)。要在 ObjectManager 中注册一个对象,我会执行以下操作:
Player player = new Player();
gameEngine.getObjectManager().addObject(player);
该对象现在将存储在 ObjectManager 类中的向量中,并且当 GameEngine 在每个帧中调用 ObjectManager drawObjects() 方法时将被绘制。
在这篇关于单例的文章之后,我现在可能有点偏执了(并且可能没有足够的时间来思考它),但我开始重新猜测并想知道我设计游戏引擎的方式是否正确(因为没有更好的词)并且不仅仅陷入了与单例模式相同的陷阱。
任何对我的帖子的评论将不胜感激。
编辑:感谢您的回答。我非常感谢他们。如果可能的话,我希望有人能给我一些关于上面发布的两个项目场景的提示。我怎样才能避免使用单例/管理器?
对于第一个,DI 是正确的反应吗?我是否应该授予对模型的视图访问权限(这可能更像是 MVC 响应)?视图是否会从实现接口中受益(以便可以插入多个不同的视图)?
在第二种情况下,还可以如何构建应用程序?抱怨只是使用 Manager 类而不是更具体的名称吗?或者在某些情况下,类可以进一步细分(例如ObjectHolder、ObjectDrawer、ObjectUpdater)?
All of a sudden I'm having a bit of an OO crisis. Over the last couple of years I've made quite good use of Singleton objects. I used them in many places.
For example, in designing an MVC Java application, I'd create a Singleton 'SystemRegistry' class to store the model and view classes (I've only worked on simple apps and the need for multiple views never came up).
When I create my model and view objects (which weren't singletons, just normal objects), I'd do something like:
SystemRegistry.getInstance().setModel(model);
In my controller classes (which were pretty much event handlers for different GUI items), I'd get access to the view or model as follows:
SystemRegistry.getInstance().getView();
I would never use the SystemRegistry class in the model portion of my app but would, at times, use it in my view to access (but rarely, if ever, to modify) information from the model.
From what I've read (notably Steve Yegge's article), this seems like a poor way to design my application. Any ideas as to better ways of structuring my code.
Also, another aspect of how I design classes, which may, or may not be related to Singletons, is the use of 'Manager-type' classes. An example is a (very simple) OpenGL-based game engine I created in C++.
The main class was GameEngine. It was the over-arcing class that stored a bunch of Managers and handled the main loop and what not. Some of the Managers stored in this class were things like:
ObjectManager, RenderingManager, LightingManager, EventManager (includes input), HUDManager, FrameRateManager, WindowManager, etc. There were probably a few more as well.
Basically these classes handled the different aspects of the game engine. The names are pretty straightforward so you should be able to get a good idea of how they're used.
Now, this was meant to be a reusable base that I could use in different projects with the need to change it ideally.
In each new game, I would create an instance of the GameEngine as a class-wide variable (most of the game logic was stored in a single class) and set up the different managers (for example, loading the window co-ordinates or lighting details from a file, setting the FPS, etc). To register an object in the ObjectManager I would do something like:
Player player = new Player();
gameEngine.getObjectManager().addObject(player);
This object will now be stored in a vector in the ObjectManager class and will be drawn when the GameEngine calls the ObjectManager drawObjects() method in each frame.
I might've gotten a bit paranoid now after that article on Singletons (and might not have had enough time to wrap my head around it), but I'm starting to second guess and wonder if the way I designed my GameEngine was proper (for lack of a better word) and didn't just fall into the same pitfalls shared by the Singleton pattern.
Any comments on my post would be much appreciated.
Edit: Thanks for the answers. I appreciate them greatly. If possible, I'd love if someone could give me some hints regarding the two project scenarios posted above. How could I have avoided the use of Singletons / Managers?
With the first one, would DI have been the correct response? Should I have even given the view access to the model (this is probably more of an MVC response)? Would the view benefit from implementing an interface (so that multiple different views can be plugged in)?
In the second case, how else could one have structured the application? Is the gripe simply the use of Manager classes as opposed to more specific names? Or is it that, in some cases, the classes can be further broken-down (e.g. ObjectHolder, ObjectDrawer, ObjectUpdater)?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
那么,你为什么要写单例呢?如果您了解您的单侧炎出了什么问题,您就知道将来要注意什么。
为什么您有那么一瞬间相信单例是解决任何类型问题的好方法?因为一本书上这么说的?不要盲目相信书本。书籍需要像其他人一样证明其主张的合理性。如果我告诉你,如果你把显示器倒过来,你的代码看起来会好得多,那么举证责任就在我身上。 即使即使我是某种代码之神。即使世界各地数以百万计的开发者每天崇拜我,如果我不能证明它的合理性,如果我不能让你“好吧,那是有道理的。我的建议仍然毫无价值。理解你为什么推荐这个,我想不出更好的方法来做到这一点”。
某本设计模式书籍也是如此。所以他们写道“设计模式很棒”,并且“单例是一种设计模式,因此它也很棒”。所以呢?他们能否证明这一主张是合理的(不,他们不能,至少在单例情况下不能),所以忽略它。
如果其他人建议您使用单例,逻辑是相同的:这些人是否真的提出了一个很好的论据来说明为什么这是一个好主意?
如果你自己想出了一个理由,今天你能看出它有什么问题吗?您忘记考虑什么?
避免将来犯太多错误的唯一方法就是从已经犯过的错误中吸取教训。单例或经理类是如何突破你的防线的?当您第一次了解它们时应该注意什么?或者就此而言,稍后,当您在代码中自由使用它们时。有哪些警告信号应该让您想知道“这些单身人士真的是正确的选择吗?”作为一名程序员,你必须相信自己的想法。你必须相信你会做出明智的决定。做到这一点的唯一方法就是从错误的决定中吸取教训。因为我们都经常这样做。我们能做的最好的事情就是确保我们不会重复做出同样的错误决定。
对于经理类,他们的问题是每个类都应该有一个职责。 “经理班”的职责是什么?它……呃……管理东西。如果你不能定义一个简洁的职责范围,那么这个类就是错误的。这些对象到底需要管理什么?管理它们意味着什么?标准库已经提供了用于存储一组对象的容器类。
然后,您需要一些代码来负责绘制存储在那里的所有对象。但它不必与存储对象是同一个对象。
还需要管理什么?找出“管理”这些对象的含义,然后您就知道需要哪些类来进行管理。它很可能不是一个单一的整体任务,而是几种不同类型的职责,应将其分为不同的类。
关于单例,我不会重复之前说过很多次的内容,所以这里有一个 链接。
最后一条建议:
搞砸 OOP。真的。好的代码并不等于 OOP。有时,课程是完成这项工作的一个很好的工具。有时,他们只是把一切搞乱,并将最简单的代码片段埋藏在无尽的抽象层后面。
研究其他范式。如果您使用 C++,您需要了解泛型编程。 STL 和 Boost 是如何利用泛型编程来编写代码来解决许多问题的很好的例子,这些代码比等效的 OOP 代码更干净、更好、更高效。
无论使用哪种语言,都可以从函数式编程中学到许多宝贵的经验教训。
有时,普通的旧式过程编程正是您需要的又好又简单的工具。
“好的设计”的关键是不尝试“好的OO设计”。如果你这样做,你就会将自己锁定在这一范式中。你不妨尝试找到一种用锤子建造房子的好方法。我并不是说这是不可能的,但是如果您还允许自己使用其他工具,那么还有更好的方法可以建造更好的房屋。
至于你的编辑:
DI 可能是避免单例的一个选择。但不要忘记老式的低技术选项:只需手动传递对需要了解“以前的”单例的对象的引用。
您的渲染器需要了解游戏对象列表。 (或者确实如此?也许它只有一个需要提供对象列表的方法。也许渲染器类本身不需要它)因此应该为渲染器的构造函数提供对该列表的引用。这就是全部内容了。当 X 需要访问 Y 时,您可以在函数参数的构造函数中向其传递对 Y 的引用。与 DI 相比,这种方法的好处是你可以让你的依赖关系变得非常明确。您知道渲染器知道可渲染对象的列表,因为您可以看到传递给构造函数的引用。而且您必须写这种依赖关系,给您一个很好的机会停下来问“这有必要吗”?并且您有消除依赖性的动机:这意味着更少的打字!
使用单例或 DI 时最终产生的许多依赖项都是不必要的。这两个工具都可以轻松轻松地在不同模块之间传播依赖关系,这就是您所做的。然后你最终得到一个设计,其中渲染器知道键盘输入,并且输入处理程序必须知道如何保存游戏。
DI 的另一个可能的缺点是一个简单的技术问题。并非所有语言都有良好的 DI 库可用。有些语言几乎不可能编写健壮且通用的 DI 库。
我认为简单地重命名它们是一个好的开始,是的。就像我上面说的,“管理”你的对象意味着什么?如果我管理这些对象,我应该做什么?
你提到的三个类别听起来像是一个很好的划分。当然,当您深入设计这些类时,您可能想知道为什么需要一个
ObjectHolder
。这不正是标准库容器/集合类所做的吗?您是否需要一个专门的类来“保存对象”,或者您可以简单地使用List
吗?所以我认为你的两个选择实际上归结为同一件事。如果您可以为类指定一个更具体的名称,那么您可能不需要将其拆分为多个较小的类。但是,如果您无法想出一个单一的名称来明确该类应该做什么,那么它可能需要分解为多个类。
想象一下,您将代码搁置了半年。当您回过头来时,您是否知道“ObjectManager”类的用途是什么?可能不会,但您会很清楚“ObjectRenderer”的用途。它渲染对象。
Well, why did you write singletons? If you understand what went wrong in your singletonitis, you know what to watch out for in the future.
Why did you believe for a second that singletons were a good solution to any kind of problem? Because a book said so? Don't blindly trust books. Books need to justify their claims just like anyone else. If I tell you your code looks much better if you turn your monitor upside down, the burden of proof is on me. Even if I'm some kind of code god. Even if millions of developers across the world worship me daily, my advice is still worth nothing if I can't justify it, and if I can't make you go "ok, that makes sense. I understand why you recommend this, and I can't think of a better way to do it".
And the same is true for a certain design patterns book. So they wrote that "design patterns are awesome", and that "the singleton is a design pattern, and therefore it, too, is awesome". So what? Can they justify this claim (no they can't, at least not in the singleton case), so ignore it.
If someone else suggested that you use a singleton, the logic is the same: did these people actually present a good argument for why it was a good idea?
And if you came up with a justification yourself, can you, today, see what was wrong with it? What did you forget to take into account?
The only way to avoid making too many mistakes in the future is to learn from the mistakes you've already made. How did singletons or manager classes slip by your defenses? What should you have been watching out for when you first learned about them? Or for that matter, later on, when you were using them liberally in your code. What warning signs were there that should have made you wonder "are these singletons really the right way to go?" As a programmer, you have to trust your own mind. You have to trust that you'll make sensible decisions. And the only way to do that is to learn from the bad decisions when you make them. Because we all do, all too often. The best we can do is to make sure we won't make the same bad decisions repeatedly.
Regarding manager classes, their problem is that every class should have exactly one responsibility. What is the responsibility of a "manager class"? It.... uh.... manages stuff. If you can't define a concise area of responsibility, then the class is wrong. What exactly needs managing about these objects? What does it mean to manage them? The standard library already provides container classes for storing a group of objects.
Then you need a bit of code with the responsibility for drawing all the objects stored there. But that doesn't have to be the same object as stores the objects.
What else needs managing? Find out what "managing" these objects means, and then you know what classes you need to do the managing. It's most likely not a single monolithic task, but several different kinds of responsibilities, which should be split out into different classes.
Regarding singletons, I won't repeat what I've said so many times before, so here's a link.
One final piece of advice:
Screw OOP. Really. Good code is not synonymous with OOP. Sometimes classes are a nice tool for the job. Sometimes, they just clutter everything up, and bury the simplest pieces of code behind endless layers of abstraction.
Look into other paradigms. If you're working in C++, you need to know about generic programming. The STL and Boost are nice examples of how to exploit generic programming to write code to solve many problems that is both cleaner, better and more efficient than the equivalent OOP code.
And regardless of language, there are many valuable lessons to be learned from functional programming.
And sometimes, plain old procedural programming is just the nice and simple tool you need.
The key to "good design" is not to attempt "good OO design". If you do that, you lock yourself into that one paradigm. You might as well try to find a good way to build a house using a hammer. I'm not saying it's not possible, but there are much better ways to build much better houses if you also allow yourself to use other tools.
As for your Edit:
DI would have been one option for avoiding singletons. But don't forget the old-fashioned low-tech option: simply manually pass a reference to the objects that need to know about your "former" singleton.
Your renderer needs to know about the list of game objects. (Or does it really? Maybe it just has one single method that needs to be given the list of objects. Maybe the renderer class in itself doesn't need it) So the renderer's constructor should be given a reference to that list. That's really all there is to it. When X needs access to Y, you pass it a reference to Y in a constructor of function parameter. The nice thing about this approach, compared to DI is that you make your dependencies painfully explicit. You know that the renderer knows about the list of renderable objects, because you can see the reference being passed to the constructor. And you had to write this dependency out, giving you a great opportunity to stop and ask "is this necessary"? And you have a motivation for eliminating dependencies: it means less typing!
Many of the dependencies you end up with when using singletons or DI are unnecessary. Both of these tools make it nice and easy to propagate dependencies between different modules, and so that's what you do. And then you end up with a design where your renderer knows about keyboard input, and the input handler has to know how to save the game.
Another possible shortcoming of DI is a simple question of technology. Not all languages have good DI libraries available. Some language make it near impossible to write a robust and generic DI library.
I think simply renaming them is a good start, yes. Like I said above, what does it mean to "manage" your objects? If I manage those objects, what am I expected to do?
The three classes you mention sound like a good division. Of course, when you dig into designing those classes, you may wonder why you even need an
ObjectHolder
. Isn't that exactly what the standard library container/collection classes do? Do you need a dedicated class for "holding objects", or can you get away with simply using aList<GameObject>
?So I think both your options really come down to the same thing. If you can give the class a more specific name, then you probably don't need to split it out into multiple smaller classes. But if the you can't think of a single name that makes it clear what the class is supposed to do, then it probably needs to be broken down into multiple classes.
Imagine that you put your code away for half a year. When you come back to it, will you have any idea what the purpose of the "ObjectManager" class was? Probably not, but you'll have a pretty good idea what the "ObjectRenderer" is for. It renders objects.
史蒂夫·叶格是对的。 Google 走得更远:他们有一个 Singleton 检测器来帮助 root出去。
我认为单例存在一些问题:
但有时您确实确实必须只有一个。我只是不会因为患有模式综合症的小男孩而得意忘形。
Steve Yegge is right. Google goes even further: they've got a Singleton detector to help root them out.
I think there are a few problems with Singletons:
But sometimes you really do have to have just one. I'd just not get carried away with Small Boy With A Pattern Syndrome.
关于您原来的问题,所有内容都已经说了,所以这是关于您编辑中的后续问题。
不要将只有一个类实例与单例模式混淆。就您而言,拥有一个管理各种事物的经理类就很好了。整个应用程序只有一个实例也是非常酷的。但是使用管理器的类不应该仅仅依赖于一个全局对象的存在(无论是通过类的静态变量还是由所有人访问的全局变量)。为了缓解这种情况,让它们在构造时需要引用管理器对象。他们都可以接收对同一对象的引用,他们不会关心。这有两个很大的副作用。首先,您可以更好地测试您的类,因为您可以轻松注入模拟管理器对象。第二个好处是,如果您稍后决定这样做,您仍然可以使用多个管理器对象(例如在多线程场景中),而无需更改客户端。
Regarding your original question all has been said, so this about the followup question in your edit.
Do not confuse having only one instance of a class with the singleton pattern. In your case, it is fine to have a manager class that manages all kinds of things. It is also totally cool to only have one instance it for your whole application. But classes that use the manager should not rely on the existence of exactly one global object (be it via a static variable for the class or a global variable that is accessed by all). To mitigate this, make them require a reference to a manager-object when they are constructed. They can all receive a reference to the same object, they won't care. This has two great side-effects. First, you can test your classes better, because you can easily inject a mock-manager object. The second benefit is that, if you decide so later, you can still use more than one manager object (e.g. in multi-threading scenarios) without having to change the clients.
最近,当我查看我的一个项目并看到“管理器”类(主要是单例)的激增时,我有了类似的认识。看来您的担忧是双重的 - 首先,是否使用全局状态(导致管理器类型类),其次,单例是否是全局状态的最佳选择。
我认为这并不是真正的黑白分明。有时,您的实体实际上应该只有一个实例,并且应该可以从程序中的任何位置访问,在这种情况下,全局状态解决方案是有意义的。如果非全局替代方案涉及创建实体的实例并通过一层又一层的方法和构造函数(以及类构造函数的层次结构呃)传递对其的引用,那么这会很方便。
另一方面,我们需要考虑全球国家的弊端。它隐藏了依赖关系。一旦引入全局变量,函数就不再是输入仅限于其参数的黑匣子。如果您有可变的全局状态,并且多个线程中的多个类可以以任意顺序访问该状态,那么您可能会遇到调试噩梦。这一切都使得理解/测试变得更加困难。
当选择全局状态时,有时静态类是比单例更好的选择(还有其他选项)。其一,它节省了额外的 GetInstance() 调用……对我来说,它感觉更自然。但值得注意的是,单例会自动允许延迟加载,如果全局实体的初始化相当重量级,这可能很重要。此外(这里有点人为),如果您需要提供全局行为的变体,您可以简单地对您的单例进行子类化。
I had a similar sort of realisation somewhat recently when looking over one of my projects and seeing the proliferation of 'manager' classes (mostly singletons). It seems that your concerns are twofold - firstly, whether to use global state at all (leading to manager type classes), and secondly, whether singletons are the best choice for global state.
I don't think it's really black and white. Sometimes you have entities which really should have exactly one instance and should be accessible from everywhere in your program, in which case, a global state solution makes sense. And it can be convenient if the non-global alternative involves creating an instance of your entity and passing around references to it through layers upon layers of methods and constructors (and hierarchies of class constructors ugh).
On the other hand, one needs to consider the evils of global state. It hides dependencies. As soon as you introduce globals, functions are no longer black boxes whose input is restricted to their parameters. And if you have mutable global state which can be accessed by multiple classes in multiple threads in any order, you can have a debugging nightmare on your hands. And this all makes comprehension/testing more difficult.
When opting for global state, sometimes a static class is a better choice than a singleton (there are other options too). For one, it saves extra GetInstance() invocations... and to me, it just feels more natural. But it's worth noting that singletons automatically allow for lazy loading, which can be important if initialisation of your global entity is rather heavyweight. Furthermore (and somewhat contrived here), should you need to provide variants of your global behaviour, you can simply subclass your singleton.
我决定是否需要单例的方式是当我回答这个问题时:“如果我在同一运行时实例化我的应用程序两次,这有意义吗?”,如果答案是肯定的,那么我就使用它。
The way I decide if I want a singleton or not is when I answer the question: "If I'd instance my application twice in the same runtime, would it make sense?", if the answer is yes then I use it.
两步回答:
1)http://misko .hevery.com/2008/08/17/singletons-are-pathological-liars/
如果创建单例是为了确保仅存在一个实例,那么单例就是很好的设计。如果它们是为了确保全球访问而创建的,那么它们的设计就很糟糕。
2) http://misko.hevery。 com/2008/08/21/where-have-all-the-singletons-gone/
通过使用依赖注入来对抗全局访问。
A two-step-answer:
1) http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/
Singletons are good design if they are created to ensure only one instance exists. They are bad design if they are created to ensure global access.
2) http://misko.hevery.com/2008/08/21/where-have-all-the-singletons-gone/
Global access is countered by using dependency injection.