Неправильно ли рефакторинг ошибок компиляции?

Я использовал некоторые рефакторинги, введя ошибки компиляции. Например, если я хочу удалить поле из моего класса и сделать его параметром для некоторых методов, я обычно сначала удаляю это поле, что вызывает ошибку компиляции для класса. Затем я бы представил параметр моим методам, который разбивал бы абонентов. И так далее. Это обычно давало мне чувство безопасности. Я вообще не читал никаких книг о рефакторинге, но я думал, что это относительно безопасный способ сделать это. Но интересно, действительно ли это безопасно? Или это плохой способ сделать что-то?

Ответ 1

Это общий и полезный метод для статически скомпилированных языков. Общая версия того, что вы делаете, может быть сформулирована следующим образом:

При внесении изменений в модуль, который может привести к аннулированию некоторых применений в клиентах этого модуля, сделайте первоначальное изменение таким образом, чтобы вызвать ошибку времени компиляции.

Существует множество следствий:

  • Если значение метода, функции или процедуры изменяется, а тип также не изменяется, измените имя. (Когда вы будете тщательно изучать и исправлять все виды использования, вы, вероятно, измените имя назад.)

  • Если вы добавите новый случай в тип данных или новый литерал для перечисления, измените имена всех существующих конструкторов типов данных или литералов перечисления. (Или, если вам посчастливилось иметь компилятор, который проверяет, является ли анализ случая исчерпывающим, есть более простые способы.)

  • Если вы работаете на языке с перегрузкой, не просто меняйте один вариант или добавляйте новый вариант. Вы рискуете, что перегрузка разрешится молча по-другому. Если вы используете перегрузку, довольно сложно заставить компилятор работать на вас так, как вы надеетесь. Единственный способ, с помощью которого я могу справиться с перегрузкой, - это рассуждать по всему миру обо всех применениях. Если ваша IDE вам не поможет, вы должны изменить имена всех перегруженных вариантов. Неприятно.

То, что вы действительно делаете, - это использовать компилятор, чтобы помочь вам изучить все места в коде, которые могут потребоваться для изменения.

Ответ 2

Я никогда не полагаюсь на простую компиляцию при рефакторинге, код может компилироваться, но могут быть введены ошибки.

Я думаю, что только написание некоторых модульных тестов для методов или классов, которые вы хотите реорганизовать, будет лучшим, а затем, проведя тест после рефакторинга, вы будете уверены, что ошибок не было.

Я не говорю, что я иду в Test Driven Development, просто напишите модульные тесты, чтобы получить необходимую уверенность в необходимости рефакторинга.

Ответ 3

Я не вижу никаких проблем с этим. Это безопасно, и до тех пор, пока вы не совершаете изменения перед компиляцией, он не имеет долгосрочного эффекта. Кроме того, у Resharper и VS есть инструменты, которые облегчают вам процесс.

Вы используете аналогичный процесс в другом направлении в TDD - вы пишете код, который может не иметь определенных методов, из-за чего он не компилируется, тогда вы пишете достаточно кода для компиляции (затем проходите тесты и т.д.)..)

Ответ 4

Когда вы будете готовы читать книги по этому вопросу, я рекомендую Michael Feather "" Эффективно работать с устаревшим кодом ". (Добавил неавтор: также классическая книга Фаулера Рефакторинг" - и 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;
     }
}

и это будет работать, но вторая аддитура будет неправильной, поскольку он вызывает метод. Дальнейшие тесты понадобятся, кроме просто компиляции.

Ответ 5

Достаточно легко подумать о примерах, где рефакторинг ошибок компилятора терпит неудачу и создает непреднамеренные результаты.
Несколько случаев, которые приходят на ум: (я предполагаю, что мы говорим о С++)

  • Изменение аргументов функции, где существуют другие перегрузки с параметрами по умолчанию. После рефакторинга наилучшее совпадение аргументов может быть не таким, как вы ожидаете.
  • Классы, в которых есть операторы cast или неявные конструкторы одиночных аргументов. Изменение, добавление, удаление или изменение аргументов любому из них может изменить наилучшие совпадения, которые вызываются, в зависимости от созвездия.
  • Изменение виртуальной функции и без изменения базового класса (или изменение базового класса по-разному) приведет к тому, что вызовы будут направлены в базовый класс.

полагаясь на ошибки компилятора, следует использовать, только если вы абсолютно уверены, что компилятор поймает каждое изменение, которое необходимо сделать. Я почти всегда склонен к подозрению в этом.

Ответ 6

Я просто хотел бы добавить ко всему мудрости, что есть еще один случай, когда это может быть небезопасно. Отражение. Это влияет на такие среды, как .NET и Java (и другие, конечно же). Ваш код будет компилироваться, но все равно будут ошибки времени выполнения, когда Reflection попытается получить доступ к несуществующим переменным. Например, это может быть довольно распространено, если вы используете ORM, например Hibernate, и забудьте обновить XML файлы отображения.

Возможно, более безопасно искать все файлы кода для конкретного имени переменной/метода. Конечно, это может вызвать много ложных срабатываний, поэтому это не универсальное решение; и вы можете использовать конкатенацию строк в своем отражении, которое также сделает это бесполезным. Но это, по крайней мере, немного ближе к безопасности.

Я не думаю, что есть 100% -ный надежный метод, хотя, не считая все код вручную.

Ответ 7

Это довольно распространенный способ, я думаю, поскольку он находит все ссылки на эту конкретную вещь. Однако современные IDE, такие как Visual Studio, имеют функцию "Найти все ссылки", которая делает это ненужным.

Однако для этого подхода есть некоторые недостатки. Для крупных проектов для компиляции приложения может потребоваться много времени. Кроме того, не делайте этого в течение длительного времени (я имею в виду, верните работу как можно скорее) и не делайте этого более чем за один раз за раз, так как вы можете забыть правильный способ, которым вы исправили в первый раз.

Ответ 8

Это способ и не может быть определенного утверждения о том, является ли оно безопасным или небезопасным, не зная, как выглядит ваш рефакторинг кода и какие варианты вы делаете.

Если он работает на вас, тогда нет оснований для изменения только ради изменения, но когда у вас есть время чтения, хотя ресурсы здесь могут дать вам новые идеи, которые вы, возможно, захотите изучить с течением времени.

http://www.refactoring.com/

Ответ 9

Еще один вариант, который вы могли бы рассмотреть, если вы используете один из сетевых расширений dotnet, - это флаг "старого" метода с атрибутом Obsolete, который будет вводить все предупреждения компилятора, но все равно оставить код вызываемым, если код за пределами вашего контроля (если вы пишете API, или, если вы не используете опцию strict в 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 */
    }

То, как я обычно это делаю, в любом случае...

Ответ 10

Это общий подход, но результаты могут различаться в зависимости от того, является ли ваш язык статическим или динамическим.

В статически типизированном языке этот подход имеет смысл, поскольку любые несоответствия, которые вы вводите, будут пойманы во время компиляции. Однако динамические языки часто будут сталкиваться только с этими проблемами во время выполнения. Эти проблемы не будут улавливаться компилятором, а скорее вашим набором тестов; если вы написали один.

У меня создается впечатление, что вы работаете со статическим языком, таким как С# или Java, поэтому продолжайте этот подход, пока не столкнетесь с какой-то серьезной проблемой, в которой говорится, что вы должны сделать иначе.

Ответ 11

Я делаю обычные рефакторинги, но все еще делаю рефакторинг, вводя ошибки компилятора. Я делаю их обычно, когда изменения не так просты и когда этот рефакторинг не является реальным рефакторингом (я меняю функциональность). Эти ошибки компилятора дают мне пятна, которые мне нужны, чтобы взглянуть и внести некоторые более сложные изменения, чем изменение имени или параметра.

Ответ 12

Это похоже на абсолютно стандартный метод, используемый в Test-Driven Development: напишите тест, ссылаясь на несуществующий класс, так что первым шагом для прохождения теста является добавление класса, затем методы и т.д., См. Beck book для исчерпывающих примеров Java.

Ваш метод рефакторинга звучит опасно, потому что у вас нет тестов на безопасность (или, по крайней мере, вы не упомянули, что у вас есть). Вы можете создать компиляционный код, который на самом деле не делает то, что вы хотите, или разбивает другие части вашего приложения.

Я бы посоветовал добавить к вашей практике простое правило: сделайте несвязанные изменения только в коде unit test. Таким образом, вы обязательно получите хотя бы локальный тест для каждой модификации, и перед тем, как сделать это, вы записали намерение модификации.

Кстати, Eclipse делает этот метод "fail, stub, write" абсурдно легким в Java: каждый несуществующий объект помечен для вас, а Ctrl-1 плюс выбор меню говорит Eclipse писать (компилируемую) заглушку для вас! Мне было бы интересно узнать, предоставляют ли другие языки и IDE аналогичную поддержку.

Ответ 13

Он "безопасен" в том смысле, что на достаточно компилируемом языке времени он заставляет вас обновлять все живые ссылки на то, что вы изменили.

Это может пойти не так, если у вас есть условно-скомпилированный код, например, если вы использовали препроцессор C/С++. Поэтому убедитесь, что вы перестраиваете все возможные конфигурации и на всех платформах, если это применимо.

Это не устраняет необходимость проверки ваших изменений. Если вы добавили параметр в функцию, компилятор не может сказать вам, было ли значение, которое вы предоставили при обновлении каждого сайта вызова для этой функции, было правильным значением. Если вы удалили параметр, вы все равно можете сделать ошибки, например, изменить:

void foo(int a, int b);

к

void foo(int a);

Затем измените вызов:

foo(1,2);

в

foo(2);

Это компилируется отлично, но это неправильно.

Лично я использую неудачи компиляции (и ссылки) как способ поиска кода для живых ссылок на меняющуюся функцию. Но вы должны иметь в виду, что это просто трудосберегающее устройство. Это не гарантирует правильность полученного кода.