一种在超类中实例化子类的方法

时间:2013-11-29 18:00:28

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

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

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字段的状态,而应该返回调用它的类uppon的新对象。我的问题是:如何明智地实施merge()方法?

问题1:AMyAbstract显然不了解子类的特定构造函数(依赖性反转原理),因此我不能(或者我可以)在超类中创建子类的实例。

问题2:在每个子类中实施merge()方法是代码重复的批次(DRY规则)。

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

上面提到的问题排除了我在阅读SOLID之前可能已经实现过的任何想法。 (从那时起,我的生活一直很悲惨;)

或者是否存在完全不同的,开箱即用的approch来实现这些对象的合并?

我很感激在C#,Java甚至PHP中的回答。

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

  • 无参数构造函数
  • 接受一个IEnumerable<T>参数的构造函数
  • 接受数组和其他修饰符的构造函数

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

4 个答案:

答案 0 :(得分:6)

您对依赖项反转规则和代码重复问题是对的。

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

生成的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 :(得分:1)

已淘汰,保留供参考(并显示我是如何达到最终解决方案的),请参阅编辑下面的代码

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

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

Foo fooAndBar = foo1.merge(bar1);

需要一个额外的泛型类型定义(因此定义类AMyAbstract&lt; T&gt;),以便能够在上面的调用中产生正确的最终类型(而不是仅仅看到AMyAbstract作为fooAndBar的类型)。

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

实际上要真正不可变,列表不应该直接在Items属性中返回,因为它可以被调用者修改(使用新的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();
    }
}

答案 2 :(得分:1)

我参加派对有点晚了,但是你还没有接受答案,我想我会加入自己的答案。

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

我认为有两种方法可以实现:

  1. 公共默认构造函数
  2. 内部Clone模板方法类似于@ naveen上面的回答
  3. 选项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只需要在外部是不可变的。

答案 3 :(得分:0)

基于@ 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的主要目的。

<强>概要

因此,不仅这个解决方案解决了问题中出现的问题,而且提供了上述额外的优势。