Как стать автором
Обновить
54
0
Игорь @CodeShaman

Программист

Не только в Python, но и в Clojure, которые теперь активно продвигает дядя Боб. Тот же Javascript. Все это уже используется активно в энтерпрайз разработке. Ну и если человек видит, что что-то имеет явное указание по соглашению, что это нельзя трогать (то же нижнее подчеркивание в начале название), но берет и игнорирует это, то вероятно такой разработчик в любом случае набедокурит в другом месте, если не в этом. Обратится к чему-то приватному, что обозначено, как приватное, можно только осознано. Если ты хотя бы базово знаком с конвенцией языка, на котором пишешь.

К тому же есть другие способы сокрытия. Через замыкания внутри тех же функций.

Тут хотелось бы спросить, а какие именно ограничения Java помогают совершать принципиально меньше ошибок? В случае с Haskell я бы мог понять такое утверждение, ведь не зря говорят "if a Haskell program compiles, it probably works". Его компилятор и система типов действительно делают нечто особенное по сравнению с большинством языков. Но Java?

Миллениалы изобрели процедурное программирование со всеми его недостатками.

Если в программе есть и процедурный и другие подходы - оно от этого так сильно хуже? Мы уже видели в виде классической Java через что приходится проходить и какие финты придумывать, чтобы существовать исключительно в чистом ОО мире. Когда нужно сделать какую-то не сложную вещь. К примеру, Go достаточно процедурный язык, что не делает его менее популярным в наше время. И его же за чем-то создали сейчас именно таким? При чем не глупые люди, которые явно знакомы со всеми парадигмами и подходами. Или набирающий популярность Rust, который тоже не является классическим ООП языком. Такие языки, как Питон - мультипарадигменные. Почему нужно боятся где-то написать процедурный код? Или ФП код? Кажется, что причин для этого нет, кроме предубеждений.

Поэтому разработчикам и остаётся писать конструкции c протоколами.

И в этом нет ничего плохого. Протоколы по сути те же интерфейсы. Прекрасно можно ими пользоваться в Питоне. А разговор начат с классов как раз по тому, что именно они в купе с наследованием и есть то самое классическое ООП, о котором идет речь. Есть ООП языки без классов и это не делает их менее ООП. Эталонного ООП просто не существует в настоящий момент. О чем речь тоже идет в статье. Есть разные вариации реализации этой идеи, но подходы могут серьезно отличаться.

И не надо пытаться сделать вид, что такая реализация ООП — это какое-то достоинство Питона.

Вроде ни кто и не пытался, не знаю, где это подается в таком виде. Такой мысли в статье не было.

Чем это будет удобнее? Объяснения нет.

Удобство заключается в том, что отсутствует сама проблема заранее точно определять кому и какое поведение принадлежит и с каким объектом оно должно ходить пришитым. А это частно далеко не самый тривиальный вопрос проектирования. В случае, когда это функция, которая работает с каким-то типом объекта согласно своей сигнатуре, рефакторинг, изменение и ее перемещение становится гораздо проще. Что и было показано в примере в статье. И в целом проще становится конструирование тех самых конвейеров из всех этих функций.

Любой, кто поработал хотя бы немного с ООП в JS, знает, что лучше бы там были обычное ООП и наследование.

Это просто холиварная тема. Как бы нет, не лучше:) И я оставил ссылку на статью, где даже есть подробно описанное мнение почему это так. И привел в пример React, где уже несколько лет никаких классов, наследования и все отлично. React в отличной форме. Можно даже сказать, что в лучшей из тех, что был.

Все аргументы сводятся к «оказывается, код можно как-то написать и без классов» и вырванным из контекста цитатам из авторитетов (причём, если тот же авторитет пишет что-то с чем вы не согласны, нужно обязательно добавить «ну это же было давно и неправда»).

Потому, как речь в целом изначально речь и идет о книгах и подходах, которые эти сами авторитеты и писали. И эти цитаты призваны показать, как менялось с годами их же мнение. Как они не боялись сказать, что в чем-то заблуждались и были где-то не правы. Или что-то переосмыслили. Если вы увидели где-то фразу, которую вырвали из контекста, то можете на нее указать. Когда я подбирал цитаты, то как минимум оставил ссылки на все оригиналы. Где можно прочитать указанные цитаты и мнения с полным контекстом, откуда они были взяты в статью.

Желательно, стараться операции I/O двигать к границам и группировать все в отдельные транзакционные функции. Там уже в ней происходит все необходимое.

Если мы говорим про последовательность шагов в рамках транзакции, то тут встает вопрос, а чего в ней больше - бизнес логики или инфраструктуры? По идее последовательность шагов в транзакции соответствует бизнес процессу. Что за чем сохраняем и как откатываем. Но в то же время мы используем низкоуровневую инфраструктуру для проведения транзакции. Тот же курсор. Так, что вероятнее лучше держать такие вещи на слое persistance, не перетягивая их в слой сервисов. Все-таки, хоть там и есть минимальная логика, но она только про работу с источниками и механизмами их отката. То есть по большей части связана с хранением данных, чем этот слой и занимается. Имя в таком случае лучше подбирать более сфокусировано на общем процессе :) update_post_and_add_comment_and_update_user_and_notify вполне может быть просто названа той причиной, почему эти действия объедены в одну транзакцию. К примеру handle_post_activity или process_post_interaction. Это так, навскидку. Для этого нужно в проекте выработать свою удобную всем конвенцию наименования. Собственно, я не называю этот слой именно репозиторием по двум причинам: во-первых, само определение, как должен быть реализован настоящий репозиторий часто вызывает некоторые холиварные споры, а во-вторых как раз по тому, что там может быть такая транзакционная работа.

В рамках работы нескольких последовательных операций с sql источником на самом деле все не так сложно. Несколько последовательных вызовов к базе группируются и работают с одним и тем же курсором в одной транзакции, если нужно обеспечить механизм rollback. Принимают его в качестве опционального параметра.

Ну, а для работы с другими источниками или серией вызовов для работы с разными источниками тут как и без репозитория приходится прописывать логику rollback уже как-то самому в зависимости от того, как можно откатить действие.

Есть варианты, к примеру, если мы работаем с серией вызовов к источнику sql и нужно сделать какой-то запрос по HTTP и в случае не успеха откатить все. Тогда все операции как раз можно обернуть в контекстный менеджер работы с базой и если HTTP часть завершится с ошибкой, то можно просто исключением прервать транзакцию и выйти из контекста инициировав в ней rollback. Соотвественно, если перед этим была какая-то логика отката по HTTP, то сначала выполняем ее.

Для разных источников sql можно сделать вложенные контекстные менеджеры. В общем большая часть проблем идентична проблемам работы с разнородными источниками и при других подходах.

Опять же, если стараться выдавливать операции I/O к границам, то для бизнес логики это все может оставаться так же абстрагированно.

Как я понял суть этой ветки разговора речь о том, когда пользователю кода не дают делать не хорошие вещи с объектами. И о ситуации, пользователи начинают обращаться к низкоуровневым полям, к которым они не должны обращаться. В общем случае с Питоном это просто решается добавлением нижних подчеркиваний в названия тех вещей, которые трогать не нужно. И это или соблюдается или нет. Добавили ли вы поведение в класс и сделали его методом. Или передали в функцию экземпляр класса, которая должна с ним работать. То, что трогать не желательно по задумке определения этой доменной сущности будет и в том и в том случае обозначатся одинаково в самом названии атрибутов. Соблюдать или игнорировать эту конвенцию можно при любом подходе.

Да, но это больше конвенция, чем реальная преграда. Для класса MyClass атрибут __myattr будет напрямую доступен просто через _MyClass__myattr. Это не говоря о всех тех непотребствах, которые можно натворить прямо в рантайме с помощью того же манки патчинга :)

На самом деле эта картинка намеренно такая "косожопая" вставлена, ведь она в тексте рядом с FizzBuzzEnterpriseEdition. Что как бы намекает, что такой подход порождает что-то подобное на этой картинке :)

С подключением из глобально инициализированного пула внутри get_users в большинстве случаев нет проблем по моему опыту. Инициализировать переменную окружения мы точно не забудем о чем нам скажет завалившийся деплой. Модуль settings просто не найдет инициализированную переменную и старт приложения завершится с ошибкой.

На счет получения сущности из разных источников в рамках одного работающего приложения. В этом случае вариант передать через аргумент коннектор в БД в виде зависимости - да, нормальное решение. Но, как я понимаю, это если в случае, когда данные лежат в одинаковых типах источников. Что само по себе может быть странно, почему сущность юзера лежит в двух разных экземплярах того же Postgres.

Если источники очень разного характера, к примеру SQL и HTTP, то получение данных из них в любом случае лучше разнести по разным функциям. Если одна и та же сущность в одинаковом виде лежит в разных базах для одного и того же приложения, то это скорее повод подумать, все ли в порядке с хранением данных. Обычно данные об одной и той же сущности лежат в разных источниках, если это все же данные разного характера. В случае с Post мы можем получать данные о просмотрах из какого-нибудь кликстрима, вроде Кликхауса. В общем случае работа с разными источниками будет реализована через разные интерфейсы на уровне непосредственного получения данных. Ведь, собственно, за абстрагирование работы с этими разными интерфейсами и отвечает слой persistence. В него мы получаем что-то из слоя infrastructure, вроде коннекта к конкретной базе или HTTP сессию, а уже функция внутри реализует всю работу по извлечению данных через АПИ этого источника и оборачивание в доменную сущность, чтоб вернуть в ее бизнес логику.

Хм, касательно второго думаю, что относительно да, до тех пор, пока в условном классе Users не идет обращение к самому экземпляру внутри методов. :) То есть, пока класс выступает по сути в роли неймспейса для всех этих функций aka методов для из группировки.

Все таки, мне кажется что основная задача наличия класса - это инкапсулирование работы с объектом по изначальной задумке. Найти функцию в модуле и найти метод в классе в целом задачи +- одинаковые по сложности. Сначала нужно найти нужный модуль или класс, потом найти в нем нужное поведение. Ну и доступ к функциям в модуле через IDE можно упростить сделав import functools и потом IDE выдаст через точку все содержимое модуля полностью аналогично с содержимым экземпляра класса. Можно даже написать алиас, если хочется меньше в коде писать символов import functools as ft. Тут главное, чтоб конвенция была для алиасов модулей. Впрочем, как и нужна конвенция для именования переменных и так далее.

выяснится, что функции работы с, например, постами нужны вместе, вместе с необходимыми dataclass, и в неймспейсе вида Post

Тут не очень понял. По идее Post - это доменная сущность и ее объявление, как и правила создания мы кладем в доменный слой нашего приложения. Который не зависит от других слоев. А уже на уровне сервисов мы выстраиваем работу с Post с помощью тех функций, которые реализуют нашу бизнес-логику. Там и встречаются данные и поведение.

Почему такая замена удобна на мой взгляд (группировка функций на уровне модулей, а не методов в классах) - так это обычно менее связанный и проще модифицируемый код. Такой подход к написанию сам тебя подталкивает к тому, чтобы делать меньше не явных преобразований и других сайд-эффектов в поведении. Ведь доступ ко всему поведению внутри метода через self открывает большое окно возможностей делать всякие дополнительные действия не явно внутри методов. С другой стороны функция, которая хочет использовать какое-то поведение явно декларирует это своей зависимостью в сигнатуре. То есть код получается более прозрачный для изучающего, как с ним работать. Опять же, чистые функции. Мы получаем на вход все наши данные и зависимости через аргументы и выдаем данные на выход.

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

Отказа от ООП как такового нет, все верно. И об этом даже есть не большая цитата от Роберта Мартина на тему того, что ООП и ФП могут и даже должны хорошо уживаться в современных системах. Отказ от так называемого классического ООП, которое по большей части получило широкое распространение благодаря Java. Это самое ООП может быть очень разным, о чем тоже есть пример в статье - JavaScript.

Никакого объединения данных и методов с помощью неймспейсов не декларируется. Как раз наоборот, данные существуют отдельно. То, что их обрабатывает существует отдельно. И фундаментально проблемы в этом нет, опять же о чем есть пара слов в статье. В ФП языках в принципе не стоит этот вопрос об объединении данных и логики в одну корзину. Что может работать с чем определяется сигнатурами функций и интерфейсами.

Так как я не работал с C#, то могу судить только из быстрого ознакомления с предметом по описанию.

Если я все правильно понял, то default interface methods можно примерно похоже реализовать с помощью модуля abc https://docs.python.org/3/library/abc.html

По поводу методов-расширений первое, что приходит на ум - это monkey patching.

В данном случае нет. Объект Post содержит в одном из своих свойств массив комментариев. Функция возвращает нам массив не прочитанных комментариев. Конкретно эта функция является чистой и не имеет побочных эффектов и ее выходные данные зависят только от входных. Передавая один и тот же объект Post на вход мы всегда будем получать один и тот же результат на выход.

Спасибо. Тут скорее не то, чтобы очень свежий взгляд, а больше желание передать опыт тем, кто начинает разрабатывать системы. И постараться пролить для них свет на те вопросы, которые вставали передо мной и, возможно, встанут перед ними.

Не очень понятно выходит тогда, а как предлагается решать такую проблему? Кажется, что наличие или отсутствие модуля persistence не убирает и не добавляет фундаментальную проблему наличия работы с источниками. И их интеграционного тестирования.

Да, благодарю за замечание, поправил. Конечно же по тексту верно:

Мы знаем, что метод get_unread_comments работает с экземпляром класса Post

По поводу:

Можете прояснить, не совсем понятно, в какой именно момент это стало ясно? Мы вроде
изначально могли раздельно все хранить

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

Вот тут немного не понял вопрос. :) Если можно, как-то развернуть его подробнее.

По поводу ROP в Python, к сожалению, хорошего ответа пока нет. Говоря о ROP, больше хотелось показать само наличие концепта и идей, в него заложенных, как пищу для размышлений и расширения кругозора. Сейчас очень интересует попробовать на практике возможности https://returns.readthedocs.io/en/latest/pages/railway.html# , но пока есть некоторые сомнения в лаконичности и естественности такого кода в Python. Это еще предстоит посмотреть и оценить, само крайне интересно, что получится. Вариант делать по Go-шному и всегда из функций возвращать значение и ошибку возможен, но в целом тоже не то, чтобы очень хорошо выглядел в Python. Пока как базовую хорошую практику взято за правило не использовать инфраструктуру фреймворка внутри бизнес-логики для работы с ошибками. То есть, после выброса ошибки, она должна быть поймана в именно хенделере, который вызвал конвеер, и выбор ответа должен произойти там же. Чтобы все заранее запланированные пути завершения работы были видны из точки вызова. Конечно, расстраивает то, что такая штука держится исключительно на конвенциях и дисциплине разработчиков. Ведь в случае с F# и ROP разработчика ударит по рукам компилятор, если какой-то из путей штатного завершения программы не был обработан. Тут мы, к сожалению, просто снова возвращаемся к фундаментальным проблемам работы с бизнес-логикой в языках, где нормальным является выброс исключений, и их отлов уже где-то на верхних уровнях в коде.

По поводу Persistence, если я правильно понял вопрос, то все настраивается через переменные окружения. Все конфигурации для подключения к источникам берутся из секретов уже на этапе деплоя собранного экземпляра приложения. Мы собираем артефакт и деплоим его в нужное окружение - на тестовый стенд или в продакшн. При прогоне интеграционных тестов соответственно просто выставляются все нужные переменные для соединения с тестовыми экземплярами этих источников, которые развернуты в отдельных контейнерах.

Отдельной Open Source базы, к сожалению, нет, где можно было бы посмотреть полноценные реализации. Весь код такого характера пишется именно на работе. Хотя понимаю суть запроса - увидеть больше деталей было бы полезно. Я взял на заметку на будущее, хотя в целом суть данной статьи была не столько до косточек разобрать, как именно писать конкретный микросервис во всех тонкостях, сколько о том, как стоит работать с информацией, изучением книг и отдельных подходов для того, чтобы избежать фрустрации, когда какая-то теория на практике не очень хорошо работает в вашем коде.

Не совсем понятно, для чего именно здесь нужна ORM. В данном случае вы создаете объект Publication и передаете его в get_unread_comments. Объект Publication не связан с ORM. Вам не обязательно его брать из базы. Вы можете создать его в коде теста перед вызовом функции.

И если мы говорим о работе с базой, то это уже не юнит-тесты. Это интеграционные тесты, которые тоже очень полезны и хороши. И при правильно настроенной инфраструктуре они работают не очень сложно.

Допустим, в рамках деплоя через CI/CD на этапе тестирования вы можете просто поднять рядом в докер-контейнере экземпляр используемой вами базы. Да, для такого тестирования у вас должен быть механизм подготовки базы перед запуском теста, механизм отката базы в дефолтное состояние после завершения теста. Обычно сам механизм вы делаете и настраиваете один раз при начале работы с интеграционными тестами. А дальше дополняете или файл базовых миграций для подготовки вашего дефолтного экземпляра базы или как-то в самом интеграционном тесте, если вам нужно создать в базе что-то специфичное для конкретного случая.

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

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Работает в
Дата рождения
Зарегистрирован
Активность