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/.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube