【问题标题】:A way around instantiating sub classes in super class在超类中实例化子类的一种方法
【发布时间】:2013-12-15 23:23:18
【问题描述】:

我有一个基础抽象类,它聚合了一个集合中的一堆项目:

abstract class AMyAbstract
{
    List<string> Items { get; private set; }

    public AMyAbstract(IEnumerable<string> items)
    {
        this.Items = new List<string>(items);
    }
}

有很多子类,我们将它们命名为FooBarBaz 等。它们都是不可变的。现在我需要一个merge() 方法,它将像这样合并两个对象的items

abstract class AMyAbstract
{
    // ...
    public AMyAbstract merge(AMyAbstract other)
    {
        // how to implement???
    }
}

Foo foo1 = new Foo(new string[] {"a", "b"});
Bar bar1 = new Bar(new string[] {"c", "d"});
Foo fooAndBar = foo1.merge(bar1);
// items in fooAndBar now contain: {"a", "b", "c", "d"}

由于对象是不可变的,merge() 方法不应更改 items 字段的状态,而是应返回调用它的类的新对象。我的问题是:如何明智地实现merge() 方法?

问题 1:AMyAbstract 显然不知道子类的特定构造函数(依赖倒置原则),因此我不能(或者我可以?)在超类中创建子类的实例。

问题 2:在 每个 子类中实现 merge() 方法会导致 大量 代码重复(DRY 规则)。

问题 3:将merge() 逻辑提取到一个全新的类并不能解决 DRY 规则问题。即使使用访问者模式,也需要大量复制/粘贴。

上面提出的问题排除了我在阅读SOLID 之前可能对实现的任何想法。 (从那以后我的生活一直很悲惨;)

或者是否有一种完全不同的、开箱即用的方法来实现这些对象的合并?

我希望能用 C#、Java 甚至 PHP 回答。

编辑:我想我遗漏了一条有效信息:尽管有很多不同的子类,但它们只能(应该)以两种,也许是三种方式构造(如单一责任原则的含义):

  • 无参数构造函数
  • 接受一个IEnumerable&lt;T&gt; 参数的构造函数
  • 接受数组和其他修饰符的构造函数

这会将访问者模式放回标签上如果我可以对构造函数施加约束——例如通过在接口中定义构造函数。但这只有在 PHP 中才有可能。在 Java 或 C# 中,无法强制执行构造函数签名,因此我无法确定如何实例化子类。一般来说,这是一个很好的规则,因为我们永远无法预测子类的作者希望如何构造对象,但在这种特殊情况下,它可能会有所帮助。所以一个辅助问题是:我能以某种方式强制一个类的实例化方式吗?在这种简单的情况下,构建器模式听起来太过分了,是吗?

【问题讨论】:

  • 我删除了语言标签,因为这个问题与特定语言的关系比与 OO 概念的关系要小 - 但让我说,好问题!
  • 谢谢 :) 现实生活中的问题。语言标签的存在是为了吸引所有那些高级编程语言专家。我很担心,如果他们看不到他们就不会来这里:(
  • 他们会来,他们会因为问题与这些语言无关而生气,他们会说这是题外话,然后投反对票并投票结束 =\ 但我认为你的问题会吸引足够的很好的关注。如果没有,那么我稍后会设置赏金。
  • 不可变类将始终返回 Foo 、 Bar 类的新对象。因此,您需要将 Foo 和 Bar 对象合并到超类引用的代码。使用“instanceOf”合并新类中的对象可能是更好的选择。
  • 请注意,如果您想要不变性,则不应返回 List,而应返回 ReadOnlyCollection。这是因为用户可以这样做:instance.Items.Add(Something) 所以你应该返回一个新列表或一个 readOnlyCollection,第二个更好,因为它向用户显示他不能改变集合,而如果你返回一个他可能认为他可以的列表副本

标签: oop design-patterns dry solid-principles single-responsibility-principle


【解决方案1】:

基于@AK_ 的comment 的简洁解决方案:

tldr:基本思想是为每个聚合字段创建多个merge 方法,而不是为整个对象使用merge 方法。

1)我们需要一个特殊的列表类型来聚合AMyAbstract 实例中的项目,所以让我们创建一个:

class MyList<T> extends ReadOnlyCollection<T> { ... }

abstract class AMyAbstract
{
    MyList<string> Items { get; private set; }

    //...
}

这里的好处是我们有一个专门的列表类型用于我们的目的,我们可以在以后进行更改。

2) 我们不想为AMyAbstract 的整个对象使用合并方法,而是希望使用一种方法来合并该对象的项目:

abstract class AMyAbstract
{
    // ...

    MyList<T> mergeList(AMyAbstract other)
    {
        return this.Items.Concat(other.Items);
    }
}

我们获得的另一个优势:decomposition 合并整个对象的问题。因此,我们将其分解为一个小问题(在这种情况下仅合并聚合列表)。

3) 现在我们可以使用我们可能想到的任何专用构造函数来创建合并对象:

Foo fooAndBar = new Foo(foo1.mergeList(bar1));

我们只返回合并列表,而不是返回整个对象的新实例,而合并列表又可以用来创建目标类的对象。在这里,我们获得了另一个优势:延迟对象实例化,这是creational patterns 的主要目的。

总结:

因此,此解决方案不仅解决了问题中提出的问题,而且还提供了上述其他优势。

【讨论】:

    【解决方案2】:

    我参加聚会有点晚了,但由于您尚未接受答案,我想我会添加自己的答案。

    其中一个关键点是集合应该是不可变的。在我的示例中,我公开了 IEnumerable 来促进这一点 - 项目的集合在实例之外是不可变的。

    我认为这有两种工作方式:

    1. 公共默认构造函数
    2. 一种内部Clone 模板方法,类似于上面@naveen 的答案

    选项 1 的代码更少,但实际上它取决于您是否允许使用没有项目且无法更改项目的 AMyAbstract 实例。

    private readonly List<string> items;
    
    public IEnumerable<string> Items { get { return this.items; } } 
    
    public static T CreateMergedInstance<T>(T from, AMyAbstract other)
        where T : AMyAbstract, new()
    {
        T result = new T();
        result.items.AddRange(from.Items);
        result.items.AddRange(other.Items);
        return result;
    }
    

    似乎满足您的所有要求

    [Test]
    public void MergeInstances()
    {
        Foo foo = new Foo(new string[] {"a", "b"});
        Bar bar = new Bar(new string[] {"c", "d"});
        Foo fooAndBar = Foo.CreateMergedInstance(foo, bar);
    
        Assert.That(fooAndBar.Items.Count(), Is.EqualTo(4));
        Assert.That(fooAndBar.Items.Contains("a"), Is.True);
        Assert.That(fooAndBar.Items.Contains("b"), Is.True);
        Assert.That(fooAndBar.Items.Contains("c"), Is.True);
        Assert.That(fooAndBar.Items.Contains("d"), Is.True);
    
        Assert.That(foo.Items.Count(), Is.EqualTo(2));
        Assert.That(foo.Items.Contains("a"), Is.True);
        Assert.That(foo.Items.Contains("b"), Is.True);
    
        Assert.That(bar.Items.Count(), Is.EqualTo(2));
        Assert.That(bar.Items.Contains("c"), Is.True);
        Assert.That(bar.Items.Contains("d"), Is.True);
    }
    

    无论您最终选择默认构造函数还是模板方法,这个答案的关键在于Items 只需要在外部是不可变的。

    【讨论】:

    • 谢谢,这是一个非常好的实现!如果不是事实,我可能会使用它 - 正如你所提到的 - 实现取决于将无参数构造函数约束放在 T 类型上。我无法承担这一点,因为这个类的许多实现都没有一个(我提到有 3 种类型的构造函数),并且一些使用它们的程序集是第 3 方,所以我会中断他们的代码。尽管如此+1,我会在下次设计课程时使用它,然后再将其推向生产:)
    • 顺便说一句,我提出的问题实际上是由AK_解决的,但他至今没有给出完整的答案。如果您有兴趣,请查看this 评论和后续评论。解决方案非常简单,我很惭愧我没有自己想出它:)
    【解决方案3】:

    OBSOLETE,留作参考(并显示我是如何得出最终解决方案的),请参阅下面 EDIT 之后的代码

    我会说构建器模式是要走的路。我们只需要一个构建器来保留实例但修改需要更改的一个字段。

    如果想获得(如您的代码所示)

    Foo fooAndBar = foo1.merge(bar1);
    

    需要额外的泛型类型定义(因此定义类 AMyAbstract )才能在上述调用中仍然产生正确的最终类型(而不是仅仅将 AMyAbstract 视为 fooAndBar 的类型)。

    注意:merge 方法在下面的代码中被重命名为 MergeItems 以明确合并的内容。 我为 Foo 和 Bar 指定了不同的构造函数,因此很明显它们不需要具有相同数量的参数。

    实际上要真正不可变,列表不应该直接在 Items 属性中返回,因为它可以被调用者修改(使用 new List(items).AsReadOnly() 产生了一个 ReadOnlyCollection,所以我只使用了这个) .

    代码:

    abstract class AMyAbstract<T> where T : AMyAbstract<T>
    {
        public ReadOnlyCollection<string> Items { get; private set; }
    
        protected AMyAbstract(IEnumerable<string> items)
        {
            this.Items = new List<string>(items).AsReadOnly();
        }
    
        public T MergeItems<T2>(AMyAbstract<T2> other) where T2 : AMyAbstract<T2>
        {
            List<string> mergedItems = new List<string>(Items);
            mergedItems.AddRange(other.Items);
            ButWithItemsBuilder butWithItemsBuilder = GetButWithItemsBuilder();
            return butWithItemsBuilder.ButWithItems(mergedItems);
        }
    
        public abstract class ButWithItemsBuilder
        {
            public abstract T ButWithItems(List<string> items);
        }
    
        public abstract ButWithItemsBuilder GetButWithItemsBuilder();
    }
    
    class Foo : AMyAbstract<Foo>
    {
        public string Param1 { get; private set; }
    
        public Foo(IEnumerable<string> items, string param1)
            : base(items)
        {
            this.Param1 = param1;
        }
    
        public class FooButWithItemsBuilder : ButWithItemsBuilder
        {
            private readonly Foo _foo;
            internal FooButWithItemsBuilder(Foo foo)
            {
                this._foo = foo;
            }
    
            public override Foo ButWithItems(List<string> items)
            {
                return new Foo(items, _foo.Param1);
            }
        }
    
        public override ButWithItemsBuilder GetButWithItemsBuilder()
        {
            return new FooButWithItemsBuilder(this);
        }
    }
    
    class Bar : AMyAbstract<Bar>
    {
        public string Param2 { get; private set; }
        public int Param3 { get; private set; }
    
        public Bar(IEnumerable<string> items, string param2, int param3)
            : base(items)
        {
            this.Param2 = param2;
            this.Param3 = param3;
        }
    
        public class BarButWithItemsBuilder : ButWithItemsBuilder
        {
            private readonly Bar _bar;
            internal BarButWithItemsBuilder(Bar bar)
            {
                this._bar = bar;
            }
    
            public override Bar ButWithItems(List<string> items)
            {
                return new Bar(items, _bar.Param2, _bar.Param3);
            }
        }
    
        public override ButWithItemsBuilder GetButWithItemsBuilder()
        {
            return new BarButWithItemsBuilder(this);
        }
    }
    
    class Program
    {
        static void Main()
        {
            Foo foo1 = new Foo(new[] { "a", "b" }, "param1");
            Bar bar1 = new Bar(new[] { "c", "d" }, "param2", 3);
            Foo fooAndBar = foo1.MergeItems(bar1);
            // items in fooAndBar now contain: {"a", "b", "c", "d"}
            Console.WriteLine(String.Join(", ", fooAndBar.Items));
            Console.ReadKey();
        }
    }
    

    编辑

    也许更简单的解决方案是避免使用构建器类,而是使用

    abstract T ButWithItems(List<string> items);
    

    直接在基类中,实现类只会像当前构建器那样实现它。

    代码:

    abstract class AMyAbstract<T> where T : AMyAbstract<T>
    {
        public ReadOnlyCollection<string> Items { get; private set; }
    
        protected AMyAbstract(IEnumerable<string> items)
        {
            this.Items = new List<string>(items).AsReadOnly();
        }
    
        public T MergeItems<T2>(AMyAbstract<T2> other) where T2 : AMyAbstract<T2>
        {
            List<string> mergedItems = new List<string>(Items);
            mergedItems.AddRange(other.Items);
            return ButWithItems(mergedItems);
        }
    
        public abstract T ButWithItems(List<string> items);
    }
    
    class Foo : AMyAbstract<Foo>
    {
        public string Param1 { get; private set; }
    
        public Foo(IEnumerable<string> items, string param1)
            : base(items)
        {
            this.Param1 = param1;
        }
    
        public override Foo ButWithItems(List<string> items)
        {
            return new Foo(items, Param1);
        }
    }
    
    class Bar : AMyAbstract<Bar>
    {
        public string Param2 { get; private set; }
        public int Param3 { get; private set; }
    
        public Bar(IEnumerable<string> items, string param2, int param3)
            : base(items)
        {
            this.Param2 = param2;
            this.Param3 = param3;
        }
    
        public override Bar ButWithItems(List<string> items)
        {
            return new Bar(items, Param2, Param3);
        }
    }
    
    class Program
    {
        static void Main()
        {
            Foo foo1 = new Foo(new[] { "a", "b" }, "param1");
            Bar bar1 = new Bar(new[] { "c", "d" }, "param2", 3);
            Foo fooAndBar = foo1.MergeItems(bar1);
            // items in fooAndBar now contain: {"a", "b", "c", "d"}
            Console.WriteLine(String.Join(", ", fooAndBar.Items));
            Console.ReadKey();
        }
    }
    

    【讨论】:

      【解决方案4】:

      关于依赖倒置规则和代码重复问题,您是对的。

      您可以在抽象类中编写合并逻辑的核心实现,并将创建新实例的任务交给派生类。在您的抽象类中创建一个抽象方法,该方法将强制所有子类实现它。目的是此方法是创建类的新实例并返回它。超类将使用此方法获取新实例并进行合并。

      生成的 java 代码将如下所示

      abstract class AMyAbstract {
          // ...
          public AMyAbstract merge(AMyAbstract other) {
              AMyAbstract obj = getNewInstance();
              // Do the merge
              // Return the merged object.
          }
      
          protected abstract AMyAbstract getNewInstance();
      }
      
      class foo extends AMyAbstract {
          protected foo getNewInstance() {
              // Instantiate Foo and return it.
          }
      }
      

      希望这会有所帮助..

      【讨论】:

      • +1 不错的灵感,一个创建自身实例的抽象工厂!
      • 这将返回AMyAbstract的实例,而在不重复代码的情况下获取所需子类的实例有点复杂。
      • 这不会返回 AMyAbstract 的实例。这将返回由 AMyAbstract 类型引用的 Foo 本身的实例。事实上,您可以像这样更改派生类中的方法签名:protected foo getNewInstance()。同时更新我的​​答案。
      • 感谢您的回答,但不幸的是它并没有解决我的问题 :( 正如我所概述的:类必须是不可变的。因此 getNewInstance() 方法必须返回一个完全构造的对象所有适当的项目都已设置。在您的实现中,它不会这样做。更改您的代码以接受构造函数参数实际上会导致我描述为 问题 2 的情况。getNewInstance() 方法将基本上执行覆盖merge() 方法的逻辑。所以我们回到squere 1。不过我为你的努力+1。
      • @MaciejSz 1. 请阅读this 2. 你可以反过来做:在合并方法中复制和连接列表,然后将它们作为参数传递给工厂方法。
      猜你喜欢
      • 1970-01-01
      • 2015-09-18
      • 1970-01-01
      • 1970-01-01
      • 2023-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多