Один из важнейших принципов объектно-ориентированного программирования – разграничение внутреннего интерфейса от внешнего.
Это обязательная практика при разработке чего-то более сложного, чем приложение «Привет, мир».
Чтобы это понять, давайте оторвемся от разработки и обратим взгляд в реальный мир.
Обычно устройства, которые мы используем, довольно сложны. Но разграничение внутреннего интерфейса от внешнего позволяет без проблем использовать их.
Например, кофемашина. Внешне просто: кнопка, дисплей, несколько дырочек… И, конечно же, результат – великолепный кофе! :)
А вот внутри… (картинка из руководства по ремонту)
Много деталей. Но мы можем использовать его, ничего не зная.
Кофемашины довольно надежны, не так ли? Мы можем пользоваться им годами, и только если что-то пойдет не так – нести в ремонт.
Секрет надежности и простоты кофемашины – все детали хорошо подогнаны и спрятаны внутри.
Если снять защитную крышку с кофемашины, то пользоваться ею будет гораздо сложнее (куда нажимать?) и опасно (может ударить током).
Как мы увидим, в программировании объекты подобны кофемашинам.
Но чтобы скрыть внутренние детали, мы воспользуемся не защитной оболочкой, а специальным синтаксисом языка и соглашениями.
В объектно-ориентированном программировании свойства и методы делятся на две группы:
Внутренний интерфейс — методы и свойства, доступные из других методов класса, но не извне.
Внешний интерфейс — методы и свойства, доступные также снаружи класса.
Если продолжить аналогию с кофемашиной – то, что внутри спрятано: бойлерная трубка, нагревательный элемент и так далее – это ее внутренний интерфейс.
Для работы объекта используется внутренний интерфейс, его детали используют друг друга. Например, к нагревательному элементу прикреплена бойлерная трубка.
Но снаружи кофемашина закрыта защитной крышкой, так что никто не сможет до нее добраться. Детали скрыты и недоступны. Мы можем использовать его возможности через внешний интерфейс.
Итак, все, что нам нужно для использования объекта, — это знать его внешний интерфейс. Мы можем совершенно не осознавать, как это работает внутри, и это здорово.
Это было общее знакомство.
В JavaScript существует два типа полей объекта (свойства и методы):
Публичный: доступен из любого места. Они составляют внешний интерфейс. До сих пор мы использовали только общедоступные свойства и методы.
Частный: доступен только изнутри класса. Они предназначены для внутреннего интерфейса.
Во многих других языках также существуют «защищенные» поля: доступные только изнутри класса и расширяющие его (например, частные, но плюс доступ из наследующих классов). Они также полезны для внутреннего интерфейса. В некотором смысле они более распространены, чем частные, поскольку мы обычно хотим, чтобы наследующие классы получили к ним доступ.
Защищенные поля не реализованы в JavaScript на уровне языка, но на практике они очень удобны, поэтому эмулируются.
Теперь мы создадим кофемашину на JavaScript со всеми этими типами свойств. Кофемашина имеет множество деталей, мы не будем моделировать их, чтобы они оставались простыми (хотя могли бы).
Давайте сначала создадим простой класс кофемашины:
класс CoffeeMachine { количество воды = 0; // количество воды внутри конструктор(мощность) { this.power = мощность; alert(`Создала кофемашину, power: ${power}`); } } // создаем кофемашину пусть CoffeeMachine = новая CoffeeMachine (100); // добавляем воду CoffeeMachine.waterAmount = 200;
На данный момент свойства waterAmount
и power
являются общедоступными. Мы можем легко получить/установить для них любое значение извне.
Давайте изменим свойство waterAmount
на protected, чтобы иметь больше контроля над ним. Например, мы не хотим, чтобы кто-либо устанавливал его ниже нуля.
Защищенные свойства обычно имеют префикс подчеркивания _
.
Это не предусмотрено на уровне языка, но между программистами существует хорошо известное соглашение о том, что к таким свойствам и методам не следует обращаться извне.
Итак, наше свойство будет называться _waterAmount
:
класс CoffeeMachine { _waterAmount = 0; установить WaterAmount (значение) { если (значение < 0) { значение = 0; } this._waterAmount = значение; } получить количество воды() { верните это._waterAmount; } конструктор(мощность) { this._power = мощность; } } // создаем кофемашину пусть CoffeeMachine = новая CoffeeMachine (100); // добавляем воду CoffeeMachine.waterAmount = -10; // _waterAmount станет 0, а не -10
Теперь доступ под контролем, поэтому установка количества воды ниже нуля становится невозможной.
Для свойства power
давайте сделаем его доступным только для чтения. Иногда случается, что свойство должно быть установлено только во время создания, а затем никогда не изменяться.
Именно так и происходит с кофемашиной: мощность никогда не меняется.
Для этого нам нужно создать только геттер, но не сеттер:
класс CoffeeMachine { // ... конструктор(мощность) { this._power = мощность; } получить мощность() { верните это._power; } } // создаем кофемашину пусть CoffeeMachine = новая CoffeeMachine (100); alert(`Мощность: ${coffeeMachine.power}W`); // Мощность: 100 Вт кофемашина.мощность = 25; // Ошибка (нет установщика)
Функции получения/установки
Здесь мы использовали синтаксис геттер/сеттер.
Но в большинстве случаев предпочтительны функции get.../set...
, например:
класс CoffeeMachine { _waterAmount = 0; setWaterAmount (значение) { если (значение < 0) значение = 0; this._waterAmount = значение; } getWaterAmount() { верните это._waterAmount; } } новый CoffeeMachine().setWaterAmount(100);
Это выглядит немного длиннее, но функции более гибкие. Они могут принимать несколько аргументов (даже если они нам сейчас не нужны).
С другой стороны, синтаксис get/set короче, поэтому в конечном итоге не существует строгого правила, решать вам.
Защищенные поля наследуются
Если мы наследуем class MegaMachine extends CoffeeMachine
, то ничто не мешает нам получить доступ к this._waterAmount
или this._power
из методов нового класса.
Таким образом, защищенные поля естественным образом наследуются. В отличие от частных, которые мы увидим ниже.
Недавнее дополнение
Это недавнее дополнение к языку. Не поддерживается в движках JavaScript или пока поддерживается частично, требует полизаполнения.
Существует готовое предложение JavaScript, почти стандартное, которое обеспечивает поддержку частных свойств и методов на уровне языка.
Рядовые должны начинаться с #
. Они доступны только изнутри класса.
Например, вот частное свойство #waterLimit
и частный метод проверки воды #fixWaterAmount
:
класс CoffeeMachine { #waterLimit = 200; #fixWaterAmount(значение) { если (значение < 0) вернуть 0; if (value > this.#waterLimit) return this.#waterLimit; } setWaterAmount (значение) { this.#waterLimit = this.#fixWaterAmount(значение); } } пусть CoffeeMachine = новый CoffeeMachine(); // невозможно получить доступ к приватным данным за пределами класса CoffeeMachine.#fixWaterAmount(123); // Ошибка CoffeeMachine.#waterLimit = 1000; // Ошибка
На уровне языка #
— это специальный знак того, что поле является частным. Мы не можем получить к нему доступ извне или из наследуемых классов.
Частные поля не конфликтуют с публичными. Мы можем одновременно иметь как частные поля #waterAmount
, так и общедоступные поля waterAmount
.
Например, давайте сделаем waterAmount
аксессором для #waterAmount
:
класс CoffeeMachine { #Количество воды = 0; получить количество воды() { верните это.#waterAmount; } установить WaterAmount (значение) { если (значение < 0) значение = 0; this.#waterAmount = значение; } } пусть машина = новая CoffeeMachine(); машина.waterAmount = 100; оповещение(machine.#waterAmount); // Ошибка
В отличие от защищенных, частные поля контролируются самим языком. Это хорошо.
Но если мы наследуем от CoffeeMachine
, у нас не будет прямого доступа к #waterAmount
. Нам нужно будет полагаться на метод получения/установки waterAmount
:
класс MegaCoffeeMachine расширяет CoffeeMachine { метод() { предупреждение (this.#waterAmount); // Ошибка: доступ возможен только из CoffeeMachine } }
Во многих сценариях такое ограничение является слишком жестким. Если мы расширим CoffeeMachine
, у нас могут появиться законные причины получить доступ к его внутренностям. Вот почему защищенные поля используются чаще, хотя они и не поддерживаются синтаксисом языка.
Частные поля недоступны как это [имя]
Частные поля особенные.
Как мы знаем, обычно мы можем получить доступ к полям, используя this[name]
:
класс Пользователь { ... сказатьПривет() { пусть fieldName = «имя»; alert(`Привет, ${this[fieldName]}`); } }
С приватными полями это невозможно: this['#name']
не работает. Это синтаксическое ограничение для обеспечения конфиденциальности.
В терминах ООП разграничение внутреннего интерфейса от внешнего называется инкапсуляцией.
Это дает следующие преимущества:
Защита пользователей, чтобы они не выстрелили себе в ногу
Представьте себе, что команда разработчиков пользуется кофемашиной. Изготовлена компанией Best CoffeeMachine, работает нормально, но была снята защитная крышка. Таким образом, внутренний интерфейс открыт.
Все застройщики цивилизованные — используют кофемашину по назначению. Но один из них, Джон, решил, что он самый умный, и внес некоторые изменения во внутренности кофемашины. Итак, через два дня кофемашина вышла из строя.
Это явно не вина Джона, а вина человека, который снял защитную крышку и позволил Джону делать свои манипуляции.
То же самое и в программировании. Если пользователь класса изменит то, что не предназначено для изменения извне – последствия непредсказуемы.
Поддерживаемый
Ситуация с программированием сложнее, чем с реальной кофемашиной, потому что мы не покупаем ее один раз. Код постоянно развивается и совершенствуется.
Если строго разграничить внутренний интерфейс, то разработчик класса сможет свободно менять его внутренние свойства и методы, даже не информируя об этом пользователей.
Если вы разработчик такого класса, то приятно знать, что приватные методы можно безопасно переименовывать, их параметры можно изменять и даже удалять, поскольку от них не зависит никакой внешний код.
Для пользователей, когда выходит новая версия, это может быть полная внутренняя переработка, но ее все равно легко обновить, если внешний интерфейс остался прежним.
Скрытие сложности
Люди обожают использовать простые вещи. По крайней мере снаружи. Внутри другое дело.
Программисты не являются исключением.
Всегда удобно, когда детали реализации скрыты и доступен простой, хорошо документированный внешний интерфейс.
Чтобы скрыть внутренний интерфейс, мы используем либо защищенные, либо частные свойства:
Защищенные поля начинаются с _
. Это хорошо известное соглашение, не соблюдаемое на уровне языка. Программистам следует обращаться к полю, начинающемуся с _
только из его класса и классов, наследующих от него.
Частные поля начинаются с #
. JavaScript гарантирует, что мы сможем получить доступ к ним только изнутри класса.
На данный момент приватные поля не очень хорошо поддерживаются браузерами, но их можно заполнить полифилом.