Practical .NET

Strategy, State and Role Patterns in .NET

Implementing design patterns in the Microsoft .NET Framework isn't all that hard to do; understanding what each pattern does for you and picking which one to use is the real issue.

Once you start working with the "classic" design patterns (the ones listed in the book "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, Helm, Johnson and Vlissides [Addison-Wesley, 1994]) you start to notice that, along with their differences, there's overlap between patterns. The Strategy, State and Role patterns, for instance, all deliver similar benefits.

To begin, all three patterns eliminate logic from your code. Instead of multipurpose objects with many If…Then statements (hard to write and time-consuming to test) these patterns deliver dedicated objects that do one thing well: eliminate branching logic. After applications are built, the patterns allow you to extend the application objects without modifying existing (working) code. Instead, you enhance your application by creating new classes. The State and Role patterns also centralize logic. Rather than have multiple If…Then/Select…Cases statements scattered throughout your application, these two patterns move that logic to one place (and, often, into one statement). This not only simplifies development, it simplifies maintenance by providing a single point-of-control when that logic needs to change.

There are similarities in implementation, also. The Strategy and State patterns, for instance, both segregate code into different classes that look the same. The State and Role patterns both support business entities that change functionality over time.

There are even similar costs associated with applying these patterns: After implementing them, you'll end up with more classes. This means that, at one level, your application is more complicated than it would be if you didn't implement these patterns. Nor is implementing these patterns a matter of cutting and pasting code. All of the patterns have several options and you have to pick the ones that make sense for your application.

You don't want to end up with more objects (and added complexity at that level) without getting the corresponding benefits. Only by understanding which of these pattern meets your needs can you ensure that you pick the pattern you need and implement it correctly to get the objects you need.

Strategic Thinking
You should be thinking about using the Strategy pattern whenever, in a method, you have an If…Then statement that picks between processing options. You should especially be thinking about the Strategy pattern if it's possible, down the line, that you'll be adding more processing options and, as a result, turning that If…Then statement into a Select…Case statement. Implementing the Strategy pattern means moving each of those processing options into a separate class, and passing the right one to your method.

For instance, in a brick-and-mortar retail business, customers can typically order products in three different ways: to be picked up at the store, to be mailed or to be digitally downloaded. The BillCustomer method needs different logic for each order method to calculate the charge to the customer. With the Strategy pattern, the BillCustomer method accepts an object that holds the processing for the current cost calculation.

In order for the BillCustomer method to accept any of the three cost-calculation classes, they all have to look alike. That goal can be achieved one of two ways: all three classes can inherit from the same base object, or the classes can all implement the same interface. For this example, I've assumed that there's no common code shared among all of the classes that could be inherited from a base class. Instead, I've had the cost-calculation classes implement an interface called ICustomerBilling that the BillCustomer method needs to accept.

As the sample implementation of the Strategy pattern for the BillCustomer method below shows, the cost-calculation objects have several properties that must be set before retrieving the order cost from a read-only property (TotalCharge). As another option, the ICustomerBilling interface could've had a method -- called something such as CalculateTotalCharge -- that returned the total charge.

Public Sub BillCustomer(process As ICustomerBilling,
                        purchase As CustomerPurchase,
                        cust As Customer)
  process.Amount = purchase.Amt
  process.DeliveryMechanism = purchase.DeliveryMechanism
  process.CustomerLocation = cust.State
  process.PurchaseDate = DateTime.Now
  purchase.UpdateOrder(process.TotalCharge)
End Sub

Of course, some logic has to exist somewhere to instantiate the right cost-calculation class and pass it to the BillCustomer method. That might be a Factory class that configures the object that holds the BillCustomer method to the object's constructor (instead of to the BillCustomer method) or some dependency injection framework (see "Dependency Injection Frameworks and Design Patterns").

Designing Strategy
Designing the cost-calculation class isn't a trivial task. The goal is to move all of the logic out of the BillCustomer method and into the cost-calculation class so that the BillCustomer method can treat all the cost-calculation classes the same way. With this pattern, if changes are required to the digital download process, that class can be changed without touching the code held in the other classes or the BillCustomer method. If a new billing method is invented then a new cost-calculation class can be created, and only the code that picks the object to pass to the BillCustomer method would need to be changed.

The downside to this pattern: If there's a change that requires all of the processing objects to be modified then multiple classes will have to be changed; this includes each cost-calculating object plus, perhaps, the BillCustomer method and the code that passes the right object to the BillCustomer method. Your only consolation is that, because the changes are spread out over multiple classes, it's possible those changes can be done in parallel by multiple members of the team.

There are variations on the Strategy pattern. For instance, if every application that calls the BillCustomer method uses a different cost-calculation process, then setting up individual cost-calculation classes -- each of which is used exactly once -- seems foolish. Instead, provided the cost-calculation process can be boiled down to calling a single method, the BillCustomer method can accept a Func or Action instead of a class or interface. This allows the developer using BillCustomer to pass in a lambda expression. Rewriting the BillCustomer method to accept a lambda expression would look like this:

Public Sub BillCustomer(process As Func(decimal, string, string, date, decimal),
                        purchase As CustomerPurchase,
                        cust As Customer)
  Dim cost As Decimal
  cost = process(purchase.Amt, purchase.DeliveryMechanism, 
                 Cust.State, DateTime.Now)
  purchase.UpdateOrder(cost)
End Sub

Code to use this version might look like this (which specifies the total cost is just the CustomerPurchase object's Amt property, plus 10 percent):

obj.BillCustomer(Function (a,m,s,d) 
                   Return a * 1.1
                 End Function,
                 Purch, Cust)

The resulting code is more succinct but, perhaps, not as self-documenting as the version that didn't accept the lambda expression.

C# developers have another option if the BillCustomer method has some logic in it that depends on which cost-calculation object is passed to it: multiple dispatch. Multiple dispatch allows you to set up different BillCustomer methods for each cost-calculation object and have the right version of BillCustomer called at runtime based on which cost-calculation object is being passed. In this scenario, each BillCustomer method has to deal with only one kind of cost-calculation object.

These two BillCustomer methods will be called based on which cost-calculation object is passed:

Public Sub BillCustomer(process As DigitalDownloadBilling,
   ...
End Sub
Public Sub BillCustomer(process As PickUpAtStoreBilling,
  ...
End Sub

To call the right version of the BillCustomer method, an application would use code like this, where the cc variable holds some cost-calculation object:

obj.BillCustomer( (dynamic) cc, Purch, Cust);

Managing State
If you're looking for similarities, State and Role can be seen as variations on the Strategy pattern. As with the Strategy pattern, the goal is to move processing code within the class out into a separate set of classes.

The State pattern assumes that the class doesn't change its interface as it changes processing. Customers who haven't paid their bills (who are in a "defaulted" state) have the same methods and properties as customers who've paid all of their bills, though the processing behind the methods and properties for the two types of customers may be different. The Role pattern doesn't make that assumption: The Role pattern says that someone applying to be a customer has different methods and properties than a current customer or an ex-customer.

In the State pattern, a Customer class might have many methods that are affected by the customer's state. Rather than put If…Then statements in each of these members to check the Customer's state and do the right thing, all the code for each state is put in a separate class. The members of the Customer class with "state-dependent code" just delegate their processing to an equivalent member on whichever state object is currently in use. The Customer object just has to make sure the right state object is being used.

In the example in Listing 1, the variable holding the state object is called CustState (here, I've assumed that there's common code shared between the state objects, so I've made them all look alike by assuming they all inherit from a BaseCustomerState class).

Listing 1 Customer Class with CustState object.
Public Class Customer
  Private CustState As BaseCustomerState

  Public Sub New(CustState As BaseCustomerState)
    Me.CustState = CustState
  End Sub

  Public Property Status() As CustomerStateValue
    Get
      Return CustState.Status
    End Get
    Set (value As CustomerStateValue)
      CustState.Status = value
    End Set
  End Property

  Public Sub PurchaseOnCredit(purchase As CustomerPurchase)
    CustState.PurchaseOnCredit(purchase)
  End Sub

In the code in Listing 1, I've assumed that whatever code creates the Customer object passes a state object into the constructor, which then places that state object in the CustState variable. After that, the other members of the class just call equivalent methods on which the state object is in the CustState variable. Other options are to have the Customer object analyze its own state and set the state object based on those values.

If the Customer's state can change at runtime then code that replaces the state object with another one might also be required. To keep that code from being spread throughout the Customer object, it makes sense for the state object to have a GetNextState method that, when called, returns the state object that should be put into the CustState variable next (this code probably belongs in the base class from which all the other state classes inherit). Alternatively, if the external application is to manage the state of the Customer object, the Customer class can expose the CustState field through a property so that the application can change the Customer's state (without that property the existence of state object is invisible to the application).

Changing Roles
The Role pattern also moves the code for each class role out into separate classes. Unlike the state object, an object can have multiple role objects in use at a time and those role classes don't necessarily share an interface. Instead, role objects add additional functionality to a business entity when the entity requires them. So, while a state object used within a business entity may never be visible to the application, a role object always is visible because the application must be able to interact with the role.

The Customer object must have a GetRole method so that the application can request a particular role. When a role object is requested from a business entity by the application, the business entity can return the object or throw an exception if the role isn't applicable. In the classical definition of the Role pattern, the business entity never creates role objects; instead, valid role objects are passed to the business entity (typically, through an AddRoles method) as part of configuring the business entity.

Typically, the role object needs to interact with the business entity (the Customer object, in this case) that created it. As a result, the role object usually accepts a reference to the business entity object in its constructor:

Public Class ApplyingCustomerRole
  
  Private cust As Customer
 
  Public Sub New(cust As Customer)
    Me.cust = cust
  End Sub

With both the State and Role patterns the developer must decide when to create the objects. If the business entity can switch frequently between different states or roles it may make sense to create all the possible state/role objects when the business entity is created (or, at least, hang on to any objects as they're created).

State, Role, Strategy and Inheritance
Problems that can be solved with the State and Role patterns are often confused with problems that can be solved with inheritance. For instance, rather than creating two state objects (DefaultedCustomer and GoodStandingCustomer), the developer might create two classes, both of which inherit from a base Customer class.

Using inheritance to handle state or role scenarios creates more problems than it solves: You end up instantiating a single customer first as one class and then as another as the customer changes state/role. It's much simpler to move a customer from one state/role to another by changing the state or role object. One way of testing whether you should use inheritance is to ask if the classes you're defining represent two business entities or the same entity changing over time. Another test: Ask if you'll be instantiating the same row in a table as two different classes. In both cases, the state or role pattern might be a better choice.

And do recognize that the name of the pattern isn't the point. For instance, the Role pattern wasn't defined in the original Design Patterns book in 1995. It was first defined by Francis G. Mossé in 2002 -- but I bet developers were implementing it long before it was named. In fact, the names of patterns might not be much help. If you've been developing a class that has what everyone has been calling two "modes," do you want to implement the State or Role pattern? If either mode provides functionality/methods that the other mode does not, then you should look to the Role pattern; if, however, the two modes just provide different processing for the same methods, then you should look to the State pattern. And if there's only one method that's different between the two modes, the Strategy pattern is probably your best choice.

As with the language you write code in and the editor you write that code with, understanding what these patterns do to you and for you (more than knowing their names) is what lets you do a good job.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube