Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Внутренний и внешний интерфейс

  1. Пример из жизни
  2. Внутренний и внешний интерфейс
    1. Интерфейсы для Menu
  3. Защита внутреннего интерфейса
  4. Приватные свойства и методы
  5. Рефакторинг
  6. Добавление обработчиков
  7. Везде self вместо this
  8. Геттеры и Сеттеры
    1. Геттеры
    2. Сеттеры
    3. Геттеры/сеттеры в ES5
  9. Итого

Отделение внутреннего интерфейса от внешнего — обязательная практика в разработке чего угодно сложнее hello world.

Чтобы это понять, отвлечемся от разработки и переведем взгляд на объекты реального мира.

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

Пример из жизни

Например, кофеварка. Простая снаружи: кнопка, индикатор, отверстия,… И, конечно, кофе:

Но внутри… (картинка из пособия по ремонту)

Масса деталей. Но мы можем пользоваться ей, совершенно не зная об этом.

Кофеварки — довольно-таки надежны, не правда ли? Можно пользоваться годами, и только когда что-то пойдет не так — придется нести к мастеру.

Секрет надежности и простоты кофеварки — в том, что все детали отлажены и спрятаны внутри. Если снять с кофеварки защитный кожух, то использование её будет более сложным (куда нажимать?) и опасным (током ударить может). Здесь, как мы увидим, объекты очень схожи с кофеварками.

Внутренний и внешний интерфейс

  • Внутренний интерфейс — это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта.
  • Внешний интерфейс — это свойства и методы, доступные снаружи объекта.

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

Внутренний интерфейс используется для обеспечения работоспособности объекта, его детали используют друг друга. Например, трубка кипятильника подключена к нагревательному элементу.

Но снаружи кофеварка закрыта специальным кожухом, чтобы никто к ним не подобрался. Детали скрыты и недоступны. Виден лишь внешний интерфейс.

Внутренний интерфейс кофеварки Внешний интерфейс кофеварки
Резервуар
Клапан
Гибкая трубка
Тепловой предохранитель
Хомут гибкой трубки
Термостат
Трубка кипятильника
Нагревательный элемент
Контактная колодка
Пластина подогрева основания
Прижимная планка шнура
Обратный клапан
Держатель фильтра
...
Отверcтие для горячей воды
Выключатель
Разъем питания
Индикатор наполненности
Фильтр с молотым кофе
...

Все, что нужно для пользования объектом — это внешний интерфейс. О внутреннем знать не обязательно.

Интерфейсы для Menu

Переведём взгляд обратно, на программирование.

Для нашего меню внешним интерфейсом является конструктор new Menu(menuId) и методы open и close. Их достаточно для использования объекта.

А свойство itemsElem является внутренним.

Внутренний интерфейс `Menu` Внешний интерфейс `Menu`
itemsElem new Menu(menuId)
open()
close()

Защита внутреннего интерфейса

Внутренний интерфейс должен быть закрыт от доступа снаружи.

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

Для такого разграничения доступа есть несколько причин:

Защита разработчиков.
Представьте, команда разработчиков пользуется кофеваркой. Кофеварка создана фирмой «Лучшие Кофеварки» и, в общем, работает хорошо, но с неё сняли защитный кожух и, таким образом, внутренний интерфейс стал доступен.

Все разработчики цивилизованны — и пользуются кофеваркой как обычно. Но хитрый Вася решил, что он самый умный, и подкрутил кое-что внутри кофеварки, чтобы кофе заваривался покрепче. Вася не знал, что те изменения, которые он произвел, приведут к тому, что кофеварка испортится через два дня.

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

В программировании — то же самое. Если разработчик при подключении меню будет менять то, что не рассчитано на изменение снаружи — последствия могут быть непредсказуемыми. Лучше защитить Васю от самого себя и от гнева других членов команды, когда они обнаружат, что меню сломалось.

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

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

Ближайшая аналогия в реальной жизни — это когда мастер пришел, снял кожух с кофеварки (он имеет право) и переделал всё внутри, так что она стала работать гораздо лучше. А пользоваться ей по-прежнему просто, и изменения незаметны.

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

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

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

Приватные свойства и методы

В ООП разделяют два основных вида свойств/методов:

  • Приватное свойство — это такое, которое доступно только изнутри самого объекта.
  • Публичное свойство — доступно и снаружи и из внешнего кода.

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

Например, в других языках:

  • В языке С++ можно открыть к приватным переменным для отдельного класса, объявив его «дружественными».
  • В языке Java можно объявлять переменные, которые доступны только внутри того же «пакета классов».

Кроме того, позже, существуют также «защищённые» методы. Мы разберём их позже, при обсуждении наследования.

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

Далее мы посмотрим, как это делается в JavaScript.

Рассмотрим приватные переменные на примере меню. В последней реализации меню все свойства и методы являются публичными.

Вспомним его код:

<div id="food-menu" class="menu">
  <span class="menu-title">Продуктовое меню</span>
  <ul class="menu-items">
    <li>Сыр</li>
    <li>Колбаса</li>
    <li>Баранки</li>
  </ul>
</div>
JS для управления меню:
function Menu(menuId) {
  *!*this.itemsElem*/!* = document.querySelector('#'+menuId+' .menu-items');

  this.open = function() { 
    this.itemsElem.style.display = 'block';
  };

  this.close = function() { 
    this.itemsElem.style.display = 'none';
  };
}

var foodMenu = new Menu('food-menu');
foodMenu.open();

С точки зрения защиты внутреннего интерфейса здесь не всё гладко. Свойство itemsElem является публичным и может быть получено как foodMenu.itemsElem.

Давайте защитим его, сделав приватным. Для этого достаточно заменить this.itemsElem на локальную переменную var itemsElem:

function Menu(menuId) {
*!*
  var itemsElem = document.querySelector('#'+menuId+' .menu-items');
*/!*

  this.open = function() { 
    *!*itemsElem*/!*.style.display = 'block';
  };

  this.close = function() { 
    *!*itemsElem*/!*.style.display = 'none';
  };
}

Теперь методы, объявленные внутри Menu имеют доступ к itemsElem через замыкание, а снаружи это свойство никак получить нельзя.

Рефакторинг

Рефакторинг — это изменение кода внутри, которое незаметно снаружи. Как правило, рефакторинг осуществляют для улучшения структуры кода, чтобы его стало проще поддерживать.

Отрефакторим меню. Пусть оформление делает CSS, а JavaScript лишь ставит необходимые классы.

function Menu(menuId) {
  var elem = document.getElementById(menuId);

  this.open = function() {
    addClass(elem, 'open');
  };

  this.close = function() {
    removeClass(elem, 'open');
  };
}

В этом коде присвоение style.display убрано. Вместо этого ставится CSS-класс, причем не на списке UL, а на внешнем элементе меню.

По умолчанию, CSS будет настроен так, что меню не видно:

.menu-items { /* класс для списка элементов */
  display: none;
}

При открытии добавляется класс open:

<div id="food-menu" class="menu *!*open*/!*">
  <span class="menu-title">...</span>
  <ul class="menu-items">...</ul>
</div>

.. Для которого будет CSS-правило, показывающее меню:

.menu.open .menu-items { /* список в открытом меню */
  display: block;
}

Указание состояния меню в CSS-класс на внешнем элементе — семантически верно, т.к. состояние действительно относится не к элементам, а к меню целиком.

Когда мы делаем правильные вещи — обычно награда не заставляет себя ждать. Здесь мы тут же получили возможность использовать CSS не только для скрытия/показа меню, но и для красивого оформления заголовка.

Например пусть у заголовка закрытого меню стоит стрелочка вправо , а у раскрытого — стрелочка вниз .

Соответствующие правила CSS:

.menu-title {
  padding-left: 18px; 
  background: url(arrow-right.png) left center no-repeat; 
}

.menu.open .menu-title {
  /* переопределить стрелку для открытого меню */
  background-image: url(arrow-down.png);       
}

Меню (рабочее):

В следующей секции мы добавим необходимые обработчики правильным с точки зрения ООП способом.

Добавление обработчиков

Оживим меню. Пусть клик на заголовке раскрывает-закрывает его. Для этого нужно добавить обработчик к элементу-заголовку .menu-title.

Код обработчика здесь достаточно прост, но в принципе может быть и большим. Поэтому его лучше вынести в отдельный метод onTitleClick:

function Menu(menuId) {
  *!*var self = this;*/!*

  var elem = document.getElementById(menuId);
  var titleElem = elem.querySelector('.menu-title');

  var isOpen = false; 

  init();

  // ---------- методы ----------

*!*
  function onTitleClick(e) {
    self.toggle();
  };
*/!*

  function init() {
    elem.onmousedown = elem.onselectstart = function() {
      return false;
    };

    titleElem.onclick = onTitleClick;
  }

  this.toggle = function() {
    isOpen ? self.close() : self.open();
  }

  this.open = function() {
    if (isOpen) return; // уже открыто
    addClass(elem, 'open');
    isOpen = true;
  };

  this.close = function() {
    removeClass(elem, 'open');
    isOpen = false;
  };
}

В этом фрагменте мы впервые встречаем приватные методы: onTitleClick и init.

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

Так как она является обработчиком, то при запуске в её this попадет элемент, на котором произошло событие. Поэтому, чтобы функция могла обратиться к объекту, ссылка на него копируется в замыкание вызовом var self = this.

*!*var self = this;*/!*

...

function onTitleClick(e) {
  *!*self*/!*.toggle();
}

Везде self вместо this

Код создания и инициализации меню в примере выше вынесен в отдельный метод init.

Это удобно, поскольку позволяет визуально отделить инициализацию от описания методов… Но при вызове init как обычной функции — ей не будет передан this, поэтому self пригодится и здесь.

В приватных методах вместо this используем self.

Даже более того! Наш код — живой. Мы добавляем новые функции, перерабатываем существующие. При этом часть кода может перемещаться из одного метода в другую, или выделяться в новый метод. Представьте себе, что будет, если код, который использует this, переместить в функцию, для которой нужен self (приватную) ?

Нужно будет переименовать все обращения к this, обязательно что-то будет забыто, ошибки.. Но есть рецепт.

Вместо this лучше использовать self, во всех методах!

Итоговый вариант меню:

Открывать такое меню можно как кликом, так и вызовом метода foodMenu.open().

Полный код: tutorial/oop/menu/index.html

Геттеры и Сеттеры

Геттеры

Допустим, внешнему коду понадобилось узнать текущее состояние меню: открыто оно или нет. Как это сделать?

Конечно, можно сделать свойство isOpen публичным: поменять переменную с var isOpen на this.isOpen. Но при этом внешний код получит возможность изменить её значение. А это было бы неправильно.

Поэтому создается специальный метод-геттер (getter) isOpen:

function Menu(menuId) {
  ...
  this.isOpen = function() {
    return isOpen;
  };
  ...
}

Все, что делает этот метод — это возврат значения приватной переменной.

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

Геттер защищает переменную от изменения. Но, кроме того, он даёт дополнительную гибкость. Если в будущем мы захотим избавиться от переменной isOpen и хранить состояние меню другим способом — это можно будет сделать без изменения внешнего интерфейса.

Возможный альтернативный вариант метода isOpen:

this.isOpen = function() {
  return hasClass(elem, 'open');
};

Для хорошей читаемости кода принято следующее соглашение.

Название метода-геттера принято начинать is..., если возвращают логическое значение (isOpen), и с get.. в остальных случаях (getValue).

Сеттеры

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

Их название традиционно начинается со слова set....

Простейший метод-сеттер занимается только изменением переменной, и выглядит примерно так:

this.setOpen = function(newIsOpen) {
  isOpen = newIsOpen;
};

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

this.setOpen = function(newIsOpen) {
  if (isOpen == newIsOpen) return; // состояние без изменений -> return
  this.toggle(); 
  isOpen = newIsOpen; 
};

Может быть, вы захотите разрешить изменить состояние, но не прочитать его. Тогда будет только сеттер. А может быть — и то и другое, тогда будет и геттер и сеттер. Все зависит от того, что вы хотите разрешить, а что - нет.

Геттеры/сеттеры в ES5

Современный стандарт JavaScript позволяет описывать геттеры и сеттеры для публичного свойства через вызов Object.defineProperty(obj, prop, descriptor).

Снаружи это выглядит так:

  • Обращение из внешнего кода происходит как будто к обычному свойству.
  • В объекте такого свойства на самом деле нет. JavaScript вызывает для чтения/записи специальные функции get/set.

Например:

function Colorer(elem) {

  // определим свойство this.color

  Object.defineProperty(this, 'color', {  // Все браузеры, IE9+ 
    get: function() { // геттер
      return elem.style.backgroundColor;
    },
    
    set: function(value) { // сеттер
      elem.style.backgroundColor = value;
    }
  });

};
 
var colorer = new Colorer(document.body);

*!*
colorer.color = "blue";
alert("Поменяли цвет на: " + colorer.color); 

colorer.color = "";
alert("Вернули цвет: " + colorer.color); 
*/!*

Вызов Object.defineProperty(obj, property, descriptor), использованный здесь, позволяет определить свойство obj.property со специальным дескриптором descriptor.

В дескрипторе может быть различная информация о свойстве, а в частности —
функции получения свойства get и установки set.

Удобный рефакторинг

Такие геттеры и сеттеры делают возможным лёгкий рефакторинг публичного свойства.

Допустим, в начале разработки есть публичное свойство, например menu.isOpen. Мы и не планируем обрабатывать его как-то особым образом.

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

Современный JavaScript позволит заменить свойство isOpen на дескриптор с get/set без изменения внешнего кода, который продолжает обращаться к «свойству».

Также можно задать геттеры/сеттеры для свойства в обычном определении объекта:

var colorer = {  // все браузеры, IE9+
  get color() { ... },
  set color(value) { ... }
}

Итого

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

    В терминологии ООП свойства и методы, которые доступны только внутри объекта, называются «приватными». В некоторых языках есть специальный синтаксис для таких свойств. В JavaScript мы можем использовать локальные переменные:

    function Menu(*!*menuId*/!*) {
      var self = this; // для обращения к объекту из приватных методов
    
      this.property = публичное свойство Menu
      
      *!*var property*/!* = приватное свойство Menu
    
      this.method = function() { публичный метод Menu }
    
      *!*function method(args)*/!* { приватный метод Menu }
    }
    

    Параметр конструктора menuId также автоматически становится приватным свойством.

Цели, которые мы достигаем, защищая внутренний интерфейс:

  • Явная индикация, куда можно лазать, а куда — нет. Защита объекта от внутренних изменений, которые могут «сломать» его.
  • Удобство при разработке и доработке. Всегда понятно, какие методы могли быть вызваны снаружи, а какие — нет. Если метод внутренний — его можно рефакторить как угодно.
  • Управление сложностью. Разработчик, который хочет использовать объект, должен видеть только простой внешний интерфейс, без перегрузки деталями его функционирования.

Особым случаем является создание виджетов.

В идеале меню должно быть «черным ящиком», всё взаимодействие с которым осуществляется через объект.

DOM-структура графической компоненты должна, по возможности, быть изолирована от внешнего воздействия.

Технически этого добиться сложно, т.к. DOM доступен всем, но тут уже вступают в дело «правила хорошего тона». Есть объект меню — делаем всё через него.

Для того, чтобы разрешить только получение значения — создают методы-геттеры. Чтобы разрешить только изменение — методы-сеттеры.

В современном стандарте JavaScript для этого предусмотрен специальный синтаксис, в частности метод Object.defineProperty(obj, prop, descriptor).

Есть меню: tutorial/oop/menu-itemvalue-src/index.html

Его код обсуждался в статье. Создайте для меню дополнительные публичные методы:

  • getItemValue(num) возвращает текст внутри пункта с номером num.
  • setItemValue(num, html) меняет текст внутри пункта с номером num на html.

Пример работы (нажмите на кнопки):

Решение
Решение

Методы:

function Menu(menuId) {
  // ...

  this.getItemValue = function(num) {
    return elem.getElementsByTagName('LI')[num].innerHTML;
  };


  this.setItemValue = function(num, html) {
    elem.getElementsByTagName('LI')[num].innerHTML = html;
  };
  // ...
}

Полный код: tutorial/oop/menu-itemvalue/index.html.

Напишите функцию-конструктор Voter(id) для голосовалки.
Она должна получать id элемента в следующей разметке:

<div id="voter" class="voter">
  <span class="down">—</span>
  <span class="vote">0</span>
  <span class="up">+</span>
</div>

По клику на + и число должно увеличиваться и уменьшаться, соответственно.

Единственный публичный метод: setVote(vote) должен устанавливать текущее число-значение.

Все остальные методы и свойства пусть будут приватными.

Результат:

Исходный документ: tutorial/oop/voter-src/index.html.

Решение
Решение

Решение: tutorial/oop/voter-private/index.html.


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!