【问题标题】:Is Refactoring by Compilation Errors Bad?通过编译错误进行重构很糟糕吗?
【发布时间】:2010-09-29 03:12:13
【问题描述】:

我已经习惯通过引入编译错误来进行一些重构。例如,如果我想从我的类中删除一个字段并将其作为某些方法的参数,我通常会先删除该字段,这会导致类的编译错误。然后我会将参数引入我的方法,这会破坏调用者。等等。这通常会给我一种安全感。我实际上还没有读过任何关于重构的书,但我曾经认为这是一种相对安全的方式。但我想知道,它真的安全吗?或者这是一种糟糕的做事方式?

【问题讨论】:

  • 我一直这样做,发现这是避免引入错误的好方法。然而,我没有删除任何代码。首先,我附加一个前导 _、2 或其他任何内容,以导致错误,然后查看代码。另外,我将添加“静态”以导致错误并删除代码。 Find All References 很好,但我希望你能“Find Multiple...”
  • 这项技术的好处是,您可以将“错误列表”工具箱窗口用作“任务列表”和“书签列表”。

标签: refactoring compiler-errors


【解决方案1】:

我认为这是一种非常常见的方式,因为它会找到对特定事物的所有引用。然而,现代 IDE(如 Visual Studio)具有查找所有引用的功能,这使得这变得不必要。

但是,这种方法也有一些缺点。对于大型项目,编译应用程序可能需要很长时间。另外,不要长时间这样做(我的意思是,尽快让事情恢复正常)并且一次不要做超过一件事,因为你可能会忘记你修补的正确方法第一次。

【讨论】:

    【解决方案2】:

    我看不出有什么问题。它是安全的,只要您在编译之前不提交更改,它就不会产生长期影响。此外,Resharper 和 VS 还提供了一些工具,可以让您更轻松地完成此过程。

    您在 TDD 的另一个方向上使用类似的过程 - 您编写的代码可能没有定义方法,这导致它无法编译,然后您编写足够的代码来编译(然后通过测试,等等.. .)

    【讨论】:

      【解决方案3】:

      这是一种常见的方法,但结果可能会因您的语言是静态的还是动态的而有所不同。

      在静态类型语言中,这种方法很有意义,因为您引入的任何差异都会在编译时被发现。然而,动态语言通常只会在运行时遇到这些问题。这些问题不会被编译器捕获,而是被您的测试套件捕获;假设你写了一个。

      我的印象是您正在使用 C# 或 Java 等静态语言,因此请继续使用这种方法,直到遇到某种表明您应该采取其他方式的重大问题。

      【讨论】:

        【解决方案4】:

        我进行通常的重构,但仍然通过引入编译器错误来进行重构。我通常在更改不是那么简单并且此重构不是真正的重构(我正在更改功能)时执行它们。这些编译器错误让我发现我需要查看并进行一些比名称或参数更改更复杂的更改。

        【讨论】:

          【解决方案5】:

          我在重构的时候从不依赖简单的编译,代码可以编译但是可能引入了bug。

          我认为最好只为要重构的方法或类编写一些单元测试,然后通过在重构后运行测试,您将确保没有引入任何错误。

          我并不是说要进行测试驱动开发,只是编写单元测试以获得重构所需的必要信心。

          【讨论】:

            【解决方案6】:

            当您准备阅读有关该主题的书籍时,我推荐 Michael Feather 的“Working Effectively with Legacy Code”。 (由非作者添加:也是福勒的经典著作“Refactoring” - 和Refactoring 网站可能有用。

            他谈到了在您进行更改之前识别您正在工作的代码的特征,并进行他所谓的临时重构。那就是通过反思找到代码的特征,然后把结果扔掉。

            您正在做的是将编译器用作自动测试。它将测试您的代码是否可以编译,但如果行为由于重构或是否有任何副作用而发生变化,则不会。

            考虑一下

            class myClass {
                 void megaMethod() 
                 {
                     int x,y,z;
                     //lots of lines of code
                     z = mysideEffect(x)+y;
                     //lots more lines of code 
                     a = b + c;
                 }
            }
            

            你可以重构添加

            class myClass {
                 void megaMethod() 
                 {
                     int a,b,c,x,y,z;
                     //lots of lines of code
                     z = addition(x,y);
                     //lots more lines of code
                     a = addition(b,c);  
                 }
            
                 int addition(int a, b)
                 {
                      return mysideaffect(a)+b;
                 }
            }
            

            这会起作用,但第二个附加项在调用该方法时会出错。除了编译之外,还需要进一步的测试。

            【讨论】:

              【解决方案7】:

              这是一种方式,如果不知道您要重构的代码是什么样的以及您做出的选择,就无法明确说明它是安全还是不安全。

              如果它对你有用,那么没有理由仅仅为了改变而改变,但是当你有时间阅读这里的资源时,可能会给你带来新的想法,随着时间的推移你可能想要探索。

              http://www.refactoring.com/

              【讨论】:

                【解决方案8】:

                很容易想到一个示例,其中编译器错误导致重构失败并产生意想不到的结果。
                想到的几个案例:(我假设我们在谈论 C++)

                • 将参数更改为存在其他重载且带有默认参数的函数。重构之后,参数的最佳匹配可能不是您所期望的。
                • 具有强制转换运算符或非显式单参数构造函数的类。更改、添加、删除或更改其中任何一个的参数都可以更改被调用的最佳匹配,具体取决于所涉及的星座。
                • 更改虚函数而不更改基类(或以不同方式更改基类)将导致调用被定向到基类。

                仅当您绝对确定编译器将捕获需要进行的每一个更改时,才应使用依赖编译器错误。我几乎总是对此持怀疑态度。

                【讨论】:

                • 这些确实是我可能错过的要点。我想给这个+5,但我只能给+1 :)
                【解决方案9】:

                这听起来类似于测试驱动开发中使用的绝对标准方法:编写引用不存在的类的测试,因此使测试通过的第一步是添加类,然后是方法,依此类推.有关详尽的 Java 示例,请参阅 Beck's book

                你的重构方法听起来很危险,因为你没有任何安全测试(或者至少你没有提到你有任何测试)。您可能创建的编译代码实际上并不能满足您的需求,或者会破坏应用程序的其他部分。

                我建议您在实践中添加一条简单的规则:仅在单元测试代码中进行非编译更改。这样,您就可以确保每次修改都至少有一个本地测试,并且在进行修改之前,您会在测试中记录修改的意图。

                顺便说一句,Eclipse 使这种“失败、存根、写入”方法在 Java 中变得异常简单:每个不存在的对象都为您标记,并且 Ctrl-1 加上一个菜单选项告诉 Eclipse 为您编写一个(可编译的)存根!我很想知道其他语言和 IDE 是否提供类似的支持。

                【讨论】:

                  【解决方案10】:

                  从某种意义上说,它是“安全的”,在经过充分编译时检查的语言中,它会强制您更新对已更改内容的所有实时引用。

                  如果您有条件编译的代码,它仍然可能出错,例如,如果您使用了 C/C++ 预处理器。因此,如果适用,请确保在所有可能的配置和所有平台上进行重建。

                  它并没有消除测试您的更改的需要。如果您向函数添加了参数,则编译器无法告诉您在更新该函数的每个调用站点时提供的值是否正确。如果你删除了一个参数,你仍然会出错,例如,更改:

                  void foo(int a, int b);
                  

                  void foo(int a);
                  

                  然后将调用从:

                  foo(1,2);
                  

                  到:

                  foo(2);
                  

                  这编译得很好,但它是错误的。

                  就个人而言,我确实使用编译(和链接)失败来搜索代码以查找对我正在更改的函数的实时引用。但是您必须记住,这只是一种节省劳动力的设备。它不保证生成的代码是正确的。

                  【讨论】:

                    【解决方案11】:

                    如果您使用的是 dotnet 语言之一,您可以考虑的另一个选项是使用 Obsolete 属性标记“旧”方法,这将引入所有编译器警告,但仍然保留代码可调用(如果有)超出您控制范围的代码(例如,如果您正在编写 API,或者如果您不使用 VB.Net 中的选项严格)。您可以愉快地重构,让过时的版本调用新版本;例如:

                        public string Username
                        {
                            get
                            {
                                return this.userField;
                            }
                            set
                            {
                                this.userField = value;
                            }
                        }
                    
                        public int Login()
                        {
                            /* do stuff */
                        }
                    

                    变成:

                        [ObsoleteAttribute()]
                        public string Username
                        {
                            get
                            {
                                return this.userField;
                            }
                            set
                            {
                                this.userField = value;
                            }
                        }
                    
                        [ObsoleteAttribute("Replaced by Login(username, password)")]
                        public int Login()
                        {
                            Login(Username, Pasword);
                        }
                    
                        public int Login(string username, string password)
                        {
                            /* do stuff */
                        }
                    

                    反正我就是这么干的……

                    【讨论】:

                      【解决方案12】:

                      我想补充一下这里的所有智慧,还有一种情况可能不安全。反射。这会影响 .NET 和 Java 等环境(当然还有其他环境)。您的代码可以编译,但是当反射尝试访问不存在的变量时仍然会出现运行时错误。例如,如果您使用像 Hibernate 这样的 ORM 并且忘记更新映射 XML 文件,这可能很常见。

                      在整个代码文件中搜索特定变量/方法名称可能会更安全一些。当然,它可能会带来很多误报,所以它不是一个通用的解决方案;并且您也可以在反射中使用字符串连接,这也会使这无用。但至少离安全更近了一点。

                      我不认为有 100% 万无一失的方法,除了手动检查所有代码。

                      【讨论】:

                        【解决方案13】:

                        这是静态编译语言的一种常见且有用的技术。您正在做的事情的一般版本可以表述如下:

                        当您对可能使该模块的客户端中的某些用途无效的模块进行更改时,请以导致编译时错误的方式进行初始更改。

                        有多种推论:

                        • 如果方法、函数或过程的含义发生变化,而类型也没有变化,则更改名称。 (当您仔细检查并修复所有用途后,您可能会改回名称。)

                        • 如果将新案例添加到数据类型或将新文字添加到枚举,请更改所有现有数据类型构造函数或枚举文字的名称。 (或者,如果你有幸拥有一个可以检查案例分析是否详尽的编译器,那么还有更简单的方法。)

                        • 如果您使用重载语言,不要只更改一个变体或添加一个新变体。您冒着以不同方式静默解决重载的风险。如果您使用重载,则很难让编译器以您希望的方式为您工作。我知道处理重载的唯一方法是对所有用途进行全局推理。如果您的 IDE 不能帮助您,您必须更改 all 重载变体的名称。不愉快。

                        您真正要做的是使用编译器来帮助您检查代码中可能需要更改的所有地方。

                        【讨论】:

                        • 然而,“它真的安全吗”问题的答案是“不”,请参阅 Vilx- 和 Shy 的回复。
                        猜你喜欢
                        • 2010-11-24
                        • 1970-01-01
                        • 1970-01-01
                        • 2015-09-02
                        • 2011-10-27
                        • 2011-10-10
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多