Как написать абстрактный конструктор класса, чтобы он был гибким для расширения в подклассах

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

abstract class Stack<T> {
  factory Stack.empty() => const _EmptyStack._();

  T get data;
  Stack<T> get bottom;
  bool get isEmpty;
  Stack<T> put(T item) => new _StackImpl(item, this);
}

class _StackImpl<T> extends Stack<T> {
  final T _data;
  final Stack<T> _bottom;

  _StackImpl(T this._data, Stack<T> this._bottom);

  T get data => _data;
  Stack<T> get bottom => _bottom;
  bool get isEmpty => false;
}

class _EmptyStack<T> extends Stack<T> {
  const _EmptyStack._();
  T get data => throw new CollectionIsEmpty();
  Stack<T> get bottom => throw new CollectionIsEmpty();
  bool get isEmpty => true;
}

Этот код вызывает две ошибки в конкретных реализациях:

[error] The class 'Stack' does not have a default generative constructor

Я нашел пример кода, который, кажется, решает эту проблему здесь, поэтому я исправил его, поместив конструктор без параметров в Stack<T> учебный класс:

abstract class Stack<T> {
    Stack();
    // ...

но теперь это вызывает проблемы с _EmptyStack<T> конструктор, который является постоянным:

Constant constructor cannot call non-constant super constructor of 'Stack<T>'

Дополнительно добавил Stack() Конструктор не позволяет использовать класс в качестве mixin.

Похоже, что эти ограничения заставляют автора думать о том, как будет расширен класс. Способ расширения List класс от dart:collection пакет, кажется, подтверждает этот вывод - для расширения есть целый отдельный класс, я не могу напрямую расширить List сам класс.

Мой вопрос более общий, чем проблема, описанная выше: как я могу написать класс, чтобы он мог быть достаточно гибким для расширения? Это включает в себя разрешение на использование таких функций, как:

  1. фабричные конструкторы в суперклассе
  2. нормальные конструкторы в подклассе
  3. const конструкторы в подклассе
  4. использоваться как миксин

Хотя я понимаю, что использование в качестве миксина может быть невозможным или даже нежелательным, другие пункты по-прежнему актуальны. Наиболее важный вопрос стоит: почему я не могу extend класс с фабричным конструктором? Это поведение отличается от любого другого языка OO, с которым я знаком.

Также связанные вопросы:

РЕДАКТИРОВАТЬ: Благодаря Günter Zöchbauer ответ я улучшил код, так что теперь он полностью работоспособен (см. Ниже). Самый важный вопрос, который я сейчас оставил: почему фабричный конструктор лишает возможности расширять класс? И как обойти это (кроме использования базового класса в качестве интерфейса)? Более простой пример, чтобы сделать точку:

class Base {
}

class _Sub extends Base {
  int someValue;
  _Sub(int this.someValue);
}

Все хорошо с этим кодом. Но скажем, я вернусь к своему Base класс во времени и хотите добавить фабричный метод:

class Base {
    factory Base.empty() => new _Sub(0);
}

Теперь каждый класс, который расширяется Base сломан из-за unresolved implicit call to super constructor, Что мне тогда делать?

Исправлен код из оригинального вопроса для справки:

abstract class Stack<T> {
  const Stack._();
  factory Stack.empty() => const _EmptyStack._();

  T get data;
  Stack<T> get bottom;
  bool get isEmpty;
  Stack<T> put(T item) => new _StackImpl(item, this);
}

class _StackImpl<T> extends Stack<T> {
  final T _data;
  final Stack<T> _bottom;

  _StackImpl(T this._data, Stack<T> this._bottom) : super._();

  T get data => _data;
  Stack<T> get bottom => _bottom;
  bool get isEmpty => false;
}

class _EmptyStack<T> extends Stack<T> {
  const _EmptyStack._() : super._();
  T get data => throw new CollectionIsEmpty();
  Stack<T> get bottom => throw new CollectionIsEmpty();
  bool get isEmpty => true;
}

void main(){

  group('stack', (){

    test('empty stack', (){
      var emptyStack = new Stack.empty();
      expect(emptyStack.isEmpty, isTrue);
      expect(() => emptyStack.data, throwsA(new isInstanceOf<CollectionIsEmpty>()));
      expect(() => emptyStack.bottom, throwsA(new isInstanceOf<CollectionIsEmpty>()));

      var emptyStack2 = new Stack.empty();
      expect(emptyStack == emptyStack2, isTrue);
    });

    test('adding to stack', (){

      var stack = new Stack<String>.empty().put("a").put("b").put("c");

      expect(stack.data, equals('c'));
      expect(stack.bottom.data, equals('b'));
      expect(stack.bottom.bottom.data, equals('a'));

    });

  });

}

1 ответ

В вашем примере я предлагаю просто использовать Stack в качестве интерфейса вместо базового класса.

  1. фабричные конструкторы в суперклассе Если у вас есть фабричный конструктор, вам также нужно добавить нормальный конструктор, если вы хотите расширить его, как указано в ответе на связанный вопрос.

  2. нормальные конструкторы в подклассе Каков был реальный вопрос здесь? Я думаю, это то же самое, что 1.

  3. Конструкторы const в подклассе Если вы хотите использовать конструктор const, все подклассы также должны иметь конструктор const. В классе с конструктором const все поля должны быть окончательными. Это не относится к вашему базовому классу, поэтому где смысл добавлять конструктор const к _EmptyStack,

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

Другие вопросы по тегам