Способ обхода инстанцирования подклассов в суперклассе
У меня есть базовый абстрактный класс, который агрегирует кучу элементов в коллекции:
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()
в совершенно новый класс не решает проблема сухого правила. Даже используя шаблон посетителя, это много копирования / вставки.
Я был бы признателен за ответ на C#, Java или даже PHP.
EDIT: я думаю, что пропустил часть допустимого информация: событие Хотя существует множество различных подклассов, они могут (должны) быть построены только двумя, возможно тремя способами (как следствие принципа единой ответственности):
- конструктор без параметров
- конструктор, принимающий один аргумент
IEnumerable<T>
- конструктор, который принимает массив и некоторый другой модификатор
Это поместило бы шаблон посетителя обратно на таблицу , если бы я мог наложить ограничение на конструкторы - например, путем определения конструктора в интерфейсе. Но это возможно только в PHP. В Java или C# подпись конструктора не может быть принудительно применена, поэтому я не могу быть уверен в том, как я буду создавать экземпляр подкласса. Это хорошее правило вообще, потому что никогда нельзя было предсказать, как автор подкласса хотел бы, чтобы объект был построен, но в данном конкретном случае это могло бы быть полезно. Таким образом, вспомогательный вопрос был бы: могу ли я каким-то образом принудить к тому, как класс экземпляр ? Шаблон строителя звучит слишком много в этом простом случае, или это так?
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 способа, которыми я вижу эту работу:
- открытый конструктор по умолчанию
- внутренний
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));
Вместо возврата нового экземпляра всего объекта мы возвращаем только Объединенный список, который в свою очередь может быть использован для создания объекта целевого класса. Здесь мы получаем еще одно преимущество: отложенная инстанциация объекта , которая является основной цельютворческих паттернов .
Резюме:
Таким образом, это решение не только решает проблемы, поставленные в вопросе, но и обеспечивает дополнительные преимущества, представленные выше.