Взгляд со стороны EcmaScript на общую теорию ООП +18


Привет, Хабр!

До сего дня я занимался лишь переводами интересных, на мой взгляд, статей англоязычных авторов. И вот настала пора самому что-то написать. Для первой статьи я выбрал тему, которая, я уверен, будет полезна junior-разработчикам, стремящимся дорасти до «мидлов», т.к. в ней будет разобрана схожесть/отличие JavaScript от классических языков программирования (С++, С#, Java) в плане ООП. Итак, начнём!

Общие положения парадигмы


Если мы посмотрим определение JavaScript по Википедии, то увидим следующее понятие:
JavaScript (/?d???v???skr?pt/; аббр. JS /?d?e?.?s./) — мультипарадигменный язык программирования. Поддерживает объектно-ориентированный, императивный и функциональный стили. Является реализацией языка ECMAScript (стандарт ECMA-262).

Как следует из этого определения, JavaScript существует не сам по себе, а является реализацией некоей спецификации EcmaScript. Помимо него, эту спецификацию реализуют и другие языки.

В EcmaScript(далее ES) присутствуют следующие парадигмы:

  • структурная
  • ООП
  • функциональная
  • императивная
  • аспектно-ориентированная(в редких случаях)

ООП в ES реализовано на прототипной организации. От начинающих разработчиков в ответ на вопрос: «Чем ООП в JS отличается от ООП в классических языках». Как правило, получают очень туманное: «В классических языках классы, а в JS прототипы».

В действительности ситуация обстоит немного сложнее. С точки зрения поведения разница между Динамической Классовой организацией и Прототипной организацией невелика(она безусловно есть, но не столь глобальная).

Посмотрите на Python или Ruby. В этих языках ООП основано на динамической классовой организации. В обоих этих языках мы можем динамически по ходу программы менять класс объекта и изменения внутри класса также динамически влияют на порождаемые им сущности. Совсем как в JS, но ведь в JS ООП основано на прототипах.

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

На чём основана Статическая классовая организация?


Основой этого типа ООП являются понятия «Класс» и «Сущность». Класс представляет собой некий формализованный обобщённый набор характеристик сущностей, которые он может породить. Т.е. это некий общий план всех порождаемых им объектов.

Характеристики бывают двух типов. Свойства(описание сущности) и методы(активности сущности, их поведение).

Сущности, порождаемые классом, представляют собой копии этого класса, но с инициализированными свойствами. Как мы видим, класс жёстко регламентирует описание сущности(предоставляя строго определённый набор свойств) и её поведение(предоставляя строго определённый перечень методов).

Приведём небольшой пример на JAVA:

class Person{
     
    String name;        // имя
    int age;            // возраст
    void displayInfo(){
        System.out.printf("Name: %s \tAge: %d\n", name, age);
    }
}

Теперь создадим инстанцию класса:

public class Program{
      
    public static void main(String[] args) {
         
        Person tom;
    }
}

У нашей сущности tom есть все характеристики класса Person, он также обладает всеми методами своего класса.

ООП парадигма предоставляет очень широкую палитру возможностей по переиспользованию кода, одна из этих возможностей Наследование.

Один класс может расширять другой класс, тем самым создавая отношение генерализации — специализации. При этом свойства генерального класса(суперкласса) копируются в сущности класса потомка при их создании, а методы доступны по ссылке(по иерархической цепи наследования). В случае статической класовой типизации эта цепь статична, а в случае динамической она может изменяться в ходе выполнения программы. Это и есть важнейшее отличие. Советую сейчас запомнить этот момент. Далее, когда мы дойдём до Прототипной организации, суть проблемы ответа «там классы, тут прототипы» станет очевидной.

Какие минусы данного подхода?

Думаю, очевидно, что:

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

На чём основана прототипная организация?


Ключевой концепцией прототипной организации является Динамически Изменяемый Объект(dynamic mutable object, dmo). DMO не нужен класс. Он сам может хранить все свои свойства и методы.

При простановке DMO некоего свойства происходит проверка на наличие этого свойства в нём. Если свойство есть, то оно просто присваивается, если его нет, то свойство добавляется и инициализируется переданным значением. DMO могут менять свою сигнатуру по ходу программы сколь угодно раз.

Приведём пример:

//Данный объект мы будем использовать в качестве прототипа
const Person = {
  name: null,
  age: null,
  sayHi() {
    return `Hi! My name is ${this.name}. I'm ${this.age} years old.`
 }
}

const Tom = {
  //Какие-то специфичные для Тома свойства и методы
}

Tom.__proto__ = Person;

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

Если мы выведем объект Tom в консоль, то увидим, что в самом объекте есть только ссылка _proto_, которая присутствует в нём по умолчанию всегда. Ссылка указывает на объект Person, который является прототипом объекта Tom.

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

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

Вернёмся к нашему Тому:

Tom.name = 'Tom'; //инициализируем Тому собственное свойство
Tom.surname = 'Williams'; //инициализируем Тому собственное свойство
Tom.age = 28;//инициализируем Тому собственное свойство

Tom.sayHi();//Вызываем метод sayHi, в Томе интерпритатор его не найдет, поэтому посмотрит в прототипе, и вот там то он есть

const tomSon = {
  name: 'John',
  age: 5,
  sayHi() {
    return this.__proto__.sayHi.call(this) + `My father is ${this.__proto__.name} ${this.surname}`;
  }
}
//Укажем, что Джон сын Тома
tomSon.__proto__ = Tom;
tomSon.sayHi();// Вернёт "Hi! My name is John. I'm 5 years old.My father is Tom Williams"

Обратите внимание, свойства name, age и метод sayHi это собственные свойства объекта tomSon. При этом, мы в tomSon sayHi явно вызываем метод прототипа sayHi так, как если бы он был в объекте Tom, но на самом деле его там нет, и он неявным способом возвращается из прототипа Person.Также мы явно оперируем свойством прототипа name и неявно получаем свойство surname, которое мы вызываем, как собственное свойство объекта tomSon, но на самом деле его там нет. Свойство surname неявным образом подтягивается через ссылку __proto__ из прототипа.

Продолжим развитие истории нашего Тома и его сына Джона.

// Допустим, Том со своей женой(мамой джона развелись)
// и суд, как часто бывает, оставил ребёнка с мамой,
// а та снова вышла замуж 

 const Ben = {
  name: 'Ben',
  surname: 'Silver',
  age: 42,
  sayHi() {
    return `Hello! I'm ${this.name} ${this.surname}. `;
  }
}

tomSon.nativeFather = Tom;
tomSon.__proto__= Ben;

tomSon.sayHi(); // фамилия у ребёнка поменялась(допустим), также поменялись некоторые его привычки(поведение)
//Теперь метод вернёт 'Hello! I'm John Silver. My father is Ben Silver'

Обратите внимание, мы по ходу программы поменяли прототип уже созданного объекта. В этом схожесть Прототипной организации и Динамической классовой организации. Именно поэтому ответ «там классы, тут прототипы» на вопрос " в чём разница между классическими языками и JavaScript?" не вполне корректен и свидетельствует о некотором непонимании теории ООП и её реализации на классах и/или прототипах.

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

Ben.hobbies = ['chess', 'badminton'];
//сущность tomSon давно уже создана, но мы добавляем свойства в её прототип и можем реализовать в ней поведение, которое будет оперировать этими свойствами
tomSon.sayAboutFathersHobies = function () {
  const reducer = (accumulator, current) => {`${accumulator} and ${current}`}
  return `My Father play ${this.hobbies.reduce(reducer)}`
}

tomSon.sayAboutFathersHobies(); // вернёт 'My Father play chess and badminton'

Это называют делегирующей моделью прототипной организации или наследованием на прототипах.

Как определяется способность сущности реализовывать некое поведение?


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

Какие плюсы у прототипного подхода?

  • Больше гибкости
  • В сущностях не присутствуют свойства, которые им не нужны

Какие минусы?

  • Менее наглядно
  • Не всегда легко отследить, что послужило отправной точкой нежелательного поведения сущности, т.е. по сравнению со статической классовой организацией прототипная менее предсказуема
  • Сообщество разработчиков программного обеспечения недостаточно хорошо знакомо с ним, несмотря на популярность и распространённость JavaScript

Заключение


На этом мы на сегодня закончим. Надеюсь, что мне удалось донести мысль о том, что отличие между классическими языками и JavaScript связано не с наличием/отсутствием классов и присутствием/отсутствием прототипов, а именно со статическим/динамическим характером организации.

Безусловно, многое осталось не рассмотренным. Я бы не хотел писать слишком длинных статей, поэтому особенности Каскадной модели в прототипной организации и средства ООП(Полиморфизм, Инкапсуляцию, Абстракцию и т.д.) мы обсудим в последующих статьях.




К сожалению, не доступен сервер mySQL