Holy Python
507 subscribers
28 links
Станьте "папой" питона!
Download Telegram
Введение

Наверняка если вы изучаете питон вы слышали об одной достаточно известной аббревиатуре - GIL,
если вы немного углублялись в тему вы могли узнать что это штука как-то связана с потокам

Сегодня мы попробуем разобраться что это, как это работает и зачем это нужно питону.

GIL

GIL(Глобальная блокировка интерпретатора) - это один из способов синхронизации потоков, который позволяет только одному потоку управлять интерпретатором Python. Задача GIL сделать интерпретатор CPython потокобезопасным.

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

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

Да, это очень резонные вопросы к данному подходу, GIL и вправду очень часто является "бутылочным горлышком" производительности многопоточных программ. Однако данный механизм не был добавлен просто так, дело в том, что GIL решает одну довольно серьёзную проблему о которой мы поговорим ниже.

Проблема гонки

У каждого объекта в питоне есть свой "счётчик ссылок", который отслеживает количество ссылок указывающий на данный объект.

Если счётчик = 0, то память занятая объектом высвобождается.

Пример:


import sys

x = 3
a = x
print(sys.getrefcount(x)) # 38 refs
del a
print(sys.getrefcount(x)) # 37 refs

Проблема в том что несколько потоков могут одновременно изменять значение одного счетчика(или обращаться к одному и тому же участку памяти), это может привести к разным непредвидемым последствиям(например утечки памяти).

Именно эту проблему в случае питона и решает GIL.

Если нет параллельных потоков, то и никаких проблем с ними не будет)

Как работает GIL?

Когда какой-либо поток захватывает GIL, остальные потоки которые хотят захватить его вынуждены ждать. Переключение между потоками может происходить: когда активный поток пытается осуществить ввод-вывод, по исчерпании лимита выполненных инструкций, либо по таймеру

Уточнения: Переключения между потоками создают издержки, это основная причина почему многопоточные программы могут быть медленнее однопоточных.

Почему в питоне используется именно GIL и его не удалили/заменили?

1. GIL очень прост в реализации
2. GIL повышает производительность однопоточных программ.

Есть достаточное количество решений проблемы гонки, однако они либо снижают производительность однопоточных и/или многопоточных программ, либо являются слишком сложными в реализации.

Плюс, даже если будет найдено адекватное решение проблемы гонки, которое удовлетворяет всем требованиям питона, оно сломает существующие расширения на C, которые зависят от GIL.

Достаточное количество разработчиков предпринимали попытки убрать GIL, однако ничего толкового из этого не вышло.

Дополнительные материалы

https://wiki.python.org/moin/GlobalInterpreterLock
https://towardsdatascience.com/python-gil-e63f18a08c65
👍73👎1
#паттерны

Введение

Сегодня мы рассмотрим паттерн проектирования "Абстрактная фабрика".

Классификация

Тип: Порождающий

Определение: Абстрактная фабрика - это порождающий паттерн проектирования, который предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов.

Грубо говоря, абстрактная фабрика - это "фабрика фабрик", данный паттерн позволяет решить проблему создания целых семейств связанных объектов, без указания конкретных классов продуктов.
С помощью абстрактной фабрики вы можете предоставить библиотеку объектов не расскрывая их реализацию.

Из чего состоит и как работает данный паттерн

1. Абстрактного класса/Интерфейса абстрактной фабрики. Содержит абстрактные методы которые возвращают абстрактные продукты, связанные одной конпцецией.

class AbstractFactory(ABC):
@abstractmethod
def create_product_a(self):
...

@abstractmethod
def create_product_b(self):
...


2. Конретные фабрики. Конкретные фабрики реализут операции которые создают конкретные продукты.

class ConcreteFactory1(AbstractFactory):
def create_product_a(self):
return ConcreteProductA1()

def create_product_b(self):
return ConcreteProductB1()

class ConcreteFactory2(AbstractFactory):
def create_product_a(self):
return ConcreteProductA2()

def create_product_b(self):
return ConcreteProductB2()


3. Абстрактные классы продуктов. Реализуют интерфейс для всех конретных продуктов своего семейства.

class AbstractProductA(ABC):
@abstractmethod
def very_important_super_function_a(self) -> str:
...

class AbstractProductB(ABC):
@abstractmethod
def very_important_super_function_b(self) -> str:
...


4. Конкретные продукты. Реализуют абстрактные продукты. Продукты одного семейства не могут взаимодействовать, с продуктами другого семейства.

class ConcreteProductA1(AbstractProductA):
def very_important_super_function_a(self) -> str:
return "Product A1"


class ConcreteProductA2(AbstractProductA):
def very_important_super_function_a(self) -> str:
return "Product A2"

class ConcreteProductB1(AbstractProductB):
def very_important_super_function_b(self) -> str:
return "Product B1"


class ConcreteProductB2(AbstractProductB):
def very_important_super_function_b(self) -> str:
return "Product B2"


5. Клиент. Клиентский код работает исключительно с абстрактной фабрикой и абстрактными продуктами.

def client(factory: AbstractFactory) -> str:
return factory.create_product_a().very_important_super_function_a()

for factory in (ConcreteFactory1, ConcreteFactory2, ...):
client(factory)


Плюсы данного паттерна

1. Реализует принцип открытости/закрытости.

2. Упращает поддержку кода.

3. Выделяет код производства продуктов в одно место, упрощая поддержку кода.

Минусы данного паттерна

1. Снижает читаемость программы из-за введения множества дополнительных классов.

Пример и задача

Дед Мороз и Пасхальный кролик не успевают сделать игрушки к празднику, которые они будут дарить детям.
Все существует 3 вида игрушек:

1. Лошадка
2. Зайчик
3. Шарик

Всё было бы просто однако Деду Морозу нужны игрушки в новогоднем стиле, а пасхальному кролику в пасхальном.
Создайте абстрактную фабрику игрушек и спасите эти праздники!

Пример из реального кода

Пример из реального кода предоставил @Tishka17. Он разработал замечательный фреймворк aiogram_dialog для разработки интерактивных диалогов и меню в телеграмм ботах, как обычное приложение с графическим интерфейсом.

Вот здесь он использует данный паттерн: https://github.com/Tishka17/aiogram_dialog/blob/develop/aiogram_dialog/manager/manager_middleware.py#L23

Вот его объяснение, какую задачу он решает в данном случае:
5👍3👎1
В библиотеке aiogram dialog есть диалог менеджер, он реализует базовую функциональность управления диалогами: старт разным способом, доступ к контексту и т.п.
Также есть менеджер - это временный объект. Он создаётся на время обработки конкретного события. Соответственно, его надо постоянно пересоздавать.
В какой-то момент я решил что неплохо дать возможность что-то в менеджере переопределить, соответственно нужно и фабрику менять.
Для решения этой проблемы абстрактная фабрика подошла лучше всего.
10👎1
6👍2👎1
Введение

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

Сегодня мы разберёмся, как правильно искать и исправлять ошибки в программе.

План исправления ошибки

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

0. Проверьте правильность "ожидаемых данных", возможно программа делает все правильно, а ошибка содержится в них.

1. Стабилизация ошибки(если возможно) - некоторые ошибки возникают не постоянно, их нужно стабилизировать - сделать так чтобы одна и таже ошибка появлялась всегда.

2. Поиск места возникновения ошибки
    a. Пробуем первые входные данные, анализируем выдвигаем гипотезы. Создаем новые входные данные, передаём в программу, на основе всех результатов ищем корреляции, проводим дебаг и выдвигаем гипотезы.
   b. Выдвигаем гипотезы, на основе предыдущих данных, тестируем, если гипотеза подтвердилась ищем причину возникновения ошибки путем дебага, затем приступаем к пункту 3 иначе повторяем процесс.

3. Исправление ошибки. Мы уже знаем проблемное место - 80% работы выполнено, теперь остаётся исправить эту ошибку.

4. Тестируем новое решение.

5. Проверьте все места в проекте, где использовался исправленный вами код. Хорошо спроектированая система не пострадает, однако в большинстве реальных проектов могут появиться побочные эффекты.

6. Ищем схожие возможные проблемы в программе.

Типичные антипаттерны

1. Виноваты все кроме моей программы. Иногда люди начинают обвинять все подряд кроме своей программы(интерпретатор, библиотеку, IDE). И так бывает очень сложно найти и исправить баг в программе, но если ты считаешь что проблема не ней, то это вообще не реально.

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

3. Поиск гаданием. Вместо применения научного метода в данном случае человек расставляет принты и "трай эксепты" по всей программе, а потом гадает где же проблема. Крайне не эффективный подход.

P. S. Если вы знаете, как дополнить пост, в частности пункт 1 и часть "типичные антипаттерны", добро пожаловать в комментарии.
👍64
#ошибки

Введение

Сегодня мы поговорим о минусах и вреде такого известного фреймворка как Django.

Немного уточнений:

1. Этот пост был сделан для того чтобы вы ознакомились с минусами данного фреймворка, я не заставляю вас не использовать его.
2. С данным фреймворком ознакомится в любом случае придется, так как бизнес все ещё его использует, однако предлагать ее внедрение в новый проект - плохая идея.
3. Если начать с джанги и не смотреть в сторону других фреймворков, будет складываться ощущение, что в ней все сделано правильно.

Проблемы архитектуры

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

1. Нарушены многие принципы написания кода и архитектуры(SOLID, CCP, принцип ацикличности зависимостей)
2. Используются некоторые антипаттерны(например: глобальные переменные, хранение настроек ввиде большого количества переменных в одном файле, подробнее о таких антипаттернах в: https://t.me/holy_python/46)
3. Сильная связность компонентов, отсутствие гибкости, зависимость от преждевременных решений. При замене какого-либо решения используемого Django, сломаются другие компоненты. Пример: Если мы поменяем Django ORM на SQLAlchemy, то сломается Django Admin.

Проблемы ORM

Django ORM, содержит массу серьёзных проблем, которые усложняют разработку.

Отсутствует:

1. Иерархические и рекурсивные запросы в SQL(CTE)
2. Возможность кастомизации group by
3. Виртуальные ключи
4. Возможность разделения таблиц на группы.
5. Виртуальные таблицы
6. Триггеры(особые разновидности хранимых процедур, которые автоматически выполняются при возникновении события на сервере базы данных)
7. Функции
8. Поддержка only по умолчанию
9. Асинхронность для БД(пытаются добавить ввиде костылей в 4.x)

Другие проблемы:

1. Используется спорный паттерн ActiveRecord. Ведёт к нарушению принципа SRP, сильно связан с бизнес логикой. Если вы захотите использовать другую абстракцию для хранения данных придётся проводить рефакторинг.

Заключение

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

P. S. Если вы знаете другие значительные минусы Django добро пожаловать в комментарии.

Дополнительные материалы

https://www.djangoproject.com
https://habr.com/ru/post/198450
https://t.me/rudepython/168523
👍143🔥1
#чистаяархитектура

Введение

Сегодня я хотел бы поговорить о трёх, часто забываемых принципах чистой архитектуры. Речь пойдёт о триаде принципов для компонентов(минимальных единиц развёртывание, с точки зрения питона, это один/несколько файлов с колом): CCP, CRP, REP.

CCP

Расшифровка: Common Closure Principle – принцип согласованного изменения.

CCP - это перефразированый SRP только для компонентов. Данный принцип требует, чтобы компонент имел единую ответственность и не имел несколько причин для изменения. Если два класса тесно связаны, физически или концептуально, настолько, что всегда будут изменяться вместе, они должны принадлежать одному компоненту.

Давайте кратко сформулируем этот принцип:

Собирайте вместе все, что изменяется по одной причине и в одно время. Разделяйте все, что изменяется в разное время и по разным причинам.

CRP

Расшифровка: Common Reuse Principle - принцип совместного повторного использования.

CRP - указывает, что в компонент должны включаться классы и модули, используемые совместно.
Данный принцип предполагает, что в компоненте должны быть классы, имеющие множество зависимостей друг от друга. Как пример абстрактные фабрики и их фасад.

Давайте кратко сформулируем этот принцип:

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

REP

REP - Release Equivalence Principle - Принцип эквивалентности повторного использования и выпуска. Данный принцип означает, что эффективно переиспользовать можно только компоненты, релизнутые через системы версионирования. Без номера версии невозможно гарантировать совместимость всех компонентов, а разработчики не смогут чётко увидеть когда версия появилась и какие изменения в ней произошли. С точки зрения архитектуры принцип означает, что классы и модули, составляющие компонент, должны принадлежать связной группе. Очень похоже на предыдущие принципы, не так ли? Но тут появляется ещё одно условие. Классы и модули, объединяемые в компонент, должны выпускаться вместе.

Давайте кратко сформулируем этот принцип:

1. Единица повторного использования = единице выпуска
2. Эффективно можно использовать только компоненты, имеющие номер версии.

Противоречия

Внимательный читатель заметил что все 3 принципа одновременно соблюсти невозможно. Принципы CCP и REP стремятся сделать компонент как можно мельче, а принцип CRP как можно крупнее. Наша задача понять когда, какой принцип важнее. Для этого я выделил набор правил.

1. В фреймворках/библиотеках должен соблюдаться именно CCP, соблюдение CRP приведёт к каше, так как в фреймворках/библиотеках важнее всего чёткость компонентов и структура.
2. В больших проектах соблюдение CCP приведёт к слишком большому количеству компонентов, что только усложнит разработку.
3. На начальной стадии развития проекта стоит соблюдать CCP, а в дальнейшем постепенно смещать фокус на REP и/или CRP.
4. Слишком много зависимостей между компонентами приводит к их неустойчивости(об этом в следующем посте), что усложняет разработку.

Заключение

Чтобы хорошо спроектировать свой проект пользуйтесь выше перечисленными правилами и старайтесь искать золотую середину между этими принципами.

Дополнительные материалы

https://books.google.com.tr/books/about/%D0%A7%D0%B8%D1%81%D1%82%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%98%D1%81.html?id=d6JSDwAAQBAJ&printsec=frontcover&source=kp_read_button&hl=ru&newbks=1&newbks_redir=0&gboemv=1&redir_esc=y
👍92
Хотите ли вы на канале видеть статьи от контрибьютера cpython @backinblacknext?
В частности, статьи про эталонную реализацию интерпретатора питона - CPython
Anonymous Poll
79%
Да
21%
Нет
By: quantum super position, MVC

Введение

Сегодня мы научимся добавлять свою функцию в builtins¹ эталонной реализации языка программирования Python - CPython.

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

В builtins присутствует функция abs, её задача - вызывать у объекта dunder-method(магический метод) abs.

Обычно abs используют для того, что бы вернуть модуль какого-то числа.

Помимо dunder-method'a abs у числовых типов обычно присутсвует neg, который отрицает число.

Но в builtins функции neg которая бы вызывала этот dunder нет!

Именно реализацией этого мы с вами сегодня и займемся.

Замечание: Стоит отметить, что на самом деле number может вызывать neg, но отдельной функции под это нет. Видимо это сделанно из соображений красоты.

Реализация

Начнём с установки. Склонируем репозиторий интерпретатора:

git clone --depth 1 --branch 3.11 https://github.com/python/cpython

Затем перейдем в директорию cpython:

cd cpython

Теперь откроем Objects/abstract.c и реализуем функцию PyNumber_Negate:

PyObject * PyNumber_Negate(PyObject *object) {
if (object == NULL) {
return null_error();
}
PyNumberMethods *methods = Py_TYPE(object)->tp_as_number;
if (methods && methods->nb_negative) {
PyObject *res = methods->nb_negative(object);
assert(_Py_CheckSlotResult(object, "neg", res != NULL));
return res;
}
return type_error("bad operand type for neg(): '%.200s'", object);
}

Эта функция получается в виде аргументов указатель на PyObject,
далее проверяем, не указывает ли object на NULL
Следущим шагом мы получаем методы object.
"if" здесь нам нужен что бы проверить, есть ли метод nb_negative и neg у объекта.
Если таковых нет, то возвращаем исключение type_error("bad operand type for neg(): '%.200s'", object)

Иначе возвращаем ng_negative объекта, можно считать что это neg.

Теперь откроем заголовочный файл Include/abstract.h и объявим наш PyNumber_Negate:

PyAPI_FUNC(PyObject *) PyNumber_Negate(PyObject *o);

Регистрация

Для начала откроем Python/bltinmodule.c и реализуем функцию builtin_neg:

static PyObject *
builtin_neg(PyObject *modle, PyObject *x){
return PyNumber_Negate(x);
}


Это и есть реализация нашего будущего neg.
Она вызывает функцию PyNumber_Negate, которая в свою очередь и делает все проверки, и в случае их успеха, возвращает нам neg.

Затем открываем файл Python/clinic/bltinmodule.c.h, тут мы обьявим макрос BUILTIN_NEG_METHODDEF.

Но перед этим стоит написать небольшую документацию нашего метода, которая будет отображаться при вызове help(neg):

PyDoc_STRVAR(builtin_negdoc,
"neg($module, x, /)\n"
"--\n"
"\n"
"Return the negate value of the argument.");

Теперь объявим наш макрос:

#define BUILTIN_NEG_METHODDEF \
{"neg", (PyCFunction)builtin_neg, METH_O, builtin_negdoc},
В дефайне макроса, как мы видим мы передаём название билтина neg, и функцию реализующую его (PyCFunction)builtin_neg
Теперь вернемся к Python/bltinmodule.c
Здесь нам нужно найти static PyMethodDef builtin_methods[], и добавить в него наш макрос BUILTNI_NEG_METHODDEF
После добавления, static PyMethodDef builtin_methods[] будет выглядеть как-то так:

static PyMethodDef builtin_methods[] = {
{"build_class", _PyCFunction_CAST(builtin_build_class),
METH_FASTCALL | METH_KEYWORDS, build_class_doc},
BUILTIN_IMPORT_METHODDEF
BUILTIN_ABS_METHODDEF
BUILTIN_ALL_METHODDEF
BUILTIN_ANY_METHODDEF
BUILTIN_ASCII_METHODDEF
BUILTIN_BIN_METHODDEF
BUILTIN_NEG_METHODDEF // а вот и наш макрос!
// здесь еще куча макросов и остального
};

Мы добавили нашу функцию в builtins питона! Осталось всё это запустить.

Сборка

Если у вас UNIX like OS, то для того что бы начать сборку вам нужно ввести в терминал следующее:
./configure --with-py-debug
Флаг --with-py-debug означает что мы собираем Python для дебага, и там не будет некоторых оптимизаций, что, конечно, ускорит сборку.
Далее вводим make -j 2 за что отвечает -j 2 можно прочитать в man make.
👍64
Если же у вас Windows, нужно ввести следующее:
PcBuild/build.bat -c Debug
Здесь мы опять же собираем питон с флагом -c Debug, что бы сборка была быстрее.

Замечание: Сборка процесс не быстрый. Эти команды могут выполняться достаточно долго, всё зависит от вашего железа.

Протестируем то что у нас получилось

создадим в корне директории cpython файл test_interpreter.py, и протестируем наш neg()

help(neg)

Запустим наш test_interpreter.py с помощью нашего нового интерпретатора
./python test_interpreter.py
Help on built-in function neg in module builtins:

neg(x, /)
Return the negate value of the argument.
Прекрасно! Та самая небольшая документация что мы писали, работает.
Теперь изменим test_interpreter.py:
print(neg(1))
print(neg(-1))

./python test_interpreter.py
-1
1
Отлично. Всё работает как нужно. Теперь давайте проверим как это будет работать
с классами, которые определит пользователь.
class SomeClass:
def init(self, number: int) -> None:
self.number = number
def neg(self):
return -2 * self.number
Мы реализовали neg у класса, который будет возвращать -2 * number
Давайте проверим!

test = SomeClass(5)
print(neg(test))

./python test_interpreter.py
-10
Чудно! Всё работает ровно так как мы ожидали.
Теперь, попробуйте реализовать какую-нибудь не столь бесполезную функцию как та что описана в статье.

1. builtins - встроенные в интерпретатор функции и типы, которые всегда доступны, например print или bool

Дополнительные материалы

https://github.com/python/cpython

Если у вас возникли трудности, вы можете взглянуть на этот форк CPython: https://github.com/Eclips4/cpython_articles/tree/3.11
В частности, на этот(https://github.com/Eclips4/cpython_articles/commit/97965a553096e1e6121ec032ab26a2794a40f315) коммит.
👍9👎3🥰2🎉2🔥1
👎2
#DDD

Введение

Похоже придётся написать пост про это. DDD - набор принципов, направленный на создание оптимальных систем объектов. Так как это не образовательный пост, а скорее пост, "на подумать" и о формирование вашего стека(неориентированный на новичков), я не буду вдаваться в подробности, вы можете сами почитать, что это. Сегодня мы обсудим полезность и реалистичночть DDD, с точки зрения времени на изучение, бизнеса реализации и сопровождения/поддержки.

Время на изучение

Времени на изучение DDD требуется невероятно много, тема крайне обширная и сложная. Чтобы полностью разобраться в DDD, придётся прочитать массу сложных статей и книг. В следствие данных факторов, мы видим крайний недостаток кадров. Кроме того перед изучением DDD, очевидно нужно понимать принципы построения архитектуры, принципы SOLID, CA и т.д. Что ещё усложняет ситуацию.

Реализация и сопровождение

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

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

Бизнес

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

Выводы

DDD, даже с учётом приличного возраста имеет массу проблем и не подходит для большинства.

Но, хочу отметить, что в данном подходе, есть реально полезные идеи, которые можно применить при проектировании классической гексагональной архитектуры, что может реально принести пользу, поэтому полностью отметать его не стоит. Хотя также не желательно устанавливать его как номер 1 в списке, желаемого для изучения.
👍15👎91🔥1
#рефакторинг

Введение

Сегодня мы поговорим, о рефакторинге, его основных идеях и антипаттернах.

Напомню, что рефакторинг - это изменение программного кода/системы, при котором не меняется поведение, а улучшается его внутренняя структура.

Принципы рефакторинга

1. Когда вы производите рефакторинг, вы не стараетесь изменить поведение и привнести в функционал что-то новое.

При проведение рефакторинга мы стремимся улучшить структуру кода. Добавление новых тестов, функционала и в целом компонентов - не относится к рефакторингу.

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

2. Применяйте рефакторинг активно при добавление "новой функции".

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

Когда вы пытаетесь понять, что делает некий код - всегда задавайте себе вопрос, а не могу ли я сделать структуру более очевидной?

3. Сообщение об ошибки - признак необходимости рефакторинга.

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

Работа с крайне запутаным и неусточивым кодом

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

Однако бизнес, размеры проекта, в конце концов нехватка времени, могут не позволять переписывать все с нуля. Что делать в таком случае?

Я предлагаю следующую тактику в 3 шага:

1. Инкапсулировать ключевые компоненты, например сделать их сервисами. Избавиться от связности. При этом постараться соблюсти принцип ацикличности зависимостей.
2. Логику, которая будет работать с ключевыми компонентами написать с нуля или провести ее рефакторинг.
3. Провести рефакторинг или переписать компоненты, которые этого требуют.

Быстрый рефакторинг не возможен

Основным антипаттерном рефакторинга является желание провести его быстро.

Вы не сможете быстро провести рефакторинг какого-либо проекта, даже если кажется иначе. Любой рефакторинг должен проводиться постепенно! В ином случае рефакторинг может привести только к усложнению кода и множеству ошибок.

Дополнительные материалы

Почитайте книжку, рефакторинг от Фаулера.
👍15🤔4👎3
#DDзвери

Введение

Сегодня я начинаю цикл статей, о достаточно редко обсуждаемых темах - TDD, MDD, BDD и др. Начнём мы с философски крайне правильной идеи построения, разработки и проектирования приложения - TDD(Test Driven Development).

Структура TDD

Сам TDD состоит из набора небольших циклов разработки:

1. Пишем тест, который заранее покрывает желаемое изменение или работу компонента
2. Реализуем изменение или компонент удволитворяющий условиям теста
3. Проводится рефакторинг с целью улучшения написанно кода

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

Плюсы данного подхода

1. Ваши тесты - чёткая спецификация вашей программы до и после реализации компонентна/программной фичи, для которой писался тест
2. Имея ограничения по тестируемости¹ вы задаете себе афинные ограничения, которые не позволяют вам использовать набор плохих программных идей(приводящих к невозможности тестирования) при реализации нужного вам поведения. Это также отражается на правильности вашей архитектуры.
3. Исследования показали, что разработка, основанная на тестировании, может привести к снижению ошибок на 40-80%.
4. В целом многие разработчики упускают покрытие тестами уже после реализации нужного им поведения, что очевидно сказывается плохо на разработке. Данный подход вынуждает вас писать тесты.
По итогу вы добьётесь хорошего покрытия.

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

Дополнительные материалы

Книжка Кента Бэка по TDD
👍73🔥1
#DDзвери

Введение

Сегодня мы продолжим обсуждать TDD. Рассмотрим бизнес версию TDD - BDD. И обсудим некоторые тонкие моменты, которые возникли в прошлой статье.

Покрытие тестами после

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

BDD или TDD на русском

Неким ответвлением/улучшением TDD является BDD, который предназначен больше для аналитиков и тестировщиков. В данном случае предлагается описывать пользовательские сценарии на естественном языке не вдаваясь в детали реализации, язык программирования и т.д.

Структура BDD

1. Описываем предполагаемый функционал
2. Переводим его в тесты(часто автоматически)
3. Далее все идентично TDD

Плюсы данного подхода

Главным плюсом является сближение заказчиков, аналитиков, тестировщиков, продактов и программистов. Полученое описание кроме как спецификации является некой документацией. Мы получаем тесты не для программистов. Мы независим ни от языка, ни от технологии.

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


Пример описания для BDD

Feature: e-mail notifier

In order to avoid delays on meetings
As a lazy HR
I want to automatically send notifications to all employees

Scenario: Send notification
Given I have entered notification text
When I pressed enter
...
Then the result is shown on all employees screen


Тонкий момент

Основной проблемой BDD как и у многих DD подходов является дороговизна. Для реализации правильного BDD тестировщики будут привлечены ещё на этапе построения задач, что удлиняет цикл разработки.

Чтобы избежать проблемы дороговизны, требуется колоссальный опыт и очень правильное выстраивание процессов разработки, что очевидно возможно не всегда.
🔥6👍1
Про чрезмерное дробление

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

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

1. Если элементы, реализующие концептуальные объекты, оказываются раздробленными и по своей физичности разделены, то они перестают выражать модель
2. Со временем при добавление новой функциональности и более сильном дробление мы перестанем улавливать суть написанного кода.
3. Чем больше мы разбиваем, тем больше зависимостей мы создаём.

Нужно стараться упрощать свою архитектурную среду, уменьшать количество раздробленных объектов и связанных компонентов. Когда ты видишь проект названный CleanArchTemplate содержащий по 10 dto, сложные раздробленные фасады и совсем неулавливаемую связь между компонентами будь то в presentation слое или в реализации гейтвеев, то можно открыто говорить, что данный код не выражает ничего. Да, он построен согласно определённому набору возможно верных принципов, но из-за сильного дробления его поддержка оказывается невозможной.

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

Если нет реальной необходимости распределять код между разными серверами, храните весь код, реализующий один концептуальный объект, в одном модуле, а то и классе.

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

Вспоминаю треугольник CCP-CRP-REP. Правильнее всего превратить его в отрезок CCP-CRP. И располагать компоненты стараясь быть ближе к середине. Такой подход позволит не уходить в чрезмерную раздробленность по компонентам и по пакетам в целом.

Обобщая сказанное выше: Использовать разбиение на пакеты стоит только для того, чтобы отделить уровень предметной области от остального кода. Оставляйте больше свободы в целом, для того чтобы можно было объединить пакеты и компоненты так, что код будет выражать модель наилучшим образом.
🔥8🥰1
Common, utils, shared

Все когда-нибудь сталкивались с пакетами из набора файлов или и того хуже одним файликом с названием utils. Сегодня мы обсудим подобные сущности и их минусы.

utils

Если подумать об ответственности и необходимости подобных сущностей мы можем заметить, что основной причиной их создания является необходимость обслуживать определённый набор объектов, компонентов и т.п. Например часто под utils прячется некая математика или функции для формата.

Минусы данного подхода очевидны. Таким образом вы задаете модель работы вашего когда по средствам вызова методов из определенного набора файлов лежащих в одном пакете. Со временем в utils появляется все больше файлов и в конечном итоге это приводит к свалке. Усложняется процесс тестирования кода и в целом какой-либо ориентации по нему. Utils - стоит рассматривать не как набор компонентов, а как единый компонент или единый объект, который к сожалению очень напоминает GodObject и нарушает все CC принципы.

Если все же есть необходимость создавать подобные сущности то делайте их как можно меньше и более явно выделяйте какие задачи они решают. Ваш utils должен выражать задачу которую он решает чётко, а не быть нечто смежным.

Кроме того, если вы думаете, что в вашем проекте подобных свалок нет, то вы можете ошибаться. Utils - всего лишь концепция при чем достаточно посредственная. Сейчас стало популярно называть старый добрый utils другими именами, типа shared, utility, services и т.п.

common

common же решает несколько иную задачу. common - общее для нескольких модулей или компонентов. Например общие протоколы или интерфейсы. Их создавать очень даже стоит дабы это общее выделять и упрощать работу с кодом. При этом следует выполнять такой же принцип, который действует и на utils. common - не должны быть большими и объединять много всего. Например common в корне проекта не очень хорошая практика. В common не должны появляться функции которые использует что-то одно и из этого следует важное правило. common не должен становиться utils!
5👍3
#DDзвери

Введение

Раннее мы уже рассмотрели достаточно интересную для бизнеса парадигму - BDD. В данном очерке предложена её несложная модификация.

Рассмотрим кусочек спецификации:

When I press enter call my mom

В целом для продактов, аналитиков и заказчиков тут все очевидно. Они даже сами могут подобное написать. Но это не совсем очевидно разработчику. Какой тест писать под слово call?

Чёрно-белая разметка

Разделим специфецирование по BDD на 3 стадии - белую, черную и результирующую.

На белой стадии, аналитик, пм или кто-либо ещё описывает функционал интерактора или всего приложения поверхностно, при этом на моменты типо "call" ставит "флажки".

На чёрной стадии за полученный BDD текст садится группа разработчиков и идёт по флажкам и описывает эти black box'ы оставляя вместо флажка ссылку на глубокую спецификацию функционала.

На результирующей стадии садится 2-ая группа разработчиков и описывает тесты по полученной разметке. Получая полную спецификацию тестами с которой уже удобно работать.
👍4🔥2
Стандартные проблемы DDD и CQRS дизайна

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

отсутствие ограниченных контекстов

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

getters/setters в рамках моделей предметной области

Стандартное недопущение. Происходит некая "деградация" и модели предметной области превращаются в модели данных, что достаточно критично для архитектуры и DDD подхода.

Напоминаю, что модели предметной области сосредоточены на транзакционной инвариантности и границах, а модели данных на данных! Чаще всего чтение данных происходит в соотношение 10 к 1 по отношению к записи и при этом в контексте масштабипования, чтение данных обычно масштабируется легче, чем запись.

отсутствие отдачи инвестиций

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

отсутвие понятия событие и журнала событий

Журнал событий позволяет вернуться назад во времени и восстановить объекты домена. Источники событий позволяют сохранять информацию о событиях и возвращать её в прошлое. Их ценность возможность взглянуть в будущей. Отсутствие такого журнала приводит к загрузке объектов домена. Особенно такой подход удобен при тестирование и отладке CQRS.

Как подпункт, можно вспомнить memento снимки, для сохранения событий и состояний, что упрощает рефакторинг доменных объектов.

CRUD и DDD

Тупой CRUD - не про DDD! В домене будут присутствовать всего 4 "глагола" Create Read Update Delete. Не только ни о каком domain export может идти речь, но и возникают вопросы с намеренностями клиента. Подобная система вместо того чтобы сказать что нам нужно сохранить, будет говорить нам что нужно делать

Главная проблема

Но главной проблемой является то что DDD/CQRS используются не по месту, при этом все мы понимаем, что это дорого. А в мелких проектах тем более крайне не нужно и вредно! Не занимайтесь оверинженерингом.

Про подобные моменты в дизайне можно почитать у моего коллеги Senior Staff из Apple @maxstrakh - вот здесь.
🔥7👍4🤩1
Evil that you need

Вы используйте pydantic? Скорее всего да, ведь он стал наряду с FastAPI основной больших объёмов легаси. Однако на самом деле, он является таким же злом как какая-нибудь джанга, но злом необходимым. Сегодня мы поговорим про одну из самых перспективных альтернатив пайдайнтика на данный момент.

Few points about pydantic

У пайдантика можно выделить ряд проблем на разных "уровнях".

Начать стоит конечно с вопросов к архитектуре. В рамках пайдантика становится невозможным разделение на слои, появляется сильная связность. Смешивается представление, описание модели, пайлантик лезит даже в домен. Нарушаются и многие классические Фаулеровские принципы(Невозможно построить честную ЧА с пайдантиком).

Следующим и достаточно очевидным недостатком является скорость. Pydantic 2 превратил "перепись на rust" в пафосную кампанию нежели реальные оптимизации, если мы обратимся к бенчмаркам предложенным @reagento_ru, то выяснится, что почти все альтернативы все также обгоняют pydantic. Более того, давайте копнем ещё глубже!

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

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

Конечно стоит отметить и документацию её вообще иногда бывает страшно читать))

В заключение можно сказать, что инструмент крайне сомнительный и по возможности стоит переходить на альтернативы типа adaptix(О нем подробнее можете поспрашивать в @reagento_ru). Но к сожалению, пайдантик легаси думаю будет ещё долго мучить нас как разработчиков.

В комментариях предлагаю накидать минусы пайдантика, которые нашли вы.
👍10