Chapter 16. Tour

I hope that if you follow the practices presented in this book, you’ll have a better chance of producing code that fits in your head; code that will sustain your organisation. What does such a code base look like?

In this last chapter, I’ll take you on a tour of the example code base that accompanies the book. I’ll point out some highlights that I feel are particularly compelling.

16.1 Navigation

You didn’t write the code, so how should you find your way in it? That depends on your motivation for looking at it. If you’re a maintenance programmer and you’ve been asked to fix a defect with an attached stack trace, you may immediately go to the top frame in the trace.

On the other hand, if you have no immediate goal and you just want to get a sense for the application, it’d be most natural to start at the program’s entry point. In a .NET code base, that’s the Main method.

In general, I find it reasonable to assume that a code reader will be familiar with the basic workings of the language, platform, and framework in use. To be clear, I don’t assume that you, the reader of this book, is familiar with .NET or ASP.NET, but when I program, I expect a team member to know the ground rules. For example, I expect that a team member knows the special significance of the Main method in .NET.

Listing 16.1 shows the Main method that meets you in the code base. It hasn’t changed since listing 2.4.

In ASP.NET Core code bases, the Main method is a piece of boiler plate that rarely changes. Since I expect other programmers who are going to work with this code base to know the basics of the framework, I find it best to keep code as unsurprising as possible. On the other hand, there’s little informational content in listing 16.1.

public static class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Listing 16.1: Entry point for the restaurant reservation system. This listing is identical to listing 2.4. (Restaurant/af31e63/Restaurant.RestApi/Program.cs)

Developers with a glancing knowledge of ASP.NET will know that the webBuilder.UseStartup<Startup>() statement identifies the Startup class as the place where the real action is. That’s where you should go look to understand the code base.

16.1.1 Seeing the big picture

Use your IDE to navigate to the Startup class. Listing 16.2 show the class declaration and constructor. It uses Constructor Injection[25] to receive an IConfiguration object from the ASP.NET framework. This is the conventional way to do things and should be familiar to anyone with experience with the framework. While unsurprising, little information is so far gained.

By convention, the Startup class should define two methods: Configure and ConfigureServices. These follow immediately after listing 16.2. Listing 16.3 shows the Configure method.

Here we learn that the system uses authentication, routing, authorisation, and the framework’s default implementation of the Model View Controller[33] (MVC) pattern. The abstraction level is high, but the code fits in your head; the cyclomatic complexity is 2, there are only three activated objects, and twelve lines of code. Figure 16.1 shows one way to plot it to a hex flower diagram. This illustrates how the code fits into the conceptual model of fractal architecture.

Images

Figure 16.1: Hex flower diagram of the Configure method in listing 16.3. There’s more than one way to fill out a hex flower. The examples in chapter 7 fill each cell with a branch according to cyclomatic complexity analysis. This one instead fills each cell with an activated object.

public sealed class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

Listing 16.2: Startup declaration and constructor. Listing 16.3 follows immediately after. (Restaurant/af31e63/Restaurant.RestApi/Startup.cs)

public static void Configure(
    IApplicationBuilder app,
    IWebHostEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();

    app.UseAuthentication();
    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}

Listing 16.3: Configure method on the Startup class declared in listing 16.2. (Restaurant/af31e63/Restaurant.RestApi/Startup.cs)

This is essentially just a laundry list. All the methods invoked in listing 16.3 are framework methods. The Configure method’s sole purpose is to enable those particular built-in features. Reading it, you know a little about what to expect from the code. For example, you should expect each HTTP request to be handled by a method on a Controller class.

Perhaps there’s more information to be gathered from the ConfigureServices method in listing 16.4?

There’s a bit more information here, but it’s still at a high level of abstraction. It also fits in your head: the cyclomatic complexity is 1, there are six activated objects (services, urlSigningKey, a new UrlIntegrityFilter object, two variables both called opts, and the object’s Configuration property), and 21 lines of code. Again, you can plot the method to a hex flower diagram like figure 16.2 to illustrate how the method fits the concept of fractal architecture. As long as you can map each chunk of a method into a cell in a hex flower diagram, the code is likely to fit in your head.

Images

Figure 16.2: Hex flower diagram of the ConfigureServices method in listing 16.4. Like figure 16.1 this diagram fills each cell with an activated object.

There are few details in the method; it works more like a table of contents for the code base. Would you like to know about authorisation? Navigate to the ConfigureAuthorization method to learn more. Would you like to investigate the code base’s data access implementation? Navigate to the ConfigureRepository method.

public void ConfigureServices(IServiceCollection services)
{
    var urlSigningKey = Encoding.ASCII.GetBytes(
        Configuration.GetValue<string>("UrlSigningKey"));

    services
        .AddControllers(opts =>
        {
            opts.Filters.Add<LinksFilter>();
            opts.Filters.Add(new UrlIntegrityFilter(urlSigningKey));
        })
        .AddJsonOptions(opts =>
            opts.JsonSerializerOptions.IgnoreNullValues = true);

    ConfigureUrSigning(services, urlSigningKey);
    ConfigureAuthorization(services);
    ConfigureRepository(services);
    ConfigureRestaurants(services);
    ConfigureClock(services);
    ConfigurePostOffice(services);
}

Listing 16.4: ConfigureServices method on the Startup class declared in listing 16.2. (Restaurant/af31e63/Restaurant.RestApi/Startup.cs)

When you navigate to learn more, you zoom in on that detail. This is an example of the fractal architecture discussed in subsection 7.2.6. At each level, the code fits in your head. When you zoom in on a detail, the higher level shouldn’t be required to understand the code at the new level.

Before we zoom in on a detail, I’d like to discuss how to navigate a code base.

16.1.2 File organisation

A question that I frequently get is how to organise the files in a code base. Should you create a subdirectory for Controllers, another for Models, one for Filters, and so on? Or should you create a subdirectory for each feature?

Few people like my answer: Just put all files in one directory. Be wary of creating subdirectories just for the sake of ‘organising’ the code.

File systems are hierarchies; they are trees: a specialised kind of acyclic graph in which any two vertices are connected by exactly one path. Put another way, each vertex can have at most one parent. Even more bluntly: if you put a file in a hypothetical Controllers directory, you can’t also put it in a Calendar directory.

As an analysis of the Firefox code base notes:

“the system architects realized there were multiple ways in which the system could be sliced-and-diced, indicating possible cross-cutting concerns, and that choosing one separation into modules would cause other cohesive parts of the system to be split up across multiple modules. In particular, choosing to divide the browser and toolkit components of Firefox has caused the places and themes components to become split.”[109]

This is the problem with hierarchies. Any attempt at organisation automatically excludes all other ways to organise things. You have the same problem with inheritance hierarchies in single-inheritance languages like C# and Java. If you decide to derive from one base class, you’ve excluded all other classes as potential bases.

“Favor object composition over class inheritance.”[39]

Just as you should avoid inheritance, you should eschew the use of directory structure to organise the code.

As with all advice, exceptions do exist. Consider the sample code base. The Restaurant.RestApi directory contains 65 code files: Controllers, Data Transfer Objects, Domain Models, filters, SQL scripts, interfaces, Adapters, etcetera. These files implement various features like reservations and the calendar, as well as cross-cutting concerns such as logging.

The only exception to the rule is a subdirectory called Options. Its four files exist only to bridge the gap from JSON-based configuration files to code. The classes in these files are specialised to adapt to the ASP.NET options system. They’re Data Transfer Objects and exist only for that singular purpose. I feel quite confident that they shouldn’t be used for any other purpose, so I decided to put them out of sight.

When I tell people that organising code files in elaborate hierarchies is a bad idea, they incredulously counter: How will we find files?

Use your IDE. It has navigation features. When, earlier, I wrote that you should use your IDE to navigate to the Startup class, I didn’t mean ’locate the Startup.cs file in the Restaurant.RestApi directory and open it.’

I meant, use your IDE to go to the definition of a symbol. In Visual Studio, for example, this command is called Go To Definition and by default bound to F12. Other commands enable you to go to implementations of interfaces, find all references, or search for a symbol.

Your editor has tabs, and you can switch between them using standard keyboard shortcuts1.

1 On Windows, that would be Ctrl + Tab.

I’ve mob-programmed with developers to teach them test-driven development. We’d be looking at a test, and I’d say something like “Okay, could we switch to the System Under Test, please?.

The driver would then think about the name of that class, go to the file view, scroll through it to find the file, and double-click to open it.

All the while, the file was open in another tab. We worked with it three minutes ago, and it was just a keyboard shortcut away.

As an exercise, hide your IDE’s file view. Learn to navigate code bases using the rich code integration offered by the IDE.

16.1.3 Finding details

A method like listing 16.4 gives you the big picture, but sometimes you need to see implementation details. If, for example, you want to learn how data access works, you should navigate to the ConfigureRepository method in listing 16.5.

From the ConfigureRepository method you can learn that it registers an IReservationsRepository instance with the built-in Dependency Injection Container. Again, the code fits in your head: the cyclomatic complexity is 1, it activates six objects, and there are 15 lines of code. Figure 16.3 shows a possible hex flower mapping.

Images

Figure 16.3: Hex flower diagram of the ConfigureRepository method in listing 16.5. Like figure 16.1 this diagram fills each cell with an activated object.

private void ConfigureRepository(IServiceCollection services)
{
    var connStr = Configuration.GetConnectionString("Restaurant");
    services.AddSingleton<IReservationsRepository>(sp =>
{
        var logger =
            sp.GetService<ILogger<LoggingReservationsRepository>>();
        var postOffice = sp.GetService<IPostOffice>();
        return new EmailingReservationsRepository(
            postOffice,
            new LoggingReservationsRepository(
                logger,
                new SqlReservationsRepository(connStr)));
    });
}

Listing 16.5: ConfigureRepository method. Here you can learn how the data access components are composed. (Restaurant/af31e63/Restaurant.RestApi/Startup.cs)

Since you’ve zoomed in on a detail, the surrounding context shouldn’t matter. What you need to keep track of in your head is the services parameter, the Configuration property, and the variables that the method creates.

You can learn a few things from this code:

• If you want to edit the application’s connection string you should use the standard ASP.NET configuration system.

• The IReservationsRepository service is actually a three-levels-deep Decorator that also involves logging and emailing.

• The innermost implementation is the SqlReservationsRepository class.

Depending on what interests you, you can navigate to the relevant type. If you want to know more about the IPostOffice interface, you can Go To Definition or Go To Implementation. If you want to look at SqlReservationsRepository, navigate to it. When you do that, you zoom in on an even deeper level of detail.

You can find code listings from SqlReservationsRepository throughout the book, for example listings 4.19, 12.2, and 15.1. They all fit in your brain as already discussed.

All the code in the code base follows these principles.

16.2 Architecture

I’ve had little to say about architecture. It’s not that I consider it unimportant, but again, good books already exist on the topic. Most of the practices I’ve presented work with a variety of architectures: layered[33]; monolithic; ports and adapters[19]; vertical slices; the actor model; micro-services; functional core, imperative shell[11]; etcetera.

Clearly, software architecture impacts how you organise code, so it’s hardly irrelevant. You should explicitly consider the architecture for each code base you work in. There’s no one-size-fits-all architecture, so you shouldn’t consider any of the following as gospel. It’s a description of a single architecture that works well for the task at hand. It’s not suitable for all situations.

16.2.1 Monolith

If you’ve looked at the book’s sample code base, you may have noticed that it looks disconcertingly monolithic. If you consider the full code base that includes the integration tests, as figure 16.4 illustrates, there are all of three packages2. Of those, only one is production code.

2 In Visual Studio these are called projects.

Images

Figure 16.4: The packages that make up the sample code base. With only a single production package, it reeks of a monolith.

The entire production code compiles to a single executable file. That includes the database access, HTTP specifics, Domain Model, logging, email functionality, authentication, and authorisation. All in one package? Isn’t that a monolith?

In a sense, you could argue that it is. From a deployment perspective, for example, you can’t separate the various parts to put them on different machines. For the purposes of this sample application, I decided that this wasn’t a ‘business’ goal.

You also can’t reuse parts of the code in new ways. What if we wanted to reuse the Domain Model to run a scheduled batch job? If you tried to do that, you would find that the HTTP-specific code would tag along, as would the email functionality.

That, however, is only an artefact of how I chose to package the code. One package is simpler than, say, four.

Internally in that single package, I’ve applied the functional core, imperative shell[11] architecture, which tends to lead towards a ports-and-adapters-style architecture[101].

I’m not that worried whether it’d be possible to separate that code base into multiple packages, should it become necessary.

16.2.2 Cycles

Monoliths tend to have a bad reputation because they so easily devolve into spaghetti code. A major reason is that inside a single package, all code can easily3 call all other code.

3 To be fair, in a language like C#, you can use the private access modifier to prevent other classes from calling a method. That’s not much of a barrier to a developer in a hurry: Just change the access modifier to internal and move on.

This often leads to one piece of code that depends on another part, that again depends on the first. An example I often see is illustrated by figure 16.5: data access interfaces that return or take as parameters objects defined by an object-relational mapper. The interface may be defined as part of the code base’s Domain Model, so the implementation is coupled to that. So far, so good, but the interface is defined in terms of the object-relational mapper classes, so the abstraction also depends on implementation details. This violates the Dependency Inversion Principle[60] and leads to coupling.

Images

Figure 16.5: A typical data access cycle. The Domain Model defines a data access interface, here called IRepository. Members are defined with return types or parameters taken from the data access layer. For example, the Row class could be defined by an object-relational mapper (ORM). Thus, the Domain Model depends on the data access layer. On the other hand, the OrmRepository class is an ORM-based implementation of the IRepository interface. It can’t implement the interface without referencing it, so the data access layer also depends on the Domain Model. In other words, the dependencies form a cycle.

In such cases, coupling manifests as cycles. As illustrated by figure 16.6, A depends on B, which depends on C, which again depends on A. No mainstream languages prevent cycles, so you have to be eternally vigilant to avoid them.

Images

Figure 16.6: A simple cycle. A depends on B, which depends on C, which again depends on A.

There is, however, a hack that you can use. While mainstream languages allow cycles in code, they do prohibit them in package dependencies. If, for example, you try to define a data access interface in a Domain Model package and you want to use some object-relational mapper classes for parameters or return values, you’ll have to add a dependency to your data access package.

Figure 16.7 illustrates what happens next. Once you want to implement the interface in the data access package, you’ll need to add a dependency to the Domain Model package. Your IDE, however, refuses to break the Acyclic Dependency Principle[60], so you can’t do that.

Images

Figure 16.7: A cycle foiled. If the Domain Model package already references the data access package, the data access package can’t reference the Domain Model package. You can’t create a dependency cycle between packages.

This should be motivation to break a code base into multiple packages. You get your IDE to enforce an architectural principle, even if it’s only at a coarse-grained level. It’s poka-yoke applied to architecture. It passively prevents large-scale cycles.

The familiar way to separate a system into smaller components is to distribute the behaviour over Domain Model, data access, ports or user interface, and a Composition Root[25] package to compose the other three together.

As figure 16.8 implies, you may also want to unit test each package separately. Now, instead of three packages, you have seven.

Images

Figure 16.8: Hypothetical decomposition of the example restaurant reservation code base. The HTTP model would contain all the logic and configuration related to HTTP and REST, the Domain Model the ‘business logic’, and the data access package the code that talks to the database. The app host package would contain the Composition Root[25] that composes the three other packages. Three test packages would target the three production packages that contain complex logic.

The passive prevention of cycles is worth the extra complexity. Unless team members have extensive experience with a language that prevents cycles, I recommend this style of architecture.

Such languages do exist, though. F# famously prevents cycles. In it, you can’t use a piece of code unless it’s already defined above. Newcomers to the language see this as a terrible flaw, but it’s actually one of its best features[116][37].

Haskell takes a different approach, but ultimately, its explicit treatment of side effects at the type level steers you towards a ports-and-adapters-style architecture. Your code simply doesn’t compile otherwise[101]!

I’ve been writing F# and Haskell for enough years that I naturally follow the beneficial rules that they enforce. I’m confident that the sample code is nicely decoupled, even though it’s packaged as a monolith. But unless you have a similar experience, I recommend that you separate your code base into multiple packages.

16.3 Usage

If you’re looking at an unfamiliar code base, you’d like to see it in action. A REST API doesn’t have a user interface, so you can’t just launch it and start clicking on buttons.

Or, to a certain degree, you can. If you run the application, you can view its ‘home’ resource in a browser. The JSON representations served by the API contain links that you can follow in your browser. It’s a limited way to interact with the system, though.

Using your browser, you can only issue GET requests. To make a new reservation, however, you’ll have to make a POST request.

16.3.1 Learning from tests

When a code base has a comprehensive test suite, you can often learn about intended usage from the tests. For example, you might want to learn how to make a new reservation in the system.

Listing 16.6 shows a test I wrote as I was expanding the code base to a multi-tenant system. It’s representative of the way these tests are written.

[Fact]
public async Task ReserveTableAtNono()
{
    using var api = new SelfHostedApi();
    var client = api.CreateClient();
    var dto = Some.Reservation.ToDto();
    dto.Quantity = 6;

    var response = await client.PostReservation("Nono", dto);
    
    var at = Some.Reservation.At;
    await AssertRemainingCapacity(client, at, "Nono", 4);
    await AssertRemainingCapacity(client, at, "Hipgnosta", 10);
}

Listing 16.6: Unit test that makes a reservation at the restaurant Nono. (Restaurant/af31e63/Restaurant.RestApi.Tests/ReservationsTests.cs)

As usual, this is code that fits in your head: it has a cyclomatic complexity of 1, six activated objects, and 14 lines of code. The abstraction level is high, in the sense that it doesn’t tell you the details of how it makes assertions, or how PostReservation is implemented.

If you’re curious about that, you may decide to navigate to the PostReservation implementation to see listing 16.7.

This Test Utility Method[66] uses an HttpClient to interact with the REST API. You may recall from listing 16.6 that the client in question communicates with a self-hosted instance of the service. When you zoom in on the PostReservation method, however, you no longer need to keep track of that. The only thing you need to know is that you have a working client.

internal static async Task<HttpResponseMessage> PostReservation(
    this HttpClient client,
    string name,
    object reservation)
{
    string json = JsonSerializer.Serialize(reservation);
    using var content = new StringContent(json);
    content.Headers.ContentType.MediaType = "application/json";

    var resp = await client.GetRestaurant(name);
    resp.EnsureSuccessStatusCode();
    var rest = await resp.ParseJsonContent<RestaurantDto>();
    var address = rest.Links.FindAddress("urn:reservations");

    return await client.PostAsync(address, content);
}

Listing 16.7: Test Utility Method[66] that makes a reservation. (Restaurant/af31e63/Restaurant.RestApi.Tests/RestaurantApiClient.cs)

This is another example of how the fractal architecture works. When you zoom in on a detail, the surrounding context becomes irrelevant. You don’t have to keep it in your head.

Specifically, you can see that the helper method serialises the reservation to JSON. It then finds the appropriate address to use for making the POST request.

That’s more detailed than before. Perhaps that taught you what you wanted to know. If you were curious about how to format the POST request, which HTTP headers to use, etcetera, then you need look no further. If, on the other hand, you’d like to know how to navigate to a particular restaurant, you’d have to zoom in on the GetRestaurant method. Or if you want to learn how to find a particular address in a JSON representation, you could zoom in on FindAddress.

Well-written tests can be a great learning resource.

16.3.2 Listen to your tests

If the book Growing Object-Oriented Software, Guided by Tests[36] had a motto, it’d be listen to your tests. Good tests can teach you more than how to interact with the System Under Test.

Keep in mind that test code is code too. You’ll have to maintain it just like you must maintain the production code. You should refactor test code when it starts to rot, just like production code.

You may introduce Test Utility Methods[66] like listings 16.7 or 16.8. It turns out that the GetRestaurant method in listing 16.8 serves as a general-purpose entry point for any HttpClient that wants to interact with this particular REST API. Since it’s a multi-tenant system, the first step for any client is to navigate to the desired restaurant.

internal static async Task<HttpResponseMessage> GetRestaurant(
    this HttpClient client,
    string name)
{
    var homeResponse =
        await client.GetAsync(new Uri("", UriKind.Relative));
    homeResponse.EnsureSuccessStatusCode();
    var homeRepresentation =
        await homeResponse.ParseJsonContent<HomeDto>();
    var restaurant =
        homeRepresentation.Restaurants.First(r => r.Name == name);
    var address = restaurant.Links.FindAddress("urn:restaurant");

    return await client.GetAsync(address);
}

Listing 16.8: Test Utility Method[66] that finds a restaurant resource based on its name. (Restaurant/af31e63/Restaurant.RestApi.Tests/RestaurantApiClient.cs)

If you look closer at listings 16.7 or 16.8, there’s nothing test-specific about them. Might they be useful in other contexts?

The benefit of a REST API is that it supports any client that ‘speaks’ HTTP and can parse JSON4. Still, if the only thing you do is to publish the API, all third-party programmers will have to develop their own client code. If a substantial segment of your clients are on the same platform as your test code, you could promote those Test Utility Methods to an ‘official’ client SDK.

4 Or XML, if you’re in that mood.

Situations like that regularly happen to me. As I refactor test code, I realise that some of it would also be useful as production code. That’s always a happy discovery. When that happens, move the code over. Profit.

16.4 Conclusion

‘Real’ engineering is a mix of deterministic processes and human judgment. If you need to build a bridge, you have formulas to calculate load-bearing strength, but you still need to involve people to deal with the myriad complexities related to the task. What sort of traffic should the bridge support? What is the desired throughput? What are the temperature extremes? What is the underground like? Are there environmental concerns?

If engineering was an entirely deterministic process, you wouldn’t need people. All it would require would be computers and industrial robots.

It’s possible that some engineering disciplines may move into that realm in the future, but if that happens, it stops being engineering; it becomes manufacturing.

You might consider this distinction merely ontological, but I believe that it pertains to the art of software engineering. There are quantitative methodologies that you can adopt. That doesn’t discharge you from an obligation to use your brain.

The task is to combine skill with appropriate processes, heuristics, and technologies to make development more likely to succeed. In this book, I’ve presented multiple techniques you can adopt today. An early reader considered some of these ideas advanced. That may be so, but they are possible.

“The future is already here — it’s just not very evenly distributed” - William Gibson

Likewise, the techniques presented here are no pie in the sky. Some organisations already use them. You could, too.

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

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