Как стать автором
Обновить

Антипаттерны проектирования: Functional Decomposition

Время на прочтение5 мин
Количество просмотров46K
Всего голосов 22: ↑12 и ↓10+2
Комментарии106

Комментарии 106

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

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

Мне показалось, что вы пишете именно об этом, или я заблуждаюсь.
Именно про это! Но (как обычно, «но»)… как раз про современные реалии.

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

С учетом того, что «работает на одном компьютере» или «работает на одном ядре» — современные аналоги «работает на моем компьютере» или «640 Кб памяти хватит всем», подобная обработка данных становится мейнстримом, а не исключением. Причем даже не только сейчас, а уже давненько, например, когда появились import static в джаве — уже тогда стало понятно, что не все должно быть в классе или объекте.

Я изначально то и имел в виду под тем, что это не антипаттерн — за последние 17 лет, прошедших с выхода оригинальной книги, многое в мире поменялось. Сейчас как раз ее перечитываю на перекурах, и думаю, что если ее воспринимать через призму реальности и опыта — книга не потеряла своей актуальности, но если ей следовать дословно (боже, вспоминаю CORBA и IDL, и дрожь берет) — она будет поопасней, чем «майн кампф» для неокрепших умов.
Не знаю, о какой книге идет речь и о каких толстых объектах, которые так мешают распараллеливать работу программы, но скажу, что и на С можно писать объектно по духу, и на Java можно писать практически функционально. Более того, в ООП функциональная декомпозиция никуда не уходит — просто к ней добавляются ирерахии классов. Иерархия совершенно не заменяет собой функциональную декомпозицию, но лишь дополняет ее — кроме функциональной (вызов функций все более низкого уровня) появляется и классовая декомпозиция. Кстати, в ранних работах по ООП структуры классов были сильно переоценены. Теперь обычно предлагается создавать структуры не потому что «теперь такая линия Партии!», а только тогда, когда это явно улучшает реализацию задачи. Да и сами иерархии теперь меньше, но зато их больше. Кроме классов есть много других средств структурировть код — те же шаблоны, например.
Почему вы настаиваете на идемпотентности функций, на которые вы якобы разбиваете задачу? Идемпотентность не имеет прямого отношения к параллелизму. Быть может, вы смешиваете понятия «чистой» и «идемпотентной» операции?
В современных реалиях

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

Я думаю, вернее было бы сказать "чистых функций".
Подходит ли под этот антипаттерн ситуация, когда для рефакторинга длинного метода (300 строк и больше) для него создаётся отдельный класс, а локальные переменные превращаются в поля этого класса? Получается класс с единственным public методом (возможно даже, статическим), за пределами которого состояние экземпляра (если он вообще доступен снаружи) не имеет смысла. Все основные критерии антипаттерна выполняются.
Да, пожайлуй подходит. Но паттерны, как и антипаттерны, не являются чем-то краеугольным и догматическим. Вполне себе могут уживатся некоторые приемы, которые могут причислить как к паттернам, так и к антипаттернам.
Я, к примеру, такие проблеммы, стараюсь решить монадами. Но они сами по себе тяжело паттернизируются в ООП.
Кто-то из отцов писал, что если функция больше 80 строк и вы не видите путей разбить её на несколько подфункций, то вы что-то напортачили с реализацией алгоритма.
В книге «Clear Code» (Чистый код) об этом пишется
Первое правило: функция должна быть компактной. Второе правило: функция должна быть еще компактнее.

Не уверен правда, можно ли автора отнести к «отцам».
Может Method Object годится, как кандидат к рефакторингу? Оборачиваешь то, что пока не можешь, ставишь todo/fixme и забываешь до лучших времен?
Если метод и бывшие «локальные переменные» статические, то формально да, хотя и тут может иметь место просто удобство от вынесения части кода в отдельный файл. Если же получается нормальный класс с конструктором, полями экземпляра и методом, отдельно оформленным в интерфейс (реализацию можно подменить при тестировании), то это просто хороший, годный рефакторинг на основе принципа Single Resposibility.
Все атрибуты класса являются приватными и используются только внутри класса.

Странно что автор отрицает 109 и 110 советы Голуба из «Веревки достаточной длины[...]»:

#109
Все данные в определении класса должны быть закрытым
#110
Никогда не допускайте открытого доступа к закрытым данным

Наличие открытых атрибутов как раз приводит к сложностям при повторном использовании и тестировании.
Под атрибутами я полагаю имелись ввиду свойства, реализуемые через геттеры и сеттеры.
Так Голуб также говорит о свойствах, и настоятельно не рекомендует устраивать из состояния экземпляра проходной двор, что как раз и делается путем выставления части данных в паблик, либо написании чистых геттеров/сеттеров (Под «чистыми» я понимаю: геттер, состоящий только из return и сеттер, состоящий только из присваивания).

Дабы не копировать сюда саму книгу и не пересказывать три странички текста, рекомендую найти эти пункты в первоисточнике — Ален Голуб. Веревка достаточной длины, чтобы выстрелить себе в ногу. Правила программирования на С и C++.
Я верно понимаю, что осознанное использование этого антипаттерна снимает с него клеймо антипаттерна?

Если я заведомо создаю класс-действие, у которого все приватно, и который имеет лишь одно единственное предназначение, но это наиболее удобный и легко поддерживаемый вариант, это ведь не повод кидаться в меня тапками?
В этих статьях-переводах отсутствует важная часть из первых пары глав — если есть глава на плечах, и отлично понимаешь, что делаешь, это не антипаттерн.
Да, наверно, «теоретики» и «практики» — это множества, которые далеко не всегда пересекаются…
Приписывание метода объекту класса — то, на чем стоит ООП, — есть логическая ошибка. Метод не может принадлежать никому, кроме самой природы. Все, что происходит вокруг нас, — это результат работы неких сил, понять которые мы не в состоянии. Мы можем лишь предполагать результат работы этих сил, но управлять ими не может никто и ничто. Исходя из этого стоит отделять функции от объектов. ИМХО.
Да здравствуют лямбды!
Я не программист, я аналитик, который сталкивается с проблемами ООП проектирования. Вопрос: я второй раз слышу про «лябдды», а что это такое, если вкратце?
Вкратце читайте в вики. И нет, это не ООП.
Спасибо!
Лямбды это «модное» название, доброе старое а-ля Smaltalk название — Замыкание, или Closure. В С/С++ суть всяческие функции обратного вызова, колбэки, передаваемые например, в системное API для перечисления файлов в каталоге, шрифтов и т.п.

На Smaltalk замыкание — нативная первоклассная конструкция языка, да так, что ветвления и циклы реализованы через них. Например, вот как 5 раз пропищать системным звуком c паузой в 1 секунду: 5 timesRepeat: [Sound bell. Time waitForSeconds: 1]. В этом примере у экземпляра SmallInteger вызывается метод timesRepeat, куда в качестве параметра передаётся экзкмпляр BlockClosure с кусочком кода в скобках []. И магия свершилась, смотрите на читабельность, недаром Smalltalk имеет такое название. И никаких костылей навроде служебной переменной цикла i, с её инициализацией, инкрементом и сравнением с константой :)
То, что вы не способны понять парадигму, еще не означает, что она ложна.
Проблема — куда «засунуть метод» — по крайней мере существовала, и такой ныне признанный антипатерн как «Active Record» есть пример неумелой попытки её решить.

Специально для «приписывания методов» придумана разновидность классов — «менеджеры». Обычно существуют в единственном, или около того, экземпляре, не имеют состояния и реализуют специальные интерфейсы, в которых прописаны все нужные методы. Наиболее известный пример — Entity Manager'ы в ORM.
Не такие ли «менеджеры» описывались в этой статье: habrahabr.ru/post/217847/?
Всё во многом зависит от языка программирования. Некоторые языки, тип Java, не допускают наличия процедур/функций/методов вне класса, так что тут вопрос в том, засовывать метод в потенциальный «полтергейст» или в базовый класс. Active Record, опять-таки, как решение «засунуть метод в базовый класс» является признанным антипаттерном в основном там, где эта проблема актуальна; там же, где это не так — например, в Javascript, — можно спокойно добавить метод в прототип в рантайме (правда, за эту излишнюю гибкость его и не любят).

Тут ещё зависит от того, что архитектор наархитектурил.
CalculateInterest — антипаттерн и плохо. А вот если вы тот же класс назовёте CalculateInterestStrategy (и можно единственный публичный метод скопировать в интерфейс), это уже будет паттерн и хорошо :-)
Да, такая мысль тоже пришла во время перевода.
Причиной холивара часто является недооценка основных качеств процедурного программирования и ООП. Не всякий «код с классами», это обязательно ООП.

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

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

Поскольку время эксплуатации и развития софта обычно в десятки раз превышает время его начальной разработки, то использование ООП, в целом дает ощутимую выгоду. Собственно, за это и не любят «процедурщину» в ООП архитектуре — она несет за собой весь ворох характерных для процедурного подхода проблем, помноженный на архитектурные заморочки (так называемый «равиоли»-код), — что приводит в итоге к сильному росту затрат на поддержку.
Да. Придерживаюсь максимы (хотя и не всегда соблюдаю), что все публичные методы желательно скопировать в интерфейсы;-) У интерфейса хотя бы можно подменить реализацию для тестирования. Кроме того, когда код организован в несколько методов, к тому же с приватной видимостью, это проще править и проще, в вероятном будущем, по мере вероятного «расхардкоживания», понемногу переносить в настройки.
Сложилось такое ощущение, что некоторые рекомендации в корне противоречат многим лучшим практикам использования ООП, но это тот ещё холивар. Поэтому не об этом.

Мой личный опыт около 7 лет — это постепенная трансформация более десятка клонированных проектов. написанный как бы на C, но фактически имеющих под собой прототип ASM Z80 кода, «переведённого» в лоб. Полный стек над операционной системой, включая тонны бесполезных и устаревших комментариев, занимал порядка 700 тыс. строк кода для каждого из них, если мне не изменяет память. К общей проблеме ещё добавился неудержимый копипаст предшественников, который на практики не давал возможности сформировать хоть какие-то библиотеки повторно используемого кода, не говоря уже об условной компиляции под разные модели.

Подход был примерно следующий:
  • Включение максимально количество проверок предупреждений и ошибок компиляции
  • Включение превращения предупреждений компиляции в ошибки
  • Отключение предыдущего пункта для предупреждений, которые сложно исправить быстро, но чтобы мозолило глаза всем
  • То же самое, но для линковки
  • Полный переход на компиляцию как C++, не меняя расширения файлов чтобы не создавать проблем команде из около 15 человек, активно работающих над проектами и сливающих изменения из одного в другой
    • Даёт возможность вызывать С++ без extern «C» обёрток
    • Значительно более серьёзный контроль типов, позволяющий выявить скрытые логические ошибки
    • Жёсткий контроль прототипов функций при линковке — тоже можно много нового открыть о проекте
  • Использование регулярных выражений для «исправления» кода чтобы избежать многочасовой обезьяньей работы
  • Поэтапное выявления схожих частей, унификация с переписыванием на C++ (в большинстве случаев с нуля без отсылок к оригинальному коду) и вынесение в общую библиотеку в течение всего времени. Т.е. от одной стабильной ветки до другой было достаточно мало изменений чтобы можно было покрыть в QA за разумное время
  • Всё делается поэтапно в режиме Continuous Integration — иначе гарантировано будут проблемы со сливанием, которые не будут замечены в тысячах файлов изменений
  • Общая зачистка кода от локальных переменных модулей, раскопированных объявлений типов и функций, покрытие автоматическими тестами и т.п.
  • Полное выпиливание условной компиляции с заменой на модули/плагины и/или мета-программирование на шаблонах
  • Анализ и оптимизация по скорости, размеру и потреблению питания (проекты для встроенных систем — POS)
  • Чтобы научить команду думать в «правильном» ООП мышлении, научить специфичным «трюкам» и использовать хороший стиль (но не только поэтому), все изменения проходили проверку прямо по рабочему месту перед коммитами. Ветки создавались только для заданий, требующих более двух дней разработки или по желанию разработчика (объём изменений, сложная отладка, и т.п.)
  • Переход на CMake. Собственная, оптимизированная по скорости работы, сборка инструментов для разработки. Общее сокращение время полной сборки с 35-40 до менее 3 минут. Реализация правильной thread model библиотеки для встроенной операционки
  • Явно много ещё чего-то, о чём так быстро не вспомню
Я правильно понимаю, что компания, которая «проще уволить» процедурного программиста теперь будет вынуждена искать себе клиентов, которые ставят задачи, хорошо ложащиеся на ООП? И разумеется, такая компания должна будет получать только деньги, хорошо ложащиеся на ООП, а процедурные деньги отвергать, как антипаттерновые.
В статье вроде не было слов «проще уволить» — была фраза «проще нанять». Мне кажется, что если, например С-программисты, успешно выполняли до этого свои функции в компании, то соответствующее направление в компании никуда не денется и они будут востребованы и дальше (как и в целом в отрасли).
А еще я теперь стараюсь не придерживаться дословно оригинальной статьи и частично «выпиливаю» вкрапления о менеджменте.
Что такое «задачи, хорошо ложащиеся на ООП» и «задачи, плохо ложащиеся на ООП»? Чет я себе такого представить не могу…
Вы себе не можете представить первое или второе?
В смысле? Если я пойму первое, то станет очевидно и второе. И наоборот.
Если верить автору, то это оба этих множества задач дополняют друг друга до множества всех задач…
Ну вот вам второе: задача сложения двух чисел плохо ложится на ООП.
Хм… Вы заставляете меня сомневаться в ваших компетенции. Что такое ООП в вашем понимании? Как в вашем понимании соотносится ООП и система элементарных типов языка?
Где вы увидели элементарные типы? Числа могут иметь очень нетривиальную структуру. В этой задаче проблема в том, что может потребоваться run-time полиморфизм от типа более, чем одного параметра (обычными виртуальными функциями не обойтись).
Что такое ООП в вашем понимании?

Object-oriented programming [...] is a programming paradigm based on the concept of «objects», which are data structures that contain data, [...] often known as attributes; and code, [...[ often known as methods.

Как в вашем понимании соотносится ООП и система элементарных типов языка?

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

Судя по вашей активной поддержке плюсами-минусами на банальной теме, вы обычный хабро-тролль. Извините, не сразу понял…
Под задачей, плохо ложащейся на ООП, может подразумеваться задача, для решения которой нецелесообразно применение ООП. Сложение двух чисел (пусть целых беззнаковых: чем они проще, тем лучше пример) — задача, плохо ложащаяся на ООП.

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

ООП можно применить почти для любой задачи. Но остается вопрос целесообразности и эффективности.
Давайте еще обсудим задачу помещения 32-битного значения в регистр процессора…
Расскажите мне про архитектуру приложения, решающую эту задачу, про функциональную декомпозицию, и паттерны проектирования, которые вы собираетесь применить при решении этой задачи.
Детский сад какой-то… Статью-то читали?
Когда вам нужно перемножить две матрицы, элементы которых являются многочленами с коэффициентами абстрактного класса Number, то без системы классов вам точно не обойтись. Но ООП, как она реализована обычно, может не хватить. Например, когда придётся сложить два числа, одно из которых — комплексное над длинными целыми числами, а другое — алгебраическое, заданное многочленом и приближённым значением корня.
Лет 20 назад мне приходилось реализовывать подобные вещи на С++. В конечном итоге «структуры, содержащие данные и методы» оказались где-то в глубине реализации, а всё взаимодействие (включая выбор метода сложения двух чисел) пришлось описывать на более глобальном уровне, там, где все типы (строящиеся динамически) известны.
«Получить число» и «вывести результат» — довольно абстрактные, туманные и не относящиеся к делу задачи. Их проще вынести в отдельный модуль и прописать процедуру работы с ним.
Ну для начала, вы сам пишете, что ООП для этой задачи не нужен. В принципе, этого уже достаточно для ответа на вопрос «какие задачи плохо ложатся на ООП». Но на самом деле, это просто тривиальный пример, из которого легко выводится соответствующий класс прикладных задач.

Вот пример из реальной жизни: есть хранилище данных от учетной системы, в нем есть информация о различных объектах учета, для каждого из них известны классифицирующие признаки (например, склад) и факты (например, масса). Нужно передать в другую систему аггрегированные данные (грубо говоря, суммы фактов во всех комбинациях всех классификаторов). Эту задачу, конечно, можно решить с помощью ООП, но без ООП (и, прямо скажем, вообще без императивного языка) она решается проще.
Исходя из темы статьи и реплики amarao про «клиентов чьи задачи хорошо ложатся на ООП», я полагал, что речь идёт о классе задач для которых актуальны понятия проектирование, паттерны проектирования и функциональная декомпозиция. И полагал, что меня поймут.
Вы привели какую-то, типа, задачу про элементарный тип чисел. Потом, правда, ваши коллеги «уточнили», что числа на самом деле сложные, а то и вообще матрицы… В общем настоящих программистов видно сразу по постановке задачи…
А дальше оказывается, что речь идет не о «ложится/не ложится». а о «лучше/хуже». В общем, мне нечего вам сказать. Классика хабра… )))
То, что есть классы задач, для которых другие парадигмы лучше и эффективней это самоочевидно. Иначе бы эти парадигмы вообще не появились на свет…
А дальше оказывается, что речь идет не о «ложится/не ложится». а о «лучше/хуже».

«Ложится — не ложится» — это и есть «лучше/хуже». Потому что очевидно, что можно любую задачу впихнуть в любую парадигму, вопрос грубой силы.
На счет вашего примера, просто определите сущности которыми вы оперируете в этой задаче и метод реализации на ООП почти сразу станет очевидным… Что такое, например, у вас «аггрегированные данные»? Какие общие свойства нужные вам у всех этих «аггрегированных данных»?
Общие свойства — это прекрасно, но для ООП нужны еще и операции. А вот с операциями там все невесело, поскольку там все операции (особенно если мы хотим получить хорошую производительность) порождают новые сущности, что, в свою очередь, приводит нас к неизменному состоянию, а это уже не ООП.

На конкретике: вот у нас есть два типа учитываемых объектов: мешки с сахаром и банки с огурцами. У каждого объекта есть склад хранения и поставщик. Нам нужно дать стандартную аналитику в двух разрезах (склад/поставщик), с аггрегацией вверх, для каждого показателя (т.е. сахар суммировать и считать среднее по весу, огурцы — по объему).
Вы шикарно ставите задачи… Что такое мешки с сахаром? Это записи в базе или объекты в памяти.
Если объекты в памяти, то должен быть очевидно список. Соответственно должен быть итератор. На итераторе реализуете, например, стандартные map/reduce и делаете выборки и аггрегации. Почитайте еще раз внимательно с карандашом и бумажкой паттерны проектирования банды четырех. Там всё разжёвано…

Хотя задача которую вы описываете, это обычно работа базы данных…
Я ставлю задачи так, как их ставит заказчик.

Ну и да, то, что вы пишете, только подтверждает мой тезис. map-reduce — это функциональный паттерн, а не объектно-ориентированный (привет гофу, где его нет). И вы совершенно правы в том, что это работа БД, а еще точнее — ETL-системы… что тоже говорит нам о том, что ООП тут не понадобилось.
Я так понимаю, что определить эквивалентность паттернов, если у них разные названия, вы не можете? Ну, о-ок… )))
Вообще-то весь смысл паттернов в том, чтобы создать непротиворечивый словарь. И map-reduce вполне описан как паттерн самостоятельно.

Так что нет, возражение не принято. Хотя я с интересом послушаю, какой паттерн из гоф вы считаете строго эквивалентным map-reduce.

(заодно хочу обратить ваше внимание на одну занятную вещь: некоторые паттерны из gof в других парадигмах являются first-class-конструкциями, не требующими ООП)
А когда это неизменное состояние стало «это уже не ООП»?
Тогда, когда у нас появилась куча гибридных парадигм. Это, на самом деле, не жесткий критерий, а скорее code smell.
Не очень понял. Что мешает состоянию быть неизменным независимо от парадигмы?
То, что парадигмы начинают сливаться.

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

Вот у нас есть функция, которая получает пинок в виде аргументов и выдает результат (без побочных эффектов). Это ФП.

А вот у нас есть объект, который получает пинка, и что-то считает, но данные отдает наружу, а не меняет свое состояние. Это ООП или ФП?

Или вот у нас есть объект, который получает пинка в виде аргументов и что-то считает, а данные отдает наружу в виде результатов — это ООП или ФП?

А если ответ на два последних вопроса кажется очевидным, то надо вспомнить, что функцию можно трактовать как объект, а вызов метода на внутренних данных объекта — как замыкание на этих данных. И да, я понимаю, что это натягивание воздушного шарика на глобус, но по степени прозрачности шарика должно быть понятно, насколько нечетки стали границы.
Ну, собственно, пока ждал ответ, я тут накидал код в качестве иллюстрации — gist.github.com/retran/706c3f35ef5d97b7f711

Получилась иллюстрация к твоему комментарию, да.

Я привык считать, что когда есть состояние (мутабельное или немутабельно) и привязанное к нему поведение — это уже ООП. А парадигма реализации поведения и состояния, функциональная или императивная — это ортогональная к ООП вещь.
Ну вот тут и возникает сложный вопрос, который и породил мое рассуждение про ETL, что считать «привязанным поведением», а что — нет. Операция суммы над множеством — привязанное поведение для множества?
Чтобы определить любую математическую структуру (каковой является любое множество) нужно определить и базовые операции над ней, без них определение не имеет смысла.

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

Отсюда, имхо, да, сумма — это привязанное поведение.
А fold (reduce) над множеством?
Можно рассматривать как поведение «множества как first-class объекта». А что смущает?
Я знаю про вариант рассматривать его как поведение функции (как объекта) которая запускается над множеством, но мне такой вариант кажется не совсем корректным.
Я знаю про вариант рассматривать его как поведение функции (как объекта) которая запускается над множеством, но мне такой вариант кажется не совсем корректным.

А в ФП это стандартный паттерн (собственно, map-reduce).

А что смущает?

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

И как теперь определять множество, у которого элемент позволяет и то, и другое? Множественное наследование? А если я хочу добавить операцию над множеством для множества, которое я не контролирую? Миксины?

А еще смущает неоднозначность. Вот есть симметричная операция (то же сложение). По каким формальным признакам определить, к какому из двух операндов она относится?
Получаются атрибуты у типов. И даже у объектов. И далее — полиморфизм по этим атрибутам. Если у меня многочлен над полем, то я могу считать НОД с помощью алгоритма Евклида, а если над кольцом без деления — нужны другие алгоритмы. Придётся выбирать реализацию НОД по атрибутам этих типов. Дальше получаем constrained types, полиморфизм по условиям на аргументы и соотношениям между ними… Имеет ли это хоть какое-нибудь отношение к парадигмам ООП или функционального программирования?
Имеет — ровно в контексте изначального вопроса, какая из парадигм (в данном случае, ООП или ФП) лучше подойдет для решения задачи. Заметим, я не буду делать утверждения, какая именно, я просто показываю, что возможны варианты.
Насчёт ООП и ФП я соглашусь с тем, что они ортогональны: первая — про инкапсуляцию и наследование, вторая — про неизменность состояния. Замечательно могут использоваться как вместе, так и по отдельности.
А я вот считаю, что ООП и ФП различаются еще и по тому, где находится поведение, касающееся конкретных данных.
Если убрать поведение из класса, то ООП в этом месте ослабнет, но ФП не появится: ведь код может продолжать изменять данные так же, как это делал старый код.
Поэтому и еще и, а не только по.
А почему ещё? Надеюсь, выбор кода путём распознавания паттернов на аргументах вы не считаете отличительной особенностью именно ФП?
Ну вот например — разделение операций и данных. Или наличие ФВП и функций как first-class citizens.

Ну, void (*)(void) было ещё в С. И операции в нём хорошо отделены от данных. От этого же он не перестаёт быть императивным.
Но на нем можно писать в функциональном стиле, кстати.
Ну да. И на C# 1.0 можно (ещё до появления темплейтов и замыканий). Функциональный стиль и ООП друг другу не противоречат. Хотя их «чистое» совместное использование может дать не очень эффективный код — лучше знать меру и в том и в другом.
А это, собственно, баг существующей математики. Там, насколько, я знаю, дублируют определения и используют наследование («множество A — это подмножество элементов B для которых определена операция...», был бы рад если бы математики, если они тут есть, меня поправили).

К каждому по отдельности. a + b и b + a — это разные операции, просто так получилось, в силу свойства симметричности, что они эквивалентны.
В реальных задачах не только математика, а проблемы — те же: выбор допустимых операций по набору признаков.
Проблема есть, но решения в рамках существующих парадигм лучше чем конструирование типов из миксинов я не знаю.
Ну, мне вот в случаях, когда поведения становится сильно больше, нравится отрывать его от данных.
Наследование есть. Но формулировка, скорее, такая: множество относится к типу B, если оно относится к типу A, и кроме того, для него и его отдельных элементов выполняются такие-то свойства. Например, поле — это кольцо с единицей и без делителей нуля, каждый элемент в котором имеет обратный.
В каком-то смысле, наследование. Как оно ложится, например, на C#, не совсем понимаю. Наверное, если тип образует поле (например, вещественные или рациональные числа), то ему нужен интерфейс IField, который наследуется от IRing (возможно, даже, без добавления новых методов).
Там макросов нет. При наличии quotations, да.
Я как-то вот так — github.com/corvusalba/my-little-lispy
Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.
Именно ;))
Слова «C or Fortran» — важное уточнение…
Только с поправкой на то, что внутренние данные объекта — тоже прекрасно представляются замыканиями же.
Вот только реализуются замыкания почему-то в виде объектов, внутренними данными которых являются захваченные переменные. Надо ли выворачивать этого ёжика наизнанку дважды?
В данном случае, конечно, нет. Это как раз иллюстрирует неоднозначность трактовки одного и того же внешнего поведения.
Необязательно. Есть еще спагетти-стек (http://en.wikipedia.org/wiki/Parent_pointer_tree).
Ну и то, что наши компьютеры на самом нижнем уровне строго императивные не мешает заниматься полезным теоретизированием на функциональные темы ;))))
… на декларативные тогда уж.
В таком случае объект можно трактовать как общее для нескольких функций замыкание на одних и тех же данных…
Можно, только придется помнить, что мутабельность замыканий — штука очень неоднозначная.
Судя по вашей активной поддержке плюсами-минусами на банальной теме, вы обычный хабро-тролль.

Мне, кстати, интересно, что вы имеете в виду: плюсовать свои комментарии я не могу, ваши комментарии минусую не я. О какой «активной поддержке» вы говорите?
Не верьте этой статье. Я возмущён.

  • «Классы с именами-«функциями»» Такой класс — это паттерн рефакторинга при запахе «длинный метод»
  • «Все атрибуты класса являются приватными» — ШТА?!? Это проблема?..
  • «Классы с единственным действием, аналогичным процедурной функции.» — there's nothing wrong with that. Уважайте single responsibility principle. Пока оно соблюдает low coupling, high cohesion — пусть хоть один метод будет, хоть ни одного, только конструктор.
  • «Вырожденная архитектура, которая полностью не соответствует объектно-ориентированной концепции.» — Пока автор пробемонстировал строго обратное: своё собственоне несоответствие.
  • «Неэффективное использование объектно-ориентированных принципов, таких как наследование и полиморфизм. В результате ПО может быть чрезвычайно дорогим в сопровождении.» — а автор знает, зачем придумали интерфейсы? Оставлю обсуждение спорного что open-closed principle в стороне, но просто загляните в начало GoF, там написано: «Favor object composition over class inheritance».
  • «Невозможность ясно задокументировать (иногда даже описать) как система работает. Модель классов не имеет никакого смыслового значения для понимания архитектуры системы.» — Шта? Это декомпозицию по функционалу сложно задокументировать? Очень любопытно посмотреть, как ИНАЧЕ он это делает.
  • «Сложность (иногда невозможность) последующего повторного использования кода.» — Не буду говорить о проблематике code reuse внутри одного проекта, замечу только, что а) всё так же голословно, б) а как иначе в принципе, кроме как по функциональным обязанностям, можно строить API?
  • «Сложность тестирования ПО.» — и снова высосано из пальца, имнсхо.
  • «Если у класса всего один метод, то попробуйте смоделировать его как часть существующего класса.» [...] «Попробуйте объединить несколько классов в новый класс». Давайте нарушим основу дизайна, single responsibility principle, и до кучи увеличим LoC per class. И эти люди говорят, что так будет легче тестировать… мой бледнолицый брат сравнивал размеры метода setUp в тесте до такого «улучшения» и после?
  • «Если класс не содержит информацию о состоянии, следует переписать его в функцию. „ — читать о stateless классах, хотя бы GoF.

Всё, больше не могу, а то начну ругаться совсем плохими словами.
Это уже не первая статья на тему «не делайте классы, объекты которых создаются лишь на единый вызов метода, делайте вместо них функции». Аргументы, на мой взгляд, каждый раз неубедительные, продолжаю так делать :-).
Паттерны проектирования — лучшее и того, что было придумано для ответа на вопрос «Зачем ты так сделал?»)
К сожалению, еще неплохо бы знать ответ на вопрос «зачем ты применил этот паттерн».
«Программирование», по крайней мере последние лет 25-30, — это не столько про собственно «программы», сколько про «обработку данных». Практически любая «процедура» — это процедура именно обработки данных. Переменные, относящиеся к одному объекту, хорошо бы собрать в один класс. И иногда просится рефакторинг типа «Introduce Parameter Object», чтоб собрать в один класс аргументы процедуры или функции.

Ту организацию кода, которую критикуют в данной статье, лучше преобразовывать не в процедуры, а, наоборот, в что-то более ориентированное на данные и выстроенное вокруг данных…
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории