构造函数中应完成多少工作? 在构造函数内部进行一些计算然后封装结果似乎是合理的。 这样,当对象方法需要结果时,我们将准备好它们。 听起来是个好方法? 不,这不对。 这是一个坏主意,原因有一个:它阻止了对象的组合并使它们不可扩展。
假设我们正在制作一个代表一个人的名字的接口:
interface Name {String first();
}
很简单,对不对? 现在,让我们尝试实现它:
public final class EnglishName implements Name {private final String name;public EnglishName(final CharSequence text) {this.parts = text.toString().split(" ", 2)[0];}@Overridepublic String first() {return this.name;}
}
这怎么了 更快吧? 它仅将名称分成几部分,然后将其封装。 然后,无论我们调用first()
方法有多少次,它都将返回相同的值,并且无需再次进行拆分。 但是,这是有缺陷的想法! 让我向您展示正确的方法并说明:
public final class EnglishName implements Name {private final CharSequence text;public EnglishName(final CharSequence txt) {this.text = txt;}@Overridepublic String first() {return this.text.toString().split("", 2)[0];}
}
这是正确的设计。 我可以看到你在微笑,所以让我证明我的观点。
不过,在开始验证之前,让我请您阅读本文: 可组合装饰器与命令式实用方法 。 它解释了静态方法和可组合装饰器之间的区别。 上面的第一个代码段看起来非常像一个对象,它非常接近命令式实用程序方法。 第二个例子是一个真实的对象。
在第一个示例中,我们正在滥用new
运算符,并将其转换为静态方法,该方法会在此时此刻为我们进行所有计算。 这就是命令式编程的目的。 在命令式编程中,我们立即执行所有计算并返回完全准备好的结果。 相反,在声明式编程中,我们尝试尽可能长时间地延迟计算。
让我们尝试使用我们的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
。 我们还不想进入数据库并从那里获取全名,将其拆分为多个部分,然后将其封装在name
。 我们只想创建一个对象的实例。 这种解析行为对我们来说将是一个副作用,在这种情况下,将减慢应用程序的速度。 如您所见,如果出现问题,我们可能只需要name.first()
,而我们需要构造一个异常对象。
我的观点是,在构造函数内部进行任何计算都是一种不好的做法,必须避免这样做,因为它们是副作用,对象所有者不要求这样做。
您可能会问,重用name
期间的性能如何? 如果我们创建了EnglishName
的实例,然后调用了name.first()
五次,则最终将对String.split()
方法进行五次调用。
为了解决这个问题,我们创建了另一个类,一个可组合的decorator ,它将帮助我们解决这个“重用”问题:
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();}
}
我正在使用jcabi-aspects的Cacheable
批注,但是您可以使用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;}@Overridepublic String first() {return this.cache.get(1L,new Callable<String>() {@Overridepublic String call() {return CachedName.this.origin.first();}});}
}
但是请不要使CachedName
可变并且延迟加载-这是一种反模式,我之前在“ 对象应该是不可变的”中已经讨论过。
这是我们的代码现在的外观:
final Name name = new CachedName(new EnglishName(new NameInPostgreSQL(/*...*/))
);
这是一个非常原始的示例,但我希望您能理解。
在此设计中,我们基本上将对象分为两部分。 第一个知道如何从英文名称中获取名字。 第二个知道如何将计算结果缓存到内存中。 现在,作为这些类的用户,我将决定如何正确使用它们。 我将决定是否需要缓存。 这就是对象构成的全部内容。
让我重申一下,构造函数中唯一允许的语句是赋值。 如果您需要在此处放置其他内容,请开始考虑进行重构-您的课程肯定需要重新设计。
翻译自: https://www.javacodegeeks.com/2015/05/constructors-must-be-code-free.html