Suppose that you have the following hierarchy of statistics-related classes, structured in a manner similar to the Template method pattern:
interface S {
// Method definitions up-to and including the S3 class
}
class S0 implements S {
// Code that counts samples
}
class S1 extends S0 {
// Code that calls the superclass methods and also computes the mean
}
class S2 extends S1 {
// Code that calls the superclass methods and also computes the variance
}
class S3 extends S2 {
// Code that calls the superclass methods and also computes the skewness
}
Suppose now that we want to extend each of these classes to e.g. check for the convergence of a metric. For my purposes, I do not need to do this extension at runtime. I can think of the following alternatives:
Create subclasses S0C
, S1C
, S2C
and S3C
from S0
, S1
, S2
and S3
respectively, each with a copy of the code that checks for convergence:
Use the Decorator pattern:
S
interface, which completely destroys any sense of modularity.
The only way for this requirement to be lifted, would be to forbid nested decorators in my code.If Java supported multiple inheritance, I would have probably been able to handle this by inheriting from both the statistics and a base convergence-checking (or whatever) class. Alas, Java does not support multiple inheritance (no, interfaces don't count!).
Is there a better way to handle this issue in Java? Perhaps a different design pattern? A more technical solution? Some sort of special ritual dance?
PS: If I misunderstand something, feel free to (gently) point it out...
EDIT:
It seems I need to clarify my goals a bit:
I do not need runtime object composition. What I want is to extend the capabilities of the S*
classes with new methods. If I could create subclasses as needed without code duplication, I'd probably do it that way. If I could do it in the location of use (unlikely), even better.
I'd rather not write the same code over and over again. Note: delegate methods and constructors are fine, I suppose, methods implementing algorithms are not.
I'd like to keep my interfaces modular. This is my main issue with the Decorator pattern - unless very specific nesting constraints are placed, you end up with a super-interface of all interfaces...
EDIT 2:
To address a few of the comments:
The S*
classes are structured using template methods:
class S0 {
int addSample(double x) {
...;
}
double getMean() {
return Double.NaN;
}
}
class S1 extends S0 {
int addSample(double x) {
super.addSample(x);
...;
}
double getMean() {
return ...;
}
}
My S*C
extended classes from the first solution would be like this:
interface S {
int addSample(double x);
double getMean();
}
class S0C extends S0 implements S {
int addSample(double x) {
super.addSample(x);
...;
}
boolean hasConverged() {
return ...;
}
}
class S1C extends S1 {
int addSample(double x) {
super.addSample(x);
...;
}
boolean hasConverged() {
return ...;
}
}
Note the duplication of the hasConverged()
method.
A convergence checking decorator would be like this:
class CC<T extends S> implements S {
T o = ...;
int addSample(double x) {
o.addSample(x);
...;
}
double getMean() {
return o.getMean();
}
boolean hasConverged() {
return ...;
}
}
The problem: If I want to combine another separator behavior besides convergence checking, I need a separate decorator e.g. NB
- and in order to have access to e.g. the hasConverged()
method, the new decorator needs to:
CC
CC
for its wrapped object type...S*
methods if I want to be able to use NB
with S*
objects without using CC
My selection of the Decorator patter was only for lack of a better alternative. It's just the cleanest solution I have found thus far.
When extending the S*
classes, I still need the originals intact. Putting e.g. the convergence functionality in a common super-class would mean that the associated behavior (and its performance impact) would now exist in all subclasses, which is definitely not what I want.
Based on your recent edit.
Decorator isn't suitable for this as you might have realised. This is because what it solves is the augmenting of a single functionality, not the augmentation of a whole class tree.
A possible way this might be accomplished is with strategy instead. Strategy is algorithmically focused; it allows you to decouple behavioral code (Sorry if a little C# slips in here and there)
Sample Class
public class S {
private List<Integer> Samples = new List<Integer>();
public void addSample(int x){
Samples.Add(new Integer(x));
}
public void Process(IOp[] operations){
for (Op operation : operations){
Process(operation);
}
}
public void Process(ICollection<IOp> operations){
for (Op operation : operations){
Process(operation);
}
}
public void Process(IOp operation){
operation.Compute(this.Samples);
}
}
Operations
public interface IOp {
// Interface is optional. Just for flexibility.
public void Compute(List<Integer> data);
}
public class Op<T> implements IOp{
// Generics is also optional. I use this to standardise data type of Result, so that it can be polymorphically accessed.
// You can also put in some checks to make sure Result is initialised before it is accessed.
public T Result;
public void Compute(List<Integer> data);
}
class ComputeMeanOperation extends Op<double>{
public void Compute(List<Integer> data){
/* sum and divide to get mean */
this.Result = /* ... */
}
}
class CheckConvergenceOperation extends Op<boolean>{
public void Compute(List<Integer> data){
/* check convergence */
this.Result = /* ... */
}
}
Usage
public static void main(String args[]) {
S s = new S();
s.addSample(1);
/* ... */
ComputeMeanOperation op1 = new ComputeMeanOperation();
CheckConvergenceOperation op2 = new CheckConvergenceOperation ();
// Anonymous Operation
Op<Integer> op3 = new Op<Integer>(){
public void Compute(List<Integer> samples){
this.Result = samples[0]; // Gets first value of samples
}
}
s.Process(op1); // Or use overloaded methods
s.Process(op2);
s.Process(op3);
System.out.println("Op1 result: " + op1.Result);
System.out.println("Op2 result: " + op2.Result);
System.out.println("Op3 result: " + op3.Result);
}
Pros:
Cons/Limitations:
Hope this fits your requirements :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With