Practical .NET
Leverage Lambda Expressions in Your Methods
Integrating lambda expressions into your methods is easy. The trick is in recognizing when to use them. And that means understanding when the strategy and factory method patterns are going to make your applications better.
By this point in your experience with the Microsoft .NET Framework, you've called methods, passing literals, variables and lambda expressions as parameters. You've also undoubtedly written methods with parameters that accept value types (integers, datetimes) and reference types (Customer objects, Window objects), but -- probably -- you've never accepted a lambda expression as a parameter to one of your own methods.
That's not really surprising: Using a lambda expression as a parameter is a niche solution, typically only required when you're implementing some version of the strategy pattern. The classic example of the strategy pattern is calculating shipping costs across a variety of shipping methods: post office, FedEx, UPS and so on. You could solve this problem by writing one method for each shipping method and requiring the client to call the right method. You could also solve the problem by writing a single method containing a Case…Select statement that chooses between the various calculations, based on a parameter passed into the method. The strategy pattern, however, is a better solution.
In general, you use the strategy pattern when your method provides a structure for performing some operation, but the developer calling the method needs to customize the process. And, even in those scenarios, lambda expressions are useful in some of those cases.
When the Strategy Pattern Is Useful
Whenever you see repeated methods doing the same thing or a Case…Select statement choosing among processing options, you should consider implementing the strategy pattern. To implement the strategy pattern solution you write a method that accepts an object that performs the parts of the process that vary from one call to another (there are variations on this solution and I'll discuss them later). You break the problem down into two smaller problems that you can work on separately: One part is creating the framework and the other part is creating a series of simple, focused strategy objects.
If the number of variations is limited, the simplest way to implement the pattern is to define an interface that describes the API for performing the operation. For shipping costs, that interface might be something as simple as this:
Public Interface IShippingCost
Public Function CalcShipping(Destination As String, Weight As Integer) As Decimal
End Interface
Two classes that might implement this interface are shown in Listing 1.
Listing 1: Two Classes Implementing Shipping Cost Strategies
Public Class PostOfficeShipping
Implements IShippingCost
Public Function CalcShipping(Destination As String, Weight As Integer) As Decimal _
Implements IShippingCost.CalcShipping
'...code to calculate Shipping through the post office
End Function
End Class
Public Class FedExShipping
Implements IShippingCost
Public Function CalcShipping(Destination As String, Weight As Integer) As Decimal _
Implements IShippingCost.CalcShipping
'...code to calculate shipping through FedEx
End Function
End Class
Your shipping cost method provides the framework for executing any strategy object by accepting classes that implement the IShippingCost interface, passing the parameters required by the method and doing something with the result:
Public Class Product
Public Sub CalculateShippingCost(shipCalc As IShippingCost)
Dim ShipCost As Decimal
ShipCost = shipCalc.CalcShipping(Me.CustomerAddress, Me.ParcelWeight)
AddShipCostToSalesOrder(ShipCost)
End Sub
As you can see, the framework method -- CalculateShippingCost -- decides what data will be passed to the strategy object's method (in this case an address and a weight), where in the process the strategy method will be called (right at the start), and what will be done with the result (added to the salesorder).
Configuring with Strategy Objects
Some process is required to pick the right IShippingCost object for your framework method. That's probably best handled through the Factory Method pattern, which centralizes that decision into one place. If you're going to use the Factory Method pattern to configure your object with the right strategy object, you probably won't want to pass the strategy object to the method that uses it (a method called "site injection"). Instead, you can set a property on the object ("setter injection") with the appropriate strategy object.
The sample factory method in Listing 2 shows the code for configuring a Product object with the right shipping strategy object.
Listing 2: A Factory Method for Configuring Product Objects for Shipping
Public Class ProductFactory
Public Shared Function GetProductById(ProductId As String)
Dim db As New ProductsContext
prod = (From p In db.Products
Where p.Id = ProductId
Select p).SingleOrDefault
Return prod
End Function
Public Shared Function ConfigureProductForShipping(
ProductId As String, Shipping As ShippingEnum) As Product
Dim prod As Product
prod = Me.GetProductById(ProductId)
Select Case Shipping
Case ShippingEnum.FedEx
prod.ShippingMethod = New FedExShipping
Case Shipping.Postal
End Select
End Function
End Class
The revised Product object to support setter injection would look like this:
Public Class Product
Public Property ShippingMethod As IShippingCost
Public Sub CalculateShippingCost()
Dim ShipCost As Decimal
ShipCost = Me.ShippingMethod.CalcShipping(Me.CustomerAddress, Me.ParcelWeight)
AddShipCostToSalesOrder(ShipCost)
End Sub
By using the strategy pattern, you make it much easier to add new shipping methods. If someone comes up with a new shipping method (for example, Amazon.com finally implements its drone delivery system) all you have to do is create a new strategy object and update the factory method to use that object. Because you don't have to touch either your framework method or your existing strategy objects, this pattern supports Vogel's First Law of Programming: You don't screw with working code.
Leveraging Lambdas
But that does assume that there are a limited number of ways that your framework method's processing can be customized. As long as the number of customizations is small, you can create a strategy object for each customization. If there are virtually an unlimited number of customizations, the strategy pattern breaks down.
That's where lambda expressions step in -- by allowing the developer using your framework method to pass in the actual code required to customize processing this time. With lambda expressions, there's no need to create a strategy object or factory method at all.
Methods that accept lambda expressions work much like my shipping cost example. Your method still provides a framework for processing: When the lambda expression will be used, what data will be passed to the lambda expression, and what will be done with the result returned from the lambda expression.
As you know from using lambda expressions, there are a variety of ways that you can take advantage of them. Sometimes all that the lambda expression has to provide is the property to be used in processing, sometimes the lambda expression provides a true/false value (usually used to decide which objects in a collection will be used), sometimes the lambda expression provides the processing to be done (as would be the case with my shipping cost example).
Those, however, are all descriptions of statement lambdas -- lambdas that will be evaluated by the framework method for processing. Your framework method can also accept expression lambdas as an Expression object that you use to build expression trees that can be extended or modified and, eventually, compiled and processed. Patrick Steele has a great column on using Expression lambdas so I won't address that here.
To support having a lambda statement passed to your method and then using that method inside your code, the only hard part is declaring a parameter for your method to accept the lambda statement. That's easy to do using the Func syntax to declare your lambda parameter. To declare a parameter using Func, you simply specify the data type of all the lambda's input parameters and, at the end of the list, specify the datatype of the return value.
Here's a method that accepts a lambda expression that must have two input parameters: One parameter is of type Customer and one parameter is a List of SalesOrders. This Func also specifies that the lambda must return a Boolean value:
Public Function DetectAdHocAuditIssues(AuditFunction As Func(
Of Customer, Of List(Of SalesOrder), Boolean))
Inside your method, to execute the lambda expression passed in the AuditFunction parameter, you just pass the appropriate parameters and catch the result. This DetectAdHocAuditIssues method passes Customer object and the Customer's related SalesOrders (retrieved earlier and held in the class' properties) to the lambda expression. If the lambda expression returns True, the method raises an exception:
Dim res As Boolean
res = AuditFunction(Me.Customer, Me.CustomersOrders)
If res Then
Throw New Exception("Audit condition failed")
End If
A program that uses this method might pass a lambda expression to the method, like this:
aud.DetectAdHocAuditIssues(Function (cust, ords)
ForEach ord In Ords
If cust.CreditStatus <> ord.CreditRequired Then
Return True
End If
Next
End Function)
Variations
There are numerous variations on these designs. A strategy object doesn't have just one method -- it can have as many methods and properties as you need to solve the problem. The critical criteria for a strategy object is that the framework method can't be required to change the way it does its job based on the strategy object passed to it -- all strategy objects should look alike to the framework method.
When considering where you'll pass the strategy object, consider if you want to be able to generate a different result every time you call the method (a very volatile requirement). In that scenario, you should pass the strategy object as one of the method parameters as I did in my first example. This site-injection technique would let me build a table of shipping costs by calling the method repeatedly, passing first the FedEx object, then the Postal object, and so on. If the strategy object won't change over the life of the object using it (a very static requirement), you could pass the strategy object to the framework object's constructor (a technique appropriately called "constructor injection").
You can also accept lambdas that don't return values: Just use Action instead of Func and only provide the input parameters. This is an example of accepting an expression that doesn't return a value, but does accept two parameters:
Public Function DeleteAdHocAuditIssues(AuditFunction As Action(
Of Customer, Of List(Of SalesOrder)))
You don't necessarily need a strategy object even if you're using setter or constructor injections. You can also use lambda statements with property injection, for example, by declaring your property as a Func, like this:
Public Property ShippingCost As Func(Of Customer, Of List(Of SalesOrder), Boolean)
Now you can set the property to a lambda statement:
aud.ShippingCost = Function (cust, ords)
ForEach ord In Ords
If cust.CreditStatus <> ord.CreditRequired Then
Return True
End If
Next
End Function
Your strategy objects don't have to share an interface. If the various strategy objects shared some code, a better solution would be to create a base class from which all strategy objects inherit. That shared code could then go in the base object, making it easier to create new strategy objects (just inherit from the base object to pick up the common code). With this solution, the framework class would reference the base class type where I've used the interface.
I'm sure there are lots of other choices out there. Whichever one you choose, using the strategy and factory patterns, making intelligent choices among constructor/setter/site injection and providing support for lambda expressions (where appropriate) can provide an extensible, flexible solution for your problems.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.