Допустим, у нас есть сервис, который должен рассылать сообщения.
public Notificator(IEmailSender sender)
Мы внедряем зависимость через интерфейс
Теперь у нас связаны три сушности
EmailSender
IEmailSender
Notificator
Отныне и навеки, так сказать.
Если же, сделать так:
type Notificator (sender: string -> string -> string -> bool) =
if sender("guest@local", "hello", "who are you?") then
Получается, что функции высшего порядка делают код чище и проще, и интерфейсы тут как бы не особо уже нужны.
Достаточно набора функций, при наличии частичного применения и различные декораторы также становятся не нужны
Не вижу в чём тут чистота. По сути ты сначала дал именование этому интерфейсу, а потом убрал именование. По-мне стало только хуже. Это какое-то движение в сторону JS получается, а не чистота.
Здравствуйте, varenikAA, Вы писали:
AA>Получается, что функции высшего порядка делают код чище и проще, и интерфейсы тут как бы не особо уже нужны. AA>Достаточно набора функций, при наличии частичного применения и различные декораторы также становятся не нужны AA>
Здравствуйте, varenikAA, Вы писали:
AA>Получается, что функции высшего порядка делают код чище и проще, и интерфейсы тут как бы не особо уже нужны. AA>Достаточно набора функций, при наличии частичного применения и различные декораторы также становятся не нужны
У обоих вариантов есть плюсы и минусы. Сервис, настраиваемый функциями высшего порядка, не имеет зависимостей, не требует лишних зависимостей (если в интерфейсе n функций, а нужна только одна), его можно скопировать в другой проект и он скомпилируется и запустится без изменений кода. С другой стороны, ты заменил
IEmailSender.sender(email: String, title: String, body: String): bool
на
sender: string -> string -> string -> bool.
Я думаю, очевидно, что второй вариант без документации совершенно непонятен. Если добавить типы, как в "настоящем фп", то будет лучше, но все равно плохо:
sender: Email -> MailTitle -> MailBody -> bool
Далее. Есть Notificator, принимающий функцию высшего порядка и есть EmailSender, который тоже функция высшего порядка. Если их писали не одновременно, их сигнатуры не будут совпадать, придется добавлять "клей", преобразующий одну функцию в другую. Это сложнее, чем подправить реализацию.
Вывод: ФП подход лучше обеспечивает инкапсуляцию, но она требует значительно больше усилий и строчек кода, ухудшает читабельность, усложняет доработки — проще изменить сигнатуру вызова IEmailSender.send в нотификаторе, чем "клей", преобразующий функции в коде сборки программы из независимых, изолированных функций.
Здравствуйте, vsb, Вы писали:
vsb>Не вижу в чём тут чистота. По сути ты сначала дал именование этому интерфейсу, а потом убрал именование. По-мне стало только хуже. Это какое-то движение в сторону JS получается, а не чистота.
Имя переменной наделено смыслом и связано с типом. Только в случае интерфейса описание типа хранится отдельно от места использования.
Здравствуйте, varenikAA, Вы писали:
vsb>>Не вижу в чём тут чистота. По сути ты сначала дал именование этому интерфейсу, а потом убрал именование. По-мне стало только хуже. Это какое-то движение в сторону JS получается, а не чистота.
AA>Имя переменной наделено смыслом и связано с типом.
Ну вот смотри. Хочу я передать свой Sender. Во втором случае я вижу в сигнатуре только типы параметров. Мне надо читать код Notificator-а, чтобы понять, что ты передаёшь в эти параметры. Ну или читать какую-то документацию, где ты русским или английским языком будешь пытаться описать эти параметры. В первом случае у меня есть чёткий интерфейс, где ясно видно, какие названия у каких параметров этого колл-бека.
AA>Только в случае интерфейса описание типа хранится отдельно от места использования.
Частично это можно было бы исправить, если твой язык позволяет указывать имена параметров в сигнатуре. Т.е. что-то вроде
type Notificator (sender: email: string -> subject: string -> body: string -> sent successfully: bool) = ...
Хотя в этом случае получается какая-то программа в одну строчку, где слишком много информации сконцентрировано. Всё равно первый вариант лучше. А то, что он хранится отдельно от места использования — ну храни рядом с местом использования, не вижу проблемы.
vsb>>Не вижу в чём тут чистота. По сути ты сначала дал именование этому интерфейсу, а потом убрал именование. По-мне стало только хуже. Это какое-то движение в сторону JS получается, а не чистота.
AA>Имя переменной наделено смыслом и связано с типом. Только в случае интерфейса описание типа хранится отдельно от места использования.
Зато в случае интерфейса функции логически сгруппированы по их назначению, за счет этого получается меньше бардака.
И еще — пусть, например, есть несколько разных несовместимых реализаций одного интерфейса. Конкретный пример — какой-нибудь интерфейс IArchiver для действий "архивация файлов", в этом интерфейсе методы CreateArchive и AddFileToArchive, реализации — ZipArchiver и RarArchiver.
Если это же делать тупо функциями — есть риск накосячить: одну функцию в процессе использования взять соответствующую одной реализации, а другую, взаимоувязанную с ней — соответсвующую другой реализации.
В общем, это была бы равноценнная замена только для для интерфейсов, состоящих из одной функции. Для более сложных интерфейсов — нет (можно, конечно, нагородить структуры из функций, но это получились бы те же интерфейсы, только вид сбоку)
Здравствуйте, varenikAA, Вы писали:
AA>Получается, что функции высшего порядка делают код чище и проще, и интерфейсы тут как бы не особо уже нужны.
Я вот тоже последнее время задаюсь вопросом, что если функциональное программирование лучше (в частности, для написания распределенных и многопоточных задач), то как в нём реализовать SOLID, GRASP, ООП-паттерны и прочие достижения ООП. Или же есть какая-то замена этим подходам, если да, то какая? В плане написания полноценного бизнес-приложения, как это делается например на Java/Spring. Пока что те книги, что я читал по ФП, полной картины не дают, и в своей работе ФП я пока применяю эпизодически, внутри классических ООП-обёрток и архитектуры, построенной по принципам SOLID и Ко. Возможно, в мире ФП пока эти принципы не выкристаллизовались ещё, или я просто не знаю где искать?
Здравствуйте, klopodav, Вы писали:
K>В общем, это была бы равноценнная замена только для для интерфейсов, состоящих из одной функции.
Совершенно верно, IEmailSender в Asp.Net Core именно такой интерфейс.
Проблема случается когда такой интерфейс лежит в неподходящей библиотеке. В данном случае в 3.1 aspnet.core
соотвественно о совместимости реализации можно забыть.
Для более сложных случаев в ЯП типа F# можно создать тип/алис:
type SmtpOptions = {
host : string
port : int
}
type IEmailSender : options : SmtpOptions -> mail : MailMessage -> Result
Здравствуйте, gyraboo, Вы писали:
G>Я вот тоже последнее время задаюсь вопросом, что если функциональное программирование лучше (в частности, для написания распределенных и многопоточных задач), то как в нём реализовать SOLID, GRASP, ООП-паттерны и прочие достижения ООП.
Практически все ООП паттерны и прочие "достижения" нужны только для того, чтобы решать проблемы привнесенные самим ООП. Из-за отсутствующей функциональной композиции начинаются пляски с декораторами, стратегиями и прочим.
Здравствуйте, mrTwister, Вы писали:
G>>Я вот тоже последнее время задаюсь вопросом, что если функциональное программирование лучше (в частности, для написания распределенных и многопоточных задач), то как в нём реализовать SOLID, GRASP, ООП-паттерны и прочие достижения ООП.
T>Практически все ООП паттерны и прочие "достижения" нужны только для того, чтобы решать проблемы привнесенные самим ООП. Из-за отсутствующей функциональной композиции начинаются пляски с декораторами, стратегиями и прочим.
А есть примеры исходников проектов, написанных полностью в функциональном стиле, решающие типичные задачи enterprise? Т.е. наличие некоей предметной области со сложными отношениями между сущностями, слой бизнес-операций, слой персистенции, сквозной функционал логирования, безопасности, транзакционности и т.д. Задачи типа многопоточности и распределённости не беру, т.к. тут всё понятно, ФП в этом изначально сильна.
И ещё вопрос — насколько полностью ФП ПО поддерживаемо? Так же просто ли его читать, как "чистый" ООП-шный код? Просто ли его отлаживать или там всё залямбдено по самую крышу, что дебаггер не позволяет по-человечески дебажить? Есть ли сформированные стереотипы и шаблонные решения, когда ты видя большой кусок кода, понимаешь что это за шаблон и не вникаешь во все детали реализации?
Здравствуйте, klopodav, Вы писали:
K>И еще — пусть, например, есть несколько разных несовместимых реализаций одного интерфейса. Конкретный пример — какой-нибудь интерфейс IArchiver для действий "архивация файлов", в этом интерфейсе методы CreateArchive и AddFileToArchive, реализации — ZipArchiver и RarArchiver. K>Если это же делать тупо функциями — есть риск накосячить: одну функцию в процессе использования взять соответствующую одной реализации, а другую, взаимоувязанную с ней — соответсвующую другой реализации.
Пример хороший, валидный. Без использования интерфейсов и их суррогатов в чисто-функциональном стиле, можно было бы записать так, чтобы generic функция CreateArchive (параметризованая типом архива) возвращала generic функцию AddFileToArchive, параметризованную тем же типом архива. И это было бы логично, перед тем, как добавлять файлы в архив, этот архив сначала надо создать.
Но нюанс в том, что сущностей, которые бы хорошо описывались интерфейсами со многими методами исчезающе мало. Хорошо, это значит, без нарушения interface segregation principle.
Здравствуйте, gyraboo, Вы писали:
G>А есть примеры исходников проектов, написанных полностью в функциональном стиле, решающие типичные задачи enterprise? Т.е. наличие некоей предметной области со сложными отношениями между сущностями, слой бизнес-операций, слой персистенции, сквозной функционал логирования, безопасности, транзакционности и т.д. Задачи типа многопоточности и распределённости не беру, т.к. тут всё понятно, ФП в этом изначально сильна.
Под рукой ссылок на конкретные исходники нет, но начать можно отсюда: How to design and code a complete program Ну и книга хороша: Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#
G>И ещё вопрос — насколько полностью ФП ПО поддерживаемо? Так же просто ли его читать, как "чистый" ООП-шный код? Просто ли его отлаживать или там всё залямбдено по самую крышу, что дебаггер не позволяет по-человечески дебажить? Есть ли сформированные стереотипы и шаблонные решения, когда ты видя большой кусок кода, понимаешь что это за шаблон и не вникаешь во все детали реализации?
На мой взгляд, если исключить неадекватов, лепящих монады и теории категорий во все дыры, то значительно проще.
1) ООП вынуждает привязывать методы к данным, хотя чаще всего в домене эта связь отсутствует. В результате, эта связь делается случайным образом, что приводит к каше в коде. Например, можно написать код в стиле ООП: "Холст.НарисуйКартину(кисть)", либо "Кисть.НарисуйКартину(холст)". Какой из этих двух вариантов правильный? Тут нет неправильного варианта. Реализован будет произвольный из них, смотря какая пятка зачесалась у программиста. Хотя в ФП стиле все однозначно: "НарисуйКартину(холст, кисть)". Нет никаких разночтений.
2) Данные жёстко связаны с методами. Какой бы из описанных в предыдущем пункте мы не выбрали вариант, мы будем жёстко связаны принятым случайным образом решением. Если мы выбираем вариант "Холст.НарисуйКартину(кисть)", то НарисуйКартину жёстко привязана к холсту и мы сможем в дальнейшем рисовать картины исключительно только на холстах. В другом варианте мы будем жёстко привязаны к кисти и сможем рисовать только кистью. С другой стороны, при ФП декомпозиции, функция НарисуйКартину(где, чем) ни к чему не привязана и может рисовать на чём угодно и чем угодно, если это пригодно для рисования.
3) ООП провоцирует на создание большого количества лишних сущностей. Для борьбы с предыдущими проблемами приходится вводить большое количество искусственных сущностей, которые отсутствуют в исходном домене. Для этих сущностей часто даже невозможно придумать адекватного названия, вот и появляются всякие FooManager, BarHelper, все это обмазывается толстым слоем абстрактных фабрик, визиторов, репозиториев и прочих паттернов, которые для эксперта из доменной области звучат, как тарабарщина и мешают тем самым применению DDD. В результате программист вместо решения задачи из предметной области воюет с визиторами и декораторами.
В результате, ФП проект со временем менее подвержен превращением в кашу. В фп стиле легче писать правильно и труднее говнокодить.
AA>Мы внедряем зависимость через интерфейс AA>Теперь у нас связаны три сушности
EmailSender вообще никак не связан с notifier’ом.
А Notifier можно использовать и без этого конкретного EmailSender’а.
Здравствуйте, Muxa, Вы писали:
M>А Notifier можно использовать и без этого конкретного EmailSender’а.
На самом деле с интерфейсами проблем много больше,
Одна из них в том, что реализация тянет за собой сборку с интерфейсом. "Хороший пример" asp.net core 3.1. интерфейс лежит в Microsoft.AspNetCore.Identity.UI.dll
Т.е. реализация будет зависит не только от dotnet core 3.1. но и еще от всего asp.net
если использовать функцию, мы не зависим ни от одной сборки.
А это уже правила хорошей разработки по Роберту Мартину.
AA>Одна из них в том, что реализация тянет за собой сборку с интерфейсом.
Определяй интерфейс в той же сборке, и не потянет.
AA>если использовать функцию, мы не зависим ни от одной сборки.
Не факт. Если заменить myTypeInstance.DoThings() на doThings(myTypeInstance), тогда то на то и выходит.
Вызов метода экземпляра класса — это тот же вызов функции, только экземпляр передаётся в виде неявного первого параметра.
А статические методы и вовсе ничем от функций не отличаются.
Здравствуйте, yenik, Вы писали:
AA>>Достаточно набора функций,
Y>А если набор функций — это 10 функций? Не легче ли инжектировать 1 интерфейс?
Интерфейс нужно реализовать, что бы его инжектировать. А 10 функций можно взять готовых или скомбинировать их неким образом.
Более того, 10 функций можно заменить структурой с 10ю полями и в отношении количества инжектируемых функций такая структура будет ничем не хуже 1-го интерфейса. А в некотором отношении даже лучше, т.к. структуры не нужно реализовывать, достаточно лишь инициировать члены.