Когда я беру интервью у людей, я часто спрашиваю их, что бы сделал злой разработчик, чтобы сделать его код трудным для тестирования. Сервлеты являются отличным примером того, что делать, если вы хотите, чтобы это было трудно проверить
Начнем с конструктора. Конструктор — это первое, на что я обращаю внимание, так как оно говорит мне о моих зависимостях. В случае сервлетов спецификация запрашивает конструктор без аргументов, чтобы контейнер мог управлять процессом создания экземпляра. Это все хорошо, но как мой сервлет должен овладеть своими зависимостями, такими как соединение с базой данных или любой другой объект, который должен быть разделен между несколькими сервлетами? Там нет хорошего пути! Единственное, что я могу сделать, — это использовать глобальное состояние и синглтоны для взаимодействия между сервлетами (что мы подробно рассмотрели здесь , здесь и здесь ).
Процесс инициализации здесь не поможет. Да, контейнер будет вызывать метод init, но единственное, что я могу получить в процессе инициализации, — это строки, которые я поместил в мой XML. Что я действительно хочу, так это обмениваться экземплярами объектов между сервлетами, чтобы я мог настроить своих соавторов. Снова я вынужден привести к глобальному состоянию и одиночкам. Но в этом есть что-то худшее. В основном принцип единоличной ответственности. Каждый класс отвечает только за одну вещь. Но в нашем случае сервлет отвечает за логику, передачу себя на коллабораторов и инициализацию коллабораторов. Теперь я хотел бы разместить свою инициализацию в одном месте, чтобы я мог контролировать порядок инициализации.Но сервлеты затрудняют это, поскольку контейнер сервлетов не гарантирует какой-либо порядок инициализации вашего кода. Это означает, что каждый сервлет является потенциальным источником инициализации. Что снова вынуждает вас к одиночной и ленивой модели инициализации.
В Java у вас есть единая иерархия наследования (что хорошо), но сервлеты отнимают это у вас, заставляя вас наследовать от своего класса по сути, не давая вам вообще никакого выбора наследования. Наследование следует использовать, когда нам нужно воспользоваться преимуществом полиморфизма, но в сервлетах полиморфизм не происходит. Какую допустимую цель выполняет наследование от HttpServlet, к которому нельзя обратиться с помощью интерфейса. Повторное использование кода через наследование, возможно, является худшим нарушением в разработке программного обеспечения. Композиция по наследству всегда должна быть целью. Хуже всего то, что это устанавливает приоритет, которому следуют в большинстве проектов. Большинство веб-приложений имеют глубокую иерархию наследования с сервлетом вверху, который, кажется, не служит никакой другой цели, кроме повторного использования кода.Тестировать коллабораторов легко, поскольку я могу тестировать каждого коллаборатора изолированно, но если коллаборация заменяется наследованием, теперь мои тесты должны тестировать всю иерархию наследования сразу. Наследование — это все или ничего.
Хорошо, если вам удалось пережить конструктор, инициализацию и наследование, вас ждут другие тонкие сюрпризы. Мы создаем веб-приложения, поэтому время жизни наиболее распространенного объекта должно быть равно продолжительности HTTP-запроса. Вместо этого у нас есть сервлеты, время жизни которых — время жизни JVM. Это создает огромную проблему, поскольку вся информация / данные HTTP-запроса теперь должны проходить через стек, а не быть частью объекта. Другими словами, у нас есть статическая кодовая база, и мы передаем данные через аргументы метода. Звучит как процедурный код для меня. Я не хочу вступать в дебаты по поводу ОО против процедурного, но я хочу отметить, что Java — это ОО, используйте его таким образом. Если вы хотите написать процедурный код, используйте процедурный язык. Играть в сильные стороны языка нет его слабости.
Конечно, нет ничего другого, что сервлеты делают неправильно. Ох, но есть, позвольте мне познакомить вас с анти-паттерном сервис-локаторов. HttpServletRequest и HttpServletResponse — это два огромных сервисных локатора. Сами они не имеют ничего интересного, но у них есть методы получения, у которых есть вещи, которые мне нужны. Ну, вообще-то, мне это тоже не нужно. Я хочу, например, пользователя. У меня есть запрос. Давайте посмотрим, это выглядит знакомо?
String cookie = request.getCookie();
long userId = cookieParser.parse(cookie).getUserId();
User user = userRepository.find(userId);
То, что я хочу, это пользователь, но то, что я получил, достойно детектива Шерлока-Холмса, когда я пытаюсь собрать воедино личность пользователя. Если бы мне пришлось делать это в одном месте, я бы не жаловался так сильно, но я должен делать это в каждом отдельном сервлете. Пожалуйста, просто дайте мне пользователя или что-то еще, что мне нужно, чтобы моя работа была выполнена. Дело в том, что в запросе есть cookie, но я хочу не cookie, а идентификатор пользователя, который встроен в cookie. Но это не идентификатор пользователя, а пользователь, которого я действительно хочу. Это нарушение закона Деметры является прямым следствием двух вещей: (1) характер запроса / ответа на определение службы и (2) неправильный срок службы сервлета.
Теперь представьте, что вы пытаетесь проверить эту вещь. Сначала я должен вызвать new, замечательно, это легко, так как у меня нет конструктора аргументов, но как мне передать все мои ложные зависимости в сервлет в качестве соавторов? Моя единственная надежда — установить какое-то глобальное состояние. Но подождите, сервлет также отвечает за инициализацию, что означает, что он загружен новыми операторами, а те отстой с точки зрения тестируемости. (см. здесь) Теперь я действительно хочу протестировать домашнюю страницу, но сервлет домашней страницы наследуется от AuthorizedServlet, который обращается к базе данных для аутентификации. Здорово, что каждый мой тест должен проверять подлинность, даже если она не проверяет. Так что, если я изменю свою аутентификацию, все мои тесты будут сломаны. Теперь я действительно хочу создать нового пользователя, чтобы протестировать домашнюю страницу, но я должен поместить своего пользователя в репозиторий макетов и назначить макет идентификатора. Затем мне нужно создать фиктивный cookie-файл и поместить его в фиктивный http-запрос, надеясь, что я смогу предоставить вам фиктивный UserRepesitory через какое-то глобальное состояние. Хотелось бы я это придумать, но сервлеты в их нынешнем виде непроверены.
Посмотрим, сможем ли мы решить эти проблемы. Сначала забудьте сервлеты и создайте свои собственные классы, которые имеют ваше собственное минимальное наследование с предпочтением композиции с временем жизни объекта, равным таковому для HTTP-запроса, чтобы ваши соавторы могли запрашивать такие объекты, как User, непосредственно в конструкторе. Таким образом, тестирование вашего кода легко. Теперь о части сервлета. Поскольку наш код является HTTP-сервлетом, он не требует запросов и ответов, его легко тестировать и он несовместим. Поэтому задача сервлета — вызывать новых операторов для построения графа объектов соавторов с правильным временем жизни и делегировать выполнение нашему коду. По сути, сервлет становится одной большой фабрикой, и объект смотрит вверх. Это все еще непроверено, как всегда, но по крайней мере там нет логики для тестирования.Поскольку природа заводов такова, что они либо работают, либо эффектно взрываются.