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

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

    До сего дня я занимался лишь переводами интересных, на мой взгляд, статей англоязычных авторов. И вот настала пора самому что-то написать. Для первой статьи я выбрал тему, которая, я уверен, будет полезна 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 связано не с наличием/отсутствием классов и присутствием/отсутствием прототипов, а именно со статическим/динамическим характером организации.

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

    Подробнее
    Реклама

    Комментарии 22

      0
      Вы один механизм __proto__ использовали для двух разных сущностей — наследования и [семейные] отношения
        0

        В конце концов proto это всего лишь синтаксический сахар вокруг свойств constructor и prototype. При этом не слишком широко применяемый и вошедший в спецификацию сравнительно недавно в 2015 году. Мне кажется что для понимания что там за кадром в классах es7 было бы полезнее знать именно основу прототипного наследования которая была в es с самых первых версий

          +1
          Разумеется! я написал, что решил применить этот синтаксис в иллюстрационных целях.
          +2
          Так же, как с помощью функций можно организовать и функциональную и процедурную парадигмы, так и с помощью прототипов можно организовать и практически класический ООП и тот кошмар который приведен в статье.
          То что можно на лету менять прототип и его содержимое, тем самым меняя поведение конечной сущности, — это заслуга отнюдь не прототипного наследования (хотя косвенно и оно участвует), а динамической основы языка. Такое же поведение можно реализовать и без прототипов, если задаться целью.
          Грубо говоря, наличие прототипов и динамичности позволяет как «вить веревки», так и «стрелять себе в ногу». Все зависит от желания пользоваться парадигмами или возможностями.
            0
            Именно об этом данная статья!
              0
              Как-то из статьи я этого не понял. Более того, понял что мысль донесена не правильно и об этом мой коментарий.
                0
                Почему мысль донесена не правильно?
                  +1
                  Вы сравниваете классы и прототипы — вещи несравниваемые. Класс это сущность парадигмы ООП. Прототип — имплементация. ООП в JS ничем (за исключением нюансов) не отличается от ООП в других языках. JS имплементирует ООП с помощью прототипов, С++ компилятором со статическими сущностями. Обусловлено это тем, что первый — динамический интерпретатор, а второй статический компилятор (или проще: так получилось).
                  А все эти способности на лету чего-то портить никак не связаны ни с ООП ни с прототипами. Все это динамичность языка.
                  А вы пытаетесь показать разницу между классами и прототипами в том что прототипы можно менять или клеить на лету.
                  В этом и есть ошибка подхода.
                  А еще, наличие обьектов не делает ваш код обьектно-ориентированным.
                    +1
                    Вы не поняли статью совершенно, даже тематику и проблематику, которую я поднял в статье. Извините, но я с вами не согласен.
                      0

                      Как в ваше разделение вписывается PHP? Вполне классические классы, но свойства на лету добавлять конкретному объекту можно

                        0
                        Я там написал про динамику и статику в статье. Об этом и статья.
            0
            Вы хоть в Java коде
            new Person()

            напишите, а то tom-то так и не создался
              0
              сущность достаточно широкое понятие и применяется в разных концепциях, уместее использовать понятие экземпляра
                0

                Как по мне, для джунов написано слишком академическим языком, из-за чего сложнее понять. Например эта статья воспринимается легче. Удивительно, но за 10 лет она практически не потеряла актуальность.

                  0
                  Почитаю! Извините если тяжело написано, я просто пытался передать свой опыт работы с начинающим разработчиками.
                    +1

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


                    • “сущности”, “инстанции”
                      “экземпляры” привычнее, иногда допустимо разговорное “инстанс”
                    • “создавая отношение генерализации — специализации”
                      смотрится уместнее в статьях про нюансы ООП, UML. Здесь лучше “наследуя его”
                    • “генерального класса(суперкласса)”
                      ≈ “родителя”
                    • “Класс представляет собой некий формализованный обобщённый набор характеристик сущностей, которые он может породить.”
                      тяжеловато, описание полегче

                    Это все конечно субъективно, может другие хабраюзеры не согласятся со мной.

                      0
                      Я употребил термин «сущность» для того чтобы показать, что это нечто физически конкретное… возможно не самая удачная идея.

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

                  учитывая тот факт, что метод добавлен непосредственно в объект (в представленном коде), а не в его прототип, в этом поможет убедиться состав объекта на которое ссылается свойство __proto__
                  Соответсвенно и последующие рассуждения искажаются допущенным недопониманием рассматриваемого вопроса
                    0
                    Мы добавили свойства объекту Ben, которая является на тот момент прототипом объекта tomSon.
                    0
                    Весьма актуальная статья на мой взгляд, с учетом продолжающегося роста популярности JS и ES6 в частности. Буду ждать следующих статей на эту тему.
                      0
                      Спасибо! Статьи будут.
                      0
                      Вопрос к объекту про типы: кто ты? В классических ООП языках тип объекта определяется классом. Класс может наследовать тип от одного или более других классов, а так же реализовывать интерфейсы. Таким образом типов у объекта может быть несколько.

                      Вопрос про свойства обьекта: чей ты? Объект может содержать собственные свойства или делегировать другому объекту (предку), и так по цепочке.

                      Классические ООП языки реализуют обе схемы, прототипные только вторую. Поэтому типы всех объектов в js одинаковы — это object. Слово class из ES6 лишь синтаксический сахар, который ни чего не меняет в плане типизации.

                      Более наглядно, с картинками, это описано в статье про python. Если там исключить классы, оставив лишь одни метаклассы, то получится почти как в JavaScript
                      blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses

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

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

                      Самое читаемое