在面向对象的世界中处理"全局"数据结构
这个问题有很多答案,我很想知道别人认为什么是"最佳实践"。
请考虑以下情况:我们有一个面向对象的程序,其中包含许多不同类所需的一个或者多个数据结构。我们如何使这些数据结构可访问?
- 例如,我们可以在构造函数中显式传递引用。这是"适当的"解决方案,但是它意味着在整个程序中复制参数和实例变量。这使得更改或者添加全局数据变得困难。
- 我们可以将所有数据结构放在单个对象中,并传递对该对象的引用。这可以是为此目的而创建的对象,也可以是程序的"主"对象。这简化了(1)的问题,但是数据结构可能相互之间也可能没有任何关系,并且将它们收集在一个对象中是相当随意的。
- 我们可以使数据结构"静态"。这使我们可以直接从其他类中引用它们,而不必传递引用。这完全避免了(1)的缺点,但显然不是OO。这也意味着该程序只能有一个实例。
当数据结构很多,而所有类都需要这些数据结构时,我倾向于使用(2)。这是OO纯度和实用性之间的折衷。其他人做什么? (对于它的价值,我主要来自Java领域,但是此讨论适用于任何OO语言。)
解决方案
我不喜欢我们提出的任何解决方案:
- 我们正在传递一堆"上下文"对象-使用它们的事物并没有指定它们真正感兴趣的字段或者数据片段
- 请参阅此处以获取有关"上帝对象"模式的说明。这是世界上最糟糕的
- 根本不要将Singleton对象用于任何事情。我们似乎自己已经发现了一些潜在的问题
我倾向于使用3),并且在线程之间的同步和锁定方面要非常小心。我同意它不是面向对象的,但是我们承认拥有全局数据,而这首先是非面向对象的。
无论我们是纯粹坚持一种编程方法还是坚持另一种编程方法,都不要太着迷,找到适合我们问题的解决方案。我认为单例(例如Logging)有完全有效的上下文。
我结合使用了一个全局对象和通过构造函数传递接口的组合。
从一个主要的全局对象(通常以程序被调用或者执行的程序命名),我们可以启动其他全局对象(也许具有自己的线程)。这样,我们可以控制主对象构造函数中程序对象的设置,并在应用程序停止在该主对象析构函数中时按正确的顺序再次将其拆解。直接使用静态类会使初始化/取消初始化这些类以受控方式使用的任何资源变得棘手。这个主要的全局对象还具有一些属性,用于获取应用程序不同子系统的接口,各种对象可能希望抓住它们进行工作。
我还将对相关数据结构的引用传递给某些对象的构造函数,在这些对象中,当它们只需要关心一小部分时,将它们与程序中的其他部分隔离开是很有用的。
一个对象是获取全局对象并浏览其属性以获取所需的接口,还是通过其构造函数传递其使用的接口,这是一种品味和直觉的问题。我们正在实现的任何对象,我们认为可能在其他项目中重用的对象,都应通过其构造函数明确地传递给它使用的数据结构。抢占全局对象的对象应该更多地与应用程序的基础结构有关。
接收通过构造函数使用的接口的对象可能更易于进行单元测试,因为我们可以为它们提供一个模拟接口,并勾选它们的方法以确保它们返回正确的参数或者与模拟接口正确交互。要测试访问主要全局对象的对象,我们必须模拟主要全局对象,以便当它们从该接口请求接口(我经常称这些服务)时,它们将获得适当的模拟对象并可以对其进行测试。
我真的真的不鼓励我们使用选项3来使数据静态化。我参与了多个项目,早期的开发人员将一些核心数据设为静态,但后来才意识到他们确实需要运行该程序的两个副本,并且进行了大量的工作,使数据变为非静态,并仔细地将引用放入一切。
因此,以我的经验,如果我们执行3),我们最终将以两倍的成本完成1)。
继续学习1,然后细化一下我们从每个对象引用的数据结构。不要使用"上下文对象",而只是传递所需的数据。是的,这使代码更加复杂,但从正面来看,它使FwurzleDigestionListener持有对Fwurzle和DigestionTract的引用这一事实立即使读者对其目的有所了解。 。
而且,根据定义,如果数据格式发生变化,则对其进行操作的类也将发生变化,因此我们无论如何都必须对其进行更改。
我们可能需要考虑改变许多对象需要了解相同数据结构的要求。似乎没有一种干净的OO共享数据的方式的一个原因是,共享数据不是非常面向对象的。
我们将需要查看应用程序的细节,但是总体思路是让一个对象负责共享数据,该共享数据根据封装在其中的数据为其他对象提供服务。但是,这些服务不应涉及为其他对象提供数据结构,而只是为其他对象提供它们满足其职责所需的信息,并在内部对数据结构执行更改。
全局数据并不像许多面向对象的纯粹主义者所声称的那样糟糕!
毕竟,在实现OO类时,通常会在操作系统上使用API。如果不是大量的全球数据和服务,这会是多么糟糕!
如果我们在程序中使用一些全局性的内容,那么我们只是在扩展这个庞大的环境,类实现可以使用一些特定于应用程序域的数据来看到操作系统。
在OO课程和书籍中通常会在各处传递指针/引用,这在学术上听起来不错。实用上,这通常是要做的事情,但是盲目绝对地遵循此规则是错误的。对于一个大小合适的程序,最终可能会在整个地方传递一堆引用,这可能导致完全不必要的繁琐工作。
在体面大小的应用程序中,全球可访问的服务/数据提供程序(显然是在漂亮的界面后面进行了抽象)是非常必要的。
选项3)虽然不是纯粹的OO,但往往是最合理的解决方案。但是我不会让你的课单身。并使用其他一些对象作为静态"字典"来管理那些共享资源。
对于这种情况,我更喜欢使用GoF书中所述的单例模式。单例与问题中描述的三个选项都不相同。构造函数是私有的(或者受保护的),因此不能在任何地方使用。我们可以使用get()函数(或者任何我们喜欢的函数)来获取实例。但是,单例类的体系结构保证对get()的每次调用都返回相同的实例。
我们应该注意不要将面向对象的设计与面向对象的实现相混淆。通常,术语OO设计用于判断实现,就像恕我直言,它在这里。
设计
如果在设计中看到很多对象都引用了完全相同的对象,则意味着有很多箭头。设计师在这里应该会感到痒。他应该验证该对象是否只是常用对象,或者它是否真的是一个实用程序(例如,COM工厂,某种注册表等等)。
从项目的需求中,他可以看到它是否真的需要一个单例(例如" Internet"),或者是否因为对象太笼统,太昂贵或者任何其他原因而共享了该对象。
执行
当要求我们使用OO语言实现OO设计时,我们会面临很多决定,就像我们提到的那样:我应该如何在设计中经常使用的对象上实现所有箭头?
这就是解决"静态成员","全局变量","神类"和"很多功能参数"的问题所在。
设计阶段应该已经阐明了对象是否需要为单例。实施阶段将决定在程序中如何表示这种单一性。