【问题标题】:Which design is better for a class that simply runs a self-contained computation?对于仅运行自包含计算的类,哪种设计更好?
【发布时间】:2024-04-18 01:10:01
【问题描述】:

我目前正在研究一个计算两个对象之间差异的类。我正在尝试确定该课程的最佳设计是什么。我看到两个选项:

1) 一次性类实例。在构造函数中获取要 diff 的对象并为此计算 diff。

public class MyObjDiffer {
  public MyObjDiffer(MyObj o1, MyObj o2) {
    // Calculate diff here and store results in member variables
  }

  public boolean areObjectsDifferent() {
    // ...
  }

  public Vector getOnlyInObj1() {
    // ...
  }

  public Vector getOnlyInObj2() {
    // ...
  }

  // ...
}

2) 可重用的类实例。构造函数不接受任何参数。有一个“calculateDiff()”方法,可以对对象进行比较,并返回结果。

public class MyObjDiffer {
  public MyObjDiffer() { }

  public DiffResults getResults(MyObj o1, MyObj o2) {
    // calculate and return the results.  Nothing is stored in this class's members.
  }
}

public class DiffResults {
  public boolean areObjectsDifferent() {
    // ...
  }

  public Vector getOnlyInObj1() {
    // ...
  }

  public Vector getOnlyInObj2() {
    // ...
  }
}

差异将相当复杂(细节对问题无关紧要),因此需要一些辅助函数。如果我采用解决方案 1,那么我可以将数据存储在成员变量中,而不必传递所有内容。它稍微紧凑一些,因为所有内容都在一个类中处理。

但是,从概念上讲,“不同”特定于一组结果似乎很奇怪。选项 2 将结果与实际计算它们的逻辑分开。

编辑:选项 2 还提供了使“MyObjDiffer”类静态的能力。谢谢kitsune,我忘了说。

我无法看到任何一个选项的任何重要优点或缺点。我认为这种事情(一个只处理一些一次性计算的类)必须经常出现,也许我错过了一些东西。所以,我想我会向云端提出这个问题。这里的一个或另一个选项有显着的优点或缺点吗?天生就更好吗?有关系吗?

我在 Java 中执行此操作,因此可能存在一些限制,但总体设计问题可能与语言无关。

【问题讨论】:

    标签: language-agnostic oop


    【解决方案1】:

    使用面向对象编程

    使用选项 2,但不要将其设为静态。

    策略模式

    这样,实例MyObjDiffer 可以传递给任何需要Strategy 来计算对象之间差异的人。

    如果以后您发现在不同的上下文中使用不同的规则进行计算,您可以创建一个新的策略来适应。使用您的代码,您将扩展 MyObjDiffer 并覆盖其方法,这当然是可行的。更好的方法是定义一个接口,并将 MyObjDiffer 作为一个实现。

    如果您想延迟决定,任何体面的重构工具都可以从 MyObjDiffer 中“提取接口”并在稍后将对该类型的引用替换为接口类型。将“选项 2”与实例方法而不是类过程一起使用,可为您提供这种灵活性。

    配置实例

    即使您从不需要编写新的比较方法,您也可能会发现指定选项来定制基本方法的行为很有用。如果您考虑使用“diff”命令来比较文本文件,您会记得有多少不同的选项:空格和大小写敏感、输出选项等。在 OO 编程中最好的模拟是考虑每个diff 进程作为一个对象,选项设置为该对象的属性。

    【讨论】:

    • 这是目前我正在做的一个快速一次性验证应用程序,所以我没有看到它(在我的具体情况下)被变成一种策略模式。但你确实提出了一个很好的观点。
    • 要考虑的一个问题:使用静态方法有什么好处?对于静态方法,我看到的唯一论点是它们使设置 Differ API 的客户端变得更容易(它们不需要以某种方式获得实例,它们总是使用相同的方法)。但这需要灵活性。
    • Static 主要是很好,因为它使调用函数变得更容易一些(并且 稍微 更少的资源密集,并不是说它真的很密集)。但是你确实失去了很多灵活性,所以在这种情况下可能不值得。
    • 如果客户真的讨厌打字,他们总是可以为实例调用编写一个静态包装器。
    • 没有快速的一次性验证应用程序。一切都是永恒的。拥有一流的对象并不复杂。 “更容易调用”没有实际价值。
    【解决方案2】:

    出于多种原因,您需要解决方案 #2。而且你不希望它是静态的。

    虽然静态看起来很有趣,但当您提出 (a) 具有相同要求的新算法或 (b) 新要求时,这将是一场维护噩梦。

    一个一流的对象(没有太多内部状态)允许您演变成一个不同的类层次结构——一些更慢,一些更快,一些内存更多,一些内存更少,一些用于旧需求,一些用于新要求。

    您的某些差异可能会导致复杂的内部状态或内存,或增量差异或基于哈希码的差异。各种可能性可能存在。

    可重用对象允许您在应用程序启动时使用属性文件选择您的不同之处。

    从长远来看,您希望尽量减少分散在整个应用程序中的新操作的数量。您希望将新操作集中在可以找到和控制它们的地方。要从旧的不同算法更改为新的不同算法,您需要执行以下操作。

    1. 编写新的子类。

    2. 更新属性文件以开始使用新的子类。

    并且完全确信没有隐藏的 d= new MyObjDiffer( x, y ) 隐藏在你不知道的地方。

    你想在任何地方使用d= theDiffer.getResults( x, y )

    Java 库所做的是它们有一个静态的 DifferFactory。该因子检查属性并发出正确的差异。

    DifferFactory df= new DifferFactory();
    MyObjDiffer mod= df.getDiffer();
    mod.getResults( x, y );
    

    工厂通常会缓存单个副本——它不必在每次调用 getDiffer 时物理读取属性。

    这种设计为您在未来提供了最大的灵活性。它看起来像 Java 库的其他部分。

    【讨论】:

      【解决方案3】:

      我真的不能说我有充分的理由说明它是“最佳”方法,但我通常会为可以与之“对话”的对象编写类。所以它就像您的“一次性使用”选项 1,除了通过调用设置器,您可以“重置”它以供其他用途。

      这里没有提供实现(这很明显),而是一个示例调用:

      MyComparer cmp = new MyComparer(obj1, obj2);
      boolean match = cmp.isMatch();
      cmp.setSubjects(obj3,obj4);
      List uniques1 = cmp.getOnlyIn(MyComparer.FIRST);
      cmd.setSubject(MyComparer.SECOND,obj5);
      List uniques = cmp.getOnlyIn(MyComparer.SECOND);
      

      ...等等。

      这样,调用者可以决定是要实例化大量对象,还是继续重用一个。

      如果对象需要大量设置,这将特别有用。假设您的比较器接受选项。可能有很多。设置一次,然后多次使用。

      // set up cmp with options and the master object
      MyComparer cmp = new MyComparer();
      cmp.setIgnoreCase(true);
      cmp.setIgnoreTrailingWhitespace(false);
      cmp.setSubject(MyComparer.FIRST,canonicalSubject);
      
      // find items that are in the testSubjects objects,
      // but not in the master.
      List extraItems = new ArrayList();
      for (Iterator it=testSubjects.iterator(); it.hasNext(); ) {
          cmp.setSubject(MyComparer.SECOND,it.next());
          extraItems.append(cmp.getOnlyIn(MyComparer.SECOND);
      }
      

      编辑:顺便说一句,我将其称为 MyComparer 而不是 MyDiffer,因为使用 isMatch() 方法似乎比使用 isDifferent() 方法更自然。

      【讨论】:

      • 所以你基本上是在做方法 1.5。该类包含所有结果,但可以重用以重新计算这些结果。关于设置的好点。
      • 现在我正在考虑它发现了一个缺点 - 只要您需要结果,该类就会保留对输入的引用。这是拥有 diffResults 类的一个很好的理由。添加它不会影响此方法的其余部分。
      • 每当我过去尝试过此操作时,对象中的某些内容总是会被遗留(由我或其他程序员),并在第二次执行时导致奇怪的错误。物件制作成本低,最好每次都制作一个新的。
      • 创建它的成本(就程序员工作量而言,而不是性能而言)取决于有多少设置选项(setIgnore() 等)。我有一个真实世界的例子,它有大约 30 行设置,然后进行 100 次测试(它是一个过滤器)。
      【解决方案4】:

      我会选择 2 号并考虑是否应该将其设为静态。

      【讨论】:

        【解决方案5】:

        为什么要编写一个唯一目的是计算两个对象之间差异的类?这听起来像是静态函数或类的成员函数的任务。

        【讨论】:

        • 我要区分的东西是密封的。无法将其添加到课程中。静态方法可以工作,这基本上是选项 2。
        • 静态方法的问题是,如果你想有额外的选项,你必须做更大的签名(或者创建一个 DiffOptions 类。太棒了!)
        【解决方案6】:

        我会选择静态构造方法,比如。

        Diffs diffs = Diffs.calculateDifferences(foo, bar);
        

        这样,在计算差异的时候就一目了然了,没有办法误用对象的接口。

        【讨论】:

        • 嗯。我喜欢这样,虽然 calculateDifferences() 方法会相当复杂。我需要一堆静态辅助方法。可能没什么大不了的,但这对我来说感觉不对。
        • 如果你愿意,calculateDifferences 可以返回 new Diffs(foo, bar);并且构造函数可以是私有的。
        • @Herms - 私有内部类可能会有所帮助。甚至是私人建筑商。
        【解决方案7】:

        我喜欢明确地开始工作而不是让它在实例化时发生的想法。此外,我认为结果足以保证他们自己的课程。你的第一个设计对我来说并不干净。使用这个类的人必须明白,在执行计算之后,一些其他类成员现在持有结果。选项 2 更清楚地了解正在发生的事情。

        【讨论】:

          【解决方案8】:

          这取决于您将如何使用差异。在我看来,将差异视为一个逻辑实体是有意义的,因为它需要支持一些操作,如“getDiffString()”、“numHunks()”或“apply()”。我可能会拿你的第一个做更多这样的事情:

          public class Diff
          {
              public Diff(String path1, String path2)
              {
                  // get diff
          
                  if (same)
                      throw new EmptyDiffException();
              }
          
              public String getDiffString()
              {
          
              }
          
              public int numHunks()
              {
          
              }
          
              public bool apply(String path1)
              {
                  // try to apply diff as patch to file at path1.  Return
                  // whether the patch applied successfully or not.
              }
          
              public bool merge(Diff diff)
              {
                  // similar to apply(), but do merge yourself with another diff
              }
          }
          

          使用这样的 diff 对象也可能有助于保存一堆补丁,或序列化为压缩存档,可能是“撤消”队列等等。

          【讨论】:

          • 这不是一个正常的差异。它没有在字符串/文件上运行,也没有为补丁生成任何东西。我的问题的一个稍微简化的版本:确保 2 个哈希表具有所有相同的键,忽略分配给这些键的实际值。没有申请,只是计算。
          最近更新 更多