Интерфейсы

Последнее обновление: 03.12.2023

Наследование в языке Dart имеет важное ограничение: мы не можем наследовать класс сразу от нескольких классов, например, следующим образом:

class Worker{
    String company = "Necrosoft";
    Worker(this.company);
    void work()=>print("works in $company");
}
class Student{
    String university = "Stanford";
    Student(this.university);
    void study() =>print("studies in $university");
}
class WorkingStudent extends Worker, Student{ // ! Ошибка - множественное наследование не допускается
}

Здесь у нас есть два класса - Worker (класс работающего) и класс Student (класс студента). Но что, если мы хотим определить класс работающего студента, например, WorkingStudent, который бы использовал функционал обоих классов. Просто унаследовать класс WorkingStudent от классов Worker и Student мы не можем, так как Dart не поддерживает множественное наследования.

Для решения этой проблемы в Dart применяется реализация интерфейсов. При этом важно понимать, что интерфейс - это не отдельная сущность, как в некоторых языках программирования (например, тип interface в C# или Java), а тот же класс. То есть класс в Dart одновременно выступает в роли интерфейса, и другой класс может реализовать данный интерфейс.

Интерфейс представляет синтаксический контракт, которому должны следовать реализующие этот интерфейс классы. То есть, если класс-интерфейс определяет какие-нибудь поля и методы, то класс, реализующий данный интерфейс, должен также определить эти поля и методы.

Для реализации интерфейсов применяется оператор implements, после которого указывает класс реализуемого интерфейса:

class Worker{
    String company = "Microsoft";
    Worker(this.company);
    void work()=>print("works in $company");
}
class Student{
    String university = "Stanford";
    Student(this.university);
    void study() =>print("studies in $university");
}
class WorkingStudent implements Worker, Student{
    
    String name;
    @override
    String company;
    @override
    String university;
    WorkingStudent(this.name, this.university, this.company);
    @override
    void study() =>print("$name studies in $university");
    @override
    void work()=>print("$name works in $company");
}

void main (){

    WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google");
    tom.study();    // Tom studies in MIT
    tom.work();     // Tom works in Google
}

В данном случае класс WorkingStudent применяет интерфейс классов Worker и Student:

class WorkingStudent implements Worker, Student

Применение интерфейса означает, что класс WorkingStudent должен реализовать все поля и методы, которые определены в классах Worker и Student. Поэтому класс WorkingStudent должен определить поле company и метод work класса Worker и поле university и метод study класса Student. По сути интерфейс - это контракт, что класс должен содержать определенный функционал (в данном случае поля и методы). Если класс WorkingStudent не определил бы все поля и методы, которые есть в классах Worker и Student, то мы столкнулись бы с ошибкой.

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

Реализация интерфейса также означает, что в любом месте программы, где требуется объект Worker или объект Student, мы можем использовать объект WorkerStudent, то есть работающий студент одновременно это и работающий и студент. Например:

void makeWork(Worker worker){
    worker.work();
}
void makeStudy(Student student){
    student.study();
}

void main (){

    WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google");
    // вместо Worker можно передать WorkingStudent
    makeWork(tom);      // Tom works in Google
    // вместо Student можно передать WorkingStudent
    makeStudy(tom);     // Tom studies in MIT
}

Совмещение наследование с реализацией

Причем мы можем комбинировать наследование и реализацию интерфейсов. Например, мы решили, что класс WorkingStudent все таки будет наследовать класс Student, а не реализовать его интерфейс:

class Worker{
    String company = "Microsoft";
    Worker(this.company);
    void work()=>print("works in $company");
}
class Student{
    String university = "Stanford";
    Student(this.university);
    void study() =>print("studies in $university");
}
class WorkingStudent extends Student implements Worker{
    
    String name;
    @override
    String company;
    WorkingStudent(this.name, super.university, this.company);
    @override
    void study() =>print("$name studies in $university");
    @override
    void work()=>print("$name works in $company");
}

void main (){

    WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google");
    tom.study();    // Tom studies in MIT
    tom.work();     // Tom works in Google
}

Теперь класс WorkingStudent наследуется от Student и реализует интерфейс класса Worker:

class WorkingStudent extends Student implements Worker{

В этом случае нам не требуется определять в классе WorkingStudent поле university, так как оно унаследовано от базового класса. Также можно было бы не определять метод study, но в данном случае оно переопределено, чтобы учитывать имя работающего студента.

Но поскольку в Student определен конструктор, то при определении конструктора WorkingStudent нам надо вызвать конструктор базового класса:

WorkingStudent(this.name, super.university, this.company);

Выражение super.university позволяет передать соответствующее значение для параметра university конструктора Student.

Подобным образом через слово super мы можем обращаться к функционалу базового класса. А к фукционалу класса-интерфейса через super мы обращаться не можем.

Стоит отметить, что начиная с версии Dart 3.0 в язык было добавлено ключевое слово interface. Класс, определенный со словом interface, может быть унаследован только в рамках своей библиотеки. Вне своей библиотеки он может быть только реализован как интерфейс.

interface class Worker{
    String company;
    Worker(this.company);
    void work()=>print("works in $company");
}
class Manager implements Worker{
     
    String name;
    @override
    String company;
    Manager(this.name, this.company);
    @override
    void work()=>print("$name works in $company");
}
 
void main (){
 
    var tom = Manager("Tom", "Google");
    tom.work();     // Tom works in Google
}

Абстрактные интерфейсы

В примерах выше классы-интерфейсы определяют некоторую реализацию по умолчанию - определяют переменные, в методах определены некоторые действия. Однако это не всегда нужно. Иногда интерфейс нужен просто, чтобы определить некоторый функционал без реализации и обозначить, что данный тип имеет определенный набор методов. В этом случае мы можем определить интерфейсы как абстрактные:

abstract interface class Worker{
    void work();
}
abstract interface class Student{
    void study();
}
class WorkingStudent implements Worker, Student{

    String name;
    String university;
    String company;
    WorkingStudent(this.name, this.university, this.company);
    @override
    void study() =>print("$name studies in $university");
    @override
    void work()=>print("$name works in $company");
}

void main (){

    WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google");
    studentStudy(tom);
    workerWork(tom);
}
void studentStudy(Student st){
    st.study();
}
void workerWork(Worker w){
    w.work();
}

В данном случае Worker не определяет реализации для метода work. Аналогично интерфейс Student не определяет реализацию метода study. Данные методы абстрактные и реализуются классом WorkingStudent.

Наследование классов vs реализация интерфейсов

При наследовании производный класс не обязан определять те же поля и методы, которые есть в базовом классе (за исключением абстрактных методов). Если в базовом классе определяется конструктор, то производный класс обязан определить свой конструктор, при котором вызывается конструктор базового класса. В производном классе мы можем обращаться к реализации базового класса с помощью ключевого слова super. Не поддерживается множественное наследование.

При реализации интерфейса производный класс должен определить все поля и методы, которые определены в классе интерфейса. Если в базовом классе есть конструктор, то производный класс НЕ обязан определять свой конструктор. В производном классе мы НЕ можем обращаться к методам реализованного интерфейса с помощью ключевого слова super. Поддерживается множественная реализация интерфейсов.

Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850