Зачем нам ООП и что это такое
Неделя статей на хабре посвященная ООП. Последняя статья вызвала у меня кучу эмоций и, к сожалению, очень плохих эмоций. Мне очень не понравилась статья. Почему? Потому что в ней передаются какие-то отрицательные эмоции об использовании ООП. Эмоции вызваны лишь тем, что человек не до конца понимает всю силу ООП и хочет убедить всех в том что ООП зло. Самое печальное что люди начинают прислушиваться и кидаться ужасными доводами, не имеющими ничего общего с действительностью. Я думаю что студентам такие статьи противопоказаны больше чем GoF, которых я бы давал как можно раньше. 
Начнем.
Что такое ООП. ООП — это и ОО программирование и проектирование. Одно без другого бессмысленно чуть более чем полностью. Создано ООП для проектирования/программирования программных продуктов. Не для моделирования процессов. Не для проектирования протоколов, а именно для программных продуктов, для их реализации. Для упрощения системы, которая будет реализовывать протокол или бизнес-процесс или что-то еще.
Когда вы начинаете использовать ООП, первое что вы должны сделать — это начать использовать объектное мышление. Я уже когда-то говорил что это самая большая проблема ООП, научиться мыслить объектно очень сложно. И очень важно учиться это делать как можно раньше (GoF с аналогиями типа мост, конструктор, фасад очень в этом помогут). Используя объектное мышление, вы легко сможете проектировать сложные системыИспользуя объектное мышление вы легко можете решить любую задачу (очень важно что любую задачу проектирования/программирования, если ее в принципе можно решить оперируя объектами и взаимодействием между ними. Т.е. ООП без объектного мышления не позволит вам начать использовать всю силу и мощь ООП.абсолютно любую)
Пойдем дальше. Итак, нам важно мыслить объектно, для того, что бы найти нужные нам абстракции объектов для решения наших задач. Если аналогии и абстракции выбраны удачно, то мы видим очень четкую картину которая позволяет нам быстро разобраться в том, что же происходит в системе. И вот тут мы начинаем вспоминать про наследование и полиморфизм. Эти два инструмента нужны для удобного масштабирования системы без дублирования кода. Но сила этих механизмов зависит от того насколько удачные абстракции и аналогии вы выбрали. Если ваше объектное мышление не позволяет вам сформировать удобную декомпозицию объектов, то наследование и полиморфизм вам не помогут. Т.е. наследование и полиморфизм это ничто иное как инструменты, которые позволяют решить проблему масштабирования системы.
Как же эти инструменты работают? Да проще пареной репы, потому что это все основано на привычных нам вещах. Люблю простые примеры из жизни:
1. Наследование. Есть пекарь. Есть печь электрическая и газовая. Ваша задача смоделировать процесс приготовления пищи пекарем в каждой из печи. Решая задачу в лоб, у нас будет много дублирования кода из-за того, что сам процесс передачи пищи в печь и сама работа с печами идентичны для обеих печей. Но если мы включаем объектное мышление, и вспоминаем про инструмент наследование, то получаем примерно следующее (диаграмму лень рисовать, сорри):
Есть печь (абстрактная печь). У нее есть поведение — включить, выключить, увеличить или уменьшить температуру, положить чего-то, достать чего-то и состояние — температура в печи, включена или выключена. Это отличный пример абстрактного объекта в котором соблюдены принципы инкапсуляции (при реализации я их обязательно буду соблюдать). И есть пекарь, конкретный такой пекарь Иван. Он умеет работать с абстрактной печью. Т.е. смотреть температуру, включать выключать и т.д. вы поняли. Сила наследования в том, что нам не придется переписывать нашего Ивана для каждой из печей, будь то электро или газовая печь. Я думаю всем ясно почему? Получается что инструмент применен правильно.
2. Полиморфизм. Печи ведь по-разному работают. Газовая потребляет газ, электро печь — электричество. Используя полиморфизм мы легко меняем поведение в наследниках абстрактной печи.
3. Инкапсуляция. Основная фишка инкапсуляции в том, что я не должен знать, что происходит внутри моей печи. Допустим, я вызываю не метод включить печь, а меняю ее свойство включена на значение true. Что произойдет в этот момент? Если принцип инкапсуляции не соблюден, то я буду вынужден печи сказать начинай потреблять горючее, т.к. я тебя включил. Т.е. пекарь знает, что печь потребляет горючее, знает, как печь работает. Или, например, мы не можем установить температуру печи ниже или выше определенного уровня. Если не соблюдать принцип инкапсуляции, то мы должны будем говорить печи проверь-ка текущую температуру, пойдет те такая? Т.е. пекарь опять слишком много знает о печи. Геттеры и сеттеры это средства языка, которые помогут нам легко реализовать отслеживание изменений состояния. Все. Если геттеры и сеттеры пустые, значит так надо на моем уровне абстракции. Геттеры и сеттеры — не могут мешать реализации инкапсуляции, криво реализовать инкапсуляцию может проектировщик/программист.
В данном примере уровень абстракции выбран хорошо. Все занимаются своими делами, все три кита ООП работают во славу. Но стоит мне выбрать плохие абстракции, как начинается сущий кошмар. И даже есть стандарты чеклисты, которые помогут понять, хорошо ли вы выбрали абстракции и верна ли ваша декомпозиция в том ли направлении вы идете (SOLID).
Еще стали добавлять абстракцию, как еще один столп ООП. Я думаю, что это скорее верно, но уж очень попахивает КЭПом.
Высказывания про типизацию меня тоже зацепили. Дело в том, что никаких проблем в том, с кем вы сейчас работаете из наследников нет. Если на текущем уровне абстракции вам важно именно использовать печь, то вам не важно какая она. Вы получаете печь? Вы решаете свои задачи? То то и оно… Почему вы считаете что это динамическая типизация мне не понятно. Вы хотели печь? Берите. Вам нужна электрическая? Ну извините, газовая вам уже не подойдет.
Остальные примеры, которые были приведены в зацепившей меня статье, лишь примеры отвратительно выбранной абстракции и аналогии в рамках поставленной задачи. Точка.
Отдельно про DTO. DTO — это паттерн. Он позволяет создать объект, который передаст информацию другому слою, другой системе, короче куда-то чего-то передаст. Почему он не может быть рассмотрен мною как объект для меня вообще загадка. Где противоречие то? Является контейнером только? Ну и что?? Это же объект в рамках рассмотренной мною объектной модели на заданном уровне абстракции, где DTO — объект и часть декомпозиции.
Про языки тоже непонятно чего говорить. Я могу проектировать ПО используя объектный подход независимо от языка. Но если язык не реализует основные инструменты для работы с объектами, то мне будет очень сложно или невозможно реализовать спроектированную мною систему.
Еще говорят что некоторые вещи нельзя представить в виде объектов и их взаимодействия. Я уверен что это не так. Просто необходимо выбрать уровень абстракции верно. Будь то реализация протокола, слоя доступа к БД, подключения плагинов, менеджера задач, бизнес процесса, системы проектирования бизнес процессов т.е. все что угодно можно представить как объекты и их взаимодействие. Все можно реализовать как объекты и взаимодействие между ними. Хорошо это или плохо чаще всего зависит лишь от вашего умения мыслить объектно.
Резюмируя. Если вы не понимаете силу ООП, то скорее всего вам надо развивать объектное мышление.
ООП с примерами (часть 1)
Волею судьбы мне приходится читать спецкурс по паттернам проектирования в вузе. Спецкурс обязательный, поэтому, студенты попадают ко мне самые разные. Конечно, есть среди них и практикующие программисты. Но, к сожалению, большинство испытывают затруднения даже с пониманием основных терминов ООП.
Для этого я постарался на более-менее живых примерах объяснить базовые понятия ООП (класс, объект, интерфейс, абстракция, инкапсуляция, наследование и полиморфизм).
Первая часть, представленная ниже, посвящена классам, объектам и интерфейсам.
Вторая часть иллюстрирует инкапсуляцию, полиморфизм и наследование
Основные понятия ООП
Класс
Представьте себе, что вы проектируете автомобиль. Вы знаете, что автомобиль должен содержать двигатель, подвеску, две передних фары, 4 колеса, и т.д. Ещё вы знаете, что ваш автомобиль должен иметь возможность набирать и сбавлять скорость, совершать поворот и двигаться задним ходом. И, что самое главное, вы точно знаете, как взаимодействует двигатель и колёса, согласно каким законам движется распредвал и коленвал, а также как устроены дифференциалы. Вы уверены в своих знаниях и начинаете проектирование.
Вы описываете все запчасти, из которых состоит ваш автомобиль, а также то, каким образом эти запчасти взаимодействуют между собой. Кроме того, вы описываете, что должен сделать пользователь, чтобы машина затормозила, или включился дальний свет фар. Результатом вашей работы будет некоторый эскиз. Вы только что разработали то, что в ООП называется класс.
Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт).
С точки зрения программирования класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов).
С точки зрения структуры программы, класс является сложным типом данных.
В нашем случае, класс будет отображать сущность – автомобиль. Атрибутами класса будут являться двигатель, подвеска, кузов, четыре колеса и т.д. Методами класса будет «открыть дверь», «нажать на педаль газа», а также «закачать порцию бензина из бензобака в двигатель». Первые два метода доступны для выполнения другим классам (в частности, классу «Водитель»). Последний описывает взаимодействия внутри класса и не доступен пользователю.
В дальнейшем, несмотря на то, что слово «пользователь» ассоциируется с пасьянсом «Косынка» и «Microsoft Word», мы будем называть пользователями тех программистов, которые используют ваш класс, включая вас самих. Человека, который является автором класса, мы будем называть разработчиком.
Объект
Вы отлично потрудились и машины, разработанные по вашим чертежам, сходят с конвейера. Вот они, стоят ровными рядами на заводском дворе. Каждая из них точно повторяет ваши чертежи. Все системы взаимодействуют именно так, как вы спроектировали. Но каждая машина уникальна. Они все имеют номер кузова и двигателя, но все эти номера разные, автомобили различаются цветом, а некоторые даже имеют литьё вместо штампованных дисков. Эти автомобили, по сути, являются объектами вашего класса.
Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом.
Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе. В данном примере, если класс – это некоторый абстрактный автомобиль из «мира идей», то объект – это конкретный автомобиль, стоящий у вас под окнами.
Интерфейс
Когда мы подходим к автомату с кофе или садимся за руль, мы начинаем взаимодействие с ними. Обычно, взаимодействие происходит с помощью некоторого набора элементов: щель для приёмки монеток, кнопка выбора напитка и отсек выдачи стакана в кофейном автомате; руль, педали, рычаг коробки переключения передач в автомобиле. Всегда существует некоторый ограниченный набор элементов управления, с которыми мы можем взаимодействовать.
Интерфейс – это набор методов класса, доступных для использования другими классами.
Очевидно, что интерфейсом класса будет являться набор всех его публичных методов в совокупности с набором публичных атрибутов. По сути, интерфейс специфицирует класс, чётко определяя все возможные действия над ним.
Хорошим примером интерфейса может служить приборная панель автомобиля, которая позволяет вызвать такие методы, как увеличение скорости, торможение, поворот, переключение передач, включение фар, и т.п. То есть все действия, которые может осуществить другой класс (в нашем случае – водитель) при взаимодействии с автомобилем.
При описании интерфейса класса очень важно соблюсти баланс между гибкостью и простотой. Класс с простым интерфейсом будет легко использовать, но будут существовать задачи, которые с помощью него решить будет не под силу. В то же время, если интерфейс будет гибким, то, скорее всего, он будет состоять из достаточно сложных методов с большим количеством параметров, которые будут позволять делать очень многое, но использование его будет сопряжено с большими сложностями и риском совершить ошибку, что-то перепутав.
Примером простого интерфейса может служить машина с коробкой-автоматом. Освоить её управление очень быстро сможет любая блондинка, окончившая двухнедельные курсы вождения. С другой стороны, чтобы освоить управление современным пассажирским самолётом, необходимо несколько месяцев, а то и лет упорных тренировок. Не хотел бы я находиться на борту Боинга, которым управляет человек, имеющий двухнедельный лётный стаж. С другой стороны, вы никогда не заставите автомобиль подняться в воздух и перелететь из Москвы в Вашингтон.
Путь к ООП: Взгляд инженера
Дисклеймер
Статья не предполагает какой-то принципиально новый взгляд на вещи, кроме как с точки зрения изучения этого материала с «абсолютного нуля».
Материал основан на записях примерно 7-летней давности, когда мой путь в изучении ООП без IT-образования только начинался. В те времена основным языком был MATLAB, много позже я перешел на C#.
Изложение принципов ООП, которое я находил, с примерами в виде каких-то яблок, груш, унаследованных от класса «фрукты» и кучей терминологии (наследование, полиморфизм, инкапсуляция и т.п.), — воспринималось как китайская грамота.
Напротив, теперь же я почему-то воспринимаю подобный материал нормально, а изложение из своей же статьи временами кажется заморочным и длинным.
Но мои старые заметки и сохранившийся ужасный код на голодисках в пипбое говорят о том, что «классическое» изложение не выполняло в те времена свои функции, и было совершенно неудачным. Возможно, в этом что-то есть.
Насколько это соответствует действительности и вашим собственным предпочтениям, — решайте сами…
Предпослылки к ООП
Код стеной
Когда я только начинал писать на MATLAB’e, то только так писать и умел. Я знал про функции и про то, что программу можно делить на части.
Затык был в том, что все примеры были отстой. Я открыл чей-то курсовик, увидел там мелкие бодяжные функции по 2-3 строчки, в сумме все это НЕ работало (не хватало чего-то), и заработало только тогда, когда я пересобрал эту дрянь в «стену».
Потом я еще несколько раз писал какие-то мелкие программки, и всякий раз недоумевал, зачем там что-то делить. Уже потом пришло понимание: код «стеной» — это нормальное состояние программы объемом примерно 1.5 страницы А4. Никаких функций и, боже упаси, ООП там НЕ нужно.
Вот так примерно выглядит матлабовский скрипт (взято из интернета).
Деление кода на функции
О том, зачем код все-таки делят на куски, я догадался, когда его объем начал становиться совершенно невообразимым (сейчас нашел в архиве говнокод – 650 строк стеной). И тогда я вспомнил про функции. Я знал, что они позволяют разделить код на мелкие блоки, которые легче отладить и переиспользовать.
Но фишка в другом – почему-то все обучающие материалы молчат о том, СКОЛЬКО у функции переменных…
Курс математики говорил о том, что функция — это y=f(x)
Это называется «функция одной переменной». Например, y=x 2 это целая ПАРАБОЛА!
Задача по математике: построить ПАРАБОЛУ по точкам. В тетрадном листе, в клеточку.
А еще бывают функции двух переменных. z=f(x,y). И для нее — о боже — можно построить ТРЕХМЕРНЫЙ график. Но мы его строить не будем, т.к. на следующем уроке будет контрольная работа. На ней мы будем строить ПАРАБОЛУ.
А Сидоров не аттестован
А потом еще один товарищ, учащийся в ВУЗе по специальности «прикладная математика», рассказывает про функции трех переменных. Для такой функции, говорит он, надо построить ЧЕТЫРЕХМЕРНЫЙ график. Но четвертое измерение – это время. И мы можем только видеть проекции четырехмерного мира на трехмерное пространство.
И далее он торопливо говорит про четырехмерный куб-тессеракт…
А если функция имеет четыре и более переменных…. Теория суперструн. Многообразие Калаби-Яу. Смертным. Не дано. Понять…
Короче говоря, это все не то. В программировании нормальное состояние функции – это double vaginal double anal. Она принимает 100 переменных и возвращает столько же, и это нормально. Ненормально другое – перечислять их через ЗАПЯТУЮ.
Про то, что можно писать как-то иначе, я понял, когда наваял ВОТ ЭТО
Куча переменных через ЗАПЯТУЮ. А вызывающий код имеет совсем другие названия этих параметров, что-то типа SelectFun(a,b,c,d….) Поэтому нужно запоминать, на каком месте какая переменная стоит. И делать их расстановку через ЗАПЯТУЮ. А если код модернизируется, и количество переменных меняется, то надо их снова расставлять через ЗАПЯТУЮ.
А зачем в этом убожестве были глобальные (расстрелять!) переменные?
Бинго! Чтобы не расставлять переменные при каждой модернизации кода через ЗАПЯТУЮ.
Но ЗАПЯТАЯ все равно преследовала меня, как в кошмарном сне.
И появился varargin. Это значит, что я могу в вызывающем коде дописать еще много аргументов через ЗАПЯТУЮ…
И тогда я подумал о массивах. Учебные примеры взахлеб рассказывали о том, что массив может быть таким:
И понимаете, Х(2,3)=6, а Х(3,3)=9, и мы… мы можем организовать на таких массивах перемножение матрицами! На прошлом уроке мы проходили ПАРАБОЛЫ, а теперь МАТРИЦЫ….
И ни в одной строчке этих охренительных учебников нет короткого и ясного: массивы нужно для того, чтобы сделать функцию 100 переменных и не упасть от их перечисления через ЗАПЯТУЮ.
В общем, мне пришла в голову идея запихать все в одну большую двумерную таблицу. Вначале все шло хорошо:
Но хотелось большего. И начало получаться что-то вроде такого:
И все вроде замечательно, но… НУЛИ! Они появились оттого, что я хотел раскидать по разным строкам разнородные данные, и количество данных разного типа было разным… А как функция должна эти нули обрабатывать? А что будет, если я захочу модернизировать код? Я же должен буду переписать обработчик этих поганых нулей внутри функции! Ведь какая-то из переменных может реально быть равной нулю…
I never asked for this…
В общем, так я узнал о СТРУКТУРАХ.
Структуры
Диск D:\
Х (переменная-папка – «объект» или «структура»)
— a.txt (переменная-файл с данными – «поле объекта», англ. field. Хранится число 5)
— b.txt (хранится число 10)
— с.txt
Y (переменная-подпапка – «объект»)
— d.txt (хранится число 2)
— e.txt
После этого мы открываем файл и пишем туда число «2».
Теперь – как это будет выглядеть в программном коде. Там нет нужды ссылаться на «корневой локальный диск», поэтому D:\ там просто отсутствует, также у нас не будет расширения файла. Что же касается остального, то вместо слэша \ в программировании обычно используется точка.
Получается вот так:
В матлабе структуры (struct) можно нафигачить прямо на месте, не отходя от кассы, т.е. код выше является исполняемым, его можно вбить в консоль и все будет сразу работать. Структура сразу появится, и туда будут добавляться сразу все «переменные-файлы» и «переменные-подпапки». Про С#, к сожалению, так сказать нельзя, структура (struct) там задается геморройней.
Структура это более крутой родственник МАССИВА ТАБЛИЦЕЙ, где вместо индексов — файло-папочная система. Структура = «переменная-папка», в которой лежат «переменные-файлы» и другие «переменные-папки» (т.е. как бы подпапки).
Все знакомо, все ровно так же как на компе, папки, в них файлы, только в файлах не фотки, а цифры (хотя и можно и фотки).
Это более продвинутая версия хранения данных для передачи в ФУНКЦИЮ по сравнению с идеей сделать МАССИВ ТАБЛИЦЕЙ, в особенности двумерной, и, задави меня тессеракт, трех- и более- мерной.
МАССИВ ТАБЛИЦЕЙ юзабелен в двух случаях:
— он маленький (зачем он тогда? Что, нельзя передать в функцию аргументы через запятую?).
— либо по нему можно сделать цикл и автоматизировать поиск/заполнение (это не всегда возможно)
В реальности МАССИВ ТАБЛИЦЕЙ обычно используется только как одномерная строка однородных данных. Все остальное в нормальных программах делается по «файло-папочной» схеме.
Тогда почему в учебниках по программированию начинают с массивов таблицами.
Да, можно здесь заняться перфекционизомом и сделать кучу вложенных объектов, но задача не в этом. Главное – что теперь внутри функции переменная индексируется не по порядковому номеру (на каком месте в списке аргументов через ЗАПЯТУЮ она стоит), а по имени. И нет никаких тупых нулей. И вызов функции теперь приемлемого вида, ЗАПЯТЫХ всего 2 штуки, можно выдохнуть спокойно.
Классы
Понятие «класс» обрушил на меня тонну терминологии: инкапсуляция, наследование, полиморфизм, статические методы, поля, свойства, ординарные методы, конструктор… #@%.
По неопытности, разобравшись со структурами, я решил, что незачем усложнять сущности без надобности, и подумал – «классы – это типа тех же структур, только посложнее».
В какой-то степени так оно и есть. Точнее это прямо в точности так и есть. Класс, если очень глубоко смотреть – это СТРУКТУРА (идейный потомок массива таблицей), которая создается ПРИ ЗАПУСКЕ программы (вообще бывает вроде бы и не только при запуске). Как и в любом потомке МАССИВА ТАБЛИЦЕЙ, там хранятся данные. К ним можно получить доступ во время работы программы.
Поэтому мой первый класс был примерно таким (пишу пример на C#, в матлабе статические поля нормально не реализуются, только через кривой «хак» c persistent переменными в статической функции).
Вышеприведенный случай – это как бы «базовое» умение класса – быть тупо массивом (структурой) с данными. Эти данные в него закидываются при запуске программы, и оттуда их можно извлечь, в точности так же, как мы вытаскивали их из структуры выше. Для этого используется ключевое слово static.
У меня был затык – если поля это переменные, а методы это функции, то как они хранятся в одном месте? Как я понял, функция (метод) в классе – это на самом деле не функция, а указатель на функцию. Т.е. это примерно такая же «переменная», как число пи, в плане работы с ней.
Короче говоря, я вначале классы понял именно в таком объеме и написал еще порцию говнокода, где использовались ТОЛЬКО статические функции. Иначе как папку с функциями я классы вообще не юзал.
@ Math % папка
— Math.m %файл заголовка
— CircleLength.m %файл с функцией
Но вернемся к теме. Кроме статических методов, в классе имеется еще и конструктор. Конструктор – в общем-то это просто функция вида y=f(x) или даже y=f(). Входных аргументов у нее может не быть, выходной обязательно есть, и это всегда новая структура (массив).
Что делает конструктор. Он просто делает структуры. Логически это выглядит примерно так:
Говнокод на матлабе, делающий аналогичные структуры безо всяких классов (где класс присутствует — см. ниже):
И на выходе имеем структуру
Y (переменная-папка)
— a (переменная-файл, равна 5)
— b (переменная-файл равна 10)
Отсюда, собственно, видно, что так называемые поля класса (не статические, без ключевого кода static) — это локальные переменные, объявляемые внутри функции — конструктора. То, что они за каким-то лешим пишутся не в конструкторе, а снаружи, есть СИНТАКСИЧЕСКИЙ САХАР.
СИНТАКСИЧЕСКИЙ САХАР — такие bullshit-фичи языка программирования, когда код начинает выглядеть, как будто его хотят обфусцировать прямо при написании. Но зато он становится короче и быстрее (якобы) пишется.
Сделав это «открытие», я, писавший в то время только на матлабе, несказанно удивился.
В матлабе, как уже писал выше, эти структуры можно создавать на месте, без всяких конструкторов, просто написав Y.a=5, Y.b=10, точно так же как вы в операционной системе можете делать файлы и папки не отходя от кассы.
А тут — какой-то бодяжный «конструктор», и все поля структуры (в матлабе они называются properties — свойства, хотя, строго говоря, свойства — это более мутная вещь, нежели поля) нужно бюрократически прописывать в заголовочном файле. Зачем? Единственная польза, которую я тогда видел в этой системе — то, что поля структуры определены заранее, и это как бы «самодокументация» — всегда можно посмотреть, что там должно быть, а чего быть не может. Вот примерно такую лажу я тогда писал:
Т.е. вы все правильно поняли: методы только статические, конструктор хз для чего (написано в документации — О, классы должны иметь конструктор — ну вот вам конструктор), все остальное я тупо не знал и решил, что познал дзен и ООП.
Но все же собрать функции (статические методы) по классам-папкам мне казалось крутой идеей, т.к. их было много, и я сел писать говнокод.
Бюрократия
И уперся в такую вещь. Есть набор функций какого-то нижнего уровня логики (они статические и распиханы по классам-папкам, сейчас названия классов опустим):
В маленьких проектах добиться такого засилья функций невозможно, учебные примеры вообще содержат 2-3 функции — типа «смотри, как мы можем строить ПАРАБОЛУ».
А тут — фигова туча функций, и у каждой, мать ее, у каждой есть выходной аргумент, и что вот с ними всеми делать? Засовывать в функции более высокого («руководящего») уровня логики! Обычно их гораздо меньше (условно, 5 шт. вместо 20). Т.е. условно, нужно вот как-то взять эти Y1,Y2, Y3….Y20 и ПЕРЕПАКОВАТЬ их в какие-то Z1,Z2…Z5. Чтобы потом можно было сделать заседание партии и на нем:
Но Z1…Z5 не берутся сами по себе. Для их создания нужны ФУНКЦИИ-ПЕРЕПАКОВЩИКИ. Условно они работают как-то так…
А потом может быть еще один «руководящий» уровень…
Короче, я понял, что попал в логистический ад. Я не мог нормально извлекать данные из ФИГОВОЙ ТУЧИ мелких функций y=f(x) без написания еще ФИГОВОЙ ТУЧИ перепаковочно-бюрократических функций, а когда данные передаются еще на уровень выше, нужны еще ПЕРЕПАКОВЩИКИ. Итоговая программа забита бюрократизмом насквозь – перепаковщиков больше, чем «бизнес-кода». Классы-«папки-для-функций» не решают этой проблемы – они всего лишь собирают чиновных перепаковщиков-идиотов по кучкам.
А потом я решил такой говнокод модернизировать, и выяснилось, что без перепиливания всей бюрократической части это невозможно!
Прямо как жизнь в России…
Я понял, что делаю что-то не то, и разобрался в ООП получше. И решение — оно, если так смотреть, идейно было на поверхности.
Идея ООП
Зачем делать кучу функций вида y=f(x), выдающую РАЗНЫЕ выходные аргументы Y1….Y20, когда можно сделать ОДИН аргумент. Что-то вроде:
Тогда абсолютно все результаты работы функций будут засованы в одну структуру, в один массив, просто в разные его отсеки. Все. Дальше Y_all можно передавать сразу наверх, на верхний уровень «руководства».
Вот именно в этом идея ООП и состоит. В учебниках пишут учебные примеры про яблоки и груши, а потом показывают программу в 5 строчек. Там вообще не нужно никакого ООП, в примерах на 5 строчек, т.к. передача данных на «высший уровень руководства» делается без проблем напрямую.
ООП нужно тогда, когда большой проект и есть проблема «бюрократизации»….
Но вернемся к сути. В реальном ООП есть СИНТАКСИЧЕСКИЙ САХАР. Приведенный выше пример с Y_all использовал просто структуры, функции f(. ) будем считать статическими. ООП – это набор сахарка, когда код начинает выглядеть вот так:
Т.е. мы как бы решили навести мутный синтаксис, в котором можно не писать Y_all 2 раза, а сделать это только 1 раз. Ибо повторение — мать заикания.
Все остальное объяснение «как работает ООП» сводится к объяснению того, как функционирует синтаксический сахар.
Как функционирует синтаксический сахар ООП
Во-первых, эту базу данных Y_all, очевидно, нужно создать до того момента, как она будет идти аргументом в функцию. Для этого нужен конструктор.
Во-вторых, предусмотреть, желательно заранее, какие «отсеки» в ней будут. Пока база данных Y_all маленькая, такая постановка задачи вызывает раздражение. Хочется помечтать о «создаваемых на ходу классах», примерно так же, как в MATLAB можно делать структуры простыми командами Y.a=5, Y.b=10. Но желание фантазировать на эту тему пропадает после отладки здорового проекта.
Далее — вызов метода (функции).
Вот так это примерно эволюционировало
| Функция | Комментарий |
|---|---|
| Y=f(X) | Так было в математике, когда мы строили по точкам ПАРАБОЛУ! |
| Х=f(X) | Нас задрали бюрократы, и у нас |
| f(X) | Зачем функции возвращать аргумент? Это архаизм времен уроков математики! И бессмысленный расход памяти! Пусть данные передаются по ссылке, тогда функция сама придет к аргументу, поменяет и уйдет. НИЧЕГО=f(X) Не гора идет к Магомету, а Магомет — к горе. |
| Х.f() | Просто вытащили аргумент Х «наружу» синтаксическим сахаром. НИЧЕГО=Х.f(НИЧЕГО) |
Теперь – как устроена внутри такая вот функция, принимающая НИЧЕГО и НИЧЕГО (ключевое слово void в C#) возвращающая.
Мне нравится, как это сделано в матлабе (с точки зрения именно понимания): функция, которую мы вызываем как X.f(), внутри пишется как
| Пример кода на MATLAB | Пример кода на C# |
|---|---|
| Переменную «по умолчанию» надо всегда писать самой первой. Обозвать ее — как угодно (можно Х, можно this, можно fuck, можно shit). Я обычно в матлабе ее называю this, для единообразия. | Переменную «по умолчанию» писать не надо вообще. При обучении программированию может показаться, что ее нет (и это был для меня затык)! Но она есть! Как тот самый суслик, она есть и скрыта в «ключевом слове this». «Переименовать» this нельзя (хотя это и к лучшему). |
Вот такая функция с «аргументом по умолчанию — this», лежащая в классе, как в папке — есть ординарный метод (ordinary method, хз, как правильно по-русски).
На самом деле пихать вообще все аргументы в единый this — не всегда правильно. Иногда нужны и какие-то еще аргументы (допустим, это ввод данных пользователем):
Иногда надо даже возвращать аргумент (например, об успешности или неуспешности какой-либо операции), а не писать void. Что, впрочем, не меняет статистики: большинство функции в ООП возвращают НИЧЕГО (void) и принимают либо ничего (аргумент по умолчанию не в счет), либо очень мало аргументов.
Напишем итоговый код
На MATLAB
Когда я разобрался с этим, то вроде бы большинство проблем с написанием кода отошло на второй план…
Свойства (properties) vs поля (fields)
| without properties | with properties |
|---|---|
| комментарий: aргумент Set_a можно назвать как угодно Set_a(int YourVarName) | комментарий: переменную внутри set <. >нужно называть всегда value |
Вещь это довольно удобная и часто используемая, но это все равно СИНТАКСИЧЕСКИЙ САХАР.
Field является полноценной переменной. Property — это 2 метода класса (get и set), синтаксис вызова которых копирует «вызов переменной».
На самом деле внутри get и set можно творить хрень:
Поэтому вроде как рекомендуется писать название properties с большой буквы, а fields — с маленькой.
Бывает (например, в интерфейсах нельзя создавать field), что надо сделать по-быстрому property, тогда можно:
Наследование, инкапсуляция, полиморфизм
Почему про них раньше не упоминал? Потому, что
— на самом деле, при написании кода они востребованы далеко не с такой силой, как о них упоминается при запросе «Ok Google, what is OOP». Я бы даже сказал, что поначалу они практически нахрен не нужны.
— там, где они нужны, о них можно прочитать (только ленивый про это дело не писал).
— большинство классов у вас будут БЕЗ наследования. Вы просто запиливаете в нужном классе ВЕСЬ функционал, и наследовать что-то не особо и нужно.
— соответственно, полиморфизм (примочка к наследованию) тоже идет лесом
— «инкапсуляция» сведется к приписыванию везде (ко всем полям, свойствам и методам) public.
Потом у Вас прирастут руки к плечам, и Вы разберетесь сами, без этой статьи, где так делать НЕ надо, особенно где НЕ надо писать public.
Но все-таки краткий обзор на них.
Наследование. Это умный копи-паст
О, в моем говнокоде есть класс MyClass, и в нем не хватает еще одного поля SHIT и еще одного метода DO_THE_SHIT()!
*Ctrl+C, Ctrl+V
*Делается новый класс MyClass_s_fichami и туда дописываются желаемое
Но все-таки мы более цивилизованные люди, и знаем, что лучше не копировать текст программы, а сослаться на него.
Допустим, мы все равно пишем на каком-то древнем языке программирования или не в курсе о такой вещи, как «наследование». Тогда мы пишем 2 разных класса
То, что мы сделали справа — это и есть «наследование». Только в нормальных языках программирования это делается одной командой:
Работает «снаружи» код ровно так же, как в варианте 2. Т.е. объект как бы становится «матрешкой» — внутри одного объекта сидит тупо другой объект, и есть «каналы связи», дергая за которые, можно обратиться к внутреннему объекту напрямую.

В матлабе дело обстоит несколько интересней. Когда вы запускаете конструктор «потомка», MyClassB, то «тихушного» вызова конструктора предка MyClassA — не происходит.
Его нужно напрямую создать. С одной стороны это напрягает:
Но если потомок вызывается вообще с другими аргументами, типа MyClassB(d), тогда можно сделать внутри преобразование, что-то типа:
В C# так сделать напрямую нельзя, и это порождает необходимость писать какие-то «преобразовывающие функции»:
или же делать «статические конструкторы» вот так:
Вроде про наследование, в основном, все.
Естественно, что никто не заставляет обязательно писать у наследника метод «F1» и свойство (property) «a» так, чтобы они обязательно транслировались в вызов метода и поля предка. Трансляция – это просто поведение «наследования» по умолчанию.
Можно (естественно! это же другие методы в другом классе, бро) написать вот так:
Инкапсуляция
… Концептуально это означет, что внутри объекта класса MyClassB в поле base сидит объект класса MyClassA, с возможностью трансляции управляющих команд снаружи. Обо всем этом написано выше и повторять смысла не имеет.
Есть такая тема с разными модификаторами доступа — public, private, protected… О них, что самое интересное, написано везде более-менее нормально, рекомендую просто прочитать об этом.
public — это будет означать, что field, property или method будут видны снаружи и за них можно будет дергать.
Если вы НЕ знаете, что делать, пишете public (вредный совет, да).
Потом найдите в себе силы и выкиньте этот public (ну или для наглядности замените на private) везде, где он лишний (сделайте «рефакторинг»). Да, разумеется, очень хорошо быть провидцем, сниматься в битве экстрасенсов и догадаться сразу, где надо сделать private.
private — это означает, что field, property или method «файло-папочного» объекта виден только изнутри методов данного класса.
НО… Именно класса, не ИНСТАНЦИИ (объекта). Если у вас есть код вида:
Такая штука используется в клонировании (подробнее см. другие источники).
Я пытался при написании кода думать об этой расстановке public и private. При черновых набросках кода на это тратится непозволительно много времени. А потом оказывается, что вообще сам код надо делать принципиально по-другому.
Если код пишется в соло, то нет смысла заморачиваться с private и public раньше времени, есть более важные задачи, например, собственно придумать и написать код…
Единственное место, где более-менее ясно, в каком месте ставить private и public — это те самые пресловутые свойства, которые ссылаются на какое-то поле.
В остальных местах для расстановки public и private надо реально смотреть, что программа делает, и обучиться этому «заочно», скорее всего, не выйдет.
protected — это означает «public» для всех методов классов-наследников и «private» для всего остального.
В общем-то логично, если считать, что классы-наследники появляются как просто «более навороченные версии» предков.
Честно говоря, уже и забыл, где в явном виде я этот protected применял. Обычно либо public, либо private. Большинство классов, которые я писал, не наследовались ни от каких других пользовательских классов, а там, где наследовались, какая-то серьезная потребность в таких вещах возникала редко.
Впечатление такое, что НЕ-public модификаторы востребованы при работе над каким-то большим проектом, который, возможно будет поддерживаться кучей людей… Понимание того, где их применять, появляется только спустя большое время втыкания в километровой длины код. При обучении «заочно» как-то дать это понимание затруднительно.
Полиморфизм
Кода я писал на матлабе, я никак не мог допетрить, зачем вообще нужен полиморфизм и ЧТО ЭТО.
Потом, когда перешел на C#, дошло, что это фича СТРОГО ТИПИЗИРОВАННЫХ ЯЗЫКОВ, и к ООП она имеет весьма слабое отношение. В матлабе можно вообще везде писать, не зная о существовании этого полиморфизма – там нет строгой типизации.
Для простоты пусть классы называются А и В
Это называется приведение типов. В C# можно САМОМУ (если знать как) написать свои кастомные самопальные системы приведения типов, чуть ли ни каких угодно типов к каким угодно другим.
Тут — просто «приведение типов» из коробки. Раз внутри объекта x, принадлежащему к классу B, сидит другой объект класса A, то один из вроде как очевидных способов приведения — замкнуть все связи от внешнего объекта на внутренний.
На самом деле так делать вовсе необязательно, но те, кто придумал «полиморфизм», решили, что наиболее очевидно будет сделать именно так. А остальные варианты пользователь сам напишет.
Простите за (уже не совсем актуальную) «политоту» образца 2008-2012 гг.
Интерфейс
Надо начать с того, как ЭТО применять.
Допустим, у нас есть списочек, и мы в него что-то хотим положить.
В матлабе наиболее просто сделать это так (называется cell array):
Вы не думаете, что это за объект, вы просто берете его и кладете в списочек.
Далее, допустим, вам нужно сделать цикл по списочку и сделать с каждым элементом что-то:
Если функция DoSomeStuff настолько умная, что переваривает все, что ей скармливают, этот код ВЫПОЛНИТСЯ.
Если функция DoSomeStuff (или ее автор) – интеллектом не блещет, то есть вероятность подавиться чем-то: цифрой, строкой, Вашим самопальным классом, Лысым Чертом или – не дай бог — Вашей Бабушкой.
MATLAB покажет красную ругань на английском в консоли и прекратит работу Вашей программы. Таким образом, Ваш код автоматически получает Премию Дарвина.
Однако, на самом деле, это плохо, потому что иногда код бывает очень сложным. Тогда Вы будете свято уверены в том, что сделали все правильно, но на самом деле ошибочная комбинация действий просто ни разу не запускалась во время тестирования.
Именно поэтому (хотя и не только поэтому) на MATLAB – успел убедиться в этом сам (примерно как на КПДВ), на ужасных размеров коде — НЕЗАЧЕМ писать большие проекты.
Теперь переходим к C#. Мы делаем списочек, и… и нас просят сразу указать ТИП объекта. Мы создаем список типа List.
В такой список можно поместить число 1.
В такой список можно поместить число 2 и даже, прости господи, 3.
Но текстовые строки – уже нет. Объекты Вашего самопального класса – строго нет. Я молчу насчет Лысого Черта и Вашей Бабушки, они там не могут оказаться ни при каком варианте.
Можно сделать отдельно списочек строк. Можно – для ваших самопальных классов.
На самом деле можно сделать и списки – отдельно — Лысых Чертей, Ваших Бабушек.
Но сложить их в один список не получится. Ваш код получит Премию Дарвина в сочетании с руганью компилятора еще до того, как вы его попробуете запустить. Компилятор предусмотрительно не дает Вам сделать функцию DoSomeStuff(item), которая «подавится» своим аргументом.
В больших проектах это реально удобно.
Но что делать, когда в один списочек сложить все-таки хочется?
— Что вы умеете делать?
— я умею петь и танцевать
— А я — Санчо…
— Что ты умеешь делать, Санчо?
— Я — Санчо.
— Ну ты можешь делать хоть что-то?
— Вы не понимаете. Я могу быть Санчо.
Поэтому пишется интерфейс. Это такой класс, от которого нужно наследоваться. Интерфейс содержит заголовки методов и свойств.
В нашем случае это те методы и свойства, которые обеспечивают НОРМАЛЬНУЮ работу функции DoSomeStuff(item). Сами свойства интерфейс при этом не реализует. Это специально так сделано. На самом деле можно было бы просто унаследоваться от какого-то класса, пригодного для употребления функцией DoSomeStuff(). Но это означает дополнительный код и забывчивого программиста.
Поэтому, если товарищ программист унаследовался от интерфейса, но забыл реализовать нужные свойства и методы класса, компилятор выпишет его коду Премию Дарвина. Таким образом, можно сделать так:
Т.е. для чего в конечном счете нужен интерфейс — для того, чтобы сделать типизированный списочек, или какое-то поле в классе, и обойти запрет на добавление (в списочек или поле) туда какой-то левой фигни.
Интерфейс — это «бюрократия». Не везде она есть и не везде она нужна, хотя да, в больших проектах нужна и полезна.
… в общем, как-то так… Извиняюсь за резковатые выражения, мне почему-то кажется, что «сухое» изложение материала было бы неудачным…


