ООП в Dart

от автора

Йоу, юзер! Меня зовут Костя, я являюсь Flutter‑разработчиком в стартапе ANTEI. Данная статья относится к циклу статей «База джуна на Flutter». Если ты не видел начальную ноду — ознакомиться можешь тут, а если уже видел — погнали дальше!

Тележка

Сегодня ты узнаешь

  1. Что такое парадигмы и почему новых больше не будет

  2. Что такое ООП

  3. Анатомия ООП — классы и поля

  4. Required or not required? This is the question

  5. Анатомия ООП — конструктор

  6. Анатомия ООП — объект класса

  7. Анатомия ООП — методы

  8. Инкапсуляция, как способ разделения сущностей

  9. Заприватим, загетим и засетим!

  10. Наследники есть?

  11. Абстракция «по‑оопшному

  12. Полиморфизм = многообразие

Что такое парадигма и почему новых больше не будет

Прежде чем нырять в недра ООП, сперва стоит понять, что же значит слово «парадигма» в словосочетании «парадигма ООП».

Если искромётно бросаться терминами, то:

Парадигма — это совокупность идей и понятий, определяющих стиль написания компьютерных программ, а также способ концептуализации, определяющий организацию вычислений и бла‑бла‑бла

Но ты то тут за лайтовыми объяснениями, поэтому интерпретирую этот термин иначе:

Парадигмы программирования — это совокупность правил и договоренностей между программистами для написания кода концептуально похоже.

Иными словами, представь, что программирование — это стройка небоскреба, а парадигмы — подходы к этой стройке. Строители коллективно собрались, и такие: «Мужики, давайте‑ка делать схожим образом, хорошо? А то вот Леха вчера начал ставить кирпичи вертикально, мол ему в таком случае проще их носить до места укладки. Михалыч балки кидает горизонтально — машина ему так привозит. Саня вообще откосы диагонально положил. Не, мужики, я то все понимаю, стены и так и сяк стоят, но емае, давайте‑ка договоримся хоть…»

По мнению Боба Мартина (умный дядька, советую его книжки к прочтению), парадигмы забирают некоторые возможности языка, чтобы вынудить разработчика придерживаться определенного стиля, и это правда, в чем мы чуть позже с тобой убедимся.

Для предотвращения путаниц уточню, что парадигмы делятся на 2 вида: императивные и декларативные:

  1. Структурное программирование, ООП (относятся к императивным)

  2. Функциональное программирование (относится к декларативному)

Однако в данный момент нас это не интересует. Пусть будет для общего сведения.

Из моей интерпретации термина может возникнуть сразу 2 закономерных вопроса:

  1. «ПарадигмЫ? Их что, много что ли?»

Да, парадигм несколько. Если быть точным — всего 3:

  1. Структурное программирование

  2. ООП (объектно‑ориентированное программирование)

  3. Функциональное программирование

Все по тому же Бобу Мартину, каждая из парадигм что‑то забирает и не дает ничего взамен. Структурное программирование забирает оператор goto, и накладывает ограничение на прямую передачу управления. Функциональное программирование забирает возможность присваивания и накладывает такое же ограничение. Объектно‑ориентированное программирование накладывает ограничение на косвенную передачу управления. Можно ли забрать у программиста что‑то еще? Наверное нет. А раз больше ничего забрать нельзя, то и парадигм новых не предвидится.

  1. «Если эти парадигмы объединяют между собой программистов разных мастей и языков, значит ли это, что парадигма применима не к какому‑то конкретному ЯП, а сразу ко всем?»

Да, парадигмы относятся к программированию в целом, а не к какому то конкретному языку. Как мы уже с тобой поняли, парадигма — это свод подходов к написанию кода. Чтобы осознать этот момент, ты можешь провести параллель с обычным языком (английским, русским, шведским и так далее) и правилами построения предложений: языки между собой различаются, однако предложения в них строятся схожим образом. Точно также работают и парадигмы: они не привязаны к конкретному языку, а всецело применимы к программированию.

Резюмируя: парадигмы в программировании — это про то, каким образом разрабатываются программы и про то, как они выглядят.

Что такое ООП

Теперь мы готовыприкоснуться к этим трем буквам. ООП — что за птица такая?

ООП (объектно‑ориентированное программирование) — парадигма разработки, в которой присутствуют объекты и присутствуют классы, к которым эти объекты относятся.

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

Как я уже сказал, в данной парадигме присутствуют объекты, и это, пожалуй, самое важное. Объектом принято называть экземпляр (иначе говоря, переменная с типом класса, объект которого мы хотим создать) класса, к которому этот объект относится. Итого получаем, что ООП — это один из способов создания программ с использованием объектов. Вот и все, ничего сложного.

Анатомия ООП — класс и поля

Теперь мы готовы переходить к тому, из чего это ООП состоит.

Представь, что ты владелец интернет-магазина. У тебя есть личный кабинет, а также бесчисленное множество карточек товаров, которые содержат в себе названия, описания, цену, рейтинг, а также определенные действия, которые можно совершать с товаром. Как бы в коде ты реализовал такую карточку?

Здесь как нельзя кстати подойдет ООП. Представим товар в виде класса. Для его объявления в Dart существует ключевое слово class, после которого идет название создаваемого класса и фигурные скобки:

class Item{}

Класс — некоторый шаблон кода, по которому создается объект. Отдаленно можно сравнить с инструкцией по сборке мебели, где сама инструкция ничего не делает, однако на ее основе можно создать новый объект — ту самую мебель.

Супер! Мы договорились, что наш товар должен иметь название, описание, цену и рейтинг. Давай добавим эту информацию в класс Item:

class Item {   String title;   String description;   int price;   int rating; }

Переменные внутри класса называются полями класса.

Если ты сейчас степ-бай-степ повторяешь за мной, то ты видишь, что твоя IDE подчеркивает красным только что созданные поля, и это неспроста. Дело в том, что в Dart мы не можем просто так сказать, что в нашем классе будет поле A, B и C. Подумай, а что будет, если ты создаешь этот объект без указания цены, названия и прочих вещей, указанных в классе? Будет ошибка, поскольку мы явно говорим, что поля у нас ненулевые, а при создании объекта мы присваиваем им null (ведь мы ничего не передаем в поля, но об этом позже). Как же избежать эту ситуацию?

Required or not required? This is the question

У нас есть аж 3 выхода из этой ситуации. Все верные, и все зависят исключительно от наших потребностей.

Итак, путь 1 — использовать nullable поля класса:

class Item {   int? price; }

Этот финт ушами позволяет сказать, что теперь поле price — опционально. Если мы укажем ему значение, скажем, 10, то цена товара будет равна 10. Или мы можем вообще ничего не указывать, и значение цены будет равно null. Такой прием может быть полезен, когда мы не можем располагать данными для определенных полей.

Путь 2 — использовать ключевое слово required в конструкторе класса. С самим конструктором класса мы разберемся позже. Главное, что необходимо сейчас — ключевое слово required:

class Item {   int price;   Item({required this.price}); }

Как ты можешь заметить, данный пример отличается от предыдущего двумя вещами:

  1. Теперь поле класса price ненулевое;

  2. В конструкторе используем ключевое слово required.

С первым пунктом, думаю, все понятно — мы точно знаем, что у поля всегда будет значение и оно никогда не будет равно null. Во втором пункте мы говорим, что,  друг, давай-ка ты обязательно задай параметр полю price при создании объекта товара, иначе объект создать нельзя. Тем самым мы гарантируем, что у каждого объекта товара будет int’овое значение price.

Путь 3 — дефолтные значения. Представь, что ты не хочешь обязывать разработчика при создании класса указывать price, как из «Путь 2», но и не хочешь, чтобы поле price было nullable. Что делать? Использовать дефолтные значения!

class Item {   int price;   Item({this.price = 10}); }

Таким образом мы убиваем трех зайцев:

  1. Поле price не может быть нулевым (что логично для цены);

  2. Мы не обязываем пользователя указывать значение цены, если цену товара он не знает;

  3. Наш товар без явного указания цены по-дефолту будет стоит 10 у.е

Анатомия ООП — конструктор

Мы с тобой успели узнать, что класс может содержать в себе n-ное количество различных полей с различными типами: теперь в одном объекте может хранится полная информация о товаре. Однако для того, чтобы иметь возможность создавать объекты класса, классу нужно создать «конструктор класса» (сорян за тройную тавтологию).

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

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

Первое, что приходит мне на ум — конструкторы Lego (не реклама, а жаль). Множество кубиков и кирпичиков, которые необходимо собрать в какую то законченную фигуру. Теперь представь, что кубики Lego — значения полей класса (price: 10, title «Массажер‑утка»), а инструкция — сам конструктор класса. Тем самым, используя кубики (=значения) и инструкцию (=конструктор класса) мы можем создать фигурку (=объект класса). Вот и все!

конструктор и объект

конструктор и объект

Конструктор класса выглядит следующим образом:

class Item {   String title;   String description;   int price;   int rating;    Item(){} // конструктор класса Item }

Первым идет название класса, к которому относится конструктор. Далее в круглых скобках идет перечисление переменных (прям как в функциях), а в теле конструктора мы выполняем определенную логику, в данном случае присваиваем передаваемые значения полям нового объекта:

class Item {   String title;   String description;   int price;   int rating;    Item(       {required String newTitle,       required newDescription,       required int newPrice,       required int newRating}) {     title = newTitle;     description = newDescription;     price = newPrice;     rating = newRating;   } }

Супер! Теперь при создании объекта класса мы сможем передавать ему в конструктор определенные значения (цену, название и т. д.), и на выходе получать полноценный объект класса, с которым можно взаимодействовать.

Не прошло и 5 секунд, а я уже предлагаю наш конструктор улучшить. Дело в том, что в нашем примере нам не нужна какая‑то дополнительная логика: все, что я хочу от конструктора, так это то, чтобы при передаче ему параметра price он присваивал его полю price, при передаче параметра title — полю title. Но вот незадача: если внутри фигурных скобок мы сделаем что то подобное

class Item {   String title;   String description;   int price;   int rating;    Item(       {required String title,       required description,       required int price,       required int reting}) {     title = title;     description = description;     price = price;     rating = rating;   } }

то компилятор сделает не то, что мы ожидаем. Компилятор будет думать следующим образом: «ага, ты мне дал в функцию Item аргумент price. Далее ты хочешь присвоить price»у price… Интересный ход, окей, присвою». При этом компилятор будет думать о price не как о поле класса price, а как об аргументе функции price. Иными словами, что, если мы хотим присвоить полю класса значение из аргумента, который называется точно так же, как и поле класса? Выход есть!

Тут нам на помощью приходит this. Данное ключевое слово говорит компилятору, что значение, которое попало в конструктор относится именно к полю класса. Зная это, модернизируем наш конструктор:

class Item {   String title;   String description;   int price;   int rating;    Item(       {required this.title,       required this.description,       required this.price,       required this.rating}); }

Анатомия ООП — объект класса

Ура, победа! Ты сделал  класс, содержащий в себе информацию о товаре, а также добавил ему конструктор. Теперь пришло время выйти из закулисья тому самому объекту класса.

Объектом класса является переменная, имеющая тип данных твоего класса.

То есть в нашем случае, объектом класса Item будет являться переменная myItem. Наполним наш товар информацией. Поскольку у класса есть конструктор, да еще и с именованными полями, то создать его объект будет несложно:

void main() {   Item myItem = Item(       title: "Крутой товар",       description: 'Реально крутой товар',       price: 1000000,       rating: 5);    // myItem - объект класса Item }

Теперь мы имеем объект класса Item myItem1! Что же мы можем с ним сделать? Ну, на текущий момент только лишь просмотреть содержимое его полей:

void main() {   Item myItem = Item(       title: "Крутой товар",       description: 'Реально крутой товар',       price: 1000000,       rating: 5);    print(myItem.description); // Реально крутой товар   print(myItem.rating); // 5 }

Возможно, хотелось бы большего.

Анатомия ООП — методы

Думаю, ты уже знаешь что такое функции и умеешь их писать. Методы класса = функции, только объявленные внутри класса, и применимы они только к объектам данного класса.

Давай попробуем создать один такой метод, который бы изменял цену товара на новое значение:

class Item {   String title;   String description;   int price;   int rating;    Item(       {required this.title,       required this.description,       required this.price,       required this.rating});    // Новый метод для смены цены   void changePrice(int newPrice) {     this.price = newPrice;   } }

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

Можно придумать пример интереснее: метод, который бы позволял провести “мнимую оценку” товара, исходя из его рейтинга и количества символов в описании. Давай попробуем это реализовать:

class Item {   String title;   String description;   int price;   int rating;    Item(       {required this.title,       required this.description,       required this.price,       required this.rating});    void changePrice(int newPrice) {     this.price = newPrice;   }    // Новый метод для вычисления продвинутого   // рейтинга.   int proRating(){     int newRating = 0;     newRating = this.rating * this.description.length;     return newRating;   } }

Чтобы использовать методы changePrice и proRating, их необходимо вызвать у объекта класса Item, то есть у myItem1:

void main() {   Item myItem = Item(       title: "Крутой товар",       description: 'Реально крутой товар всем советую!!!!',       price: 1000000,       rating: 5);    print(myItem.price); // 1000000   myItem.changePrice(3456);   print(myItem.price); // 3456   print(myItem.proRating()); // 185 }

Иногда могут возникнуть ситуации, когда не очень то и хочется создавать объект класса, чтобы выполнить какой то метод. Что это за ситуация такая? Пример из моей практики: при решении задачек на leetcode функцию со своим решением я кладу в класс Solution. Для выполнения этой функции мне не очень хочется создавать экземпляр Solution, да и зачем, если я не буду взаимодействовать с его полями. Скорее всего, мне бы хотелось сделать что то вроде print(Solution.mySolution()) и получить результат операции. 

Для того, чтобы иметь возможность провернуть такую авантюру, необходимо перед объявлением возвращаемого типа функции дописать ключевое слово static, говорящее о том, что данный метод применим не к объекту класса, а к самому классу:

// Новый статичный метод   static int twoSum(int a, int b){     return a+b;   }

Резюмируя: методы класса — это функции, доступные только объектам класса (или же только самому классу, если они имеют ключевое слово static).

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

Инкапсуляция — как способ разделения сущностей

Ого, юзер, ты настроен серьезно. Что ж, теперь разберемся с основными принципами ООП, и первый из них — инкапсуляция.

Инкапсуляция — это принцип ООП, согласно которому необходимо отделять логику от поведения. В данном контексте логика — то, как что‑то устроено внутри, а поведение — то, как оно взаимодействует с другими сущностями.

Начнем с коробки передач автомобиля (кто не автолюб, не переживаем, аналогия доступна всем). Ты дергаешь ручку передач, а машина самостоятельно меняет передачу в двигателе. С точки зрения неопытного водителя выглядит это как магия: достаточно произвести пару маневров рукой, и вот машина на другой передаче. Теперь представим неидеальный и жестокий мир, в котором не существует ручки передач: каждый раз водителю по ходу движения нужно каким‑то образом залезать в двигатель и менять передачу вручную (то есть делать то, что в привычном понимании автомобиль делает сам).

Теперь сравним это с миром прогеров: в данном случае «ручка передач» есть не что иное, как способ взаимодействия с классом, а то, что делает машина (смена передачи) — ее метод. В коде это бы выглядело следующим образом:

// Класс коробки передач class AutoTransmission {   int currentTransmission;   AutoTransmission([this.currentTransmission = 0]);    void changeTransmission(int value) {     if (value < 5) {       this.currentTransmission = value;     } else {       print('У тебя коробка на 4 передачи, дядя');       print('*передача не изменилась*');     }   } }
void main() {   AutoTransmission auto = AutoTransmission();    print(auto.currentTransmission); // 0   auto.changeTransmission(3);   print(auto.currentTransmission); // 3   auto.changeTransmission(5); // У тебя коробка на 4 передачи, дядя                               // *передача не изменилась*   print(auto.currentTransmission); // 3 }

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

Именно поэтому инкапсуляция и предлагает отделять логику от поведения: сугубо ради безопасного использования объектов и классов.

Но как осознать инкапсуляцию еще лучше? Не находишь, что «инкапсуляция» уж очень созвучна с «капсула»? Такая аналогия имеет место быть, ведь капсула обладает оболочкой и содержимым, которое недоступно извне. Таким образом, оболочка — это класс, а содержимое то, что находится внутри класса..

И, о чудо, оказывается‑то мы с тобой уже и за инкапсуляцию шарим, ведь все ее тейки основаны на идее классов и объектов, но с небольшими дополнениями к уже сказанному…

Заприватим, загетим и засетим!

Представим, что мы разрабатываем мини‑программу для «Растратбанка». В данном случае нам нужно реализовать класс, содержащий номер счета и его баланс:

class BankAccount{   int bankNumber;   int balance;    BankAccount({required this.bankNumber, required this.balance}); }

Мы способны обращаться к номеру счета и к балансу следующим образом:

void main() {   BankAccount bank = BankAccount(     bankNumber: 1234564325365,     balance: 100   );      print(bank.balance); // 100 }

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

void main() {   BankAccount bank = BankAccount(     bankNumber: 1234564325365,     balance: 100   );   print(bank.balance); // 100    bank.balance = 5000000000;   print(bank.balance); // 5000000000 }

Непорядок… Тут нам на помощь придут приватные поля класса.

Приватное поле — поле, которое защищено от внешних вмешательств, то есть никто «снаружи» не сможет его изменить.

Для объявления поля приватным в Dart необходимо добавить нижнее подчеркивание _ перед названием поля:

class BankAccount {   int _bankNumber;   int _balance;    BankAccount(this._bankNumber, this._balance); }

Теперь мы не можем обратиться к этому полю извне*.

*тут небольшое уточнение. В Dart приватность поля ограничивается не самим классом, а целиком файлом. Иными словами, если класс BankAccount определен в main.dart, то в функции void main() мы все еще можем обратиться к этим полям через bank._balance. Ограничения приватных полей распространяется на обращение к ним из других файлов.

main.dart (там же, где и находится класс BankAccount):

void main() {   BankAccount bank = BankAccount(14345454354354, 100);   print(bank._balance); // 100    bank._balance = 5000000000;   print(bank._balance); // 5000000000 }

second_main.dart:

import 'oop.dart';  void main(){   BankAccount bank2 = BankAccount(1678472349832, 200);    print(bank2._balance); // The getter '_balance' istn't defined                          // for the type 'BankAccount'. }

Но если очень хочется, как тогда быть? Реализвать геттер и сеттер внутри класса BankAccount для безопасного взаимодействия с полями.

1) Get’тер

Предназначается для возвращения значения поля класса при его вызове извне.

Выглядит он следующим образом:

class BankAccount {   int _bankNumber;   int _balance;    BankAccount(this._bankNumber, this._balance);    int get bankNumber => _bankNumber;   int get balance => _balance; }

Это ключевое слово позволяет получать значение поля balance извне. Теперь выполнил код в файле second_main.dart мы получим не ошибку, а вполне ожидаемый результат:

import 'oop.dart';  void main() {   BankAccount bank2 = BankAccount(1678472349832, 200);    print(bank2.balance); // 200 }

2) Set’тер

Предназначен для изменения значения поля класса извне.

Выглядит он следующим образом:

class BankAccount {   int _bankNumber;   int _balance;    BankAccount(this._bankNumber, this._balance);    int get bankNumber => _bankNumber;   int get balance => _balance;    // setter для номера счета   set bankNumber(int newNumber) {     if (newNumber.toString().length == 8) {       _bankNumber = newNumber;     } else {       print("!!! Номер счета не изменен !!!");       print("Номер счета должен содержать 8 цифр");     }   } } 

Как ты можешь заменить, сеттер состоит из ключевого слова set, названия сеттера, передаваемых аргументов и тела сеттера. Безопасность использования сеттера заключается в том, что в его теле можно добавить дополнительную валидирующую логику для значения, на которое собираемся изменить поле объекта.

Пример работы сеттера приведен ниже:

import 'oop.dart';  void main() {   BankAccount bank2 = BankAccount(16784723, 200);    print(bank2.bankNumber); // 16784723    bank2.bankNumber = 12345678;   print(bank2.bankNumber); // 12345678    bank2.bankNumber = 123456789; // !!! Номер счета не изменен !!!   print(bank2.bankNumber); // 12345678 }

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

Наследники есть?

Следующим под горячую руку попадает наследование. Что это?

Наследование — возможность другим классам наследовать реализацию класса-родителя.

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

class VendingMachine {   List<String> positions = [     'salad',     'cola',     'whiskey'   ];   int currentMoney;   String? currentOrder;    VendingMachine([this.currentMoney = 0]);    void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   }    String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return tmp;     } else {       return "Your order is empty!";     }   } }  class CoffeeMachine {   List<String> positions = [     'espresso',     'double-espresso',     'triple-espresso'   ];   int currentMoney;   String? currentOrder;    CoffeeMachine([this.currentMoney = 0]);    void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   }    String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return tmp;     } else {       return "Your order is empty!";     }   } } 
void main() {   VendingMachine vending = VendingMachine();   CoffeeMachine coffee = CoffeeMachine();    vending.makeOrder('salad');   print(vending.giveOrder()); // salad    coffee.makeOrder('espresso');   print(coffee.giveOrder()); // espresso    print(coffee.giveOrder()); // Your order is emty!   print(vending.giveOrder()); // Your order is empty! }

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

// Базовый класс машины, продающей товары class BaseSellingMachine {   List<String> positions = [];   int currentMoney;   String? currentOrder;    BaseSellingMachine([this.currentMoney = 0]);    void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   }    String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return tmp;     } else {       return "Your order is empty!";     }   } }

И отнаследоваться от него дочерним классам VendingMachine и CoffeeMachine при помощи ключевого слова extends:

// Наследник класса BaseSellingMachine class VendingMachine extends BaseSellingMachine {   List<String> positions = ['salad',                             'cola',                             'whiskey'                            ]; }  // Наследник класса BaseSellingMachine class CoffeeMachine extends BaseSellingMachine {   List<String> positions = ['espresso',                             'double-espresso',                             'triple-espresso'                            ]; }

Что сейчаспроизошло? Мы имеем базовый класс родитель, хранящий в себе метод для оплаты, метод для выдачи товара, а также поле с текущей внесенной суммой. Также мы имеем два класса наследника — VendingMachine и CoffeeMachine. Что же привнесло ключевое слово extends?

Extends позволяет дочернему классу (в нашем примере VendingMachine) перенять абсолютно все, что было у родительского класса (BaseSellingMachine), а также добавить функционал, характерный для конкретной реализации. Например, отличительной особенностью вендинга является метод для захвата товара, а отличительной особенностью кофемашины — приготовление кофе. Реализуем этот функционал в дочерних классах BaseSellingMachine:

class VendingMachine extends BaseSellingMachine {   List<String> positions = ['salad', 'cola', 'whiskey'];    // Функция по захвату товара   void pickPosition() {     if (currentOrder != null) {       print('Picking up position: $currentOrder');     } else {       print('Nothing to pick!');     }   } }
class CoffeeMachine extends BaseSellingMachine {   List<String> positions = [     'espresso',     'double-espresso',     'triple-espresso'   ];    // Функция по приготовлению кофе   void prepareCoffee() {     if (currentOrder != null) {       print('Preparing coffee: $currentOrder');     } else {       print('Nothing to cook!');     }   } }

Но что, если мы захотим в дочерние классы добавить новые поля: в класс вендинга — поле id конкретного аппарата, а в класс кофемашины — serviceLife, характеризующую срок службы конкретной машины? Дочерние классы наследуют все от родительского, за исключением его конструктора. В данном случае, помимо использования this в конструкторе дочернего класса также необходимо использовать super для полей класса родителя:

VendingMachine(int currentMoney, String? currentOrder,       {required this.vendingId})       : super(currentMoney, currentOrder);
CoffeeMachine(int currentMoney, String? currentOrder,       {required this.serviceLife})       : super(currentMoney, currentOrder);

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

void main() {   VendingMachine vending = VendingMachine(10, 'salad', vendingId: 228);   CoffeeMachine coffee = CoffeeMachine(10, 'espresso', serviceLife: 10);     print(       'Vending money: ${vending.currentMoney}, position: ${vending.currentOrder}, id: ${vending.vendingId}');   // Vending money: 10, position: salad, id: 228     print(       'Coffee money: ${coffee.currentMoney}, position: ${coffee.currentOrder}, service life: ${coffee.serviceLife}');   // Coffee money: 10, position: espresso, service life: 10 } 

В Dart существуют и другие методы наследования (implements, mixin), однако здесь мы на них останавливаться не будем, поскольку общая суть наследования у них одинаковая (однако советую с ними ознакомиться).

Резюмируя: наследование позволяет перенимать реализацию одних классов другими.

Абстракция по-оопшному!

Следующее свойство ООП — абстракция. Давай вспомним вендинговый аппарат и кофемашину:

class BaseSellingMachine {   List<String> positions = [];   int currentMoney;   String? currentOrder;    BaseSellingMachine([this.currentMoney = 0, this.currentOrder]);    void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   }    String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return 'Here is your $tmp';     } else {       return "Your order is empty!";     }   } }
class VendingMachine extends BaseSellingMachine {   List<String> positions = ['salad', 'cola', 'whiskey'];   int vendingId;    VendingMachine(int currentMoney, String? currentOrder,       {required this.vendingId})       : super(currentMoney, currentOrder);    void pickPosition() {     if (currentOrder != null) {       print('Picking up position: $currentOrder');     } else {       print('Nothing to pick!');     }   } }
class CoffeeMachine extends BaseSellingMachine {   List<String> positions = [     'espresso',     'double-espresso',     'triple-espresso'];   int serviceLife;    CoffeeMachine(int currentMoney, String? currentOrder,       {required this.serviceLife})       : super(currentMoney, currentOrder);    void prepareCoffee() {     if (currentOrder != null) {       print('Preparing coffee: $currentOrder');     } else {       print('Nothing to cook!');     }   } }

Мы выносили базовые вещи в BaseSellingMachine, там их реализовывали и унаследовывали их в дочерних классах вендинга и кофемашины. Но что, если в базовом классе ничего не реализовывать, а лишь разметить, какими свойствами должны обладать наследники и оставить реализацию каждого свойства за ними? В этом нам поможет абстрактный класс!

abstract class BaseSellingMachine {   void makeOrder(String order);   String giveOrder(); }

У абстрактных классов есть 2 правила:

  1. Объект абстрактного класса нельзя создать, поскольку он не содержит в себе ничего, кроме как описания функционала;

  2. Абстрактные классы существуют для того, чтобы от них наследовались и самостоятельно реализовывали функционал.

Таким образом, перепишем код для наших машин:

class VendingMachine implements BaseSellingMachine {   int currentMoney;   String? currentOrder;    List<String> positions = ['salad', 'cola', 'whiskey'];   int vendingId;    VendingMachine(       {required this.vendingId,         this.currentMoney = 0,           this.currentOrder        });    void pickPosition() {     if (currentOrder != null) {       print('Picking up position: $currentOrder');     } else {       print('Nothing to pick!');     }   }    @override   String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return 'Here is your $tmp';     } else {       return "Your order is empty!";     }   }    @override   void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   } } 
class CoffeeMachine implements BaseSellingMachine {   List<String> positions = [     'espresso',     'double-espresso',     'triple-espresso'   ];   int serviceLife;   int currentMoney;   String? currentOrder;    CoffeeMachine(       {required this.serviceLife,         this.currentMoney = 0,           this.currentOrder        });    @override   String giveOrder() {     if (currentOrder != null) {       String tmp = currentOrder!;       currentOrder = null;       return 'Here is your $tmp';     } else {       return "Your order is empty!";     }   }    @override   void makeOrder(String order) {     if (positions.contains(order)) {       currentOrder = order;     } else {       print('Choose real position');     }   } } 

Абстрактный класс BaseSellingMachineI является интерфейсом для дочерних классов VendingMachine и CoffeeMachine

Абстракция помогает выделить только базовые вещи, присущие каждому наследнику, а конкретную реализацию оставить за ними.

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

Абстракция — очень важный аспект ООП, позволяющий реализовывать полиморфизм, о котором поговорим далее.

Полиморфизм = многообразие

На греческом языке πολύμορφος (полиморфус) означает «многообразие». Биология же термину «полиморфизм» дает следующее определение:

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

В целом, в программировании данный термин означает почти то же самое: способность одних типов быть похожими на другие. Запутанненько… Время примеров!

Создадим абстрактный класс Swimming:

abstract class Swimming {   void canSwim() {} }

Мы определяем, что объекты-наследники точно имеют свойство плавать. Давай создадим таких наследников:

class Fish implements Swimming {   String color;   int length;   int weight;   String type;    Fish({     required this.color,     required this.length,     required this.weight,     required this.type,   });    @override   void canSwim() {     print('Swimming $type - fish!');   } }
class Boat implements Swimming {   String boatName;   int length;   int weight;    Boat({     required this.boatName,     required this.length,     required this.weight,   });    @override   void canSwim() {     print('Swimming boat "$boatName"');   } }

Теперь наша программа точно знает, что и Fish, и Boat умеют плавать, поскольку они точно содержат унаследованные метод toSwim() от Swimming. Для чистоты эксперимента также создадим класс WaterPlane, который будет самодостаточным, однако он тоже будет содержать в себе метод toSwim() (до чего техника дошла, самолеты теперь и плавать могут…)

Сейчас следим за руками:

void swimForrestSwim(Swimming swimmingCreature) {   swimmingCreature.canSwim(); }  void main() {   Fish karas = Fish(color: 'white', length: 10, weight: 1, type: 'Clown');   Boat teplohoad = Boat(boatName: 'Alexandra', length: 200, weight: 1000000);    swimForrestSwim(karas); // Swimming Clown - fish!   swimForrestSwim(teplohoad); // Swimming boat "Alexandra" }

То есть в функцию, принимающую аргумент типа Swimming мы передали объект класса Fish и класса Boat. Но почему же компилятор такое схавал?

Все потому, что компилятор не видит разницы между классом Swimming и Fish, так как второй является наследником первого, а значит он точно умеет плавать. Такая же история и с Boat.

А что, если мы попробуем засунуть в эту функцию SwimmingPlane? У него ведь тоже есть метод toSwim():

void swimForrestSwim(Swimming swimmingCreature) {   swimmingCreature.canSwim(); }  void main() {   WaterPlane plane = WaterPlane(length: 15, maxSpeed: 200, color: 'red');    swimForrestSwim(plane);   // Error: The argument type 'WaterPlane' can't be assigned   // to the parameter type 'Swimming' }

Увы, тут уже компилятор не проведешь, поскольку он не может быть уверен в том, что у объекта SwimmingPlane есть метод toSwim() .

В примере с Fish и Boat проявляется тот самый полиморфизм: данные объекты не являлись прямыми объектами класса Swimming, однако смогли мимикрировать под объекты этого класса.

Резюмируя: полиморфизм — это когда формы различаются, но действия они выполняют одинаковые.

Заключение

Надеюсь мне удалось помочь тебе понять принципы ООП, поскольку это необходимо не только для собеседований, но и в реальной, «полевой» разработке! Пиши свое мнение в комментариях, буду учитывать и совершенствовать материал. Подписывайся на веселую тележку, ставь лайки, жми на колокольчики! Увидимся в следующих частях, юзер 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/845748/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *