In this chapter
As we mentioned in chapter 1, a sauce béarnaise is an emulsified sauce made from egg yolk and butter, but this doesn’t magically instill in you the ability to make one. The best way to learn is to practice, but an example can often bridge the gap between theory and practice. Watching a professional cook making a sauce béarnaise is helpful before you try it out yourself.
When we introduced Dependency Injection (DI) in the last chapter, we presented a high-level tour to help you understand its purpose and general principles. But that simple explanation doesn’t do justice to DI. DI is a way to enable loose coupling, and loose coupling is first and foremost an efficient way to deal with complexity.
Most software is complex in the sense that it must address many issues simultaneously. Besides the business concerns, which may be complex in their own right, software must also address matters related to security, diagnostics, operations, performance, and extensibility. Instead of addressing all of these concerns in one big ball of mud, loose coupling encourages you to address each concern separately. It’s easier to address each in isolation, but ultimately, you must still compose this complex set of issues into a single application.
In this chapter, we’ll take a look at a more complex example. You’ll see how easy it is to write tightly coupled code. You’ll also join us in an analysis of why tightly coupled code is problematic from a maintainability perspective. In chapter 3, we’ll use DI to completely rewrite this tightly coupled code base to one that’s loosely coupled. If you want to see loosely coupled code right away, you may want to skip this chapter. If not, when you’re done with this chapter, you should begin to understand what it is that makes tightly coupled code so problematic.
The idea of building loosely coupled code isn’t particularly controversial, but there’s a huge gap between theory and practice. Before we show you in the next chapter how to use DI to build a loosely coupled application, we want to show you how easily it can go wrong. A common attempt at loosely coupled code is building a layered application. Anyone can draw a three-layer application diagram, and figure 2.1 proves that we can too.
Drawing a three-layer diagram is deceptively simple, but the act of drawing the diagram is akin to stating that you’ll have sauce béarnaise with your steak: it’s a declaration of intent that carries no guarantee with regard to the final result. You can end up with something else, as you shall soon see.
There’s more than one way to view and design a flexible and maintainable complex application, but the n-layer application architecture constitutes a well-known, tried-and-tested approach. The challenge is to implement it correctly. Armed with a three-layer diagram like the one in figure 2.1, you can start building an application.
Mary Rowan is a professional .NET developer working for a local Certified Microsoft Partner that mainly develops web applications. She’s 34 years old and has been working with software for 11 years. This makes her one of the more experienced developers in the company. In addition to performing her regular duties as a senior developer, she often acts as a mentor for junior developers. In general, Mary is happy about the work that she’s doing, but it frustrates her that milestones are often missed, forcing her and her colleagues to work long hours and weekends to meet deadlines.
She suspects that there must be more efficient ways to build software. In an effort to learn about efficiency, she buys a lot of programming books, but she rarely has time to read them, as much of her spare time is spent with her husband and two girls. Mary likes to go hiking in the mountains. She’s also an enthusiastic cook, and she definitely knows how to make a real sauce béarnaise.
Mary has been asked to create a new e-commerce application on ASP.NET Core MVC and Entity Framework Core with SQL Server as the data store. To maximize modularity, it must be a three-layer application.
The first feature to implement should be a simple list of featured products, pulled from a database table and displayed on a web page (an example is shown in figure 2.2). And, if the user viewing the list is a preferred customer, the price on all products should be discounted by 5%.
To complete her first feature, Mary will have to implement the following:
Product
class, which represents a single database rowLet’s look over Mary’s shoulder as she implements the application’s first feature.
Because Mary will need to pull data from a database table, she has decided to begin by implementing the data layer. The first step is to define the database table itself. Mary uses SQL Server Management Studio to create the table shown in table 2.1.
Column Name | Data Type | Allow Nulls | Primary Key |
Id | uniqueidentifier | No | Yes |
Name | nvarchar(50) | No | No |
Description | nvarchar(max) | No | No |
UnitPrice | money | No | No |
IsFeatured | bit | No | No |
To implement the data access layer, Mary adds a new library to her solution. The following listing shows her Product
class.
Listing 2.1 Mary’s Product
class
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal UnitPrice { get; set; }
public bool IsFeatured { get; set; }
}
Mary uses Entity Framework for her data access needs. She adds a dependency to the Microsoft.EntityFrameworkCore.SqlServer NuGet package to her project, and implements an application-specific DbContext
class that allows her application to access the Products table via the CommerceContext
class. The following listing shows her CommerceContext
class.
Listing 2.2 Mary’s CommerceContext
class
public class CommerceContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<Product> Products { get; set; } ①
protected override void OnConfiguring( ②
DbContextOptionsBuilder builder)
{
var config = new ConfigurationBuilder() ③
.SetBasePath( ③
Directory.GetCurrentDirectory()) ③
.AddJsonFile("appsettings.json") ③
.Build(); ③
string connectionString = ④
config.GetConnectionString( ④
"CommerceConnectionString"); ④
④
builder.UseSqlServer(connectionString); ④
}
}
Because CommerceContext
loads a connection string from a configuration file, that file needs to be created. Mary adds a file named appsettings.json to her web project, with the following content:
{
"ConnectionStrings": {
"CommerceConnectionString":
"Server=.;Database=MaryCommerce;Trusted_Connection=True;"
}
}
Warning CommerceContext
loads the connection string from a configuration file — this is a trap. It causes every new CommerceContext
to read the configuration file, even though the configuration file typically doesn’t change while an application is running. A CommerceContext
shouldn’t contain a hard-coded connection string, but neither should it load a configuration value from the configuration system. This is discussed in section 2.3.3.
CommerceContext
and Product
are public types contained within the same assembly. Mary knows that she’ll later need to add more features to her application, but the data access component required to implement the first feature is now completed (figure 2.3).
Now that the data access layer has been implemented, the next logical step is the domain layer. The domain layer is also referred to as the domain logic layer, business layer, or business logic layer. Domain logic is all the behavior that the application needs to have, specific to the domain the application is built for.
With the exception of pure data-reporting applications, there’s always domain logic. You may not realize it at first, but as you get to know the domain, its embedded and implicit rules and assumptions will gradually emerge. In the absence of any domain logic, the list of products exposed by CommerceContext
could technically have been used directly from the UI layer.
Warning Implementing domain logic in either the UI or data access layers will lead to pain and suffering. Do yourself a favor and create a domain layer from the beginning.
The requirements for Mary’s application state that preferred customers should be shown the product list prices with a 5% discount. Mary has yet to figure out how to identify a preferred customer, so she asks her coworker Jens for advice:
Mary: I need to implement this business logic so that a preferred customer gets a 5% discount.
Jens: Sounds easy. Just multiply by .95.
Mary: Thanks, but that’s not what I wanted to ask you about. What I wanted to ask you is, how should I identify a preferred customer?
Jens: I see. Is this a web application or a desktop application?
Mary: It’s a web app.
Jens: Okay, then you can use the
User
property of theHttpContext
to check if the current user is in the rolePreferredCustomer
.
Mary: Slow down, Jens. This code must be in the domain layer. It’s a library. There’s no
HttpContext
.
Jens: Oh. [Thinks for a while] I still think you should use the
HttpContext
of ASP.NET to look up the value for the user. You can then pass the value to your domain logic as a boolean.
Mary: I don’t know...
Jens: That’ll also ensure that you have good Separation of Concerns because your domain logic doesn’t have to deal with security. You know, the Single Responsibility Principle! It’s the Agile way to do it!
Mary: I guess you’ve got a point.
Jens is basing his advice on his technical knowledge of ASP.NET. As the discussion takes him away from his comfort zone, he steamrolls Mary with a triple combo of buzzwords. Be aware that Jens doesn’t know what he’s talking about:
As discussed in chapter 1, the Single Responsibility Principle (SRP) states that each class should only have a single responsibility, or, better put, a class should have only one reason to change.3
3 Robert C. Martin, Agile Principles, Patterns, and Practices in C# (Pearson Education, 2007), 115.
If we put SQL statements in a view that contains HTML markup, we’d all quickly agree that changes to the markup will happen at different times, at different rates, and for different reasons than changes to SQL statements. Our SQL statements change when we’re changing our data model or need to do performance tuning. Our markup, on the other hand, changes when we need to change the look and feel of the web application. These are different concerns that change for different reasons. Putting SQL statements directly into a view is, therefore, an SRP violation.
More often than not, however, it can be more challenging to see whether a class has multiple reasons to change. What often helps is to look at the SRP from the perspective of code cohesion. Cohesion is defined as the functional relatedness of the elements of a class or module. The lower the relatedness, the lower the cohesion, and the higher the risk a class violates the SRP.
Being able to detect SRP violations is one thing, but determining whether a violation should be fixed is yet another. It isn’t wise to apply the SRP if there are no symptoms. Needlessly splitting up classes that cause no maintainability problems can add extra complexity. The trick in software design is to manage complexity.
Armed with Jens’ unfortunately poor advice, Mary creates a new C# library project and adds a class called ProductService
, shown in listing 2.3. To make the ProductService
class compile, she must add a reference to her data access layer, because the CommerceContext
class is defined there.
Listing 2.3 Mary’s ProductService
class
public class ProductService
{
private readonly CommerceContext dbContext;
public ProductService()
{
this.dbContext = new CommerceContext(); ①
}
public IEnumerable<Product> GetFeaturedProducts(
bool isCustomerPreferred)
{
decimal discount =
isCustomerPreferred ? .95m : 1;
var featuredProducts = ②
from product in this.dbContext.Products ②
where product.IsFeatured ②
select product;
return
from product in ③
featuredProducts.AsEnumerable() ③
select new Product ③
{ ③
Id = product.Id, ③
Name = product.Name, ③
Description = product.Description, ③
IsFeatured = product.IsFeatured, ③
UnitPrice = ③
product.UnitPrice * discount ③
};
}
}
Creates a new CommerceContext instance for later use
Gets all products from the database, filtered by featured products
Creates a list of discounted products based on the discount percentage for the given customer
Mary’s happy that she has encapsulated the data access technology (Entity Framework Core), configuration, and domain logic in the ProductService
class. She has delegated the knowledge of the user to the caller by passing in the isCustomerPreferred
parameter, and she uses this value to calculate the discount for all the products.
Further refinement could include replacing the hard-coded discount value (.95) with a configurable number, but, for now, this implementation will suffice. Mary’s almost done. The only thing still left is the UI layer. Mary decides that it can wait until tomorrow. Figure 2.4 shows how far Mary has come with implementing the architecture envisioned in figure 2.1.
What Mary doesn’t realize is that by letting the ProductService
depend on the data access layer’s CommerceContext
class, she tightly coupled her domain layer to the data access layer. We’ll explain what’s wrong with that in section 2.2.
The next day, Mary resumes her work with the e-commerce application, adding a new ASP.NET Core MVC application to her solution. Don’t worry if you aren’t familiar with the ASP.NET Core MVC framework. The intricate details of how the MVC framework operates aren’t the focus of this discussion. The important part is how Dependencies are consumed, and that’s a relatively platform-neutral subject.
ASP.NET Core MVC takes its name from the Model View Controller design pattern.4 In this context, the most important thing to understand is that when a web request arrives, a controller handles the request, potentially using a (domain) model to deal with it, and forms a response that’s finally rendered by a view.
4 Martin Fowler et al., Patterns of Enterprise Application Architecture, 330.
A controller is normally a class that derives from the abstract Controller
class. It has one or more action methods that handle requests; for example, a HomeController
class typically has a method named Index
that handles the request for the default page. When an action method returns, it passes on the resulting model to the view through a ViewResult
instance.
The next listing shows how Mary implements an Index
method on her HomeController
class to extract the featured products from the database and pass them to the view. To make this code compile, she must add references to both the data access layer and the domain layer. This is because the ProductService
class is defined in the domain layer, but the Product
class is defined in the data access layer.
Listing 2.4 Index
method on the default controller class
public ViewResult Index()
{
bool isPreferredCustomer = ①
this.User.IsInRole("PreferredCustomer"); ①
var service = new ProductService(); ②
var products = service.GetFeaturedProducts( ③
isPreferredCustomer); ③
this.ViewData["Products"] = products; ④
return this.View();
}
Determines whether a customer is a preferred customer
Creates ProductService from the domain layer
Gets the list of featured products (defined in the data access layer) from the ProductService
Stores the list of products in the controller’s generic ViewData dictionary for later use by the view
As part of the ASP.NET Core MVC lifecycle, the User
property on the HomeController
class is automatically populated with the correct user object, so Mary uses it to determine if the current user is a preferred customer. Armed with this information, she can invoke the domain logic to get the list of featured products.
Note When Mary created her domain layer, she again created tightly coupled code. In this case, HomeController
is tightly coupled to ProductService
. This wouldn’t be that bad if ProductService
was a Stable Dependency but, as you learned in chapter 1, ProductService
is a Volatile Dependency. It’s Volatile because it introduces a requirement to set up and configure a relational database.
In Mary’s application, the list of products must be rendered by the Index
view. The following listing shows the markup for the view.
Listing 2.5 Index
view markup
<h2>Featured Products</h2>
<div>
@{
var products = ①
(IEnumerable<Product>)this.ViewData["Products"]; ①
foreach (Product product in products) ②
{
<div>@product.Name (@product.UnitPrice.ToString("C"))</div>
}
}
</div>
Gets the products populated by the controller
Loops through the products, formats their UnitPrice, and renders them as HTML
ASP.NET Core MVC lets you write standard HTML with bits of imperative code embedded to access objects created and assigned by the controller that created the view. In this case, the HomeController
’s Index
method assigned the list of featured products to a key called Products
that Mary uses in the view to render the list of products. Figure 2.5 shows how Mary has now implemented the architecture envisioned in figure 2.1.
With all three layers in place, the applications should theoretically work. But only by running the application can she verify whether that’s the case.
Mary has now implemented all three layers, so it’s time to see if the application works. She presses F5 and the web page shown in figure 2.2 appears. The Featured Products feature is now done, and Mary feels confident and ready to implement the next feature in the application. After all, she followed established best practices and created a three-layer application ... or did she?
Did Mary succeed in developing a proper, layered application? No, she didn’t, although she certainly had the best of intentions. She created three Visual Studio projects that correspond to the three layers in the planned architecture. To the casual observer, this looks like the coveted layered architecture, 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 you need functionality from a different library, you can easily add a reference to it and write code that creates new instances of the types defined in the other libraries. Every time you add a reference, though, you take on a Dependency.
When working with solutions in Visual Studio, it’s easy to lose track of the important Dependencies. This is 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 application relate to each other, we can draw a graph of the dependencies (see figure 2.6).
The most remarkable insight to be gained from figure 2.6 is that the UI layer depends on both domain and data access layers. It seems as though the UI could bypass the domain layer in certain cases. This requires further investigation.
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 and UI layers so that none of these concerns pollute the domain model. In large applications, it’s essential to be able to work with each area of the application in isolation. To evaluate Mary’s implementation, we can ask a simple question: Is it possible to use each module in isolation?
In theory, we should be able to compose modules 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. Can we use the modules in Mary’s application in new and exciting ways? Let’s look at some likely scenarios.
Note The following analyses discuss 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.
If Mary’s application becomes a success, the project stakeholders would like her to develop a rich client version in Windows Presentation Foundation (WPF). Is this possible to do while reusing the domain and data access layers?
When we examine the dependency graph in figure 2.6, we can quickly ascertain that no modules are depending on the web UI, so it’s possible to remove it and replace it with a WPF UI. Creating a rich client based on WPF is a new application that shares most of its implementation with the original web application. Figure 2.7 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 UI layer is certainly possible with Mary’s implementation. Let’s examine another interesting decomposition.
Mary’s market analysts figure out that, to optimize profits, her application should be available as a cloud application hosted on Microsoft Azure. In Azure, data can be stored in the highly scalable Azure Table Storage Service. This storage mechanism is based on flexible data containers that contain unconstrained data. The service enforces no particular database schema, and there’s no referential integrity.
Although the most common data access technology on .NET is based on ADO.NET Data Services, the protocol used to communicate with the Table Storage Service is HTTP. This type of database is sometimes known as a key-value database, and it’s a different beast than a relational database accessed through Entity Framework Core.
To enable the e-commerce application as a cloud application, the data access layer must be replaced with a module that uses the Table Storage Service. Is this possible?
From the dependency graph in figure 2.6, we already know that both the UI and domain layers depend on the Entity Framework–based data access layer. If we try to remove the data access layer, the solution will no longer compile without refactoring all other projects because a required Dependency is missing. In a big application with dozens of modules, we could also try to remove the modules that don’t compile to see what would be left. In the case of Mary’s application, it’s evident that we’d have to remove all modules, leaving nothing behind, as figure 2.8 shows.
Although it would be possible to develop an Azure Table data access layer that mimics the API exposed by the original data access layer, there’s no way we could apply that to the application without touching other parts of 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.
We could analyze the application for other combinations of modules, but this would be a moot point because we already know that it fails to support an important scenario. Besides, not all combinations make sense.
For instance, we could ask whether it would be possible to replace the domain model with a different implementation. But, 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 reason to exist.
Why did Mary’s implementation fail to achieve the desired degree of composability? Is it because the UI has a direct dependency on the data access layer? Let’s examine this possibility in greater detail.
Why does the UI depend on the data access library? The culprit is this domain model’s method signature:
The GetFeaturedProducts
method of the ProductService
class returns a sequence of products, but the Product
class is defined in the data access layer. Any client consuming the GetFeaturedProducts
method must reference the data access layer to be able to compile. It’s possible to change the signature of the method to return a type defined within the domain model. It’d also be more correct, but it doesn’t solve the problem.
Let’s assume that we break the dependency between the UI and data access library. The modified dependency graph would now look like figure 2.9.
Would such a change enable Mary to replace the relational data access layer with one that encapsulates access to the Azure Table service? Unfortunately, no, because the domain layer still depends on the data access layer. The UI, in turn, still depends on the domain model. If we try to remove the original data access layer, there’d be nothing left of the application. The root cause of the problem lies somewhere else.
The domain model depends on the data access layer because the entire data model is defined there. Using Entity Framework to implement a data access layer may be a reasonable decision. But, from the perspective of loose coupling, 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 CommerceContext
class and assigns it to a private member variable:
this.dbContext = new CommerceContext();
This tightly couples the ProductService
class to the data access layer. There’s no reasonable way you can Intercept this piece of code and replace it with something else. The reference to the data access layer is hard-coded into the ProductService
class!
The implementation of the GetFeaturedProducts
method uses CommerceContext
to pull Product
objects from the database:
var featuredProducts =
from product in this.dbContext.Products
where product.IsFeatured
select product;
The reference to CommerceContext
within GetFeaturedProducts
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. If you look back at the benefits of DI as discussed in chapter 1, you’ll see that Mary’s application fails to have the following:
At this point, you may ask yourself what the desired dependency graph should look like. For the highest degree of reuse, the lowest amount of dependencies is desirable. On the other hand, the application would become rather useless if there were no dependencies at all.
Which dependencies you need and in what direction they should point depends on the requirements. But because we’ve already established that we have no intention of replacing the domain layer with a completely different implementation, it’s safe to assume that other layers can safely depend on it. Figure 2.10 contains a big spoiler for the loosely coupled application you’ll write in the next chapter, but it does show the desired dependency graph.
The figure shows how we inverted the dependency between the domain and data access layers. We’ll go into more detail on how to do this in the next chapter.
We’d like to point out a few other issues with Mary’s code that ought to be addressed.
Product
class. A public Product
class belongs in the domain model.CommerceContext
class (shown in listing 2.2). From the perspective of its consumers, the dependency on this configuration value is completely hidden. As we alluded to when discussing listing 2.2, this implicitness contains a trap.
Although the ability to configure a compiled application is important, only the finished application should rely on configuration files. It’s more flexible for reusable libraries to be imperatively configurable by their callers, instead of reading configuration files themselves. In the end, the ultimate caller is the application’s entry point. At that point, all relevant configuration data can be read from a configuration file directly at startup and fed to the underlying libraries as needed. We want the configuration that CommerceContext
requires to be explicit.
It’s surprisingly easy to write tightly coupled code. Even when Mary set out with the express intent of writing a three-layer application, it turned into a largely monolithic piece of Spaghetti Code.5 (When we’re talking about layering, we call this Lasagna.)
5 William J. Brown et al., AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis (Wiley Computer Publishing, 1998), 119.
One of the many reasons that it’s so easy to write tightly coupled code is that both the language features and our tools already pull us in that direction. If you need a new instance of an object, you can use the new
keyword. If you don’t have a reference to the required assembly, Visual Studio makes it easy to add. But every time you use the new
keyword, you introduce a tight coupling. As discussed in chapter 1, not all tight coupling is bad, but you should strive to prevent tight coupling to Volatile Dependencies.
By now you should begin to understand what it is that makes tightly coupled code so problematic, but we’ve yet to show you how to fix these problems. In the next chapter, we’ll show you a more composable way of building an application with the same features as the one Mary built. We’ll also address those other issues discussed in section 2.3.3 at the same time.
18.119.162.49