Practical .NET

3 Ways to Manage Dependent Classes in the .NET Framework

Once you start implementing current design practices, you'll find that your typical object consists of a lot of other objects.

In a previous column, I discussed how you can structure your application to lower your maintenance costs while applying modern design principles (Interface Segregation, Dependency Inversion and the Single Responsibility principle). The goal is to create focused, dedicated objects that work together in a loosely coupled way to create your applications.

In that previous column, for example, I recommended using factory methods to retrieve a SalesOrder object and then configure that SalesOrder object with the various other objects it needed. The goal of using the factory method was to allow you to extend or even replace parts of your object structure without having those changes ripple through your application.

The goal of configuring the SalesOrder object with multiple other objects, however, is to work with simple objects that are easier to code, test, document, explain and understand … and that you can also assemble to use in ways that you might not have considered when you created the objects. But that does create the problem of how to pass the objects that the SalesOrder object needs to the SalesOrder object. What's the best way to configure the SalesOrder object? You have, in fact, three ways to pass an object (to "insert the dependency") to the SalesOrder and I'll look at all three in this column.

Leveraging the Constructor
One method is to use constructor insertion: You pass all the related objects to the SalesOrder's constructor when you new it up. Typical code looks like Listing 1.

Listing 1: A Factory Method To Configure a SalesOrder Using Constructor Injection
Public Shared Function GetSalesOrderById(ByVal soId As String, 
                                         ByVal custId As String,
                                         ByVal billingType As String, 
                                         ByVal shippingType As string) Returns ISalesOrder
  Dim so As ISalesOrder
  so = SalesOrderRepository.GetSalesOrderById(Id)
  Dim cust As ICustomer   cust = CustomerFactory.GetCustomerById(custId)
  // Similar code to retrieve Shipping and Billing objects

  so = New SalesOrder(cust, 
                      ship,
                      bill),
                      cust.Addresses(AddressType.Billing)                      
                      cust.Addresses(AddressType.Shipping))
  Return so
End Function

There are a number of problems with this approach. The first is that it might be unrealistic to expect the developer who's using the GetSalesOrderById method to know the CustomerId at the moment that the GetSalesOrderById method is called (the same is true of the shipping and billing types). Even if those values are available, we're being mean to the developer: Expecting the developer to pull together an enormous amount of information in order to use the factory method doesn't, in fact, make life easier for the developer.

Therefore, it might be easier to retrieve the SalesOrder object inside the factory method and then use the custId and the shipping/billing types retrieved with the SalesOrder to retrieve the Customer, Shipping and Billing objects.

There's another issue with passing objects to the constructor: It suggests that the relationship between the SalesOrder and the other object won't change over the life of the SalesOrder object. That's probably true of, for example, the relationship between the SalesOrder and the Customer object (notice that I'm referring to the life of the SalesOrder object, not the life of the sales order business entity). But it's probably not true of the Shipping object, which might change several times over the life of the SalesOrder object (in fact, it's possible that the reason the application is retrieving the SalesOrder object is to change the shipping method).

Setter Injection
What often works best with factory methods is to pass the related objects to a property on the object being configured (this is called "setter injection"). With setter injections, the factory method looks like this:

so = New SalesOrder
so.Customer = CustomerFactory.GetCustomerById(so.custId)
so.Shipping = ShippingFactory.GetShippingByType(so.shippingType)
so.Billing =  BillingFactory.GetBillingByType(so.billingType)
so.BillingAddress = cust.Addresses(AddressType.Billing)                      
so.ShippingAddress = cust.Address(AddressType.Shipping)

Of course, having a Customer property on the SalesOrder object allows the client to change the Customer object associated with the SalesOrder -- something that I just suggested is forbidden by the company's business rules. However, if the factory method and the SalesOrder object are part of the same class library, the setter portion of the Customer property can be declared as Friend (Visual Basic) or internal (C#). This ensures that only the factory method is allowed to set the property and that the client sees the Customer property as read-only. If that's not possible then having the developer pass the customer Id and constructor injection might be your best choice.

Setter injection also supports "optional" configuration. Often, for example, a SalesOrder may be retrieved without being used for billing purposes. In those scenarios, retrieving and setting the Billing property is unnecessary. To support those scenarios, the GetSalesOrderById method might support parameters that allow the client to specify "how much" configuration is required and, if the right parameter is passed to the method, skip setting the Billing property.

An alternative solution is to provide different factory methods that configure the SalesOrder for different tasks: GetSalesOrderById and GetSalesOrderByIdForBilling, for example. Only the second factory method would go to the trouble of setting the Billing property (and that second method would probably call the GetSalesOrderById method to do most of the work of configuring the SalesOrder).

Site Injection
The third option for inserting a dependency is "site injection": The object is passed as a parameter to the method that needs it. This is a great choice in, at least, two scenarios:

  • When the method isn't frequently used. The object being passed to the method only needs to be created when the method is actually called
  • When the method may be called multiple times with different objects

For example, the Shipping object has three different versions: One that calculates "normal" shipping costs, one that calculates "priority" shipping costs and one that calculates overnight ("express") shipping costs. And, with those three options available, it wouldn't be surprising to find that many parts of the application's UI are expected to present a list of all three of those costs.

To support that workflow, a good design for the SalesOrder object might include a CalculateShippingCost method that, when passed a ShippingCost object, returns the cost of that shipping method. Site injection isn't compatible with having factory method configuration, but you might use a factory method to retrieve the correct object to be passed to the method. The code to calculate express shipping might look like this:

Dim shippingMethodCode As ShippingTypeEnum
shippingMethodCode = so.ShippingMethod
Dim shipMethod As IShipMethod
shipMethod = ShippingMethodFactory(shippingMethodCode)
Dim shippingCost As Decimal
shippingCost = so.CalculateShippingCost(shipMethod)

That's a lot of typing, but I'm just trying to make what's going on as obvious as possible. Most developers would probably collapse those six lines into this single line:

shippingCost = so.CalculateShippingCost(ShippingMethodFactory(so.ShippingMethod))

To support generating the list of all shipping costs, the ShippingFactory might have a read-only property of all of the available ShippingMethod objects. The list of shipping costs could then be generated with code like this:

Dim shippingCost As Decimal
For Each sm As IShipMethod In ShippingMethodFactory.AllMethods
  shippingCost = so.CalculateShippingCost(sm)
  // Add shipping cost to UI
Next

The options I've discussed in this column (and the previous one) may strike you as an awful lot of overhead. If so, you're free to consider that, because I'm a consultant who's paid by the hour, that I'm just trying to drive up my billable hours. But, of course, you don't have to write all this code yourself -- there are dependency injection containers that will take care of finding and returning the related objects for you (the .NET Framework gives you two: Unity and the Managed Extensibility Framework).

But, regardless of whether you write all the code yourself or use a tool, if your goal is to create focused, dedicated objects while keeping your maintenance costs to a minimum, this is the kind of design you want to think about.

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

  • 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