Refactoring to Adaptive Object Modeling: Strategy
Introduction
Adaptive Object Modeling is a relatively new concept in object oriented design that has found some roots in advanced development processes. It allows that applications are in continual flux (or their requirements are) from adjustments to business rules, environments and user requirements. It's precepts are that using metadata for business objects, rules and process flows, instead of hard coded, static method code, makes the application more flexible to user/client needs. The AOM design model also requires that certain domain 'facts' are maintained, i.e. that there are controlled interpretations of the effects of the 'immediate' changes from user-generated metadata.
To better understand and practice the techniques of AOM design, I am writing this and several other articles that explain the usage and conversion (or refactoring effort) from a static code model to AOM design.
This article will deal with Strategy methodology of Adaptive Object Modeling. Strategy methodology maintains that the operations and business rules or code flow of a class should be held as a collection of properties, as opposed to single method calls, which can changed at runtime. A Strategy pattern is a set of algorithms. So a Strategy pattern as it relates to AOM is a group of different strategies that can be dynamically added as business rules to a entity at runtime.
BackGround
Using a Strategy design method is an interesting way to extend the configuration possibilities of simple class object methodology. It gives a way to define a class's operations and rules dynamically, or at runtime, from a database, configuration file or user interface. Virtually any meta-data source can define the operations for a given AOM class structure. The Strategy design method uses, in this example, interface contracts and reflection to help define the limits of the operational calls.
Here we see the exact Adaptive Object Modeling UML for the Strategy class pattern based on the Design Patterns GOF95 model. Notice that the Entity object accepts and has specific operations (or strategies) associated with it. The strategy deals with actual business rule and operational rule implementations.
How to use the code
We start this refactoring example where we left off from our other example
article Properties.
We still have the same Entity and EntityTypes, and now we would like to dynamically
add some operational methods or Strategies. To accomplish this we need
to create a class to hold our dynamic interface contracts that are loaded during
runtime. This takes the form of a simple container (or entity-attribute type)
object that holds the interfaces or contracts for speaking to different strategies.
This is loaded with metadata at runtime for the specific object operation that
we wish to implement. Notice we have added also a collection object OperationsCollection
,
which contains the shared operations between the entity and the entity type.
public interface IOperation { void OperationMethod(object[] parameters); } //Collection of attributes public class OperationsCollection { public void Add(IOperation obj) { //.....addition code to collection } }
Here we see that the EntityType
object from our last example has
an added parameter of type OperationsCollection
. This operations
collection will allow the Entity object to have dynamically loaded
meta-data driven business rules associated with it at runtime. Now we can store
any given business rule we like within our class objects, without recompile.
public abstract class EntityType { private string _typeName; private Type _type; private EntityAttributeCollection _attributes = new EntityAttributeCollection(); private OperationsCollection _operations = new OperationsCollection(); public EntityType(string name, Type type) { _typeName = name; _type = type; } public string Name { get{return _typeName;} set{_typeName = value;} } public Type TypeOf { get{return _type;} set{_type = value;} } public EntityAttributeCollection Attributes { get{return _attributes;} set{_attributes = value;} } public OperationsCollection Operations { get{return _operations;} set{_operations = value;} } }
We also have created functional method objects inheriting from IOperation
,
which define exact method operations we will allow to be adapted to our entity
object. Having the interface allows us to tie that interface to any class method
we wish, as long as that class implements IOperation
.
NOTE: The operational methods could be contained in other assemblies, the namespace
encapsulation becomes unimportant, because you can add attributes or methods
via the metadata, without concern to it's source at compile time. Also the operation
method interface IOperation
could be changed to dynamically define
strongly type parameters as well, but this will be saved for either another
article or your own invention. Remember, reflection is used heavily as we will
see in the factory class, and is the key to dynamic representation of contractual
interfaces.
public class JobOperation1 : IOperation { void IOperation.OperationMethod(object[] parameters) { Console.WriteLine("..." + this.GetType().Name + ".OperationMethod method called."); foreach(object obj in parameters) Console.WriteLine("... parameter Type:" + obj.GetType() + " Value:" + obj); } } public class JobOperation2 : IOperation { void IOperation.OperationMethod(object[] parameters) { Console.WriteLine("..." + this.GetType().Name + ".OperationMethod method called."); foreach(object obj in parameters) Console.WriteLine("... parameter Type:" + obj.GetType() + " Value:" + obj); } }
We now are ready to focus on the AOMFactory
class, which is a
static factory implementation loosely based on Deyan
Petrov's DataSourceFactory. The factory is where we actually will load our
runtime data for this example. Our example code will use an xml config file
for the actual meta-data, but in actual project code, as I stated above
a variety of sources are available, not in the least, the client or user interface.
Remember, this factory class is only for this example, and while I would recommend
a factory or builder pattern for this piece, it is up to you the developer to
find the suitable method for loading your classes from meta-data.
The first piece of data we get after this revision to our Properties
example is the meta-data for the operation. We will use this data to
build and define all the possible types of operational interfaces we can load
to our entity types, making them available to as needed to specific entities.
The class with the methods we wish to add inherits from the IOperation
interface, providing access to it's methods directly.
string name = Convert.ToString(hash["name"]); //the name of this attribute if(name == null || name == string.Empty) throw new FactoryException("No operation name specified."); //get the attribute type string strOperationType = Convert.ToString(hash["type"]); if(strOperationType == null || strOperationType == string.Empty) throw new FactoryException("No Type specified for operation " + name); //get the type for a strongly typed parameter Type operationType = Type.GetType(strOperationType); if(operationType == null) throw new FactoryException("Could not load class Type for type " + strOperationType + " for operation " + name);
Here we make sure the class implements the interface, and creates an instance of the interface from the class type that holds the wanted operational methods. As we said above this gives a lot of flexibility to business flow, and allows different assemblies to provide new data on the fly.
Type interfaceType = operationType.GetInterface(typeof(IOperation).FullName); if(interfaceType == null) throw new FactoryException("No interface of type IOperation exists for operation " + name); IOperation operation = (IOperation)Activator.CreateInstance(operationType); if(_entityOperations == null) _entityOperations = new Hashtable(); if(!_entityOperations.Contains(operation)) _entityOperations.Add(name,operation);
The config file defines the different operations by name and their implementation classes full type name. As we said above, the actual operational namespace can be internal or external. The meta-data from the config file appears thus:
<entityOperations>
<entityOperation name="JobOperation1"
type="Strategy.ImplClasses.JobOperation1" />
<entityOperation name="JobOperation2"
type="Strategy.ImplClasses.JobOperation2" />
</entityOperations>
Next we see we check to see if the hashtable that will hold our Strategy interfaces exists and contains the current data. If not we will add the new operation to the entitytype object.
EntityType entityType = (EntityType)Activator.CreateInstance( entityTypeOf,new object[]{name,entityTypeOf}); foreach(string attribute in attributes) .... foreach(string operation in operations) if(!entityType.Operations.Contains((IOperation)_entityOperations[operation])) entityType.Operations.Add((IOperation)_entityOperations[operation]);
The meta-data from the config file appears as below. Notice the operations
xml node. This is where we put all the possible operation types by name for
each EntityType instance. The operation names are comma delimited. This is how
we define which strategy and business rule relationships we will associate with
the entity instance.
<entityTypes>
<entityType name="ExecutiveJobEntityType"
type="Strategy.ImplClasses.ExecutiveJobEntityType"
attributes="Attribute1,Attribute3"
operations="JobOperation1,JobOperation2" />
<entityType name="EmployeeJobEntityType"
type="Strategy.ImplClasses.EmployeeJobEntityType"
attributes="Attribute2,Attribute4"
operations="JobOperation2" />
</entityTypes>
We now are at a point where we can test our code.
Here we see that the entity is first established from the entity type and it's operations have been called.
Expanding the Example
How can we expand this example to functional code? We need to establish how the operational methods influence program flow, and define our entity relationships and entitytype relationships if applicable. These items we will cover in the next article.
Points of Interest
This is the third installment in the series I am writing on real world adaptive object modeling. All examples and the bulk of this article are taken from my professional experience as an architect. The examples given are templates only, and the designer must keep in mind that they are the ones who must decide where different patterns, if any, may be best used in their code.
Deciding to perform a refactoring effort from existing code to a pattern or enhanced design model must be weighed on the necessity and need of the code itself. Patterns and Adaptive Models are only design templates, helpers to accommodate better overall design. I might stress that making the effort to use advanced design methodologies will strengthen your overall design ability, but like your basic coding skills, it is something learned and cultivated.
If this or any other in this series on adaptive object modeling design is helpful or you have questions or comments please e-mail me at chris.lasater@gmail.com.
Related Articles
Other articles in this series include:
- Type-Object
- Properties
- Strategies
- Entity-Relationships and Accountability/Cardinality
- Ghost Loading, Data Mapping...
- Interpreter methodologies
- Discussions on Practical Project usefulness of design.
- Discussions on the AOM framework, UI, and Advanced Concepts
History
This is the second revision and is the first installment in a series.
Credits
Thanks to: Joe Yoder, Ralph Johnson, for their various articles on the AOM pattern, Martin Fowler for his interest in my articles and his work on the Accountability pattern, and Doga Oztuzun, for his insight and sharing of his relevant project data. Also to anyone else I have forgotten.