views:

397

answers:

4

Say I have a hierarchy of classes, let's use the classic Shape examples:

abstract class Shape
Circle : Shape
Square : Shape

I have a second hierarchy of renderer classes that handle the rendering of shapes in different ways:

abstract class ShapeRenderer
HtmlShapeRenderer : ShapeRenderer
WindowsFormsShapeRenderer : ShapeRenderer

Allowing these to vary independently would traditionally involve using the Bridge pattern. Allowing the rendering actions to be extended without modifying the Shape classes would traditionally involve the Visitor pattern.

However, both of these focus exclusively on extending the implementation side and not the abstraction side. Say I wanted to add a new Shape, say Triangle - I want to be able to support rendering the Triangle as well. Since both the Visitor and the Bridge pattern rely on "flattening" the abstraction hierarchy into a set of methods, e.g.:

public abstract class ShapeRenderer
{
     public abstract void RenderCircle(Circle c);
     public abstract void RenderSquare(Square s);
}

The only way to extend the Shape hierarchy is to modify the code of the base ShapeRenderer class, which is a breaking change.

Jon, to clarify: Using the Bridge or Visitor allows clients to provide alternative rendering implementations, but requires them to know about all potential Shapes. What I'd like to be able to do is allow clients to also be able to extend the Shape class and require them to provide a rendering implementation for their new class. This way, existing code can work with any type of Shape, without worrying about the particulars of rendering them.

Is there a common solution to this sort of problem usable in C#?

+2  A: 

I think it should be a breaking change. If you add a shape, the existing renderers clearly aren't going to be able to cope - they'll need to be changed.

You could change ShapeRenderer to add RenderTriangle() as a virtual (non-abstract) method which just logs the fact that it can't render appropriately, and then fix up the renderers one at a time, but fundamentally you're not going to be able to render the new type without more code.

What kind of non-breaking change are you really hoping to achieve?

Jon Skeet
But we all want magic! [absolute encapsulation, etc :)]
mlvljr
+1  A: 

Design to an interface not an implementation.

Hey - I get to use the same answer twice today (i guess it's arguable that Renderer is an implementation)...

I'm not sure I'd go with the ShapeRenderer class. What about an IRenderHTML, IRenderWindows that are implemented by the shape classes?

You get extensibility with the Shapes as well as with the Renderings.

I think it may be better OO to say hey circle go render yourself, than to pass the circle to a utility class for rendering. You could readily add new shapes and new renderings by letting the shapes do the rendering themselves.

mson
We had considered this, but I'm not sure that the shape itself should have to specify on what platforms it can be rendered. The drawback here is that adding another rendering platform requires going back and implementing the new rendering interface on all shapes.
If (in the unlikely event) you have to implement a new platform, you have to implement the render anyway. A polymorphic render for any platform may reduce impact on your displayed content. Adding an additional interface to an existing class should be pretty low risk.
mson
If you really think that implementing an interface is too big a drawback, you could delegate the render. I think this makes the design more complex than it needs to be.
mson
+1  A: 

My solution here would almost definitely be to use an Abstract Factory, in which case I'd load a dictionary of ShapeRenderers keyed by type, where type is a sub-class of Shape and let the factory provide the ShapeRenderer required for each Shape (and possibly platform, eg. Window, Web, iPhone).

Doing it this way means that adding a new shape would require only a change to a config store so the factory knows what renderers to map with what shapes, and a new assembly containing the concrete implementations.

PolyglotProgrammer
The only sticking point is that the renderer interface would have to use the abstract Shape class. The concrete renderer would then have to downcast the Shape that was passed in in order to work with it. Usually the presence of a downcast is an indication that something isn't modeled right.
+2  A: 

How about the strategy pattern? Where the strategy is a reference to a RenderEngine implementation. When you want to add a new shape, you create a new implementation of the rendering engines which know about the new Shape implementation and implements the corresponding rendering function. You add a virtual function to Shape which acts as a helper function to select the correct shape rendering function - i.e. Circle objects call the renderCircle() function etc.

In C++ that might look something like:

class Triangle : public Shape
{
  public:
      Triangle( const RenderEngine& whichRenderEngine );
      void render( void ) { renderStrategy->renderTriangle( *this );

  private:
      RenderEngine* renderStrategy;
};

class TriangleRender : HTMLShapeRender
{
   public:
      // if inheriting from concrete class, all other rendering functions 
      // already exist... otherwise re-implement them here.

      void renderTriangle( const Triangle& t ) { /* impl */ }
};

HTMLRenderer r; // doesn't know about Triangles.
Circle c( &r );
c.render();

Square s( &r );
s.render();

// Now we add Triangle
TriangleRenderer tr;
Triangle t( &tr );
t.render();

Square s2( &tr );  // tr still knows how to render squares... 
s2.render();
ceretullis