Русский
Русский
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'ей.

Подробнее..

Перевод Финальные классы в PHP, Java и других языках

23.11.2020 12:04:01 | Автор: admin
Использовать финальные классы или не использовать финальные классы? Вот в чём вопрос. А еще в том, когда и как это делать правильно.



Почему стоит использовать финальные классы


Максимальное уменьшение области видимости


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

Поощрение подхода композиция вместо наследования


Принцип открытости/закрытости гласит: класс должен быть открыт для расширения, но закрыт для изменений.

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

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

Почему этот класс не финальный?


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

Заблуждения


Когда мы в первый раз изучали ООП, мы приводили классический пример наследования. Тем не менее, когда Алан Кей создал Smalltalk, наследование не было его главной концепцией. Основной концепцией был обмен сообщениями, то есть вы можете отправлять сообщения объектам, а они инкапсулируют в себе данные и логику. Вы можете изменять их поведение, используя различные объекты, что на самом деле является композицией. Но в итоге концепция наследования настолько популярна, что в конечном счёте затмевает композицию.

Выгоды от использования финальных классов


  • Понятные контракты. Использование интерфейсов заставит вас мыслить в терминах связей между объектами.
  • Изолированные блоки кода без побочных эффектов. Внедрение интерфейсов в качестве зависимостей устранит все неприятные побочные эффекты кода, над которым вы работаете.
  • Тестируемость. Подмена на фиктивные зависимости чрезвычайно проста, если они являются интерфейсами.
  • Низкая и управляемая сложность. Поскольку всё изолировано, вам не нужно беспокоиться о волнообразных изменениях. Это значительно снижает сложность вашего кода.
  • Низкая когнитивная нагрузка. С уменьшением сложности ваш мозг сможет сосредоточиться на самом важном.
  • Гибкость кода. Удаляя любую ненужную связь, вы сможете рефакторить намного легче, чем раньше.
  • Уверенность в себе. Возможность тестировать свой код изолированно не оставляет сомнений в правильности его изменения.

Композиция вместо наследования


  • Если вы чувствуете необходимость перенастроить объект, изменить части алгоритма или переписать часть реализации, подумайте о создании нового класса вместо переопределения существующего.
  • Допустим, вам нужно создать иерархию классов, где подклассы являются подходящими заменами своего родительского класса. Это была бы классическая ситуация, когда вы всё ещё можете подумать о наследовании. Однако результат может быть даже лучше, если вы наследуетесь не от конкретных родительских классов, а от абстрактных интерфейсов.

Что вам следует начать делать [вместо того что вы делаете сейчас]


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

Итого: Интерфейсы Финальные классы Композиция
Подробнее..

Можно ли пингвина наследовать от птицы?

07.03.2021 16:16:59 | Автор: admin

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


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


image


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


Но и это, к сожалению, тоже нельзя назвать правильным ответом.


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


Пингвин и птица в разных бизнес-контекстах могут быть описаны совершенно по-разному.


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


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


Более того, в одном и том же софте могут присутствовать сразу оба варианта. В различных bounded context могут быть разные модели.


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


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


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


Upd. Попросили в саму статью приложить результаты опроса:


Подробнее..

Как не копировать код в Rust

12.04.2021 20:16:10 | Автор: admin

Первое правило хорошего тона в программировании (или одно из первых) гласит: "Не копируй код". Используй функции и наследование.

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

impl dyn Trait

Если вы посмотрите на реализацию итератора, то увидите, что у него есть один required метод (то есть тот, который нужно реализовать нам) и provided методы (те, которые реализованы за нас). Что нам это даёт? А то, что если мы напишем свою реализацию какой-либо коллекции, то чтобы реализовать для нее итератор нам нужно всего-навсего имплементировать next(). Все остальные фп-шные штуки вроде map(), filter(), fold() и т.д. будут реализованы автоматически.

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

enum Damage{    Physical(f32),    Magic(f32)}trait Character{  fn get_magic_resistance(&self) -> f32;  fn get_physical_resistance(&self) -> f32;  fn set_hp(&mut self, new_value: f32);  fn get_hp(&self) -> f32;  fn get_type(&self) -> CharacterType;  fn get_dmg(&self) -> Damage;}impl dyn Character {  fn make_hurt(&mut self, dmg: Damage) {    match dmg{      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())    }  }}#[derive(Debug, Clone, Copy)]enum CharacterType {    Mage,    Warrior,    Rogue}impl Default for CharacterType {    fn default() -> Self{        CharacterType::Warrior    }}#[derive(Default)]struct Player{    ty: CharacterType,    phys_resist: f32,    mag_resist: f32,    hp: f32,    dmg: f32}impl Player {    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {        Self{ ty, hp, dmg, .. Default::default() }    }}impl Character for Player{    #[inline]    fn get_magic_resistance(&self) -> f32 {        self.mag_resist    }        #[inline]    fn get_physical_resistance(&self) -> f32{        self.phys_resist    }        #[inline]    fn set_hp(&mut self, new_value: f32){        self.hp = new_value;    }        #[inline]    fn get_hp(&self) -> f32 {        self.hp    }        #[inline]    fn get_type(&self) -> CharacterType {        self.ty    }        fn get_dmg(&self) -> Damage{        match self.ty {            CharacterType::Mage => Damage::Magic(self.dmg),            _ => Damage::Physical(self.dmg)        }    }}struct EnemyWarrior{    ty: CharacterType,    phys_resist: f32,    mag_resist: f32,    hp: f32,    dmg: f32}impl Default for EnemyWarrior {    fn default() -> Self {        Self{            ty: CharacterType::Warrior,            phys_resist: 0.,            mag_resist: 0.,            hp: 10.,            dmg: 1.        }    }}impl Character for EnemyWarrior{    #[inline]    fn get_magic_resistance(&self) -> f32 {        self.mag_resist    }        #[inline]    fn get_physical_resistance(&self) -> f32{        self.phys_resist    }        #[inline]    fn set_hp(&mut self, new_value: f32){        self.hp = new_value;    }        #[inline]    fn get_hp(&self) -> f32 {        self.hp    }        fn get_type(&self) -> CharacterType {        CharacterType::Warrior    }        fn get_dmg(&self) -> Damage{        match self.ty {            CharacterType::Mage => Damage::Magic(self.dmg),            _ => Damage::Physical(self.dmg)        }    }}fn main(){    let mut player = Player::new(CharacterType::Warrior, 10., 1.);    let mut enemy = EnemyWarrior::default();        <dyn Character>::make_hurt(&mut enemy, player.get_dmg());    println!("{}", enemy.get_hp());}

TL;DR сделали 2 структуры и реализовали для них трейт Character, в котором 6 required методов и 1 provided метод.

Но внимательный читатель совершенно справедливо спросит: "Так мы ведь всё равно скопировали код?" Верно, из-за того, что в трейтах нельзя объявлять поля, нам пришлось городить required методы для получения нужных данных. Сейчас-то, конечно, их всего 6, но потом у нас могут добавиться передвижения, анимации, левел-апы и проч. Конечно, целый один метод у нас один на все имплементации, но код-то мы все равно копируем?

Deref<Target=_>

Тут нам на помощь приходит трейт Deref. Честно говоря, не знаю, насколько это хорошая практика использовать его с такой целью, но, например, в image есть такое: ImageBuffer реализует Deref для [P::Subpixel] и, соответственно, имеет все методы массива, которые есть в стандартной библиотеке. Давайте перепишем наш пример под Deref и посмотрим, сколько места нам удалось сэкономить.

use std::ops::Deref;use std::ops::DerefMut;enum Damage{    Physical(f32),    Magic(f32)}#[derive(Default)]struct Character{    ty: CharacterType,    phys_resist: f32,    mag_resist: f32,    hp: f32,    dmg: f32}impl Character {    #[inline]    fn get_magic_resistance(&self) -> f32 {        self.mag_resist    }        #[inline]    fn get_physical_resistance(&self) -> f32{        self.phys_resist    }        #[inline]    fn set_hp(&mut self, new_value: f32){        self.hp = new_value;    }        #[inline]    fn get_hp(&self) -> f32 {        self.hp    }        #[inline]    fn get_type(&self) -> CharacterType {        self.ty    }        fn get_dmg(&self) -> Damage{        match self.ty {            CharacterType::Mage => Damage::Magic(self.dmg),            _ => Damage::Physical(self.dmg)        }    }  fn make_hurt(&mut self, dmg: Damage) {    match dmg{      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())    }  }}#[derive(Debug, Clone, Copy)]enum CharacterType {    Mage,    Warrior,    Rogue}impl Default for CharacterType {    fn default() -> Self{        CharacterType::Warrior    }}#[derive(Default)]struct Player(Character);impl Player {    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {        Self(Character{ ty, hp, dmg, .. Default::default() })    }}impl Deref for Player {    type Target = Character;        #[inline]    fn deref(&self) -> &Self::Target{        &self.0    }}impl DerefMut for Player {    #[inline]    fn deref_mut(&mut self) -> &mut Self::Target{        &mut self.0    }}struct EnemyWarrior(Character);impl Default for EnemyWarrior {    fn default() -> Self {        Self(Character {            ty: CharacterType::Warrior,            phys_resist: 0.,            mag_resist: 0.,            hp: 10.,            dmg: 1.        })    }}impl Deref for EnemyWarrior {    type Target = Character;        #[inline]    fn deref(&self) -> &Self::Target{        &self.0    }}impl DerefMut for EnemyWarrior {    #[inline]    fn deref_mut(&mut self) -> &mut Self::Target{        &mut self.0    }}fn main(){    let mut player = Player::new(CharacterType::Warrior, 10., 1.);    let mut enemy = EnemyWarrior::default();        enemy.make_hurt(player.get_dmg());    println!("{}", enemy.get_hp());}

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

Но у такого подхода тоже есть минус. Если нам нужны другие данные, помимо "родительских", тогда нам придётся создавать дополнительные структуры, чтобы хранить уже свои, независимые от "родительских", данные. Тогда нам придется писать код вроде:

struct Child (Parent, ChildInner);impl Child {  pub fn get_some_field_from_inner(&self) -> &ChildInner::Field {    &self.1.field  }}

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

Заключение

В заключение хочу сказать, что в расте не помешало бы явное наследование структур. Всё-таки, как ни крути, в других языках оно позволяет писать с одной стороны хорошо читаемый, с другой стороны лаконичный код (если, конечно, это не множественное наследование). Подходы в Rust, конечно, позволяют экономить какое-то количество места, но хотелось бы чего-то более явного. Да и интерфейсы Deref, DerefMut вообще не предназначены для того, для чего мы их использовали в данной статье. Они, как следует из названия, нужны чтобы разыменовывать умные указатели. А если вы объединяете несколько структур данных, то вам придется использовать синтаксис self.1, который далеко не очевидный. В общем, как и всегда, есть и плюсы и минусы.

Вот, собственно, и все, что я хотел сказать на эту тему. Может, я что-то упустил? Напишите в комментариях, если вас тоже волнует эта тема, хочется знать, что не мне одному не хватает наследования в Rust.

Подробнее..

Принцип подстановки Барбары Лисков (предусловия и постусловия)

28.05.2021 00:20:41 | Автор: admin

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

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

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

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

Предусловия не могут быть усилены в подклассе

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

<?phpclass Customer{    protected float $account = 0;    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        $this->account += $sum;    }}class  MicroCustomer extends Customer{    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        // Усиление предусловий        if ($sum > 100) {             throw new Exception('Вы не можете положить на больше 100$');        }        $this->account += $sum;    }}

Добавление второго условия как раз является усилением. Так делать не надо!

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

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

Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает метод в родительском классе.

<?phpclass Foo{    public function process(int|float $value)    {       // some code    }}class Bar extends Foo{    public function process(int|float|string $value)    {        // some code    }}

Пример ниже показывает, как дочерний класс VIPCustomer может принимать в аргумент переопределяемого метода putMoneyIntoAccount более широкий (более абстрактный) объект Money, чем в его родительском методе (принимает Dollars).

<?phpclass Money {}class Dollars extends Money {}class Customer{    protected Money $account;    public function putMoneyIntoAccount(Dollars $sum): void    {        $this->account = $sum;    }}class VIPCustomer extends Customer{    public function putMoneyIntoAccount(Money $sum): void    {        $this->account = $sum;    }}

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

Постусловия не могут быть ослаблены в подклассе

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

<?phpclass Customer{    protected Dollars $account;    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($result < 0) { // Постусловие            throw new Exception();        }        return $result;    }}class  VIPCustomer extends Customer{    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($sum < 1000) { // Добавлено новое поведение            $result -= 5;          }               // Пропущено постусловие базового класса              return $result;    }}

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

Сюда-же можно отнести и Ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа (ШО?!), который возвращает родительский метод.

На примере будет проще. Здесь в методе render() дочернего класса, JpgImage объявлен типом возвращаемого значения, который в свою очередь является подтипом Image, который возвращает метод родительского класса Renderer.

<?phpclass Image {}class JpgImage extends Image {}class Renderer{    public function render(): Image    {    }}class PhotoRenderer extends Renderer{    public function render(): JpgImage    {    }}

Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. Усилили :)

Инвариантность

Здесь должно быть чуть проще.

Все условия базового класса - также должны быть сохранены и в подклассе.

Инварианты это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.

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

<?php class Wallet{    protected float $amount;    // тип данного свойства не должен изменяться в подклассе}

Здесь также стоит упомянуть исторические ограничения (правило истории):

Подкласс не должен создавать новых мутаторов свойств базового класса.

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

<?phpclass Deposit{    protected float $account = 0;    public function __construct(float $sum)    {        if ($sum < 0) {            throw new Exception('Сумма вклада не может быть меньше нуля');        }        $this->account += $sum;    }}class VipDeposit extends Deposit{    public function getMoney(float $sum)    {        $this->account -= $sum;    }}

С точки зрения класса Deposit поле не может быть меньше нуля. А вот производный класс VipDeposit, добавляет метод для изменения свойства account, поэтому инвариант класса Deposit нарушается. Такого поведения следует избегать.

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

Выводы

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

Стоит упомянуть, что нужно страться избавляться от пред/пост условий. В идеале они должны быть определенны как входные/выходные параметры метода (например передачей в сигнатуру готовых value objects и возвращением конкретного валидного объекта на выход).

Надеюсь, было полезно.

Источники

  1. Вики - Принцип подстановки Барбары Лисков

  2. Metanit

  3. PHP.watch

  4. Telegram канал, с короткими заметками

Подробнее..

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

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)    }}


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

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

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

Заключение


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

Библиотека дляработы сiOS-пермишенами, отидеи дорелиза (часть1)

07.12.2020 20:14:08 | Автор: admin

Привет! Из этого мини-цикла статей ты узнаешь:

  • Как унаследовать Swift-класс не целиком, а лишь то в нём, что тебе нужно?

  • Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

  • Как раздербанить ресурсы iOS, чтобы достать оттуда конкретные системные иконки и локализованные строки?

  • Как поддержать completion blocks даже там, где это не предусмотрено дефолтным API системных разрешений?

А вообще, здесь о том, как я попытался написать ультимативную библиотеку для работы с пермишенами в iOS с какими неожиданностями столкнулся и какие неочевидные решения нашёл для некоторых проблем. Буду рад, если окажется интересно и полезно!

Немного о самой либе

Однажды мне пришла в голову следующая мысль всем мобильным разработчикам то и дело приходится работать с системными разрешениями. Хочешь использовать камеру? Получи у юзера пермишен. Решил присылать ему уведомления? Получи разрешение.

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

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

В итоге я решил написать собственную библиотеку. Попытаться сделать идеально. Буду благодарен, если напишете в комментариях, получилось ли. А вообще, PermissionWizard...

  • Поддерживает новейшие фичи iOS 14 и macOS 11 Big Sur

  • Отлично работает с Mac Catalyst

  • Поддерживает все существующие типы системных разрешений

  • Валидирует твой Info.plist и защищает от падений, если с ним что-то не так

  • Поддерживает коллбэки даже там, где этого нет в дефолтном системном API

  • Позволяет не париться о том, что ответ на запрос какого-нибудь разрешения вернётся вневедомом потоке, пока ты его ждёшь, например, в DispatchQueue.main

  • Полностью написан на чистом Swift

  • Обеспечивает унифицированное API вне зависимости от типа разрешения, с которым ты прямо сейчас работаешь

  • Опционально включает нативные иконки и локализованные строки для твоего UI

  • Модульный, подключай лишь те компоненты, что тебе нужны

Но перейдём наконец-то к действительно интересному...

Как унаследовать класс не целиком, а лишь то в нём, что тебе нужно?

Начав работать над PermissionWizard, я довольно быстро понял, что для большинства поддерживаемых типов разрешений мне нужны одни и те же элементы:

  • Свойство usageDescriptionPlistKey

  • Методы checkStatus и requestAccess

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

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

Только вот оказалось всё сложнее, чем можно было ожидать:

  • Некоторые типы пермишенов (например, дом и локальная сеть) не позволяют проверить текущий статус разрешения, не выполнив собственно запрос на доступ к нему, и унаследованное объявление checkStatus оказывается в таком случае неуместным. Оно лишь сбивает с толку торчит в автоподстановке, хотя не имеет имплементации.

  • Для работы с пермишеном геолокации не годится стандартное объявление requestAccess(completion:), поскольку для запроса на доступ необходимо определиться, нужен он нам всегда, или только когда юзер активно пользуется приложением. Здесь подходит requestAccess(whenInUseOnly:completion:), но тогда опять-таки выходит, что унаследованная перегрузка метода болтается не в тему.

  • Пермишен на доступ к фотографиям использует сразу два разных plist-ключа один на полный доступ (NSPhotoLibraryUsageDescription) и один, чтобы только добавлять новые фото и видео (NSPhotoLibraryAddUsageDescription). Видим, что опять-таки наследуемое свойство usageDescriptionPlistKey получается лишним логичнее иметь два отдельных и с более говорящими названиями.

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

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

class SupportedType {    func requestAccess(completion: (Status) -> Void) { }}final class Bluetooth: SupportedType { ... }final class Location: SupportedType {    @available(*, unavailable)    override func requestAccess(completion: (Status) -> Void) { }        func requestAccess(whenInUseOnly: Bool, completion: (Status) -> Void) { ... }}

Переопределение метода, помеченное атрибутом @available(*, unavailable), не только делает его вызов невозможным, возвращая при сборке ошибку, но и полностью скрывает его из автоподстановки в Xcode, то есть фактически как будто исключает метод из наследования.

Разумеется, я не открыл здесь никакой Америки, однако решение оказалось не слишком широко известным, поэтому решил им поделиться.

Как позволить юзеру твоей CocoaPods- или Carthage-библиотеки компилировать лишь те её части, что он действительно использует?

PermissionWizard поддерживает 18 видов системных разрешений от фото и контактов до Siri и появившегося в iOS 14 трекинга. Это в свою очередь означает, что библиотека импортирует и использует AVKit, CoreBluetooth, CoreLocation, CoreMotion, EventKit, HealthKit, HomeKit и ещё много разных системных фреймворков.

Нетрудно догадаться, что если ты подключишь такую либу к своему проекту целиком, пусть даже будешь использовать её исключительно для работы с каким-то одним пермишеном, Apple не пропустит твоё приложение в App Store, поскольку увидит, что оно использует подозрительно много разных API конфиденциальности. Да и собираться такой проект будет чуть дольше, а готовое приложение весить чуть больше. Требуется какой-то выход.

CocoaPods

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

pod 'PermissionWizard/Assets' # Icons and localized stringspod 'PermissionWizard/Bluetooth'pod 'PermissionWizard/Calendars'pod 'PermissionWizard/Camera'pod 'PermissionWizard/Contacts'pod 'PermissionWizard/FaceID'pod 'PermissionWizard/Health'pod 'PermissionWizard/Home'pod 'PermissionWizard/LocalNetwork'pod 'PermissionWizard/Location'pod 'PermissionWizard/Microphone'pod 'PermissionWizard/Motion'pod 'PermissionWizard/Music'pod 'PermissionWizard/Notifications'pod 'PermissionWizard/Photos'pod 'PermissionWizard/Reminders'pod 'PermissionWizard/Siri'pod 'PermissionWizard/SpeechRecognition'pod 'PermissionWizard/Tracking'

В свою очередь, Podspec нашей библиотеки (файл, описывающий её для CocoaPods) выглядит примерно следующим образом:

Pod::Spec.new do |spec|    ...    spec.subspec 'Core' do |core|    core.source_files = 'Source/Permission.swift', 'Source/Framework'  end    spec.subspec 'Assets' do |assets|    assets.dependency 'PermissionWizard/Core'    assets.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'ASSETS' }        assets.resource_bundles = {      'Icons' => 'Source/Icons.xcassets',      'Localizations' => 'Source/Localizations/*.lproj'    }  end    spec.subspec 'Bluetooth' do |bluetooth|    bluetooth.dependency 'PermissionWizard/Core'    bluetooth.pod_target_xcconfig = { 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'BLUETOOTH' }    bluetooth.source_files = 'Source/Supported Types/Bluetooth*.swift'  end    ...    spec.default_subspec = 'Assets', 'Bluetooth', 'Calendars', 'Camera', 'Contacts', 'FaceID', 'Health', 'Home', 'LocalNetwork', 'Location', 'Microphone', 'Motion', 'Music', 'Notifications', 'Photos', 'Reminders', 'Siri', 'SpeechRecognition', 'Tracking'  end

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

#if BLUETOOTH    final class Bluetooth { ... }#endif

Carthage

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

В корне нашей либы создаём файл Settings.xcconfig и пишем в нём следующее:

#include? "../../../../PermissionWizard.xcconfig"

По умолчанию Carthage устанавливает зависимости в директорию Carthage/Build/iOS, так что вышеприведённая инструкция ссылается на некий файл PermissionWizard.xcconfig, который может быть расположен юзером нашей библиотеки в корневой папке своего проекта.

Очертим и его примерное содержимое:

ENABLED_FEATURES = ASSETS BLUETOOTH ...SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) $(ENABLED_FEATURES) CUSTOM_SETTINGS

Наконец, необходимо указать нашей либе, что она должна ссылаться на Settings.xcconfig как на дополнительный источник настроек для сборки. Чтобы это сделать, добавляем в проект библиотеки ссылку на указанный файл, а затем открываем project.pbxproj любым удобным текстовым редактором. Здесь ищем идентификатор, присвоенный только что добавленному в проект файлу, как на примере ниже.

A53DFF50255AAB8200995A85 /* Settings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Settings.xcconfig; sourceTree = "<group>"; };

Теперь для каждого имеющегося у нас блока XCBuildConfiguration добавляем строку с базовыми настройками по следующему образцу (строка 3):

B6DAF0412528D771002483A6 /* Release */ = {isa = XCBuildConfiguration;baseConfigurationReference = A53DFF50255AAB8200995A85 /* Settings.xcconfig */;buildSettings = {...};name = Release;};

Ты можешь спросить, зачем помимо флагов с нужными компонентами мы также проставляем некое CUSTOM_SETTINGS. Всё просто в отсутствие этого флага мы считаем, что юзер библиотеки не попытался её настроить, то есть не создал PermissionWizard.xcconfig в корне своего проекта, и включаем сразу все поддерживаемые либой компоненты.

#if BLUETOOTH || !CUSTOM_SETTINGS    final class Bluetooth { ... }#endif

На этом пока всё

В следующей части поговорим о том, как я среди 5 гигабайт прошивки iOS 14 нашёл нужные мне локализованные строки и как добыл иконки всех системных пермишенов. А ещё расскажу, как мне удалось запилить requestAccess(completion:) даже там, где дефолтное системное API разрешений не поддерживает коллбэки.

Спасибо за внимание!

Подробнее..

Категории

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

  • Имя: Макс
    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