Сколько работы должно быть сделано в конструкторе? Кажется разумным делать некоторые вычисления внутри конструктора, а затем инкапсулировать результаты. Походит на хорошее решение? Нет, это не так. Это плохая идея по одной причине: она предотвращает композицию объектов и делает их не-расширяемыми.
Предположим, что мы создаем интерфейс, который будет представлять имя человека:
interface Name {
String first();
}
Довольно легко, правда? Теперь давайте попробуем реализовать это:
public final class EnglishName implements Name {
private final String name;
public EnglishName(final CharSequence text) {
this.name = text.toString().split(" ", 2)[0];
}
@Override
public String first() {
return this.name;
}
}
Что случилось с этим? Это быстрее, правда? Он разбивает имя на части только один раз и инкапсулирует их. Затем,
независимо от того, сколько раз мы вызываем метод first (), он будет
возвращать одно и то же значение и ему не нужно будет снова выполнять
разбиение. Однако, это ошибочное мышление! Позвольте мне показать вам правильный путь и объяснить:
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];
}
}
Это правильный дизайн. Я вижу, что вы улыбаетесь, поэтому позвольте мне доказать свою точку зрения.Прежде
чем я начну доказывать, позвольте мне попросить вас прочитать эту
статью: Composable Decorators vs. Imperative Utility Methods. Он объясняет разницу между статическим методом и композиционными декораторами. Первый фрагмент, приведенный выше, очень близок к императивному методу служебной программы, хотя он и выглядит как объект. Второй пример - истинный объект.В
первом примере мы злоупотребляем новым оператором и превращаем его в
статический метод, который делает все вычисления для нас прямо здесь и
сейчас. Вот что такое императивное программирование. В императивном программировании мы делаем все вычисления прямо сейчас и возвращаем полностью готовые результаты. В декларативном программировании мы вместо этого пытаемся как можно дольше откладывать вычисления.Давайте попробуем использовать наш класс EnglishName:
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.first (), если что-то пойдет не так, и нам нужно создать объект исключения.Я хочу сказать, что любые вычисления внутри конструктора - плохая
практика, и их следует избегать, потому что они являются побочными
эффектами и не запрашиваются владельцем объекта.Что касается производительности во время повторного использования имени, вы можете спросить. Если мы создадим экземпляр EnglishName и затем вызываем name.first () пять раз, мы получим пять вызовов метода String.split ().Чтобы решить эту проблему, мы создадим другой класс, компонуемый
декоратор, который поможет нам решить эту проблему «повторного
использования»:
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-аспектов, но вы можете
использовать любые другие инструменты кэширования, доступные на Java
(или других языках), например Guava Cache:
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 изменчивым и лениво загруженным -
это анти-паттерн, о котором я говорил ранее в «Объекты должны быть
неизменными».Вот как будет выглядеть наш код:
final Name name = new CachedName(
new EnglishName(
new NameInPostgreSQL(/*...*/)
)
);
Это очень примитивный пример, но я надеюсь, что вы поняли.В этом проекте мы в основном разбиваем объект на две части. Первый знает, как получить первое имя от английского имени. Второй знает, как кэшировать результаты этого вычисления в памяти. И теперь я, как пользователь этих классов, решаю, как именно их использовать. Я решу, нужно ли кэширование или нет. Это и есть объектная композиция.Позвольте мне повторить, что единственный допустимый оператор внутри конструктора - это присвоение. Если вам нужно что-то добавить, подумайте о рефакторинге - вашему классу определенно нужна редизайн.
Комментариев нет:
Отправить комментарий