Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

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?

share|improve this question
3  
Hmm... you might want to look into the Visitor Pattern: en.wikipedia.org/wiki/Visitor_pattern –  FrustratedWithFormsDesigner Jun 25 '13 at 19:29
1  
This could also go on codereview.SE, but I don't think it's off-topic here either. –  Bobson Jun 25 '13 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. –  kevin cline Jun 25 '13 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. –  Jesse C. Slicer Jun 25 '13 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 Jun 25 '13 at 22:05
add comment

3 Answers

up vote 1 down vote accepted

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
share|improve this answer
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 Jun 25 '13 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 Jun 25 '13 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 Jun 25 '13 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 Jun 26 '13 at 15:48
add comment

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())).

share|improve this answer
    
+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 Jun 26 '13 at 15:32
add comment

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.

share|improve this answer
1  
This doesn't make much sense to me. Comparessor is supposed to compress objects, not create their instances. –  svick Jun 25 '13 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. –  m3th0dman Jun 26 '13 at 2:03
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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