Comments 55
def get_unread_comments(pubication: Publicaion) -> list[Comment]
Это всё конечно замечательно, но есть один нюанс. У вас появился немаленького такого размера захардкоженый глобальный контекст, который используется внутри функции, причем практически всегда этот контекст раскидан по множеству модулей выполненных в таком же передовом стиле, и отладка и написание юниттестов превращается в ад (либо в такие безжалостные моки, что половина сути тестирования теряется)
А можно, немного более полно раскрыть комментарий? Пока не очень понятно, о каком именно захаркоженном глобальном контексте идет речь и почему написание юнит-тестов превращается в боль.
Если мы говорим о написании любого классического юнит-теста, то, тестируя функцию, нам в любом случае придется подготовить входные данные согласно сигнатуре этой функции. В данном случае нам нужно инициализировать экземпляр объекта Publication и передать его в get_unread_comments.
Опять же, тестирование в целом - это большое благо. И если есть время тестировать каждую функцию отдельно такими юнит-тестами, то это классно. Но иногда можно протестировать интеграционно готовый воркфлоу по всем основным бизнес-сценариям. Положительным и отрицательным, где каждая такая встроенная функция пройдет тестирование в рамках другой функции, куда она встроена, в рамках бизнес-процесса.
Вот смотрите - предположим, вы используете ORM.
Значит, прежде чем протестировать эту функцию, вам нужно:
Настроить маппинги в ORM
Создать базу данных
Заполнить базу тестовыми данными
Делать это для каждого теста
И при этом вам надо еще специально позаботиться о том, чтобы тесты случайно не запустились одновременно, ибо если им требуются разные датасеты, они могут конфликтовать. А еще возможно, что после любого теста который подразумевает модификацию данных, вам надо очистить базу от результатов теста (ну или пересоздать/переинициализировать тестовое окружение).
Не совсем понятно, для чего именно здесь нужна ORM. В данном случае вы создаете объект Publication и передаете его в get_unread_comments. Объект Publication не связан с ORM. Вам не обязательно его брать из базы. Вы можете создать его в коде теста перед вызовом функции.
И если мы говорим о работе с базой, то это уже не юнит-тесты. Это интеграционные тесты, которые тоже очень полезны и хороши. И при правильно настроенной инфраструктуре они работают не очень сложно.
Допустим, в рамках деплоя через CI/CD на этапе тестирования вы можете просто поднять рядом в докер-контейнере экземпляр используемой вами базы. Да, для такого тестирования у вас должен быть механизм подготовки базы перед запуском теста, механизм отката базы в дефолтное состояние после завершения теста. Обычно сам механизм вы делаете и настраиваете один раз при начале работы с интеграционными тестами. А дальше дополняете или файл базовых миграций для подготовки вашего дефолтного экземпляра базы или как-то в самом интеграционном тесте, если вам нужно создать в базе что-то специфичное для конкретного случая.
Но повторюсь, это не юнит-тест. И в целом, лучше всего, если ваша бизнес-логика не знает ничего о том, как вы получаете данные. В идеале, как я постарался раскрыть в статье, вы работаете в бизнес-логике именно с доменными моделями. Допустим, с теми же Pydantic моделями, которые держите и объявляете отдельно в доменном слое вашего приложения. Pydantic умеет работать с той же SQLAlchemy. И тогда любые бизнес-функции вы можете тестировать именно юнит-тестами, без интеграции с хранилищами. Все, что вам нужно для теста - это создать в коде нужную модель со своими тестовыми данными и передать ее на вход для тестирования. Извлекать из хранилища вам ее нужно именно когда вы хотите написать интеграционный тест.
И более того, такие же настройки вам предстоит делать перед любым тестом, который хоть как то обращается к вашим функциям которые ходят в базу. А если вы для облегчения жизни решите мокать свои функции - то однажды потом, сломав свои функции, вы не сломаете тесты которые на них опирались - ведь вы используете моки, а они то как раз сохранили старое поведение.
Ну вы статью прочтите хоть. Там в разделе "Микросервисы" приведена структура приложения. Только в модуле persistence
содержатся "функции которые ходят в базу", (от себя добавлю - их тестирование тривиально т.к. это паттерн "репозиторий" - просто вытащить и передать в нужной структуре наверх).
Само наличие модуля persistence и есть проблема. Вот смотрите - вам нужно оттестировать функцию get_users_list. Какие есть варианты?
Первый - создать тестовую базу, сконфигурировать ваш модуль так чтобы он на неё смотрел и прогнать тест. Минусы - а если база это «чуть больше чем sql” - например какая нибудь nosql, то вам нужно для теста сконфигурировать еще и сервис БД.
Второй вариант - вы мокаете функции модуля persistence. Но потом вы меняете какую - нибудь функцию, например поменяв семантику параметра - но ваши тесты не ломаются потому что ходят на моки которые реализуют старую семантику, а код ломается потому полагается на старую семантику а реализация использует новую.
А задача сделать несколько бакэндов, например sql и nosql превращается либо в пляски с бубном вокруг динамической загрузки модулей в рантайме, либо в натягивание статиков поверх синглетона реального бакэнда
Не очень понятно выходит тогда, а как предлагается решать такую проблему? Кажется, что наличие или отсутствие модуля persistence не убирает и не добавляет фундаментальную проблему наличия работы с источниками. И их интеграционного тестирования.
Если вы о части модуля persistence, то её не нужно тестировать. В ней нет нетривиальной бизнес-логики, зачем там тесты?
У вас есть некий метод, который возвращает что-то.
Значит, у вас должен быть тест который проверяет корректность поведения.
И стек вызова этого кода заходит в ваш модуль persistence.
Это означает, что для прохождения юнит-теста и доказательства работы вашего кода, вы должны засетапить источник данных и залить в него эталонные данные, чтобы впоследствии получить результат на основании этих данных и сверить фактический ответ с ожидаемым.
Это не обсуждается, ибо это просто базовое требование.
А теперь просто говорим "наш продукт работает с базой в MSSQL и MongoDB". И всё - теперь вы для юнит-тестов должны притащить MSSQL и монгу. Можно конечно сказать "ой да мы это интеграционными тестами проверим" - но как правило каждый раз когда такое говорится это всё уходит в вечный техдолг.
Зачем?
Зачем что?
Зачем натягивать функции поверх глобальных объектов?
Ну затем, что у вас нет контекста приложения в явном виде.
Где лежит ваша сессия к базе, чтобы её можно было проверить и при необходимости реконнектнуть? Ну конечно, в persistence._SESSION или чем-то подобном. Где лежат маппинги классов к таблицам? Ой да это мы вообще, скопировали пример из документации (но на самом деле тоже в какой-то глобальной переменной). И вот ваш код зависит от кучи глобальных объектов и набит сайдэффектами.
Зачем натягивать поверх синглетонов?
Ну вот вы написали "тривиальных функций" в persistence. А теперь поступило требование заказчика - не SQL'ный бакэнд (вот у него такая техническая политика, у него всё в тарантуле лежит). И теперь у вас две реализации persistence, одна через ORM а другая через API (ибо нет тарантула в вашем ORM). И как вы будете выбирать бакэнд?
И вы начинаете приседать с бубном - вы пишете persistece_sql и persistence_nosql, и собственно ваш persistence перерождается - в нем вы загружаете один из реальных модулей и все ваши функции просто проксируются на загруженный модуль.
Но потом в процесс врывается статический линтер, который требует от вас задекларировать все сигнатуры превентивно (да-да, в динамически загруженном юните символ с точки зрентия статического анализа то Any) и вы наконец-то отказываетесь от модулей и пишете реализации в классах и называете их "драйверами". Но опять же - поскольку контекста приложения как сущности у вас нет, вы создаете совместно используемый глобальный репозиторий ваших объектов. А так как вы уже наваяли в куче мест походов в модель через функции - вы переписываете все функции чтобы они ходили на ваш глобальный статический репозиторий.
Вполне типовые грабли.
Фантастика. В хорошем смысле слова.
У меня есть пара практических вопросов.
Я от ROP-а в чисто функциональном стиле отказался как раз из-за синтаксического мусора, который он генеряет в Kotlin. Вы вроде сказали, что в Python те же проблемы, но не рассказали как это в итоге делаете. Расскажете?
Затем, функции в persistance. Они всё-таки по природе своей stateful (имеют источник подключения к БД условной). Вы их в итоге к глобальному окружению приколачиваете? Или через частичное применение в рантайме собираете? Или ещё как-то? Так же буду благодарен за подробности.
И вообще, если можете скинуть ссылку на код в этом стиле, который ходит в базу, ходит по хттп и чё-нить в очередь публикует (желательно одновременно) - изучу с большим интересом:)
А за State-driven и action-driven - отдельное спасибо. Я пару лет думал на эту тему в фоне и ни как не мог это лаконично сформулировать.
А если в рантайме собираете - чем это отличается от объекта?:) Я опять же отошёл от чистой функциональности и утешаю себя тем, что конструктор(p1, p2) + метод(p3) = функции(p1, p2, p3) :)
По поводу ROP в Python, к сожалению, хорошего ответа пока нет. Говоря о ROP, больше хотелось показать само наличие концепта и идей, в него заложенных, как пищу для размышлений и расширения кругозора. Сейчас очень интересует попробовать на практике возможности https://returns.readthedocs.io/en/latest/pages/railway.html# , но пока есть некоторые сомнения в лаконичности и естественности такого кода в Python. Это еще предстоит посмотреть и оценить, само крайне интересно, что получится. Вариант делать по Go-шному и всегда из функций возвращать значение и ошибку возможен, но в целом тоже не то, чтобы очень хорошо выглядел в Python. Пока как базовую хорошую практику взято за правило не использовать инфраструктуру фреймворка внутри бизнес-логики для работы с ошибками. То есть, после выброса ошибки, она должна быть поймана в именно хенделере, который вызвал конвеер, и выбор ответа должен произойти там же. Чтобы все заранее запланированные пути завершения работы были видны из точки вызова. Конечно, расстраивает то, что такая штука держится исключительно на конвенциях и дисциплине разработчиков. Ведь в случае с F# и ROP разработчика ударит по рукам компилятор, если какой-то из путей штатного завершения программы не был обработан. Тут мы, к сожалению, просто снова возвращаемся к фундаментальным проблемам работы с бизнес-логикой в языках, где нормальным является выброс исключений, и их отлов уже где-то на верхних уровнях в коде.
По поводу Persistence, если я правильно понял вопрос, то все настраивается через переменные окружения. Все конфигурации для подключения к источникам берутся из секретов уже на этапе деплоя собранного экземпляра приложения. Мы собираем артефакт и деплоим его в нужное окружение - на тестовый стенд или в продакшн. При прогоне интеграционных тестов соответственно просто выставляются все нужные переменные для соединения с тестовыми экземплярами этих источников, которые развернуты в отдельных контейнерах.
Отдельной Open Source базы, к сожалению, нет, где можно было бы посмотреть полноценные реализации. Весь код такого характера пишется именно на работе. Хотя понимаю суть запроса - увидеть больше деталей было бы полезно. Я взял на заметку на будущее, хотя в целом суть данной статьи была не столько до косточек разобрать, как именно писать конкретный микросервис во всех тонкостях, сколько о том, как стоит работать с информацией, изучением книг и отдельных подходов для того, чтобы избежать фрустрации, когда какая-то теория на практике не очень хорошо работает в вашем коде.
То есть, после выброса ошибки, она должна быть поймана в именно
хенделере, который вызвал конвеер, и выбор ответа должен произойти там
же.
Угу, я так же извернулся.
По поводу Persistence, если я правильно понял вопрос
Кажется не правильно поняли:)
Вот эта функция:
async def get_users() -> list[User]:
Я так понял - это статическое определение именно функции, а не переменной функционального типа. Соответственно подключение к внешнеей системе (БД, например) она берёт из глобальной переменной.
Отсюда я вижу два следствия:
При её использовании, надо догадаться что эту глобальную переменную надо про инициализировать
Нельзя иметь в программе два источника данных, которые будут брать юзеров из разных источников.
Или у вас всё-таки что-то в таком духе:
fun getUsers(ds: DataSource): List<User> = { ds.fetchUsers() }
val db1Users = getUsers(DataSource(db1))
val db2Users = getUsers(DataSource(db2))
class Client(private val getUsers: () -> List<User>) {
fun showUsers() {
getUsers().forEach { println(it) }
}
}
fun main() {
val client = Client(db1Users)
client.showUsers()
}
Ну т.е. как вы инжектите инфраструктуру в такие функции?
Касательно второго, комментария, я утверждаю (самовнушаю ), что следующая запись является эквивалентом предыдущей:
class Users(private val ds: DataSource) {}
fun getUsers(): List<User> = ds.fetchUsers()
}
class Client(private val users: Useres)) {
fun showUsers() {
users.getUsers().forEach { println(it) }
}
}
fun main() {
val client = Client(Users(DataSource(db1)))
client.showUsers()
}
То есть конструктор выполняет функцию частичного применения своих параметров к методам объекта. Забыл уже терминологию
С подключением из глобально инициализированного пула внутри get_users в большинстве случаев нет проблем по моему опыту. Инициализировать переменную окружения мы точно не забудем о чем нам скажет завалившийся деплой. Модуль settings просто не найдет инициализированную переменную и старт приложения завершится с ошибкой.
На счет получения сущности из разных источников в рамках одного работающего приложения. В этом случае вариант передать через аргумент коннектор в БД в виде зависимости - да, нормальное решение. Но, как я понимаю, это если в случае, когда данные лежат в одинаковых типах источников. Что само по себе может быть странно, почему сущность юзера лежит в двух разных экземплярах того же Postgres.
Если источники очень разного характера, к примеру SQL и HTTP, то получение данных из них в любом случае лучше разнести по разным функциям. Если одна и та же сущность в одинаковом виде лежит в разных базах для одного и того же приложения, то это скорее повод подумать, все ли в порядке с хранением данных. Обычно данные об одной и той же сущности лежат в разных источниках, если это все же данные разного характера. В случае с Post мы можем получать данные о просмотрах из какого-нибудь кликстрима, вроде Кликхауса. В общем случае работа с разными источниками будет реализована через разные интерфейсы на уровне непосредственного получения данных. Ведь, собственно, за абстрагирование работы с этими разными интерфейсами и отвечает слой persistence. В него мы получаем что-то из слоя infrastructure, вроде коннекта к конкретной базе или HTTP сессию, а уже функция внутри реализует всю работу по извлечению данных через АПИ этого источника и оборачивание в доменную сущность, чтоб вернуть в ее бизнес логику.
Хм, касательно второго думаю, что относительно да, до тех пор, пока в условном классе Users не идет обращение к самому экземпляру внутри методов. :) То есть, пока класс выступает по сути в роли неймспейса для всех этих функций aka методов для из группировки.
Понял, спасибо.
Ну и за пост в целом ещ раз спасибо - я в восторге от него, он хорошо лёг на актуальную для меня сейчас проблему с разрастанием модулей в ООП-стиле. У меня пайплайны лежат в тех модулях, с стоянием которых они наиболее сцепленны и это ведёт к жирным модулям и сильной сцепленности модулей из-за пайплайнов, которым надо состояние нескольких модулей потрогать. И я второй день думаю о том как бы мне идею разделения состояния и пайплайнов в свою практику встроить
Боль слоя persistence и БД начинается с транзакциями, когда, например, 2 метода репозитория должны быть выполнены в транзакции, а третий опционально, а еще хуже, когда, при определенной бизнес логике, нужно выполнить rollback.
Подскажите пожалуйста, как с этим работаете ?
всегда считаете, что, грубо говоря, 1 http запрос есть атомарная операция и безусловно делаете commit (или rollback) ? или сессия проталкивается по всем слоям ?
В рамках работы нескольких последовательных операций с sql источником на самом деле все не так сложно. Несколько последовательных вызовов к базе группируются и работают с одним и тем же курсором в одной транзакции, если нужно обеспечить механизм rollback. Принимают его в качестве опционального параметра.
Ну, а для работы с другими источниками или серией вызовов для работы с разными источниками тут как и без репозитория приходится прописывать логику rollback уже как-то самому в зависимости от того, как можно откатить действие.
Есть варианты, к примеру, если мы работаем с серией вызовов к источнику sql и нужно сделать какой-то запрос по HTTP и в случае не успеха откатить все. Тогда все операции как раз можно обернуть в контекстный менеджер работы с базой и если HTTP часть завершится с ошибкой, то можно просто исключением прервать транзакцию и выйти из контекста инициировав в ней rollback. Соотвественно, если перед этим была какая-то логика отката по HTTP, то сначала выполняем ее.
Для разных источников sql можно сделать вложенные контекстные менеджеры. В общем большая часть проблем идентична проблемам работы с разнородными источниками и при других подходах.
Опять же, если стараться выдавливать операции I/O к границам, то для бизнес логики это все может оставаться так же абстрагированно.
В рамках работы нескольких последовательных операций с sql источником на самом деле все не так сложно. Несколько последовательных вызовов к базе группируются и работают с одним и тем же курсором в одной транзакции, если нужно обеспечить механизм rollback. Принимают его в качестве опционального параметра.
Т.е. в слое сервисов должна порождаться транзакция, и пропихиваться в репозитории ?
Или репозиторий должен содержать транзакционные методы, типа, утрирую, update_post_and_add_comment_and_update_user_and_notify ?
Или же мы должны собирать аггрегат в классическом виде, и его сохранять полностью ?
P.S. не придираюсь ни в коем случае, тема мне очень интересна. За сам пост большое спасибо, +- к этому и пришли.
Желательно, стараться операции I/O двигать к границам и группировать все в отдельные транзакционные функции. Там уже в ней происходит все необходимое.
Если мы говорим про последовательность шагов в рамках транзакции, то тут встает вопрос, а чего в ней больше - бизнес логики или инфраструктуры? По идее последовательность шагов в транзакции соответствует бизнес процессу. Что за чем сохраняем и как откатываем. Но в то же время мы используем низкоуровневую инфраструктуру для проведения транзакции. Тот же курсор. Так, что вероятнее лучше держать такие вещи на слое persistance, не перетягивая их в слой сервисов. Все-таки, хоть там и есть минимальная логика, но она только про работу с источниками и механизмами их отката. То есть по большей части связана с хранением данных, чем этот слой и занимается. Имя в таком случае лучше подбирать более сфокусировано на общем процессе :) update_post_and_add_comment_and_update_user_and_notify вполне может быть просто названа той причиной, почему эти действия объедены в одну транзакцию. К примеру handle_post_activity или process_post_interaction. Это так, навскидку. Для этого нужно в проекте выработать свою удобную всем конвенцию наименования. Собственно, я не называю этот слой именно репозиторием по двум причинам: во-первых, само определение, как должен быть реализован настоящий репозиторий часто вызывает некоторые холиварные споры, а во-вторых как раз по тому, что там может быть такая транзакционная работа.
Вообще, в Go можно уйти от возврата ошибок в сторону того же отправления всех ошибок в специальный канал.
Однако, предвижу тонны проблем с дебаггингом, так как в стектрейсах и сентри, скорее всего точка отстрела ошибки будет в той гороутине, которая тащит и как-то "обрабатывает" ошибки из канала. Т.е. могут возникнуть трудности с определением места возникновения ошибки.
Open source нет, ведь код пишется на работе
Чот прям грустно стало...
И в конце структура папочек, которую предполагает стандартный шаблон asp.net core.
Вообще статья странная. Я думал, что обычно с такого подхода и начинают. То есть сначала фигачим всё в процедурном стиле, потом разработчик узнает о паттернах и DDD и жизнь начинает играть новыми красками и фабриками, после этого приходит осознание, что не везде оно надо.
То есть статья-то правильная и хорошая, но когда уже лет 5 пишешь в таком стиле, странно читать это как "смотрите, свежий взгляд на то, как можно писать микросервисы"
Я, конечно со стороны C# говорю. Может питон как-то иными путями развивается.
Спасибо. Тут скорее не то, чтобы очень свежий взгляд, а больше желание передать опыт тем, кто начинает разрабатывать системы. И постараться пролить для них свет на те вопросы, которые вставали передо мной и, возможно, встанут перед ними.
Да, по поводу папочек нелья не отметить алтернативный подход, когда у нас есть папка user и там уже в ней все про это - и инфраструктура, и персистентносиь и сервисы, и апи. А вообще в идеале у каждого файла кода могли бы быть теги. Тогда можно будет и по слоям группировать, и по сущности. В этом плане редакторы кода пока плохо развиваются. Жаль идея Билла Гейтса загнать всю файловую систему в бд не взлетела.
И вот теперь нам уже намного проще. Мы знаем, что метод Post работает с экземпляром класса Post
А что за метод Post? Я что то упустил немного
Получается очень интересная штука: нам больше не обязательно привязывать поведение и сами данные, они могут существовать раздельно в нашей кодовой базе.
Можете прояснить, не совсем понятно, в какой именно момент это стало ясно? Мы вроде изначально могли раздельно все хранить
Да, благодарю за замечание, поправил. Конечно же по тексту верно:
Мы знаем, что метод get_unread_comments работает с экземпляром класса Post
По поводу:
Можете прояснить, не совсем понятно, в какой именно момент это стало ясно? Мы вроде
изначально могли раздельно все хранить
Согласно классической концепции ООП, объект содержит поведение или методы. Нам необходимо определить, какое поведение соответствует каждому объекту и встроить его в объект. Однако, в предыдущем тексте я постарался показать, что не обязательно решать эту задачу и старательно определять, в какой объект мы должны вложить поведение. Во многом упрощает многие аспекты работы над программой.
Но ведь в любом случае get_unread_comments(post) для работы нужны какие-то данные типа строк подключения, конфигурации и тп. Этот метод их будет неявно брать из какого-то общего глобального контекста?
В данном случае нет. Объект Post содержит в одном из своих свойств массив комментариев. Функция возвращает нам массив не прочитанных комментариев. Конкретно эта функция является чистой и не имеет побочных эффектов и ее выходные данные зависят только от входных. Передавая один и тот же объект Post на вход мы всегда будем получать один и тот же результат на выход.
А в питоне нет функционала методов-расширений или default interface methods?
Так как я не работал с C#, то могу судить только из быстрого ознакомления с предметом по описанию.
Если я все правильно понял, то default interface methods можно примерно похоже реализовать с помощью модуля abc https://docs.python.org/3/library/abc.html
По поводу методов-расширений первое, что приходит на ум - это monkey patching.
Не совсем понятно, а где тут отказ от ООП? Если у микросервиса есть персистанс - то он уже является объектом с точки зрения ООП (в изначальном смысле, объект - как стейт+список обрабатываемых событий, где стейт - это персистанс, а события - это entrypoints). Вот если от предлагаемой структуры микросервиса переходить к стилю Pipes&Filters - можно говорить об отказе от ОПП.
Да и в предложенной структуре кода вместо класса просто используется namespace как объединение данных и методов, особой разницы (кроме сложностей в использовании) нет.
Так что в чем смысл предложенного метода - не совсем понятно.
Отказа от ООП как такового нет, все верно. И об этом даже есть не большая цитата от Роберта Мартина на тему того, что ООП и ФП могут и даже должны хорошо уживаться в современных системах. Отказ от так называемого классического ООП, которое по большей части получило широкое распространение благодаря Java. Это самое ООП может быть очень разным, о чем тоже есть пример в статье - JavaScript.
Никакого объединения данных и методов с помощью неймспейсов не декларируется. Как раз наоборот, данные существуют отдельно. То, что их обрабатывает существует отдельно. И фундаментально проблемы в этом нет, опять же о чем есть пара слов в статье. В ФП языках в принципе не стоит этот вопрос об объединении данных и логики в одну корзину. Что может работать с чем определяется сигнатурами функций и интерфейсами.
Ну, так или иначе все равно нужна группировка разных функций, иначе "найти" нужное (тем более обеспечивать автоматический intellisense и прочие фишки) будет сложно.
Классы+методы нужны, кроме прочего, и как метод структурирования кода - и тут никаких вариантов не предлагается, просто "группировка функций в неймспейсах" - недостаточная замена.
А как только начнется группировка, то выяснится, что функции работы с, например, постами нужны вместе, вместе с необходимыми dataclass, и в неймспейсе вида Post, что приведет к модульному коду в духе старого Pascal, из которого Java и выросла. Ну, длинный путь чтобы дойти до ООП.
Но для небольших проектов - да, вполне нормальный подход. Если еще и с ООА сочетать.
Все таки, мне кажется что основная задача наличия класса - это инкапсулирование работы с объектом по изначальной задумке. Найти функцию в модуле и найти метод в классе в целом задачи +- одинаковые по сложности. Сначала нужно найти нужный модуль или класс, потом найти в нем нужное поведение. Ну и доступ к функциям в модуле через IDE можно упростить сделав import functools и потом IDE выдаст через точку все содержимое модуля полностью аналогично с содержимым экземпляра класса. Можно даже написать алиас, если хочется меньше в коде писать символов import functools as ft. Тут главное, чтоб конвенция была для алиасов модулей. Впрочем, как и нужна конвенция для именования переменных и так далее.
выяснится, что функции работы с, например, постами нужны вместе, вместе с необходимыми dataclass, и в неймспейсе вида Post
Тут не очень понял. По идее Post - это доменная сущность и ее объявление, как и правила создания мы кладем в доменный слой нашего приложения. Который не зависит от других слоев. А уже на уровне сервисов мы выстраиваем работу с Post с помощью тех функций, которые реализуют нашу бизнес-логику. Там и встречаются данные и поведение.
Почему такая замена удобна на мой взгляд (группировка функций на уровне модулей, а не методов в классах) - так это обычно менее связанный и проще модифицируемый код. Такой подход к написанию сам тебя подталкивает к тому, чтобы делать меньше не явных преобразований и других сайд-эффектов в поведении. Ведь доступ ко всему поведению внутри метода через self открывает большое окно возможностей делать всякие дополнительные действия не явно внутри методов. С другой стороны функция, которая хочет использовать какое-то поведение явно декларирует это своей зависимостью в сигнатуре. То есть код получается более прозрачный для изучающего, как с ним работать. Опять же, чистые функции. Мы получаем на вход все наши данные и зависимости через аргументы и выдаем данные на выход.
Да классическое ОПП вообще жеско переоцененная идея. Наследование — тут вообще всего 2 ходовых кейса, UI фреймворки и геймдев. Модификаторы доступа — их можно просто выкинуть, и жопа не отвалится. Интерфейсы с виртуальными функциями — полезная штука, но их в 80% случаев используют для оверинжиниринга.
Так что тенденция такая, что в более новых языках от ооп останутся только композиция, методы и утиная типизация с вирт функциями. А про остальное забудут так же, как про другие убогие идеи, типа посылки сообщений в smalltalk и протопиного наследования.
Чистая архитектура, паттерны проектирования, примеры реализации принципов SOLID, дядя Боб
По традиции, не могу не кинуть козюлю в человека, который не считает вышеперечисленное вредным мусором
Вам самим не стремно такое в статью сувать?
Все это замечательно с примером поста, пока над кодовой базой работает всего лишь один человек. А когда работают несколько, или еще ни дай бог новые люди приходят, то получается так, что люди начинают снова и снова повторно реализовывать вот все эти «функции, принимающие на вход Publication и что-то с ним делающие». И начинают обращаться напрямую к его полям, которые слишком низкоуровневые, чтобы с ними работать напрямую. В итоге код превращается в лапшу - чего не произошло бы, будь у Publication (или Post) четкий интерфейс с закрытыми свойствами для внутреннего использования.
Классический пример тут - на клиентской стороны, Apollo Client. Он возвращает GraphQL-объекты как POJO и не дает навешивать на них методы. В итоге получается, что когда хочется добавить общую логику проверки на такие объекты (например, isEditable), то приходится, во-первых, называть эти функции postIsEditable (а не просто isEditable - иначе поиск по коду превращается в кошмар), а во-вторых, они начинают плодиться в разных местах с немного разным смыслом, как кролики (а кто-то начинает напрямую лезть в свойства объекта и проверять, не зная, что есть функция для этого).
Тут коротко просто отвечу той же мыслью, что высказана в статье. В Питоне нет по настоящему ничего приватного. "Все мы здесь взрослые, отвественые люди". Если кто-то захочет куролесить, изменяя объект так, как этого делать не следует, он всегда сможет это сделать. Такие вещи только код-ревью решаются, так или иначе. Ну и дисциплиной и ответственностью самих разработчиков, опять же так задуман язык его создателем. Тут оно, как есть :)
В Питоне нет по настоящему ничего приватного
Но есть name mangling через префикс двойного подчеркивания, типа __myattr
Да, но это больше конвенция, чем реальная преграда. Для класса MyClass атрибут __myattr будет напрямую доступен просто через _MyClass__myattr. Это не говоря о всех тех непотребствах, которые можно натворить прямо в рантайме с помощью того же манки патчинга :)
Ну да, а еще можно добраться до локальных переменных вызывающего кода через stack frame, и вообще даже сдуру известно что сломать. Но это надо делать специально, и тут уж ССЗБ уровня "а еще я могу используя указатель на объект по смещению прочесть VMT и напрямую вызвать методы"
Как я понял суть этой ветки разговора речь о том, когда пользователю кода не дают делать не хорошие вещи с объектами. И о ситуации, пользователи начинают обращаться к низкоуровневым полям, к которым они не должны обращаться. В общем случае с Питоном это просто решается добавлением нижних подчеркиваний в названия тех вещей, которые трогать не нужно. И это или соблюдается или нет. Добавили ли вы поведение в класс и сделали его методом. Или передали в функцию экземпляр класса, которая должна с ним работать. То, что трогать не желательно по задумке определения этой доменной сущности будет и в том и в том случае обозначатся одинаково в самом названии атрибутов. Соблюдать или игнорировать эту конвенцию можно при любом подходе.
Всё правильно написал автор :-)
Я изучал Java целый год, понял что это всё хрень,
и перешёл на golang :-) с ограниченным ООП
Интересно. В ответ хочу порекомендовать недавно вышедшую книгу "Паттерны разработки на Python". В ней рассказывается о том когда и как писать классы, чтобы получить как можно больше пользы от ООП.
Даже первый вопрос — «Зачем нам вообще нужны классы?» — неправильный, потому что разговор про ООП нужно начинать не с классов, а с интерфейсов. И, как ни странно, применительно к Питону, на этом же можно и заканчивать, потому что интерфейсов в чистом виде там нет, а без них ООП теряет значительную часть своей полезности. Поэтому разработчикам и остаётся писать конструкции c протоколами.
По сути, такой код:
from typing import Protocol
class ProtocolType(Protocol):
// ...
def my_func(x: ProtocolType):
это кривоватый способ написать:interface SomeType {
function my_func() {
в других языках. И не надо пытаться сделать вид, что такая реализация ООП — это какое-то достоинство Питона.мы должны понять, как же организовать наши функции. Ответ в данном случае довольно прост: использовать пространства имён. Создавайте модули, в которых вы структурируете ваши функции согласно архитектуре приложенияТолько смотрите, случайно не оберните эти функции в объявление класса, а то не дай бог получится нормальное ООП. По сути, вы предлагаете то же самое ООП, только методы почему-то нужно писать вне классов. Чем это будет удобнее? Объяснения нет.
JavaScript — это ООП-язык с самого начала, просто использующий прототипное наследование, и даже оно лучше классического Java-наследования.… JavaScript никогда не нуждался в классах, которые в него добавили в 2015 году.Это всё вообще какие-то фантазии, вы уж простите. Любой, кто поработал хотя бы немного с ООП в JS, знает, что лучше бы там были обычное ООП и наследование.
Я не увидел в статье серьёзных аргументов в пользу отказа от ООП. Настолько, что, честно говоря, даже неясно, понимает ли автор то, о чём пишет, или просто где-то слышал, что ООП это не круто, а ФП круто. Все аргументы сводятся к «оказывается, код можно как-то написать и без классов» и вырванным из контекста цитатам из авторитетов (причём, если тот же авторитет пишет что-то с чем вы не согласны, нужно обязательно добавить «ну это же было давно и неправда»). Хотелось бы статью с примерами кода, из которых было бы понятно, как лучше.
Миллениалы изобрели процедурное программирование со всеми его недостатками.
Если в программе есть и процедурный и другие подходы - оно от этого так сильно хуже? Мы уже видели в виде классической Java через что приходится проходить и какие финты придумывать, чтобы существовать исключительно в чистом ОО мире. Когда нужно сделать какую-то не сложную вещь. К примеру, Go достаточно процедурный язык, что не делает его менее популярным в наше время. И его же за чем-то создали сейчас именно таким? При чем не глупые люди, которые явно знакомы со всеми парадигмами и подходами. Или набирающий популярность Rust, который тоже не является классическим ООП языком. Такие языки, как Питон - мультипарадигменные. Почему нужно боятся где-то написать процедурный код? Или ФП код? Кажется, что причин для этого нет, кроме предубеждений.
Поэтому разработчикам и остаётся писать конструкции c протоколами.
И в этом нет ничего плохого. Протоколы по сути те же интерфейсы. Прекрасно можно ими пользоваться в Питоне. А разговор начат с классов как раз по тому, что именно они в купе с наследованием и есть то самое классическое ООП, о котором идет речь. Есть ООП языки без классов и это не делает их менее ООП. Эталонного ООП просто не существует в настоящий момент. О чем речь тоже идет в статье. Есть разные вариации реализации этой идеи, но подходы могут серьезно отличаться.
И не надо пытаться сделать вид, что такая реализация ООП — это какое-то достоинство Питона.
Вроде ни кто и не пытался, не знаю, где это подается в таком виде. Такой мысли в статье не было.
Чем это будет удобнее? Объяснения нет.
Удобство заключается в том, что отсутствует сама проблема заранее точно определять кому и какое поведение принадлежит и с каким объектом оно должно ходить пришитым. А это частно далеко не самый тривиальный вопрос проектирования. В случае, когда это функция, которая работает с каким-то типом объекта согласно своей сигнатуре, рефакторинг, изменение и ее перемещение становится гораздо проще. Что и было показано в примере в статье. И в целом проще становится конструирование тех самых конвейеров из всех этих функций.
Любой, кто поработал хотя бы немного с ООП в JS, знает, что лучше бы там были обычное ООП и наследование.
Это просто холиварная тема. Как бы нет, не лучше:) И я оставил ссылку на статью, где даже есть подробно описанное мнение почему это так. И привел в пример React, где уже несколько лет никаких классов, наследования и все отлично. React в отличной форме. Можно даже сказать, что в лучшей из тех, что был.
Все аргументы сводятся к «оказывается, код можно как-то написать и без классов» и вырванным из контекста цитатам из авторитетов (причём, если тот же авторитет пишет что-то с чем вы не согласны, нужно обязательно добавить «ну это же было давно и неправда»).
Потому, как речь в целом изначально речь и идет о книгах и подходах, которые эти сами авторитеты и писали. И эти цитаты призваны показать, как менялось с годами их же мнение. Как они не боялись сказать, что в чем-то заблуждались и были где-то не правы. Или что-то переосмыслили. Если вы увидели где-то фразу, которую вырвали из контекста, то можете на нее указать. Когда я подбирал цитаты, то как минимум оставил ссылки на все оригиналы. Где можно прочитать указанные цитаты и мнения с полным контекстом, откуда они были взяты в статью.
Я считаю, что в Enterprise разработке нет места уверенности, в том, что "все мы взрослые люди". Очень многие ограничения в таких языках, как Java, помогают допустить как можно меньше ошибок, связанных с человеческим фактором
Тут хотелось бы спросить, а какие именно ограничения Java помогают совершать принципиально меньше ошибок? В случае с Haskell я бы мог понять такое утверждение, ведь не зря говорят "if a Haskell program compiles, it probably works". Его компилятор и система типов действительно делают нечто особенное по сравнению с большинством языков. Но Java?
Не совсем правильно выразился. Я имел ввиду такие конструкции, как модификаторы доступа. Для одного разработчика это ненужная вещь, согласен. В компании, где 30.000 сотрудников вещь незаменимая. Может быть я чего-то не знаю, но как я понял, в Python аналогичных конструкций не существует и все держится на добром слове
Не только в Python, но и в Clojure, которые теперь активно продвигает дядя Боб. Тот же Javascript. Все это уже используется активно в энтерпрайз разработке. Ну и если человек видит, что что-то имеет явное указание по соглашению, что это нельзя трогать (то же нижнее подчеркивание в начале название), но берет и игнорирует это, то вероятно такой разработчик в любом случае набедокурит в другом месте, если не в этом. Обратится к чему-то приватному, что обозначено, как приватное, можно только осознано. Если ты хотя бы базово знаком с конвенцией языка, на котором пишешь.
К тому же есть другие способы сокрытия. Через замыкания внутри тех же функций.
Тебе не нужно классическое ООП в твоём бэкенд микросервисе