Додавання складності для видалення дубліката коду


24

У мене є кілька класів, які успадковують усі із загального базового класу. Базовий клас містить колекцію декількох об'єктів типу T.

Кожен дочірній клас повинен мати можливість обчислювати інтерпольовані значення з колекції об'єктів, але оскільки дочірні класи використовують різні типи, обчислення змінюються незначно біт від класу до класу.

Поки я копіював / вставляв свій код з класу в клас і вносив незначні зміни до кожного. Але зараз я намагаюся видалити дублюваний код і замінити його одним загальним методом інтерполяції в моєму базовому класі. Однак це виявляється дуже складно, і всі рішення, які я вважав, здаються занадто складними.

Я починаю вважати, що принцип DRY не застосовується настільки в подібній ситуації, але це звучить як богохульство. Наскільки складність занадто велика при спробі видалити дублювання коду?

Редагувати:

Найкраще рішення, яке я можу придумати, виглядає приблизно так:

Базовий клас:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

Дитячий клас А:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

Дитячий клас B:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
Надмірно мікро-застосування таких понять, як DRY та Code Reuse, призводить до набагато більших гріхів.
Affe

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

Це насправді не відповідь, скоріше спостереження: Якщо ви не можете легко пояснити, що робить базовий клас , що має фактичний вигляд , можливо, найкраще його не мати. Інший спосіб поглянути на це (я припускаю, що ви знайомі з SOLID?) "Чи потрібен будь-який імовірний споживач цієї функціональності заміну Ліскова?" Якщо для узагальненого споживача інтерполяційної функціональності немає ймовірного ділового випадку, базовий клас не має значення.
Том Ш

1
Перше - зібрати триплет X, Y, Z у тип позиції та додати інтерполяцію до цього типу як член або, можливо, статичний метод: Інтерполяція позиції (Позиція інше, відношення).
Кевін Клайн

Відповіді:


30

Якимось чином ви відповіли на власне запитання тим зауваженням в останньому абзаці:

Я починаю вважати, що принцип DRY не застосовується настільки в подібній ситуації, але це звучить як богохульство .

Кожен раз, коли ви знайдете практику, не дуже практичну для вирішення своєї проблеми, не намагайтеся використовувати цю практику релігійно (слово богохульство є своєрідним попередженням для цього). Більшість методів має свої Whens і Whys і навіть якщо вони покривають 99% всіх можливих випадків, є ще , що на 1% , де вам може знадобитися інший підхід.

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

Незважаючи на це, існування цих крайових справ не повинно використовуватися як привід для неохайного кодування копіювання та вставки або повної відсутності модулів для багаторазового використання. Просто, якщо ви не знаєте, як написати як загальний, так і читабельний код для якоїсь проблеми на якійсь мові, то, мабуть, менш погано мати надмірність. Подумайте, хто має підтримувати код. Чи легше вони житимуть із надмірністю або затуманенням?

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


10
Ще один момент щодо подібного, але трохи іншого відрізняється тим, що, хоча вони схожі за кодом, не означає, що вони повинні бути схожими в "бізнесі". Залежно від того, що це, звичайно, але іноді корисно тримати речі окремо, оскільки, хоча вони виглядають однаково, вони можуть базуватися на різних ділових рішеннях / вимогах. Отже, ви можете розглянути на них як зовсім інші обчислення, навіть якщо вони з кодом можуть бути схожими. (Не правило чи що-небудь, а лише щось, про що слід пам’ятати, вирішуючи, чи варто поєднувати чи ремонтувати речі :)
Свиш

@Svish Цікавий момент. Ніколи про це не думав.
Філ

17

У дитячих класах використовуються різні типи, обчислення варіюється крихітно від класу до класу.

Перше і головне: спочатку чітко визначте, яка частина змінюється, а яка НЕ ​​змінюється між класами. Після того, як ви визначили це, ваша проблема вирішується.

Зробіть це як першу вправу перед початком повторного факторингу. Все інше автоматично встане на своє місце.


2
Добре кажучи. Це може бути просто проблемою, коли повторювана функція занадто велика.
Карл Білефельдт

8

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

Однак таке рефакторинг у деяких мовах простіше, ніж на інших. Це досить просто в таких мовах, як LISP, Ruby, Python, Groovy, Javascript, Lua тощо. Зазвичай не дуже складно в C ++ за допомогою шаблонів. Більш болісно в C, де єдиним інструментом можуть бути препроцесорні макроси. Часто болісно на Java, а іноді просто неможливо, наприклад, намагаючись написати загальний код для обробки декількох вбудованих числових типів.

У більш виразних мовах не виникає сумнівів: переробляйте щось більше, ніж пару рядків коду. За допомогою менш виразних мов вам доводиться врівноважувати біль від рефакторингу проти тривалості та стабільності повторного коду. Якщо повторний код довгий або може змінюватися часто, я схильний до рефакторації, навіть якщо отриманий код читати дещо складно.

Я прийму повторний код лише в тому випадку, якщо він короткий, стабільний, а рефакторинг - занадто некрасивий. В основному я визначаю майже всі дублювання, якщо я не пишу Java.

Конкретну рекомендацію щодо вашої справи дати неможливо, оскільки ви не опублікували код або навіть вказали, якою мовою ви користуєтесь.


Луа - це не абревіатура.
DeadMG

@DeadMG: відмічено; не соромтеся редагувати. Ось чому ми дали вам всю цю репутацію.
Кевін Клайн

5

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

При цьому базовий клас виконує алгоритм, і коли мова йде про варіацію для кожного підкласу, він визначає абстрактний метод, який несе відповідальність за підклас. Подумайте про спосіб, і сторінка ASP.NET відкладає ваш код для реалізації, наприклад, Page_Load.


4

Звучить, як ваш "один загальний метод інтерполяції" в кожному класі робиться занадто багато, і його слід перетворити на більш дрібні методи.

Залежно від того, наскільки складний ваш розрахунок, чому не могла кожна "частина" обчислення бути таким віртуальним методом

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

І переосмислити окремі частини, коли вам потрібно внести «незначну варіацію» в логіку розрахунку.

(Це дуже крихітний і марний приклад того, як ви могли додати багато функціональності, перекриваючи невеликі методи)


3

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

Однак ви також маєте право бути обережними щодо таких богохульних думок. Постарайтеся дуже важко продумати свої варіанти, перш ніж ви вирішите залишити це в спокої.

Чи було б, наприклад, краще помістити цей повторний код у клас / метод корисності, а не в базовий клас? Див. Переважний склад над успадкуванням .


2

DRY - це настанова, яку слід дотримуватися, а не непорушне правило. У якийсь момент вам потрібно вирішити, що не варто мати рівні X спадкування та шаблони Y у кожному класі, який ви використовуєте, просто сказати, що в цьому коді немає повторення. Пару хороших запитань було б, чи знадобиться мені більше часу, щоб витягнути ці подібні методи та реалізувати їх як єдине, тоді він би шукав усі вони, якщо виникне необхідність зміни або є їх потенціал до змін, які б скасувати в першу чергу мою роботу з вилучення цих методів? Чи я в тій точці, коли додаткова абстракція починає розуміти, де чи що цей код є проблемою?

Якщо ви можете відповісти "так" на будь-яке з цих питань, тоді у вас є вагомий випадок залишити потенційно дублюваний код


0

Ви мусите задати собі запитання: "навіщо мені це рефактор"? У вашому випадку, коли у вас є "подібний, але інший" код, якщо ви вносите зміни в один алгоритм, вам потрібно переконатися, що ви також відображаєте цю зміну в інших місцях. Зазвичай це рецепт катастрофи, незмінно хтось пропустить одне місце і введе іншу помилку.

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

// this code is similar to class x function b

Коментувати буде досить. Проблема вирішена.


0

Вирішуючи, чи краще мати один більший метод або два менші методи з функціоналом, що перекриваються, першим питанням на суму 50 000 доларів є те, чи може змінюватися частина поведінки, що перекривається, і чи слід однаково застосовувати будь-які зміни до менших методів. Якщо відповідь на перше питання - так, але відповідь на друге питання - ні, то методи повинні залишатися окремими . Якщо відповідь на обидва запитання - «так», необхідно зробити щось, щоб забезпечити синхронізацію кожної версії коду; у багатьох випадках найпростіший спосіб - це мати лише одну версію.

Є кілька місць, де Microsoft, схоже, пішла проти принципів DRY. Наприклад, Microsoft явно не рекомендує методам приймати параметр, який би вказував на те, чи невдача має спричинити виняток. Хоча це правда, що параметр "відкидання винятків" є некрасивим у API загального використання методу, такі параметри можуть бути дуже корисними у випадках, коли метод Try / Do має складатися з інших методів Try / Do. Якщо зовнішній метод повинен викидати виняток, коли відбувається помилка, то будь-який внутрішній виклик методу, який не вдається, повинен викинути виняток, який зовнішній метод може дозволити поширюватися. Якщо зовнішній метод не повинен викидати виняток, то внутрішній також не є. Якщо параметр використовується для розмежування спроби / зробити, тоді зовнішній метод може передати його внутрішньому методу. В іншому випадку для зовнішнього методу потрібно буде викликати "спробувати" методи, коли він повинен вести себе як "спробувати", а "зробити" методи, коли він повинен поводитись як "робити".

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