关于c#:重构Singleton过度使用

Refactoring Singleton Overuse

今天我顿悟了,我做错了一切。一些历史:我继承了一个C应用程序,它实际上只是静态方法的集合,完全是C代码的过程混乱。我尽我当时所知重构了它,带来了很多大学毕业后的OOP知识。长话短说,代码中的许多实体都是单件的。

今天我意识到我需要3个新的类,每个类都遵循相同的单例模式来匹配软件的其余部分。如果我继续沿着这条光滑的斜坡滚动,最终应用程序中的每个类都将是单例的,这在逻辑上与原始的静态方法组没有什么不同。

我需要帮助重新思考这个问题。我知道依赖注入,这通常是用来打破单例诅咒的策略。但是,我有一些与重构相关的具体问题,以及所有与此相关的最佳实践。

  • 如何接受使用静态变量来封装配置信息?我有一个关于使用静态的大脑障碍,我认为这是由于大学早期的OO课程,教授说静态很糟糕。但是,每次访问这个类时,我应该重新配置它吗?在访问硬件时,是否可以保留一个指向所需地址和变量的静态指针,或者我应该继续执行Open()Close()操作?

  • 现在我有一个方法作为控制器。具体来说,我会不断地(通过硬件驱动程序)对一些外部仪器进行数据轮询。这种类型的控制器应该是前进的道路,还是应该在程序启动时为每个仪器生成单独的线程?如果是后者,如何使这个面向对象?我应该创建名为InstrumentAListenerInstrumentBListener的类吗?或者有什么标准的方法来处理这个问题?

  • 有没有更好的全局配置方法?现在我只需要在代码中大量地添加Configuration.Instance.Foo。几乎每个类都使用它,所以将它作为一个单例保存可能是有意义的。有什么想法吗?

  • 我的很多类都是像SerialPortWriterDataFileWriter这样的东西,它们必须等待这些数据流入。由于它们一直处于活动状态,我应该如何安排它们来监听数据进入时生成的事件?

  • 任何其他的资源,书籍,或评论如何摆脱单件和其他模式的过度使用将是有益的。


    好吧,这是我攻击这个问题的最佳机会:

    (1)静力学

    EDCOX1的0个问题可能是.NET中不同的东西,也就是说C++。静态基本上意味着它可以在类本身上访问。至于它的可接受性,id说它更多的是用来对一个类执行非实例特定操作的东西。或者只是一般的事情,比如Math.Abs(...)。对于全局配置,您应该使用一个静态访问的属性来保存当前/活动配置。另外,可能还有一些用于加载/保存设置配置的静态类,但是配置应该是一个对象,以便可以对其进行操作,等等。公共类myconfiguration{public const string defaultconfigPath="/config.xml";

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
      protected static MyConfiguration _current;
      public static MyConfiguration Current
      {
        get
        {
          if (_current == null)
            Load(DefaultConfigPath);
          return _current;
        }
      }

      public static MyConfiguration Load(string path)
      {
        // Do your loading here
        _current = loadedConfig;
        return loadedConfig;
      }

      // Static save function

      //*********** Non-Static Members *********//

      public string MyVariable { get; set; }
      // etc..
    }

    (2)控制器/硬件

    您可能应该研究一种反应性方法,IObserver<>IObservable<>,它是反应性框架(RX)的一部分。

    另一种方法是使用ThreadPool来安排轮询任务,因为如果有很多硬件要汇集,可能会获得大量线程。在使用任何一种线程之前,请确保了解很多关于它的信息。犯错误是很容易的,你甚至可能意识不到。这本书是一个优秀的来源,将教你很多。

    无论采用哪种方式,您都应该为管理硬件构建服务(实际上只是一个名称),这些硬件负责收集有关服务的信息(本质上是一个模型模式)。从那里,您的中央控制器可以使用它们访问保存在控制器中的程序逻辑和服务中的硬件逻辑的数据。

    (3)全局配置

    我可能在第1点中提到过这个主题,但一般来说,这就是我们要讨论的问题,如果你发现自己打字太多,你可以假设.Instance是一个对象,就把它从那里拉出来。

    1
    2
    MyConfiguration cfg = MyConfiguration.Current
    cfg.Foo // etc...

    (4)监听数据

    同样,反应式框架可以帮助您解决问题,或者您可以构建一个事件驱动的模型,该模型使用触发器来输入数据。这将确保在数据进入之前不会阻塞线程。它可以大大降低应用程序的复杂性。


    对于初学者,您可以通过"注册表"模式限制使用singleton,这实际上意味着您有一个singleton,它允许您访问其他一些预配置的对象。

    这不是一个"修复",而是一个改进,它使许多单例对象变得更加正常和可测试。如。。。(完全是人为的例子)

    1
    HardwareRegistry.SerialPorts.Serial1.Send("blah");

    但真正的问题似乎是你正在努力制作一组能够很好地协同工作的对象。在OO中有两种步骤……配置对象,并让对象完成它们的工作。

    所以,也许可以看看如何配置非单例对象以一起工作,然后将其挂起到注册表之外。

    静态:

    这里的规则有很多例外,但是一般来说,要避免它,但是它对于做单例和创建在对象上下文之外进行"一般"计算的方法很有用。(比如math.min)

    数据监控:

    最好按照您的提示来做,创建一个带有一组预配置对象的线程来进行监视。使用消息传递在线程之间进行通信(通过线程安全队列),以限制线程锁定问题。使用注册表模式访问硬件资源。

    您需要像InstrumentListner这样的东西,它使用InstrumentProtocol(每个协议的子类)来识别日志数据。这里可以使用命令模式。

    配置:

    拥有您的配置信息,并使用类似于"构建器"的模式将您的配置转换为以特定方式设置的一组对象。也就是说,不要让你的类知道配置,做一个以特定方式配置对象的对象。

    串行端口:

    我用这些做了很多工作,我有一个串行连接,它生成一个字符流,作为一个事件发布。然后我有一些东西可以将协议流解释成有意义的命令。我的协议类使用一个常规的"IConnection",其中一个serialconnection继承了它…..我还拥有tcpconnections、mockconnections等,能够注入测试数据,或者将串行端口从一台计算机传输到另一台计算机,等等。所以协议类只解释流、拥有状态机和调度命令。协议预先配置了一个连接,在协议中注册了各种各样的东西,所以当它拥有有意义的数据时,它们将被触发并做它们的事情。所有这些都是从一开始的一个配置构建的,或者如果有什么变化的话,可以即时重建。


  • 你(OP)似乎全神贯注于OO设计,好吧,我在考虑静态变量的时候会这样说。核心概念是封装和重用;有些东西您可能不太关心重用,但您几乎总是想要封装。如果它是一个静态变量,它不是真正封装的,是吗?想想谁需要访问它,为什么,以及您可以将它隐藏在客户机代码之外多远。好的设计通常可以在不破坏客户的情况下改变其内部结构,这就是您想要考虑的。我同意Scott Meyers(有效C++)关于很多事情。OOP远远超出了class关键字。如果您从未听说过它,请查看属性:是的,它们可以是静态的,而且C有一个非常好的使用它们的方法。而不是使用静态变量。就像我在这个列表项的开头所暗示的那样:当类随着时间的变化而变化时,想想如何以后不要自暴自弃,这是许多程序员在设计类时做不到的。好的。

  • 看看有人提到的RX框架。对于您描述的这种情况,要使用的线程模型如果没有关于用例imho的更多细节,是不容易确定的。确保你知道你在用线程做什么。很多人无法找到挽救他们生命的线索;这并不难,当(重新)使用代码时,确保安全是可以做到的。记住,控制器应该经常与它们控制的对象分开(例如,不是同一类);如果你不知道,可以查阅一本关于MVC的书,然后购买"四人帮"。好的。

  • 取决于你需要什么。对于许多应用程序来说,一个几乎完全由静态数据填充的类已经足够好了;就像一个免费的单例类。它可以做得非常好。有时,您更希望有多个实例或玩注入,这使得它更加复杂。好的。

  • 我建议线程和事件。使代码事件驱动变得容易实际上是C IMHO的一个优点。好的。

  • 嗯,杀死单身汉…好的。

    在我的经验中,年轻的程序员把单例应用放在很多更常见的用途上,不过是浪费类关键字而已。也就是说,它们的意思是将一个有状态的模块放入一个Highlander类中;还有一些不好的单例实现要匹配。这是因为他们没有学习他们在做什么,或者只是在大学里有Java,我不知道。回到C语言中,它被称为在文件范围内使用数据并公开API。在C语言(和Java)中,你有点像它是一个比很多语言都多的类。哎呀!=class关键字;学习好lhs。好的。

    一个编写得体的类可以使用静态数据来有效地实现一个单例,并使编译器完成保持一个单例的工作,或者像您将要得到任何东西一样。除非你真的知道你在干什么,否则不要用继承来取代单身。对这些东西的继承做得不好,会导致更脆弱的代码,对waaaay了解得更多。类应该是哑的,数据是智能的。这听起来很愚蠢,除非你仔细看了一下声明。将继承imho用于这种情况通常是一件坏事(tm),因为某种原因,语言具有模块/包的概念。好的。

    如果你愿意的话,嘿,你很久以前就把它变成了单件的,对吗?坐下来想一想:我如何才能最好地构造这个应用程序,以使它以xxx的方式工作,然后思考如何以xxx的方式影响事情,例如,这样做将是线程间争用的一个来源吗?你可以在一个小时内完成很多事情。当你长大后,你会学到更好的技巧。好的。

    下面是一个关于xxx方法的建议,从:(可视化)编写(^hing)一个复合控制器类开始,该类作为它引用的对象的管理器工作。这些对象是您的单例对象,而不是控制器持有它们,它们只是这些类的实例。这不是许多应用程序的最佳设计(特别是在重线程的IMHO中可能是一个问题),但它通常可以解决导致大多数年轻人只接触到一个单例的问题,并且它可以适用于大量的程序。就像设计图案CS102。忘了你在CS607中学到的单身汉吧。好的。

    这个控制类,也许"应用程序"更合适;,基本上解决了您对单例和存储配置的需求。如何以一种崇高的OO方式(假设你确实理解OOP),而不是开枪(再次),是你自己教育的一个练习。好的。

    如果这表明,我不喜欢所谓的单例模式,尤其是它经常被误用。将代码库移出它,通常取决于您准备使用多少重构。单子就像是全局变量:方便,但不是黄油。嗯,我想我会把它放在我的报价文件里,有一个很好的短语…好的。

    老实说,您比这里的任何人都了解更多有关代码库和应用程序的信息。所以没人能真正为你设计它,建议比行动更能说明问题,至少我来自哪里。好的。好啊。


    既然您知道依赖注入,您是否考虑过使用IOC容器来管理生命周期?请看我对静态类问题的回答。


    好问题。我的一些想法…

    c中的static只应用于与给定类的所有实例完全相同的数据。既然你现在被困在单身的地狱里,不管怎么说,你只有一个所有事情的实例,但是一旦你打破了这一点,这就是一般规则(至少,这是为我)。如果您开始对类进行线程处理,那么您可能希望在静态使用上退后,因为这样会有潜在的并发问题,但这是以后可以解决的问题。

    我不确定您的硬件实际上是如何工作的,但是假设有一些基本功能在所有这些功能中都是相同的(例如,您如何在原始数据级别或类似级别与它们进行交互),那么这是创建类层次结构的完美实例。基类使用虚拟方法实现低级的/类似的东西,以便子类重写以实际正确地解释数据/向前提供数据/做任何事情。

    祝你好运。


    我最近不得不解决一个类似的问题,我所做的工作似乎对我很好,也许它会帮助你:

    (1)将所有"全局"信息分组为一个类。我们称之为Configuration

    (2)对于所有使用这些静态对象的类,将它们更改为(最终)继承自一个看起来类似

    1
    2
    3
    4
    5
    6
    abstract class MyBaseClass {
        protected Configuration config; // You can also wrap it in a property
        public MyBaseClass(Configuration config) {
            this.config = config;
        }
    }

    (3)相应地更改从MyBaseClass派生的类的所有构造函数。然后只需在开始时创建一个Configuration实例,并将其传递到所有地方。

    欺骗:

    • 您需要重构许多构造函数以及调用它们的每个地方。
    • 如果您不从对象派生顶级类,这就不能很好地工作。好吧,您可以将config字段添加到派生类中,这就不那么优雅了。

    赞成的意见

    • 不需要太多的努力来改变继承和构造函数,您可以使用config切换所有Configuration.Instance
    • 您完全摆脱了静态变量;因此,如果您的应用程序突然变成了一个库,并且有人试图同时调用多个方法或其他方法,那么现在就没有问题了。


    在一个应用程序/流程中,我限制自己最多只能有两个单件。其中一个通常被称为sysconfig,它包含一些可能以全局变量或其他损坏概念结束的内容。我没有第二个名字,因为到目前为止,我还没有真正达到我的极限。-)

    静态成员变量有它们的用途,但我把它们看作是直肠病学家。当你需要一个救命恩人,但几率应该是"百万比一"(塞恩菲尔德参考),你找不到更好的方法来解决这个问题。

    创建一个实现线程侦听器的基本工具类。它的派生类将具有特定于工具的驱动程序等。为每个工具实例化派生类,然后将对象存储在某种类型的容器中。在清理时,只需遍历容器。每个仪器实例都应该通过向它传递一些注册信息来构造,这些信息关于将输出/状态/任何内容发送到何处。发挥你的想象力。OO的功能变得相当强大。