The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:
Использование свойств объектов в качестве параметров конфигурации - очень распространенная ошибка, которую мы постоянно делаем, в основном потому, что наши объекты изменяемы - мы их настраиваем. Мы изменяем их поведение, внедряя параметры или даже целые объекты настроек/конфигурации в них. Нужно ли мне сказать, что это злоупотребление и неуважение с философской точки зрения? Я могу, но давайте рассмотрим это с практической точки зрения.
Допустим, есть класс, который должен считывать веб-страницу и возвращать ее содержимое:
class Page {
private final String uri;
Page(final String address) {
this.uri = address;
}
public String html() throws IOException {
return IOUtils.toString(
new URL(this.uri).openStream(),
"UTF-8"
);
}
}
Кажется простым и прямолинейным, верно? Да, это довольно связный и надежный класс. Вот как мы используем его для чтения содержимого главной страницы Google:
String html = new Page("http://www.google.com").html();
Все хорошо, пока мы не начинаем делать этот класс более мощным. Допустим, мы хотим настроить кодировку. Мы не всегда хотим использовать "UTF-8"
. Мы хотим, чтобы это было настраиваемым. Вот что мы делаем:
class Page {
private final String uri;
private final String encoding;
Page(final String address, final String enc) {
this.uri = address;
this.encoding = enc;
}
public String html() throws IOException {
return IOUtils.toString(
new URL(this.uri).openStream(),
this.encoding
);
}
}
Готово, кодировка инкапсулирована и настраиваема. Теперь предположим, что мы хотим изменить поведение класса для ситуации с пустой страницей. Если загружается пустая страница, мы хотим вернуть "<html/>"
. Но не всегда. Мы хотим, чтобы это было настраиваемым. Вот что мы делаем:
class Page {
private final String uri;
private final String encoding;
private final boolean alwaysHtml;
Page(final String address, final String enc,
final boolean always) {
this.uri = address;
this.encoding = enc;
this.alwaysHtml = always;
}
public String html() throws IOException {
String html = IOUtils.toString(
new URL(this.uri).openStream(),
this.encoding
);
if (html.isEmpty() && this.alwaysHtml) {
html = "<html/>";
}
return html;
}
}
Класс становится больше, да? Здорово, мы хорошие программисты и наш код должен быть сложным, верно? Чем сложнее он, тем лучше программисты мы! Я иронизирую. Определенно нет! Но давайте двигаться дальше. Теперь мы хотим, чтобы наш класс все равно продолжал работу, даже если кодировка не поддерживается на текущей платформе:
class Page {
private final String uri;
private final String encoding;
private final boolean alwaysHtml;
private final boolean encodeAnyway;
Page(final String address, final String enc,
final boolean always, final boolean encode) {
this.uri = address;
this.encoding = enc;
this.alwaysHtml = always;
this.encodeAnyway = encode;
}
public String html() throws IOException,
UnsupportedEncodingException {
final byte[] bytes = IOUtils.toByteArray(
new URL(this.uri).openStream()
);
String html;
try {
html = new String(bytes, this.encoding);
} catch (UnsupportedEncodingException ex) {
if (!this.encodeAnyway) {
throw ex;
}
html = new String(bytes, "UTF-8")
}
if (html.isEmpty() && this.alwaysHtml) {
html = "<html/>";
}
return html;
}
}
Класс растет и становится все мощнее! Теперь пришло время представить новый класс, который мы назовем PageSettings
:
class Page {
private final String uri;
private final PageSettings settings;
Page(final String address, final PageSettings stts) {
this.uri = address;
this.settings = stts;
}
public String html() throws IOException {
final byte[] bytes = IOUtils.toByteArray(
new URL(this.uri).openStream()
);
String html;
try {
html = new String(bytes, this.settings.getEncoding());
} catch (UnsupportedEncodingException ex) {
if (!this.settings.isEncodeAnyway()) {
throw ex;
}
html = new String(bytes, "UTF-8")
}
if (html.isEmpty() && this.settings.isAlwaysHtml()) {
html = "<html/>";
}
return html;
}
}
Класс PageSettings
в основном является хранилищем параметров без какого-либо поведения. У него есть геттеры, которые дают нам доступ к параметрам: isEncodeAnyway()
, isAlwaysHtml()
и getEncoding()
. Если мы продолжим двигаться в этом направлении, в этом классе может быть несколько десятков настроек конфигурации. Это может выглядеть очень удобно и является очень типичным шаблоном в мире Java. Например, посмотрите на JobConf
из Hadoop. Так мы будем называть нашу высоконагруженную настройку Page
(я предполагаю, что PageSettings
является неизменяемым).
String html = new Page(
"http://www.google.com",
new PageSettings()
.withEncoding("ISO_8859_1")
.withAlwaysHtml(true)
.withEncodeAnyway(false)
).html();
Однако, несмотря на то, что это может показаться удобным на первый взгляд, такой подход совершенно неправильный. Главным образом потому, что он побуждает нас создавать крупные и несвязные объекты. Они увеличиваются в размерах и становятся менее тестируемыми, менее поддерживаемыми и менее читаемыми.
Чтобы предотвратить это, я бы предложил здесь простое правило: поведение объекта не должно быть настраиваемым. Или, более технически, инкапсулированные свойства не должны использоваться для изменения поведения объекта.
Свойства объекта существуют только для координирования местоположения представляемой объектом сущности реального мира. uri
представляет собой координату, а свойство alwaysHtml
типа boolean является триггером изменения поведения. Видите разницу?
Итак, что же нам следует делать вместо этого? Какой должен быть правильный дизайн? Мы должны использовать составные декораторы. Вот как:
Page page = new NeverEmptyPage(
new DefaultPage("http://www.google.com")
)
String html = new AlwaysTextPage(
new TextPage(page, "ISO_8859_1")
page
).html();
Вот как будет выглядеть наша DefaultPage
(да, я немного изменил ее дизайн):
class DefaultPage implements Page {
private final String uri;
DefaultPage(final String address) {
this.uri = address;
}
@Override
public byte[] html() throws IOException {
return IOUtils.toByteArray(
new URL(this.uri).openStream()
);
}
}
Как видите, я реализую интерфейс Page
. Теперь декоратор TextPage
, который преобразует массив байтов в текст с использованием указанной кодировки:
class TextPage {
private final Page origin;
private final String encoding;
TextPage(final Page page, final String enc) {
this.origin = page;
this.encoding = enc;
}
public String html() throws IOException {
return new String(
this.origin.html(),
this.encoding
);
}
}
Теперь NeverEmptyPage
:
class NeverEmptyPage implements Page {
private final Page origin;
NeverEmptyPage(final Page page) {
this.origin = page;
}
@Override
public byte[] html() throws IOException {
byte[] bytes = this.origin.html();
if (bytes.length == 0) {
bytes = "<html/>".getBytes();
}
return bytes;
}
}
И, наконец, AlwaysTextPage
:
class AlwaysTextPage {
private final TextPage origin;
private final Page source;
AlwaysTextPage(final TextPage page, final Page src) {
this.origin = page;
this.source = src;
}
public String html() throws IOException {
String html;
try {
html = this.origin.html();
} catch (UnsupportedEncodingException ex) {
html = new TextPage(this.source, "UTF-8").html();
}
return html;
}
}
Вы можете сказать, что AlwaysTextPage
будет делать два вызова к инкапсулированному origin
в случае неподдерживаемой кодировки, что приведет к дублированному HTTP-запросу. Это верно и так было задумано. Мы не хотим, чтобы происходила дублированная передача данных через HTTP. Давайте введем еще один класс, который будет кэшировать полученную страницу (не потокобезопасный, но это сейчас не важно):
class OncePage implements Page {
private final Page origin;
private final AtomicReference<byte[]> cache =
new AtomicReference<>;
OncePage(final Page page) {
this.origin = page;
}
@Override
public byte[] html() throws IOException {
if (this.cache.get() == null) {
this.cache.set(this.origin.html());
}
return this.cache.get();
}
}
Теперь наш код должен выглядеть так (обратите внимание, я сейчас использую OncePage
):
Page page = new NeverEmptyPage(
new OncePage(
new DefaultPage("http://www.google.com")
)
)
String html = new AlwaysTextPage(
new TextPage(page, "ISO_8859_1")
"UTF-8"
).html();
Вероятно, это самый кодоемкий пост на этом сайте до сих пор, но надеюсь, что он читабелен и я смог передать идею. Теперь у нас есть пять классов, каждый из которых довольно небольшой, легко читаемый и удобный для повторного использования.
Просто следуйте правилу: никогда не делайте классы настраиваемыми!
Translated by ChatGPT gpt-3.5-turbo/33 on 2023-09-05 at 10:34