Почему лучше разделять два метода на два класса при соблюдении принципа единственной ответственности?
Что проверяют
Понимаете ли вы SRP как «одна причина для изменения», а не как «один метод на класс». Вопрос с подвохом: интервьюер ждёт, что вы объясните, из чего следует разделение, и часто дожимает вопросом о том, когда дробить как раз не надо.
Как отвечать
Начните с формулировки: у класса должна быть одна причина меняться, то есть один заказчик изменений. Роберт Мартин уточнял принцип именно так: модуль отвечает перед одним актором.
Разберите на примере из вопроса. Класс с методами generate() и save_to_s3() меняется по двум независимым поводам: аналитика меняет состав отчёта, инфраструктура меняет хранилище. Отсюда практические следствия:
- тесты: чтобы проверить формулу в отчёте, приходится поднимать заглушку хранилища. После разделения генератор проверяется как чистая функция на входных данных;
- зависимости: в одном классе оказываются и правила предметной области, и
boto3. Клиент облака попадает в модуль, где ему нечего делать; - переиспользование: тот же отчёт понадобится отдать в HTTP-ответе. С разделением это просто новый потребитель, без него это флаг в сигнатуре метода;
- связность: если первый метод работает с полями
aиb, а второй сcиd, класс уже фактически распался на две части, осталось это зафиксировать.
Свяжите с остальными принципами: после разделения генератор зависит от абстракции хранилища (Protocol в Python), а не от конкретной реализации, и новый способ сохранения не требует правки старого кода.
Назовите и обратную сторону, за это ставят плюс: дробление ради дробления даёт десяток классов с одним методом и без состояния. В Python такой класс это обычная функция в модуле, а лишний слой косвенности мешает читать.
Где спотыкаются
Пересказывают формулировку «класс должен делать одну вещь», не умея сказать, что считать одной вещью. Критерий это причина изменения, а не размер файла.
Делят по словам, а не по смыслу: UserService, UserManager, UserHelper. Имена разные, причина изменения одна, связность не выросла.
Заводят класс без состояния с единственным методом execute. Это функция, переодетая в объект, и в Python она стоит лишнего импорта, конструктора и уровня вложенности.