Вообще меня, как и многих едва ли не в школе научили, что вот, есть такая штука, как объектно-ориентированное программирование. Есть объекты и всё такое. Потом тому же учили в институте. Но как-то так вышло, что моё понимание ООП со времён института очень сильно изменилось, и хочется это изменение зафиксировать. Поэтому «сейчас» означает «не так, как тогда», впрочем, видимо через год-два можно будет написать новое «сейчас», которое также будет отличаться.
Итак, что было
Нас учили, что основные понятия ООП, это:
- Инкапсуляция
- Наследование
- Полиморфизм
При этом инкапсуляция – сокрытие всего так, чтобы снаружи знали поменьше о классе; наследование – возможность создавать новые классы на базе существующих, заменять в них методы, дополнять, и использовать части предка; полиморфизм – возможность использовать один класс вместо другого (как правило из той же иерархии)… ну в общем тут был совершенно замечательный пример про рисование – фигура – линия, фигура – квадрат.
То, что я извлёк (это вероятно относится и ко мне и к программе, т.к. очень многие после той же программы думают также) – это то, что я могу написать в резюме «я знаю ООП», что в итоге сводится к тому, что я умею записывать процедурный код в классы, и моим любимыми паттернами являются недоделанный Template Method и God Object (не в крайнем проявлении, но как направление).
Что надо было вбить или услышать
Инкапсуляция - это не только то, что можно сокрыть часть данных и методов, но сам принцип, что мы ограничиваем внешнее знание об объекте с целью уменьшения количества зависимостей. А также важны примеры, как это делать или не делать… без фанатизма «все свойства должны быть закрытыми», но с пониманием того, что надо задумываться «а по сути это ожидается снаружи?».
Простите крамольную мысль необразованного человека, но интерфейсы – тоже форма инкапсуляции, потому что оставяют от всего интерфейса класса только несколько согласованных методов, уменьшая зависимости.
Наследование - о да! Наследование решает все проблемы
. И на следующем шаге порождает в два раза больше, если использовать неправильно. Особенно прикольны рассуждения «этот метод нужен в 3 из 5 наследников, давайте поднимем его в родителя». Вообще наверное корень зла в том, что наследование у меня в своё время спозиционировалось, как способ повторного использования кода, хотя таковым не является. Наследования – это отношение «является» и очень сильная связь между двумя классами. Если нужно повторное использование кода – есть масса других механизмов.
Мой любимый Template Method хотя и имеет право на жизнь в принципе, но в тех объёмах, в которых он использовался (и используется порой :-Р) – страшное зло, потому что исполнение неявно скачет из одного класса в другой и когда что произойдёт предсказать сложно (как и что обрушится, если что-то изменить). (З.Ы. Там надо было его ограничивать и делать более строгим, никаких реализаций по умолчанию, которые потом опционально перекрываются).
Ещё про наследование – очень важная вещь – принцип подстановки Лискоу. Что объект порождённого класса должен использоваться везде где используется его предок без нарушения интерфейса (в частном) и вообще обещаний (в целом). Является ли правильной цепочка наследования Фигура – Прямоугольник – Квадрат? В общем нет, потому что с точки зрения программирования Квадрат часто не является Прямоугольником (как «честно» реализовать методы setWidth/setHeight в квадрате?).
Полиморфизм – вот тут интересно, пусть это моё имхо, но полиморфизм надо рассказывать на базе интерфейсов (каждый класс – неявно порождает интерфейс, который наследуют предки, так что для классов и наследников всё верно). И это тоже к принципу подстановки Лискоу – кто реализует интерфейс – реализует его полностью и соблюдая обещания, если нельзя полностью – то что-то не так, возможно нужно два интерфейса вместо одного.
А сюда кстати завязана инкапсуляция с другого конца – каждый код должен очень чётко и минимально сформулировать, что ему надо от других объектов, и больше ничего не спрашивать. Именно поэтому я считаю, что интерфейс должен предоставляться «потребителем» объекта. Т.е. если есть алгоритм сортировки, то он даёт интерфейс «Сортируемое» – и уже задача пользователя реализовать его или адаптер, если они хотят сортироваться этим алгоримом. Можно пойти и дальше, отвязав для нескольких потребителей общий интерфейс, важно, не столкнуться с ситуацией, когда интерфейс распухает, а каждый потребитель использует только часть – тогда нужны разные интерфейсы. // Лирическое отступление
Вообще. Наверное одна из причин была в том, что на ОО программирование смотрелось со статической позиции – как распихать код по классам, и не было никакого понимания, что классы должны взаимодействовать и основная собака здесь – это должно быть понятно, гибко и эффективно.
З.Ы. повторное использование. Написал выше, что без наследования, а как иначе? Мы создали объект, вызвали метод – вот мы и использовали код повторно. Надо поменять? Сделаем, чтобы метод (или конструктор объекта, это зависит от ситуации) принимал другой объект, реализующий меняющуюся часть – вот у нас появился «набор» методов – для каждого нового объекта это новый метод (это и есть полиморфизм). Код использовался повторно, а мы не обременили себя жёсткой связью «наследуется», код гибкий.
Что ещё
А ещё очень интересно расширить применение и показать альтернативы. Расширить применение – показать, что такое TDD, объяснить, зачем и дать попробовать… ещё при обучении. Это классная лакмусовая бумажка, показывающая закрепощённость и неправильность ОО-кода.
Альтернативы – мне очень понравилось познакомиться с миром функционального программирования, не могу сказать, что я по нему спец, но это однозначно добавило новых подходов в моё программирование. Об этом тоже надо рассказать.
Функциональное программирование – всё функция. В применении к вебу страница – это функция от GET, POST, FILES, COOKIES. Функция не имеет состояния – т.е. результат всегда зависит только от параметров, и это один из полезных принципов. Функция может быть суперпозицией других функций, функции могут передаваться как параметры, может быть замыкание – функция, принимающая в качестве параметров часть того места, где она быа создана (не знаю, правильно ли это теоретически, там carrying и всё такое, но по сути так).
Что это даёт в моём ООП?
То, что в объектах не должно быть состояния помимо их настройки, да, настройка может меняться, но при этом вся динамика – отдельно. Пример. Есть класс, обрабатывающий ввод данных, должен ли этот класс сам хранить введённые данные и ошибки ввода? Нет, они не имеют к нему отношение – они являются результатом проверки. Он хранит настройку «как проверять», её можно менять по ходу, но он не хранит сами результаты, желательно даже во время обработки.
Ещё одна штука – связь вызовов методов. Имхо желательно минимизировать зависимости «вызови метод А, получи результат, добавь и вызови метод Б», и не дай бог метод А что-то меняет внутри объекта и без него метод Б вызвать нельзя это совсем ужас… Да и вообще, начинал писать абзац к тому, что очень многие вещи решаются паттерном «Команда», что является ничем иным, как замыканием в функциональном программировании – это объект, который можно настроить при создании, а дальше у него есть только один метод с определёнными параметрами… такой аналог функции-параметра.
Итого – сейчас очень тянет к архитектуре, где объекты хранят только то, что определяет их работу, но не промежуточные данные, сами объекты маленькие, фактически близки к функции с несколькими предзаданными параметрами… возможно к семейству функций с одинаковыми настройками. А результат работы программы – некий прогон входных данных через сеть связанных объектов. Сама сеть от этого меняется только если входные данные заставляют её измениться, все же временные эффекты после выполнения забываются. И этот подход транслируется на каждый отдельный кусочек выполнения.