Предисловие

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

Почему посчитал нужным написать и опубликовать эту статью: 

  • основы забываются в ежедневных задачах. Мы упираемся в код, перестаем смотреть на него через призму объектно-ориентированного программирования и просто кодим;

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

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

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

  • реальность ― то, что вокруг нас, 

  • виртуальность ― то, как мы нашу реальность транслируем в код. 

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

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

Программировать нужно осознанно.

Итак, поехали!

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

Здорово, что мы можем написать всё, что хотим, и всё, что не хотим. Но нужно ли описывать монстра? 

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

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

Сквозь всю статью я пронесу эту трапецию и буду ее заполнять.

Она будет заполняться элементами, которые помогут из кода, который мы можем писать, сделать код хороший. Буду заполнять ее по принципу «от простого к сложному». На базе нижних уровней буду строить верхние. Для объяснения непонятного буду опираться на понятное. 

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

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

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

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

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

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

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

ООП ― площадь опоры

Возьму определение ООП из википедии: 

Объе́ктно-ориенти́рованное программи́рование (сокр. ООП)методология программирования, основанная на представлении программы в виде совокупности взаимодействующих объектов, каждый из которых является экземпляром определённого класса, а классы образуют иерархию наследования

Мы видим, что в ООП во главе стола ― объект. А если верить JavaScript, то всё есть объект (даже массив). То есть всё, с чем мы можем работать ― это объект. А что такое объект? 

Объект

Снова прибегнем к определению из википедии:

Объе́кт в программировании — некоторая сущность в цифровом пространстве, обладающая определённым состоянием и поведением, имеющая определённые свойства (атрибуты) и операции над ними (методы). Как правило, при рассмотрении объектов выделяется то, что объекты принадлежат одному или нескольким классам, которые определяют поведение (являются моделью) объекта. Термины «экземпляр класса» и «объект» взаимозаменяемы.

Объект ― первый элемент в моей трапеции. 

Ненадолго перенесемся в реальность и увидим, что всё, что нас окружает ― это объекты. У всего есть какие-то свойства и с этим «всем» или при помощи этого «всего» можно что-то сделать. Смотря на знакомый объект, мы понимаем, что можно сделать конкретно с ним, а также мы можем представить взаимодействие нескольких известных объектов. 

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

 

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

Классы

Снова обратимся к вики:

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

Получается, что класс ― это некая языковая конструкция, которая проецирует объект. В классе мы описываем состояние и поведение объекта. Когда мы увидели картинку конкретной бутылки пива (т. е. конкретный объект), то в голове у нас появилась проекция этой бутылки (т. е. конкретный класс).

А еще есть такое понятие как абстрактный класс

Абстрактный класс

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

Вам звонит друг и говорит:

― Купи мне две бутылки пшеничного пива.

― Какого именно? 

― Не важно. Главное пшеничного.

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

― Какое конкретно пиво вам нужно? 

― Вон те две бутылки на верхней полке, крайние справа (ссылочное обращение к объекту, к слову пришлось).

Опять возвращаемся к вики: 

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

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

Интерфейс и полиморфизм

Интерфе́йс (англ. interface) — программная/синтаксическая структура, определяющая отношение между объектами, которые разделяют определённое поведенческое множество и не связаны никак иначе. При проектировании классов, разработка интерфейса тождественна разработке спецификации (множества методов, которые каждый класс, использующий интерфейс, должен реализовывать).

Для понимания интерфейса представим еще один пример.

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

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

Все действия, что я назвал, сложны в конкретной реализации, но просты на уровне интерфейсов:

  • чайная ― это абстракция, которая умеет наливать чай. Чайная может наливать разные чаи разным гостям; 

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

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

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

Способность чайной угощать чаем любого гостя и есть полиморфизм.

Полиморфизм в языках программирования и теории типов — способность функции обрабатывать данные разных типов.

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

Вот мы и разобрали первого из трех китов ООП ― полиморфизм. 

Возьмем на разбор следующего ― инкапсуляцию. 

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

Википедия говорит следующее: 

Инкапсуляция (англ. encapsulation, от лат. in capsula) — в информатике, процесс разделения элементов абстракций, определяющих ее структуру (данные) и поведение (методы); инкапсуляция предназначена для изоляции контрактных обязательств абстракции (протокол/интерфейс) от их реализации. На практике это означает, что класс должен состоять из двух частей: интерфейса и реализации. В реализации большинства языков программирования (C++, C#, PHP и другие) обеспечивается механизм сокрытия, позволяющий разграничивать доступ к различным частям компонента.

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

Для наглядности сравним два массива: один из PHP, другой из JS. 

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

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

Где-то на просторах интернета написано, что инкапсуляция ― это возможность организовать доступ к данным и методам объекта. Иными словами, возможность использовать модификаторы доступа. Но это не полное определение. Инкапсуляция определяет: 

  • что есть что-то, что может хранить в себе состояние и иметь поведение, 

  • что к состоянию и поведению этого чего-то можно ограничивать доступ с помощью модификаторов доступа.

Когда мы говорим про инкапсуляцию, в разговорах часто появляется слово «сокрытие». Давайте о нем тоже поговорим. 

Сокрытие

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

Для примера возьмем кофемашину. 

Кофемашина ― это штука, которая максимально проста в использовании. Жми себе кнопки да кофе пей.

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

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

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

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

Можем ли мы смотреть на кофемашину сразу с пяти точек зрения? Не можем. Мы можем лишь менять свои точки зрения. Так и в программировании: мы смотрим с какой-то точки и видим только то, что нам нужно, а то, что нам не нужно ― не видим. Если нашему взгляду будет доступно что-то лишнее, то это может быть помехой. 

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

Осталось поговорить о последнем из трех китов ООП ― о наследовании. 

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

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

Допустим мы хотим создать новую сущность ― кружку. Кружка нужна, чтобы пить чай. И допустим, что у нас еще нет такой сущности, но есть сущность ― чайник. 

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

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

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

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

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

Для себя я определил две причины плохого наследования: 

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

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

Эти причины можно объединить в одно емкое предложение: если при замене объекта родительского класса объектом дочернего класса поведение системы/кода/класса меняется, наследовать не стоит. Узнаете принцип? 

Однако, если есть нужда переиспользовать метод, можно использовать внедрение зависимости с последующим делегированием этой зависимости. 

 

Внедрение зависимости

Внедрение зависимости (англ. Dependency injection, DI) — процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (англ. Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единственной ответственности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.

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

Есть три типа внедрения зависимости: ассоциация, агрегация и композиция.

Ассоциация ― самый низко связанный тип внедрения зависимости. Иногда его еще называют «использованием». Это когда один объект использует другой. И это, собственно, всё. Пример: профессор использует палочку. 

Эту палочку может использовать любой профессор. Она лежит в лекционном зале. И эта палочка может использоваться не только профессором, а вообще кем угодно. Ассоциируются два разных объекта с разными жизненными циклами. Ассоциация ― это способ внедрения зависимости через метод set().

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

Например, университет не может существовать без персонала, а персонал без университета может. То есть мы для создания объекта «университет» обязаны указать набор преподавателей. 

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

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

Кафедра без университета не нужна, как и университет без кафедр. То есть в момент создания университета мы создаем кафедры. Иными словами в конструкторе класса «университет» создаются новые классы кафедр. Не передаются, а именно создаются. 

Переиспользование кода 

Внедрение зависимости является одним из способов переиспользования кода. Вообще этих способов не так много.

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

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

  2. Внедрение (инъекция) зависимостей.

  3. Трейты и миксины.

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

Поэтому добавим еще один отдельный уровень в нашу трапецию. 

Что дальше

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

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

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

И до новых встреч!

Комментарии (7)


  1. IvanSTV
    13.09.2022 11:04
    +1

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


  1. eandr_67
    13.09.2022 12:49
    +8

    Это всё — про то, как оформить код. Но до того, как заниматься написанием кода, надо сначала найти способ достижения заданной цели (т.е. алгоритм, который этот код будет реализовывать). И если великолепно оформленный — по всем заветам ООП — код, обрабатывающий большой массив данных, имеет вычислительную сложность O(n³) при наличии общеизвестного алгоритма O(n∙log(n)) — это безусловный говнокод, демонстрирующий умение писать код при абсолютном незнании элементарных основ программирования.

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

    Мантра «полиморфизм, инкапсуляция, наследование» — это не «киты ООП», а всего лишь наиболее модный из вариантов компонентного программирования (включающего множество разных вариантов ООП). Тот же Go великолепно обходится и без наследования, и даже без классов. И даже JavaScript много лет прекрасно жил без классов — пока корпорации, заведующие стандартизацией JS, не решили удешевить подготовку JS-разработчиков.


  1. beeptec
    13.09.2022 14:46

    • основы забываются в ежедневных задачах. Мы упираемся в код, перестаем смотреть на него через призму объектно-ориентированного программирования и просто кодим;

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

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

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

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


  1. mafia8
    13.09.2022 15:47

    Правило №1 при объяснении ООП: Не использовать код.


  1. Akela_wolf
    13.09.2022 16:54
    +2

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

    Мало того, автор сразу же пошел в рассмотрение некоторого "хорошего кода" не дав определения какой код считать хорошим, а какой плохим. Соответственно, статья "висит в воздухе", по сути рассказывая о том "что такое ООП", но не рассказывая как и когда это самое ООП следует применять.


  1. beeptec
    13.09.2022 18:46
    +1

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


  1. VXP
    15.09.2022 12:53

    Благодарю) Очень полезно, как по мне