复制一个对象有哪些好的设计模式?

3
我需要复制一个层次结构比较深的对象(即,该对象有多个不同类型的成员,每个成员都有多个不同类型的成员等等)。进行深度复制需要在许多类中实现clone()方法,这似乎对我的应用程序来说过于夸张。
我想出的解决方案相当简单,但我想知道是否不好。我的解决方案如下:
  • 定义一个空接口Settings
  • 定义一个名为IsCopyable的接口,其中包含getSettings()applySettings(Settings s)方法。
  • 对于实现了IsCopyable的类X,编写实现接口Settings的类,该类保存必须应用于X对象以进行复制的“设置”。(我通常将此类嵌套在类X中,因此我有X.Settings implements Settings,但这可以在其他地方完成。)
然后,要复制X类的实例:
X myX = new X();
// Stuff happens to myX.
// Now we want to copy myX.
X copyOfX = new X();
copyOfX.applySettings(myX.getSettings());

即,要复制一个给定的对象,需要创建该对象的新实例,然后在要复制的对象上调用getSettings(),将生成的Settings对象作为值传递给新实例上的applySettings()。(当然,复制过程可以包装在一个名为copy()或其他名称的成员中。)
这对我的特定问题非常有效,但我做了一些愚蠢的事情吗? 我(糟糕地)重新发明了已经存在的东西吗?
提前致谢。
Chris

我不明白。您是在一个类中实现所有复制逻辑,可能是对象层次结构中最顶层的类。是这样吗?如果是,您如何使其复制所有子对象及其子对象? - Yaroslav Yakovlev
+1 对于引起了很多兴趣的问题。 - KLE
5个回答

4

我想回答你最后一个问题的答案是“是”,只是为了好玩 :-) 但我知道你想要一些论据,所以让我试试:

  • 如果一个概念(这里是复制)是唯一的对象,则可以考虑在同一类中合并两个概念(这将在类本身中实现该方法)。例如:

    • 如果对于某些对象,有几种复制方式,取决于......,那么复制确实应该脱离原始对象。
    • 如果对于所有对象,都有一种唯一的复制方式,那么创建一个不同的对象来执行复制是否真的有很大价值?(例如,如果总复杂度太高,难以阅读和理解,因此最好将其分解)。
  • 我一直很难通过调用显式构造函数来创建副本。原因是构造函数是唯一无法继承的方法(除了静态方法...),因此它们不能是通用的(不可能为所有可复制的对象拥有唯一的接口)。这意味着您无法在整个应用程序中编写通用代码,以复制您的对象。每次我尝试时,都会有一个时刻,我真的需要以一般方式进行复制。

  • 显式调用构造函数也意味着将来将无法替换子类。假设您有一个算法A,它在变量B上工作。如果将B的子类C传递给A,则当A创建其B变量的副本(其实际类型为C)时,将使用B构造函数创建该副本,因此它将不是相同的类,并且可能会更改行为。因此,通过调用构造函数进行复制非常有限。

  • 显式调用构造函数意味着无法使用接口。您可以在许多地方阅读有关接口的价值......因此,例如,在我们的应用程序中,许多对象不是直接在我们的代码中实例化的,而是要求定位器/工厂提供接口(或类),具有许多可能的优势(如果您的应用程序某天需要这个):

    • 如果我想在特定上下文中将每个A对象替换为B子类,例如在某些自动化测试期间测量昂贵操作的性能,那么这很容易。我们还需要将HashMaps替换为子类,以查找插入到Map中的一个非可序列化对象,后来在序列化期间导致错误。
    • 如果有一个接口,那么在我的常规代码中,只涉及接口创建对象(除了工厂)。因此,我根本没有依赖于具体类,这是非常好的,因为您知道(对接口的依赖关系具有更少的传递依赖关系,并且更容易进行测试模拟)。
    • 在我们的情况下,这个工厂实际上是由Spring支持的,因此通过Spring完成实例化。需要采取许多其他步骤(代理,拦截,初始化方法......)。

在我们的应用程序中,通常会创建一个(或几个)克隆器。给定一个顶级对象,它们知道如何制作它的深层副本。使用通用克隆器的优点是代码只需编写一次,适用于整个应用程序。通常,它还在应用程序之间重复使用...
实现:例如,使用反射递归获取每个成员。然而有许多要避免的陷阱:
- 循环:A引用B,B引用C,C引用A。因此,我保留了已经被复制的对象的Map,引用该复制品。当我复制一个对象,但发现它已经在映射中时,我不会复制它,而是替换其已经制作好的复制品。 - 特殊类型:枚举不应该被复制(还有其他一些静态对象)。某些库类也可能存在问题,因此您可以保留不希望复制或以特殊方式复制的特殊类的Set或Map。 - 您可能会遇到final字段的问题...
特定情况
通常有特定的对象,其中默认方式不正确。我们既希望有一个通用实现,又希望根据需要进行重载。对于它们,我们使用以下方法:
- 如果我们可以修改对象,则让它们实现特定的CustomizedCopier接口,并且它们在该方法中的代码负责按照他们想要的方式进行复制。如果通用代码看到这个接口,它将不执行任何操作。 - 如果我们无法修改对象(JRE,第三方代码...),则有一个Map/Registry Map存储特定类以及我们希望为它们使用的特定复制器。请注意,有时也会使用此技巧来自定义复制,而不是一般性地自定义复制,仅适用于某些特殊用例,因为它可以重载对象复制的方式。
实际上,我通常会最终使用几个克隆器。例如,克隆数据持久化实体通常会使用此知识以稍微不同的方式进行克隆(例如,ID和审核字段可以设置为null)。
我通常还有一个类,用于满足其他需求的依赖项搜索:
- toString():创建一个复杂对象的调试字符串。 - equals()和hashCode()实现(如果需要)。 - 重新初始化对象图形的所有属性的默认值(考虑多选项卡巨大表单中“重置”按钮的实现)。 - 检查对象图中某个位置的对象是否存在。 - 控制对象图的可序列化性(HttpSession是序列化的典型用例;在开发过程中,我们显式检查对象,以检测不可序列化的对象,并为开发人员提供最佳错误消息)。 - ...
请注意,复制通常用于多线程。理想情况下,在多线程环境中重用的对象是不可变的。如果不是,则通常建议进行克隆以确保程序全局一致性...
性能
使用反射并不总是很快。通常,对于大量使用且经常需要复制的复制,我们会在对象本身中实现复制。但是我们发现只有少数几个需要被复制且数量庞大的类,因此这只是一种例外情况,我们会在它们变得有用时(我之前在帖子中介绍了如何使用注册表)插入它们。

1

这可能是一种代码异味,您可能需要采取另一种方法。您的对象应该是“明显可克隆的”,有几种创建此类型对象的方法:

  • 您的对象是值对象(在C#中为struct),仅引用其他值对象或不可变对象。(在这种情况下,克隆就像影响变量一样容易)
  • 您的对象是不可变的,因此您不需要克隆它,只需共享引用即可。
  • 您的对象是“属性包”,因为每个成员也很容易克隆。(WCF中的绑定就是这样)
  • 您的对象只是可序列化数据,因此您可以将其序列化并反序列化以创建新实例。

我不了解您的设计足够多,无法告诉您是否是最佳解决方案,但通常我会创建一个工厂,它只是一个“属性包”,用于创建我的对象,然后我在工厂上实现ICloneable。


序列化/反序列化的想法非常优雅;我之前没有想到过。 - Microserf

0

我目前遇到了一个类似的情况,需要复制一个用于asp.net的客户服务器控件。问题在于使用MemberwiseClone()进行克隆会由于复杂的控件层次结构而产生一些不良影响。

我还不知道如何解决这个问题,但我有一些实现ICloneable接口的想法,但不使用MemberwiseClone(),而是创建一个新实例。

public class MyObject : ICloneable
{
   ...

   public object ICloneable.Clone()
   {
      MyObject clone = new MyObject();
      //copy everything I want to have from this object to the clone

      return clone;
   }
}

然后你只需要调用

MyObject someObjInstance = new MyObject();
...
...
MyObject clone = ((ICloneable)someObjInstance).Clone();
...

你将会得到一个全新的对象,其中包含你需要复制的成员。我不认为拥有你提到的设置有什么好处,因为实现Clone()的类将知道要复制什么。

当然,这取决于你具体的目的。这意味着创建新实例,而不是像MemberwiseClone()一样进行位复制。


0

0

如果您的接口Settings为空(没有属性或方法),我会称之为代码异味。按照惯例,接口应该以字母“ I”或ISettings开头。

你如何实现方法applySettings(Settings s)?由于如您所述,Settings接口为空,您无法对s参数进行任何操作,除非将其转换为另一个对象。这是不好的,或者更好地描述为“无意义”,因为参数是强类型的,但是该强类型被忽略了。

此外,我看不到有充分的理由来使用applySettings()方法。如果您有一个对象和第二个对象具有所需值,只需使用第二个对象即可。如果第二个对象正在使用中(在某个其他对象图中),则复制它并使用它。没有理由编写逻辑,说“使此对象成为另一个对象的副本”,因为您必须已经编写了“制作副本”的逻辑。

从客户端的角度来看,制作深拷贝应该很容易。ICloneable接口非常适合这个任务。作为一种设计模式,这被称为原型模式。如果您需要不同的方式来创建副本(即不同类型的深拷贝操作),则可以考虑使用Builder模式来实现Clone()方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接