Фон
Я работаю над текущим проектом 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()
это уже сложно. Со временем это станет более сложным.
Я делаю это правильно?
Ответы:
Да, вы находитесь на правильном пути!
По своему опыту я заметил, что когда все сложно, изменения происходят небольшими шагами. Что вы сделали, это шаг 1 в процессе эволюции (или процесс рефакторинга). Вот шаг 2 и шаг 3:
Шаг 2
Это «Шаблон дизайна шаблона», и он на шаг впереди в процессе рефакторинга. Если базовый класс изменяется, подклассы (A, B, C) не должны быть затронуты. Вы можете добавить новые подклассы относительно легко. Однако сразу же на картинке выше вы можете видеть, что абстракция нарушена. Необходимость «пустой реализации» является хорошим индикатором; это показывает, что с вашей абстракцией что-то не так. Возможно, это было бы приемлемым решением на короткий срок, но, кажется, есть лучшее.
Шаг 3
Это «шаблон разработки стратегии», который подходит для вашего случая. Существуют разные стратегии выполнения задания, и каждый класс (A, B, C) реализует его по-своему.
Я уверен, что в этом процессе есть шаг 4 или шаг 5 или гораздо более эффективные методы рефакторинга. Однако этот позволит вам устранить дубликаты кода и убедиться, что изменения локализованы.
источник
Вы на самом деле делаете правильные вещи. Я говорю это потому что:
источник
Вы видите, что подобный код разделяет многое с дизайном, управляемым событиями (особенно .NET). Наиболее приемлемый способ - сохранить общее поведение как можно более мелкими.
Позвольте высокоуровневому коду повторно использовать кучу небольших методов, оставьте высокоуровневый код за пределами общей базы.
У вас будет много котельной плиты в ваших листовых / бетонных реализациях. Не паникуйте, все в порядке. Весь этот код прост, понятен. Вам придется иногда переставлять его, когда что-то ломается, но это будет легко изменить.
Вы увидите много шаблонов в коде высокого уровня. Иногда они реальны, в большинстве случаев это не так. «Конфигурация» из пяти параметров там выглядит похожей, но они не являются. Это три совершенно разные стратегии.
Также хочу отметить, что вы можете делать все это с композицией и никогда не беспокоиться о наследовании. У вас будет меньше связи.
источник
Если бы я был вами, я, вероятно, добавлю еще 1 шаг в начале: исследование на основе UML.
Рефакторинг кода, объединяющий все общие части, не всегда лучший шаг, звучит скорее как временное решение, чем как хороший подход.
Рисует схему UML, придерживайтесь простых, но эффективных вещей, помните о некоторых основных понятиях вашего проекта, таких как «что должно делать это программное обеспечение?» "Каков наилучший способ сохранить эту часть программного обеспечения абстрактной, модульной, расширяемой и т. д. и т. д.?" "Как я могу реализовать инкапсуляцию в лучшем виде?"
Я просто говорю это: не заботьтесь о коде прямо сейчас, вам нужно заботиться только о логике, когда у вас есть ясная логика, все остальное может стать действительно легкой задачей, в конце концов, все это проблем, с которыми вы сталкиваетесь, просто вызвано плохой логикой.
источник
Самый первый шаг, независимо от того, куда это идет, должен разбить очевидно большой метод
A::ExecJob
на более мелкие части.Поэтому вместо
Вы получаете
С этого момента, есть много возможных путей. Мое предположение: сделайте A базовым классом вашего класса hierachy и виртуальным ExecJob, и вам будет легко создавать B, C, ... без лишних копий-вставок - просто замените ExecJob (теперь пятистрочный) на измененный версия.
Но почему так много уроков? Может быть, вы можете заменить их всех одним классом, у которого есть конструктор, который может сказать, какие действия необходимы в
ExecJob
.источник
Я согласен с другими ответами о том, что ваш подход идет в правильном направлении, хотя я не думаю, что наследование является лучшим способом реализации общего кода - я предпочитаю композицию. Из часто задаваемых вопросов C ++, которые могут объяснить это намного лучше, чем я когда-либо мог: http://www.parashift.com/c++-faq/priv-inherit-vs-compos.html
источник
Во-первых, вы должны убедиться, что наследование действительно является подходящим инструментом для работы - только потому, что вам нужно общее место для функций, используемых вашими классами,
A
чтобыF
это не означало, что общий базовый класс здесь правильный - иногда отдельный помощник класс делает работу лучше. Это может быть, это не так. Это зависит от наличия отношения «есть-а» между A и F и вашим общим базовым классом, что невозможно сказать по искусственным именам AF. Здесь вы найдете сообщение в блоге, посвященное этой теме.Предположим, вы решили, что общий базовый класс в вашем случае является правильным. Тогда вторая вещь , которую я хотел бы сделать , это убедиться , что ваши фрагменты кода S1-S5 каждыми реализованы в отдельных методах
S1()
дляS5()
вашего базового класса. После этого функции «ExecJob» должны выглядеть так:Как вы видите, поскольку S1-S5 - это просто вызовы методов, код больше не блокируется, дублирование кода почти полностью удалено, и вам больше не нужна проверка параметров, что позволяет избежать проблемы увеличения сложности, которую вы можете получить. в противном случае.
Наконец, но только в качестве третьего шага (!), Вы можете подумать о том, чтобы объединить все эти методы ExecJob в один из вашего базового класса, где выполнение этих частей может контролироваться параметрами, так, как вы это предложили, или с помощью шаблон метода шаблона. Вы должны решить сами, стоит ли это усилий в вашем случае, основываясь на реальном коде.
Но ИМХО, основной метод разбивки больших методов на маленькие - гораздо, гораздо важнее избежать дублирования кода, чем применять шаблоны.
источник