使用敏捷方法或其他方法编写松散耦合代码的建议

发布于 2024-08-12 09:25:08 字数 1051 浏览 7 评论 0原文

最近我一直在非常认真地阅读罗伯特·C·马丁(又名鲍勃叔叔)的书。我发现他谈论的很多东西对我来说都是现实的救星(小函数大小,非常具有描述性的名称,等等)。

我还没有解决的一个问题是代码耦合。我不断遇到的一个问题是我将创建一个对象,例如包装数组的对象。我确实会在一个班级中进行一些工作,但随后必须调用另一个班级在另一个班级中进行一些工作。到了我将相同数据传递 3-4 层深度的程度,这似乎没有意义,因为很难跟踪该对象传递的所有位置,所以当需要更改它时我有很多依赖。这看起来根本不是一个好的做法。

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

编辑:按要求提供一个现实世界的例子(是的,我完全同意,否则很难理解):

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
  }

}

I have been reading Robert C. Martin's (aka Uncle Bob) books very intensely as of late. I have found a lot of the things he talks about to be real life savers for me (small function size, very descriptive names for things, etc).

The one problem I haven't gotten past yet is code coupling. One problem I keep having over and over is that I will create a object, like something that wraps an array for instance. I do will some work on it in one class, but then have to call another class to do work on it in a different class. To the point where I'm passing the same data 3-4 levels deep, and that just doesn't seem to make sense because it's hard to keep tract of all the places this object is getting passed, so when it's time to change it I have a ton of dependencies. This seems like anything but a good practice.

I was wondering if anyone knew of a better way to deal with this, it seems that Bob's advice (though it's probably me misunderstanding it) seems to make it worse because it has me creating lots more classes. Thanks ahead of time.

EDIT: By Request a real world example (yes I totally agree that it is hard to make sense of otherwise):

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
  }

}

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

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

发布评论

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

评论(5

蛮可爱 2024-08-19 09:25:08

查看示例代码,您的 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
    }
}

Looking at your sample code, your Chart class is tightly coupled to the ChartCalculation class (it's instantiating a new instance of it). If the Chart class uses the ChartCalculation through an interface (IChartCalculation in my sample code) you can remove this coupling. The concrete implementation of IChartCalculation used by the Chart class could be passed to the Chart class by it's constructor (dependency injection), perhaps by a factory class. An alternative might be to use an IOC framework (though often this may introduce more complexity than necessary). In this way the Chart class can use different concrete implementations of IChartCalculation and both can vary independently as long as they are coded to/implement the interface.

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
    }
}
氛圍 2024-08-19 09:25:08

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

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

如果您访问类的私有成员,或者传递另一个类公开的数组的引用以供第三方修改,等等,您就会遇到耦合问题。

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

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

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

因此,假设所有这些层次结构都是合理的,并将图表更改为购物车,因为这对我来说更有意义:

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

对于架构良好的图表库,请参阅

It seems to me you are misunderstanding coupling with (relatively) complex software development.

If you are passing the data around to be consumed, be it via a class or more than one, all the consumers can do is to access the public interface of the class with the data. This is fine regarding to coupling, because you can change how are those classes implemented internally without breaking anything as long as you have your interface constant. It is expected that data could have many clients, as long as they don't depend on anything but the specified public interface.

You would have a problem with coupling if you accessed private members of the classes, or if you passed a reference of the array another class exposes to be modified by third parties, and so on.

It's true though that if you want to change that public interface and the class is being consumed in many places but that would happen anyway, even if you used composition instead of parameter passing, so the most important thing is to design your public interfaces well enough so changes are not a common occurrence.

Summing up, while sometimes this may point to a design problem (where a better design might translate into a class hierarchy where you don't need to pass that much data around), it isn't per se something bad, and in certain cases it's even needed.

EDIT: First of all, is CartCalculations really needed or are you creating that class just to follow some rule? Then, if CartCalculations is warranted, why are you passing an int[] instead of CartItems where you can have control about what is done and how to the list of items? Finally, why do you feel that is brittle? Because you might forget to pass the parameter (compile error, no biggie)? Because somebody might modify the list where it shouldn't (somewhat controllable via having the CartItems which would be the only one loading the data)? Because if you need to change how items are represented (again no biggie if you wrap the array in a class where you could make that kind of changes).

So, assuming all this hierarchy is warranted, and changing Chart to Cart because it makes more sense to me:

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

For a well architected chart library, see http://www.aditus.nu/jpgraph/jpgarchitecture.php

扎心 2024-08-19 09:25:08

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

interface DataProvider
{
  double[] getDataPoints(); 
}

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

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

To avoid coupling, in your example, I would create some kind of DataProvider that can fetch the data points to your graph. This way you can have several different types of datasources that you change depending on what you want kind of data you want to chart. E.g.

interface DataProvider
{
  double[] getDataPoints(); 
}

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

And then I would skip the ChartCalculation class completely and just do the calculations in the chart class. If you feel the need to reuse the code for calculating stuff I would create a toolkit class(similar to the Math class in Java) that can calculate average, median, sums or whatever you need. This class would be general and it would know nothing about the chart or what last items means.

月下伊人醉 2024-08-19 09:25:08

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

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

Code Complete calls it "Streamline parameter passing":

If you're passing a parameter among several routines, that might indicate a need to factor those routines into a class that share the parameter as object data.
Streamlining parameter is not a goal, per se, but passing lots of data around suggests that a different class organization might work better.

尤怨 2024-08-19 09:25:08

某些耦合在应用程序中是有用的,而且我认为是必要的,并且如果更改类型,您将收到编译器错误的事实并不一定会使其变得脆弱。

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

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

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

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

Some coupling is useful in an application, and I would say necessary, and the fact that you will get compiler errors if you change your type doesn't necessarily make it brittle.

I think how to look at this is based on how you will be using the data, and how types may change.

For example, if you expect that you will not always use an int[], but later will need ComplexDataType[] then you may want to put this data into it's own class, and pass that class around as a parameter. That way if you need to make changes, you want to extend it from just mapping integer data to complex numbers, then you would create new setters and getters, so that you insulate the data from the application, and you are free to change the implementation all you want with little care, as long as you keep to the contracts you already agreed to. So, if you state that int arrays can be used in the constructor, or as a setter, then allow that, but also allow other types, ie double, ComplexDataType for example.

One thing to look at is if you want the transformations that a class may do to affect all the instances of your data. So, you have your data transfer object, and you have two threads. One will create a 3D spherical graph, and the other a 2D pie chart. I may want to modify the DTO I have in each graph, to get it into a format for graphing, but it would cause all kinds of problems for the pie chart if the data is changed for spherical coordinates. So, at that point, as you pass data around, you may want to take advantage of the final keyword on the parameter, and make your DTO immutable, so anything you pass out will be a copy, but allow them to replace the information if explicitly requested (they call a setter).

I think this will help to reduce problems as you call several classes deep, but you may need to return the DTO if it was modified, but in the end it is probably a safer design.

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