Сколько работы должно быть выполнено в конструкторе? Представляется целесообразным выполнить некоторые вычисления внутри конструктора, а затем инкапсулировать результаты. Таким образом, когда результаты требуются объектными методами, они будут готовы. Похоже, хороший подход? Нет, это не так. Это плохая идея по одной причине: она предотвращает сложение объектов и делает их нерасширяемыми.
Допустим, мы создаем интерфейс, который будет представлять имя человека:
|
1
2
3
|
interface Name { String first();} |
Довольно легко, правда? Теперь давайте попробуем реализовать это:
|
01
02
03
04
05
06
07
08
09
10
|
public final class EnglishName implements Name { private final String name; public EnglishName(final CharSequence text) { this.parts = text.toString().split(" ", 2)[0]; } @Override public String first() { return this.name; }} |
Что с этим не так? Это быстрее, верно? Он разбивает имя на части только один раз и инкапсулирует их. Затем, независимо от того, сколько раз мы вызываем метод first() , он вернет одно и то же значение и не будет нуждаться в повторном разбиении. Однако это ошибочное мышление! Позвольте мне показать вам правильный путь и объяснить:
|
01
02
03
04
05
06
07
08
09
10
|
public final class EnglishName implements Name { private final CharSequence text; public EnglishName(final CharSequence txt) { this.text = txt; } @Override public String first() { return this.text.toString().split("", 2)[0]; }} |
Это правильный дизайн. Я вижу, как ты улыбаешься, поэтому позволь мне доказать свою точку зрения.
Прежде чем я начну доказывать, позвольте мне попросить вас прочитать эту статью: Композиционные декораторы против методов императивных утилит . Это объясняет разницу между статическим методом и составными декораторами. Первый приведенный выше фрагмент кода очень похож на метод императивной утилиты, даже если он выглядит как объект. Второй пример — это настоящий объект.
В первом примере мы злоупотребляем new оператором и превращаем его в статический метод, который выполняет все вычисления для нас прямо здесь и сейчас . Вот что такое императивное программирование. В императивном программировании мы делаем все вычисления прямо сейчас и возвращаем полностью готовые результаты. В декларативном программировании мы вместо этого пытаемся отложить вычисления как можно дольше.
Давайте попробуем использовать наш класс EnglishName :
|
01
02
03
04
05
06
07
08
09
10
11
|
final Name name = new EnglishName( new NameInPostgreSQL(/*...*/));if (/* something goes wrong */) { throw new IllegalStateException( String.format( "Hi, %s, we can't proceed with your application", name.first() ) );} |
В первой строке этого фрагмента мы просто создаем экземпляр объекта и помечаем его name . Мы пока не хотим идти в базу данных и извлекать полное имя оттуда, разбивать его на части и инкапсулировать их в name . Мы просто хотим создать экземпляр объекта. Такое поведение анализа будет побочным эффектом для нас и в этом случае замедлит работу приложения. Как видите, нам может понадобиться только name.first() если что-то пойдет не так, и нам нужно создать объект исключения.
Я хочу сказать, что любые вычисления внутри конструктора — плохая практика, и их следует избегать, поскольку они являются побочными эффектами и не запрашиваются владельцем объекта.
Что касается производительности во время повторного использования name , вы можете спросить. Если мы name.first() экземпляр EnglishName и затем вызовем name.first() пять раз, мы получим пять вызовов метода String.split() .
Чтобы решить эту проблему, мы создаем другой класс, составной декоратор , который поможет нам решить эту проблему «повторного использования»:
|
01
02
03
04
05
06
07
08
09
10
11
|
public final class CachedName implements Name { private final Name origin; public CachedName(final Name name) { this.origin = name; } @Override @Cacheable(forever = true) public String first() { return this.origin.first(); }} |
Я использую аннотацию Cacheable из jcabi-aspect , но вы можете использовать любые другие инструменты кэширования, доступные в Java (или других языках), такие как Guava Cache :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
public final class CachedName implements Name { private final Cache<Long, String> cache = CacheBuilder.newBuilder().build(); private final Name origin; public CachedName(final Name name) { this.origin = name; } @Override public String first() { return this.cache.get( 1L, new Callable<String>() { @Override public String call() { return CachedName.this.origin.first(); } } ); }} |
Но, пожалуйста, не делайте CachedName изменчивым и лениво загружаемым — это анти-паттерн, который я обсуждал ранее в « Объектах должны быть неизменяемыми» .
Вот как теперь будет выглядеть наш код:
|
1
2
3
4
5
|
final Name name = new CachedName( new EnglishName( new NameInPostgreSQL(/*...*/) )); |
Это очень примитивный пример, но я надеюсь, что вы поняли идею.
В этом проекте мы в основном разделяем объект на две части. Первый знает, как получить имя из английского имени. Второй знает, как кэшировать результаты этого расчета в памяти. И теперь я, как пользователь этих классов, решил, как именно их использовать. Я решу, нужно ли мне кэшировать или нет. Вот что такое объектная композиция.
Позвольте мне повторить, что единственным допустимым оператором внутри конструктора является присваивание. Если вам нужно добавить что-то еще, подумайте о рефакторинге — ваш класс определенно нуждается в редизайне.
| Ссылка: | Конструкторы должны быть свободны от кода от нашего партнера JCG Егора Бугаенко в блоге About Programming . |
