增加复杂性以删除重复的代码


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);

儿童A级:

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和代码重用等概念的过度微观应用会导致更大的罪恶。
2012年

1
您会得到一些很好的一般答案。编辑以包含示例函数可能会帮助我们确定您是否在特定情况下超出了示例范围。
Karl Bielefeldt '02

这并不是一个真正的答案,更多的是观察:如果您不能轻松地解释分解后的基类的作用,那么最好不要一个。另一种看待它的方式是(我假设您熟悉SOLID?)“此功能的任何可能的使用者是否需要Liskov替代”?如果插值功能的一般使用者没有可能的业务案例,则基类没有任何价值。
汤姆W

1
第一件事是将三元组X,Y,Z收集为Position类型,并将插值添加为该类型的成员或静态方法:Position interpolate(Position other,ratio)。
凯文·克莱恩

Answers:


30

在某种程度上,您用上一段中的评论回答了自己的问题:

我开始认为DRY原则在这种情况下不太适用,但这听起来像是亵渎神灵

每当您发现某些解决问题的实践都不是真正实用的方法时,请不要虔诚地使用该实践(亵渎词是对此的一种警告)。大多数实践都有其时机原因,即使它们涵盖了所有可能情况的99%,但仍有1%的情况您可能需要其他方法。

具体地说,关于DRY,我还发现,有时甚至有几份重复但简单的代码要比一个巨大的怪兽实际上更好,后者会使您在看时感到不适。

话虽如此,这些边缘情况的存在不应该用作草率复制粘贴代码或完全缺乏可重用模块的借口。简而言之,如果您不知道如何以某种语言为某个问题编写通用代码和可读代码,那么拥有一些冗余可能就不那么糟糕了。想想谁必须维护代码。他们会更容易受到冗余或混淆的困扰吗?

有关您的特定示例的更具体建议。您说这些计算是相似的,略有不同。您可能想尝试将计算公式分解为较小的子公式,然后让所有稍有不同的计算调用这些帮助函数来进行子计算。您可以避免每次计算都依赖于某些过分泛化的代码,并且仍然具有一定程度的重用性的情况。


10
关于相似但略有不同的另一点是,即使它们在代码上看起来相似,也并不意味着它们在“业务”上必须相似。当然要视情况而定,但是有时最好将它们分开,因为即使它们看起来相同,它们也可能基于不同的业务决策/要求。因此,您可能希望将它们视为完全不同的计算,即使它们在代码方面可能看起来相似。(不是规则或任何内容,只是在决定是否应合并或重构内容时要牢记的一点:)
Svish 2012年

@Svish有趣的一点。从来没有那样想过。
Phil

17

子类使用不同的类型,每个类的计算差异很小。

首先,最重要的是:首先要清楚地确定各个类之间哪些部分正在更改,哪些部分未更改。确定后,您的问题就解决了。

在开始重构之前,请先做该练习。此后,其他所有内容将自动放置在适当的位置。


2
说得好。这可能只是重复功能太大的问题。
Karl Bielefeldt '02

8

我相信,几乎可以用某种方式排除重复多于几行代码的所有重复,而且几乎总是如此。

但是,某些语言的重构比其他语言更容易。在LISP,Ruby,Python,Groovy,Javascript,Lua等语言中,这非常容易。通常在使用模板的C ++中不太困难。在C中更痛苦,因为C中唯一的工具可能是预处理器宏。在Java中通常很痛苦,有时甚至根本不可能,例如,尝试编写通用代码来处理多个内置数字类型。

在更具表现力的语言中,毫无疑问:只需重构两行代码即可。对于表达能力较低的语言,您必须在重构的痛苦与重复代码的长度和稳定性之间取得平衡。如果重复的代码很长,或者可能经常更改,那么即使生成的代码有些难以阅读,我也倾向于重构。

仅当重复代码简短,稳定且重构过于难看时,我才会接受重复的代码。基本上,除非编写Java,否则几乎排除了所有重复项。

由于您尚未发布代码,甚至未指明使用的语言,因此无法针对您的情况给出具体建议。


Lua不是首字母缩写词。
DeadMG

@DeadMG:已注意到;随时进行编辑。这就是为什么我们给您如此声誉的原因。
凯文·克莱恩

5

当您说基类必须执行一个算法,但是每个子类的算法都不同时,这听起来像是Template Pattern的理想候选者。

这样,基类将执行该算法,并且当每个子类的变体出现时,它会遵循一种抽象方法,这是子类的责任。考虑一下方式,例如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

在我看来,从某种意义上说,DRY可以被带走太远了,这是正确的。如果两个相似的代码段可能朝着非常不同的方向发展,那么您可能会因尝试不一开始重复自己而导致问题。

但是,您也应该警惕这种亵渎性的想法。在决定不做任何事情之前,请尽力思考一下您的选择。

例如,将重复的代码放在实用程序类/方法中而不是在基类中会更好吗?请参阅优先于继承而不是继承


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方法组成的情况下,此类参数非常有用。如果应该假定外部方法在发生故障时引发异常,则任何失败的内部方法调用都应引发外部方法可以传播的异常。如果外部方法不应该引发异常,那么内部方法也不会。如果使用参数来区分try / do,然后外部方法可以将其传递给内部方法。否则,对于外部方法,当它应该表现为“ try”时,有必要调用“ try”方法,而当它应该表现为“ do”时,则需要“ do”方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.