Классы и ООП в Dart
Если ООП для вас новое или вы учите 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 как основа веб-интеграций.