The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
На сколько работы следует выполнять в конструкторе? Кажется разумным проводить вычисления внутри конструктора и после этого инкапсулировать результаты. Таким образом, когда результаты потребуются методам объекта, у нас они уже будут готовы. Звучит как хороший подход? Нет, это плохая идея по одной причине: она препятствует композиции объектов и делает их не расширяемыми.
Предположим, мы создаем интерфейс, который представляет имя человека:
Довольно просто, верно? Теперь давайте попробуем воплотить это в жизнь.
Что не так с этим? Это быстрее, верно? Он разбивает имя на части только один раз и инкапсулирует их. Затем, несмотря на то, сколько раз мы вызываем метод first()
, он будет возвращать одно и то же значение и не будет нуждаться в повторном разбиении. Однако, это неправильное мышление! Позвольте мне показать вам правильный путь и объяснить:
Это правильный дизайн. Вижу, что вы улыбаетесь, поэтому позвольте мне доказать свою точку зрения.
Прежде чем начать доказательство, позвольте мне попросить вас прочитать эту статью: Composable Decorators vs. Imperative Utility Methods. В ней объясняется разница между статическим методом и композиционными декораторами. Первый фрагмент выше очень близок к императивному утилитарному методу, хотя и выглядит как объект. Второй пример является истинным объектом.
В первом примере мы злоупотребляем оператором new
и превращаем его в статический метод, который выполняет все вычисления прямо здесь и сейчас. Вот что означает императивное программирование. В императивном программировании мы выполняем все вычисления немедленно и возвращаем полностью готовые результаты. В декларативном программировании, наоборот, мы стараемся откладывать вычисления настолько долго, насколько это возможно.
Давайте попробуем использовать наш класс EnglishName
:
В первой строке этого фрагмента мы просто создаем экземпляр объекта и помечаем его как name
. Мы пока не хотим обращаться к базе данных и извлекать полное имя оттуда, разделять его на части и инкапсулировать их внутри name
. Мы просто хотим создать экземпляр объекта. Такое поведение разбора будет для нас побочным эффектом и в данном случае замедлит работу приложения. Как видите, нам может понадобиться только name.first()
, если что-то пойдет не так и нам понадобится создать объект исключения.
Моя точка зрения заключается в том, что выполнение любых вычислений внутри конструктора является плохой практикой и должно быть избегано, потому что они являются побочными эффектами и не запрашиваются владельцем объекта.
Что касается производительности при повторном использовании name
, вы можете спросить. Если мы создаем экземпляр EnglishName
и затем вызываем name.first()
пять раз, мы получим пять вызовов метода String.split()
.
Чтобы решить эту проблему, мы создаем еще один класс - составной декоратор, который поможет нам решить эту проблему “повторного использования”:
Я использую аннотацию Cacheable
из jcabi-aspects, но вы можете использовать любые другие инструменты кэширования, доступные в Java (или других языках), например Guava Cache:
Но, пожалуйста, не делайте CachedName
изменяемым и отложенной загрузки. Это антипаттерн, о котором я уже говорил в статье “Объекты должны быть неизменяемыми”.
Вот как будет выглядеть наш код теперь:
Это очень примитивный пример, но я надеюсь, что вы понимаете идею.
В этом дизайне мы в основном разделяем объект на две части. Первая знает, как получить имя из английского имени. Вторая знает, как кэшировать результаты этого вычисления в памяти. И теперь это мое решение, как пользователь этих классов, как именно я буду их использовать. Я решу, нужно ли мне кэширование или нет. В этом и заключается композиция объектов.
Позвольте мне повторить, что единственное разрешенное выражение внутри конструктора - это присваивание. Если вам нужно поместить что-то еще туда, начните думать о рефакторинге - вашему классу определенно нужен пересмотр.
Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-17 at 14:10