Як рефакторний код до якогось загального коду?


16

Фон

Я працюю над поточним проектом C #. Я не програміст на C #, передусім програміст на C ++. Тож мені було призначено в основному легкі та рефакторинг завдання.

Код - безлад. Це величезний проект. Оскільки наш клієнт вимагав частого випуску з новими функціями та виправленнями помилок, усі інші розробники були змушені застосовувати грубу силу під час кодування. Код дуже незрозумілий, і всі інші розробники погоджуються з ним.

Я не для того, щоб обговорювати, чи правильно вони це зробили. Коли я рефакторинг, мені цікаво, чи роблю це правильно, оскільки мій код, що ремонтується, здається складним! Ось моє завдання як простий приклад.

Проблема

Є шість класів: A, B, C, D, Eі F. Усі класи мають функцію ExecJob(). Усі шість реалізацій дуже схожі. В основному, спочатку A::ExecJob()було написано. Потім була потрібна дещо інша версія, яка була реалізована в B::ExecJob()модифікації copy-paste 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()є вже складним. З часом воно стане складнішим.

Чи я це роблю правильно?


У книзі " Рефакторинг" Мартіна Фаулера є багато специфічних прийомів коду рефакторингу, які можуть вам бути корисними.
Аллан

Відповіді:


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 або набагато кращі підходи до рефакторингу. Однак цей дозволить вам усунути повторюваний код та переконатися, що зміни локалізовані.


Основна проблема, яку я бачу з рішення, викладеного в "Крок 2", полягає в тому, що конкретна реалізація S5 існує двічі.
користувач281377

1
Так, дублювання коду не усувається! І це ще один показник того, що абстракція не працює. Я просто хотів поставити крок 2 там, щоб показати, як я думаю про процес; покроковий підхід до пошуку чогось кращого.
Гувен

1
+1 Дуже гарна стратегія (і я не кажу про схему )!
Йордао

7

Ви насправді робите правильно. Я говорю це тому, що:

  1. Якщо вам потрібно змінити код для загальної функціональності завдання, вам не потрібно змінювати його в усіх 6 класах, які містили б код, якщо ви не записуєте його в загальний клас.
  2. Кількість рядків коду буде зменшено.

3

Ви бачите такий тип коду, який багато використовується в дизайні, керованому подіями (особливо .NET). Найдоцільніший спосіб - тримати вашу спільну поведінку якомога дрібніше.

Нехай код високого рівня повторно використовує купу дрібних методів, залишає код високого рівня поза спільною базою.

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

У коді високого рівня ви побачите безліч візерунків. Іноді вони справжні, в більшості випадків вони не є. «Конфігурація» з п'яти параметрів там виглядає схожою, але вони не є. Це три абсолютно різні стратегії.

Також хочу зазначити, що ви можете робити все це композицією і ніколи не турбуватися про спадщину. У вас буде менше зв’язок.


3

Якби я був ти, я, мабуть, додати ще один крок на початку: дослідження на основі 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
}

З цього моменту існує багато можливих шляхів. Моє взяти на себе: Зробіть базовий клас ієрахії вашого класу та віртуальний ExecJob, і створити B, C, ... без занадто багато копіювання-вставки стає просто - просто замініть ExecJob (тепер п'ятилінійний) модифікованим версія.

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

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


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 в один із базового класу, де виконання цих частин може контролюватися параметрами, точно так, як ви запропонували, або використовуючи шаблон методу шаблон. Ви повинні вирішити, чи варто ваших зусиль у вашому випадку, виходячи з реального коду.

Але основна методика розбиття великих методів на дрібні методи IMHO - це набагато важливіше для уникнення дублювання коду, ніж застосування шаблонів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.