Как рефакторинг кода в какой-то общий код?

16

Фон

Я работаю над текущим проектом C #. Я не программист C #, в первую очередь программист C ++. Так что передо мной были поставлены простые задачи по рефакторингу.

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

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

проблема

Есть шесть классов: A, B, C, D, Eи F. У всех классов есть функция ExecJob(). Все шесть реализаций очень похожи. В основном сначала A::ExecJob()было написано. Затем потребовалась немного другая версия, которая была реализована B::ExecJob()путем копирования-вставки-модификации A::ExecJob(). Когда требовалась другая, немного другая версия, C::ExecJob()была написана и так далее. Все шесть реализаций имеют некоторый общий код, затем несколько разных строк кода, затем снова некоторый общий код и так далее. Вот простой пример реализации:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

Где SNгруппа точно таких же утверждений.

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

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

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

Хотя все реализации совместно используют общий код, а ExecJob()функции выглядят симпатичнее, существуют две проблемы, которые меня беспокоят:

  • Для любого изменения CommonTask()необходимо протестировать все шесть (и может быть больше в будущем) функций.
  • CommonTask()это уже сложно. Со временем это станет более сложным.

Я делаю это правильно?

Donotalo
источник
Книга Рефакторинга Мартина Фаулера содержит множество специфических методов рефакторинга кода, которые могут оказаться полезными.
Аллан

Ответы:

14

Да, вы находитесь на правильном пути!

По своему опыту я заметил, что когда все сложно, изменения происходят небольшими шагами. Что вы сделали, это шаг 1 в процессе эволюции (или процесс рефакторинга). Вот шаг 2 и шаг 3:

Шаг 2

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

Это «Шаблон дизайна шаблона», и он на шаг впереди в процессе рефакторинга. Если базовый класс изменяется, подклассы (A, B, C) не должны быть затронуты. Вы можете добавить новые подклассы относительно легко. Однако сразу же на картинке выше вы можете видеть, что абстракция нарушена. Необходимость «пустой реализации» является хорошим индикатором; это показывает, что с вашей абстракцией что-то не так. Возможно, это было бы приемлемым решением на короткий срок, но, кажется, есть лучшее.

Шаг 3

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

Это «шаблон разработки стратегии», который подходит для вашего случая. Существуют разные стратегии выполнения задания, и каждый класс (A, B, C) реализует его по-своему.

Я уверен, что в этом процессе есть шаг 4 или шаг 5 или гораздо более эффективные методы рефакторинга. Однако этот позволит вам устранить дубликаты кода и убедиться, что изменения локализованы.

Guven
источник
Основная проблема, которую я вижу в решении, описанном в «Шаге 2», заключается в том, что конкретная реализация S5 существует дважды.
user281377
1
Да, дублирование кода не исключено! И это еще один показатель того, что абстракция не работает. Я просто хотел сделать шаг 2, чтобы показать, как я думаю о процессе; пошаговый подход к поиску чего-то лучшего.
Guven
1
+1 Очень хорошая стратегия (и я не говорю о шаблоне )!
Джордао
7

Вы на самом деле делаете правильные вещи. Я говорю это потому что:

  1. Если вам нужно изменить код для общей функциональности задачи, вам не нужно менять его во всех 6 классах, которые будут содержать код, если вы не напишите его в общем классе.
  2. Количество строк кода будет уменьшено.
према
источник
3

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

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

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

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

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

Том Керр
источник
3

Если бы я был вами, я, вероятно, добавлю еще 1 шаг в начале: исследование на основе UML.

Рефакторинг кода, объединяющий все общие части, не всегда лучший шаг, звучит скорее как временное решение, чем как хороший подход.

Рисует схему UML, придерживайтесь простых, но эффективных вещей, помните о некоторых основных понятиях вашего проекта, таких как «что должно делать это программное обеспечение?» "Каков наилучший способ сохранить эту часть программного обеспечения абстрактной, модульной, расширяемой и т. д. и т. д.?" "Как я могу реализовать инкапсуляцию в лучшем виде?"

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

Микро
источник
Это должно быть первым шагом, прежде чем будет произведен какой-либо рефакторинг. Пока код не будет понят достаточно для отображения (uml или какая-либо другая карта джокеров), рефакторинг будет работать в темноте.
Kzqai
3

Самый первый шаг, независимо от того, куда это идет, должен разбить очевидно большой метод A::ExecJobна более мелкие части.

Поэтому вместо

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

Вы получаете

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

С этого момента, есть много возможных путей. Мое предположение: сделайте A базовым классом вашего класса hierachy и виртуальным ExecJob, и вам будет легко создавать B, C, ... без лишних копий-вставок - просто замените ExecJob (теперь пятистрочный) на измененный версия.

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

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

user281377
источник
2

Я согласен с другими ответами о том, что ваш подход идет в правильном направлении, хотя я не думаю, что наследование является лучшим способом реализации общего кода - я предпочитаю композицию. Из часто задаваемых вопросов C ++, которые могут объяснить это намного лучше, чем я когда-либо мог: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html

Муравей
источник
1

Во-первых, вы должны убедиться, что наследование действительно является подходящим инструментом для работы - только потому, что вам нужно общее место для функций, используемых вашими классами, Aчтобы Fэто не означало, что общий базовый класс здесь правильный - иногда отдельный помощник класс делает работу лучше. Это может быть, это не так. Это зависит от наличия отношения «есть-а» между A и F и вашим общим базовым классом, что невозможно сказать по искусственным именам AF. Здесь вы найдете сообщение в блоге, посвященное этой теме.

Предположим, вы решили, что общий базовый класс в вашем случае является правильным. Тогда вторая вещь , которую я хотел бы сделать , это убедиться , что ваши фрагменты кода S1-S5 каждыми реализованы в отдельных методах S1()для S5()вашего базового класса. После этого функции «ExecJob» должны выглядеть так:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

Как вы видите, поскольку S1-S5 - это просто вызовы методов, код больше не блокируется, дублирование кода почти полностью удалено, и вам больше не нужна проверка параметров, что позволяет избежать проблемы увеличения сложности, которую вы можете получить. в противном случае.

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

Но ИМХО, основной метод разбивки больших методов на маленькие - гораздо, гораздо важнее избежать дублирования кода, чем применять шаблоны.

Док Браун
источник