I am writing software for compiling programs. Therefore have a Compiler
that compiles a given sourcecode. It then returns a CompileResult
that is similiar to an Either
type (it is actually internally delegating to an Either
object). Now there can be 2 (non-exceptional) cases:
- The compile is successfull (
CompileSuccess
) - The compile failed (
CompileFailure
)
Let’s assume the compile was successfull. I have seen two or maybe three ways to handle it:
return new CompileResult( new CompileSuccess("success info") )
whereCompileResult
contains aCompileSuccess
.return new CompileSuccess("success info")
whereCompileSuccess
implementsCompileResult
.CompileResult.from( CompileSuccess("success info") )
which could possibly implemented to return either aCompileResult
object or aCompileSuccess
object (as in 2.)
Are there other ways to accomplish what I want to do? I saw the 2. “inheritation” way in Google guavas Either and in Scalas Either. In guava the 3. way is used for the Option
type. What are the differences and when to choose what?
8
The major issue with your two first solutions is that you couple the compile phase and the creation/instancing of the result. Your third way add a dependency of your class with an unique way to create the result (CompileResult).
Let’s solve the first point (delegating Result creation):
class SuccessDetails {
private String description;
public SuccessDetails (String description) {
this.description = description;
}
}
class FailureDetails {
private String description;
public FailureDetails (String description) {
this.description = description;
}
}
class FailResultDetailed extends Result {
private FailureDetails details;
public FailResultDetailed (FailureDetails details) { this.details = details; }
}
class SuccessResultDetailed extends Result {
private SuccessDetails details;
public SuccessResultDetailed (SuccessDetails details) { this.details = details; }
}
class Result {}
class SuccessResult extends Result {
private String description;
public SuccessResult (String description) { this.description = description; }
}
class FailResult extends Result {
private String description;
public FailResult (String description) { this.description = description; }
}
interface IResultFactory {
public SuccessResultDetailed success (SuccessDetails details);
public FailResultDetailed fail (FailureDetails details);
}
class ResultFactory implements IResultFactory {
public ResultFactory () { }
public SuccessResultDetailed success (SuccessDetails details) { return new SuccessResultDetailed(details); }
public FailResultDetailed fail (FailureDetails details) { return new FailResultDetailed(details); }
}
class Compiler {
private IResultFactory resultFactory;
public Compiler (IResultFactory resultFactory) { this.resultFactory = resultFactory; }
public Result compile (String code, Boolean result) {
if (result)
return resultFactory.success(new SuccessDetails("Ok"));
else
return resultFactory.fail(new FailureDetails("You've made a mistake"));
}
}
Now your are free to change the way you handle and create the Result object, but you might want to add some details.
The most intuitive way to solve it (for a OO developer) is to extract the details in a bunch of other classes:
class SuccessWithWarningsDetails extends SuccessDetails {
private int[] lines;
public SuccessWithWarningsDetails (String description, int[] lines) {
super(description);
this.lines = lines;
}
}
class CompilerThatThrowsWarnings {
private IResultFactory resultFactory;
public CompilerThatThrowsWarnings (IResultFactory resultFactory) { this.resultFactory = resultFactory; }
public Result compile (String code, Boolean result) {
if (result)
return resultFactory.success(new SuccessWithWarningsDetails("Ok", new int[] {1, 2, 3}));
else
return resultFactory.fail(new FailureDetails("You've made a mistake"));
}
}
But again, we couple the compile phase and the creation/instancing of the result.
We can try to apply the same process we have done the first time:
interface IResultDetailsFactory {
public SuccessDetails success (String description);
public SuccessWithWarningsDetails successWithWarnings (String description, int[] lines);
public FailureDetails failure (String description);
}
class ResultDetailsFactory implements IResultDetailsFactory{
public ResultDetailsFactory () { }
public SuccessDetails success (String description) { return new SuccessDetails(description); }
public SuccessWithWarningsDetails successWithWarnings (String description, int[] lines) { return new SuccessWithWarningsDetails(description, lines); }
public FailureDetails failure (String description) { return new FailureDetails(description); }
}
class CompilerThatThrowsWarningsWithoutCreation {
private IResultFactory resultFactory;
private IResultDetailsFactory resultDetailsFactory;
public CompilerThatThrowsWarningsWithoutCreation (IResultFactory resultFactory, IResultDetailsFactory resultDetailsFactory) {
this.resultFactory = resultFactory;
this.resultDetailsFactory = resultDetailsFactory;
}
public Result compile (String code, Boolean result) {
if (result)
return resultFactory.success(resultDetailsFactory.successWithWarnings("Ok", new int[] {1, 2, 3}));
else
return resultFactory.fail(resultDetailsFactory.failure("You've made a mistake"));
}
}
We are so close to the ending, if only we hadn’t the resultFactory depency…
Why? because we only care about producing Result, not how they are arranged.
interface IResultDetailsAndResultFactory {
public SuccessResultDetailed success (String description);
public SuccessResultDetailed successWithWarnings (String description, int[] lines);
public FailResultDetailed failure (String description);
}
class ResultDetailsAndResultFactory implements IResultDetailsAndResultFactory {
private IResultFactory resultFactory;
public ResultDetailsAndResultFactory (IResultFactory resultFactory) { this.resultFactory = resultFactory; }
public SuccessResultDetailed success (String description) { return resultFactory.success(new SuccessDetails(description)); }
public SuccessResultDetailed successWithWarnings (String description, int[] lines) { return resultFactory.success(new SuccessWithWarningsDetails(description, lines)); }
public FailResultDetailed failure (String description) { return resultFactory.fail(new FailureDetails(description)); }
}
class CompilerThatThrowsWarningsWithoutCreationAndResult {
private IResultDetailsAndResultFactory resultFactory;
public CompilerThatThrowsWarningsWithoutCreationAndResult (IResultDetailsAndResultFactory resultFactory) {
this.resultFactory = resultFactory;
}
public Result compile (String code, Boolean result) {
if (result)
return resultFactory.successWithWarnings("Ok", new int[] {1, 2, 3});
else
return resultFactory.failure("You've made a mistake");
}
}
Now we have not only a Compile phase that only depends on a factory to build detailed Results, but we also have results that only depends on a factory to handle the way they will be processed.
1