0

I have a couple of classes that I've nested together (not in the sense you may think so bare with me), to create what I call a prefab object (similar to Unity's prefab system).

In this particular case, I have a prefab called BarkingHorse that contains its own instances of objects CullingBird and DivingWeasel. All of these classes derive from a base class called RenderObject. There is a master object called Almighty that manages the rendering of individual objects. Now, when handling prefabs, the Almighty class should remain ignorant of objects contained within. For example:

// Almighty Class
public void Render(RenderObject obj) {
    obj.Render();
}

// BarkingHorse Class
public override void Render() {
    if (ShowCullingBird)
        cullingBird.Render();

    if (ShowDivingWeasel)
        divingWeasel.Render();
}

Now, normally this would be straight forward but in this case it isn't. BarkingHorse has a texture associated with it and is thus required to inform the DeviceContext prior to rendering. However, CullingBird and DivingWeasel do not have textures, and are rendered in a drab Color.Gray unless the DeviceContext has been notified that they do not have images. So for example with more basic code:

// Almighty Class
public void Render(...) {
    bool hasTexture = !string.IsNullOrWhiteSpace(obj.ImageFile);
    if (hasTexture)
        TellDeviceContextAboutTexture();

    obj.Render();

    if (hasTexture)
        TellDeviceContextTextureIsGone();
}

The code depicted above works well when the three class are separated and rendered as such; however, with the above-above code it does not as the DeviceContext still believes there is a texture. I know this is the issue because I've tested it.

My base class RenderObject contains the following properties that could prove to be useful here:

public RenderObject Parent { get; protected set; }
public List<RenderObject> Children { get; protected set; }
public void AddChild(RenderObject obj) {
    obj.Parent = this;
    Children.Add(obj);
}

However, I am unsure about taking the approach I am about to discuss as I believe from a public API standpoint this may not be a good idea. This leads us to the reason I'm here.


The Idea

I believe I could add a bool to the Render method on Almighty called renderChildren that could allow the method to render the object's children; but this is where my head begins spinning about the potential drawbacks. Either way, a basic representation would be:

// Almighty Class
public void Render(RenderObject obj, bool renderChildren) {
    bool hasTexture = !string.IsNullOrWhiteSpace(obj.ImageFile);
    if (hasTexture)
        TellDeviceContextAboutTexture();

    obj.Render();

    if (hasTexture)
        TellDeviceContextTextureIsGone();

    if (renderChildren) {
        foreach (RenderObject child in obj.Children) {
            if (!child.Enabled)
                continue;

            hasTexture = !string.IsNullOrWhiteSpace(child.ImageFile);
            if (hasTexture)
                TellDeviceContextAboutTexture();

             child.Render();

             if (hasTexture)
                 TellDeviceContextTextureIsGone();
        }
    }

    TellDeviceContextTextureIsGone();
}

The Questions

  • What are the drawbacks of this new idea?
  • Is there a smarter way to tackle this without passing around arguments?
  • Will what I have work fine from a public API standpoint or should I go back to the drawing board?

Notes

I forgot to mention that one of my concerns is that the ShowCullingBird type properties aren't able to be accessed from that level; in that case I would need to create an Enabled or Visible property on the RenderObject class.

Also, if you can think of a better title, please feel free to rename this post.

3 Answers 3

1

Instinctively, I feel like Runtime Polymorphism could go a long way here.

public abstract class RenderObject
{
    public void Render()
    {
        // Baseline render implementation goes here.
    }
}

public class BarkingHorse : RenderObject
{
    public override void Render()
    {
        // Custom logic to render a Barking Horse, and/or call base.Render()
        // or simply omit the override.
    }
}

To call it:

RenderObject obj = new BarkingHorse();
obj.Render();  // Renders a Barking Horse.  "Woof whinney."

Render() can go into the base class. You don't need The Almighty to make this work.

If you're having trouble managing the textures, have a look at the Flyweight Pattern.

1
  • I forgot to mention that Render is part of the base class; sorry about that.
    – Taco
    Commented Nov 15, 2018 at 0:25
1

Assuming that the Almighty class is necessary, and that TellDeviceContextAboutTexture needs to / should occur there, rather than in the RenderObject classes, I would suggest exposing an interface from that to the RenderObjects.

public interface IRenderingContext // Almighty implements this
{ 
    void WithTexture(Action renderingAction);
}

// Inside Almighty
public void Render(RenderObject obj) 
{
    obj.Render(this);
}

public void WithTexture(Action renderingAction)
{
    TellDeviceContextAboutTexture();
    renderingAction();
    TellDeviceContextTextureIsGone();
}

// Inside BarkingHorse
public void Render(IRenderingContext context)
{
    context.WithTexture(() => 
    {
        // Do rendering
    });

    if(ShowCullingBird)
        cullingBird.Render(context); // Does not call WithTexture
}

If this is always based on ImageFile, then it could be part of the base RenderObject.Render method.

1

I would put all of the logic for walking the object tree (which can be done recursively) in the base class, and offer another internal method for rendering only the object itself. So in other words there is

  1. One public method (Render) for rendering the object (by calling RenderInternal) and calling Render on all the children, which will in turn call their own chilren.

  2. One protected method (RenderInternal) which a developer must override to implement the rendering logic for that particular type.

For example:

abstract class RenderObject
{
    public string Name { get; set; }

    public List<RenderObject> Children { get; private set; } 

    protected readonly IDeviceContext _context;

    public RenderObject(IDeviceContext context)
    {
        _context = context;
        Children = new List<RenderObject>();
    }

    public void Render()
    {
        this.RenderInternal();
        foreach (var o in Children) o.Render();
    }

    protected abstract void RenderInternal();
}

Then I would have a separate derived class per behavior. Not per animal-- that would never scale. You only need a class if there is something about the behavior that depends on it or if different fields are needed (e.g. ImageFile is not needed in a textured object). So in this case, you can have an ImageRenderObject and a TexturedRenderObject.

The implementations are short because most of the logic is still in the base class. All you need to do is implement RenderInternal.

class ImageRenderObject : RenderObject
{
    public string ImageFile { get; set; }

    public ImageRenderObject(IDeviceContext context) : base(context) {}

    protected override void RenderInternal()
    {
        Console.WriteLine("ImageRenderObject '{0}' is rendering an image of {1}", this.Name, this.ImageFile);
    }
}

class TexturedRenderObject : RenderObject
{
    public TexturedRenderObject(IDeviceContext context) : base(context) {}

    protected override void RenderInternal ()
    {
        _context.StartTexture();
        Console.WriteLine("TexturedRenderObject is rendering {0}", this.Name);
        _context.EndTexture();
    }
}

Now you'll notice I added a _context field. This can be populated by constructor injection and a factory class:

class RenderObjectFactory
{
    protected readonly IDeviceContext _context;

    public RenderObjectFactory(IDeviceContext context)
    {
        _context = context;
    }

    public T Create<T>(string name) where T : RenderObject
    {
        return Create<T>(name, Enumerable.Empty<RenderObject>());
    }

    public T Create<T>(string name, string imageFile) where T : ImageRenderObject
    {
        var instance = Create<T>(name, Enumerable.Empty<RenderObject>());
        instance.ImageFile = imageFile;
        return instance;
    }

    public T Create<T>(string name, IEnumerable<RenderObject> children) where T : RenderObject
    {
        var type = typeof(T);
        var instance = (T)Activator.CreateInstance(type, new[] {_context} );
        instance.Children.AddRange(children);
        instance.Name = name;
        return instance;
    }
}

For testing, I created a dummy device context:

interface IDeviceContext
{
    void StartTexture();
    void EndTexture();
    void Paint(string o);
}

class DeviceContext : IDeviceContext
{
    public void StartTexture()
    {
        Console.WriteLine("Starting texture on device context.");
    }

    public void EndTexture()
    {
        Console.WriteLine("Ending texture on device context.");
    }

    public void Paint(string s) 
    {
        Console.WriteLine("Drawing {0}", s);
    }
}

Now your calls are much simpler. Here is how you'd call it from the top level. Note that here we are constructing the DeviceContext and factory with the new keyword, but in your application you may want to construct them in the Composition Root.

public static void Main()
{
    var factory = new RenderObjectFactory(new DeviceContext());
    var divingWeasel = factory.Create<ImageRenderObject>("DivingWeasel", "DivingWeasel.png");
    var cullingBird = factory.Create<ImageRenderObject>("CullingBird", "CullingBird.png");
    var barkingHorse = factory.Create<TexturedRenderObject>("BarkingHorse", new RenderObject[] {divingWeasel, cullingBird} );
    var almighty = new Almighty();
    almighty.Render(barkingHorse);
}

This way we meet your requirements:

  1. You don't have to pass arguments
  2. The almighty class doesn't need to know anything about the object it is rendering
  3. Each class has its own behavior-specific implementation, e.g. by setting textures up or not
  4. Separation of concerns is improved
  5. The solution scales without additional coding if new animals are added that have the same behaviors as the old animals.

Output:

Starting texture on device context.
TexturedRenderObject is rendering BarkingHorse
Ending texture on device context.
ImageRenderObject 'DivingWeasel' is rendering an image of DivingWeasel.png
ImageRenderObject 'CullingBird' is rendering an image of CullingBird.png

Here is the example on DotNetFiddle: Link

1
  • This is a very thorough example so +1; I would like to point out that I’m not actually coding animals. I am creating an object that has particular sub objects relevant to it but the relevant objects can be used in other ways therefore are their on classes and have VERY different representations and behaviors.
    – Taco
    Commented Nov 15, 2018 at 1:26

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.