Принцип единственной ответственности

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

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

Итак, почему кодовые базы настолько сложны, что нам хочется умереть? Просто потому, что люди, написавшие первую версию, спешили, а те, кто пришел позже, просто продолжали добавлять беспорядок. Конечный результат: лапшекод, к которому мало кто хочет прикасаться и которое никто не понимает.

Добро пожаловать в первый день работы!

Все статьи серии

SOLID принципы на примере Laravel

– Никто не говорил, что это будет так трудно . . .

Но так не должно быть.

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

Эти принципы представлены аббревиатурой SOLID. Возможно, вы слышали о термине “принципы SOLID” раньше, возможно, нет. В случае, если вы только откладывали это обучение на “когда-нибудь”, что ж, давайте убедимся, что сегодня именно тот день!

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

“S ” – Одна ответственность

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

Когда я столкнулся с этим в первый раз, определение, которое мне было представлено, гласило: “должна быть одна и только одна причина для изменения класса”. Я подумал: “что??! Перемены? Какие перемены? Зачем меняться?” именно поэтому я сказал ранее, что если вы прочтете об этом в разных местах, то получите связанные, но несколько разные и потенциально запутанные определения.

Пришло время для чего-то серьезного: Если вы похожи на меня, вы, вероятно, задаетесь вопросом: “хорошо, все хорошо. Но почему, черт возьми, это должно меня волновать? Я не собираюсь начинать писать код в совершенно ином стиле с завтрашнего дня только потому, что так говорит какой-то сумасшедший, который когда-то написал книгу (а теперь мертв).”

Превосходно!

И это тот дух, который нам нужно поддерживать, если мы действительно хотим чему-то научиться. Так почему же все эти песни и танцы о “единственной ответственности” вообще имеют какое-то значение? Разные люди объясняют это по-разному, но для меня этот принцип заключается в том, чтобы привнести дисциплину и сосредоточенность в ваш код.

Давайте рассмотрим пример, прежде чем я объясню свою интерпретацию. В отличие от примеров, найденных на большинстве других ресурсов в интернете, которые вы вроде как понимаете, но все равно удивляетесь и задаетесь вопросом – как они помогут вам в реальных случаях? Давайте погрузимся в что-то конкретное.

Когда приложение Laravel получает веб-запрос, URL-адрес сопоставляется с маршрутами, которые вы определили в web.php и api.php, и если есть совпадение, данные запроса достигают контроллера. Вот как выглядит типичный метод контроллера в реальных приложениях продакшен уровня:

class UserController extends Controller {
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
           'first_name' => 'required',
           'last_name' => 'required',
           'email' => 'required|email|unique:users',
           'phone' => 'nullable'
       ]);

       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->withInput();
       }

       // create new user
       $user = User::create([
           'first_name' => $request->first_name,
           'last_name' => $request->last_name,
           'email' => $request->email,
           'phone' => $request->phone,
       ]);

       return redirect()->route('login');
    }
}

Мы все писали такой код. И легко увидеть, что он делает: регистрирует новых пользователей. Он выглядит прекрасно и прекрасно работает, но есть проблема — он не защищен от будущего. И под будущим я имею в виду, что он не готов справиться с изменениями, не создавая беспорядка.

Почему же так?

Вы можете сказать, что эта функция предназначена для маршрутов, определенных в web.php файле, то есть для традиционных страниц, отображаемых сервером. Проходит несколько дней, и теперь ваш клиент/работодатель получает разработанное мобильное приложение, а это значит, что этот маршрут не будет полезен для пользователей, регистрирующихся с мобильных устройств. Что ты делаешь? Создаёшь аналогичный маршрут в api.php файле и пишешь для него функцию контроллера, управляемую JSON? Ладно, а дальше что? Копируешь весь код из этой функции, вносишь несколько изменений и пользуешься? Это действительно то, что делают многие разработчики, но так они копают себе яму.

Проблема в том, что HTML и JSON – не единственные форматы API в мире. Как насчет клиента, у которого есть устаревшая система, работающая в формате XML? А потом еще один для мыла. И gRPC. И Бог знает, что еще придумают завтра.

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

Но затем приходит удар в живот, Немезида разработки программного обеспечения – изменение. Предположим теперь, потребности вашего клиента / работодателя изменились. Теперь они хотят, чтобы во время регистрации пользователя мы регистрировали IP-адрес, а также добавляли опцию для поля, указывающего, что они прочитали и поняли правила и условия.

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

Упс…

Как мы попали в этот ад?

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

Он делает слишком много вещей! И да, как вы могли заметить, знание того, как создавать новых пользователей в системе, на самом деле не должно быть работой методов контроллера.

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

class UserController extends Controller {
    public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
           'first_name' => 'required',
           'last_name' => 'required',
           'email' => 'required|email|unique:users',
           'phone' => 'nullable'
       ]);

       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->withInput();
       }

       UserService::createNewUser($request->all());
       return redirect()->route('login');
    }
}

Теперь взгляните на код: он гораздо компактнее и понятнее… и самое главное, адаптивно к изменениям. Продолжая наше предыдущее обсуждение, где у нас было десять различных типов API, каждый из них теперь вызывает одну функцию UserService::createNewUser($request->all()); и покончит с ней. Если в логике регистрации пользователя требуются изменения, UserService позаботится об этом, в то время как методы контроллера вообще не нуждаются в изменениях. Если потребуется отправка SMS-подтверждения регистрации пользователя, то UserService об этом позаботится (вызвав какой-то другой класс, который знает, как отправлять SMS), и снова контроллеры останутся нетронутыми.

Вот что я подразумевал под акцентом и дисциплиной: акцент в коде (одна вещь делает только одну вещь) и дисциплина разработчика (не поддается краткосрочным решениям).

Что ж, это была настоящая экскурсия! И мы рассмотрели только один из пяти принципов. Давайте двигаться дальше!

Автор: Анкуш Тхакур 9 ноября 2020 года

Рейтинг
( 6 оценок, среднее 4.17 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Комментарии: 4
  1. WD3

    Здравствуйте. В вашей логике UserService отвечает и за создание пользователя (1 ответственность), и за рассылку уведомлений (2 ответственность), то где здесь SRP? Видимо, ваш UserService отвечает за рассылку на уровне интерфейсов, но это совершенно не SRP. А что если ваш божественный UserService отвечает еще и за выборку пользователей, и за их удаление с соответствующими уведомлениями, и за весь спектр работ с моделью User, то это точно не SRP. И почему сервис напрямую обращается к модели User? Да, eloquient позволяет. Но что если нужно написать юнит тест? Как вы протестируете создание юзера и рассылку уведомлений? Все “будет” в одном тесте? Чем вы подмените модель User? Я вам советую пересмотреть ваше видение SRP, а так же прекратить писать божественные сервисы и обратить внимание на классификацию сервисов в ДДД. Потому что я на 100% уверен, что UserService совершенно не ООП, а набор разрозненных функций, построенных вокруг модели User. Такое часто бывает в мире ларки. Но если вы начали писать статью про солид, то разберитесь в основах хотя бы. И простите за резкий комментарий.

    П.С. $validator = Validator::make в методе контроллера совершенно не SRP. Вот если вынести проверки в отдельный реквест, это похоже на SRP.

    П.П.С. Вопрос вам: в чем различие и что предпочтительнее UserService::createNewUser($request->all()); или UserService::createNewUser($request->validated());

    1. Maxyc Webber (автор)

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

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

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

  2. Артур Ким
  3. Артур Ким

    Хотя даже выше приведенный пример не верный. сделать метод в кастом реквесте, который вернет валиднай DTO, а в контроллере вызывать метод, передавайть ДТО в метод сервиса – профит

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.