Search code examples
vbadesign-patternsmodelingstrategy-pattern

Applying the Strategy Pattern


Imagine you have a factory where products are carried through the manufacturing process by belts. Somewhere in the midst of this, it is necessary to split a stream of products from a single belt input out onto multiple belts based on some business logic. Perhaps they need to be split out by color with output belts for red, green and blue products. Perhaps it's some sort of simple load balancing so products need to be split out based on percentages to each output belt.

                  ________________
                 / ___Color=Red___...
    ____________/ /_______________
 ..._Products___  ____Color=Green_...
                \ \_______________
                 \____Color=Blue__...

Or

                  __________
                 / ___60%___...
    ____________/ /_________
 ..._Products___  ____20%___...
                \ \_________
                 \____20%___...

Specifically I'm trying to reverse engineer the routing functionality of ProModel. The routing methodology should be able to be changed at runtime, as should the routing properties of the individual output "belts" (say changing the color criteria). If the routing methodology is switched back to one that was previously defined, it remembers the settings that were used.

To keep my code generalized, the products are "Entity" objects, the split off belts are "Route" objects, the internal routing logic is a "RoutingRule" and the whole input belt/internal logic/output belts thing is a "Router". Based on ProModel's usage, the "RoutingRule" applies to the "Router" and not the individual "Routes" - if the rule is RouteByPercent then every outgoing "Route" should have a percent, whereas if it is RouteByAttribute, each "Route" would have an Attribute/Comparator/Criteria (e.g. "Color", "=", "Blue") set to test against incoming "Entities".

I can see that using the Strategy Pattern would allow flexibility to support and swap out "RoutingRule" objects (for example the list of possible rules in the ProModel link); however, I don't quite see how I can have the state information for each rule stored without tightly coupling the "RoutingRule" and the "Route" objects.

When applying the Strategy Pattern to this case, how should I store RoutingRule-specific attributes that apply per Route? I'm trying to think through my options and I don't like any of them:

  1. The naive way would be to have member variables in the Route class for all possible RoutingRules: m_Percent, m_EntityAttribute/m_EntityComparator/m_EntityCriteria ... but that would be horrible for obvious reasons. Any time I added a new kind of RoutingRule, I'd have to add member variables to the Route class.
  2. The way I had been planning on doing it was with an associative array (Dictionary or custom HashTable) so attributes could be added or removed on the fly. This is generally fine, but there has to be some code to a) initialize the Rule-specific attributes b) tell me which attributes need to be set for the current rule (I can't just prompt the user for all the Route attributes because they may not all be used by the currently selected rule) c) validate that the Route attributes are in a consistent state.
  3. The only other way I can think of is to have the Route attributes stored inside the RoutingRule itself, an associative array inside an associative array so I can look up the Route by name and get a list of the exact attributes which apply per Route for the current rule. This way decouples the most, but then I'm not sure how to structure it so that asking a Route about its properties is a transparent process. Instead of Route.Attributes(strAttrName) like #1 and #2, it would be more like RoutingRule.RouteAttributes(Route.Name).Attributes(strAttrName).
  4. Maybe I'm overthinking this?

Incidentally, I'm doing this in VBA with all the limits that implies.


Solution

  • Check this skeleton written in some language:

    class Entity { // Or call this the EntityHolder
      Map attrs;
      RealEntity re; // The payload
    }
    
    abstract class RouteElement {
      void process(Entity entity);
      Map attrs;
    }
    
    interface RouterRule {
      RouteElement pickNextRouteElement(Entity e, List<RouteElement> allOutRoutes);
    }
    
    class Router extends RouteElement {
      List<RouteElement> outRoutes;
      RouteElement deadRoute;
      RouterRule routerRule;
    
      void process(Entity entity) {
        RouteElement nextRouteElement = routerRule.pickNextRouteElement(entity, outRoutes);
    
        if (nextRouteElement == null) {
          if (deadRoute) {
            deadRoute.process(entity);
          }
        } else {
          nextRouteElement.process(entity);
        }
      }
    
      void setRouterRule(RouterRule rr) {
        routerRule = rr;
      }
    
      RouterRule getRouterRule() {
        return routerRule;
      }
    
      void addOutRouteElement(RouteElement re) {
        outRoutes.add(re);
      }
    
      void setDeadRoute(RouteElement re) {
        deadRoute = re;
      }
    }
    
    class ColorRouterRule implements RouterRule {
    
      RouteElement pickNextRouteElement(Entity e, List<RouteElement> allOutRoutes) {
        foreach(RouteElement re in allOutRoutes) {
          if (e.getAttr("color") == re.getAttr("color") {
        return re;
          }
        }
    
        return null;
      }
    }
    
    class RandomRouterRule implements RouterRule {
    
      RouteElement pickNextRouteElement(Entity e, List<RouteElement> allOutRoutes) {
        int rand = random(0, allOutRoutes.length());
        return allOutRoutes.get(rand);
      }
    }
    
    class PercentRouterRule implements RouterRule {
      RouteElement pickNextRouteElement(Entity e, List<RouteElement> allOutRoutes) {
        int[] weights = int[allOutRoutes.length()];
    
        int prevWeight = 0;
        for (i = 0; i < weights, i++) {
          weights[i] = prevWeight + allOutRoutes.get(i).getAttr("percent");
          prevWeight = weights[i];
        }
    
        int rand = random(0, prevWeight);
    
        for (i = 0; i < weights, i++) {
          if (rand <= weights[i]) {
            return allOutRoutes[i];
          }
        }
      }
    }
    
    class RouterManager {
    
      Router routerBeingManaged;
      Stack<RouterRule> oldRouterRules;
    
      void setNewRouterRule(RouterRule rr) {
        oldRouterRules.push(routerBeingManaged.getRouterRule());
        routerBeingManaged.setRouterRule(rr);
      }
    
      void revertToOldRouterRule() {
        routerBeingManaged.setRouterRule(ooldRouterRules.pop());
      }
    
      ..
    }
    
    
    class Route extends RouteElement {
      List<RouteElement> route;
    
      void addRouteElement(..) {
        ..
      }
    
      void process(Entity entity) {
        foreach(RouteElement as re in route) {
          re.process(entity);
        }
      }
    }
    
    class MyApp {
    
      void main() {
        Route r = new Route();
    
        // Build the route
        r.addRouteElement(...);
        r.addRouteElement(...);
    
        Router router1 = new Router();
        router1.addOutRouteElement(..);
        router1.addOutRouteElement(..);
        router1.addOutRouteElement(..);
    
        RouterManager router1Mgr = new RouterManager(router1);
        router1Mgr.setRouterRule(new RandomRouterRule());
    
        r.addRouteElement(router1);
    
        r.addRouteElement(...);
        r.addRouteElement(...);
    
        // Get entities from somewhere
        r.process(entity);
    
        // Change the router rule
        router1Mgr.setRouterRule(new PercentRouterRule());
    
        // Get entities from somewhere
        r.process(entity);
    
        // Revert the router rule
        router1Mgr.revertToOldRouterRule();
    
        // Get entities from somewhere
        r.process(entity);
      }
    }