Юнит тесты — обязательная часть моих проектов. Это база, к которой добавляются другие виды тестов. В статье Тестирование и экономика проекта я рассказал почему тестирование выгодно для экономики проекта и показал, что юнит тестирование лидирует с экономической точки зрения. В комментариях было высказано мнение, что тестирование требует больших усилий, и даже юнит тестирование неприемлемо из-за этого. Одной из причин этого является неопытность команды в тестировании. Чтобы написать первую тысячу тестов команда тратит много времени, пробуя и анализируя различные подходы.
В этой статье я расскажу о лучших практиках, к которым я пришел за более чем 10 лет тестирования различных проектов. Эти практики позволят начать юнит тестирование без заметного снижения производительности программистов.
Я определяю юнит тестирования как тестирование одного продакш юнита в полностью контролируемом окружении.
Продакшн юнит — это обычно класс, но может быть также и функция, и файл.
Важно, чтобы юнит соответствовал принципал SOLID, в этом случае юнит тесты будут лаконичными. Юниты узкоспециализированны и очень хорошо выполняют одну конкретную задачу, для которой они созданы. В большинстве случаев юниты взаимодействуют друг с другом, делегируя выполнение специализированных задач.
Полностью контролируемое окружение — это окружение имитирующие среду, в которой юнит работает в программе, но полностью открытое для тестирования и настройки. Поведение окружения задается для конкретного тестового кейса через лаконичный API и любое поведение вне этого кейса для него не определено. В этом окружении не должно быть других продакшн юнитов, иначе возрастает сложность тестов и из юнит тестирования мы переходим к интеграционному тестированию.
Постарайтесь не применять наследование. Вместо него используйте композицию зависимостей. Часто наследование применяют для реализации принципа DRY (don’t repeat yourself), вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid), увеличивая сложность юнитов.
Если посмотреть на юнит тест, то для большинства можно четко выделить 3 части кода:
Arrange (настройка) — в этом блоке кода мы настраиваем тестовое окружение тестируемого юнита;
Act — выполнение или вызов тестируемого сценария;
Assert — проверка того, что тестируемый вызов ведет себя определенным образом.
Этот паттерн улучшает структуру кода и его читабельность, однако начинать писать тест нужно всегда с элемента Act.
Прежде чем продолжить рассмотрение структуры теста, я хотел бы рассказать немного о подходе, который я называю Driven Approach. К сожалению, я не могу перевести это лаконично на русский язык. Поэтому просто расскажу, что за этим стоит, и может быть, вы поможете с лаконичным переводом.
Суть в том, что код, который вы пишите, должен иметь причину своего существования. Важно, чтобы причина была существующей, а не предполагаемой, и эта причина должна иметь в конечном итоге связь с бизнес историей.
С чего мы начинаем разработку конкретного функционала? — с требований бизнеса, которые типично выглядят так: “Пользователь с любой ролью должен иметь возможность создать запись, таким образом, он выполнит такую то бизнес операцию”.
Используя driven approach первое что мы должны сделать —
Как видите, существование каждого этапа реализации обусловлено существованием предыдущего этапа. И каждый этап, даже самый последний — создание записи в таблице базы данных имеет связь с изначальной бизнес историей. Каждый этап реализуется только в том объеме, который нужен предыдущему этапу.
Данный подход позволяет небольшими шагами реализовывать сложные бизнес истории, оставаясь все время сфокусированным только на нужном функционале, и избегать over engineering.
AAS — этот тот же AAA паттерн, но с измененным порядком частей, отсортированных с учетом Driven approach и переименованной Arrange частью в Setup, чтобы отличать их по названию.
Первое, что мы делаем, при создании теста — мы создаем Act. Обычно это создание экземпляра класса тестируемого юнита и вызов его функции. С одной стороны — это самый простой шаг, а с другой это то, что диктует нам бизнес история.
Второе — мы проверяем что Act действует ожидаемо. Мы пишем Assert часть, где выражаем требуемые последствия Act, в том числе с точки зрения бизнес истории.
И вот, когда у нас готовы первые 2 части, мы можем остановиться и подумать, как наш юнит будет выполнять требуемые действия. Да, да, именно сейчас мы можем первый раз задуматься, как реализовать его.
Смотрите, сам вид Act и его сигнатура продиктованы предыдущим шагом, тут нам нечего изобретать. Как предыдущий шаг хочет вызывать наш юнит, так он и будет его вызывать. Ожидаемые действия тоже продиктованы предыдущим шагом и самой бизнес историей.
Так что именно сейчас, когда мы будем писать последнюю часть теста, мы можем остановиться и продумать, как наш юнит будет работать и какое runtime окружение ему для этого нужно. И здесь мы переходим более подробно к “Контролируемому окружению” и дизайну юнита.
Из принципа SOLID, с точки зрения юнит тестирования очень важны 2 принципа:
Single responsibility principle — позволяет снизить количество тест кейсов для юнита. В среднем на юнит должно приходиться от 1 до 9 тест кейсов. Это очень хороший индикатор качества юнита — если тест кейсов больше или хочется их сгруппировать, то вам точно нужно разделить его на два и больше независимых юнитов.
Dependency inversion principle — позволяет легко создавать и управлять сложнейшими окружениями для тестирования через IoC контейнеры. В соответствии с данным принципом, юнит должен зависеть от абстракций, что позволяет передавать ему любые реализации его зависимостей. В том числе, и не продакшен реализации, созданные специально для его тестирования. Эти реализации не имеют в себе никакой бизнес логики и созданы не только под конкретный тестируемый юнит, но и под конкретный сценарий его тестирования. Обычно они создаются с помощью одной из библиотек для mock объектов, такой как moq.
IoC контейнеры позволяют автоматически создавать экземпляр тестируемого юнита и экземпляры его зависимостей, сразу реализованные как mock объекты. Использование такого IoC контейнера очень важный шаг к снижению стоимости поддержания кода и его дружелюбности к автоматическому рефакторингу.
Кстати, несколько слов о качестве кода тестов и продакшн. Самым качественным кодом должен быть код тестов. Причина этому одна — это его размер. На 1 строку продакшн кода в среднем приходиться 2-3 строки тестового кода, то есть его в 2-3 раза больше чем продакшн кода. В этих условиях он должен хорошо читаться, быть структурированным, иметь хорошую типизацию и быть очень дружелюбным к инструментам автоматического рефакторинга. Это цели, которые достойны отдельных мероприятий и усилий.
Много приложения реализовано в распределенной и модульной архитектуре, где разные части написаны на различных языках, скажем, клиент-серверные приложения, где клиент написан под веб на typescript и сервер написанный на c#. Важной целью для таких проектов будет приведение тестов для любой части, независимо от языка к единому подходу. Это значит, что все тесты на проекте используют AAA или AAS подход. Все тесты используют mock библиотеки с похожим API. Все тесты используют IoC. И все тесты используют одинаковые метафоры. Это позволяет повысить переносимость удачных практик на разные части проекта, упростить адаптацию новых коллег (выучил раз и применяй везде).
В среднем, на один продакшн юнит приходиться 1-9 тестов. Если тестов больше или у вас возникло желание сгруппировать тесты, то это — четкий сигнал проверить код продакшн юнита. Вполне возможно, что он нуждается в декомпозиции.
Моя команда создает клиент-серверные приложения, где мы используем angular на клиенте и .net core для серверной части. В следующей статье я хочу показать на примерах, как мы пишем юнит тесты под angular и с#. Как мы делаем их похожими, как располагаем в проектах, какие библиотеки применяем.
В этой статье я расскажу о лучших практиках, к которым я пришел за более чем 10 лет тестирования различных проектов. Эти практики позволят начать юнит тестирование без заметного снижения производительности программистов.
Я определяю юнит тестирования как тестирование одного продакш юнита в полностью контролируемом окружении.
Продакшн юнит — это обычно класс, но может быть также и функция, и файл.
Важно, чтобы юнит соответствовал принципал SOLID, в этом случае юнит тесты будут лаконичными. Юниты узкоспециализированны и очень хорошо выполняют одну конкретную задачу, для которой они созданы. В большинстве случаев юниты взаимодействуют друг с другом, делегируя выполнение специализированных задач.
Полностью контролируемое окружение — это окружение имитирующие среду, в которой юнит работает в программе, но полностью открытое для тестирования и настройки. Поведение окружения задается для конкретного тестового кейса через лаконичный API и любое поведение вне этого кейса для него не определено. В этом окружении не должно быть других продакшн юнитов, иначе возрастает сложность тестов и из юнит тестирования мы переходим к интеграционному тестированию.
О наследование
Постарайтесь не применять наследование. Вместо него используйте композицию зависимостей. Часто наследование применяют для реализации принципа DRY (don’t repeat yourself), вынося общий код в родителя, но тем самым нарушая принцип KISS (keep it simple stupid), увеличивая сложность юнитов.
AAA (Arrange, Act, Assert) паттерн
Если посмотреть на юнит тест, то для большинства можно четко выделить 3 части кода:
Arrange (настройка) — в этом блоке кода мы настраиваем тестовое окружение тестируемого юнита;
Act — выполнение или вызов тестируемого сценария;
Assert — проверка того, что тестируемый вызов ведет себя определенным образом.
Этот паттерн улучшает структуру кода и его читабельность, однако начинать писать тест нужно всегда с элемента Act.
Driven approach
Прежде чем продолжить рассмотрение структуры теста, я хотел бы рассказать немного о подходе, который я называю Driven Approach. К сожалению, я не могу перевести это лаконично на русский язык. Поэтому просто расскажу, что за этим стоит, и может быть, вы поможете с лаконичным переводом.
Суть в том, что код, который вы пишите, должен иметь причину своего существования. Важно, чтобы причина была существующей, а не предполагаемой, и эта причина должна иметь в конечном итоге связь с бизнес историей.
С чего мы начинаем разработку конкретного функционала? — с требований бизнеса, которые типично выглядят так: “Пользователь с любой ролью должен иметь возможность создать запись, таким образом, он выполнит такую то бизнес операцию”.
Используя driven approach первое что мы должны сделать —
- Это создать место в UI слое, где пользователь может создать запись, скажем, страницу в приложении, на которой будет кнопка “Создать запись”. Почему мы это сделали? — потому что это требует бизнес история.
- Кнопка “Создать запись” будет требовать реализации обработчика click события.
- Обработчик события будет требовать реализации создания записи в терминах слоя бизнес логики.
- В случае клиент-серверной архитектуры, клиент будет обращаться к некоторому end point на стороне сервера для создания этой записи.
- Сервер, в свою очередь, может работать с базой данных, где такая запись должна быть создана в отдельной таблице.
Как видите, существование каждого этапа реализации обусловлено существованием предыдущего этапа. И каждый этап, даже самый последний — создание записи в таблице базы данных имеет связь с изначальной бизнес историей. Каждый этап реализуется только в том объеме, который нужен предыдущему этапу.
Данный подход позволяет небольшими шагами реализовывать сложные бизнес истории, оставаясь все время сфокусированным только на нужном функционале, и избегать over engineering.
AAS (Act, Assert, Setup) паттерн
AAS — этот тот же AAA паттерн, но с измененным порядком частей, отсортированных с учетом Driven approach и переименованной Arrange частью в Setup, чтобы отличать их по названию.
Первое, что мы делаем, при создании теста — мы создаем Act. Обычно это создание экземпляра класса тестируемого юнита и вызов его функции. С одной стороны — это самый простой шаг, а с другой это то, что диктует нам бизнес история.
Второе — мы проверяем что Act действует ожидаемо. Мы пишем Assert часть, где выражаем требуемые последствия Act, в том числе с точки зрения бизнес истории.
И вот, когда у нас готовы первые 2 части, мы можем остановиться и подумать, как наш юнит будет выполнять требуемые действия. Да, да, именно сейчас мы можем первый раз задуматься, как реализовать его.
Смотрите, сам вид Act и его сигнатура продиктованы предыдущим шагом, тут нам нечего изобретать. Как предыдущий шаг хочет вызывать наш юнит, так он и будет его вызывать. Ожидаемые действия тоже продиктованы предыдущим шагом и самой бизнес историей.
Так что именно сейчас, когда мы будем писать последнюю часть теста, мы можем остановиться и продумать, как наш юнит будет работать и какое runtime окружение ему для этого нужно. И здесь мы переходим более подробно к “Контролируемому окружению” и дизайну юнита.
Принципы SOLID
Из принципа SOLID, с точки зрения юнит тестирования очень важны 2 принципа:
Single responsibility principle — позволяет снизить количество тест кейсов для юнита. В среднем на юнит должно приходиться от 1 до 9 тест кейсов. Это очень хороший индикатор качества юнита — если тест кейсов больше или хочется их сгруппировать, то вам точно нужно разделить его на два и больше независимых юнитов.
Dependency inversion principle — позволяет легко создавать и управлять сложнейшими окружениями для тестирования через IoC контейнеры. В соответствии с данным принципом, юнит должен зависеть от абстракций, что позволяет передавать ему любые реализации его зависимостей. В том числе, и не продакшен реализации, созданные специально для его тестирования. Эти реализации не имеют в себе никакой бизнес логики и созданы не только под конкретный тестируемый юнит, но и под конкретный сценарий его тестирования. Обычно они создаются с помощью одной из библиотек для mock объектов, такой как moq.
IoC контейнеры позволяют автоматически создавать экземпляр тестируемого юнита и экземпляры его зависимостей, сразу реализованные как mock объекты. Использование такого IoC контейнера очень важный шаг к снижению стоимости поддержания кода и его дружелюбности к автоматическому рефакторингу.
Качество кода
Кстати, несколько слов о качестве кода тестов и продакшн. Самым качественным кодом должен быть код тестов. Причина этому одна — это его размер. На 1 строку продакшн кода в среднем приходиться 2-3 строки тестового кода, то есть его в 2-3 раза больше чем продакшн кода. В этих условиях он должен хорошо читаться, быть структурированным, иметь хорошую типизацию и быть очень дружелюбным к инструментам автоматического рефакторинга. Это цели, которые достойны отдельных мероприятий и усилий.
Однотипность тестирования
Много приложения реализовано в распределенной и модульной архитектуре, где разные части написаны на различных языках, скажем, клиент-серверные приложения, где клиент написан под веб на typescript и сервер написанный на c#. Важной целью для таких проектов будет приведение тестов для любой части, независимо от языка к единому подходу. Это значит, что все тесты на проекте используют AAA или AAS подход. Все тесты используют mock библиотеки с похожим API. Все тесты используют IoC. И все тесты используют одинаковые метафоры. Это позволяет повысить переносимость удачных практик на разные части проекта, упростить адаптацию новых коллег (выучил раз и применяй везде).
Количество тестов для одного продакшн юнита
В среднем, на один продакшн юнит приходиться 1-9 тестов. Если тестов больше или у вас возникло желание сгруппировать тесты, то это — четкий сигнал проверить код продакшн юнита. Вполне возможно, что он нуждается в декомпозиции.
Моя команда создает клиент-серверные приложения, где мы используем angular на клиенте и .net core для серверной части. В следующей статье я хочу показать на примерах, как мы пишем юнит тесты под angular и с#. Как мы делаем их похожими, как располагаем в проектах, какие библиотеки применяем.
andreyverbin
Как вы пишите много много юнит тестов и не огребаете при рефакторинге? Удается ли вам ловить реальные баги с помощью юнитов? Расскажите как, потому что сколько я не пытался заставить юнит тесты работать всегда получается одно и то же:
— Любой рефакторинг превращается в борьбу с тестами, время на рефакторинг увеличивается в разы.
— Тесты ломаются от чего угодно, только не от багов. Это из-за моков, но как писать юниты без них непонятно.
ApeCoder
Возможно, вам помогут идеи Джеймса Шора
andreyverbin
Отличная ссылка, спасибо! Оказывается я использую Traffic Cop :) Например, есть утилиты, чтобы перехватить запрос к базе и вернуть ошибку или модифицировать результат. Аналогично для запросов к разным сервисам.
С такой инфраструктурой нет смыла писать классические юнит тесты. Тесты высокоуровневых компонент, часто (но не всегда) протестируют низкоуровневые. Нет никакой нужды тестировать каждый сервис, если можно просто дернуть HTTP API, которое дёрнет тот же самый сервис и заодно проверить настройки сериализации, авторизацию и много чего ещё. Те же HTTP запросы проверят репозитории/DAO, генерацию SQL и т.п.
Хочется узнать как у автора все устроено.
hVostt
ИМХО
andreyverbin
То что вы описываете, это в идеальном мире радуги и единорогов. В реальном мире люди обмазываются моками и их тесты документируют больше использование соков, чем то, зачем этот код нужен и что он делает.
Если тесты не ловят багов, а стоят в поддержке и написании как полноценная команда программистов, то наймите каждому программисту выделенного тестера, а разницу в зп положите в карман.
lxsmkv
Вообще это указывает на проблемы в архитектуре, если вы хотите «перепаять» внутренности не ломая контракты (рефакторинг), а при этом контракты ломаются. Или тесты зацеплены не за контракты, а за «внутряк». Или вы делаете не рефакторинг, а модифицируете приложение.
Ну и юниты я бы не писал на все подряд, а только на контракты с внешними системами.
Когда я сомневаюсь правильно ли я выбрал охват теста, я стараюсь ответить себе на вопрос: «что является предметом этого теста». Предмет теста нужно четко выделять.
Ну и хорошие новости: если тест «ломается» значит он действительно что-то распознает. Другое дело дает ли он вам необходимую информацию. Ту которую вы бы хотели получать.
Что такое мок? Это обьект который при обращении к нему отвечает как настоящая система, но при этом ей не является. Т.е. с его помощью вы заполняете пробел в коммуникационном контуре приложения для выполнения тестировочной задачи. Мок может влиять на тест в том случае, если контракт/протокол общения с этим объектом поменялся, и тест уже общается по новому, а мок еще по старому.
А еще помогает, понять, что тесты это программный комплекс, и относиться к ним соответствующим образом. Это приложение. Прикладная программа, для проверки работоспособности различный частей основного приложения. Вы относитесь к тестам как к «гражданам второго сорта», а они совершенно равноправные и тоже являются приложением. Если применять к тестировочному ПО тот же уровень инженерной добросовестности как и к тестируемому приложению, то все должно начать работать на вас, а не против вас.
Перестаньте бороться с тестами, примите их как «сигнализацию» для вашего кода.
andreyverbin
Я ниже раскрыл идею, речь не про ломание контрактов, а про изменения в сигнатурах и поведении внутренних компонент.
Я с ними не борюсь, более того, команда их пишет, буквально тысячи их. Только это не юнит тесты ни разу. Несмотря на то, что я честно старался научиться их писать и упражнялся в этом где-то 3 или 4 года, от юнит тестов я получил только головную боль и потерянное время. А коллеги говорят, что оно у них работает. Вот мне и интересно как они решают те проблемы, которые заставили меня отказаться от юнит тестов.
lxsmkv
А тесту совершенно безразлично откуда он получит необходимое вспомогательное значение (хоть константу пиши). Так что вы мы по идее не нарушаем этим чистоту теста. Способ получения данных из кеша предметом теста не является, а проверяется какой-то более высокоуровневый функционал.
Представим, нам для эксперимента(теста) нужен ментос и банка кока-колы. Мы же не станем бегать каждый раз за ними в магазин, а ночью на заправку. Мы поставите автомат который всегда по требованию будет выдавать нам новую банку колы и пачку жевательных конфет. Мы постараемся максимально отвязаться от процесса приобретения вспомогательного материала, т.е. абстрагироваться от него, чтобы сконцентрироваться на том что нам важно. Так и тут.
Возьмем другую ситуацию (она мне немного ближе), у нас UI тестирование, фронтенд переименовал локаторы элементов — конечно это затронет тесты. И необходимо придумать способ при внесении таких изменений снизить трудозатраты на адаптацию тестов. Отказываться из-за этого от тестов совсем нет нужды. А трудозатраты на подгон тестирующей системы под изменившийся тестируемый обьект будут всегда, в том или ином обьеме.
Несмотря на это я совсем ни за что не агитирую. Каждый сам должен решать какие виды тестирования ему пользу приносят, а какие нет. Да, юнит тесты могут совсем быть нерентабельными, там кода нужно порой в полтора раза больше чем на само приложение. И я против оголтелого фанатизма и покрытия «квадратно-гнездовым», «потому что все так делают».
anonymous Автор
Добрый день, рефакторинг не представляет проблем — моки строго типизированы и в целом весь код очень дружественен к инструментам автоматического рефакторинга.
Баги можно ловить юнит тестами. Есть два ярких случая:
1. Код с неочевидным поведением — при прочтении кода, не понятно как код себя ведет. Примеры такого кода любят давать на собеседовании, на проверку знаний спецификации языка или среды.
2. Код с вычислениями — в вычислениях могут существовать критические случаи, которые в повседневном использовании редко возникают, и как следствие их сложно найти. Юнит тесты сразу позволяют проверить как поведет себя код в этих условиях.
По третьему вопросу: я планирую еще одну статью, где на примере хочу показать как мы это делаем, пока статьи нет, вы можете найти меня в скайпе по нику и можем обсудить, как вам улучшить ситуацию.
andreyverbin
Не понятно как получается так, что рефакторинг не представляет проблем.
Возьмём кеш, который повсеместно в приложении используется и положим у него есть метод “bool TryGetCachedValue(string key, out T value);” Представим себе рефакторинг — сигнатура этого метода меняется на “ItemState TryGetCachedValue(string key, out T value);“ Положим кеш не простой, а write thru, то есть в нем, кроме самих данных есть ещё соединение с БД и TCP соединение с парой других микросервисов. Из-за этой особенности кеш замокан в каждом тесте.
Без тестов у нас было M мест, где нужно поменять код, в связи с изменением сигнатуры TryGetCachedValue. С тестами появилось ещё N мест. Если N много больше M, то будем говорить, что моки затрудняют рефакторинг. Кажется N всегда больше M.
Эти рассуждения находят подтверждение в моей практике. Часто видел в проектах «комбинаторный взрыв» количества изменений из-за моков. Для меня теория и практика сошлись и я от моков и unit тестов отказался (не от тестов вообще).
Как вы решаете проблему с большим каскадом изменений в тестах, после изменений вроде описанного выше?
ApeCoder
Тут вопрос, что вы называете моками и что юнит тестами.
Вот терминология у Фаулера моки
andreyverbin
Опираюсь на то, с чего начал автор.
Исходя из этого, я делаю вывод, что а) unit test пишется для одного класа b) все вокруг него мокается с помощью чего-то, похожего на Moq. Это именно та среда, от корой я, в своем время, уходил. Но автор явно топит за этот подход, поэтому мне и интересно.
Я так и делаю, типа такого получается
В этом тесте, наверное 50% всей системы задействовано — очереди сообщений, коннекторы к бирже, всякие кеши и in-memory представление аккаунта и рыночных данных и т.п. Тестовыми является только коннектор к бирже, «PrimaryAccount.AddOrder» это обертка над ним, которая генерирует нужное событие.
Не надо так делать, будет больно. Оно начнет в boolean превращаться там где не ожидаешь, а увидеть это глазами в коде невозможно.
anonymous Автор
Очень сложно ответить на вопрос, с данным примером. Формально вы правы, мест для изменения больше. Вот только, в практике я с тами мало встречался. Если это рефакторинг снижающий технический долг, то он направлен на упрощение юнита, однако переход от boolean к ItemState увеличивает complexity. Если у вас рефакторинг обусловлен бизнес задачей, то скорее всего решение будет сложнее, больше юнитов будет вовлечено, но каждый юнит сам по себе должен быть простым и укладываться в 1-9 тест кейсов.
Стараюсь избегать комбинаторных взрывов при рефакторинг. Существуют практики, которые позволяют решить данную проблему. Основная идея — Не революция, а эволюция маленькими шагами. Можно создать ICache2 с новым методом, и не спешно переводить код на использование новой сигнатуры.
Еще одна мысль. Тесты существуют не для снижения сложности рефакторинга, а для увеличения качества кода. Они в автоматическом режиме позволяют ответить на вопрос — работает ли ваш код, как ожидается.
anonymous Автор
как удалить?
voondervaflya
Вероятнее всего, написание тестов позволило избежать возникновения некоторых багов, которые вылезли бы, если тесты не были бы написаны. Когда пишешь тест, автоматически задумываешься о входных данных юнита, например.
dipsy
andreyverbin
«Я не пишу юнит-тесты с моками» и «я не пишу тесты» это разные вещи.
vep
Не понятная идея — «не делайте так». А как делать?
По моим наблюдениям, наследование также часто применяют для реализации отношения «является» при построениии иерархии классов. Вы предлагаете отказаться от иерархии?
anonymous Автор
Использовать композицию из различных сервисов, которые объявляются как зависимости юнита.
Для примера: скажем есть контроллеры и часть из них используют проверку прав пользователя, можно сделать BaseController и наследники через protected функцию будут проверять права пользователя. Я так предлагаю не делать.
Вместо этого, общий код выноситься в сервис и каждый контроллер, которому этот код нужен объявляет от него зависимость.
lamerok
А почему
Мы нарушаем принцип KISS (keep it simple stupid)? Насколько я понимаю, если следовать Принципу подстановки Барбары Лисков, то все будет просто и наследование очень даже хорошо, или я ошибаюсь?
anonymous Автор
Вы не ошибаетесь, наследование рабочий инструмент и принцип очень важен. Я говорю о случае, когда наследование вводиться из-за желания переиспользовать код. Вариант с отдельным сервисом даст более «чистый» код.