基于这个答案,这里提供了建造者模式的准不可变版本:
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
public class GenericBuilder<MT, IT> {
private final Supplier<MT> mMutable;
private final Function<MT, IT> mImmutable;
private final List<Consumer<MT>> mModifiers = new ArrayList<>();
protected GenericBuilder(
final Supplier<MT> mutator, final Function<MT, IT> immutable ) {
mMutable = mutator;
mImmutable = immutable;
}
public static <MT, IT> GenericBuilder<MT, IT> of(
final Supplier<MT> supplier, final Function<MT, IT> immutable ) {
return new GenericBuilder<>( supplier, immutable );
}
public <V> GenericBuilder<MT, IT> with(
final BiConsumer<MT, V> consumer, final V value ) {
mModifiers.add( instance -> consumer.accept( instance, value ) );
return this;
}
public IT build() {
final var value = mMutable.get();
mModifiers.forEach( modifier -> modifier.accept( value ) );
mModifiers.clear();
return mImmutable.apply( value );
}
}
使用示例:
final var caret = CaretPosition
.builder()
.with( CaretPosition.Mutator::setParagraph, 5 )
.with( CaretPosition.Mutator::setMaxParagraph, 10 )
.build();
当修改器的引用被释放时,返回对象的状态实际上是不可变的。 CaretPosition
类类似于:
public class CaretPosition {
public static GenericBuilder<CaretPosition.Mutator, CaretPosition> builder() {
return GenericBuilder.of( CaretPosition.Mutator::new, CaretPosition::new );
}
public static class Mutator {
private int mParagraph;
private int mMaxParagraph;
public void setParagraph( final int paragraph ) {
mParagraph = paragraph;
}
public void setMaxParagraph( final int maxParagraph ) {
mMaxParagraph = maxParagraph;
}
}
private final Mutator mMutator;
private CaretPosition( final Mutator mutator ) {
mMutator = mutator;
}
从这里开始,CaretPosition
可以自由地引用其内部的 Mutator
实例,这很方便,因为这样可以避免在非必要情况下暴露对不可变类的访问器方法,同时又不违反封装性。
这只是“准不可变”的,因为如果保留了对可变实例的引用,则其值可以被更改。以下是如何违反不可变性的示例:
final var mutable = CaretPosition.builder()
.with( CaretPosition.Mutator::setParagraph, 5 )
.with( CaretPosition.Mutator::setMaxParagraph, 10 );
final var caret = mutable.build();
mutable.setParagraph( 17 );
System.out.println( "caret para: " + caret.toString() );
如果
caret.toString()
包含段落值,则生成的字符串将包含17而不是5,违反了不可变性。采用这种方法的另一个缺点是,如果在
build()
时执行验证,则第二次调用
setParagraph
将无法通过验证器。
避免这种情况的方法包括:
-
不可变的复制构造函数。 将可变成员变量复制到不可变实例中,这意味着需要复制所有成员变量。
-
Mutator
复制构造函数。 将
Mutator
复制到新对象引用中,这样可以在构建所需类型的真正不可变实例时避免复制所有成员变量。
-
克隆。 在构造不可变实例时克隆Mutator,这要么需要在各处实现
Serializable
,要么使用
深度复制库。
-
库。 放弃此解决方案,转而使用
Project Lombok、
AutoValue或
Immutables。
Mutator
复制构造函数选项类似于:
private Mutator() {
}
private Mutator( final Mutator mutator) {
mParagraph = mutator.mParagraph;
mMaxParagraph = mutator.mMaxParagraph;
}
那么针对CaretPosition
的变更是微不足道的 - 使用其拷贝构造函数来实例化Mutator
:
private CaretPosition( final Mutator mutator ) {
mMutator = new Mutator( mutator );
}
Person
实例并调用其setter方法相比,并没有增加多少东西。 - Jesper