Перейти к основному содержимому

Классы и ООП в Dart

Разработчику Архитектору
Сначала — общие понятия (раздел 4 "Код")

Если ООП для вас новое или вы учите Dart с нуля, сначала пройдите материалы без привязки к синтаксису: парадигмы и уровни абстракции, затем ООП — о разделезачем объекты, введение, абстракция, инкапсуляция, наследование, полиморфизм.

Ниже — классы, миксины и интерфейсы в Dart.

Теория и синтаксис Dart

Понятие ООПКак выражено в Dart
Класс, АДТclass; одиночное наследование классов
Инкапсуляция_ для library-private полей
Наследованиеextends; миксины with для горизонтального кода
Интерфейсimplements — контракт без реализации
Полиморфизм подтиповпереопределение @override
Параметрический полиморфизмgenerics (List<T>)

Определения — раздел 4-08-oop.


Классы и ООП в Dart

Dart — объектно-ориентированный язык: каждое значение — объект, корень иерархии — Object. Базовый синтаксис классов уже разобран в основах; здесь — механизмы, которые отличают Dart от "классического" ООП и важны в реальных проектах и во Flutter.

Связанные темы: типы и record, паттерны и sealed, функции и методы.

КЛАСС Кот
поля: имя, возраст
метод мяукнуть()
КОНЕЦ

объект barsik := новый Кот(имя="Барсик", возраст=3)
barsik.мяукнуть()

Разбор:

  • Это псевдокод, который показывает базовую идею класса и экземпляра без привязки к синтаксису Dart.
  • КЛАСС Кот описывает шаблон объекта: какие данные хранит (имя, возраст) и какое поведение имеет (мяукнуть).
  • объект barsik := новый Кот(...) демонстрирует создание экземпляра через конструктор с начальными данными.
  • barsik.мяукнуть() иллюстрирует вызов метода конкретного объекта через точку.

Поля, конструкторы и this

Класс объединяет состояние (поля) и поведение (методы):

class User {
final String id;
String displayName;

User(this.id, this.displayName);

void rename(String name) {
displayName = name;
}
}

Разбор:

  • class User задаёт пользовательский тип с состоянием и методами.
  • final String id; фиксирует идентификатор после инициализации и защищает от случайного изменения.
  • String displayName; оставлен изменяемым, поэтому метод rename может обновлять имя.
  • Конструктор User(this.id, this.displayName); использует сокращённую инициализацию полей параметрами.
  • void rename(String name) инкапсулирует изменение состояния в одном месте класса.

Синтаксис User(this.id, this.displayName) — сокращение: параметры конструктора сразу инициализируют одноимённые поля. Именованные параметры в конструкторе повышают читаемость:

class Config {
final String host;
final int port;

Config({required this.host, this.port = 8080});
}

Разбор:

  • В конструкторе используются именованные параметры в {...}, поэтому при вызове видно, что именно передаётся.
  • required this.host делает host обязательным аргументом.
  • this.port = 8080 задаёт значение по умолчанию, если параметр не указан.
  • Такой стиль снижает риск перепутать порядок аргументов и улучшает читаемость API класса.

Инкапсуляция — библиотека и префикс _

В Dart нет ключевых слов public / private на уровне члена. Приватность — по области видимости библиотеки (файла) — имя, начинающееся с _, видно только внутри того же файла (или части library, если несколько файлов объединены директивой part).

class Account {
final String _id;
double _balance = 0;

Account(this._id);

void deposit(double amount) {
if (amount <= 0) throw ArgumentError.value(amount);
_balance += amount;
}

double get balance => _balance;
}

Разбор:

  • Префикс _ у _id и _balance делает поля приватными в пределах файла.
  • deposit(double amount) реализует проверку входных данных и защищает инвариант баланса.
  • throw ArgumentError.value(amount); явно сигнализирует о некорректном аргументе.
  • Геттер balance даёт внешний read-only доступ к _balance без прямой записи.
  • Это классический пример инкапсуляции: изменение состояния проходит через валидируемый метод.

Снаружи файла к _balance не обратиться — только через публичный API (deposit, balance). Это защищает инварианты объекта без boilerplate геттеров на каждое поле.


late — отложенная инициализация

Ключевое слово late сообщает компилятору: поле будет присвоено до первого чтения, но не обязательно в конструкторе.

class Report {
late final String title;

void load(Map<String, dynamic> json) {
title = json['title'] as String;
}
}

Разбор:

  • late final откладывает инициализацию поля до момента, когда данные реально доступны.
  • Поле title можно присвоить только один раз, после чего оно становится неизменяемым.
  • title = json['title'] as String; показывает типичный разбор JSON с явным приведением типа.
  • Если прочитать title до вызова load, возникнет LateInitializationError.

Варианты:

  • late final — присвоить один раз, затем только читать.
  • late без final — изменяемое поле с отложенным стартом.

Ошибка LateInitializationError возникает, если прочитать late-поле до инициализации. В Flutter late часто применяют для контроллеров (TextEditingController), которые нельзя создать в конструкторе виджета до initState — см. поле ввода в Lab.

Не путать с nullable (String?) — late обещает "значение появится позже, но не null", ? — "может быть null".


final, const и неизменяемые объекты

  • final на поле — ссылку переназначить нельзя; для изменяемых коллекций содержимое всё ещё можно менять, если тип коллекции изменяемый.
  • const конструктор — объект создаётся на этапе компиляции и может быть каноническим (один экземпляр на одинаковые аргументы).
class Point {
final double x;
final double y;

const Point(this.x, this.y);
}

const origin = Point(0, 0);

Разбор:

  • final double x, y делает координаты неизменяемыми после создания точки.
  • const Point(...) позволяет создавать compile-time константы.
  • const origin = Point(0, 0) — канонический экземпляр начала координат.
  • Такие объекты можно безопасно переиспользовать и сравнивать по ссылке.

Все поля класса с const-конструктором должны быть final и тоже допускать const-инициализацию. Во Flutter const виджеты уменьшают пересборки дерева — см. Flutter и Hello Flutter (Lab).


Factory-конструкторы

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

class Logger {
static final Map<String, Logger> _cache = {};

final String name;

factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}

Logger._internal(this.name);
}

Разбор:

  • factory Logger(String name) — точка входа, которая может вернуть уже существующий объект.
  • _cache хранит singleton-экземпляры по имени логгера.
  • putIfAbsent создаёт объект только при первом обращении к ключу.
  • Logger._internal — приватный конструктор, скрытый от внешнего кода.

Типичные случаи factory:

  • кэш и singleton;
  • выбор реализации по аргументу (factory Auth.fromConfig(...)OAuth или Basic);
  • парсинг: factory User.fromJson(Map<String, dynamic> json).

Именованный redirecting factory перенаправляет на другой конструктор:

factory Vector.zero() = Vector.fromValues;

Разбор:

  • Redirecting factory не создаёт объект сам, а перенаправляет вызов на другой конструктор.
  • Vector.zero() становится удобным алиасом для Vector.fromValues.
  • Логика создания остаётся в одном месте, API класса — более читаемым.

Наследование и абстрактные классы

Один класс наследует один суперкласс (extends). Переопределение методов помечают @override:

abstract class Shape {
double area();

void describe() => print('фигура, площадь $area');
}

class Circle extends Shape {
final double radius;
Circle(this.radius);

@override
double area() => 3.14159 * radius * radius;
}

Разбор:

  • abstract class Shape задаёт контракт метода area() без обязательной реализации в базе.
  • describe() может иметь реализацию по умолчанию и переиспользоваться наследниками.
  • Circle extends Shape реализует абстрактный метод через @override.
  • Формула 3.14159 * radius * radius — конкретная реализация поведения подтипа.

abstract class нельзя инстанцировать; методы без тела задают контракт. abstract interface class (Dart 3) — вариант для чистого контракта без реализации по умолчанию.


Интерфейсы без ключевого слова interface

Любой класс задаёт неявный интерфейс — набор публичных методов. Реализация через implements (нужно описать все методы заново) или extends (наследование реализации):

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

class Product implements Serializable {
final String sku;
Product(this.sku);

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

Разбор:

  • abstract class Serializable описывает контракт сериализации.
  • implements Serializable заставляет класс явно реализовать все методы интерфейса.
  • toJson() возвращает Map, пригодный для jsonEncode.
  • Такой паттерн часто используют в API-слое и DTO-моделях.

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


Миксины (mixins)

mixin добавляет поведение без цепочки наследования "от животного к млекопитающему". Подключение — with:

mixin Timestamped {
DateTime? updatedAt;
void touch() => updatedAt = DateTime.now();
}

class Document with Timestamped {
final String title;
Document(this.title);
}

Разбор:

  • mixin Timestamped добавляет поле updatedAt и метод touch().
  • with Timestamped подмешивает поведение в класс Document без наследования.
  • touch() обновляет метку времени последнего изменения.
  • Это композиция поведения вместо глубокой иерархии классов.

Ограничение on задаёт, к каким классам можно примешивать mixin:

mixin Pilot on Vehicle {
void fly() => print('взлёт');
}

Разбор:

  • mixin Pilot on Vehicle можно применять только к классам, наследующим Vehicle.
  • Ограничение on защищает от некорректного подключения mixin к "неподходящему" типу.
  • Внутри mixin можно безопасно вызывать API базового класса Vehicle.

Миксины — основа композиции в Dart; не злоупотребляйте глубокими иерархиями extends.


Геттеры, сеттеры и операторы

Свойства могут быть вычисляемыми:

class Rectangle {
final double width, height;
Rectangle(this.width, this.height);

double get area => width * height;
set scale(double factor) {
// в реальном классе поля должны быть изменяемыми или через внутреннее состояние
}
}

Разбор:

  • get area вычисляет значение на лету из width и height.
  • set scale показывает синтаксис сеттера; в реальном классе здесь меняют внутреннее состояние.
  • Геттер/сеттер позволяют обращаться к логике как к "свойству" объекта.

Операторы переопределяют через operator:

class Vector {
final int x, y;
const Vector(this.x, this.y);

Vector operator +(Vector other) => Vector(x + other.x, y + other.y);
}

Разбор:

  • operator + переопределяет операцию сложения для пользовательского типа.
  • Vector(x + other.x, y + other.y) создаёт новый объект-результат (иммутабельный стиль).
  • После такого переопределения можно писать v1 + v2 вместо v1.add(v2).

Не забывайте согласованно переопределять == и hashCode, если объекты сравнивают по значению (как для коллекций в Map/Set).


Sealed и final class (Dart 3)

sealed class — закрытая иерархия для исчерпывающего switch. final class запрещает наследование снаружи файла. base / interface — тонкая настройка, кто может расширять или реализовывать тип. В прикладном коде чаще всего встречаются sealed для моделей состояния и final для "листьев" иерархии.


Область видимости

Помимо _ на уровне библиотеки:

  • топ-уровневые функции и переменные в файле — видны всему файлу;
  • локальные — внутри блока if, цикла, метода;
  • статические члены класса — static, принадлежат типу, не экземпляру.

Вложенные функции (как в статье про функции) видят переменные внешней функции — основа замыканий.


ООП и исключения

Методы бросают исключения через throw; перехват — try/catch. Собственный тип:

class ValidationException implements Exception {
final String field;
ValidationException(this.field);

@override
String toString() => 'ошибка поля $field';
}

Разбор:

  • implements Exception помечает тип как исключение Dart.
  • Поле field хранит имя проблемного поля формы или DTO.
  • toString() задаёт человекочитаемое сообщение при выводе/логировании ошибки.
  • Такие типы удобно перехватывать отдельной веткой on ValidationException.

Dart не объявляет checked exceptions в сигнатуре — контракт ошибок документируют в комментариях и тестах.


Сводка — что запомнить

МеханизмЗачем
_Скрыть детали реализации в файле
lateИнициализация после конструктора
const / finalПредсказуемость и производительность
factoryКэш, подтипы, fromJson
abstractКонтракт без экземпляра
implements / withКонтракт и миксины без лишнего наследования
sealedЗакрытый набор вариантов для switch

Дальше: паттерны для разбора вариантов данных, консоль и HTTP для сервисного слоя, чек-лист для самопроверки.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.