Chapter 8. Relayering your application

This chapter covers

  • Layering an application
  • Refactoring to layers
  • Separating concerns with layers

Now that we’ve covered some of the fundamentals of software development, it’s time to start applying them in larger concepts and practices. The first thing that we’re going to look at is the idea of layering your application code. Throughout this chapter you’ll see how layering application code makes use of fundamentals such as encapsulation and separation of concerns.

The concept and use of the term layer are fairly commonly known in software development circles. For years we’ve referred to the data access layer (DAL) when talking about how an application communicates with a data repository. Often projects go so far as to move the DAL code into a separate assembly as they physically reinforce the boundaries of that layer.

Developers and architects also commonly discuss the user interface as if it’s a separate entity altogether. It’s at this point, though, that the practice of layering applications seems to be paid nothing more than lip service. When you enter a brown-field project, it’s not uncommon to see that the code in the user interface has intimate knowledge of, and even responsibility for, functionality that has nothing to do with rendering the user interface for the user.

In this chapter, we show how you can work toward implementing a strong layering scheme in an existing brownfield application. The techniques discussed will enable you to increase the speed with which changes can be made to the code. They’ll also allow developers to focus on certain segments of the application without having to worry about impacting other areas.

Repairing the layering of an application is the first significant piece of architectural rework that should be done when trying to revitalize a codebase. Relayering isn’t something you’ll be able to implement in a couple of days. Creating layers in an existing codebase takes time and patience. It can also involve a significant amount of change to the existing code.

Don’t let the workload dissuade you. We wouldn’t recommend it if there wasn’t a corresponding benefit (we’ll talk about that as well). For now, let’s move on and set the next piece of the foundation to your new and improved codebase.

8.1. Pain points

A common pain felt on brownfield projects is one where changes are required but the affected code is spread throughout the codebase. In some cases, changing data access code may require working in the code-behind for an ASPX page, while other times it may be in a class file that’s in another project.

Consider the examples shown in listings 8.1 through 8.3 for a page that displays a list of musical artists and each one’s upcoming tour dates. Listing 8.1 shows a snippet taken from an ASPX page with a repeater outputting a list of artists.

Listing 8.1. A repeater displaying a list of artists

In listing 8.1, we’re using an ASPX repeater control to show a list of artists. Each item in the repeater also has a nested repeater for upcoming tour dates for each artist.

Listing 8.2 shows the code-behind for this page. It retrieves a DataSet from a service class and populates the repeater from listing 8.1 with it.

Listing 8.2. Code-behind for the page listing our artists

When the page loads, it retrieves the relevant data and binds the top-level repeater to it. As each item is bound to a particular artist row, the page also binds the nested repeater to the tour dates.

Finally, listing 8.3 shows another class that does the heavy lifting. It retrieves the list of artists from a database.

Listing 8.3. Service class that provides the list of artists to the code-behind

In listing 8.3, a DataSet is populated with two tables: Artists and Tours . The tables are also connected with a DataRelation .

On the surface, there’s an apparent separation of responsibility into various layers. The web page (WhosOnTour in listing 8.2) calls to a service (TourService in listing 8.3) that calls to a (static) data access class (TwilightDB).

But it doesn’t take too deep a dive to spot some obvious problems. One of the most spasm-inducing is how the repeater in listing 8.1 is tied directly to the column names that are retrieved from the data access class. The TourService is equally heinous by creating a relation between two DataTable objects in a DataSet based on the ArtistID column. If anything changes in the data structure being returned by the TwilightDB.GetData method, there will be ramifications all the way up to the UI.

This hodgepodge of code organization and poor separation of concerns can cause numerous problems for a development team. As you can imagine, developers may struggle to find pieces of code that are included in listings 8.1, 8.2, and 8.3 because those pieces exist in one place for one implementation but in another place when they move to a different part of the application.

What you may also see is a team that doesn’t know where to put their new code. These problems manifest themselves in questions such as “Should I put the Customer cancellation logic in the click event or in its own class? There are other screens doing it both ways, so is either all right?” The sense of orderliness may feel lost when you’re working in the code.

Before leaving the pain points, we should add a caveat. Creating clearly defined layers in an application won’t solve every instance of the pain points outlined here. But having layers will force on you some of the principles that we discussed in chapter 7, such as separation of concerns. It’s those principles that will help you to more easily solve the problems that aren’t directly addressed through layering. Let’s move on and define some of the base ideas of layers as we work our way to a well-organized codebase.

The first topic is a clarification on layers versus tiers.

8.2. Tiers versus layers

It’s important to distinguish between a layer and a tier. They’re often used synonymously (at least when talking physical separation), but for the purpose of this discussion they need to be differentiated.

8.2.1. Tiers

The concept of having multiple tiersin an application, at least in the Microsoft space, became popular in the 1990s as a way to explain the architectural requirements of a distributed software system. The growing popularity of the web focused the need to tier applications to accommodate the fact that there were three distinct components in the applications: the user interface, the business logic, and the data storage repository. This architecture is commonly referred to as three-tier, and it was an extension of the previous two-tier (or client-server) architecture, which would usually combine the business and data access layers. Figure 8.1 shows a typical three-tier application.

Figure 8.1. Traditional three-tier architecture. The user accesses the presentation tier, which talks to the business logic tier, which talks to the data access tier.

At the time, the notion of a tier provided quite a few benefits. The use of tiers allowed an application to distribute processing across more than one computer. This architecture became even more prevalent with the increased popularity of web services, which were often constructed over the business logic tier.

Plus, there was a noble theory that the various layers could be reused across multiple applications. For example, your data access layer could be used by more than one application, and similarly, you could create multiple UIs over your business logic layer. This technique was applied successfully to white papers and demo applications. In the real world, this panacea was a little more sporadic.

Since the start of the multitier concept, each tier has often represented a physical separation: the installation and execution of code on physically different hardware. In a three-tier system, that hardware may be a client computer, a middleware (or application) server, and a database server representing each of the three tiers. The definition of a tier has grown to have a one-to-one relationship with a hardware installation.

Although this physical separation of concerns is laudable, it’s primitive. Tiers make no space for the separations needed at a more granular level in the code. For example, the middle tier in a three-tier system, the business logic code, encompasses a variety of tasks, such as validation, logging, and business rules. Some of these may even span tiers. This point is where we turn to logical layers to represent the code at that granular level.

8.2.2. Logical layers

Because physical tiers don’t adequately represent all the layering concepts that should be built into applications, there’s also a need for the concept of a logical layer. A logical layer has no inherent knowledge of hardware. Its existence is based on your software design and its role is more task based.

The primary difference between logical layers and tiers is that often many logical layers will exist within one physical tier. For example, within your UI tier, you may have one logical layer that’s responsible for retrieving data from the business logic tier. Then you may have another layer that will take the data from that tier and convert it into a format more suitable for your UI. Then you could have a further layer that performs some basic validation of this UI object before sending it back to the conversion layer, which subsequently processes it and sends to the layer that communicates with the business tier. Figure 8.2 shows this logical layering within both the UI and business logic tiers.

Figure 8.2. Very often, a physical tier will contain several logical layers.


“Logical” tiers?

Although we defined a tier as a physical representation, it was (and is) common to combine physical tiers on the same server; the UI could reside on the same server as the business logic. In this case, your tiers behave more like all-encompassing logical layers that are composed of smaller layers. But for the sake of argument, it’s still helpful to think of a tier as a physical entity.


A logical layer could also span more than one tier. Tier spanning usually applies to layers dealing with infrastructure, such as logging or error handling. In a traditional three-tier architecture, spanning of tiers was often accomplished through the use of the ubiquitous Utilities project that all other projects referenced. We’ll come back to “layers” of this type in section 8.5.

Logical layers are typically focused in scope. We’ve already discussed one common example: a layer that converts objects from one form into another. This layer is common in applications that use web services, where you’re consuming an object that may be of someone else’s design. Creating a logical layer to convert it to something more appropriate for your specific needs will help you isolate yourself from changes to that web service. This concept will be covered more later in the chapter, and we’ll expand on it in chapter 12.

A logical layer provides two other benefits to your code. The first is a clearer definition of where different concerns, and the related code, should exist in your application. The old adage, “A place for everything and everything in its place,” applies nicely here. Each code concept has its place preassigned (the data access layer, or the validation layer, for example) and any code relating to that concept should be located in that place/layer.

The second benefit is a clear separation of the interaction of a layer with its neighbors. Between each layer is a seam, or gap, that needs to be bridged. Bridging the gap between layers requires the creation of contracts, through interfaces, so that both sides of the gap know what’s expected. The best way to accomplish this is through the use of interfaces and interface-based design, which we’ll discuss now.

8.3. Isolation through interface-based design

In chapter 7, the discussion of OO fundamentals touched briefly on the concept of interface-based design. To review briefly, this technique defines how two classes will interact with each other through its methods, properties, conventions, or however you wish to interface between these two objects. The expectations of what will be provided to a particular class, and what that class will return given those parameters, are determined by establishing and using an interface.

Once you’ve established this contract through the creation of an interface, each class, the consumer and the supplier, can go away and work on the implementation independently. The interface has been predetermined and both classes have a clear understanding of the others’ responsibilities. All that’s left is the implementation.

The reason for bringing up interface-based design here is that it can be useful in helping you define your logical layers. And in doing so, it helps you insulate your code from changes to other parts of the application.

By this point, you may still be a little fuzzy on the rationale behind logical layers. You may be asking, “What’s the point?”

Consider what happens when you use a third-party component for some piece of functionality. It exposes certain methods and properties, and you use the ones that provide value to you. How the work gets done behind the scenes makes no difference, as long as it does what the documentation claims.

But do you go through the exercise of testing the methods that you call on the third-party component? The correct answer is no, but truth be told, we’ve worked with some dicey libraries in the past. Generally speaking, you don’t because you assume that the vendor has done its due diligence and tested them for you.

Furthermore, the library is isolated from changes made in your application; you can make any changes you want in your application and they won’t affect the methods’ internal processing in the external component. Yes, changing the inputs to the methods you call will change the output. but the outputs will be well known (and hopefully well documented). Put another way, any changes made to your application don’t require the vendor to recompile its component.

In keeping with our practice of reinforcing fundamentals, we’ve essentially just defined encapsulation all over again.

You can think of a logical layer as a third-party component to the code that’s calling it. As you design the contracts through the implementation of interfaces that will define the layer, it’s helpful to do so with an eye toward isolating the layer just as if you were building it as an external library. So in the process of implementing these contractual interfaces, you’re isolating your code from other parts of the application.

That’s the isolation part. Insulation is a side effect of isolation. The interfaces defined for the interaction between neighboring layers demark the boundaries for the logical layer. As long as the contract is well understood by both the consumer and the producer (where the consumer is the client that calls the contracted code and the producer is the contracted code itself), the boundaries help insulate the layer from external changes. By using an interface, the contract between the consumer and the producer is clearly defined.

Think back to the third-party component. It was insulated from changes to the client (the calling application) by the very nature of the fact that it’s an external assembly. There’s no technical way that you can change its implementation.

We’ve talked quite a bit about contracts, which is probably a bit esoteric. In our experience, they’re often (but not always) synonymous with interfaces. A contract is an interface with the methods you wish to use in the contract. At some point, you’ll need a concrete implementation of that interface, but as long as the inputs and outputs are well defined, the client code can be written against it now for the purpose of compiling and testing. In chapter 9, we’ll talk about how to inject a concrete implementation of an interface into a class.

The interfaces that exist as layer boundaries also have the polite side effect of increasing reversibility (see chapter 7, section 7.2.5). Assuming a layer is well defined, isolated, and insulated, you have the option of replacing its code entirely if you so choose. Being able to reverse or entirely replace code in a segmented way is important when refactoring to use well-defined logical layers. The ability to isolate change or replacement of code allows you to work without worry of corrupting ancillary parts of the application. Let’s look at how you can accomplish and benefit from this.

8.4. Anticorruption layer

In the previous section, we talked about isolating layers from one another with well-defined contracts. We mentioned the benefits of not having to alter your logical layer if the client code is changed.

On the flip side, a well-defined contract can help protect the client code from volatility within the layer. Known as an anticorruption layer, it creates an environment where changes within a layer don’t affect calling code.

Although anticorruption layers can be used effectively when working within your own codebase, they’re most useful when working with external dependencies. Consider a third-party user control like one of the myriad of grid controls available on the market. It isn’t uncommon for a project to upgrade from one version of a user control to a newer one. During these upgrades, the user control may have changed in some fashion that breaks your code. When that happens you now have to work through your codebase in order to bring it back into harmony with the external dependency (in this example, the new version of the grid).

Sometimes making those changes can be a trying experience. The user control may be used in several (dozen) forms. This means you’ll have to visit each one to make modifications, and that’s a violation of the DRY principle discussed in chapter 7.

But what if you’d created a custom wrapper around that third-party control and used the wrapper in each of the screens? What if you’d created your own class that exposed only the functionality you specifically need in your application? And within that class, the methods exposed would simply delegate to the base grid?

Now whenever you upgrade to a new version of the control, the only changes needed are within the wrapper. As long as the public interface for the wrapper stays the same, none of the client code that uses it needs to know that you’re using a new control. (In this case, interface refers not to an explicit .NET interface but to the public properties and methods exposed by the wrapper class.)

This wrapper is an anticorruption layer. The next time the vendor upgrades their grid control, and you implement it in your application, the ripple effect through the screens won’t occur. Instead, the ripple will be contained within the boundaries of the custom wrapper.

There are also some benefits from a testability standpoint. Although it’s rare that you’d use a UI control in a class for automated testing, it’s sometimes done on file and message dialogs. It may seem odd to do so, but if you wrap the dialog in a well-defined interface, you have much more freedom to test a class that uses it. You can create your own test subclass that implements the interface or mock out the interface with a mocking engine. Testing this logic no longer becomes one of those areas of the application where you say, “Yeah, we should be testing it, but...”


Anticorruption layers in practice

Upgrading a user control may not be quite as simple as we’ve made it out to be. For example, the new version may include functionality that you want and that isn’t exposed by your existing wrapper. Now you need to alter the public interface of the custom wrapper. Also, if something has fundamentally changed in how the control (or whatever code you’re wrapping) functions, a nice fuzzy interface around it isn’t going to hide that.

Although we acknowledge that anticorruption layers aren’t always as clear-cut as we’d like them to be, they’re still valuable. You may balk at the idea of wrapping a third-party grid control or even a third-party text box, but the benefits far outweigh any trepidation you may feel about a bit of extra code. It may feel as if you’re adding extraneous code, but the feeling will pass. Most likely it will pass when you upgrade the control being wrapped for the first time.



Challenge your assumptions: Wrapping extreme

Because external dependencies are (by and large) out of our control on projects, we have to mitigate the effect of their changes as much as possible. The same goes for anything external. You can take this to the extreme, and we have in some cases. On some projects we’ve seen custom user controls created for the most mundane things. A class that wraps the standard Textbox control is one example.

In that case, the wrapper did save us some work. When we hit a requirement for a specific type of behavior that the standard text box didn’t support, we upgraded from the stock WinForms text box to a new third-party text box control. The process for that upgrade was limited to modifying the custom wrapper that had been created.


One of the important things to note about creating anticorruption layers is that you design them to meet your needs. You are, after all, applying the Adapter pattern onto the external dependency, so it makes sense to adapt it to meet your specific requirements. Creating the wrapper so that it’s agnostic to the dependency that it contains is usually best accomplished by using interface-based design. Create an interface that the client code will be able to work with consistently and that provides the encapsulated dependency with all information that it needs. When the time comes to modify that encapsulated dependency, the defined interface with the clients isn’t modified unless absolutely necessary. Now the changes to the dependency are hidden from the client code.


The Adapter pattern

Our apologies for unceremoniously throwing in the name of a pattern. We promise that this is an easy one to wrap your head around (so to speak).

The Adapter pattern adapts the interface from one class (the methods/properties of a grid control) into another (the interface we create specifically for our application). As you can see, a custom wrapper around a user control is a textbook definition of it. In fact, it is sometimes referred to as the Wrapper pattern.


When you’re creating anticorruption layers, an effective technique is to expose only the bare minimum required interface. Don’t fall into the trap of creating a wrapper for a third-party control and exposing all the properties, methods, and events that the third-party control exposes. If all you need from a grid object is the ability to bind data to it, expose only one method for that purpose. Exposing more than you require will increase the brittleness of your anticorruption layer, which will reduce its effectiveness in shielding the client code from changes. In addition, grid controls are notoriously chatty and wrapping every property is best left to interns or code generators.

Although we’ve specifically mentioned anticorruption layers in the context of protecting code from changing external dependencies, it’s also possible to do the same to your own code. In essence, logical layering and the encapsulation of functionality within each layer is providing an anticorruption effect to its client neighbor layer.

Internal anticorruption layers are effective when dealing with areas of the application that are in a steady state of change. If a piece of your application is regularly modified, and you’re seeing ripple effects of those modifications, consider implementing an anticorruption layer to lessen the burden of change on the developers. By creating seams, the anticorruption layer can significantly reduce the impact of the ripples induced by changes.

By segmenting code-behind interfaces, you’re also creating another useful effect: seams. Seams are locations in the code where clearly defined decoupling has occurred. An application with interfaces and interface-based design helping to create logical layers will have seams at this point of decoupling. Calling, or client, code that only references the interfaces (contracts) with its neighboring logical layers is decoupled and has an observable seam. See figure 8.3.

Figure 8.3. A seam is the interface that one logical layer uses to communicate with another.

Seams provide you with a couple of benefits. The first takes you back to the discussions on automated testing in chapter 4. In that chapter (specifically, section 4.3.2), we mentioned interaction testing.

As a quick refresher, interaction tests are those that verify the interactions with other dependencies that occur within the method under test. Strong logical layers, and their associated seams, can more easily enable interaction testing. Because all the dependencies for a method will have been created to comply with defined interface contracts, you’re able to generate mock objects that can take the place of the required dependencies. Being able to do this is one key to effectively and efficiently testing code in isolation.

The second benefit that you’ll quickly realize when refactoring a brownfield application is reversibility. As you’ll see later in this chapter, you’ll be able to first put existing code behind a seam. After that’s completed, the next step in your refactoring may be to rework the code that was just moved. Sometimes on brownfield applications the easiest way to do this is to start fresh. Because everything is decoupled due to the use of the newly created interface contract, it’s easy to implement a new concrete class that can contain your refactored or rewritten code. Because it’s in its own new class, and it must conform to the declared interface contract, you’re able to do this work in complete isolation.


One man’s seam...

Some of you may be familiar with Michael Feathers’ definition of a seam in his book Working Effectively with Legacy Code. He defines a seam as “a place where you can alter behavior in your program without editing in that place.” When we talk of seams, it’s using Feathers’ definition of object seams. That is, we can alter the behavior of the program by providing an alternate implementation of the interface we’ve used to define the seam. As he mentions, seams are extremely useful for testing, which is one of the reasons for creating them.

Feathers’ definition is more general because it includes other methods where behavior can be altered, such as with preprocessor macros and the Java linker. In the .NET world, these aren’t nearly as common as object seams.


Having that isolation can be extremely important in providing developers with the ability to create code that’s fully tested and verified before they reverse out the existing code with their new implementation. At that point, reversing the existing code becomes a relatively trivial process. The only thing that requires changing in the client code is how the appropriate class meeting the constraints of the interface is “newed up” (how a concrete class implementing the interface is created). If your code is nicely decoupled and using an inversion of control container (which we cover in chapter 9), changing that client code will probably require modifications only in one central spot.

The combination of interface-based design, logical layers, and seams will provide that ability to completely isolate code from different parts of the application. This isolation makes modifying existing code without creating application-wide ripple effects much easier.

Although seams and interface-based design can cleanly isolate segments of your code from each other, there usually are other pieces of functionality that need to cross those seams. Because these components, or modules, cross one or more of the seams that you’ve created in your application, they tend to be best represented as vertically aligned layers. We’ll discuss those now.

8.5. Vertical layers

Until now, we’ve talked about logical layers as horizontal concerns within the codebase. Those horizontal concerns perform some functionality specific to a particular tier. For example, consider a layer that prepares objects for serialization across a network to the business logic tier. This layer is a special-purpose layer within the presentation tier.

By contrast, a vertical concern is one that spans more than one tier. Logging has been mentioned as one example. Others include security checks, error checking, performance monitoring, and transactions. All these concerns are applicable across many, if not all, of the horizontal layers that you’d create. That is, logging is an action that could occur at the UI level, in a services layer, in the data access layer, and in the domain model.

Figure 8.4 depicts an architecture with both horizontal and vertical layers. The horizontal layers are color-coded by physical tier.

Figure 8.4. A vertical layer (along the left) will span several, sometimes all, horizontal layers. In this example, the physical tiers are grouped by color.

Because of the vertical nature of these concerns, it’s hard to fit them into the layering metaphor. But layers are very much a part of the vernacular in software design, so we’ll shunt aside our misgivings.

As is the trend in this book, we’re going to strongly suggest that you fall back on fundamentals. Like with all other areas of your code, you should fully encapsulate the vertical layers that you create. Good encapsulation will provide you with the ability to walk down many different roads when implementing something like logging in your code.

You can integrate vertical layers into the horizontal layers in a number of ways. The naive way is to sprinkle your code with calls to the vertical layer, as shown in listing 8.4.

Listing 8.4. Naive vertical layer implementation

As you can see, sprinkling calls, at the points indicated , through the application code will create noise and clutter in the code. Not only that, but you’ve thrown separation of concerns and encapsulation out the window in this code.

The method shouldn’t be responsible for logging. If you require that every method log information about its state and use, there will be a lot of extraneous lines of code like you see in listing 8.4.

Using this technique also opens the very real possibility that developers will forget to write the necessary line of code that logs the action or that they’ll create the line of code incorrectly. Although we believe this is a workable implementation, it isn’t optimal in many ways.


Noise and clutter

Noise and clutter in code is anything that distracts the reader from the main thing that the code is doing. One example is comments that state the obvious (like // Initialize the variable to 0). Noise can also be caused by lines of code, such as logging entry points, that bear no consequence to the purpose of the code that contains them.

The primary concern with noise and clutter is that the codebase becomes more difficult to wade through. As we mentioned in chapter 7, readability is a primary part of the maintainability of an application’s codebase. If the code is cluttered with inconsequential entries, the readability will decline and the maintainability will follow.

That said, reducing noise and clutter can become a religious programmer’s battle because of the subjective nature of “noise.” There’s no simple metric you can use to measure noise and clutter. All we can suggest is that you trust your instincts and do your best to minimize noise in your code.


Ideally, you’d like to remove the logging call line from listing 8.4. You don’t want any imposed line noise or clutter in your code, nor do you want to force your developers to remember to insert logging code into their methods. You also want to have all these needs satisfied while still retaining efficient and effective logging. You aren’t asking for much, are you?

It’s possible to accomplish these lofty goals. One technique is to use aspect-oriented programming.

8.6. Aspect-oriented programming

Aspect-oriented programming (AOP) is a way of applying separation of concerns to tasks that have traditionally been hard to isolate. Logging is the canonical example because it’s common and anyone who’s tried it has almost certainly done it the brute-force way as shown in listing 8.4 earlier. But it seems impossible that logging could be separated from the code that needs to be logged.

The promise of AOP is to do exactly that: isolate code that’s extraneous to the fundamental functionality of a class. You’ve already seen how this could be advantageous. Any code that isn’t directly related to a method’s core purpose is noise. For someone who’s scanning it (presumably to solve a problem or add a feature), it’s that much harder to determine what it’s supposed to do because the logging code needs to be mentally filtered out as you read it.

So the question remains: how can you separate code from a method that seems inextricably linked to it? Let’s see what the .NET Framework can do for you.

Here’s a trivial class that should be self-explanatory:

public class MyClass
{
public void DoSomething( )
{
Debug.WriteLine( "I'm doing some work" );
}
}

As fine a bit of code as we’ve ever written! We’ll leave the creation of a console application to execute this code as an exercise for the reader.

Now, let’s say you want to log all calls to any method in this class without having to add logging code to each method. One way you can accomplish this is by using a custom attribute. You’d like the previous code to look like this instead:

[Logging]
public class MyClass
{
public void DoSomething( )
{
Debug.WriteLine( "I'm doing some work" );
}
}

The only difference is the addition of the [Logging] attribute to the class. You need to create this attribute and add the ability to log the name of the method being called.

A LoggingAttribute class alone won’t work. On their own, custom attributes are merely annotations for classes, methods, and parameters. They store metadata but don’t do anything intrinsically. You need to do a little more work to execute behavior when the Common Language Runtime (CLR) finds an attribute.

A full explanation of what’s involved is beyond the scope of this book, particularly because there are better ways to go about it. But we’ll include the code to do so in listing 8.5 for completeness.

Listing 8.5. Custom logging attribute
[AttributeUsage(AttributeTargets.Class)]
public class LoggingAttribute : ContextAttribute
{
public LoggingAttribute() : base("Logging") { }

public override void GetPropertiesForNewContext(
IConstructionCallMessage msg)
{
var loggingProperty = new LoggingProperty();
msg.ContextProperties.Add(loggingProperty);
}
}
public class LoggingProperty : IContextProperty,
IContributeServerContextSink
{
public bool IsNewContextOK(Context newCtx) { return true; }
public void Freeze(Context newContext) { }
public string Name { get { return "LoggingProperty"; } }

public IMessageSink GetServerContextSink(IMessageSink nextSink)
{
return new LoggingAspect(nextSink);
}
}
public class LoggingAspect : IMessageSink
{
private readonly IMessageSink _next;

public LoggingAspect(IMessageSink next)
{
_next = next;
}

public IMessage SyncProcessMessage(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
if ( methodCall != null )
{
var messageToLog = String.Format(
"About to call {0}", methodCall.MethodName);
Debug.WriteLine(messageToLog);
}
return _next.SyncProcessMessage(msg);
}

public IMessageCtrl AsyncProcessMessage(IMessage msg,
IMessageSink replySink) { return null; }

public IMessageSink NextSink { get { return _next; } }
}

As you can see, creating a custom logging attribute isn’t trivial. It involves several classes and namespaces from the System.Runtime.Remoting namespace, such as ContextAttribute , IMessageSink , and IContextProperty .

Not only that, but when you’re finished, you still don’t quite get what you want. To use this attribute, the classes you wish to log must derive from System.ContextBoundObject, as shown here:

[Logging]
public class MyClass : ContextBoundObject
{
public void DoSomething( )
{
Debug.WriteLine( "I'm doing some work" );
}
}

With this code, whenever we call DoSomething on a MyClass object, it will output two lines to the Output window: About to call DoSomething and I'm doing some work (it will also output About to call .ctor for the constructor).

You can see the advantage of AOP by what’s missing from the DoSomething method. There are no calls to the logging mechanism (in this case, writing to the Debug window), but the method is still logged. When you look through the method, you’re no longer bogged down by infrastructure code and you can focus solely on what the method’s primary function is.

Having said that, this implementation isn’t ideal, which is why we’ve skimmed over the explanation of the code. First, the developer still needs to remember to tag the class with an attribute. Second, you may not want to log every single method in a class, though the code could be modified to work at the method level.

Finally, having your classes derive from ContextBoundObject is both restrictive and confusing. The derivation from the class is a coupling in itself, which you should avoid if possible. On top of that, a new developer looking at this may wonder why all the classes derive from it. It’s not immediately obvious that it’s being done to support logging.


Note

Keep in mind that listing 8.5 isn’t the only way to roll your own AOP mechanism in .NET. But it’s a good starting point and one of the less verbose ones.


Luckily, a number of frameworks are available for .NET programmers to use for AOP, such as Aspect#, PostSharp, and the Policy Injection Block in the Enterprise Library from Microsoft. The main goal of all these frameworks is to encapsulate cross-cutting concerns (vertical layers) in one location. The example of sprinkling logging entries through the codebase functions adequately, but then logging isn’t controlled in one location. In fact, it’s the polar opposite of being encapsulated in one place: it’s strewn throughout the code from top to bottom. And woe betide the developer who wishes to change the signature of that method call.

Most AOP frameworks operate under a concept similar to the hand-rolled example version. They intercept method calls and launch code before and/or after the originally expected method call. The interceptor code is what encapsulates the vertical layer or concern, which in this example is the logging. Now all of your logging capability is handled in one central location, the interceptor, and no longer is creating noise in the main classes and methods.

There’s an intuitiveness to this approach. It matches the common OnExecuting/OnExecuted event pattern used in several places in the .NET Framework. As such, it’s easy to learn and we’ve found developers pick it up easily.

Vertical layers may not immediately jump out at you when you’re designing your application. We’ve harped on logging, which is the canonical AOP example. But almost anything you do in your code that isn’t directly related to a function’s primary concern could be a candidate. You could use aspects to ensure methods are wrapped in a transaction, to ensure only certain users can execute methods, and even to perform trivial parameter validation (such as ensuring an age parameter is greater than zero).

These are common scenarios, though thinking of them as aspects may be new. Be aware of situations where you’re creating code that will affect many different layers and address them as cross-cutting concerns from the start. If possible, try to encapsulate the concerns in order to make your code more readable, to preserve the separation of concerns, and to ensure that your code adheres to the single responsibility principle’s main tenet: there should be one and only one reason to change any piece of code.

Now that you’ve spent a lot of time thinking about the structure of the various concerns in your application, you’re possibly wondering where the business logic fits into all this. If there are multiple tiers and even more layers in a well-factored application, there has to be somewhere to best locate the information that defines how the business works. We’d like to point you to one of the layers from figure 8.4, but it’s not quite that easy, so let’s take a look at this from a different perspective.

8.7. Taking a domain-centric approach

In many brownfield applications, the layering is sporadic and often nonexistent. Some screens may use a business logic service, and others may work directly against a database. A business logic class may be a hodgepodge of methods placed there for no other reason than that the developer didn’t know where else to put them.

As you prepare to refactor this code, it’s a good idea to think about the overall approach you’d like to take in achieving layered bliss. What’s your overall goal in designing the seams and layers? How do you decide where to place functionality in the layers?

We’ll be taking a domain-centric approach to this refactoring. You’ll refactor with an eye toward the domain that the application was designed to model. If the application is a family tree generator, we’d expect to have concepts like Ancestor and Descendent and Relationship modeled in it. If it’s a music library, then Song, Playlist, and Artist should appear.

This concept is abstract so a more concrete (though still fairly high-level) example will help illustrate.

Consider a traditional n-tier web application for a site that manages timesheets for consultants. The user navigates to a page that lists their timesheets to date. The code-behind instantiates a TimesheetService object and calls timeSheetService.GetTimesheets(consultantId), which retrieves the consultant information along with their timesheets.

The TimesheetService, in turn, may look like listing 8.6.

Listing 8.6. Traditional code-behind
public class TimesheetService
{
public ConsultantData GetTimesheets( int consultantId )
{
var consultantData = new ConsultantData( );
var consultantService = new ConsultantDataService( );
var consultantDataSet = consultantService
.GetConsultantDataSet( consultantId );
var consultant = new Consultant( );
// Insert code to translate the consultants into a
// Consultant object

consultantData.Consultant = consultant;

var timesheetService = new TimesheetDataService( );
var timesheetDs = timesheetService
.GetTimesheets( consultantId );

IList<Timesheet> timesheets = new List<Timesheet>( );
// Insert code to translate the timesheetDs into a List
// of Timesheet objects
consultantData.Timesheets = timesheets;

return consultantData;
}
}

The relevant data services would consist of standard ADO.NET code for returning datasets to this class. Figure 8.5 shows how this request might look.

Figure 8.5. Traditional n-tier representation of a data request. Although we show it here, the domain often doesn’t exist as a separate layer.

There’s nothing magical about this code. Regardless of whether the use of DataSets makes you cringe, they can be used quite successfully, and have been for several years.

But to a growing number of people (including us), this type of code obfuscates the real reason for creating the application. There’s talk of services and datasets and data objects, which overshadow the core objects we’re interested in: Consultants and Timesheets. As it is, we’ve encountered many applications that don’t even bother taking the time to convert the datasets into objects and simply forward them on to the web page for processing. Indeed, Microsoft doctrine and training encourages this.

Instead, some argue that you should start from the core objects that the application is abstracting. You should focus on the domain, rather than the database, the servers, and the programming language.

This approach, made popular by Eric Evans in his book Domain-Driven Design (Addison-Wesley Professional, 2003), as well as Jimmy Nilsson’s Applying Domain-Driven Design and Patterns (Addison-Wesley Professional, 2006), has led to the domain-centric approach we use when we move a brownfield application into layers.

To return to the list of timesheets, we’ll sketch a possible way of doing this with a focus on the domain (see figure 8.6).

Figure 8.6. With a domain-centric approach, the domain encapsulates both the business logic and the domain objects.

This diagram doesn’t look too different from the traditional n-tier approach. It’s almost as if we’ve combined the business logic layer and the domain.

The major difference is in the direction of the arrows. They all flow into the Domain layer. There’s no sense of hierarchy, where a request is passed from one layer to the next.

In this version, the web page holds a reference to a repository, which, in this case, is the IConsultantRepository. This interface lives in the domain along with the actual domain objects (Consultant and Timesheet). The implementation, on the other hand, is stored in the data access layer.


Where do you get your repositories?

If you’re new to the idea of repositories, figure 8.6 may have you wondering, “If I create a ConsultantRepository in my client code, doesn’t that mean I need a reference to the data access from my UI?” This question should raise some alarm bells.

There are different ways to avoid linking your UI layer to your data access layer. You could use a factory or service locator to create it. (The names are intuitive enough that you should be able to tell what they do—which is good because we don’t have the space to go into lengthy discussions of either.)

Another popular (and our favorite) way is through the use of an inversion of control container. We’ll talk about IoC containers more in the next chapter.

The salient point is that you can have the best of both worlds. Use a domain interface in the client while the implementation is neatly tucked away in the data access layer.


This is how you’re able to use repositories as domain concepts while still divorcing them from the database-specific code needed to implement them. Without getting into too many details of domain-driven design, repositories are responsible for retrieving objects from a datastore and saving them back again. It provides the client code with a well-defined interface (or contract) while still decoupling it from the underlying database technology.

Figure 8.6 is a much simplified version of the architecture we favor. That figure ignores concepts such as logging and data translation services and unit tests. To bring it back to the abstract, figure 8.7 is a more robust depiction of what we’re working toward.

Figure 8.7. The domain-centric architecture we’re working toward. It highlights the importance of the domain as the center of the application.

Note a couple of things about this diagram. First, notice that the domain is at the center of the diagram. This arrangement is to emphasize its importance and nothing more. This diagram could be flattened into a more rectangular version similar to figure 8.1 at the beginning of the chapter. If we did, you may notice that it’s not vastly different from the traditional n-tier architecture.

Second, the Utilities wedge needs some explanation. The code that it represents deals with vertical concerns, such as logging or performance monitoring, that span most or all layers of the application. Because of this, all aspects of the architecture refer to it. We’ve already covered vertical layers in detail in the previous section.

The Automated Tests wedge is similar to the Utilities wedge but in the other direction. It needs to refer to each layer rather than be referenced by each one. To use terms from chapter 5, the Utilities wedge has high afferent coupling while the Automated Tests wedge has high efferent coupling.

Finally, the Data Access wedge is kind of an outsider. There are no references to it, save from the Automated Tests. So how (and where) do we instantiate the data access classes? We alluded to this in the sidebar “Where do you get your repositories?” The interfaces for much of the data access classes are kept in the domain. To get concrete implementations, we rely on factories, service locators, and/or IoC containers. In the case of the first two, we’d likely need to add another layer to the diagram. This layer would lie between the Application Services and the Data Access wedges. (Technically, an IoC container takes the place of this layer so using it doesn’t obviate the need for it; you just don’t need to implement it yourself.) But including it in the diagram would require more arrows and there are already more than we’d like in it.

There’s much more that we could say about domain-driven design but, as we noted earlier, there are books that better serve those questions. For now, let’s talk about how you can work to implement layering in your brownfield applications.

8.8. Refactoring to layers

We’ve covered a lot of concepts in this chapter. Chances are, you’ve read this far and you’re at the point where you’re thinking “All well and good, but how am I ever going to implement this in my code?” Well, friend, it’s time to cover that very topic.

The normal reaction when implementing a wide-reaching concept like logical layering is to be confused about where to start. The codebase of a brownfield project is usually well established, and picking a spot to start refactoring can be a difficult and risky decision.

On top of that, you may have to decide how to start the refactoring. Like anything major you do with a codebase, refactoring to logical layers and seams is going to introduce some risk. You have to figure out the best ways to mitigate that risk. The remainder of this chapter is dedicated to helping you make these decisions and organize the refactoring as you’re undertaking it. Figure 8.8 shows the high-level steps we’ll take.

Figure 8.8. An overview of the steps involved when you refactor your application to use layers

The first step in the process is to choose where to start.

8.8.1. Pick a starting point

Not to confuse terminology, but a logical way to begin refactoring to layers is to examine your physical tiers. This approach is typically an easy way to identify where the seams are because you have a physical restriction already in place.

But your true starting point should be with a test, if at all possible. And if that’s not possible, make it possible. Only with an adequate safety net should you make sweeping changes like the ones we’re proposing. The code already works in a known way. When you’re finished, you had better be sure it still works in the same way.

Because ours is a brownfield application, you’ll almost certainly run into cases where writing a test for a piece of code seems impossible. It’s not. We’ve already referred to Michael Feathers’ Working Effectively with Legacy Code. The book has dozens of scenarios to address this specific concern. Indeed, that’s its very theme. From “I Need to Change a Monster Method and I Can’t Write Tests for It” to “I Don’t Understand the Code Well Enough to Change It,” you’ll be hard-pressed to find an excuse that hasn’t been covered.

Once you have a test (or two or five) in place and have identified where to place the seam, you have a choice of whether to work either up (toward the UI) or down (from the UI) from the split. We recommend working down. As you’ll see shortly, starting at the UI and working down allows you to thin out the responsibilities of each layer and push the remaining code further for later refactoring.

The process is akin to whittling. You start with a single chunk of code and shave a thin layer off while still leaving a slightly smaller (but still hefty) chunk of code to work on. Once the thin layer has been cleaned up, you go back to the big chunk, shave off another layer, and repeat the process until each layer is manageable. In this way, you make incremental improvements.

8.8.2. Make incremental improvements

Before we go further: do not underestimate the amount of work you’ll undertake! We cannot stress this enough. Once you recognize how to layer an application, it’s frighteningly easy to start a small refactoring and have it escalate. Often, this escalation happens in small steps so that you don’t even realize that the variable renaming you started 6 hours ago has turned into a major overhaul of your data access strategy.


Note

Don’t approach refactoring to layers on a Friday afternoon if you expect to have it done by the weekend.


Incremental improvement is the key to success. In chapter 2, we advocated the mantra check in early/check in often. You refactor to layers in the same way—in small, easily reversible steps, backed up by unit and integration tests.

Needing to work this way is why we spent the first six chapters on the ecosystem. The ecosystem is there to help you for just this sort of task. It’s designed for the babystep approach to refactoring so that if anything goes wrong along the way, you’ll not only know about it right away, you’ll also be able to back out with relative ease.

As a rule of thumb, start with a single screen, preferably a simple one that only reads data. Perhaps use an administration screen that lists users. Examine what it does and how it’s currently coded. Try to pick out, and define, a single interface (contract) that will define the first seam between the screen and the layer immediately below it.


Tip

Remember to design with an eye toward isolation and testability. This type of refactoring offers a great opportunity to increase the test coverage on the project whether through test-after development (TAD) or test-driven design (TDD) practices. As much as possible, you’d like each layer to be insulated from external changes either above or below it. If you start at the screen level, keep things simple. You aren’t re-architecting the entire code for this screen—just the logic for the screen itself.


Ignore the data access and business logic itself for the time being. It will be refactored in due time. Instead, focus solely on the way the screen itself interacts with other components and try to isolate those into well-defined interfaces.

Figure 8.9 shows an outline of how to accomplish this. Here, the first box depicts the original version of the code with no layering. In the first step, you identify a seam to interact with the layer immediately below it.

Figure 8.9. When refactoring to layers, the key is incremental changes—refactoring only a small portion of each screen at a time.

But notice that you don’t refactor the entire block of code to layers right away. Rather, you start with a single seam. You create the contract (interface) and, better yet, you’ve already got the implementation for it. It’s the original block of code you’re refactoring. You simply need to move it out into a separate class that implements the interface you’re using in the original screen.


Note

The box representing the initial layer in figure 8.9 will probably not be very clean. If you’re lucky, it should consist of a simple cut and paste of the original code from the screen you’re refactoring, except for any code that’s to remain in the UI layer. Your goal here is only to layer the application. Once the layers are defined, you’re at a good point from which you can launch further refactorings in isolation later.


Once you’ve created the first seam of your new layer, you should be at a good stage to check in the code. Again, the ecosystem comes into play and you can run the build script to make sure the application compiles and all tests pass, particularly the one(s) you created before starting your refactoring.

From here, you can apply the same technique to define further layers all the way down the request pipe.


Note

In figure 8.9, we’ve depicted each layer as being easily sliced off the one above it. In practice, you may need to use the same technique we did with the screen, where you define a single seam first, and then refactor the rest of the functionality into each layer.


Notice that we’re ignoring a huge blob of code in the screen which is visible to the left of the layers in each stage. This is intentional. When you start out, you’re working with a single seam in a single layer. The rest of the code can be left as is while you’re working out the details of the layers. When finished, you’ll have several layers defined but they’ll be very thin as you haven’t fully fleshed them out yet.

At this point, you can now further refine your layers by moving more code into them from the Screen Code area on the left.

In figure 8.10, you start from the last point in the previous diagram (figure 8.9). You pick out a bit of code to move into Layer 1, thus thinning out the screen code further. As you progress, you whittle away at the code, moving it further and further down the line but leaving pieces of it in each layer as you go until you reach the bottom layer.

Figure 8.10. Further refining the layers

Notice in both figures 8.9 and 8.10 that the overall size of the code doesn’t change. You’re always working with the same amount of code. All you’re doing is moving it to additional classes (which you create along the way) for the sake of maintainability.

You continue in this fashion until the final refactoring, shown in figure 8.11.

Figure 8.11. The final refactorings

Here, you remove the last vestiges of code from the screen that don’t belong there. Until this point, you’ve been paring away at the code, moving more and more of it out into appropriate layers. When you move the last bit of “lost” code into its appropriate place, you’re left with a fully layered architecture, at least for one screen. Now you can move on to the next screen.

Luckily, moving on to subsequent screens is far less work.

8.8.3. Refactor another screen

So now you’ve defined your seams and you have an appropriate set of layers for one screen.

As we warned you earlier, you’ve got your work cut out for you. But don’t be discouraged. The good news is that the first screen is by far the most difficult. Not only are you moving code around, you also need to define the layers (and the classes to go into them).

Once the first screen is done, you have two advantages:

  • The layers are defined.
  • Having gone through the exercise once, you have a better idea of what to expect.

Refactoring large codebases into layers

All of this information about refactoring your codebase to layers can be intimidating, especially if your brownfield project is large. Sure, we’ve suggested some tips for how you can get started and progress through the refactoring process. Like you, we’ve taken these ideas and looked at some large projects and thought that we’d never be able to successfully refactor them.

We won’t kid you at all. Significant architectural refactorings to large codebases are tough to complete. There’s only one reason for that: the sheer amount of code.

If you’re taking the slow but steady approach in which you’ll work on the refactoring when the team has time, refactoring large codebases into good logical layers will seem to take an eternity. It’s our opinion that the larger the codebase, the more dedicated your refactoring effort needs to be.

We’ve worked on projects like this and we’ve had success when refactoring to layers. Not all of the individual efforts were successes, but the project results were. The key for us when working on the large codebases was to make it a focused effort. And note that that effort wasn’t necessarily by the entire team. In some cases, it was only one person.

Another key factor for our success was working in isolation in the code (for example, through a branch in our version control). We discussed this concept in chapter 2. The isolation allowed us to make radical changes and experiment with ideas while still retaining an easy way to back out those ideas that didn’t work.

Don’t think that large codebase’s can’t be refactored, but be aware that they’ll impose a significant resource drain on your team. Sometimes, though, the largest codebases are the ones that need these changes the most. Making the effort could well be the best decision you make for the health of the project.


Once the layers are defined, you have a framework to expand on. You won’t necessarily reuse the same layers for every screen you work on, but the exercise does help to crystallize your thinking as to what constitutes a good layer in your application.

Working on this size of refactoring one screen at a time can help you better manage both the impact of the changes being made and the size of the current task at hand. The one drawback to this approach is that it can be hard to see all the positive impact that all of your changes are having right away. In the next section, we’ll make sure you get recognized for all your hard work.

8.8.4. Recognize the advances being made

Although you’re refactoring, it can be easy to get mired in the details, particularly if the effort starts to become routine. You may not be recognizing the payoff from all your hard work.

Having appropriate metrics in place can help reveal the benefits of your refactoring. You should notice a drop in coupling as you refactor code out into layers. It helps to take a measurement of this before you start and at regular intervals, if only to see how hard you should be patting yourself on the back.

Informal code reviews are often helpful as well, provided they focus solely on the layering. Having others review your seams and how you’ve separated responsibility is always a good idea, especially when you’ve been working on it for some time.


Note

Be sure these layering reviews don’t get bogged down in the details of the actual code within the layers. As we’ve mentioned, once the layers are defined, you can circle back and evaluate the implementations.


It may even be useful to do some early refactoring as a group or at least in a pair-programming session. But keep in mind that the more people involved, the longer it will take to do even the simplest thing. How layers are defined is a subjective topic and people will have different opinions.

Because of the potential size of the entire refactoring to layers task, you’ll at some point have to decide when to stop making changes. It’s both hard to notice you’re in a place where you should stop, and difficult to have the self-control to stop doing the work.

8.8.5. Know when to stop

As you can imagine, refactoring to layers is an arduous task. At times, it seems there’s no end in sight. The ecosystem you so painstakingly set up in part 1 certainly will start to pay off in spades now. With every minor shaving of code into a layer, you can run your automated build and know instantly if there’s a problem. Consequently, the task becomes easier as you go and it’s common to get into a good rhythm once the initial layers are defined. Furthermore, if you’re generating metrics at regular intervals, you’ll have feedback on your progress.

But there will come a time when enough is enough and you’ll need to stop, or at least pause, your efforts. Usually, it’s some external factor, such as an upcoming critical release or a new feature receiving higher priority. Other times, it may be the realization that the section you’re working on is adequately layered and there are other refactorings that will provide a bigger win for the effort.

Whatever the case, at regular intervals you should step back from your task and take stock of the situation. Ask yourself if there’s still value in continuing or if there are other tasks that are more important. Resist the urge to finish off “just one last thing” if the team is asking you to move on to something else.

Relayering your application isn’t an easy task. Throughout the process, you’ll second-guess yourself, often wondering if you’re making any progress or just making things worse. Although these steps can be done by one person, it helps to work on these steps collaboratively with the team (or through pair-programming) to avoid getting mired in minutiae. This is especially true when you’re first starting out and are unsure what makes a “good” layer in your brownfield project.

8.9. Summary

Layering is the first application of your recently found OO fundamentals. It’s an important topic for brownfield applications because there layering is typically nonexistent, sporadically applied, or poorly defined. In this chapter, we explored the concept of a physical tier and how a layer fits in with this concept.

Next, we reviewed the definition of interface-based design and how it’s fundamental in defining layers. We discussed how a well-defined contract (interface) can provide an anticorruption layer around code to help isolate it from external changes.

After that, we examined vertical layers and dove briefly into aspect-oriented programming as a mechanism to handle cross-cutting concerns such as logging and performance monitoring.

We then talked about the benefits of taking a domain-centric approach to refactoring before diving into an explanation on how to start your refactoring, how to measure your progress, and when to stop.

In the next chapter, we’ll expand on one of the ideas we touched on here. Specifically, how can we break apart the dependencies in code so that we can properly layer the application?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset