DRY 没有单一代码?

发布于 2024-09-28 23:37:00 字数 3439 浏览 3 评论 0原文

我不想重复自己(DRY),但我不能有一段代码。例如,这里的代码重复了 3 次,并且有相同的错误:

class StarWars : Movie
{
   //Calculate "base ^ exponent"
   public float Power(float base, float exponent)
   {
      return (base * exponent);
   }
}

class Customer: Object
{
   //Calculate "base ^ exponent"
   public float Exponential(float base, float exponent)
   {
      return (base ^ exponent);
   }
}

class Student: Person
{
   //Calculate "base ^ exponent"
   public float CalculateExpoential(float base, float exponent)
   {
      return CalculateExponential(2.7182818, exponent * Ln(base));
   }
}

现在理想情况下,我会将这个通用函数提取到它自己的帮助程序中:

class LibraryOfHelperCode
{
    public static float Exponentiation(float base, float exponent)
    {
       return Exp(2.71828183, base * Ln(exponent));
    }
}

并将现有代码转换为使用它:

class StarWars : Movie
{
   //Calculate "base ^ exponent"
   public float Power(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

class Customer: Object
{
   //Calculate "base ^ exponent"
   public float Exponential(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

class Student: Person
{
   //Calculate "base ^ exponent"
   public float CalculateExpoential(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

值是,现在我已经从其中提取了重复的代码

  • 将幂
  • 指数
  • CalculateExpoential

转换为单个函数。这意味着如果存在任何错误,只需修复一次。在这种情况下这是好事,因为有一个错误:

   public float CalculateExpoential(float base, float exponent)
   {
      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

几年后:

   public float CalculateExpoential(float base, float exponent)
   { 
      //19990321: Oops, need to handle when exponent is zero
      if (exponent == 0)
         return 1.0;

      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

然后:

   public float CalculateExpoential(float base, float exponent)
   { 
      //19990321: Oops, need to handle when exponent is zero
      if (exponent == 0)
         return 1.0;

      //20040523: Another special case
      if (Base = 0.0) && (Exponent > 0.0) then
         return 0.0; // 0**n = 0, n > 0

      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

最后:

   public float CalculateExpoential(float base, float exponent)
   { 
      //20101027: Microsoft just release a method in .NET framework 4.0 that does
      //what we need. Use it:
      return Math.Pow(base, exponent);
   }

每个人都得到了修复。另一方面,我不能保证任何一个增量修复都不会破坏现有代码。

想象一个人正在打电话:

char ps = Math.Trunc(Exponential(ProblemSize, ProblemComplexity));

并且从未预料到该值会大于 128。他错了。虽然代码一直错误:但它碰巧可以工作。

现在我来修复问题,突然代码由于溢出和/或环绕而崩溃。


我今天面临的问题是 DRY 通用代码的更改会影响使用它的所有地方。唯一可接受的(政治上的)解决方案是为使用它的每个可执行文件/模块/命名空间/类保留库类的副本。

消除任何干燥感。

有什么办法摆脱这个混乱吗?当我不能重复自己,但继续获得修复和改进,因为它们被添加到单个 DRY 代码中时?


我的意思是...我应该共享代码,但在每个版本中对其进行分支吗?但政治上的问题是没有人希望代码全部被反向集成。

i want to not repeat myself (DRY), but i cannot have a single piece of code. For example here is code repeated 3 times with the same bug:

class StarWars : Movie
{
   //Calculate "base ^ exponent"
   public float Power(float base, float exponent)
   {
      return (base * exponent);
   }
}

class Customer: Object
{
   //Calculate "base ^ exponent"
   public float Exponential(float base, float exponent)
   {
      return (base ^ exponent);
   }
}

class Student: Person
{
   //Calculate "base ^ exponent"
   public float CalculateExpoential(float base, float exponent)
   {
      return CalculateExponential(2.7182818, exponent * Ln(base));
   }
}

Now ideally i would have extracted this common function into it's own helper somewhere:

class LibraryOfHelperCode
{
    public static float Exponentiation(float base, float exponent)
    {
       return Exp(2.71828183, base * Ln(exponent));
    }
}

And converted the existing code to use it:

class StarWars : Movie
{
   //Calculate "base ^ exponent"
   public float Power(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

class Customer: Object
{
   //Calculate "base ^ exponent"
   public float Exponential(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

class Student: Person
{
   //Calculate "base ^ exponent"
   public float CalculateExpoential(float base, float exponent)
   {
      return LibraryOfHelperCode.Exponentiation(base, exponent);
   }
}

The value is that now i've extracted the repeated code from

  • Power
  • Exponential
  • CalculateExpoential

into a single function. This means that if there are any bugs, they only have to be fixed once. Which is good in this case, because there is a bug:

   public float CalculateExpoential(float base, float exponent)
   {
      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

and a few years after that:

   public float CalculateExpoential(float base, float exponent)
   { 
      //19990321: Oops, need to handle when exponent is zero
      if (exponent == 0)
         return 1.0;

      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

and later on:

   public float CalculateExpoential(float base, float exponent)
   { 
      //19990321: Oops, need to handle when exponent is zero
      if (exponent == 0)
         return 1.0;

      //20040523: Another special case
      if (Base = 0.0) && (Exponent > 0.0) then
         return 0.0; // 0**n = 0, n > 0

      //19971012: Oops, should be natrual log of base, not exponent
      return Exp(2.71828183, exponent * Ln(base));    
   }

and finally:

   public float CalculateExpoential(float base, float exponent)
   { 
      //20101027: Microsoft just release a method in .NET framework 4.0 that does
      //what we need. Use it:
      return Math.Pow(base, exponent);
   }

And everyone gets the fixes. On the other hand, i cannot guarantee that any one of those increment fixes won't break existing code.

Imagine a guy was calling:

char ps = Math.Trunc(Exponential(ProblemSize, ProblemComplexity));

and was never expecting the value to be larger than 128. He was wrong. And while the code was wrong all this time: it happened to work.

Now i come along and fix things, and suddenly code is crashing due to overflow and/or wraparound.


The problem i'm facing today is that a change in DRY common code affects everywhere it's used. The only acceptable (polotical) solution is to keep a copy of the library class for every executable/moduble/namespace/class that uses it.

Undoing any DRYness.

Is there any way out of this mess? When i can not repeat myself, but continue to get fixes and improvements as they are added to the single DRY code?


i mean...should i have shared code, but branch it at every release? But the issue that is polotically nobody wants the code every to be reverse-integrated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

柠栀 2024-10-05 23:37:00

仅当您更改库的界面时,您的程序才会中断。如果更改库的实现会破坏您的程序,则您可能将程序与库的绑定过于牢固。该程序不应依赖于图书馆的内部运作。
如果您不断更改库的界面并破坏您的项目,则可能需要花费更多时间来设计库。

您还应该对您的库使用版本控制。针对库的特定分支/版本构建代码。如果库接口发生较大变化,并且您不想更新现有项目,请为新接口创建一个新分支,新项目可以使用该分支,而旧项目可以继续使用旧分支。可以针对一个分支编写错误修复并合并到另一分支中。

Git 特别擅长这类事情。使用子模块将您的项目链接到库的特定提交。

Your programs should only break if you change your library's interface. If changing the implementation of the library breaks your program, you're probably binding the program to the library too strongly. The program shouldn't depend on the library's internal workings.
If you're constantly changing your library's interface and breaking your projects, you probably need to spend more time designing your library.

You should also use version control for your libraries. Build your code against a specific branch/version of the library. If the libraries interface changes significantly and you don't want to update the existing projects, make a new branch for the new interface, which new projects can use, while old projects can continue using the old branch. Bug fixes can be written against one branch and merged into the other.

Git is particularly good at this sort of thing. Use submodules to link your project to a specific commit of a library.

太傻旳人生 2024-10-05 23:37:00

我认为在每个使用此功能的类中都有一个辅助类并不会真正消除干燥性。即使您确实在各处重复包含/声明帮助器,但这也使您不必重复任何进一步的功能。

您所做的任何架构更改(例如继承或助手)都将不得不影响以某种方式使用它们的所有内容,如果继承没有意义,那么与某种 Date 对象或 Helper 组合可能是一个不错的选择要走的路。

I don't think that having a helper class in each of your classes that use this functionality really undoes the DRY-ness. Even though you do repeat the inclusion/declaration of the helper everywhere, that then gives you the power to not have to repeat any of the further functionality.

Any architectural changes you make (such as either inheritance or a helper) are going to have to affect everything that uses them in some way, and if inheritance doesn't make sense, composition with some kind of Date object or Helper is probably a good way to go.

埋情葬爱 2024-10-05 23:37:00

唯一可以接受的(政治上的)
解决方案是保留一份副本
每个人的图书馆课程
可执行文件/模块/命名空间/类
使用它。

DRY 是特定软件解决方案的设计原则,但并不总是在跨程序集或域边界时有意义。领域驱动设计的方法使用诸如有界域上下文之类的概念来处理跨程序集和项目的可共享代码问题。

虽然您用通用语言向我们提出了一个问题,但该问题没有通用的解决方案。


Dan G 对组合提出了很好的观点(使主根对象包含一个可以实现所需行为而无需使用实现的子对象)。只要有意义,微软的架构指南就提倡这种方法而不是继承。


如果可以的话,我会投票给 Meager 和 Dan G,他们都提出了很好的评论。

The only acceptable (polotical)
solution is to keep a copy of the
library class for every
executable/moduble/namespace/class
that uses it.

DRY is a principle of design for a particular sofware solution, but does not always make sense across assembly or domain bounds. The methodology of Domain Driven Design uses concepts such as bounded domain contexts to deal with issues of shareable code across assemblies and projects.

While you have given us a problem in generic language, there is no generic solution for this issue.


Dan G makes a good point about composition (making the main root object contain a sub object that can implement the needed behavior without having to use implementation). Microsoft's Architecture guide advocates this approach over inheritance whenever it makes sense.


I would upvote Meager and Dan G if I were able, they both made good comments.

酷到爆炸 2024-10-05 23:37:00

看来您的问题不在于干燥性,而在于依赖项的版本控制。因此,您所遇到的情况是,您的辅助类(带有错误修复)是每个主类的依赖项。但是,只有一个主要类实际上引用了包含修复程序的帮助器类的版本。另外两个类可以自由选择何时升级到改进的帮助器类(通过依赖关系管理过程)。

StarWars-1.0.0.jar -> Helpers-1.0.0.jar
Empire-1.0.1.jar -> Helpers-1.0.1.jar
Jedi-1.0.0.jar -> Helpers-1.0.0.jar

助手团队发布更新来修复问题,其他团队决定何时升级。

你仍然是 DRY,但你可以掌控变化。

It would seem that your issue is not so much one of DRY-ness but rather version control of dependencies. So what you have is a situation where your helper class (with the bug fix) is a dependency of each of your primary classes. However, only one of the primary classes actually references the version of the helper class that contains the fix. The other two classes are at liberty to choose when they will upgrade to the improved helper class (via a dependency management process).

StarWars-1.0.0.jar -> Helpers-1.0.0.jar
Empire-1.0.1.jar -> Helpers-1.0.1.jar
Jedi-1.0.0.jar -> Helpers-1.0.0.jar

The helpers team issue an update to fix stuff, and the other teams decide when they will upgrade.

You're still DRY, but you manage the change.

暮倦 2024-10-05 23:37:00

看来这里的部分问题是需求变化的正常结果。三段代码依赖于“今天”这一共同的业务概念。有些事情发生了变化,“今天”不再有单一的概念。一些代码需要继续基于银河标准日历的当前理解,而一些代码现在需要基于本地恒星系统日历的更灵活的概念(谈论本地化头痛!)。

这只是发生的正常事情。用 SRP 术语来说,由单段代码处理的单一职责现在变成了需要由单独代码段处理的两项职责,因为已经确定了新的变更向量。是时候重构了。

Piskvor 提出了一种解决该问题的方法,该方法在某些情况下效果很好。如果您使用 IOC 容器或至少某种形式的 DI,另一种方法是引入 IHelper 接口(希望它从一开始就存在)。然后,您所需要的只是接口的新实现和一些配置,以便为每个系统连接正确的实现。

wllmsaccnt 也是正确的,DRY 通常不适合跨越您不想创建依赖项的系统边界。事实上,不同的系统有不同的产品负责人,这几乎是一个事实上的变革向量,主张有一个单独的代码库,即使一个系统一开始是另一个系统的分支。共享代码库意味着共享所有权,如果这在政治上不现实,那么在技术上就不合适。

It seems that part of the problem here is the normal result of changing requirements. Three pieces of code rely on the common business concept of "Today". Something has changed such that there is no longer a single concept for "Today." Some of the code needs to continue to the current understanding based on the galactic standard calendar and some code now needs a more flexible concept based on the local star system's calendar (talk about a localization headache!).

This is just a normal thing that happens. Put in SRP terms, what was a single responsibility, handled by a single piece of code is now two responsibilities that needs to be handled by separate pieces of code because a new vector for change has been identified. Time to refactor.

Piskvor suggested one way to tackle the problem which is good in some scenarios. Another approach, if you are using IOC containers, or at least some form of DI, would be to introduce an IHelper interface (hopefully it was there from the start). Then all you need is new implementation of the interface and some configuration to wire up the correct implementation for each system.

wllmsaccnt is also correct that DRY is often not appropriate across system boundaries where you don't want to create dependencies. The fact that different systems have different product owners is pretty much a defacto vector for change that argues for a separate code bases, even if one starts out as a fork of the other. A shared code base means shared ownership and if that is not politically realistic then its not technically appropriate.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文