Наследование, миксины и абстрактные классы
Наследование позволяет строить новые классы на основе существующих, не переписывая общий код заново.
Суть:
extendsнаследует поведение,@overrideменяет его, абстрактные классы задают контракт без реализации, а миксины (with) добавляют возможности нескольким классам сразу.
Полиморфизм — третий кит ООП после инкапсуляции и наследования — заслуживает отдельного слова. Он означает, что переменная типа Animal может хранить любого потомка, и вызов animal.sound() выберет нужную реализацию автоматически, в зависимости от реального типа объекта. Благодаря этому вы пишете код, работающий с абстракцией, а конкретику подставляет среда выполнения. Это основа того, почему Flutter оперирует типом Widget, а на экране оказываются ваши конкретные виджеты.
Если у вас есть классы Cat и Dog, оба умеют дышать, есть и спать. Дублировать этот код в каждом — расточительно. Наследование выносит общее в родительский класс Animal, а потомки добавляют только своё. Так строится вся иерархия виджетов Flutter: StatelessWidget и StatefulWidget наследуют от общего Widget.
class Animal {
final String name;
Animal(this.name);
String sound() => '...';
void describe() => print('$name говорит ${sound()}');
}
class Dog extends Animal {
Dog(String name) : super(name); // вызов конструктора родителя
@override
String sound() => 'Гав'; // переопределяем поведение
}
void main() {
Dog('Рекс').describe(); // Рекс говорит Гав
}
Аннотация @override сообщает, что метод переопределяет родительский. super обращается к родителю — например, чтобы вызвать его конструктор или метод.
Как работают абстракции и миксины под капотом
Абстрактный класс (abstract class) нельзя создать напрямую — он лишь задаёт контракт: «у всех потомков должен быть метод sound()», но как именно — решает потомок. Миксин (mixin ... with) — это горизонтальное переиспользование: набор методов, который можно «подмешать» к любому классу, даже не родственному по иерархии.
Animal (абстрактный контракт)
/ \
Dog Cat наследование (extends): "является"
|
+ with Swimmer миксин: "умеет плавать"
|
класс Dog получил метод swim() от миксина,
не наследуя весь класс Swimmer
abstract class Shape {
double area(); // контракт без тела
}
class Circle extends Shape {
final double r;
Circle(this.r);
@override
double area() => 3.14159 * r * r;
}
mixin Swimmer {
void swim() => print('Плыву');
}
class Duck extends Animal with Swimmer {
Duck(String name) : super(name);
@override
String sound() => 'Кря';
}
# Аналог наследования и переопределения на Python
class Animal:
def __init__(self, name):
self.name = name
def sound(self):
return '...'
def describe(self):
print(f'{self.name} говорит {self.sound()}')
class Dog(Animal):
def sound(self): # override
return 'Гав'
class Cat(Animal):
def sound(self):
return 'Мяу'
for a in [Dog('Рекс'), Cat('Мурка')]:
a.describe() # каждый звучит по-своему — полиморфизм
Частые ошибки
- Забыть
@overrideпри переопределении — код часто работает, но теряется проверка совпадения сигнатур. - Пытаться создать объект абстрактного класса — это запрещено, абстракция только для наследования.
- Глубокие иерархии наследования. Пять уровней
extends— путь к хрупкому коду; чаще лучше композиция или миксины.
Best practices
- Предпочитайте композицию наследованию: Flutter строит интерфейс из вложенных виджетов, а не из длинных цепочек
extends. - Используйте миксины для общих возможностей (логирование, плавание) без жёсткой иерархии.
- Абстрактные классы задавайте, когда нужен единый контракт для группы классов.
Dart 3 добавил тонкий контроль над наследованием через модификаторы классов: final запрещает наследование вовсе, base требует наследования без реализации интерфейса, interface наоборот. Эти инструменты нужны авторам библиотек, чтобы задать чёткие правила использования своих классов. Новичку их знать не обязательно, но полезно понимать: если компилятор жалуется на запрещённое наследование, скорее всего, автор класса сознательно его закрыл.
Итог: наследование, абстракции и миксины — это инструменты переиспользования. Во Flutter вы будете больше композировать виджеты, чем наследоваться, но понимание иерархии классов необходимо, чтобы читать исходники фреймворка.