I’m generally not one in the past who has studied other’s
development patterns, so I generally just go with whatever works best for me. I found not real long ago, that doesn’t really
work… So, I did actually put some thought into how I worked on my projects. This has been heavily exercised in the
restructure of RuneEngine V2 over the past year. The code base is the most organized and the
most stable, but also the largest it has ever been. Therefore, I am confident this is working well
for me. So, I decided to share some
details in my new design process towards development.
Ok, I’ve always done this, but I’ve taken it more seriously.
Whether I write it down or just
visualize it, or even go ahead and set it up in Visual studio. The first thing I do is define objects that I
need in a given area. I also ensure that
I know an approximation of what members I need.
Like I said, I’ve always done this; this is a pretty standard starting
point for planning phases.
The next part is where I determine which methods are used
commonly across the objects and/or if there are methods/properties that may
come in handy across other parts of the project. Then, I place all of these common methods and
properties in groups and apply them to interfaces accordingly. Ideally, the first code I write is at this
point. If you’re not overly fond of
interfaces and/or believe that they aren’t a necessary part of development you
may want to re-evaluate that statement and review MSDN and observe the
inheritance trees of .NET objects. I
would agree they’re not required, but when you practice this way you get a few
amazing advantages you may not have thought of.
First of all, one example is extension methods for
interfaces. These are a great addition
to an interface driven design pattern. You
can define behaviors with these interfaces and avoid rewriting the same logic
in every implemented object. This is
more appropriate than inheritance; because it is flat there is no call stack
for this. So, this does become an
optimization later. One major con is
that interface casting is expensive, you can control how much time you spend
casting interfaces in many ways, so this is not overly an issue. An example of a major pro would be
extensibility of your code base. Along
with the extension methods, other methods that are geared to accept the
interfaces and not objects can act on any object that implements them. This gives phenomenal ability to add whatever
you need to adhering to the rules in the interface, and all of your code base
will support it. This is my purpose in
designing this way. One issue that you
run into that is not really a pro or a con is that you do have to define
everything, every time. So, it does take
more work to add new objects than deep inheritance trees and abstract objects,
however this does promote flatter code overall, so again it runs faster.
As I am designing the interfaces, I put a lot of thought
into a few things. Will it return an
error code or throw exception? Do these
methods require specific implementation that is not optional? Can I confine them into fewer methods? Should this be publically accessed? These are good questions to ask because they
guide the process into making useful interfaces that are not too tedious and
remain useful. It is easy to make an
interface that is a total waste. The
most useful interfaces in RuneEngine V2 are the IContentReference and derived
interfaces. The reason these are so
useful is because they define rules that allow the RenderManager to use them without
the need of any further information about the content. IMeshContentReference for example, is used
for custom geometry, models, animated models, and will be used for terrain as
well. One interface can handle all types
of rendering. To make it even more
useful, this interface can be defined by a 3rd party and game
specific code, and guess what? Now, that
custom rendering logic is available to the RenderManager as well.
After I do all of this and start rolling with the project
and ensuring that my interfaces work in their desired environments and the
objects I’m building can get their full behavior from this set up. I go through again and make sure the
interfaces don’t have any methods that across the board aren’t used. The IMeshContentReference did have a few
methods that weren’t being used by the RenderManager or the other objects so
they were removed. The objects that
relied on them kept these methods, but the interface does not define them so
they became object specific.
The last big key to the puzzle is actually one of the more
important, that is done from the beginning, but mentioned last for its
importance. CHUNK IT, do not try to list
out every interface and all of its methods from the beginning. Build in chunks. This gives you time to focus on each
individual area closely. It also allows
you to overtime realize a more appropriate location for existing and new
interfaces/classes. I avoid moving
things in too big of a hurry, but I’d rather do it now than later. The longer you wait the more work it will
require to do. I generally define a
chunk as a new feature or system. Anytime
I create a new DLL I tried to handle its major features right off the bat.
A good example of how I work in chunks is this. When I was starting the restructure of the
content system, I knew I wanted to work in ContentReferences as before, but
they were going to become interfaces. So,
that was an immediate. I also, knew
already from prior that the ContentReferences did require some uniqueness so I
created the Mesh, Material and Texture reference interfaces. These were the most important and I believed
there was a chance I could confine all my rendering code to use them
exclusively (I was right). I didn’t
start working on the implementation of any of the reference object
implementations until after I had developed them into the RenderManager and the
RuneContentManager objects. I had to
confirm their use and entry, what methods they would require before every
implementing a single object. I did find
in implementing the objects that the methods required a little tweaking, but
there was little to no change during this stage.
The more general stance on this is as follows, start with a
list. Use the list to define rules in
the form of an interface. Start the
implementation at the most system level objects that use the interfaces as
parameters. Ensure the interfaces will
work then implement the objects. Make
minor changes if necessary as the objects are implemented. Only work in chunks of implementation as
opposed to the entire code base at once.
If an object doesn’t apply in any of these systems and uses no methods
that would be required in other objects, there is no purpose for an
interface. Basically, if you define
IContentShaderReference when ContentShaderReference is the one and only object
that will use it. Well get rid of the
interface. IContentReference should be
enough. However, if there is any chance
IContentShaderReference could be used in another case. Yes I would include it. Only exclude an interface definition if you
know there is no chance another object will exist in need of those methods that
will be passed into the same methods this object will. Simpler put, if this object could be singing “I’m
the only one” Then you probably don’t need an interface.
Most of this may seem pretty general, but as usual this may
help you. It’s just an approach I’ve
been using. You may want to review your
approach and see if you do any of this and whether or not the ideas may benefit
you. If you ever find yourself cussing
because you don’t have enough flexibility or you have to write large extensions
to your entire code base every time a new object is added. Then this is a good approach for you.
Happy coding.
No comments:
Post a Comment