设计问题:如何最好地使用构图

Posted

技术标签:

【中文标题】设计问题:如何最好地使用构图【英文标题】:DESIGN PROBLEM: How to use composition the best 【发布时间】:2020-02-23 10:33:48 【问题描述】:

我正在做一个项目,但对它的设计有些怀疑。 我怎样才能最好地设计以下问题(在 JAVA 中):

A 类具有以下属性:

像素哈希集,其中每个像素的 x,y 坐标和值 v 介于 0-1 之间。 B 类的实例。

B类具有以下功能:

一个获取像素并返回其左邻居的函数。

当我在 A 类中时,我想对 A 中的每个像素使用 B.function,并且仅当它不存在时才将其添加到 HashSet 中。问题是我不想将 HashSet 发送给函数,如果它可能已经存在,那么从函数返回新的 Pixel 实例有多糟糕(这个函数将在许多像素上运行并且会创建许多未使用的实例像素)。

我还有什么其他选择?

【问题讨论】:

如果你有一个HashSet<Pixel>,查看一个像素是否在集合中的唯一方法是创建另一个像素,所以除非你改变这个方面,否则你真的没有任何其他的B 类中函数的选项,因为即使您确实传入了 HashSet,它也必须创建 Pixel 对象。 谢谢,所以我想知道如果在每次调用 B.function 时,该函数都会创建 10 个我不会使用的实例,那么性能会有多糟糕?这样做可以接受吗? 你不认为 double 的二维数组能更好地表示这一点吗? @ErwinBolwidt 在我的例子中,给定一个图像,我可能只需要在其中保存一些像素,所以我认为保存一个代表所有图像的二维数组可能是浪费的。 一张大小的图片有多少像素?为每个像素创建对象、存储 X 和 y 以及分配 HashSet 都会产生一定的开销。如果您的像素位于矩形区域,您只需要该区域的矩阵。如果您仍然需要 Hash 查找,以 Point(x 和 y)对象作为键、Double 作为值的 HashMap 是一种更自然的表示 【参考方案1】:

由于您使用Set<Pixel>,您必须创建新的 Pixel 实例来检查它是否存在于集合中。

如果 set 在调用 B.function 方法后包含 N 元素,您将创建额外的 N Pixel 节点。如果所有元素都是新的,您只需将它们添加到 set 中,否则Garbage Collection 需要清除它们。缺点之一是我们需要创建m(其中m <= N - 集合中已经存在的Pixel-s 的数量),然后我们需要通过GC 收集它们。 m/N 比率有多大取决于您的算法以及您实际在做什么。

让我们计算我们需要为集合中的N = 1_000_000 像素消耗多少内存。我们知道int4 bytesdouble8 bytes,让我们为对象添加额外的8 bytes 并为引用添加8 bytes。它为Pixel 对象的每个实例提供32 bytes。我们需要创建提供32MBN 对象。假设我们的比率是50%,所以我们分配16MB 只是为了检查它是不需要的。

如果这是您无法支付的成本,您需要开发算法,允许您按照left-to-right 的顺序迭代Set<Pixel>。所以,PixelX 的左邻居在X 之前。

假设PixelX(x, y)的左邻居是像素X'(x - 1, y)Pixel B(0, y) 没有左邻。您需要使用TreeSet 并在Pixel 类中实现Comparable<Pixel> 接口。简单的实现可能如下所示:

@Override
public int compareTo(Pixel o) 
    return this.y == o.y ? this.x - o.x : this.y - o.y;

这允许您按从左到右的顺序迭代集合:(0, 0), (1, 0), ...., (x - 1, y), (x, y), (x + 1, y), ... , (maxX, maxY)。因此,当您迭代它时,您可以检查前一个元素是否是当前Pixel 的左邻居。示例实现如下所示:

void addNeighboursIfNeeded() 
    Set<Pixel> neighbours = new HashSet<>(pixels.size());
    Pixel last = null;
    for (Pixel p : pixels) 
        if (p.getX() == 0 || p.isLeftNeighbour(last)) 
            // a left border pixel
            // or last checked element is a left neighbour of current pixel.
            last = p;
            continue;
        
        // last element was not our left-neighbour so we need to call b method
        Pixel left = b.getLeft(p);
        neighbours.add(left);
        last = p;
    
    // add all new neigbours
    pixels.addAll(neighbours);

这应该允许您保存为重复的Pixel 对象分配的内存。

【讨论】:

【参考方案2】:

我可以在这里看到一些关于面向对象编程的问题。

    封装违规:当您从 A 调用 B 的函数时,该函数对 A 的数据进行操作(您通过不发送 HashMap 来避免这种情况),它违反了封装(如果有原因,虽然它是可以接受的)。是否可以将该函数(在 A 的 HashSet 上操作)移动到 A?这将保护 A 的状态不被暴露。 类的增殖:有可能会有大量的Point类型的对象。您可以考虑使用 Flyweight GOF 设计模式,它将每个点的状态外化并使其可重用。并将大幅减少数量。 将大量点传递给 B 中的方法:如果您可以将方法从 B 转移到 A,则该点得到解决。无论如何,java 将通过引用传递这个集合。但在这种情况下,它对外部类的修改开放(需要注意这方面)。 Point类型的抽象:如果Point类只有状态没有行为,会导致违反封装。你能把 getNeighbour() 方法改成 Point 吗?因为它将使 Point 不可变(这是必不可少的)。当然,实际的算法可以委托给另一个类(如果它的职责独立变化并且具有算法层次结构,请在此处考虑 GOF 策略模式)。 集合中点的唯一性:您的集合将适当注意适当的哈希和类 Point 的逻辑相等性。

【讨论】:

以上是关于设计问题:如何最好地使用构图的主要内容,如果未能解决你的问题,请参考以下文章

如何最好地定义聚合关系?

几大平面设计构图技巧,你get到了吗

打破构图的平衡!增强设计感染力

如何最好地存储kd树中的行

哪个 ORM 框架可以最好地处理 MVCC 数据库设计?

文档设计也需要坚持DRY原则--支付中心应用部署结构图完善