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
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.
-
I forgot to mention that Render is part of the base class; sorry about that.Taco– Taco2018年11月15日 00:25:21 +00:00Commented Nov 15, 2018 at 0:25
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.
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
One public method (
Render
) for rendering the object (by callingRenderInternal
) and calling Render on all the children, which will in turn call their own chilren.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:
- You don't have to pass arguments
- The almighty class doesn't need to know anything about the object it is rendering
- Each class has its own behavior-specific implementation, e.g. by setting textures up or not
- Separation of concerns is improved
- 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
-
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– Taco2018年11月15日 01:26:09 +00:00Commented Nov 15, 2018 at 1:26
Explore related questions
See similar questions with these tags.