Способ обхода инстанцирования подклассов в суперклассе


У меня есть базовый абстрактный класс, который агрегирует кучу элементов в коллекции:

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

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

Существует множество подклассов, назовем их Foo, Bar, Baz, и т.д. Все онинеизменны . Теперь мне нужен метод 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() в каждом подклассе является лотом повторения кода (сухое правило).

Задача 3: Извлечение логики merge() в совершенно новый класс не решает проблема сухого правила. Даже используя шаблон посетителя, это много копирования / вставки.

Проблемы, представленные выше, исключают любую идею реализации, которая могла бы быть у меня до того, как я прочитал о SOLID. (с тех пор моя жизнь стала несчастной;) Или существует совершенно другой, нестандартный подход для достижения слияния таких объектов?

Я был бы признателен за ответ на C#, Java или даже PHP.

EDIT: я думаю, что пропустил часть допустимого информация: событие Хотя существует множество различных подклассов, они могут (должны) быть построены только двумя, возможно тремя способами (как следствие принципа единой ответственности):

  • конструктор без параметров
  • конструктор, принимающий один аргумент IEnumerable<T>
  • конструктор, который принимает массив и некоторый другой модификатор

Это поместило бы шаблон посетителя обратно на таблицу , если бы я мог наложить ограничение на конструкторы - например, путем определения конструктора в интерфейсе. Но это возможно только в PHP. В Java или C# подпись конструктора не может быть принудительно применена, поэтому я не могу быть уверен в том, как я буду создавать экземпляр подкласса. Это хорошее правило вообще, потому что никогда нельзя было предсказать, как автор подкласса хотел бы, чтобы объект был построен, но в данном конкретном случае это могло бы быть полезно. Таким образом, вспомогательный вопрос был бы: могу ли я каким-то образом принудить к тому, как класс экземпляр ? Шаблон строителя звучит слишком много в этом простом случае, или это так?

4 3

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

Надеюсь, это поможет..

Устаревший , сохраненный для справки (и показывающий, как я пришел к окончательному решению), см. код после EDIT ниже

Я бы сказал, что шаблон строителя - это путь, по которому нужно идти. Нам просто нужен конструктор, который сохраняет экземпляр, но изменяет одно поле, которое должно быть изменено.

Если вы хотите получить (как показано в вашем коде)

Foo fooAndBar = foo1.merge(bar1);

Необходимо дополнительное определение универсального типа (таким образом, определяя класс AMyAbstract ), чтобы иметь возможность все еще производить правильный окончательный тип (вместо того, чтобы просто видеть AMyAbstract как тип для fooAndBar) в приведенном выше вызове.

Примечание: метод merge был переименован в MergeItems в приведенном ниже коде, чтобы прояснить, что такое слияние. Я указал разные конструкторы для Foo и Bar, чтобы было понятно, что они не должны иметь одинаковое количество параметров.

На самом деле, чтобы быть действительно неизменяемым, список не должен быть непосредственно возвращен в свойстве Items, так как он может быть изменен вызывающим (используя new Элемент списка).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();
    }
}

EDIT

Возможно, более простым решением было бы избежать класса builder, а вместо этого иметь

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

Я немного опоздал на вечеринку, но так как вы еще не приняли ответ, я подумал, что добавлю свой собственный.

Одним из ключевых моментов является то, что коллекция должна быть неизменяемой. В моем примере я показал IEnumerable, чтобы облегчить это-коллекция элементов неизменна вне экземпляра.

Есть 2 способа, которыми я вижу эту работу:

  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 должен быть неизменным только снаружи.

Аккуратное решение, основанное на комментарии @AK_ :

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

Еще одно преимущество, которое мы получаем: разложение о проблеме слияния всего объекта. Поэтому вместо этого мы разбиваем его на небольшие проблемы (объединяя только агрегированный список в данном случае).

3) и теперь мы можем создать объединенный объект, используя любой специализированный конструктор, который мы могли бы подумайте о:

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

Вместо возврата нового экземпляра всего объекта мы возвращаем только Объединенный список, который в свою очередь может быть использован для создания объекта целевого класса. Здесь мы получаем еще одно преимущество: отложенная инстанциация объекта , которая является основной цельютворческих паттернов .

Резюме:

Таким образом, это решение не только решает проблемы, поставленные в вопросе, но и обеспечивает дополнительные преимущества, представленные выше.