And Yet Another Level of Indirection

Published 14 January 08 10:35 AM | jons 

Overview

In this approach we produce machine-generated code that contains references to human-generated instances of objects that implement extension interfaces. The machine-generated code obtains these object instances by calling a human-generated factory mechanism. The extension objects returned by this factory mechanism might do nothing or might add functionality to the machine-generated code. As the machine-generated code executes, it, at various points in the life cycle of the class instance, invokes methods on these extension objects that, in turn, perform their magic.

Goals

It is important to remember what our goals are here. We want to generate most of the code using templates that we have designed and implemented (or at least swiped from someone else). In those cases where this machine-generated code is insufficient, we want to be able to add code to fill out the functionality of the machine-generated code to meet the needs of our application development. That added code should be "easy" to write; that added code should be easy to read and understand, that added code should not require any complicated logic (for example, reflection), and that added code should not require a lot of infrastructure code. Finally, the approach that we take should add minimal clutter to the application.

When Would You Use This Approach?

If I had a situation in which virtually every machine-generated class needed to be extended and those extensions could be shared across multiple domain classes, I would use the interface extension method. However, in my experience, the inheritance approach and partial classes approach that we discussed earlier are much easier to use. As I note later on in this post, the interface-based approach can add behavioral functionality but does very little if anything for adding data functionality.

Some Notes on Architecture

Let me take a moment to outline some architectural elements of this approach:

  • We would build code generation templates that produced machine-generated code.
  • Each machine-generated domain class would contain references to objects that implemented extension interfaces. Examples of these extension interfaces would include the validation of contents of the machine-generated class instance and authorization for applying various operations against the contents of the machine-generated class.
  • At various spots in the machine-generated code, the machine-generated code would invoke methods on these extension interfaces. For example, the machine-generated code would, before performing any operation, invoke a method on the authorization interface to determine if the current user was authorized to perform that operation. Each of these extension methods on the authorization interface would return an "allow or deny" indicator.
  • Each extension interface is realized by either a class, possibly abstract, or an object that implements an explicitly defined interface.
  • The object construction process would tailor each object instance by combining the machine-generated code with instances of objects that implement the defined interfaces.
  • The object construction process would obtain these instances of extension objects from factory methods.

What Can We Extend?

Let's step back for a moment and take a look at the relationship between the machine-generated code and the human-generated code in the extension method. Unless the machine-generated code passes a reference to itself (that is, the machine-generated domain object), the extension code has no way to access the contents of the domain object. (Actually, it is possible through some use of reflection to obtain this data but that violates one of our goals to make things simple for the programmer producing the extension method.) Even if the machine-generated code passes in a reference to itself when it calls the extension method, the type of passed-in parameter in the method must be quite general. In the worst case, the passed-in parameter must be typed as Object. If all domain objects inherit from a small number of well-known classes, it may be possible to declare the type of the passed in parameter more narrowly. But, in either case, the only way that the extension code can access the actual domain class instance is to cast it into the appropriate type. Even then, the extension code is limited to the public surface of the domain class. If a particular property is read-only, the extension code cannot change the value of that property (without being overly clever in the use of reflection).

Thus, the interface approach is most suited to adding behavioral functionality to the machine-generated code, particularly when the extension functionality only requires read-only access to the contents of the domain object. For example, the machine-generated code could call a method on a validation interface (passing in the reference to the domain object) to get back a set of errors. The validation method needs to have access to the contents of the class but it does not need to make any changes that are specific to the extension. The machine-generated code can handle the returned errors in a standard way. Basically, any function that returns values (without other side effects) is suitable for this approach.

One interesting intersection between behavior and data is to define extension methods that optionally load child collections. For example, we might define an Employee object in our domain that has several child collections associated with it; it may well be the case that a particular child collection is only needed under certain circumstances. We could define a set of extension methods that "lazy load" the child collections. In those circumstances where we did not need the child collection, we could provide an extension object that did nothing; in other circumstances where we did need a particular child collection, the factory methods would provide an extension object that loaded the contents of the child collection. Obviously, some care must be taken to ensure that the domain class responds appropriately when the methods involving a particular child collection are invoked and the domain class is setup to not load that child collection.

The Implementation of the Extensions

My usage of the word "interface" in this post really refers to two different ways of defining the relationship between the caller and called method. First, one can define an interface using a class with virtual methods that could (or perhaps must) be overridden. In a circumstance where there are no extensions, the factory method can return an instance of the base class, which probably does nothing. In those circumstances where extension logic is required, the factory method can return an instance of a derived child class, which presumably does something other than nothing. Second, one can define an explicit interface and then produce a series of classes that implement this interface; here, it would be necessary to create dummy implementations of each interface to satisfy the situation where no extension was required to a domain class. The choice about which one of these approaches to use involves consideration of a number of factors, all which are beyond the scope of this post. 

What Is the Behavior of the Factory Methods?

Each factory method could determine the appropriate class to return based on one of three different approaches:

  • First, the factory method could return the same object all the time for a given class; when the machine-generated code for class X asks for the authorization object for class X, it always get an instance of the same object.
  • Second, the factory method could consider the environment of the application as a part of deciding which object to return; for example if the application were working in a detached mode (where the central database was not available), the factory method might return a instance of class that is different from what it might return when the central database was available. Note that the environment also includes configuration options.
  • Finally, the factory method could consider the contents of the object when deciding what extension to return; this implies that the construction of the object is deferred until the contents of the particular object instance have been populated. For example, the GetAuthorization method on the authorization interface might return a different authorization object if the Employee data was in one division versus another division.

The dynamic decision-making capability of the factory methods is a major benefit of this approach. However, this benefit is, at the very least, at the outer edge of code generation; specifically, you might well use this approach even if you were not using code generation.

How and When Do We Invoke the Factory Methods?

The templates that I use for code generation of domain classes typically have a number of shared/static functions that operate as domain class factory methods. The caller invokes these static/shared methods on the domain classes, possibly providing some parameters.  The domain class factory method assembles and returns an new domain class instance with the appropriate contents. If the extension interface instances are added to the newly-created domain object in this method, the object instance must provide some means to set the references to these extension interfaces; this could be done through the parameters passed to the constructor or through setter methods that are only provided to support the specification of the extension logic for a given domain class. It is also possible that the code in the constructor could do all of the tailoring; the domain class factory method simply instantiates the domain object and the constructor makes all of the calls to the extension factory methods. I typically do not do this because I want to pass in mock implementations of the extension objects to support unit testing; that is usually only possible by adding a secondary domain class factory method that allows the caller (in this case, the unit test) to provide its own version of the extension.

All of the above assumes that the construction is done at the beginning of the life cycle of the object. It is also possible to tailor the object "just in time" when the code in the machine-generated class needs to perform some function. There are two rationales for delaying the tailoring. First, the application may never get around to invoking the methods that require the "just in time" tailoring. If the creation of the extension method is expensive and the usage of the domain class never gets around to that particular bit of tailoring, the performance of the application just got better. Second, the factory method may want to make use of the contents of the object to decide which of several different extension implementations might be returned to the object.

Not the Same Thing as Dependency Injection

While this approach is very similar to most dependency injection mechanisms, it is not the same. The typical dependency injection mechanism is setup to inject configuration-specified objects across the entire application, not on a class-by-class basis.

Evaluation

The advantages of this approach are that:

  • The relationship of the machine-generated and human-generated code is fairly clean. There is low coupling between the domain class and the extensions that are used to tailor it.
  • A given extension object could be used to extend multiple domain classes.
  • One can substitute different extensions to handle different environments and to handle unit testing chores.

The disadvantages of this approach are that:

  • The extension interfaces might define several methods and the specific class at hand may need to implement only one of them, leaving a lot of clutter in the other methods of the extension.
  • The extension class has only public-surface access to the parent domain class; this limits the extensions to those that only need read-only access to the domain class instance and can return all of the needed values as a return value from a function or as modifications to reference-type parameter objects.
New Comments to this post are disabled

About jons

Jon Stonecash is a technology consultant and has been designing, developing, and testing various kinds of software for such a long time that he has had the opportunity to make most of the serious software development mistakes at least once. His long term interests center about databases and the aspects of the application that handle data access and business logic. He is also interested in the tools that assist the development process, particularly code generation.