I've been pondering this question for a bit and I've come up with an alternate solution. This may be a bit unorthodox and anti-object oriented, but if you're not faint of heart read on...
Building upon the Apple example: the Apple class can contain many properties, these properties which could be categorized into related groups. For the example I rolled with an Apple class with some properties related to the apple's seeds and others related to the apple's skin.
- Apple
a. Seed
a1. GetSeedCount
a2. ...
b. Skin
b1. GetSkinColor
b2. ...
I'm using a dictionary object to store all the apples properties.
I wrote extension methods to define accessors to the properties, using different classes to keep them separate and organized.
By using a dictionary for the properties, you can iterate through all properties stored thusfar at any point (if you have to check all of them, as it sounded like you needed in your update method). Unfortunately you lose strong typing of the data (at least in my sample I did because I'm using a Dictionary< string, string>. You could have separate dictionaries for every type needed, but that would require more plumbing code to route the property access to the correct dictionary.
Using extension methods to define accessors to the properties allows you to separate the code for each logical categories of properties. This keeps things organized into separate chunks of related logic.
Here is a sample I came up with to test how this would work, given with the standard warning that if you were to continue down this path robustification would be in order (validation, error handling, etc.).
Apple.cs
namespace ConsoleApplication1
{
using System.Collections.Generic;
using System.Text;
public class Apple
{
// Define the set of valid properties for all apple objects.
private static HashSet<string> AllowedProperties = new HashSet<string>(
new string [] {
"Color",
"SeedCount"
});
// The main store for all properties
private Dictionary<string, string> Properties = new Dictionary<string, string>();
// Indexer for accessing properties
// Access via the indexer should be restricted to the extension methods!
// Unfortunately can't enforce this by making it private because then extension methods wouldn't be able to use it as they are now.
public string this[string prop]
{
get
{
if (!AllowedProperties.Contains(prop))
{
// throw exception
}
if (Properties.ContainsKey(prop))
{
return this.Properties[prop];
}
else
{
// TODO throw 'property unitialized' exeception || lookup & return default value for this property || etc.
// this return is here just to make the sample runable
return "0";
}
}
set
{
if (!AllowedProperties.Contains(prop))
{
// TODO throw 'invalid property' exception
// these assignments are here just to make the sample runable
prop = "INVALID";
value = "0";
}
this.Properties[prop] = value.ToString();
}
}
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (var kv in this.Properties)
{
sb.AppendFormat("{0}={1}\n", kv.Key, kv.Value);
}
return sb.ToString();
}
}
}
AppleExtensions.cs
namespace AppleExtensionMethods
{
using System;
using ConsoleApplication1;
// Accessors for Seed Properties
public static class Seed
{
public static float GetSeedCount(this Apple apple)
{
return Convert.ToSingle(apple["SeedCount"]);
}
public static void SetSeedCount(this Apple apple, string count)
{
apple["SeedCount"] = count;
}
}
// Accessors for Skin Properties
public static class Skin
{
public static string GetSkinColor(this Apple apple)
{
return apple["Color"];
}
public static void SetSkinColor(this Apple apple, string color)
{
apple["Color"] = ValidSkinColorOrDefault(apple, color);
}
private static string ValidSkinColorOrDefault(this Apple apple, string color)
{
switch (color.ToLower())
{
case "red":
return color;
case "green":
return color;
default:
return "rotten brown";
}
}
}
}
Here is a test drive:
Program.cs
namespace ConsoleApplication1
{
using System;
using AppleExtensionMethods;
class Program
{
static void Main(string[] args)
{
Apple apple = new Apple();
apple.SetSkinColor("Red");
apple.SetSeedCount("8");
Console.WriteLine("My apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
apple.SetSkinColor("green");
apple.SetSeedCount("4");
Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
apple.SetSkinColor("blue");
apple.SetSeedCount("0");
Console.WriteLine("Now my apple is {0} and has {1} seed(s)\r\n", apple.GetSkinColor(), apple.GetSeedCount());
apple.SetSkinColor("yellow");
apple.SetSeedCount("15");
Console.WriteLine(apple.ToString());
// Unfortunatly there is nothing stopping users of the class from doing something like that shown below.
// This would be bad because it bypasses any behavior that you have defined in the get/set functions defined
// as extension methods.
// One thing in your favor here is it is inconvenient for user of the class to find the valid property names as
// they'd have to go look at the apple class. It's much easier (from a lazy programmer standpoint) to use the
// extension methods as they show up in intellisense :) However, relying on lazy programming does not a contract make.
// There would have to be an agreed upon contract at the user of the class level that states,
// "I will never use the indexer and always use the extension methods!"
apple["Color"] = "don't panic";
apple["SeedCount"] = "on second thought...";
Console.WriteLine(apple.ToString());
}
}
}
Addressing your comment from 7/11 (the date, not the store) :)
In the sample code you provided, there is a comment that states:
"As you can see, I can't call
BasicBroodmother methods on "monster"
You realize you could do something like this at that point:
BasicBroodmother bm = monster as BasicBroodmother;
if (bm != null)
{
bm.Eat();
}
There isn't much meat to your code, (I understand it was just an example), but when I look at it I get the feeling that you should be able to improve the design. My immediate thought was having an abstract class for broodmother which would contain default implementations of any attributes/actions that are common to all broodmothers. Then specialized broodmothers, like the magical broodmother, would contain any specialized attributes/actions specific to the magical broodmother, but also inherit from the abstract class and if necessary override the nessecary base attributes/actions.
I would take a look at the Strategy pattern for the design of the actions so that the actions (i.e. behaviours like eat, spawn, attack) can be swappable based the type of monster.
[edit 7/13]
Don't have time to go into details right now (need sleep), but I put together some sample code showing a different approach.
The code consists of:
- Broodfather.cs - abstract class filled with all things common to different Broodfathers "types."
- BasicBroodFather.cs - concrete class that inherits from Broodfather.
- BroodfatherDecorator.cs - abstract class to be inherited by all Broodfather decorators.
- MagicalBroodfather.cs - this class decorates/wraps a Broodfather with "magic"
- BloodthirstyBroodfather.cs - this class decorates/wraps a Broodfather with "bloodthirst"
- program.cs - demonstrates two examples: The first starts with a basic broodfather that gets wrapped by magic then by bloodthirst. The second starts with a basic broodfather and wraps it in the other order bloodthirst, then magic.