2

最近,我一直在非常认真地阅读 Robert C. Martin(又名 Bob 叔叔)的书。我发现他谈到的很多东西对我来说都是现实生活中的救星(小函数大小,非常描述性的事物名称等)。

我还没有解决的一个问题是代码耦合。我一遍又一遍地遇到的一个问题是我将创建一个对象,例如包装数组的东西。我确实会在一个班上做一些工作,但后来不得不打电话给另一个班在另一个班上做。以至于我将相同的数据传递了 3-4 层,这似乎没有意义,因为很难跟踪该对象正在传递的所有地方,所以当是时候改变它了我有很多依赖项。这似乎不是一个好习惯。

我想知道是否有人知道更好的方法来处理这个问题,似乎 Bob 的建议(尽管我可能误解了它)似乎使情况变得更糟,因为它让我创建了更多的类。提前谢谢。

编辑:通过请求一个真实世界的例子(是的,我完全同意这很难理解):

class ChartProgram () {
int lastItems [];

  void main () {
  lastItems = getLast10ItemsSold();
  Chart myChart = new Chart(lastItems);
  }
}
class Chart () {
  int lastItems[];
  Chart(int lastItems[]) {
  this.lastItems = lastItems;
  ChartCalulations cc = new ChartCalculations(this.lastItems);
  this.lastItems = cc.getAverage();
}
class ChartCalculations {
  int lastItems[];
  ChartCalculations (int lastItems[]){
  this.lastItems = lastItems;
  // Okay so at this point I have had to forward this value 3 times and this is 
  // a simple example. It just seems to make the code very brittle
  }
  getAverage() {
  // do stuff here
  }

}
4

5 回答 5

1

在我看来,您误解了与(相对)复杂的软件开发的耦合。

如果您要传递数据以供使用,无论是通过一个类还是多个类,消费者所能做的就是使用数据访问该类的公共接口。这对于耦合来说很好,因为只要您的接口保持不变,您就可以更改这些类的内部实现方式而不会破坏任何内容。预计数据可以有许多客户端,只要它们不依赖于指定的公共接口以外的任何东西。

如果您访问类的私有成员,或者如果您传递了另一个类公开以供第三方修改的数组的引用,那么您将遇到耦合问题,依此类推。

确实,如果您想更改该公共接口并且该类在许多地方都被使用,但无论如何都会发生这种情况,即使您使用组合而不是参数传递,所以最重要的是设计您的公共接口足够好所以变化并不常见。

总而言之,虽然有时这可能指向设计问题(更好的设计可能会转化为不需要传递那么多数据的类层次结构),但它本身并不是什么坏事,在某些情况下它甚至是必需的。

编辑:首先,真的需要 CartCalculations 还是您创建该类只是为了遵循一些规则?那么,如果 CartCalculations 是有保证的,你为什么要传递一个 int[] 而不是 CartItems ,在那里你可以控制做什么以及如何进入项目列表?最后,为什么你觉得它很脆弱?因为您可能忘记传递参数(编译错误,没什么大不了的)?因为有人可能会修改不应该修改的列表(通过拥有 CartItems 来控制它,这将是唯一加载数据的列表)?因为如果您需要更改项目的表示方式(如果您将数组包装在可以进行此类更改的类中,这也没什么大不了的)。

因此,假设所有这些层次结构都是必要的,并将 Chart 更改为 Cart,因为它对我来说更有意义:

class CartProgram () {
  CartItems lastItems;

  void main () {
    lastItems = getLast10ItemsSold();
    Cart myChart = new Cart(lastItems);
  }
}
class Cart () {
  CartItems lastItems;
  Cart(CartItems lastItems) {
  this.lastItems = lastItems;
  CartCalulations cc = new CartCalculations(this.lastItems);
  cc.getAverage();
}

class CartCalculations {
  CartItems lastItems;
  CartCalculations (CartItems lastItems){
  this.lastItems = lastItems;
  // Okay so at this point I have had to forward this value 3 times and this is 
  // a simple example. It just seems to make the code very brittle
  }
  getAverage() {
  // do stuff here
  }
}

class CartItems {
   private List<Item> itemList;

   public static CartItems Load(int cartId) {
       //return a new CartItems loaded from the backend for the cart id cartId
   }

   public void Add(Item item) {
   }

   public void Add(int item) {
   }

   public int SumPrices() {
       int sum = 0;
       foreach (Item i in itemList) {
          sum += i.getPrice()
       }
       return sum;
   } 
}

class Item 
{
    private int price;
    public int getPrice() { return price; }
}

有关架构良好的图表库,请参阅http://www.aditus.nu/jpgraph/jpgarchitecture.php

于 2009-11-24T06:47:53.970 回答
1

为了避免耦合,在您的示例中,我将创建某种可以将数据点获取到您的图表的 DataProvider。通过这种方式,您可以拥有几种不同类型的数据源,这些数据源可以根据您想要绘制的数据类型进行更改。例如

interface DataProvider
{
  double[] getDataPoints(); 
}

class Chart 
{
 void setDataProvider(DataProvider provider)
 {
   this.provider = provider;
 }
}

然后我会完全跳过 ChartCalculation 类,只在图表类中进行计算。如果你觉得需要重用代码来计算东西,我会创建一个工具包类(类似于 Java 中的 Math 类),它可以计算平均值、中位数、总和或任何你需要的东西。这个类是通用的,它对图表或最后一项的含义一无所知。

于 2009-11-24T09:39:19.093 回答
1

查看您的示例代码,您的 Chart 类与 ChartCalculation 类紧密耦合(它正在实例化它的一个新实例)。如果 Chart 类通过接口(在我的示例代码中为 IChartCalculation)使用 ChartCalculation,则可以消除这种耦合。Chart 类使用的 IChartCalculation 的具体实现可以通过它的构造函数(依赖注入)传递给 Chart 类,也许是工厂类。一种替代方法可能是使用IOC框架(尽管这通常可能会引入不必要的复杂性)。通过这种方式,Chart 类可以使用 IChartCalculation 的不同具体实现,并且只要它们被编码为/实现接口,两者都可以独立变化。

class ChartProgram () 
{
    int lastItems [];

    void main () {
    lastItems = getLast10ItemsSold();
    Chart myChart = new Chart(lastItems, new ChartCalculations());
}

class Chart
{
    int[] lastItems;

    IChartCalculation chartCalculations;

    public Chart(int[] lastItems, IChartCalculations cc)
    {
        this.lastItems = lastItems;
        chartCalculations = cc;
        int average = cc.GetAverage(lastItems);
    }
}

interface IChartCalculation
{
    int GetAverage(int[] lastItems);
}


class ChartCalculation : IChartCalculation
{
    public int GetAverage(int[] lastItems)
    {
        // do stuff here
    }
}
于 2009-11-24T16:56:19.733 回答
0

Code Complete 将其称为“简化参数传递”:

如果您在多个例程之间传递一个参数,这可能表明需要将这些例程分解到一个类中,该类将参数作为对象数据共享。
精简参数本身并不是一个目标,但传递大量数据表明不同的类组织可能会更好。

于 2009-11-24T06:47:25.873 回答
0

一些耦合在应用程序中很有用,我会说是必要的,而且如果你改变你的类型,你会得到编译器错误这一事实并不一定会使它变得脆弱。

我认为如何看待这一点取决于您将如何使用数据以及类型可能如何变化。

例如,如果您希望您不会总是使用int[],但稍后会需要,ComplexDataType[]那么您可能希望将此数据放入它自己的类中,并将该类作为参数传递。这样,如果您需要进行更改,您希望将其从仅将整数数据映射到复数进行扩展,然后您将创建新的 setter 和 getter,以便将数据与应用程序隔离,并且您可以自由更改实现只要您遵守您已经同意的合同,您就可以毫不费力地满足您的所有需求。因此,如果您声明int数组可以在构造函数中使用,或者作为 setter 使用,则允许这样做,但也允许其他类型,double例如ComplexDataType.

需要注意的一件事是,如果您希望类可能进行的转换影响数据的所有实例。所以,你有你的数据传输对象,你有两个线程。一个将创建一个 3D 球面图,另一个将创建一个 2D 饼图。我可能想修改每个图形中的 DTO,将其转换为图形格式,但如果数据更改为球坐标,则会导致饼图出现各种问题。因此,此时,当您传递数据时,您可能希望利用final参数上的关键字,并使您的 DTO 不可变,因此您传递的任何内容都将是一个副本,但如果明确地允许它们替换信息请求(他们称为二传手)。

我认为这将有助于减少深度调用多个类时出现的问题,但是如果 DTO 被修改,您可能需要返回它,但最终它可能是一个更安全的设计。

于 2009-11-24T20:09:59.047 回答