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

Полиморфизм

Перевод Основы JavaScript почему вы должны знать, как работает JS-движок

15.03.2021 16:12:10 | Автор: admin

Для будущих учащихся на курсе "JavaScript Developer. Basic" подготовили перевод полезного материала.

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


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

Ниже вы увидите однострочную функцию, которая возвращает свойство lastName переданного аргумента. Просто добавив одно свойство к каждому объекту, мы получим падение производительности более чем на 700%!

Объясню подробнее почему так происходит. Отсутствие статической типизации в JavaScript приводит к такому поведению. Если рассматривать это как преимущество перед другими языками, такими как C# или Java, то в данном случае получается скорее "Faustian bargain" ("Фаустовская сделка". Жертвование духовных ценностей ради материальных выгод; происхождение выражения связано с именем Дж. Фауста).

Торможение на полной скорости

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

Здорово!

Пусть другие делают тяжелую работу. Зачем беспокоиться о том, как работают движки?

В нашем примере кода ниже, у нас есть пять объектов, в которых хранятся имена и фамилии персонажей из Star Wars (Звездных Войн). Функция getName возвращает значение фамилии. Измерим общее время выполнения этой функции:

(() => {   const han = {firstname: "Han", lastname: "Solo"};  const luke = {firstname: "Luke", lastname: "Skywalker"};  const leia = {firstname: "Leia", lastname: "Organa"};  const obi = {firstname: "Obi", lastname: "Wan"};  const yoda = {firstname: "", lastname: "Yoda"};  const people = [    han, luke, leia, obi,     yoda, luke, leia, obi   ];  const getName = (person) => person.lastname;

пример однострочного кода

console.time("engine");  for(var i = 0; i < 1000 * 1000 * 1000; i++) {     getName(people[i & 7]);   }  console.timeEnd("engine"); })();

На Intel i7 4510U время выполнения составляет около 1.2 секунд. Пока всё хорошо. Теперь мы добавим еще одно свойство к каждому объекту и выполним его снова.

(() => {  const han = {    firstname: "Han", lastname: "Solo",     spacecraft: "Falcon"};  const luke = {    firstname: "Luke", lastname: "Skywalker",     job: "Jedi"};  const leia = {    firstname: "Leia", lastname: "Organa",     gender: "female"};  const obi = {    firstname: "Obi", lastname: "Wan",     retired: true};  const yoda = {lastname: "Yoda"};
const people = [    han, luke, leia, obi,     yoda, luke, leia, obi];
const getName = (person) => person.lastname;
console.time("engine");  for(var i = 0; i < 1000 * 1000 * 1000; i++) {    getName(people[i & 7]);  }  console.timeEnd("engine");})();

Наше время исполнения теперь составляет 8.5 секунд, что примерно в 7 раз медленнее, чем в нашей первой версии. Это похоже на торможение на полной скорости. Как такое могло случиться?

Пора присмотреться к работе движка повнимательнее.

Объединенные Силы: Интерпретатор и Компилятор

Движок это та часть (компонент) программы, которая читает и выполняет исходный код. У каждого крупного производителя браузера есть свой движок. Mozilla Firefox имеет Spidermonkey, Microsoft Edge это Chakra/ChakraCore, а Apple Safari называет свой движок JavaScriptCore. Google Chrome использует V8, который также является движком для Node. js. Выпуск V8 в 2008 году ознаменовал поворотный момент в истории движков. V8 заменил браузеру относительно медленный интерпретатор JavaScript.

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

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

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

Основная идея современных движков объединить лучшее из обоих миров:

  • Быстрый запуск интерпретатора.

  • Быстрое выполнение компилятора.

Современный движок использует интерпретатор и компилятор. Источник: imgflipСовременный движок использует интерпретатор и компилятор. Источник: imgflip

Осуществление обеих целей начинается с интерпретации. Параллельно движок помечает часто выполняемые части кода как "Hot Path" ("Горячий Путь") и передает их компилятору вместе с контекстной информацией, собранной во время выполнения. Этот процесс позволяет компилятору адаптировать и оптимизировать код под текущий контекст.

Поведение компилятора мы называем "Just in Time" или просто JIT (Just-in-time compilation, компиляция на лету).

При хорошей работе движка возможны некоторые сценарии, в которых JavaScript даже превосходит C++. Неудивительно, что большая часть его усилий идет на contextual optimisation ("контекстную оптимизацию").

Взаимодействие между Интерпретатором и КомпиляторомВзаимодействие между Интерпретатором и Компилятором

Static Types (Статическая типизация) во время Runtime (Время выполнения): Inline Caching (Встроенное Кэширование)

Inline-кэширование, или IC, является основным методом оптимизации в движках JavaScript. Интерпретатор должен осуществить поиск, прежде чем он сможет получить доступ к свойствам объекта. Это свойство может быть частью прототипа объекта, оно должно иметь возможность доступа к нему с помощью метода Геттера (getter method) или даже через прокси-сервер. Поиск свойства достаточно затратный процесс с точки зрения скорости исполнения.

Движок присваивает каждому объекту тип ("type"), который он генерирует во время выполнения. V8 называет эти "типы" ("types"), которые не входят в стандарт ECMAScript, скрытые классы или формы объектов. Для того чтобы два объекта имели одну и ту же форму объекта, они должны обладать точно одинаковыми свойствами в одном и том же порядке. Таким образом, объект {firstname: "Han", lastname: "Solo"} будет присвоен к другому классу, нежели {lastname: "Solo", firstname: "Han"}.

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

Что делает Inline Caching, так это исключает операции поиска. Неудивительно, что это приводит к значительному повышению производительности.

Возвращаясь к нашему предыдущему примеру: Все объекты в первом запуске имели только два свойства, firstname и lastname, в одном и том же порядке. Допустим, внутреннее имя этой формы объекта p1. Когда компилятор применяет IC, он предполагает, что функция только передает форму объекта p1 и сразу же возвращает значение lastname.

Inline Caching в действии (Мономорфное)Inline Caching в действии (Мономорфное)

Однако во втором прогоне мы имели дело с 5 различными формами объектов. Каждый объект имел дополнительное свойство, и в yoda отсутствовало firstname. Что происходит, когда мы имеем дело с несколькими фигурами объектов?

Вмешательство Ducks или различная типизация (Intervening Ducks or Multiple Types)

Функциональное программирование использует хорошо известную концепцию duck typing ( "утиной типизации"), при котором хороший код (good code) вызывает функции, способные работать с несколькими типами. В нашем случае, пока переданный объект имеет свойство "lastname", все в порядке.

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

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

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

Полиморфизм и Мегаморфизм в действии

Ниже мы видим полиморфный Inline Cache с 2 различными формами объектов.

Полиморфный Inline CacheПолиморфный Inline Cache

И мегаморфный IC из нашего примера кода с 5-ю разными формами объектов:

Мегаморфный Inline CacheМегаморфный Inline Cache

Класс JavaScript в помощь

Итак, у нас было 5 форм объектов и мы столкнулись с мегаморфной IC. Как мы можем это исправить?

Мы должны убедиться, что движок отмечает все 5 наших объектов и их формы как одинаковые. Это означает, что все создаваемые нами объекты должны будут наделяться всеми возможными свойствами. Мы могли бы использовать объектные литералы (object literals), но я нахожу JavaScript-классы лучшим решением.

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

(() => {  class Person {    constructor({      firstname = '',      lastname = '',      spaceship = '',      job = '',      gender = '',      retired = false    } = {}) {      Object.assign(this, {        firstname,        lastname,        spaceship,        job,        gender,        retired      });    }  }
const han = new Person({    firstname: 'Han',    lastname: 'Solo',    spaceship: 'Falcon'  });  const luke = new Person({    firstname: 'Luke',    lastname: 'Skywalker',    job: 'Jedi'  });  const leia = new Person({    firstname: 'Leia',    lastname: 'Organa',    gender: 'female'  });  const obi = new Person({    firstname: 'Obi',    lastname: 'Wan',    retired: true  });  const yoda = new Person({ lastname: 'Yoda' });  const people = [    han,    luke,    leia,    obi,    yoda,    luke,    leia,    obi  ];  const getName = person => person.lastname;  console.time('engine');  for (var i = 0; i < 1000 * 1000 * 1000; i++) {    getName(people[i & 7]);  }  console.timeEnd('engine');})();

Когда мы снова выполняем эту функцию, то видим, что время ее выполнения возвращается к 1.2 секундам. Задача выполнена!

Резюме

Современные JavaScript-движки сочетают в себе преимущества интерпретатора и компилятора: Быстрый запуск приложений и быстрое выполнение кода.

Inline Caching это мощный метод оптимизации. Лучше всего он работает, когда в оптимизированную функцию передается только одна форма объекта.

Мой пример показал эффективность Inline кэширования различных типов и проблемы при работе с мегаморфными кэшами.

Использование классов JavaScript является хорошей практикой. Статически типизированные транспайлеры (Static typed transpilers), такие как TypeScript, делают мономорфное IC более привлекательным для использования.


Узнать подробнее о курсе "JavaScript Developer. Basic"

Смотреть открытый вебинар по теме: Какими задачами проверяют ваше знание JavaScript

Подробнее..

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

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

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



Введение


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

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

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

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

Polymorphism


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

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

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

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

Pure Fabrication


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

Indirection


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

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

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

Protected Variations


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

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

Вывод


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



Читать ещё:


Подробнее..

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

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

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



Введение


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

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

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


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

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

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

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

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

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


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

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

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

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


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

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

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

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

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


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

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

Полиморфизм


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

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

Подробнее..

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

Абстракция


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

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

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

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

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

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

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


Полиморфизм


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

Заключение


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

Категории

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

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