A comprehensive example
2.1 Doing it wrong
2.1.2 Smoke test
Mary has now implemented all three layers, so it’s time to see if the application works. She presses F5 and soon receives this message:
The specified named connection is either not found in the configuration, not intended to be used with the EntityClient provider, or not valid.
Because Mary used the default constructor of CommerceObjectContext (shown in list- ing 2.1), it implicitly expects that a connection string named CommerceObjectContext is present in the web.config file. As I alluded to when I discussed listing 2.2, this implicitness contains a trap. During the night, Mary forgot the implementation details of her Domain Layer. The code compiles, but the site doesn’t work.
In this case, fixing the error is straightforward. Mary inserts the correct connection string in the web.config file. When she runs the application, the web page shown in figure 2.3 appears.
Listing 2.3 Index View markup
Get products populated by Controller
Figure 2.8 Mary has now implemented all three layers in the application. This figure is identical to figure 2.2, but repeated here to illustrate the current state of Mary’s application.
37
Doing it wrong
The Featured Products feature is now done, and Mary feels confident and ready to implement the next feature in the application. After all, she has followed established best practices and created a three-layer application.
2.1.3 Evaluation
Did Mary succeed in developing a proper, layered appli- cation? No, she did not—although she certainly had the best of intentions. She created three Visual Studio proj- ects that correspond to the three layers in the planned architecture, as shown in figure 2.9. To the casual observer, this looks like the coveted layered architec- ture, but, as you’ll see, the code is tightly coupled.
Visual Studio makes it easy and natural to work with solutions and projects in this way. If we need functional- ity from a different library, we can easily add a refer- ence to it and write code that creates new instances of the types defined in these other libraries. Every time we add a reference, we take on a DEPENDENCY.
DEPENDENCYGRAPH
When working with solutions in Visual Studio, it’s easy to lose track of the important DEPENDENCIES, because
Visual Studio displays them together with all the other project references that may point to assemblies in the .NET Base Class Library (BCL).
To understand how the modules in Mary’s applica- tion relate to each other, we can draw a graph of the dependencies (see figure 2.10).
The most remarkable insight to be gained from fig- ure 2.10 is that the User Interface library is dependent
on both Domain and Data Access libraries. It seems as though the User Interface may bypass the Domain Layer in certain cases. This bears further investigation.
EVALUATINGCOMPOSABILITY
A major goal of building a three-layer application is to separate concerns. We’d like to separate our Domain Model from the Data Access Layer and the User Interface Layer so that none of these concerns pollute the Domain Model. In large applications, it’s essential for it to be possible to work with one area of the application in isolation.
To evaluate Mary’s implementation, we can ask a simple question:
TEST Is it possible to use each module in isolation?
In theory, we should be able to compose modules in any way we like. We may need to write new modules to bind existing modules together in new and unanticipated ways, but, ideally, we should be able to do so without having to modify the existing modules.
Figure 2.9 Mary’s e-commerce web application has a Visual Studio project for each layer in the planned architecture—but is it a three-layer application?
Figure 2.10 Dependency graph for Mary’s application showing how the modules depend on each other. The arrows point towards a module’s DEPENDENCY.
NOTE The following analysis discusses whether modules can be replaced, but be aware that this is a technique we use to evaluate composability. Even if we never want to swap modules, this sort of analysis uncovers potential issues regarding coupling. If we find that the code is tightly coupled, all the benefits of loose coupling are lost.
Can we use the modules in Mary’s application in new and exciting ways? Let’s look at some likely scenarios.
NEWUSERINTERFACE
If Mary’s application becomes a success, the project’s stakeholders would like her to develop a rich client version in Windows Presentation Foundation (WPF). Is this possi- ble to do while reusing the Domain Layer and the Data Access Layer?
When we examine the dependency graph in figure 2.10, we can quickly ascertain that no modules are depending on the Web User Interface, so it’s possible to remove it and replace it with a WPF User Interface.
Creating a rich client based on WPF is a new application that shares most of its implementation with the original web application. Figure 2.11 illustrates how a WPF
application would need to take the same dependencies as the web application. The original web application can remain unchanged.
Replacing the User Interface Layer is certainly possible with Mary’s implementa- tion, so let’s examine another interesting decomposition.
NEW DATA ACCESS LAYER
Imagine that market analysts figure out that, to optimize profits, Mary’s application should be available as a cloud application hosted on Windows Azure. In Windows Azure, data can be stored in the highly scalable Azure Table Storage Service. This stor- age mechanism is based on flexible data containers that contain unconstrained data. The service enforces no particular database schema, and there’s no referential integrity. The protocol used to communicate with the Table Storage Service is HTTP, and the most obvious data access technology on .NET is based on ADO.NET Data Services.
This type of database is sometimes known as a key-value database, and it’s a different beast than a relational database accessed through the Entity Framework.
Figure 2.11 Replacing the Web User Interface with a WPF User Interface is possible because no module depends on the Web User Interface. The original Web User Interface remains in the figure in grayscale to illustrate the point that adding a new user interface doesn’t preclude the original.
39
Doing it wrong
To enable the e-commerce application as a cloud application, the Data Access library must be replaced with a module that uses the Table Storage Service. Is this possible?
From the dependency graph in figure 2.10, we already know that both User Inter- face and Domain libraries depend on the Entity Framework-based Data Access library. If we try to remove the Data Access library, the solution will no longer compile, because a required DEPENDENCY is missing.
In a big application with dozens of modules, we could also try to remove those modules that don’t compile to see what would be left. In the case of Mary’s applica- tion, it’s evident that we’d have to remove all modules, leaving nothing behind.
Although it would be possible to develop an Azure Table Data Access library that mimics the API exposed by the original Data Access library, there’s no way we could inject it into the application.
The application isn’t nearly as composable as the project stakeholders would have liked. Enabling the profit-maximizing cloud abilities requires a major rewrite of the application, because none of the existing modules can be reused.
OTHERCOMBINATIONS
We could analyze the application for other combinations of modules, but it would be a moot point because we already know that it fails to support an important scenario.
Besides, not all combinations make sense. We could ask whether it would be possi- ble to replace the Domain Model with a different implementation. In most cases, this would be an odd question to ask, because the Domain Model encapsulates the heart of the application. Without the Domain Model, most applications have no raison d’être (reason for being).
2.1.4 Analysis
Why did Mary’s implementation fail to achieve the desired degree of composability? Is it because the User Interface has a direct dependency on the Data Access library? Let’s examine this possibility in greater detail.
Figure 2.12 Attempting to remove the relational Data Access library leaves nothing left, because all other modules depend on it. There’s no place where we can instruct the Domain library to use the new Azure Table Data Access library instead of the original.
DEPENDENCYGRAPHANALYSIS
Why is the User Interface dependent on the Data Access library? The culprit is this Domain Model method signature:
The GetFeaturedProducts method returns a sequence of products, but the Product class is defined in the Data Access library. Any client consuming the GetFeatured- Products method must reference the Data Access library to be able to compile.
It’s possible to change the signature of the method to return a sequence of a type defined within the Domain Model. It would also be more correct, but it doesn’t solve the problem.
Let’s assume that we break the dependency between the User Interface and Data Access libraries. The modi- fied dependency graph would now look like figure 2.13.
Would such a change enable Mary to replace the rela- tional Data Access library with one that encapsulates access to the Azure Table service? Unfortunately, no, because the Domain library still depends on the Data Access library. The User Interface, in turn, still depends on the Domain Model, so if we try to remove the origi- nal Data Access library, there would be nothing left of the application.
The root cause of the problem lies somewhere else.
DATA ACCESSINTERFACEANALYSIS
The Domain Model depends on the Data Access library because the entire data model is defined there. The Product class was generated when Mary ran the LINQ to Entities wizard. Using the Entity Framework to implement a Data Access Layer may be a rea- sonable decision.
However, consuming it directly in the Domain Model isn’t.
The offending code can be found spread out in the ProductService class. The constructor creates a new instance of the CommerceObjectContext class and assigns it to a private member variable:
this.objectContext = new CommerceObjectContext();
This tightly couples the ProductService class to the Data Access library. There’s no reasonable way we can intercept this piece of code and replace it with something else. The reference to the Data Access library is hard-coded into the ProductService class. The implementation of the GetFeaturedProducts method uses the Commerce- ObjectContext to pull Product objects from the database:
Figure 2.13 Dependency graph of the hypothetical situation where the dependency of the User Interface on the Data Access library has been severed.
41
Doing it right
var products = (from p in this.objectContext .Products
where p.IsFeatured select p).AsEnumerable();
This only reinforces the hard-coded dependency, but, at this point, the damage is already done. What we need is a better way to compose modules without such tight coupling.
MISCELLANEOUSOTHERISSUES
Before I show you the better alternative, I’d like to point out a few other issues with Mary’s code that ought to be addressed.
■ Most of the Domain Model seems to be implemented in the Data Access library.
Whereas it’s a technical problem that the Domain Model library references the Data Access library, it’s a conceptual problem that the Data Access library defines such a class as the Product class. A public Product class belongs in the Domain Model.
■ Under the influence of Jens, Mary decided to implement the code that deter- mines whether or not a user is a preferred customer in the User Interface. How- ever, how a customer is identified as a preferred customer is a piece of Business Logic, so it ought to be implemented in the Domain Model.
Jens’ argument about separations of concern and the SINGLE RESPONSIBILITY
PRINCIPLE is no excuse for putting code in the wrong place. Following the SINGLE
RESPONSIBILITY PRINCIPLE within a single library is entirely possible—that’s the expected approach.
■ The ProductService class relies on XML configuration. As you saw when we fol-
lowed Mary’s efforts, she forgot that she had to put a particular piece of config- uration code in her web.config file. Although the ability to configure a compiled application is important, only the finished application should rely on configuration files. It’s much more flexible for reusable libraries to be impera- tively configurable by their callers.
In the end, the ultimate caller is the application itself. At that point, all rele- vant configuration data can be read from a .config file and fed to the underly- ing libraries, as needed.
■ The View (as shown in listing 2.3) seems to contain too much functionality. It
performs casts and specific string formatting. Such functionality should be moved to the underlying model.
In the next section, I’ll show you a more composable way of building an application with the same features as the one Mary built. I’ll also address these minor issues at the same time.