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"
);
}
}
看起来很简单直接,对吧?是的,这是一个相当有凝聚力和稳定的类。下面是我们使用它来读取谷歌首页内容的方法:
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
类基本上是一个参数的持有者,没有任何行为。它有一些getter方法,可以让我们访问这些参数:isEncodeAnyway()
、isAlwaysHtml()
和getEncoding()
。如果我们继续沿着这个方向发展,这个类中可能会有几十个配置设置。这看起来非常方便,在Java世界中这是一种非常典型的模式。例如,看看Hadoop中的JobConf
。我们将以这种高度可配置的方式调用我们的Page
(我假设PageSettings
是不可变的)。
String html = new Page(
"http://www.google.com",
new PageSettings()
.withEncoding("ISO_8859_1")
.withAlwaysHtml(true)
.withEncodeAnyway(false)
).html();
然而,无论这种方法乍一看多么方便,它实际上是非常错误的。主要是因为它鼓励我们制造庞大且不协调的对象。它们会变得越来越庞大,变得难以测试、难以维护和难以阅读。
为了防止这种情况发生,我建议在这里遵循一个简单的规则:对象行为不应该是可配置的。或者更具体地说,封装的属性不能用于改变对象的行为。
对象属性仅用于协调对象所代表的现实世界实体的位置。uri
是坐标,而alwaysHtml
布尔属性是一种改变行为的触发器。看到区别了吗?
那么,我们应该做什么呢?什么样的设计才是正确的呢?我们必须使用可组合的装饰器。以下是操作方法:
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:33