Добавление сложности для удаления дублирующего кода


24

У меня есть несколько классов, которые все наследуются от общего базового класса. Базовый класс содержит коллекцию из нескольких объектов типа T.

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

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

Я начинаю думать, что принцип СУХОЙ не применяется в такой ситуации, но это звучит как богохульство. Насколько сложна слишком большая попытка удалить дублирование кода?

РЕДАКТИРОВАТЬ:

Лучшее решение, которое я могу придумать, выглядит примерно так:

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

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 и повторное использование кода, приводит к гораздо более серьезным грехам
Affe

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

Это не совсем ответ, скорее наблюдение: Если вы не можете легко объяснить , что факторизованный-аут базового класс делает , это может быть лучше не иметь. Другой способ взглянуть на это (я полагаю, вы знакомы с SOLID?) «Требует ли какой-либо вероятный потребитель этой функциональности подстановка Лискова»? Если для обобщенного потребителя функциональных возможностей интерполяции нет вероятного экономического обоснования, базовый класс не имеет значения.
Том

1
Прежде всего, нужно собрать триплет X, Y, Z в тип Position и добавить интерполяцию к этому типу в качестве члена или, возможно, статического метода: Position interpolate (Position other, ratio).
Кевин Клайн

Ответы:


30

В некотором смысле, вы ответили на свой вопрос с этим замечанием в последнем абзаце:

Я начинаю думать, что принцип СУХОЙ не применяется в такой ситуации, но это звучит как богохульство .

Всякий раз, когда вы находите какую-то практику не очень практичной для решения вашей проблемы, не пытайтесь использовать эту практику религиозно (слово богохульство является своего рода предупреждением для этого). У большинства практик есть свои « когда» и « почему», и даже если они охватывают 99% всех возможных случаев, все еще есть тот 1%, где вам может понадобиться другой подход.

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

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

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


10
Еще один момент, связанный с похожим, но немного отличающимся, состоит в том, что, хотя они выглядят похожими в коде, это не значит, что они должны быть похожими в «бизнесе». Зависит от того, что это, конечно, но иногда полезно держать вещи отдельно, потому что, хотя они выглядят одинаково, они могут основываться на разных бизнес-решениях / требованиях. Таким образом, вы можете захотеть рассматривать их как совершенно разные вычисления, даже если их код будет выглядеть похожим. (Не правило или что-то в этом роде, а просто что-то, о чем следует помнить при принятии решения о том, следует ли что-то комбинировать или реорганизовывать :)
Свиш,

@Svish Интересный момент. Никогда не думал об этом таким образом.
Фил

17

Дочерние классы используют разные типы, расчеты немного варьируются от класса к классу.

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

Сделайте это в качестве первого упражнения перед началом повторного факторинга. Все остальное автоматически станет на свои места после этого.


2
Хорошо сказано. Это может быть просто проблемой слишком большой повторной функции.
Карл Билефельдт

8

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

Однако в некоторых языках этот рефакторинг проще, чем в других. Это довольно просто в таких языках, как LISP, Ruby, Python, Groovy, Javascript, Lua и т. Д. Обычно не так уж сложно в 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

СУХОЕ - это руководство, которому нужно следовать, а не нерушимое правило. В какой-то момент вам нужно решить, что не стоит иметь X уровней наследования и Y шаблонов в каждом используемом вами классе, просто чтобы сказать, что в этом коде нет повторений. Пара хороших вопросов, которые нужно задать, - это то, что мне понадобится больше времени, чтобы извлечь эти схожие методы и реализовать их как один, а затем проанализировать все из них, если возникнет необходимость в изменениях, или есть ли у них потенциал для изменения, которое могло бы произойти. отменить мою работу по извлечению этих методов? Я нахожусь в точке, где дополнительная абстракция начинает понимать, где или что делает этот код, является проблемой?

Если вы можете ответить «да» на любой из этих вопросов, у вас есть веские основания оставить потенциально дублированный код


0

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

В этом случае рефакторинг алгоритмов в один гигантский сделает его слишком сложным, что сделает его слишком сложным для будущего обслуживания. Так что, если вы не можете разумно выделить общие вещи, просто:

// this code is similar to class x function b

Комментарий будет достаточным. Проблема решена.


0

При принятии решения о том, лучше ли иметь один метод большего размера или два метода меньшего размера с перекрывающейся функциональностью, первый вопрос стоимостью 50 000 долларов США заключается в том, может ли изменяющаяся часть поведения измениться, и следует ли какое-либо изменение применять к меньшим методам одинаково. Если ответ на первый вопрос - «да», а ответ на второй - «нет», то методы должны оставаться отдельными . Если ответ на оба вопроса - «да», то необходимо что-то предпринять, чтобы обеспечить синхронизацию каждой версии кода; во многих случаях самый простой способ сделать это - иметь только одну версию.

Есть несколько мест, где Microsoft, похоже, идет вразрез с принципами DRY. Например, Microsoft явно не рекомендует, чтобы методы принимали параметр, который указывал бы, должен ли сбой вызвать исключение. Хотя это правда, что параметр «failures throw исключений» является уродливым в API «общего использования» метода, такие параметры могут быть очень полезны в тех случаях, когда метод Try / Do должен состоять из других методов Try / Do. Если внешний метод должен генерировать исключение при возникновении сбоя, то любой внутренний вызов метода, который завершается неудачей, должен генерировать исключение, которое внешний метод может распространять. Если внешний метод не должен вызывать исключение, то и внутренний тоже не будет. Если параметр используется для различения между попыткой / выполнением, тогда внешний метод может передать его внутреннему методу. В противном случае необходимо, чтобы внешний метод вызывал методы «try», когда он должен вести себя как «try», и методы «do», когда он должен вести себя как «do».

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.