Сколько работы должно быть выполнено в конструкторе? Представляется целесообразным выполнить некоторые вычисления внутри конструктора, а затем инкапсулировать результаты. Таким образом, когда результаты требуются объектными методами, они будут готовы. Похоже, хороший подход? Нет, это не так. Это плохая идея по одной причине: она предотвращает сложение объектов и делает их нерасширяемыми.
Допустим, мы создаем интерфейс, который будет представлять имя человека:
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 . |