вторник, 4 марта 2014 г.

Dart. OOP. Наставление для падавана.

Dart OOP.

Посидев несколько часов над онлайновым мануалом и скудными статьями, решил собрать в один текст ООП часть Дарта. Как и обещали разработчики, дизайн языка "заточен" под джавистов. Но с приятными нюансами и дополнениями.



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


class Human {
  String name;
  int age;
  int id;
  Human(String name, int age, int index){
    this.name = name;
    this.age = age;
    id = index;
  }
}

Классика жанра, не так ли? Human - имя класса. Это же имя носит метод-конструктор. Конструктор не имеет типа возвращаемогозначения. Внутри методов контекст класса доступен непосредственно: id - это переменная класса. Для случаев конфликта имен (например, с именами параметров метода) контекст класса однозначно определяется ключевым словом this.

 -- Специфика Dart. --
1) Здесь нет модификаторов доступа, как в "приличных языках". Вопрос инкапсуляции решен синтаксическим "хаком": приватными считаются все члены класса, чье имя начинается с подчеркивания "_".

2) Геттеры, сеттеры.

Это синтаксический сахар для эмуляции публичных свойств, которые фактически являются специальными методами. Объявляется в традициях JavaScript: set, get. Метод set принимает значение, метод get возвращает значение. Для геттера не предусмотрен синтаксис с блоком параметров в круглых скобках. 
Т.е. если для сеттера расширенный синтаксис set(value){/* код */}
то для геттера - просто get name{/* код */}. Пример:

class User{
  String _name; // целевая переменна _имя
  int _age; // целевая переменная _возраст.
  User(String this._name, String this._age){}// конструктор (см. Автоприсвоение в конструкторе)

    // краткий синтаксис методов
  String get name => _name; // геттер для имени
  set name(String val) => _name = val; // сеттер для имени

    // расширенный синтаксис
  int get age { return _age; } // геттер для возраста
  set age(int val) {_age = val;} // сеттер для возраста
}

void main(){
  User user = new User('Olya Petrova', 22);
  user.name = 'Olya Polyakova';
  user.age = 23;
  print ("user ${user.name}, ${user.age} years old.");
}

Результат вывода:
user Olya Polyakova, 23 years old.

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

 -- Вкусняшки Dart. --
1.Именованные конструкторы. Синтаксис языка позволяет добавить несколько конструкторов. В отличие от статически типизированных языков, Dart не варьирует конструкторы по сигнатуре, как в Java. Для внесения вариативности Dart дает дополнительное имя конструктору, через точку от имени класса.

class Human {
  String name;
  int age;
  int id;
  Human.nonamed(int age, int index){
    name = "Unknown";
    this.age = age;
    id = index;
  }
}

 -- Параметры методов. --
1. Необязательные параметры. Подобно другим "правильным" языкам, Dart позволяет определить необязательные параметры. Их ставят в конце списка и берут в квадратные скобки. Если параметр не был предан, его значение = null.

class Human {
  String name;
  int age;
  int id;
  Human(int index, int age, [String name]){
    if(name != null)
      this.name = name;
    else
      this.name = "Unknown";
    this.age = age;
    id = index;
  }
}
2. Именованные параметры. Эти параметры определяются подобно необязательным, но берутся в фигурные скобки.

  Human(int age, int index, {String name}){
    if(name != null)
      this.name = name;
    else
      this.name = "Unknown";
    this.age = age;
    id = index;
  }

Смысл последнего примера спорен, но понятие о синтаксисе он дает.
Передаются именованные параметры парой имя : значение, как в объектах JavaScript.

Human user = Human.nonamed(25, 1, name: "Вася Пупкин");

3. Автоприсвоение в конструкторе. Это "сахар", сокращающий запись рутинного присвоения значений параметров членам класса.
class Human {
  String name;
  int age;
  int id;
  Human(String this.name, int this.age, int this.id){}
}
Суть сокращений, думаю, очевидна.

 --- Наследование. ---
Наследование предполагает два логичных следствия - возможную общность сигнатур методов в дереве наследования (например: общий набор методов управления объектом), что приводит к полиморфизму, и возможность наследования реализаций универсального функционала (например: методы сортировки, поиска, отображения).
Как мы знаем из классического сишного ООП, наследование бывает простое, когда класс Б наследуется от класса А, и множественное, когда класс В наследуется от А и Б. В ходе эволюции ООП множественное наследование было многократно оспариваемо, переосмысляемо и, в итоге, мы видим разделение классического сишного множественного наследования на Интерфейсы и Примеси (mixins).
В Dart есть все три типа наследования, придуманные в ООП для взыскательного разработчика.
1. Обычное наследование.
Т.е. простое наследование class B от class A. Поля и связанные с ними методы в примере не имеют особого смысла, просто демонстрируют разный функционал дочерних классов.

class Letter{
  String _letter = '';
  Letter(String letter){
    _letter = letter;
  }
  String getLetter(){
    return _letter;
  }
}

class A extends Letter{
  int _index = 0;
  A(int index):super('A'){
    _index = index;
  }
  int getIndex(){
    return _index;
  }
}

class B extends Letter{
  String _action = null;
  B(String action):super('B'){
    _action = action;
  }
  String getAction(){
    return _action;
  }
}

Нюансы.
1.2 Родительский контекст доступен при помощи слова "super". Использование "super" в конструкторе см. ниже.

class A {
  void foo(){}
}

class B extends A{
  void bar(){
    super.foo();
  }
}

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

1.3 Передача параметров в родительский конструктор. Мы можем обратиться к контексту родительского класса через ключевое слово super. В продолжение предпоследнего примера:

class C extends Letter {
  String _subLetter;
  C(String letter):super(letter){
    _subLetter = 'C';
  }

}
Здесь letter уходит в конструктор родительского класса, а _subLetter присваивается 'C'.

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

abstract class A{
String getA(){ return 'A';}
String getAny(); // объявление метода
}

class B extends A{
String getAny(){ return 'B'; } // реализация
}

class C extends A{
String getAny(){ return 'C'; } // реализация
}

3. Интерфейсы.
Подобно Java, Dart предоставляет механизм интерфейсов. Интерфейсы не содержат какой-либо реализации функционала, но обеспечивают общий тип всем имплементациям (реализациям, вопрощениям) всем дочерним классам. Для интерфейсов в Dart нет отдельного ключевого слова, т.е. это обычный класс, как правило - абстрактный. Наследуется интерфейс ключевым словом implements.

// базовый интерфейс. Жывотнае
class Animal {
  void move(); // этот метод должен быть реализован в дочерних классах
}

// реализация интерфейса, класс Цобаки
class Dog implements Animal{
  String name = 'dog';
  void move(){ walk(); }// реализация метода родительского интерфейса
  void walk(){ print ("$name walks");} // метод дочернего класса
}

// класс Рыбки, тот же интерфейс
class Fish implements Animal{
  String name = 'fish';
  void move(){ swim(); }
  void swim(){ print ("$name swims");}
}

// еще одна реализация интерфейса, Птыц
class Bird implements Animal{
  String name = 'bird';
  void move(){ fly(); }
  void fly(){ print ("$name flies");}
}

// использование классов
void walkPets(){

// функция, принимающая тип Animal
  void walkPet(Animal pet){
    pet.move();
  }
  // список "питомцев"
  List pets = [
    new Dog(), new Fish(), new Bird()
  ];
  // вызов базового метода у каждого "питомца"
  pets.forEach((pet) => walkPet(pet));
}

walkPets();

Результатом выполнения кода будет:

dog walks
fish swims
bird flies

4. Примеси (mixins).
Примеси - это обратная интерфейсам стратегия наследования. Вместо описания типа, сигнатур методов наследуется непосредственно функционал класса. Ключевое слово для "примешивания" - with. Примеси существуют в Ruby, Python, Scala, D, PHP.
Есть несколько условий (ограничений) для использования пимесей в Dart:
- отсутствие объявленного конструктора,
- наследование прямо от Object (для плагина IDEA пока актуально прописывать: extends Object ),
- отсутствие обращений к контексту super.

// класс с наследуемым функционалом
abstract class Walker {
  String name;
  void walk(){ print ("$name walks");}
}

// класс, использующий функционал из Walker
class Man extends Object with Walker{
  String name = 'man';
}

// еще один класс, использующий Walker
class Cat extends Object with Walker{
  String name = 'cat';
}

// использование классов
void mixIn(){
  Man man = new Man();
  Cat cat = new Cat();
  man.walk();
  cat.walk();
}
Результат:

man walks
cat walks

Примечание.
Сам по себе синтаксис Dart предполагает использование "with" сразу за именем объявляемого класса.

class Man with Walker{
  String name = 'man';
}

Man man = new Man();
man.walk();


И это нормально работает в плагине для Eclipse. Но, по причине несовершенства суровой действительности, плагин для IDEA либо не понимает примесей принципиально (IDEA 12), либо только через такой вот капитанский (от "капитан Очевидность") способ прямого указания предка, читай: "костыль". Вероятно, синтаксический анализатор был спилен с Java плагина, и добавление странного нового слова напрямую пока не нашло вменяемой реализации, впрочем это мои персональные домыслы.

В ситуации, когда нужно использовать сразу несколько видов наследования, картина будет приблизительно такой:

abstract class A{} // интерфейс

abstract class B{} // класс для примешивания

abstract class C{} // родительский класс

class D extends C with B implements A{} // дочерний класс


Польза и смысл использования примесей достаточно наглядны. В C++ очень много и долго критиковали множественное наследование. Но, как показывает жизнь, какие-то его аспекты оказываются востребованы. Интерфейсы и примеси - это два разных аспекта множественного наследования, отделенные от жесткой  вертикали классов для избавления от некоторых вредных противоречий.

На этом, пожалуй, все. Возможны дополнения и исправления в будущем.

Комментариев нет:

Отправить комментарий