WhiningBuilder
From APIDesign
(→Drawbacks) |
(→Summary) |
||
(46 intermediate revisions not shown.) | |||
Line 1: | Line 1: | ||
- | + | [[ChameleonBuilder]]'s core trick - e.g. the change of the return type - has a problem! It works only for a single essential attribute. Can we modify the [[builder]] pattern to work with multiple essential attributes? Another problem of the [[ChameleonBuilder]] is the way it reports errors - it doesn't push the user towards proper fix. Can we improve that and encourage [[cluelessness]] in users of our [[builder]]s? | |
=== Multiple Throws === | === Multiple Throws === | ||
- | [[Java]] allows only a single return value - yet, it allows multiple '''throws''' - and throwing an [[exception]] is kind a return value as well. As the [[BuilderWithConditionalException]] it is possible use a generic type parameter to control whether the [[exception]] thrown from the '''build()''' method is [[checked exception]] or unchecked one. Let's do the same with multiple throws for two essential attributes (name and mimeType): | + | [[Java]] allows only a single return value - yet, it allows multiple '''throws''' - and throwing an [[exception]] from a method invocation is a kind of a return value as well. As the [[BuilderWithConditionalException]] shows it is possible use a generic type parameter to control whether the [[exception]] thrown from the '''build()''' method is [[checked exception]] or unchecked one. Let's do the same with multiple throws for two essential attributes (name and mimeType): |
<source lang="java"> | <source lang="java"> | ||
Line 42: | Line 42: | ||
</source> | </source> | ||
- | Please note that ''MissingName'' and ''MissingMIMEType'' are | + | Please note that ''MissingName'' and ''MissingMIMEType'' are generic type parameters - e.g. they aren't real exceptions, just specifications/place holders for the actual {{JDK|java/lang|Exception}} subclass. This gives us early compile time warning as in [[ChameleonBuilder]]. When one tries to compile: |
<source lang="java"> | <source lang="java"> | ||
Line 51: | Line 51: | ||
</source> | </source> | ||
- | one gets a compilation error, as the build method throws a [[checked exception]]. Even if one specifies '''name''', the error still remains. Only if both '''name''' and '''mimeType''' are specified the [[checked exception]]s go away (as they are replaced by runtime [[exception]]s). | + | one gets a compilation error, as the [[builder]] ''whines''! Its '''build''' method throws a [[checked exception]]. Even if one specifies '''name''', the error still remains. Only if both '''name''' and '''mimeType''' are specified the [[checked exception]]s go away (as they are replaced by runtime [[exception]]s) and the compilation succeeds. |
- | The runtime and compile time | + | The runtime and compile time aspects work in orchestration: if a [[checked exception]] is reported - it will also be thrown. E.g. trying to put a '''try'''/'''catch''' block around the '''build()''' call makes no sense. If the [[exception]] is declared, it will really be generated. |
=== Explaining Why === | === Explaining Why === | ||
- | One problem of the [[ChameleonBuilder]] lies in insufficient reporting of the proper error when an essential attribute isn't specified. That is not that great in the previous example either - one only gets an {{JDK|java/lang|Exception}} not | + | One problem of the [[ChameleonBuilder]] lies in insufficient reporting of the proper error when an essential attribute isn't specified. That is not that great in the previous example either - one only gets an {{JDK|java/lang|Exception}}. It is not that easy to find the real reason for the failure. But that can be easily improved: |
<source lang="java"> | <source lang="java"> | ||
Line 74: | Line 74: | ||
with this change the user always needs to handle the specific exception. If '''name''' isn't specified, one needs to catch '''MissingNameException'''. If '''mimeType''' attribute isn't specified, one needs to catch '''MissingMIMETypeException'''. Both exception classes may have proper [[Javadoc]] explaining that rather than catching them, it is advised to set the corresponding attribute. | with this change the user always needs to handle the specific exception. If '''name''' isn't specified, one needs to catch '''MissingNameException'''. If '''mimeType''' attribute isn't specified, one needs to catch '''MissingMIMETypeException'''. Both exception classes may have proper [[Javadoc]] explaining that rather than catching them, it is advised to set the corresponding attribute. | ||
- | With such support the programmers should be able to understand what is wrong and properly recover from | + | With such support the programmers should be able to understand what is wrong and properly recover from their mistakes while coding in an editor, without the need to execute their code. |
=== Why Does It Work? === | === Why Does It Work? === | ||
- | The [[ChameleonBuilder]] works only with a single essential attribute because each attribute can influence only a single generic type. The reason why | + | The [[ChameleonBuilder]] works only with a single essential attribute because each attribute can influence only a single generic type. The reason why the [[WhiningBuilder]] pattern works is that one method can have unlimited number of {{JDK|java/lang|Exception}}s in its '''throws''' clause. Depending on the number of essential attributes one defines '''N''' type parameters: |
<source lang="java"> | <source lang="java"> | ||
Line 89: | Line 89: | ||
</source> | </source> | ||
- | Each of the '''E''' type | + | Each of the '''E'''-th type parameter is representing independent single condition and can be used for whining: Was the associated attribute set or not? All of the conditions need to be satisfied otherwise the '''build()''' method keeps whining - e.g. throwing a [[checked exception]]. This kind of ''or-behavior'' is impossible to achieve with a single return type in [[Java]] type system. |
=== Drawbacks === | === Drawbacks === | ||
- | The biggest strength of this pattern - e.g. '''N''' independent | + | The biggest strength of this pattern - e.g. '''N''' independent conditions is also its biggest flaw: Imagine [[clueless]] users seeing the signature with n-[[exception]] type parameters! They would be scared. On the other hand, the typical usage of the [[builder]] pattern doesn't expose the type variables at all: |
<source lang="java"> | <source lang="java"> | ||
Line 104: | Line 104: | ||
</source> | </source> | ||
- | The code looks OK in spite of enormous number number of generic type parameters. Most of the time the generics types remain invisible. | + | The code looks OK in spite of enormous number number of generic type parameters. Most of the time the generics types remain invisible - especially if one uses [[HiddenBuilder]] pattern as well. |
- | === Evolution === | + | === [[Evolution]] === |
+ | An interesting question from an [[evolution]] point of view: what happens when we need to add a new essential attribute? Obviously, if we want 100% [[BackwardCompatibility]] then we should add new [[builder]] class: | ||
- | [[ | + | <source lang="java"> |
+ | public final class Builder2<E1,E2,E3 extends Exception> extends Builder<E1,E2> { | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | this class can add new type parameters, overwrite the already existing methods of '''Builder''' to return '''Builder2''', etc. But that isn't the best solution. | ||
+ | |||
+ | If 99% [[BackwardCompatibility]] is enough, we could add new generic type parameter into already existing '''Builder''' class. That is of course not [[source compatible]] - but thanks to [[erasure]] of generic types, it is [[binary compatible]] and that is what matters the most in [[Java]]. Moreover the common usage of the [[builder]] doesn't expose the generics in code at all - so extending the number of generic types parameters should be mostly harmless and it is [[I|my]] favorite solution. | ||
+ | |||
+ | === Summary === | ||
+ | |||
+ | Using [[checked exception]] to track essential attributes and to whine when they aren't set is a nice and no-overhead way to co-relate the requested runtime behavior with compiler and let users know at compile time that their code is wrong and needs additional tweaks. Evolution of this pattern works nicely with respect to [[binary compatibility]] in [[Java]] thanks to [[erasure]] of generic types. | ||
+ | |||
+ | |||
+ | |||
+ | [[Category:APIDesignPatterns]] | ||
+ | [[Category:APIDesignPatterns:Creational]] | ||
+ | [[Category:APIDesignPatterns:Exceptions]] |
Current revision
ChameleonBuilder's core trick - e.g. the change of the return type - has a problem! It works only for a single essential attribute. Can we modify the builder pattern to work with multiple essential attributes? Another problem of the ChameleonBuilder is the way it reports errors - it doesn't push the user towards proper fix. Can we improve that and encourage cluelessness in users of our builders?
Contents |
Multiple Throws
Java allows only a single return value - yet, it allows multiple throws - and throwing an exception from a method invocation is a kind of a return value as well. As the BuilderWithConditionalException shows it is possible use a generic type parameter to control whether the exception thrown from the build() method is checked exception or unchecked one. Let's do the same with multiple throws for two essential attributes (name and mimeType):
public final class Builder<MissingName extends Exception,MissingMIMEType extends Exception> { public Source build() throws MissingName, MissingMIMEType { if (this.name == null) { throw (MissingName)new Exception("name missing"); } if (this.mimetype == null) { throw (MissingMIMEType)new Exception("MIME type missing"); } // create and return source; } public static Builder<Exception,Exception> newFromText(String content) { // create new builder without name and MIME type return theBuilder; } public Builder<RuntimeException,MissingMIMEType> name(String name) { // specifying name this.name = name; // keeps the "mime type" exception unchanged but // turns the "name" exception into runtime one return (Builder<RuntimeException,MissingMIMEType>)this; } public Builder<MissingName,RuntimeException> mimeType(String mimeType) { // specifying MIME Type this.mimeType = mimeType; // keeps the "name" exception unchanged but // turns the "mime type" exception into runtime one return (Builder<MissingName,RuntimeException>)this; } }
Please note that MissingName and MissingMIMEType are generic type parameters - e.g. they aren't real exceptions, just specifications/place holders for the actual Exception subclass. This gives us early compile time warning as in ChameleonBuilder. When one tries to compile:
private static Source buildMySource() { return Source.newFromText("function hi() { print('Hi'; }"). build(); }
one gets a compilation error, as the builder whines! Its build method throws a checked exception. Even if one specifies name, the error still remains. Only if both name and mimeType are specified the checked exceptions go away (as they are replaced by runtime exceptions) and the compilation succeeds.
The runtime and compile time aspects work in orchestration: if a checked exception is reported - it will also be thrown. E.g. trying to put a try/catch block around the build() call makes no sense. If the exception is declared, it will really be generated.
Explaining Why
One problem of the ChameleonBuilder lies in insufficient reporting of the proper error when an essential attribute isn't specified. That is not that great in the previous example either - one only gets an Exception. It is not that easy to find the real reason for the failure. But that can be easily improved:
public final class MissingNameException extends Exception { } public final class MissingMIMETypeException extends Exception { } // and change the method public static Builder<MissingNameException, MissingMIMETypeException> newFromText(String content) { // uninitialized builder return theBuilder; }
with this change the user always needs to handle the specific exception. If name isn't specified, one needs to catch MissingNameException. If mimeType attribute isn't specified, one needs to catch MissingMIMETypeException. Both exception classes may have proper Javadoc explaining that rather than catching them, it is advised to set the corresponding attribute.
With such support the programmers should be able to understand what is wrong and properly recover from their mistakes while coding in an editor, without the need to execute their code.
Why Does It Work?
The ChameleonBuilder works only with a single essential attribute because each attribute can influence only a single generic type. The reason why the WhiningBuilder pattern works is that one method can have unlimited number of Exceptions in its throws clause. Depending on the number of essential attributes one defines N type parameters:
public final class Builder<E1 extends Exception, E2, extends Exception, ... En extends Exception> { public Result build() throws E1, E2, E3, ..., En { // check the attributes and return createdResult; } }
Each of the E-th type parameter is representing independent single condition and can be used for whining: Was the associated attribute set or not? All of the conditions need to be satisfied otherwise the build() method keeps whining - e.g. throwing a checked exception. This kind of or-behavior is impossible to achieve with a single return type in Java type system.
Drawbacks
The biggest strength of this pattern - e.g. N independent conditions is also its biggest flaw: Imagine clueless users seeing the signature with n-exception type parameters! They would be scared. On the other hand, the typical usage of the builder pattern doesn't expose the type variables at all:
Source src = Builder.newFromText("content"). attr1("value"). attr2(42). ... attrN(3.14). build();
The code looks OK in spite of enormous number number of generic type parameters. Most of the time the generics types remain invisible - especially if one uses HiddenBuilder pattern as well.
Evolution
An interesting question from an evolution point of view: what happens when we need to add a new essential attribute? Obviously, if we want 100% BackwardCompatibility then we should add new builder class:
public final class Builder2<E1,E2,E3 extends Exception> extends Builder<E1,E2> { }
this class can add new type parameters, overwrite the already existing methods of Builder to return Builder2, etc. But that isn't the best solution.
If 99% BackwardCompatibility is enough, we could add new generic type parameter into already existing Builder class. That is of course not source compatible - but thanks to erasure of generic types, it is binary compatible and that is what matters the most in Java. Moreover the common usage of the builder doesn't expose the generics in code at all - so extending the number of generic types parameters should be mostly harmless and it is my favorite solution.
Summary
Using checked exception to track essential attributes and to whine when they aren't set is a nice and no-overhead way to co-relate the requested runtime behavior with compiler and let users know at compile time that their code is wrong and needs additional tweaks. Evolution of this pattern works nicely with respect to binary compatibility in Java thanks to erasure of generic types.