DartMiddleCoding

Как реализовать интерфейсы в Dart (с помощью implements)?

В Dart интерфейсами служат обычные и абстрактные классы; implements обязывает реализовать все публичные члены без наследования кода. Dart 3 добавил interface class для явного запрета наследования. Один класс может реализовывать несколько интерфейсов через запятую.

Интерфейсы в Dart через implements

В Dart нет отдельного ключевого слова interface. Роль интерфейсов играют обычные классы и абстрактные классы: любой класс автоматически экспортирует свой публичный контракт как неявный интерфейс. Ключевое слово implements обязывает реализующий класс предоставить собственную реализацию всех публичных членов без наследования кода из оригинала.

Объявление интерфейса через abstract class

Стандартный подход — объявить абстрактный класс без реализации методов. Dart 3 добавил ключевое слово interface class для явного обозначения классов, предназначенных только для реализации.

// Традиционный способ (Dart 2 и 3)
abstract class Repository<T> {
  Future<T?> findById(String id);
  Future<List<T>> findAll();
  Future<void> save(T entity);
  Future<void> delete(String id);
}

// Явный интерфейс (Dart 3+)
interface class Logger {
  void log(String message) {}
  void error(String message) {}
}

Реализация интерфейса

class User {
  final String id;
  final String name;
  User(this.id, this.name);
}

class InMemoryUserRepository implements Repository<User> {
  final _store = <String, User>{};

  @override
  Future<User?> findById(String id) async => _store[id];

  @override
  Future<List<User>> findAll() async => _store.values.toList();

  @override
  Future<void> save(User entity) async {
    _store[entity.id] = entity;
  }

  @override
  Future<void> delete(String id) async {
    _store.remove(id);
  }
}

void main() async {
  final repo = InMemoryUserRepository();
  await repo.save(User('1', 'Alice'));
  await repo.save(User('2', 'Bob'));

  final users = await repo.findAll();
  print(users.map((u) => u.name)); // (Alice, Bob)

  final alice = await repo.findById('1');
  print(alice?.name); // Alice
}

Реализация нескольких интерфейсов

Класс может реализовывать произвольное количество интерфейсов через запятую — это основной механизм множественного полиморфизма в Dart.

abstract class Printable {
  String toPrintString();
}

abstract class Serializable {
  Map<String, dynamic> toJson();
  void fromJson(Map<String, dynamic> json);
}

class Product implements Printable, Serializable {
  String name;
  double price;

  Product(this.name, this.price);

  @override
  String toPrintString() => '$name: \$$price';

  @override
  Map<String, dynamic> toJson() => {'name': name, 'price': price};

  @override
  void fromJson(Map<String, dynamic> json) {
    name = json['name'] as String;
    price = (json['price'] as num).toDouble();
  }
}

void main() {
  final p = Product('Laptop', 999.99);
  print(p.toPrintString());    // Laptop: $999.99
  print(p.toJson());           // {name: Laptop, price: 999.99}

  // Полиморфизм по интерфейсу:
  Printable printable = p;
  print(printable.toPrintString());
}

interface class в Dart 3

Модификатор interface class запрещает наследовать класс через extends из другого библиотечного файла — можно только реализовывать. Это гарантирует стабильность контракта.

// В файле auth_service.dart
interface class AuthService {
  Future<bool> login(String email, String password) =>
      Future.value(false);
  Future<void> logout() async {}
}

// В другом файле — можно только implements, не extends
class FirebaseAuthService implements AuthService {
  @override
  Future<bool> login(String email, String password) async {
    // реальная логика Firebase
    return true;
  }

  @override
  Future<void> logout() async {
    // логика выхода
  }
}

Подводные камни

  • При implements нужно реализовать абсолютно все публичные члены: геттеры, сеттеры, операторы, константы — пропустить их нельзя, иначе ошибка компиляции.
  • Реализация класса, у которого есть конструктор с обязательными параметрами, требует собственного конструктора — Dart не унаследует его автоматически при implements.
  • Приватные члены (_field) не входят в интерфейс и недоступны реализующему классу из другого файла.
  • Если реализуемый класс изменит публичный API (добавит метод), все реализации сломаются — нет default-реализаций как в Java 8+ default методах (если только не использовать миксины).
  • При реализации нескольких интерфейсов с методами одинакового имени но разными сигнатурами возникает конфликт, который нельзя разрешить — только один метод можно объявить.
  • Аннотация @override не обязательна синтаксически, но её отсутствие скрывает намерение и затрудняет рефакторинг — всегда добавляйте её.
  • Тест через is работает для реализованных интерфейсов, но runtimeType вернёт конкретный класс, а не интерфейс — не путайте их при отладке.

Common mistakes

  • Сводить «реализовать интерфейсы в Dart (с помощью implements)» к синтаксису и не объяснять sound type system.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии dart-15.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «реализовать интерфейсы в Dart (с помощью implements)» и подтверждает ее корректным примером.
  • Умеет связать ответ с event loop, тестированием и отладкой на устройстве.
  • Называет ограничения подхода dart-15, включая производительность, память и сопровождение.

Sources

Related topics