3

For all you OOD experts. What would be the recommended way to model the following scenario?

I have a certain class hierarchy similar to the following one:

class Base {
   ...
}

class Derived1 : Base {
   ...
}

class Derived2 : Base {
   ...
}
...

Next, I would like to implement different compression/decompression engines for this hierarchy. (I already have code for several strategies that best handle different cases, like file compression, network stream compression, legacy system compression, etc.)

I would like the compression strategy to be pluggable and chosen at runtime, however I'm not sure how to handle the class hierarchy. Currently I have a tighly-coupled design that looks like this:

interface ICompressor {
   byte[] Compress(Base instance);
}

class Strategy1Compressor : ICompressor {
   byte[] Compress(Base instance) {

      // Common compression guts for Base class
      ...
      //

      if( instance is Derived1 ) {
         // Compression guts for Derived1 class 
      }
      if( instance is Derived2 ) {
         // Compression guts for Derived2 class
      }

      // Additional compression logic to handle other class derivations
      ...
   }

}

As it is, whenever I add a new derived class inheriting from Base, I would have to modify all compression strategies to take into account this new class. Is there a design pattern that allows me to decouple this, and allow me to easily introduce more classes to the Base hierarchy and/or additional compression strategies?

5
  • 3
    Hmm... you might want to look into the Visitor Pattern: en.wikipedia.org/wiki/Visitor_pattern Commented Jun 25, 2013 at 19:29
  • 1
    This could also go on codereview.SE, but I don't think it's off-topic here either.
    – Bobson
    Commented Jun 25, 2013 at 21:16
  • 2
    The answer is language-dependent, so I added the C# tag. The answer would be quite different in dynamically typed languages. Commented Jun 25, 2013 at 21:43
  • 1
    I would go with an Attribute-based solution. Adorn the each class with an attribute which declares what type(s) of compression it supports and then your compressor can reflect on that attribute and get the information needed to choose what compression method to use on the instance. Commented Jun 25, 2013 at 21:56
  • @kevincline, thanks! Indeed I'm coding in C# - I tried to keep the question language agnostic but I guess I failed miserably ;) - I don't have experience in dynamically typed languages so I couldn't foresee the need of clarification.
    – sgorozco
    Commented Jun 25, 2013 at 22:05

3 Answers 3

3

Visitor Pattern:

enter image description here

Note that the code contains no if-then-else or switch-case structures to select the appropiate compressor. It's not needed since the visitor patterns allow for the appropiate dispatch.

public interface IBase {
    void accept(ICompressorVisitor e);
    void setName(String s);
    String getName();
}

public class Base1 implements IBase {
    private String name="";
    @Override
    public void accept(ICompressorVisitor e) {
        e.visit(this);      
    }

    @Override
    public void setName(String s) {
        this.name = s;      
    }

    @Override
    public String getName() {
        return "Base1("+this.name+")";
    }

}

public class Base2 implements IBase  {
    private String name="";
    @Override
    public void accept(ICompressorVisitor e) {
        e.visit(this);      
    }

    @Override
    public void setName(String s) {
        this.name = s;      
    }

    @Override
    public String getName() {
        return "Base2("+this.name+")";
    }
}

public interface ICompressorVisitor {
    void visit(Base1 base);
    void visit(Base2 base);
}

public class CompressorX implements ICompressorVisitor {

    @Override
    public void visit(Base1 base) {
        System.out.println("Compressing "+base.getName()+" using algorithm X optimized for Base1");

    }

    @Override
    public void visit(Base2 base) {
        System.out.println("Compressing "+base.getName()+" using algorithm X optimized for Base2");
    }

}

public class CompressorY implements ICompressorVisitor {

    @Override
    public void visit(Base1 base) {
        System.out.println("Compressing "+base.getName()+" using algorithm Y optimized for Base1");

    }

    @Override
    public void visit(Base2 base) {
        System.out.println("Compressing "+base.getName()+" using algorithm Y optimized for Base2");

    }

}

public class Test {

    public static void main(String[] args) {
        Base1 b1 = new Base1();
        b1.setName("a");
        Base2 b2 = new Base2();
        b2.setName("b");

        CompressorX x = new CompressorX();
        CompressorY y = new CompressorY();

        b1.accept(x);
        b1.accept(y);

        b2.accept(x);
        b2.accept(y);

    }

}

Output of test:

Compresing Base1(a) using algorithm X optimized for Base1
Compresing Base1(a) using algorithm Y optimized for Base1
Compresing Base2(b) using algorithm X optimized for Base2
Compresing Base2(b) using algorithm Y optimized for Base2
4
  • 1
    This still involves adding a new function to each and every compressor each time a new base is written, and feels overly complex even for that. It's easier to just call x.visit(b1); and let the compiler figure out at runtime which method to dispatch, and that introduces no more dependencies than already exist. It's better than the OP because needing a new method is easier for the compiler to check than needing a new switch case, but it doesn't achieve the stated goal.
    – Bobson
    Commented Jun 25, 2013 at 21:40
  • 1
    @Bobson Except your suggestion won't work if you have variable of type Base, which is the common situation here. And if you have new class that doesn't require special handling, then this pattern will still work, assuming you add void visit(Base) to ICompressorVisitor.
    – svick
    Commented Jun 25, 2013 at 22:02
  • @svick - Ok, that's valid. I hadn't considered the variable of type Base scenario - it's not well illustrated in the sample code here. If it gets edited to have two IBase variables and two ICompressor variables, instead of having the concrete types, I'll remove my downvote (it's locked in right now).
    – Bobson
    Commented Jun 25, 2013 at 22:33
  • 1
    Thank you. I didn't implement the Visitor pattern as suggested, however by researching the pattern I found the concept of Multiple Dispatch. I researched Multiple Dispatch implementations in C# and ended up using the dynamic type as suggested in Visitor and multiple dispatch via C# dynamic. As @Bobson suggested, there's no way to completely decouple the hierarchy from the strategy as I originally intended, but with this concept coupled with extension methods, I get closer to the goal.
    – sgorozco
    Commented Jun 26, 2013 at 15:48
1

Assuming that each combination of Derived and compression strategy compresses differently, then there is no way you can decouple them. You can refactor the linkage out somewhat to avoid switch and if/else statements, but you can't remove it entirely - each one will have to be aware of the others.

If you can separate the compression algorithm from knowledge of what it's compressing, you can store a ICompressor somewhere, and then populate it at runtime. ICompressor would have to operate on a standard type, and each Derived would need to be able to convert to that type (probably via function call like .Compress(this.PrepForCompression()) or .Compress(this.Serialize())).

1
  • +1 You are right Bobson, the combination of Derived and strategy is unique (there is common code that can be shared between Derived specializations, but it still is unique. Thanks. It really took me a while to grasp this fact.
    – sgorozco
    Commented Jun 26, 2013 at 15:32
-2

You can try a combination of strategy and factory:

public abstract class Compressor {
    private Base base;

    public Compressor(BaseType baseType) {//BaseType can be an enum
        if (baseType == DERIVED1) {
            base = new Derived1();
        } else if (baseType == DERIVED2) {
            base = new Derived2();
        }
    }

    protected Base GetBase() {
        return base;
    }

    public abstract byte[] Compress();
}

public class Strategy1Compressor : ICompressor {
    public byte[] Compress() {
        Base base = super.GetBase();//hope this is legal in C#
        //do compression logic based on base
    }
}

You can also provide a factory over the compressors:

public class CompressorFactory {
    public static Compressor CreateCompressor(CompressorType compressorType, 
                                              BaseType baseType) {
        if (compressorType == STRATEGY1_COMPRESSOR) {//again enum
            return new Strategy1Compressor(baseType);
        } else if //...
    }    
}

Note: I hope that the code above is legal C# code, as I am familiar with Java.

2
  • 1
    This doesn't make much sense to me. Comparessor is supposed to compress objects, not create their instances.
    – svick
    Commented Jun 25, 2013 at 22:04
  • @svick Compressor class has an abstract method called Compress, which has the purpose of compressing objects. CompressorFactory is the one that creates Compressor type objects.
    – Random42
    Commented Jun 26, 2013 at 2:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.