【问题标题】:Is it bad practice to have a long initialization method?有一个长的初始化方法是不好的做法吗?
【发布时间】:2010-04-12 13:20:16
【问题描述】:

Manypeople 争论过函数大小。他们说一般来说功能应该很短。意见从 15 行到“大约一屏”不等,今天大概是 40-80 行。
此外,函数应该始终只完成一项任务。

但是,在我的代码中,有一种函数在这两个标准中经常失败:初始化函数。

例如,在音频应用程序中,必须设置音频硬件/API,必须将音频数据转换为合适的格式,并且必须正确初始化对象状态。这显然是三个不同的任务,根据 API,这可以轻松跨越 50 多行。

init-functions 的特点是它们通常只被调用一次,因此不需要重复使用任何组件。您还会将它们分解成几个较小的函数吗?您认为大型初始化函数可以吗?

【问题讨论】:

    标签: function initialization code-size


    【解决方案1】:

    我仍然会按任务分解函数,然后从面向公众的初始化函数中调用每个较低级别的函数:

    void _init_hardware() { }
    void _convert_format() { }
    void _setup_state() { }
    
    void initialize_audio() {
        _init_hardware();
        _convert_format();
        _setup_state();
    }
    

    编写简洁的函数既是为了隔离错误和变化,也是为了保持可读性。如果您知道故障在_convert_format(),您可以更快地找到导致错误的大约 40 行。如果您提交的更改仅涉及一个功能,则同样适用。

    最后一点,我经常使用assert(),因此我可以“经常失败并尽早失败”,并且函数的开头是几个健全性检查断言的最佳位置。保持函数简短可以让您根据其更狭窄的职责集更彻底地测试该函数。对执行 10 种不同操作的 400 行函数进行单元测试非常困难。

    【讨论】:

    • +1: "没有必要重复使用任何组件。"重复使用不是问题。编写可以被其他人理解和维护的东西要重要得多。
    • 我记得有一条建议是将下划线开头的标识符留给 C 编译器的内部使用,并在程序中避免使用它们。此外,您应该将这三个一次性初始化函数标记为static。这一次,它们不会在当前源文件之外使用。另外一个好处是,智能编译器会看到它们只被调用一次,并且只是内联代码(以防你担心调用开销)。
    【解决方案2】:

    如果分解成更小的部分可以使代码结构更好和/或更易读 - 无论函数做什么,都要这样做。这与代码质量无关。

    【讨论】:

      【解决方案3】:

      我仍然会尝试将功能分解为逻辑单元。它们应该尽可能长或短。例如:

      SetupAudioHardware();
      ConvertAudioData();
      SetupState();
      

      为它们指定清晰的名称可以让所有内容更加直观和易读。此外,将它们分开可以让未来的更改和/或其他程序更容易重用它们。

      【讨论】:

        【解决方案4】:

        在这种情况下,我认为这归结为个人喜好问题。我更喜欢让函数只做一件事,因此我会将初始化拆分为单独的函数,即使它们只被调用一次。但是,如果有人想在一个函数中完成所有操作,我不会太担心(只要代码清晰)。还有更重要的事情需要争论(比如花括号是否属于自己的单独行)。

        【讨论】:

          【解决方案5】:

          如果您有很多组件需要相互插入,那么拥有一个大方法当然是相当自然的——即使每个组件的创建在可行的情况下都被重构为一个单独的方法。

          一种替代方法是使用依赖注入框架(例如 Spring、Castle Windsor、Guice 等)。这有明确的优点和缺点......虽然通过一种大方法工作可能会很痛苦,但您至少对所有内容的初始化位置有一个很好的了解,并且无需担心可能会发生什么“魔法” .再说一次,在部署之后不能更改初始化(例如,它可以使用 Spring 的 XML 文件)。

          我认为设计代码的主体以便它可以被注入是有意义的 - 但注入是通过框架还是只是一个硬编码(可能很长)的列表初始化调用是一个选择,可能会因不同的项目而改变。在这两种情况下,除了运行应用程序之外,很难测试结果。

          【讨论】:

            【解决方案6】:

            首先,应该使用工厂而不是初始化函数。也就是说,你有一个new AudioObjectFactory,而不是initialize_audio()(你可以在这里想一个更好的名字)。这保持了关注点的分离。

            但是,也要注意不要过早抽象。显然,您确实已经有两个问题:1)音频初始化和 2)使用该音频。直到,例如,您抽象了要初始化的音频设备,或者在初始化期间可能配置给定设备的方式,您的工厂方法(audioObjectFactory.Create() 或其他)实际上应该只保留一个大方法。早期的抽象只会混淆设计。

            请注意,audioObjectFactory.Create() 不是可以进行单元测试的东西。测试它是一个集成测试,直到它的部分可以被抽象出来,它仍然是一个集成测试。稍后,您可能会发现您有多个不同的工厂用于不同的配置;此时,将硬件调用抽象到接口中可能是有益的,这样您就可以创建单元测试以确保各个工厂以正确的方式配置硬件。

            【讨论】:

            • 实际上,这就是我正在做的事情。该类本身应该是一个音频播放器对象,它抽象了 CoreAudio 的混乱处理。确实,音频数据准备和音频硬件初始化非常紧密地联系在一起,因为不同的音频数据需要不同的配置,反之亦然。
            【解决方案7】:

            我认为尝试计算行数并以此确定功能是错误的方法。对于初始化代码之类的东西,我通常有一个单独的函数,但主要是为了使 Load 或 Init 或 New 函数不会混乱和混乱。如果您可以像其他人建议的那样将其分成几个任务,那么您可以将其命名为有用的东西并帮助组织。即使你只调用一次,这也不是一个坏习惯,而且你经常会发现在其他时候你可能想重新初始化一些东西,并且可以再次使用该函数。

            【讨论】:

              【解决方案8】:

              我只是想把它扔掉,因为它还没有被提及 - Facade Pattern 有时被引用为复杂子系统的接口。我自己并没有做太多,但比喻通常是像打开电脑(需要几个步骤),或者打开家庭影院系统(打开电视,打开接收器,关灯等)。 .)

              根据代码结构,可能值得考虑抽象出大型初始化函数。我仍然同意 meagar 的观点,尽管将函数分解为_init_X(), _init_Y() 等是一个不错的方法。即使您不打算在此代码中重用 cmets,在您的下一个项目中,当您对自己说“我是如何初始化那个 X 组件的?”时,返回并选择它会容易得多较小的 _init_X() 函数比从较大的函数中挑选它要多,尤其是当 X 初始化分散在整个函数中时。

              【讨论】:

                【解决方案9】:

                正如您所标记的,函数长度是一个非常主观的问题。但是,标准的最佳实践是隔离经常重复和/或可以作为其自身实体运行的代码。例如,如果您的初始化函数正在加载特定库将使用的库文件或对象,则该代码块应该模块化。

                话虽如此,有一个很长的初始化方法也不错,只要它不长,因为有很多重复的代码或其他可以抽象掉的sn-ps。

                希望对您有所帮助,
                卡洛斯·努涅斯

                【讨论】:

                  猜你喜欢
                  • 2011-02-15
                  • 1970-01-01
                  • 2011-01-26
                  • 2018-03-11
                  • 1970-01-01
                  • 2015-04-10
                  • 2011-05-30
                  • 2013-01-24
                  • 2018-10-15
                  相关资源
                  最近更新 更多