Я не умею программировать на объектно-ориентированных языках.
Я не учился.
После 5 лет промышленного программирования на Java я до сих пор не знаю, как создать хорошую систему в объектно-ориентированном стиле.
Я просто не понимаю.
Я пытался учиться, честно.
Я изучал паттерны, читал код проектов с открытым исходным кодом, пытался выстроить в голове связные концепции, но так и не понял принципов создания качественных объектно-ориентированных программ.
Возможно, кто-то другой их понял, но не я.
И вот несколько вещей, которые меня смущают.
Я не знаю, что такое ООП
Серьезно.Мне сложно сформулировать основные идеи ООП.
В функциональном программировании одна из основных идей — безгражданство.
В структурном – разложение.
В модульном функционал разделен на полноценные блоки.
В любой из этих парадигм доминирующие принципы применимы к 95% кода, и язык разработан таким образом, чтобы поощрять их использование.
Я не знаю таких правил для ООП.
Принято считать, что объектно-ориентированное программирование основано на 4 основных принципах (когда я был маленьким, их было всего 3, но тогда деревья были большими).
Эти принципы:
- Абстракция
- Инкапсуляция
- Наследование
- Полиморфизм
Абстракция Абстракция — мощный инструмент программирования.
Именно это позволяет нам строить большие системы и сохранять контроль над ними.
Вряд ли мы когда-либо приблизились бы к сегодняшнему уровню программ, если бы не были вооружены таким инструментом.
Однако как абстракция связана с ООП? Во-первых, абстракция не является свойством исключительно ООП или программирования в целом.
Процесс создания уровней абстракции распространяется практически на все области человеческого знания.
Таким образом, мы можем судить о материалах, не вдаваясь в подробности их молекулярной структуры.
Или говорить о предметах, не упоминая материалы, из которых они изготовлены.
Или говорить о сложных механизмах, таких как компьютер, турбина самолета или человеческое тело, не запоминая отдельных деталей этих сущностей.
Во-вторых, абстракции в программировании существовали всегда, начиная с трудов Ады Лавлейс, которую принято считать первым программистом в истории.
С тех пор люди постоянно создавали абстракции в своих программах, часто используя для этого лишь простейшие инструменты.
Так, Абельсон и Сассман в своей пресловутой книга описать, как создать систему решения уравнений, поддерживающую комплексные числа и даже полиномы, используя только процедуры и связанные списки.
Какие же дополнительные средства абстракции предоставляет ООП? Не имею представления.
Разделить код на подпрограммы? Любой язык высокого уровня может это сделать.
Совмещаете процедуры в одном месте? Модулей для этого достаточно.
Типизация? Она существовала задолго до ООП.
Пример с системой решения уравнений наглядно показывает, что построение уровней абстракции зависит не столько от средств языка, сколько от способностей программиста.
Инкапсуляция Основное преимущество инкапсуляции — сокрытие реализации.
Клиентский код видит только интерфейс и может полагаться только на него.
Это освобождает разработчиков, которые могут решить изменить реализацию.
И это действительно здорово.
Но опять же вопрос, причем здесь ООП? Все Вышеупомянутые парадигмы предполагают сокрытие реализации.
При программировании на C вы размещаете интерфейс в заголовочных файлах, Oberon позволяет делать поля и методы локальными для модуля, и, наконец, абстракция во многих языках строится просто через подпрограммы, которые также инкапсулируют реализацию.
Более того, сами объектно-ориентированные языки часто нарушить правило инкапсуляции , обеспечивающие доступ к данным через специальные методы — геттеры и сеттеры в Java, свойства в C# и т.д. (В комментариях мы выяснили, что некоторые объекты в языках программирования не являются объектами с точки зрения ООП: объекты передачи данных — это отвечают исключительно за передачу данных и, следовательно, не являются полноценными объектами ООП, и поэтому им нет необходимости сохранять инкапсуляцию.
С другой стороны, методы доступа лучше всего сохранять для сохранения архитектурной гибкости.
) Более того, некоторые объектно-ориентированные языки, например Python, вообще ничего не пытаются скрыть, а полагаются исключительно на интеллект разработчиков, использующих код. Наследование Наследование — одна из немногих новых вещей, которые действительно появились на сцене благодаря ООП.
Нет, объектно-ориентированные языки не породили новой идеи — наследование можно реализовать в любой другой парадигме — но ООП впервые вывело эту концепцию на уровень самого языка.
Преимущества наследования также очевидны: когда вы почти удовлетворен каким-то классом, вы можете создать его потомка и переопределить некоторую часть его функциональности.
В языках, поддерживающих множественное наследование, таких как C++ или Scala (в последнем — за счет трейтов), появляется другой вариант использования — миксины, небольшие классы, позволяющие «подмешивать» функционал в новый класс, не копируя код. Итак, именно это отличает ООП как парадигму от других? Хм.
если да, то почему мы так редко используем его в реальном коде? Помните, что я говорил о том, что 95% кода подчиняется правилам доминирующей парадигмы? Я не шутил.
В функциональном программировании не менее 95% кода использует неизменяемые данные и функции без побочных эффектов.
В модульном подходе почти весь код логически упакован в модули.
Сторонники структурного программирования, следуя заветам Дейкстры, стараются разбить все части программы на мелкие части.
Наследование используется гораздо реже.
Может в 10% кода, может в 50%, в некоторых случаях (например, при наследовании от классов фреймворка) — в 70%, но не более.
Потому что в большинстве ситуаций это легко незачем .
Более того, наследование опасный за хороший дизайн.
Настолько опасно, что «Банда четырех» (вроде бы проповедники ООП) в своей книге рекомендуют по возможности заменять ее делегированием.
Наследование в том виде, в котором оно существует в популярных сейчас языках, приводит к хрупкости конструкции.
Унаследованный от одного предка, класс больше не может быть унаследован от других.
Изменение предка также становится опасным.
Конечно, существуют модификаторы Private/protected, но они также требуют значительных экстрасенсорных способностей, чтобы угадать, как класс может измениться и как клиентский код может его использовать.
Наследование настолько опасно и неудобно, что крупные фреймворки (такие как Spring и EJB в Java) отказываются от него в пользу других, необъектно-ориентированных инструментов (например, метапрограммирования).
Последствия настолько непредсказуемы, что некоторые библиотеки (например, Guava) присваивают своим классам модификаторы, запрещающие наследование, а в новом языке Go было решено вообще отказаться от иерархии наследования.
Полиморфизм Возможно, полиморфизм — лучшее, что есть в объектно-ориентированном программировании.
Благодаря полиморфизму объект типа Person при выводе выглядит как «Шандоркин Адам Импполитович», а объект типа Point — как «[84.23 12.61]».
Именно это позволяет написать «Mat1*Mat2» и получить произведение матриц, подобное произведению обычных чисел.
Без него было бы невозможно читать данные из входного потока, не заботясь о том, приходят ли они из сети, файла или строки в памяти.
Везде, где есть интерфейсы, подразумевается и полиморфизм.
Мне очень нравится полиморфизм.
Так что я даже не буду о нем говорить проблемы на основных языках.
Я также умолчу об узости подхода к диспетчеризации только по типам и о том, как это можно было бы сделать .
В большинстве случаев работает как надо, что неплохо.
Вопрос в том, является ли полиморфизм тем самым принципом, который отличает ООП от других парадигм? Если бы вы меня спросили (а раз вы читаете этот текст, то можете предположить, что вы спросили), я бы ответил «нет».
А причина всё тот же процент использования в коде.
Возможно, интерфейсы и полиморфные методы встречаются немного чаще, чем наследование.
Но сравните количество строк кода, которые они занимают, с количеством строк, написанных в обычном процедурном стиле — последних всегда больше.
Глядя на языки, которые поощряют такой стиль программирования, я бы не назвал их полиморфными.
Языки, поддерживающие полиморфизм — да, это нормально.
Но не полиморфные языки.
(Впрочем, это мое мнение.
Вы всегда можете не согласиться.
) Итак, абстракция, инкапсуляция, наследование и полиморфизм — всё это есть в ООП, но ничто из этого не является его неотъемлемым атрибутом.
Тогда что такое ООП? Существует мнение, что суть объектно-ориентированного программирования заключается, по сути, в объектах (звучит вполне логично) и классах.
Это идея объединения кода и данных, а также идея о том, что объекты в программе отражают сущности реального мира.
К этому мнению мы еще вернемся, но сначала давайте расставим точки над «i».
Чей ООП круче?
Из предыдущей части понятно, что языки программирования могут сильно различаться по способу реализации объектно-ориентированного программирования.Если взять совокупность всех реализаций ООП на всех языках, то, скорее всего, вы не найдете ни одной общей для всех функции.
Чтобы как-то ограничить этот зоопарк и прояснить рассуждения, я остановлюсь только на одной группе — чисто объектно-ориентированных языках, а именно Java и C#.
Термин «чисто объектно-ориентированный» в данном случае означает, что язык не поддерживает другие парадигмы или реализует их посредством того же ООП.
Python или Ruby, например, не будут чистыми, потому что на них можно легко написать полноценную программу без единого объявления класса.
Чтобы лучше понять суть ООП в Java и C#, рассмотрим примеры реализации этой парадигмы в других языках.
Болтовня.
В отличие от своих современных аналогов, этот язык был динамически типизирован и использовал стиль передачи сообщений для реализации ООП.
Вместо вызова методов объекты отправляли друг другу сообщения, и если получатель не мог обработать то, что пришло, он просто пересылал сообщение кому-то другому.
Обычный Лисп.
Первоначально CL следовала той же парадигме.
Тогда разработчики решили, что писать `(send obj 'some-message)` слишком долго, и преобразовали обозначение в вызов метода — `(some-method obj)`.
Сегодня Common Lisp имеет развитую систему объектно-ориентированного программирования (CLOS) с поддержкой множественного наследования, мультиметодов и метаклассов.
Отличительной особенностью является то, что ООП в CL вращается не вокруг объектов, а вокруг обобщенных функций.
Клодюр.
В Clojure есть 2 системы объектно-ориентированного программирования — одна унаследована от Java, а вторая основана на мультиметодах и больше похожа на CLOS. Р.
Этот язык статистического анализа данных также имеет 2 системы объектно-ориентированного программирования — S3 и S4. Оба унаследованы от языка S (что неудивительно, учитывая, что R — это реализация коммерческого языка S с открытым исходным кодом).
S4 во многом соответствует реализациям ООП в современных основных языках.
S3 — более легкий вариант, просто реализуемый с помощью самого языка: создается одна общая функция, отправляющая запросы на основе атрибута «класс» полученного объекта.
JavaScript. Идеологически похож на Smalltalk, хотя использует другой синтаксис.
Вместо наследования здесь используется прототипирование: если нужного свойства или вызываемого метода нет в самом объекте, то запрос передается объекту-прототипу (свойству прототипа всех объектов JavaScript).
Интересным фактом является то, что поведение всех объектов класса можно изменить, заменив один из методов-прототипов (например, очень красиво выглядит добавление метода `.
toBASE64` для класса string).
Питон.
В целом он следует той же концепции, что и основные языки, но также поддерживает передачу поиска атрибута другому объекту, как в JavaScript или Smalltalk. Хаскелл.
В Haskell вообще нет состояния, а значит, и объектов в обычном понимании.
Однако своеобразное ООП здесь все же присутствует: типы данных могут принадлежать одному или нескольким классам типов.
Например, почти все типы в Haskell находятся в классе Eq (отвечает за операции сравнения между двумя объектами), а все числа дополнительно находятся в классах Num (операции над числами) и Ord (операции над числами).
<, <=, > =, > ).
В менструальных языках типы соответствуют классам (данных), а классы типов соответствуют интерфейсам.
С состоянием или без гражданства?
Но вернемся к более распространенным системам объектно-ориентированного программирования.Чего я никогда не мог понять, так это связи объектов с внутренним состоянием.
До изучения ООП все было просто и прозрачно: есть структуры, хранящие несколько связанных данных, есть процедуры (функции), которые их обрабатывают. выгулять (собаку), снять (счет, сумму).
Потом объекты приехали, и это тоже было ничего страшного (правда, читать программы стало гораздо сложнее - моя собака гуляла [кто?], а со счета снимались деньги [откуда?]).
Затем я узнал о сокрытии данных.
Я еще мог выгуливать собаку, но уже не мог смотреть на состав ее еды.
Еда ни на что не повлияла (наверное, можно было бы написать food.eat(dog), но я все равно предпочитаю, чтобы моя собака ела еду, а не наоборот).
Еда — это всего лишь данные, и мне (и моей собаке) просто нужно было получить к ним доступ.
Все Только .
Но в рамки парадигмы, как в старые джинсы конца 90-х, вписаться уже не удалось.
Хорошо, у нас есть методы доступа к данным.
Давайте потворствуем этому небольшому самообману и сделаем вид, что наши данные действительно скрыты.
Но теперь я знаю, что объекты — это в первую очередь данные, а потом, возможно, методы, которые их обрабатывают. Я понял, как писать программы, к чему стремиться при проектировании.
Не успел я насладиться просветлением, как увидел в Интернете слово без гражданства (готов поклясться, оно было окружено сиянием, а над буквами т и л висел ореол).
Краткое изучение литературы открыло чудесный мир прозрачного потока управления и простой многопоточности без необходимости отслеживать согласованность объектов.
Конечно, мне сразу захотелось прикоснуться к этому чудесному миру.
Однако это означало полный отказ от любых правил – теперь не было ясно, должна ли собака гулять сама, или для этого нужен специальный Управляющий прогулками; нужен ли вам счет, или банк возьмет на себя всю работу, и если да, то должен ли он списывать деньги статически или динамически и т. д. Количество вариантов использования выросло в геометрической прогрессии, и все будущие варианты использования могут привести к необходимости серьезный рефакторинг.
Я до сих пор не знаю, когда объект следует сделать без сохранения состояния, когда он должен иметь состояние, а когда он должен быть просто контейнером данных.
Иногда это очевидно, но в большинстве случаев это не так.
Типизация: статическая или динамическая?
Еще одна вещь, которую я не могу решить относительно таких языков, как C# и Java, — являются ли они статически или динамически типизированными.Большинство людей, вероятно, воскликнут: «Что за чушь! Разумеется, статически типизировано! Типы проверяются во время компиляции! Но действительно ли все так просто? Правда ли, что, указав тип X в параметрах метода, программист может быть уверен, что ему всегда будут передаваться объекты типа X? Правильно - не может, потому что.
в метод X можно будет передать параметр типа X или его наследник .
Казалось бы, и что? У потомков класса X по-прежнему будут те же методы, что и у X. Методы есть методы, но логика работы может оказаться совершенно другой.
Самый распространенный случай — когда дочерний класс оказывается оптимизированным под нужды, отличные от X, и наш метод может опираться именно на эту оптимизацию (если такой сценарий кажется вам нереальным, попробуйте написать плагин для какой-нибудь разработанной библиотеки с открытым исходным кодом — либо вы потратите несколько недель на анализ архитектуры и алгоритмов библиотеки, либо просто будете вызывать методы с подходящей сигнатурой наугад).
В результате программа работает, но скорость работы падает на порядок.
Хотя с точки зрения компилятора всё правильно.
Показательно, что Scala, которую называют преемником Java, во многих местах по умолчанию позволяет передавать только аргументы указанного типа, хотя это поведение можно изменить.
Другая проблема — значение null, которое можно передать вместо практически любого объекта в Java и вместо любого объекта Nullable в C#.
null принадлежит сразу всем типам и в то же время не принадлежит ни одному.
null не имеет ни полей, ни методов, поэтому любой его вызов (кроме проверки на null) приводит к ошибке.
Кажется, все к этому привыкли, но для сравнения Haskell (и та же Scala) вынуждена использовать специальные типы (Maybe в Haskell, Option в Scala) для обертывания функций, которые в других языках могли бы возвращать null. В результате о Haskell часто говорят «на нем сложно скомпилировать программу, но если получится, то, скорее всего, она будет работать корректно».
С другой стороны, основные языки явно не являются динамически типизированными и поэтому не обладают такими свойствами, как простые интерфейсы и гибкие процедуры.
В результате написание в стиле Python или Lisp также становится невозможным.
Какая разница, как называется эта типизация, если все правила и так известны? Разница в том, с какой стороны вы подходите к проектированию архитектуры.
Существует давняя дискуссия о том, как построить систему: сделать много типов и мало функций или мало типов и много функций? Первый подход активно используется в Haskell, второй — в Lisp. Современные объектно-ориентированные языки используют нечто среднее.
Я не хочу сказать, что это плохо — наверное, в этом есть свои преимущества (ведь не следует забывать, что Java и C# — многоязычные платформы), но каждый раз, когда я начинаю новый проект, я задаюсь вопросом, с чего начать проектирование - с типами или с функциональностью.
И далее.
Я не знаю, как смоделировать проблему.
Считается, что ООП позволяет отображать в программе объекты реального мира.
Однако на самом деле у меня есть собака (с двумя ушами, четырьмя лапами и ошейником) и счет в банке (с менеджером, клерками и обеденным перерывом), а в программе - WalkManager, AccountFactory. ну вы получаете идея.
И дело не в том, что в программе есть вспомогательные классы, не отражающие объекты реального мира.
Это факт изменения потока управления .
WalkingManager лишает меня радости выгула собаки, и я получаю деньги с бездушного банковского счета (эй, а где та милая девушка, у которой я поменял деньги на прошлой неделе?).
Возможно, я сноб, но я был гораздо счастливее, когда данные в компьютере были просто данными, даже если они описывали мою собаку или банковский счет. Я мог делать с данными все, что было удобно, не обращая внимания на реальный мир.
Я также не знаю, как правильно разложить функциональность.
В Python или C++, если мне нужна была небольшая функция для преобразования строки в число, я просто писал ее в конце файла.
В Java или C# я вынужден поместить его в отдельный класс StringUtils. В языках до OO я мог бы объявить специальную обертку, возвращающую два значения из функции (выведенную сумму и баланс счета).
В ООП-языках мне придется создать полноценный класс Transaction Result. И для нового участника проекта (или даже для меня самого через неделю) этот класс будет выглядеть столь же важным и фундаментальным в архитектуре системы.
150 файлов, и все одинаково важные и фундаментальные — о да, прозрачная архитектура, чудесные уровни абстракции.
Я не умею писать эффективные программы.
Эффективные программы используют мало памяти — иначе сборщик мусора будет постоянно замедлять выполнение.
Но для выполнения простейшей операции в объектно-ориентированных языках приходится создать десяток объектов.
Чтобы сделать один HTTP-запрос, мне нужно создать объект типа URL, затем объект типа HttpConnection, затем объект типа Request. ну, вы поняли.
В процедурном программировании я бы просто вызывал несколько процедур, передавая им структуру, созданную в стеке.
Скорее всего, в памяти будет создан только один объект — для хранения результата.
В ООП мне постоянно приходится захламлять память.
Возможно, ООП — действительно красивая и элегантная парадигма.
Возможно, я просто недостаточно умен, чтобы это понять.
Вероятно, найдется кто-то, кто сможет создать действительно хорошую программу на объектно-ориентированном языке.
Что ж, я могу им только позавидовать.
Теги: #ООП #Функциональное программирование #модульное программирование #структурное программирование #программирование #ООП
-
Агрегатор Вакансий Hotwork
19 Oct, 24 -
Плагин Поиска Mozilla — Сервис Whois
19 Oct, 24 -
Факторный Анализ Для Чайников
19 Oct, 24