Русский
Русский
English
Статистика
Реклама

Инкапсуляция

Перевод Объектно-ориентированный JavaScript простыми словами

07.10.2020 12:08:05 | Автор: admin


Доброго времени суток, друзья!

В JavaScript существует 4 способа создать объект:

  • Функция-контруктор (constructor function)
  • Класс (class)
  • Связывание объектов (object linking to other object, OLOO)
  • Фабричная функция (factory function)

Какой метод следует использовать? Какой из них является лучшим?

Для того, чтобы ответить на эти вопросы мы не только рассмотрим каждый подход в отдельности, но и сравним между собой классы и фабричные функции по следующим критериям: наследование, инкапсуляция, ключевое слово this, обработчики событий.

Давайте начнем с того, что такое объектно-ориентированное программирование (ООП).

Что такое ООП?


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

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

Второй аспект ООП состоит в структурировании кода, когда у нас имеется несколько проектов разного уровня. Это называется наследованием (inheritance) или классификацией (созданием подклассов) (subclassing).

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

Перейдем с способам создания объектов.

Способы создания объекта


Функция-конструктор

Конструкторами являются функции, в которых используется ключевое слово this.

    function Human(firstName, lastName) {        this.firstName = firstName        this.lastName = lastName    }

this позволяет сохранять и получать доступ к уникальным значениям создаваемого экземпляра. Экземпляры создаются с помощью ключевого слова new.

const chris = new Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyierconst zell = new Human('Zell', 'Liew')console.log(zell.firstName) // Zellconsole.log(zell.lastName) // Liew

Класс

Классы являются абстракцией (синтаксическим сахаром) над функциями-конструкторами. Они облегчают задачу создания экземпляров.

    class Human {        constructor(firstName, lastName) {            this.firstName = firstName            this.lastName = lastName        }    }

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

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

Экземпляры также создаются с помощью ключевого слова new.

const chris = new Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Связывание объектов

Данный способ создания объектов был предложен Kyle Simpson. В данном подходе мы определяем проект как обычный объект. Затем с помощью метода (который, как правило, называется init, но это не обязательно, в отличие от constructor в классе) мы инициализируем экземпляр.

const Human = {    init(firstName, lastName) {        this.firstName = firstName        this.lastName = lastName    }}

Для создания экземпляра используется Object.create. После создания экземпляра вызывается init.

const chris = Object.create(Human)chris.init('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Код можно немного улучшить, если вернуть this в init.

const Human = {  init () {    // ...    return this  }}const chris = Object.create(Human).init('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Фабричная функция

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

Вот простой пример фабричной функции.

function Human(firstName, lastName) {    return {        firstName,        lastName    }}

Для создания экземпляра нам не требуется ключевое слово this. Мы просто вызываем функцию.

const chris = Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

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

Определение свойств и методов


Методы это функции, объявленные в качестве свойств объекта.

    const someObject = {        someMethod () { /* ... */ }    }

В ООП существует два способа определения свойств и методов:

  • В экземпляре
  • В прототипе

Определение свойств и методов в конструкторе

Для определения свойства в экземпляре необходимо добавить его в функцию-конструктор. Убедитесь, что добавляете свойство к this.

function Human (firstName, lastName) {  // Определяем свойства  this.firstName = firstName  this.lastname = lastName  // Определяем методы  this.sayHello = function () {    console.log(`Hello, I'm ${firstName}`)  }}const chris = new Human('Chris', 'Coyier')console.log(chris)



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

Для добавления свойства в прототип используют prototype.

function Human (firstName, lastName) {  this.firstName = firstName  this.lastname = lastName}// Определяем метод в прототипеHuman.prototype.sayHello = function () {  console.log(`Hello, I'm ${this.firstName}`)}



Создание нескольких методов может быть утомительным.

// Определение методов в прототипеHuman.prototype.method1 = function () { /*...*/ }Human.prototype.method2 = function () { /*...*/ }Human.prototype.method3 = function () { /*...*/ }

Можно облегчить себе жизнь с помощью Object.assign.

Object.assign(Human.prototype, {  method1 () { /*...*/ },  method2 () { /*...*/ },  method3 () { /*...*/ }})

Определение свойств и методов в классе

Свойства экземпляра можно определить в constructor.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName      this.lastname = lastName      this.sayHello = function () {        console.log(`Hello, I'm ${firstName}`)      }  }}



Свойства прототипа определяются после constructor в виде обычной функции.

class Human (firstName, lastName) {  constructor (firstName, lastName) { /* ... */ }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}



Создание нескольких методов в классе проще, чем в конструкторе. Для этого нам не нужен Object.assign. Мы просто добавляем другие функции.

class Human (firstName, lastName) {  constructor (firstName, lastName) { /* ... */ }  method1 () { /*...*/ }  method2 () { /*...*/ }  method3 () { /*...*/ }}

Определение свойств и методов при связывании объектов

Для определения свойств экземпляра мы добавляем свойство к this.

const Human = {  init (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    this.sayHello = function () {      console.log(`Hello, I'm ${firstName}`)    }    return this  }}const chris = Object.create(Human).init('Chris', 'Coyier')console.log(chris)



Метод прототипа определяется как обычный объект.

const Human = {  init () { /*...*/ },  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}



Определение свойств и методов в фабричных функциях (ФФ)

Свойства и методы могут быть включены в состав возвращаемого объекта.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}



При использовании ФФ нельзя определять свойства прототипа. Если вам нужны такие свойства, можно вернуть экземпляр класса, конструктора или связывания объектов (но это не имеет смысла).

// Не делайте этогоfunction createHuman (...args) {  return new Human(...args)}

Где определять свойства и методы


Где следует определять свойства и методы? В экземпляре или в прототипе?

Многие считают, что для этого лучше использовать прототипы.

Однако на самом деле это не имеет особого значения.

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

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

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

Предварительный вывод


На основе изученного материала можно сделать несколько выводов. Это мое личное мнение.

  • Классы лучше конструкторов, поскольку в них легче определять несколько методов.
  • Связывание объектов кажется странным из-за необходимости использовать Object.create. Я постоянно забывал об этом при изучении данного подхода. Для меня это было достаточной причиной отказаться от его дальнейшего использования.
  • Классы и ФФ использовать проще всего. Проблема состоит в том, что в ФФ нельзя использовать прототипы. Но, как я отметил ранее, это не имеет особого значения.

Далее мы будем сравнивать между собой классы и ФФ как два лучших способа создания объектов в JavaScript.

Классы против ФФ Наследование


Прежде чем переходить к сравнению классов и ФФ, необходимо познакомиться с тремя концепциями, лежащими в основе ООП:

  • наследование
  • инкапсуляция
  • this

Начнем с наследования.

Что такое наследование?

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

Это происходит двумя способами:

  • с помощью инициализации экземпляра
  • с помощью цепочки прототипов

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

Понимание создания подклассов

Создание подклассов это когда дочерний проект расширяет родительский.

Рассмотрим это на примере классов.

Создание подклассов с помощью класса

Для расширения родительского класса используется ключевое слово extends.

class Child extends Parent {    // ...}

Например, давайте создадим класс Developer, расширяющий класс Human.

// класс Humanclass Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}

Класс Developer будет расширять Human следующим образом:

class Developer extends Human {  constructor(firstName, lastName) {    super(firstName, lastName)  }    // ...}

Ключевое слово super вызывает constructor класса Human. Если вам это не нужно, super можно опустить.

class Developer extends Human {  // ...}

Допустим, Developer умеет писать код (кто бы мог подумать). Добавим ему соответствующий метод.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

Вот пример экземпляра класса Developer.

const chris = new Developer('Chris', 'Coyier')console.log(chris)



Создание подклассов с помощью ФФ

Для создания подклассов с помощью ФФ необходимо выполнить 4 действия:

  • создать новую ФФ
  • создать экземпляр родительского проекта
  • создать копию этого экземпляра
  • добавить в эту копию свойства и методы

Данный процесс выглядит так.

function Subclass (...args) {  const instance = ParentClass(...args)  return Object.assign({}, instance, {    // Свойства и методы  })}

Создадим подкласс Developer. Вот как выглядит ФФ Human.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}

Создаем Developer.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    // Свойства и методы  })}

Добавляем ему метод code.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    }  })}

Создаем экземпляр Developer.

const chris = Developer('Chris', 'Coyier')console.log(chris)



Перезапись родительского метода

Иногда возникает необходимость перезаписать родительский метод внутри подкласса. Это можно сделать следующим образом:

  • создать метод с тем же именем
  • вызвать родительский метод (опционально)
  • создать новый метод в подклассе

Данный процесс выглядит так.

class Developer extends Human {  sayHello () {    // Вызываем родительский метод    super.sayHello()    // Создаем новый метод    console.log(`I'm a developer.`)  }}const chris = new Developer('Chris', 'Coyier')chris.sayHello()



Тот же процесс с использованием ФФ.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {      sayHello () {        // Вызываем родительский метод        human.sayHello()        // Создаем новый метод        console.log(`I'm a developer.`)      }  })}const chris = new Developer('Chris', 'Coyier')chris.sayHello()



Наследование против композиции

Разговор о наследовании редко обходится без упоминания композиции. Эксперты вроде Eric Elliot считают, что всегда, когда это возможно, следует использовать композицию.

Что же такое композиция?

Понимание композиции

По сути, композиция это объединение нескольких вещей в одну. Наиболее распространенным и самым простым способом объединения объектов является использование Object.assign.

const one = { one: 'one' }const two = { two: 'two' }const combined = Object.assign({}, one, two)

Композицию легче всего объяснить на примере. Допустим, у нас имеется два подкласса, Developer и Designer. Дизайнеры умеют разрабатывать дизайн, а разработчики писать код. Оба наследуют от класса Human.

class Human {  constructor(firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}class Designer extends Human {  design (thing) {    console.log(`${this.firstName} designed ${thing}`)  }}class Developer extends Designer {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

Теперь предположим, что мы хотим создать третий подкласс. Этот подкласс должен быть смесью дизайнера и разработчика он должен уметь как разрабатывать дизайн, так и писать код. Назовем его DesignerDeveloper (или, если угодно, DeveloperDesigner).

Как нам его создать?

Мы не может одновременно расширить классы Designer и Developer. Это невозможно, поскольку мы не можем решить, какие свойства должны быть первыми. Это называется проблемой ромба (ромбовидным наследованием).



Проблема ромба может быть решена с помощью Object.assign, если мы отдадим одному объекту приоритет над другим. Однако, в JavaScript не поддерживается множественное наследование.

// Не работаетclass DesignerDeveloper extends Developer, Designer {  // ...}

Здесь нам пригодится композиция.

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

Реализация этого подхода приводит к следующему.

const skills = {    code (thing) { /* ... */ },    design (thing) { /* ... */ },    sayHello () { /* ... */ }}

Нам больше не нужен класс Human, ведь мы можем создать три разных класса с помощью указанного объекта.

Вот код для DesignerDeveloper.

class DesignerDeveloper {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      code: skills.code,      design: skills.design,      sayHello: skills.sayHello    })  }}const chris = new DesignerDeveloper('Chris', 'Coyier')console.log(chris)



Мы можем сделать тоже самое для Designer и Developer.

class Designer {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      design: skills.design,      sayHello: skills.sayHello    })  }}class Developer {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      code: skills.code,      sayHello: skills.sayHello    })  }}

Вы заметили, что мы создаем методы в экземпляре? Это лишь один из возможных вариантов. Мы также можем поместить методы в прототип, но я нахожу это лишним (при таком подходе кажется, что мы вернулись к конструкторам).

class DesignerDeveloper {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }}Object.assign(DesignerDeveloper.prototype, {  code: skills.code,  design: skills.design,  sayHello: skills.sayHello})



Используйте тот подход, который считаете самым подходящим. Результат будет одинаковым.

Композиция с помощью ФФ

Композиция с помощью ФФ заключается в добавлении распределенных методов в возвращаемый объект.

function DesignerDeveloper (firstName, lastName) {  return {    firstName,    lastName,    code: skills.code,    design: skills.design,    sayHello: skills.sayHello  }}



Наследование и композиция

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

Возвращаясь к примеру с Designer, Developer и DesignerDeveloper, нельзя не отметить, что они также являются людьми. Поэтому они могут расширять класс Human.

Вот пример наследование и композиции с использованием синтаксиса классов.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}class DesignerDeveloper extends Human {}Object.assign(DesignerDeveloper.prototype, {  code: skills.code,  design: skills.design})



А вот тоже самое с использованием ФФ.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${this.firstName}`)    }  }}function DesignerDeveloper (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code: skills.code,    design: skills.design  })}



Подклассы в реальном мире

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

Например: событие click является MouseEvent (событием мыши). MouseEvent это подкласс UIEvent (событие пользовательского интерфейса), который, в свою очередь, является подклассом Event (событие).



Другой пример: HTML Elements (элементы) являются подклассами Nodes (узлов). Поэтому они могут использовать все свойства и методы узлов.



Предварительный вывод относительно наследования

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

Продолжим сравнение.

Классы против ФФ Инкапсуляция


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

В JavaScript скрываемыми сущностями являются переменные и функции, которые доступны только в текущем контексте. В данном случае контекст это тоже самое, что область видимости.

Простая инкапсуляция

Простейшей формой инкапсуляции является блок кода.

{  // Переменные, объявленные здесь, будут иметь блочную область видимости}

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

const food = 'Hamburger'{  console.log(food)}



Но не наоборот.

{  const food = 'Hamburger'}console.log(food)



Обратите внимание, что переменные, объявленные с помощью ключевого слова var, имеют глобальную или фукнциональную область видимости. Старайтесь не использовать var для объявления переменных.

Инкапсуляция с помощью функции

Функциональная область видимости похожа на блочную. Переменные, объявленные в функции, доступны только внутри нее. Это относится ко всем переменным, даже объявленным с помощью var.

function sayFood () {  const food = 'Hamburger'}sayFood()console.log(food)



Когда же мы находимся внутри функции, то имеем доступ к переменным, объявленным за ее пределами.

const food = 'Hamburger'function sayFood () {  console.log(food)}sayFood()



Функции могут возвращать значения, которые могут быть использованы впоследствии за пределами функции.

function sayFood () {  return 'Hamburger'}console.log(sayFood())



Замыкание

Замыкание это продвинутая форма инкапсуляции. Это просто функция внутри другой функции.

// Пример замыканияfunction outsideFunction () {  function insideFunction () { /* ... */ }}


Переменные, объявленные в outsideFunction, могут использоваться в insideFunction.

function outsideFunction () {  const food = 'Hamburger'  console.log('Called outside')  return function insideFunction () {    console.log('Called inside')    console.log(food)  }}// Вызываем outsideFunction, которая возвращает insideFunction// Сохраняем insideFunction в переменной "fn"const fn = outsideFunction()



Инкапсуляция и ООП

При создании объектов мы хотим, чтобы одни свойства были открытыми (публичными), а другие закрытыми (частными или приватными).

Рассмотрим пример. Скажем, у нас имеется проект Car. При создании нового экземпляра мы добавляем ему свойство fuel (топливо) со значением 50.

class Car {  constructor () {    this.fuel = 50  }}


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

const car = new Car()console.log(car.fuel) // 50


Пользователи также могут самостоятельно устанавливать количество топлива.

const car = new Car()car.fuel = 3000console.log(car.fuel) // 3000

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

Существует два способа это сделать:

  • использование частных свойств по соглашению
  • использование настоящих частных полей

Частные свойства по соглашению

В JavaScript частные переменные и свойства, обычно, обозначаются с помощью нижнего подчеркивания.

class Car {  constructor () {    // Отмечаем свойство "fuel" как частное, которое не должно использоваться за пределами класса    this._fuel = 50  }}

Как правило, мы создаем методы для управления частными свойствами.

class Car {  constructor () {    this._fuel = 50  }  getFuel () {    return this._fuel  }  setFuel (value) {    this._fuel = value    // Определяем вместимость бака    if (value > 100) this._fuel = 100  }}

Для определения и установки количества топлива пользователи должны использовать методы getFuel и setFuel, соответственно.

const car = new Car()console.log(car.getFuel()) // 50car.setFuel(3000)console.log(car.getFuel()) // 100

Но переменная "_fuel" в действительности не является частной. Она доступна извне.

const car = new Car()console.log(car.getFuel()) // 50car._fuel = 3000console.log(car.getFuel()) // 3000

Для ограничения доступа к переменным следует использовать настоящие частные поля.

По-настоящему частные поля

Поля это термин, объединяющий переменные, свойства и методы.

Частные поля классов

Классы позволяют создавать частные переменные с помощью префикса "#".

class Car {  constructor () {    this.#fuel = 50  }}

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



Частные переменные должны определяться вне конструктора.

class Car {  // Определяем частную переменную  #fuel  constructor () {    // Используем ее    this.#fuel = 50  }}

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

class Car {  #fuel = 50}

Теперь переменная "#fuel" доступна только внутри класса. Попытка получить к ней доступ за пределами класса приведет к возникновению ошибки.

const car = new Car()console.log(car.#fuel)



Для управления переменной нам нужны соответствующие методы.

class Car {  #fuel = 50  getFuel () {    return this.#fuel  }  setFuel (value) {    this.#fuel = value    if (value > 100) this.#fuel = 100  }}const car = new Car()console.log(car.getFuel()) // 50car.setFuel(3000)console.log(car.getFuel()) // 100

Лично я предпочитаю использовать для этого геттеры и сеттеры. Я нахожу такой синтаксис более читаемым.

class Car {  #fuel = 50  get fuel () {    return this.#fuel  }  set fuel (value) {    this.#fuel = value    if (value > 100) this.#fuel = 100  }}const car = new Car()console.log(car.fuel) // 50car.fuel = 3000console.log(car.fuel) // 100

Частные поля ФФ

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

function Car () {  const fuel = 50}const car = new Car()console.log(car.fuel) // undefinedconsole.log(fuel) // Error: "fuel" is not defined

Для управления частной переменной fuel также используются геттеры и сеттеры.

function Car () {  const fuel = 50  return {    get fuel () {      return fuel    },    set fuel (value) {      fuel = value      if (value > 100) fuel = 100    }  }}const car = new Car()console.log(car.fuel) // 50car.fuel = 3000console.log(car.fuel) // 100

Вот так. Легко и просто!

Предварительный вывод относительно инкапсуляции

Инкапсуляция с помощью ФФ проще и легче для восприятия. Она основана на области видимости, которая является важной частью JavaScript.

Инкапсуляция с помощью классов предполагает использование префикса "#", что может быть несколько утомительным.

Классы против ФФ this


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

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

Названными контекстами являются:

  • глобальный контекст
  • контекст создаваемого объекта
  • контекст свойства или метода объекта
  • простая функция
  • стрелочная функция
  • контекст обработчика событий

Но вернемся к статье. Давайте рассмотрим особенности использования this в классах и ФФ.

Использование this в классах

При использовании в классе this указывает на создаваемый экземпляр (контекст свойства/метода). Вот почему экземпляр инициализируется в constructor.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    console.log(this)  }}const chris = new Human('Chris', 'Coyier')



Использование this в функциях-конструкторах

При использовании this внутри функции и new для создания экземпляра, this будет указывать на экземпляр.

function Human (firstName, lastName) {  this.firstName = firstName  this.lastName = lastName  console.log(this)}const chris = new Human('Chris', 'Coyier')



В отличии от ФК в ФФ this указывает на window (в контексте модуля this вообще имеет значение undefined).

// Для создания экземпляра не используется ключевое слово "new"function Human (firstName, lastName) {  this.firstName = firstName  this.lastName = lastName  console.log(this)}const chris = Human('Chris', 'Coyier')



Таким образом, в ФФ не следует использовать this. В этом состоит одно из основных отличий между ФФ и ФК.

Использование this в ФФ

Для того, чтобы иметь возможность использовать this в ФФ, необходимо создать контекст свойства/метода.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayThis () {      console.log(this)    }  }}const chris = Human('Chris', 'Coyier')chris.sayThis()



Несмотря на то, что мы можем использовать this в ФФ, нам это не нужно. Мы можем создать переменную, указывающую на экземпляр. Такая переменная может использоваться вместо this.

function Human (firstName, lastName) {  const human = {    firstName,    lastName,    sayHello() {      console.log(`Hi, I'm ${human.firstName}`)    }  }  return human}const chris = Human('Chris', 'Coyier')chris.sayHello()

human.firstName является более точным, нежели this.firstName, поскольку human явно указывает на экземпляр.

На самом деле нам даже не нужно писать human.firstName. Мы можем ограничиться firstName, поскольку данная переменная имеет лексическую область видимости (это когда значение переменной берется из внешнего окружения).

function Human (firstName, lastName) {  const human = {    firstName,    lastName,    sayHello() {      console.log(`Hi, I'm ${firstName}`)    }  }  return human}const chris = Human('Chris', 'Coyier')chris.sayHello()



Рассмотрим более сложный пример.

Сложный пример


Условия таковы: у нас имеется проект Human со свойствами firstName и lastName и методом sayHello.

Также у нас имеется проект Developer, наследующий от Human. Разработчики умеют писать код, поэтому у них должен быть метод code. Кроме того, они должны заявлять о своей принадлежности к касте разработчиков, поэтому нам необходимо перезаписать метод sayHello.

Реализуем указанную логику с помощью классов и ФФ.

Классы

Создаем проект Human.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastname = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}

Создаем проект Developer с методом code.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

Перезаписываем метод sayHello.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }  sayHello () {    super.sayHello()    console.log(`I'm a developer`)  }}

ФФ (с использованием this)

Создаем проект Human.

function Human () {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${this.firstName}`)    }  }}

Создаем проект Developer с методом code.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    }  })}

Перезаписываем метод sayHello.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    },    sayHello () {      human.sayHello()      console.log('I\'m a developer')    }  })}

ФФ (без this)

Поскольку firstName за счет лексической области видимости доступна напрямую мы можем опустить this.

function Human (firstName, lastName) {  return {    // ...    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}function Developer (firstName, lastName) {  // ...  return Object.assign({}, human, {    code (thing) {      console.log(`${firstName} coded ${thing}`)    },    sayHello () { /* ... */ }  })}

Предварительный вывод относительно this

Простыми словами, классы требуют использования this, а ФФ нет. В данном случае я предпочитаю использовать ФФ, поскольку:

  • контекст this может меняться
  • код, написанный с помощью ФФ, является более коротким и чистым (в том числе, благодаря автоматической инкапсуляции переменных)

Классы против ФФ Обработчики событий


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

Поскольку обработчики событий изменяют контекст this, работа с ними в классах может быть проблематичной. В тоже время в ФФ таких проблем не возникает.

Однако изменение контекста this не имеет значения, если мы знаем, как с этим справиться. Рассмотрим простой пример.

Создание счетчика

Для создания счетчика воспользуемся полученными знаниями, включая частные переменные.

Наш счетчик будет содержать две вещи:

  • сам счетчик
  • кнопку для увеличения его значения



Вот как может выглядеть разметка:

<div class="counter">  <p>Count: <span>0</span></p>  <button>Increase Count</button></div>

Создание счетчика с помощью класса

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

class Counter {  constructor (counter) {    // ...  }}// Использованиеconst counter = new Counter(document.querySelector('.counter'))

В классе необходимо получить 2 элемента:

  • <span>, содержащий значение счетчика нам нужно обновлять это значение при увеличении счетчика
  • <button> нам нужно добавить обработчик событий, вызываемых данным элементом

class Counter {  constructor (counter) {    this.countElement = counter.querySelector('span')    this.buttonElement = counter.querySelector('button')  }}

Далее мы инициализируем переменную count текстовым содержимым countElement. Указанная переменная должна быть частной.

class Counter {  #count  constructor (counter) {    // ...    this.#count = parseInt(countElement.textContent)  }}

При нажатии кнопки значение счетчика должно увеличиваться на 1. Реализуем это с помощью метода increaseCount.

class Counter {  #count  constructor (counter) { /* ... */ }  increaseCount () {    this.#count = this.#count + 1  }}

Теперь нам необходимо обновить DOM. Реализуем это с помощью метода updateCount, вызываемого внутри increaseCount:

class Counter {  #count  constructor (counter) { /* ... */ }  increaseCount () {    this.#count = this.#count + 1    this.updateCount()  }  updateCount () {    this.countElement.textContent = this.#count  }}

Осталось добавить обработчик событий.

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

Добавим обработчик к this.buttonElement. К сожалению, мы не можем использовать increaseCount в качестве функции обратного вызова. Это приведет к ошибке.

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount)  }  // Методы}



Исключение выбрасывается, потому что this указывает на buttonElement (контекст обработчика событий). В этом можно убедиться, если вывести значение this в консоль.



Значение this необходимо изменить таким образом, чтобы оно указывало на экземпляр. Это можно сделать двумя способами:

  • с помощью bind
  • с помощью стрелочной фукнции

Большинство использует первый способ (однако второй проще).

Добавление обработчика событий с помощью bind

bind возвращает новую функцию. В качестве первого аргумента ему передается объект, на который будет указывать this (к которому this будет привязан).

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount.bind(this))  }  // ...}

Это работает, но выглядит это не очень хорошо. К тому же bind это продвинутая функция, с которой сложно иметь дело новичкам.

Стрелочные функции

Стрелочные функции, помимо прочего, не имеют собственного this. Они заимствуют его из лексического (внешнего) окружения. Поэтому код счетчика может быть переписан следующим образом:

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', () => {      this.increaseCount()    })  }  // Методы}

Есть еще более простой способ. Мы можем создать increaseCount в виде стрелочной функции. В этом случае this будет указывать на экземпляр.

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount)  }  increaseCount = () => {    this.#count = this.#count + 1    this.updateCounter()  }  // ...}

Код

Вот полный код примера:



Создание счетчика с помощью ФФ

Начало аналогичное мы просим пользователя найти и передать разметку счетчика:

function Counter (counter) {  // ...}const counter = Counter(document.querySelector('.counter'))

Получаем необходимые элементы, которые по умолчанию будут частными:

function Counter (counter) {  const countElement = counter.querySelector('span')  const buttonElement = counter.querySelector('button')}

Инициализируем переменную count:

function Counter (counter) {  const countElement = counter.querySelector('span')  const buttonElement = counter.querySelector('button')  let count = parseInt(countElement.textContext)}

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

function Counter (counter) {  // ...  const counter = {    increaseCount () {      count = count + 1    }  }}

DOM будет обновляться с помощью метода updateCount, который вызывается внутри increaseCount:

function Counter (counter) {  // ...  const counter = {    increaseCount () {      count = count + 1      counter.updateCount()    },    updateCount () {      increaseCount()    }  }}

Обратите внимание, что вместо this.updateCount мы используем counter.updateCount.

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

Мы можем добавить обработчик событий к buttonElement, используя counter.increaseCount в качестве колбэка.

Это будет работать, поскольку мы не используем this, поэтому для нас не имеет значения то обстоятельство, что обработчик меняет контекст this.

function Counter (counterElement) {  // Переменные  // Методы  const counter = { /* ... */ }  // Обработчики событий  buttonElement.addEventListener('click', counter.increaseCount)}

Первая особенность this

Вы можете использовать this в ФФ, но только в контексте метода.

В следующем примере при вызове counter.increaseCount будет вызван counter.updateCount, поскольку this указывает на counter:

function Counter (counterElement) {  // Переменные  // Методы  const counter = {    increaseCount() {      count = count + 1      this.updateCount()    }  }  // Обработчики событий  buttonElement.addEventListener('click', counter.increaseCount)}

Однако обработчик событий работать не будет, потому что значение this изменилось. Данная проблема может быть решена с помощью bind, но не с помощью стрелочных функций.

Вторая особенность this

При использовании синтаксиса ФФ, мы не можем создавать методы в виде стрелочных функций, потому что методы создаются в контексте функции, т.е. this будет указывать на window:

function Counter (counterElement) {  // ...  const counter = {    // Не делайте так    // Не работает, поскольку this указывает на window    increaseCount: () => {      count = count + 1      this.updateCount()    }  }  // ...}

Поэтому при использовании ФФ я настоятельно рекомендую избегать использования this.

Код




Вердикт относительно обработчиков событий

Обработчики событий меняют значение this, поэтому использовать this следует крайне осторожно. При использовании классов советую создавать колбэки обработчиков событий в виде стрелочных функций. Тогда вам не придется прибегать к услугам bind.

При использовании ФФ рекомендую вообще обходиться без this.

Заключение


Итак, в данной статье мы рассмотрели четыре способа создания объектов в JavaScript:

  • Функции-конструкторы
  • Классы
  • Связывание объектов
  • Фабричные функции

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

Во-вторых, мы увидели, что подклассы легче создавать с помощью классов. Однако, в случае композиции лучше использовать ФФ.

В-третьих, мы резюмировали, что, когда речь идет об инкапсуляции, ФФ имеют преимущество перед классами, поскольку последние требуют использования специального префикса "#", а ФФ делают переменные частными автоматически.

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

На этом у меня все. Надеюсь, статья вам понравилась. Благодарю за внимание.
Подробнее..

Шаблоны GRASP Polymorphism, Pure Fabrication, Indirection, Protected Variations

01.10.2020 18:11:51 | Автор: admin
Привет, Хабр! Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса Архитектор высоких нагрузок в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

Специально к старту нового набора на курс Архитектура и шаблоны проектирования я продолжаю серию своих публикаций про шаблоны GRASP.



Введение


Описанные в книге Craig'а Larman'а Applying UML and patterns, 3rd edition, GRASP'овские паттерны являются обобщением GoF'овских паттернов, а также непосредственным следствием принципов ООП. Они дополняют недостающую ступеньку в логической лестнице, которая позволяет получить GoF'овские паттерны из принципов ООП. Шаблоны GRASP являются скорее не паттернами проектирования (как GoF'овские), а фундаментальными принципами распределения ответственности между классами. Они, как показывает практика, не обладают особой популярностью, однако анализ спроектированных классов с использованием полного набора GRASP'овских паттернов является необходимым условием написания хорошего кода.

Полный список шаблонов GRASP состоит из 9 элементов:

  • Information Expert
  • Creator
  • Controller
  • Low Coupling
  • High Cohesion
  • Polymorphism

В прошлый раз мы обсудили паттерн Controller. Сегодня предлагаю рассмотреть оставшиеся паттерны из списка.

Polymorphism


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

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

Наличие в коде конструкции switch является нарушением данного принципа, switch'и подлежат рефакторингу.

Злоупотребление полиморфизмом приводит к переусложнению кода и в общем случае не приветствуется.

Pure Fabrication


Необходимо обеспечивать low coupling и high cohesion. Для этой цели может понадобиться синтезировать искуственную сущность. Паттерн Pure Fabrication говорит о том, что не стоит стесняться это сделать. В качестве примера можно рассматривать фасад к базе данных. Это чисто искуственный объект, не имеющий аналогов в предметной области. В общем случае любой фасад относится к Pure Fabrication (если это конечно не архитектурный фасад в соответствующим приложении).

Indirection


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

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

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

Protected Variations


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

На самом деле, это не паттерн, а цель, достигаемая соблюдением остальных паттернов.

Вывод


Шаблоны GRASP состоят из 8 паттернов:
1) Information Expert информацию обрабатываем там, где она содержится.
2) Creator создаем объекты там, где они нужны.
3) Controller выносим логику многопоточности в отдельный класс или компонент.
4) Low Coupling 5) High Cohesion проектируем классы с однородной бизнес-логикой и минимальным количеством связей между собой.
6) Polymorphism различные варианты поведения системы при необходимости оформляем в виде полиморфных вызовов.
7) Pure Fabrication не стесняемся создавать классы, не имеющие аналог в предметной области, если это необходимо для соблюдения Low Coupling и High Cohesion.
8) Indirection любой класс вызываем через его интерфейс.
9) Protected Variations применяя все вышесказанное, получаем устойчивый к изменениям код.



Читать ещё:


Подробнее..

Принципы объектно-ориентированного программирования

28.10.2020 16:07:32 | Автор: admin
Привет, Хабр! Меня зовут Владислав Родин. В настоящее время я являюсь руководителем курса Архитектор высоких нагрузок в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

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



Введение


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

Вообще устроено все следующим образом: есть само объектно-ориентированное программирование. У него есть принципы. Из принципов объектно-ориентированного программирования следуют разобранные нам шаблоны GRASP (как вариант SOLID принципы), из которых, в свою очередь, следуют шаблоны GoF. Из них же следует ряд интересных вещей, например, enterprise паттерны.

Объектно-ориентированная парадигма


Определение гласит, что Объектно-ориентированное программирование это парадигма программирования, в которой основной концепцией является понятие объекта, который отождествляется с предметной областью.

Таким образом, система представляется в виде набора объектов предметной области, которые взаимодействуют между собой некоторым образом. Каждый объект обладает тремя cоставляющими: идентичность (identity), состояние (state) и поведение (behaviour).

Состояние объекта это набор всех его полей и их значений.

Поведение объекта это набор всех методов класса объекта.

Идентичность объекта это то, что отличает один объект класса от другого объекта класса. С точки зрения Java, именно по идентичности определяется метод equals.

Принципы объектно-ориентированного программирования


Объектно-ориентированное программирование обладает рядом принципов. Представление об их количестве расходится. Кто-то утверждает, что их три (старая школа программистов), кто-то, что их четыре (новая школа программистов):

  1. Абстрация
  2. Инкапсуляция
  3. Наследование
  4. Полиморфизм

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

Инкапсуляция


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

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

Таким образом, получается, что если класс A обращается к полям класса B напрямую, это приводит не к тому, что нарушается информационная безопасность, а к тому, что класс A завязывается на внутренне устройство класса B, и попытка изменить внутреннее устройство класса B приведет к изменению класса А. Более того, класс A не просто так работает с полями класса B, он работает по некоторой бизнес-логике. То есть логика по работе с состоянием класса В лежит в классе А, и когда мы захотим переиспользовать класс В, это не удастся сделать, ведь без кусочка класса А класс В может быть бесполезным, что приведет к тому, что класс В придется отдавать вместе с классом А. Экстраполируя это на всю систему, получается, что переиспользовать можно будет только всю систему целиком.

Инкапсуляция является самым недооцененным принципом, который, к сожалению, мало кем интерпретируется правильно. Она позволяет минимизировать число связей между классами и подсистемами и, соответственно, упростить независимую реализацию и модификацию классов и подсистем.

Наследование


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

Наследование является самым переоцененным принципом. Когда-то считалось, что У идеального программиста дерево наследования уходит в бесконечность и заканчивается абсолютно пустым объектом, потому как когда-то люди не очень хорошо понимали то, что наследование это способ выразить такое свойство реального мира как иерархичность, а не способ переиспользовать код, отнаследовав машину от холодильника, потому что у обоих предметов есть ручка. Наследования желательно по возможности избегать, потому что наследование является очень сильной связью. Для уменьшения количества уровней наследования рекомендуется строить дерево снизу-вверх.

Полиморфизм


Полиморфизм это возможность использовать классы потомки в контексте, который был предназначен для класса предка.

За самым садистским определением кроется возможность языка программирования для декомпозиции задачи и рефакторинга if'ов и switch'ей.

Подробнее..

Зачем нужно понимать ООП

03.12.2020 00:05:42 | Автор: admin


Часто я встречаю разработчиков, которые пишут код на объектно-ориентированном языке программирования, но не понимают принципов ООП. Это могут быть начинающие девелоперы, которые еще на собеседованиях сталкиваются с проблемами объяснения принципов. А также это могут быть, казалось бы, опытные программисты, которые не понимают принципов, заложенных в язык программирования, на котором они пишут. Второй случай хотелось бы встречать реже, но на практике это не так. Часто разработчики смотрят на наследование или полиморфизм, как на особенности языка, как на какой-то технический инструмент и не думают, о вещах, которые лежат в основе этих механизмов.

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

Примеры кода буду приводить из iOS разработки.

Я считаю, если ты пишешь код на объектно-ориентированном языке программирования, ты обязан не только знать определения, но и понимать суть, которая вложенная в эту парадигму.

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

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

Наследование


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

Так как ООП это про моделирование, то код мы пишем начиная с абстракции частей системы и взаимодействие между этими частями, которые мы должны записать в виде кода. Например, социальная сеть, которая состоит из пользователей, взаимодействующих друг с другом. Помимо пользователей, система состоит из более мелких компонентов, таких как сообщения, посты, лайки, комментарии. Даже сам пользователь может являть собой подсистему в системе. Как человек состоит из различных органов и частей тела: сердце, мозг, руки, пальцы, так и пользователь в системе может состоять из более мелких составляющих. Но уже не руки или глаза, а адрес, интересы, записи об образовании, которые можно выносить как отдельные объекты. Уже на этапе анализа можно проследить принципы объектно-ориентированного подхода. У нас есть пользователь абстракция реального человека. Но у пользователя могут быть разные роли: админ, обычный пользователь, VIP пользователь, анонимный посетитель. Они все являются абстракциями реальных людей и пользователями данной системы. Но каждая из вышеперечисленных ролей имеет свои особенности и при этом все имеют общее они пользуются системой и должны зарегистрироваться в системе (у каждого может быть свой способ) и они все должны пройти процедуру входа в систему.

Это и есть принцип наследования, где каждый админ/VIP-клиент/аноним являются пользователями, но не каждый пользователь должен быть админом или VIP-пользователем.

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

Еще пример ошибочной трактовки принципа наследования, это когда базовый класс и наследник являются представителями разных логических групп. Выглядит это следующим образом, реализуем MVC в iOS проекте, где UIViewController это Controller с абстрактными методами, которые должен реализовать наследник. А наследник это уже Model. Там где по логике проектирования должно быть взаимодействие между двумя группами классов, один класс становиться одновременно и Model и Controller. Не говорим уже о том, что UIViewController в реалиях iOS разработки еще и берет на себя роль View. В итоге мы получаем один объект, который делает все сам. Если у нас есть пользователь (User), то он будет и Controller и View одновременно.

Пример наследования в iOS
/**    Нужно сделать экран профиля пользователя, в котором отображаются имя и фамилия. Этот экран должен переиспользоваться. */// INCORRECTclass UserProfileViewController: UIViewController {    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    /**    Заполнение данных оставляем классу наследнику в виде абстрактных методов     */    // MARK: - Abstract methods    func firstName() -> String? {        return nil    }    func lastName() -> String? {        return nil    }    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        firstNameLabel.text = firstName()        lastNameLabel.text = lastName()    }}/**    Создаем класс-наследник, который отвечает за функцию заполнения данных. В таком случае, наследник будет выполнять роль не только UIViewController, а и роль модели, которая предоставляет данные для отображения. */class UserProfileModel: UserProfileViewController {    override func firstName() -> String? {        return "Name"    }    override func lastName() -> String? {        return "Last name"    }}// CORRECT    /**    Корректней будет, добавить новый класс-модель, которая будет предоставлять данные.     */class UserProfileModel {    func firstName() -> String? {        return "Name"    }    func lastName() -> String? {        return "Last name"    }}/**    В таком случае у нас будут два отдельных класса, каждый из которых имеет свою зону ответственности. */class UserProfileViewController: UIViewController {    var model: UserProfileModel?    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        setupUserInfo()    }    // MARK: - Private    private func setupUserInfo() {        firstNameLabel.text = model?.firstName()        lastNameLabel.text = model?.lastName()    }}// PERFECT/**    Еще лучше, взаимодействие между двумя типами классов, контроллер и модель, сделать через протокол, чтобы можно было создавать и использовать разные модели. */protocol UserProfileProtocol {    func firstName() -> String?    func lastName() -> String?}class UserProfileViewController: UIViewController {    var model: UserProfileProtocol?    // MARK: - IBOutlets    @IBOutlet private var firstNameLabel: UILabel!    @IBOutlet private var lastNameLabel: UILabel!    // MARK: - Lifecycle    override func viewDidLoad() {        super.viewDidLoad()        setupUserInfo()    }    // MARK: - Private    private func setupUserInfo() {        firstNameLabel.text = model?.firstName()        lastNameLabel.text = model?.lastName()    }}class UserProfileModel: UserProfileProtocol {        // MARK: - UserProfileProtocol        func firstName() -> String? {        return "Name"    }    func lastName() -> String? {        return "Last name"    }}


Представляю эту картину в реальной жизни, у нас есть регистратура, где некий администратор ведет записи о посетителях в тетрадку. Также в этой тетради можно читать данные о посетителях. Мы получаем посетителя Model, человека в регистратуре Controller и тетрадь View. И в вышеуказанной интерпретации принципа наследования мы получаем, что посетитель всегда является регистратором и тетрадкой. Уже дико выглядит то, что человек и тетрадка одно целое. И логика наследования нарушается. Если рассматривать, что посетитель и регистратор люди и принять тот факт, что представитель класса регистратора может стать посетителем, то логичней сказать, что регистратор является наследником посетителя.

Это пример, когда непонимание принципа наследования приводит к сложности понимания системы.

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

Абстракция


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

Абстракция гласит останавливаем внимание на важных и необходимых аспектах объекта и игнорируем ненужные для нас.

Как это выглядит: когда мы описываем что-то, мы упоминаем только о тех вещах, которые важны в нашем повествовании. Например, когда парень рассказывает другу о том, как в салоне видел крутую машину, он говорит о важных для них вещах: мощность двигателя, систему тормозов, диаметр колес. Хотя особенностей автомобиля безграничное множество. В своем разговоре ребята не упоминают о молекулярном составе автомобиля, хотя такая характеристика определенно существует у физического тела. Незнание законов физики заставляет упустить такой показатель как сила трения между определенными деталями. Та даже дело не в образовании, в данном разговоре все эти детали не важны, они упускаются.

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

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

Также к абстракции я бы отнес декомпозицию, когда сложный объект разбивается на систему. Мы абстрагируемся от некоторых особенностей и переносим их в отдельный компонент. Пример: пользователь у которого есть место проживание, то есть адрес. Адрес в свою очередь состоит из города, улицы, номера дома и т. д. В этот момент мы думаем, а нужно ли указывать страну или регион? Если это приложения для пользования администрацией конкретного района города, то можно упустить такие детали. В итоге мы получаем пользователя, который абстрагируется от некоторых деталей адреса. Опять-таки, непонимание того, что мы не только пишем код, но и занимаемся моделированием, приводит к тому, что у нас есть, допустим, MenuViewController, который состоит из 5000+ строк кода.

Пример абстракции через декомпозицию
/**    Распространенная ситуация: создаем класс, к примеру, простую модель пользователя. Но с добавлением функционала, все больше появляется полей и методов в этом классе. */// INCORRECTclass User {    let firstName: String    let lastName: String    let fullName: String    let age: Int    let birthday: Date    let street: String    let postalCode: Int    let city: String    var phoneNumber: String?    var phoneCode: String?    var phoneFlag: UIImage?    var isLoggined: Bool = false    var isAdmin: Bool = false    // MARK: - Init        init(firstName: String,         lastName: String,         fullName: String,         age: Int,         birthday: Date,         street: String,         postalCode: Int,         city: String) {        self.firstName = firstName        self.lastName = lastName        self.fullName = fullName        self.age = age        self.birthday = birthday        self.street = street        self.postalCode = postalCode        self.city = city    }    // MARK: - Admin functionality        func createNewReport() {        guard isAdmin else { return }                print("New report created")    }        func updateReport(for user: User) {        guard isAdmin else { return }                print("Update report for \(user.fullName)")    }}// CORRECT/**    Правильней будет, декомпозировать код, абстрагируя части большого сложного класса на маленькие компоненты. */class Address {    let street: String    let postalCode: Int    let city: String    init(street: String,         postalCode: Int,         city: String) {        self.street = street        self.postalCode = postalCode        self.city = city    }}class Name {    let firstName: String    let lastName: String    init(firstName: String,         lastName: String) {        self.firstName = firstName        self.lastName = lastName    }    var fullName: String {        firstName + " " + lastName    }}class PhoneNumber {    let phone: String    let code: String    let flag: UIImage    init(phone: String,         code: String,         flag: UIImage) {        self.phone = phone        self.code = code        self.flag = flag    }}class User {    /**    В результате, класс User уменьшился в размерах, при этом мы абстрагируемся от деталей имени и адреса.     */    let name: Name    let address: Address    let birthday: Date    var phoneNumber: PhoneNumber?    init(name: Name,         address: Address,         birthday: Date) {        self.name = name        self.address = address        self.birthday = birthday    }}/**    Так как после логина система получает залогиненого Пользователя, то класс User не должен отвечать за состояния системы. За статус логина будет отвечать новая сущность, тем самым система абстрагируется от деталей логики этого статуса. */class LoginSession {    var user: User?    var isLoggined: Bool {        user != nil    }}/**Дополнительные свойства Администратора выносяться в класс-наследник Пользователя. */class Admin: User {    func createNewReport() {        print("New report created")    }        func updateReport(for user: User) {        print("Update report for \(user.fullName)")    }}


Полиморфизм


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

Полиморфизм плавно вытекает с наследования. Гласит он следующее: можно создавать классы наследники, которые будут имитировать интерфейс базового класса, но со своей собственной реализацией. Этот принцип отражается в таком принципе SOLID как принцип Барбары Лисков: мы можем подставлять объекты классов наследников там, где предполагается использование базового класса, при этом замена не должна никак себя проявлять.

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

Пример в коде: множество наследников UIViewController, которые пушаться, презентятся и добавляются в UITabBarController как обычные UIViewController.
Как по мне, самый простой принцип. Но он довольно часто нарушается, когда класс наследник превращается в что-то новое и его уже нельзя использовать там, где использовался базовый класс. Также к нарушению относятся многочисленные опциональные методы и поля, которые в ходе неправильного наследования не нужны классу-наследнику. В этот момент, когда предполагается выполнение определенного метода базового класса, ничего не происходит. В лучшем случае ничего не произойдет, но может получиться так, что приложение либо неадекватно начинает себя вести либо крашиться вовсе.

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

Пример: есть базовый класс автомобиль, который предположительно должен заехать в гараж. Создаем летающий автомобиль-наследник (как в фильме Назад в будущее 2). Какой будет результат при попытке загнать этого монстра в гараж? Да, он может залететь в гараж, если функция езды будет полностью заменена на функцию полета. А если функция полета была новым функционалом, а функция езды вообще заблокирована? То мы не сможем спрятать нашего летуна от непогоды. Это уже не будет автомобиль, это будет что-то новое. Результат из-за неправильного моделирования и наследования был нарушен принцип полиморфизма и получился нежелательный результат.

Летающий автомобиль

Добавлю, что полиморфизм тесно связан с наследованием и проблемы в наследовании отзываются еще большими проблемами в полиморфизме.

Инкапсуляция


И вот пришла очередь к инкапсуляции. Ходит много дискуссий, что же собой представляет инкапсуляция. И существует как минимум два определения:
Инкапсуляция это сокрытие методов и полей класса, которые не нужны при использовании объектов этого класса.
Инкапсуляций это обьединение данных и методов, которые обрабатывают эти данные.

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

Для меня инкапсуляция это проектирование класса таким образом, чтобы скрыть те методы и поля, которые могут нарушить логику работы класса, заложенную в этот класс. То есть, инкапсуляция != сокрытие. Сокрытие это механизм, который попросту скрывает часть возможностей класса, тогда как инкапсуляция отвечает за то, какие возможности будут доступны, а какие могут нарушить абстракцию этого класса и, соответственно, должны быть скрыты.

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

Для этого я создаю класс наследник UIButton и в наследнике добавляю метод, который устанавливает цвет кнопки в цвет с альфа каналом 50% от оригинального. А также я добавляю метод, который возвращает бэкграунд цвет кнопки в оригинальный, без альфа канала.

Если эти два метода будут доступны при использовании моей кастомной кнопки, то может нарушиться поведение этой кнопки. К примеру в момент нажатия можно вызвать метод, который устанавливает оригинальный цвет. Или же вдруг сделать кнопку светлее, как будто она нажата, хотя это не так.

Пример кнопки с открытыми методами, которые могут нарушить повидение этой кнопки
/**    Создаем кнопку, у которой при нажатии цвет бэкграунда устанавливается в оригинальный цвет но с альфаканалом 0,5 */// INCORRECTclass Button: UIButton {     /**    Добавляем два метода, которые устанавливают цвет бекграунда для состояния нажатой кнопки и нормального состояния кнопки     */    func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }    override var isSelected: Bool {        didSet {            if isSelected {                decorateSelected()            } else {                decorateDeselected()            }        }    }}// SAMPLE /**    Проблемой будет то, что методы, декорирующие кнопку в разных состояних, являются публичными. А это значит, что можно нарушить логику работы кнопки, вызвав метод в неправильный момент. */let button = Button()button.decorateSelected()// CORRECTclass Button: UIButton {        override var isSelected: Bool {        didSet {            if isSelected {                decorateSelected()            } else {                decorateDeselected()            }        }    }     /**    Мы сделали методы, настраивающие внешний вид кнопки, приватными, тем самым обеспечили правильную логику отображения.     */    // MARK: - Private    private func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        private func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }}// PERFECT /**    Но! У кнопки остаеться возможнось измененить цвет через базовое поле var backgroundColor: UIColor?. Поэтому, немного заморочившись, делаем невозможным менять цвет в момент, когда кнопка нажата. */class Button: UIButton {            override var backgroundColor: UIColor? {        get {            super.backgroundColor        }        set {            if isHighlighted == false {                super.backgroundColor = newValue            }        }    }        override var isHighlighted: Bool {        willSet {            if newValue {                decorateSelected()            }        }                didSet {            if isHighlighted == false {                decorateDeselected()            }        }    }    // MARK: - Private    private func decorateSelected() {        backgroundColor = backgroundColor?.withAlphaComponent(0.5)    }        private func decorateDeselected() {        backgroundColor = backgroundColor?.withAlphaComponent(1)    }}


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

По поводу объединения данных и методов, которые обрабатывают эти данные, я с этим согласен, но с оговоркой. В моем понимании ООП моделирование объектов в коде, что само собой подразумевает, наличие состояния и действий у этого объекта.

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

Заключение


Что в целом я хотел сказать? Я считаю, что программирование сложных систем, которые состоят из множества компонентов, начинается с моделирования, а не с кодинга. А для моделирования лучше всего подходит ООП парадигма, так как она собрала в себя принципы, позволяющие упростить моделирование и дальнейшее написания кода. Поэтому правильное понимание принципов ООП ключ к грамотному моделированию. В свою очередь хорошо спроектированная система проще переноситься в код и этот код проще поддерживается в будущем.
Подробнее..

Категории

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

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru