I feel like I know design patterns, but this is escaping me. I have two separate projects, one as a library for the other. The library reads XML files and parses them into data structures. It is only meant to be responsible for converting to and from XML. My other project is an engine that acts on the data that the library assembles. It will probably contain classes that mirror the ones in the library, but with behavior methods. In case you're wondering, the reason for the separation of engine and library is that there is a third project, an editor, that modifies the XML data. It's a game.
I'm now creating a class in the library that represents a set of commands that can be executed in the engine, as well as an object that contains many different commands. My problem is that I can't figure out a good pattern for defining these commands and the container that abstracts them, such that the engine doesn't have to switch on types. If there weren't two projects here, just one, it would be easy. The different commands would implement an interface containing an Execute()
method or something of that sort. But it won't work here. The library doesn't have the ability to implement the behavior of the commands, just their attributes as pulled from XML.
I can, in the library, create an interface that the container can use, which contains methods to load and save the XML data. But the engine will still have to switch
on the commands in order to either:
- run a method in the engine's container class to modify its own state accordingly, or
- create the correct type of engine object which controls the command's behavior, and then execute that through its interface.
The container in question is a cutscene for my game and the keyframes in it. The commands will control its behavior, such as music, images, text, etc.
Here is some sample code from the library:
public class SceneInfo
{
/* Other stuff... */
public List<KeyFrameInfo> KeyFrames { get; private set; }
}
public class KeyFrameInfo
{
public List<IKeyFrameCommandInfo> Commands { get; private set; }
}
public class KeyFramePlayCommandInfo : IKeyFrameCommandInfo
{
public int Track { get; set; }
public static KeyFramePlayCommandInfo FromXml(开发者_开发问答XElement node)
{
var info = new KeyFramePlayCommandInfo();
info.Track = node.GetInteger("track");
return info;
}
public void Save(XmlTextWriter writer)
{
writer.WriteStartElement("PlayMusic");
writer.WriteAttributeString("track", Track.ToString());
writer.WriteEndElement();
}
}
I haven't written the engine half yet, but it would access keyframe.Commands
, iterate through it, and do... something. I'm trying to avoid a type switch, without over-engineering this problem. It might have classes like KeyFramePlayCommand
(compare to KeyFramePlayCommandInfo
).
Any good patterns that solve this problem?
For problems dealing with instantiation (particularly, abstracting instantiation away from logic) Factory usually comes into play.
public void KeyCommandFactory{
public static GetKeyCommand(IKeyCommandInfo info){
/* ? */
}
}
Because you're only exposing a single interface, we have another problem: which class to instantiate?
The best way I can think of currently is a key/type mapping.
public void KeyCommandFactory{
private static Map<string,Type> Mappings;
private static KeyCommandFactory(){
/* Example */
Mappings.Add("PlayMusic",typeof(PlayMusicCommand));
Mappings.Add("AnimateFrame",typeof(AnimateFrameCommand));
Mappings.Add("StopFrame",typeof(StopFrameCommand));
}
public static GetKeyCommand(IKeyCommandInfo info){
return (IKeyCommandInfo)Activator.CreateInstance(Mappings[info.CommandName]); // Add this property
}
}
If you are willing to stick to a convention you could even try using reflection, so that the command names matches the names of your Command objects, but this is of course less flexible.
public void KeyCommandFactory{
public static GetKeyCommand(IKeyCommandInfo info){
Type type = Type.GetType("Namespace.To." + info.CommandName + "Command");
return (IKeyCommandInfo)Activator.CreateInstance(type, info); // Add this property
}
}
I would put this outside of your library (in the engine). In the above, I pass the info instance as a parameter to the new instance of your command.
You could create an interface for a factory that creates a, command executor for a given command type. Also add a function to register the factory to your library. When you need to execute a command, you can give the registered factory the command, and get an executor object. If I understood your problem correctly, this code lives on the library side.
Now, from your application side, you can create a factory implementation, register all known command types with classes that can execute them, and finally register this factory to the library.
There are quite a few different ways to do it. I'll add assume you won't want to add void Execute()
to SceneInfo
, KeyFrameInfo
, or IKeyFrameCommandInfo
- they are info classes after all. So, let's create a SceneRunner
class:
public class SceneRunner
{
public ExecuteScene(SceneInfo scene) {
// loop over scene.KeyFrames and keyFrames.Commands, and execute
}
}
Since we don't know how to execute these commands, let's create a factory that would allow us to obtain a class that knows how to do it:
public interface IKeyFrameCommandFactory
{
IKeyFrameCommand GetCommand(IKeyFrameCommandInfo info);
}
And add the factory interface, and a registration mechanism to the runner. Since the application side will probably not want to deal with instances of this, let's make both static:
public class SceneRunner
{
static public RegisterFactory(IKeyFrameCommandFactory factory)
{
this.factory = factory;
}
static private IKeyFrameCommandFactory factory = null;
}
So far, so good. Now, time for the factory code (on the library client side). This is very similar to what Daryl suggested (you could use his code verbatim, by just adding the interface specification):
public void KeyCommandFactory: IKeyFrameCommandFactory
{
private static Map<Type, Type> mappings;
public IKeyCommand GetKeyCommand(IKeyCommandInfo info)
{
Type infoType = info.GetType();
return Activator.CreateInstance(mappings[infoType], info);
}
}
If you don't like to go into reflection, you can implement the Prototype Pattern on your commands, and use IDictionary<Type, IPrototype> mappings;
as your map, and mappings[infoType].Clone()
to get a new instance of a command.
Now, there are two things left: associating info classes with command classes, and registering your factory. Both should be fairly obvious.
For the associations, you can either add a RegisterCommand(Type infoType, IPrototype command)
to your factory, and add the associations to your program entry point, or chuck them associations in a static method, and call that method from program entry point. You could even design an attribute to specify the info class of a command class, parse your assembly, and add the associations automatically.
And finally, register your factory with your SceneRunner by calling SceneRunner.RegisterFactory(new KeyCommandFactory())
.
精彩评论