9 Stratified design: Part 2

Image

In this chapter

  • Learn to construct abstraction barriers to modularize code.
  • Discover what to look for in a good interface (and how to find it).
  • Know when design is good enough.
  • Discover how stratified design helps maintenance, testing, and reuse.

In the last chapter, we learned how to draw call graphs and look for layers to help us organize our code. In this chapter, we continue deepening our understanding of stratified design and honing our design intuition with three more patterns. These patterns help us with the maintenance, testing, and reuse of our code.

Patterns of stratified design

Just as a reminder, we are looking at the practice of stratified design through the lenses of four patterns. We already covered pattern 1 in the last chapter. Because we’ve already got the fundamentals down, in this chapter we will cover the remaining three. Here they are again for reference.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

this chapter will cover the remaining three

Pattern 1: Straightforward implementation

The layer structure of stratified design should help us build straightforward implementations. When we read a function with a straightforward implementation, the problem the function signature presents should be solved at the right level of detail in the body. Too much detail is a code smell.

Pattern 2: Abstraction barrier

Some layers in the graph provide an interface that lets us hide an important implementation detail. These layers help us write code at a higher level and free our limited mental capacity to think at a higher level.

Pattern 3: Minimal interface

As our system evolves, we want the interfaces to important business concepts to converge to a small, powerful set of operations. Every other operation should be defined in terms of those, either directly or indirectly.

Pattern 4: Comfortable layers

The patterns and practices of stratified design should serve our needs as programmers, who are in turn serving the business. We should invest time in the layers that will help us deliver software faster and with higher quality. We don’t want to add layers for sport. The code and its layers of abstraction should feel comfortable to work in.

We’ve already seen the fundamentals: drawing the call graph and looking for layers. Now let’s dive right into pattern 2.

Pattern 2: Abstraction barrier

The second pattern we will look at is called an abstraction barrier. Abstraction barriers solve a number of problems. One is clearly delegating responsibilities between teams.

Patterns

  • Image Straightforward implementations
  • ImageAbstraction barrier**

    ** you are here

  • Image Minimal interface
  • Image Comfortable layers

Before abstraction barrier

Image

After abstraction barrier

Image

Abstraction barriers hide implementations

An abstraction barrier is a layer of functions that hide the implementation so well that you can completely forget about how it is implemented even while using those functions.

Image

Functional programmers strategically employ abstraction barriers because they let them think about a problem at a higher level. For instance, the marketing team can write and read functions having to do with marketing campaigns without dealing with the dirty work of for loops and arrays.

Patterns

  • Image Straightforward implementations
  • ImageAbstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

Ignoring details is symmetrical

Image

The abstraction barrier allows the marketing team to ignore details of the implementation. But there’s a symmetrical “not caring.” The dev team who implements the barrier doesn’t have to care about the details of the marketing campaign code that uses the functions in the abstraction barrier. The teams can work largely independently, thanks to the power of the abstraction barrier.

You have likely encountered this with libraries or APIs that you have used. Let’s say you’re using a weather data API from a company called RainCo to make a weather app. Your job is to use the API to display it to the user. The RainCo team’s job is to implement the weather data service. They don’t care what your app does! The API is an abstraction barrier that clearly delineates the responsibilities.

The dev team is going to test the limits of the abstraction barrier by changing the underlying data structure of shopping carts. If the abstraction barrier is built correctly, the marketing team won’t notice and their code won’t have to change at all.

Patterns

  • Image Straightforward implementations
  • ImageAbstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

Swapping the shopping cart’s data structure

Image

function remove_item_by_name(cart, name) {

var idx = indexOfItem(cart, name);

if(idx !== null)

return splice(cart, idx, 1);

return cart;

}

 

function indexOfItem(cart, name) {

for(var i = 0; i < cart.length; i++) {

if(cart[i].name === name)

return i;

}

return null;

}

linear search through an array, which could be a fast hash map lookup

Sarah is onto something. We need to address the poor performance of linear search through arrays. We shouldn’t hide the poor performance behind a clean interface.

The obvious thing to try is to use a JavaScript object (as a hash map) instead of an array. Adding to, removing from, and checking containment are all fast operations on a JavaScript object.

Image It’s your turn

Which functions need to change to implement this change?

Image

Image Answer

Just the functions in the highlighted layer need to change. No other functions assume that the shopping cart is an array. These are the functions that create the abstraction barrier.

Image

Re-implementing the shopping cart as an object

Re-implementing our shopping cart as a JavaScript object will make it more efficient and also more straightforward (yay pattern 1!). The object is a more appropriate data structure for random additions and removals.

Cart as array

function add_item(cart, item) {

return add_element_last(cart, item);

}

 

function calc_total(cart) {

var total = 0;

 

for(var i = 0; i < cart.length; i++) {

var item = cart[i];

total += item.price;

}

return total;

}

 

function setPriceByName(cart, name, price) {

var cartCopy = cart.slice();

for(var i = 0; i < cartCopy.length; i++) {

if(cartCopy[i].name === name)

cartCopy[i] =

setPrice(cartCopy[i], price);

}

return cartCopy;

 

}

 

function remove_item_by_name(cart, name) {

var idx = indexOfItem(cart, name);

if(idx !== null)

return splice(cart, idx, 1);

return cart;

}

 

function indexOfItem(cart, name) {

for(var i = 0; i < cart.length; i++) {

if(cart[i].name === name)

return i;

}

return null;

}

 

function isInCart(cart, name) {

return indexOfItem(cart, name) !== null;

}

this function doesn’t make sense anymore, so it’s gone

Cart as object

function add_item(cart, item) {

return objectSet(cart, item.name, item);

}

 

function calc_total(cart) {

var total = 0;

var names = Object.keys(cart);

for(var i = 0; i < names.length; i++) {

var item = cart[names[i]];

total += item.price;

}

return total;

}

 

function setPriceByName(cart, name, price) {

if(isInCart(cart, name)) {

var item = cart[name];

var copy = setPrice(item, price);

return objectSet(cart, name, copy);

} else {

var item = make_item(name, price);

return objectSet(cart, name, item);

}

}

 

function remove_item_by_name(cart, name) {

return objectDelete(cart, name);

 

 

 

}

 

 

 

 

 

 

 

 

 

function isInCart(cart, name) {

return cart.hasOwnProperty(name);

}

a built-in to know if an Object contains a key

Sometimes, unclear code is due to using the wrong data structure. Our code is smaller and cleaner—and more efficient. And the marketing department’s code will still work unchanged!

The abstraction barrier lets us ignore details

What lets us change the data structure without changing all of the code that uses shopping carts?

We originally used an array to store the items in the shopping cart. We recognized that it was not efficient. We modified a handful of functions that operated on the shopping cart and completely swapped out the data structure it used. The marketing team didn’t have to change their code. They didn’t even have to know it happened! How did we manage that?

Patterns

  • Image Straightforward implementations
  • ImageAbstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

The reason we could swap out a data structure and only change five functions was because those functions define an abstraction barrier. Abstraction is just a fancy way of saying, “What details can I ignore?” We call a layer an abstraction barrier when all of the functions together in a layer let us ignore the same thing when working above that layer. It’s a level of indirection that lets us ignore unwanted details.

Image

The abstraction barrier in this case means the functions above that layer don’t need to know what the data structure is. They can use those functions exclusively and treat the implementation of the cart as a detail they don’t care about, so much so that we can change from an array to an object and no function above the abstraction barrier notices.

Notice how there are no arrows that cross the dotted line. If a function above the line called splice() on a cart, for example, it would be violating the abstraction barrier. It would be using an implementation detail that it shouldn’t care about. We would call it an incomplete abstraction barrier. The solution is to add a new function to complete the barrier.

When to use (and when not to use!) abstraction barriers

Abstraction barriers are useful in the design of our code, but they shouldn’t be used everywhere. When should they be used?

Patterns

  • Image Straightforward implementations
  • ImageAbstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

1. To facilitate changes of implementation

In cases of high uncertainty about how you want to implement something, an abstraction barrier can be the layer of indirection that lets you change the implementation later. This property might be useful if you are prototyping something and you still don’t know how best to implement it. Or perhaps you know something will change; you’re just not ready to do it yet, like if you know you will want to get data from the server eventually, but right now you’ll just stub it out.

However, this benefit is often a trap, so be careful. We often write a lot of code just in case something might change in the future. Why? To save writing other code! It’s a silly practice to write three lines today to save three lines tomorrow (when tomorrow may never come); 99% of the time, the data structure never changes. The only reason it did in our example was that the team never stopped to think about efficiency until very late in development.

2. To make code easier to write and read

Abstraction barriers allow us to ignore details. Sometimes those details are bug magnets. Did we initialize the loop variables correctly? Did we have an off-by-one error in the loop exit condition? An abstraction barrier that lets you ignore those details will make your code easier to write. If you hide the right details, then less adept programmers can be productive when using it.

3. To reduce coordination between teams

Our development team could change things without talking with marketing first. And marketing could write simple marketing campaigns without checking in with development. The abstraction barrier allows teams on either side to ignore the details the other team handles. Thus, each team moves faster.

4. To mentally focus on the problem at hand

Now we get to the real prize of abstraction barriers. They let you think more easily about the problem you are trying to solve. Let’s face it. We have limited mental capacity, and we have a lot of details to worry about. An abstraction barrier makes some details unimportant to the problem we are solving right now. It means we are less likely to make a mistake and less likely to get tired.

Pattern 2 Review: Abstraction barrier

Abstraction barrier is a very powerful pattern. It strongly decouples code above the barrier from code at and below the barrier. An abstraction barrier decouples by defining details that don’t have to be considered on either side of the barrier.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

Typically, the code above the barrier can ignore implementation details such as which data structure is used. In our example, the marketing code (above the barrier) doesn’t have to care if the cart is an array or an object.

The code at or below the barrier can ignore the higher-level details like what the functions are being used for. The functions in the barrier can be used for anything, and they don’t have to care. In our example, the code at the barrier doesn’t care what the marketing campaign is all about.

All abstractions work like that: They define what code above and below doesn’t have to care about. Any particular function could define the same details to ignore. The abstraction barrier just makes this definition very strongly and explicitly. It declares that no marketing code should ever have to know how the shopping cart is implemented. All of the functions in the abstraction barrier work together to make this possible.

We should be careful of the trap of “making future change easy.” Abstraction barriers make change easy, but that’s not why we should use them. They should be used strategically to reduce inter-team communication and help clarify messy code.

The key thing to remember about abstraction barriers is that it’s all about ignoring details. Where is it useful to ignore details? What details can you help people ignore? Can you find a set of functions that, together, help you ignore the same details?

Our code is more straightforward

Touching base with pattern 1: Straightforward implementation

After changing out the data structure, most of our functions are now one-liners. The number of lines of code is not the important thing. What is important is that the solution is expressed at the correct level of generality and detail. One-liners typically don’t have room to mix up levels, so they are a good sign.

function add_item(cart, item) {

return objectSet(cart, item.name, item);

}

 

function gets_free_shipping(cart) {

return calc_total(cart) >= 20;

}

 

function cartTax(cart) {

return calc_tax(calc_total(cart));

}

 

function remove_item_by_name(cart, name) {

return objectDelete(cart, name);

}

 

function isInCart(cart, name) {

return cart.hasOwnProperty(name);

}

Two functions still have complex implementations:

function calc_total(cart) {

var total = 0;

var names = Object.keys(cart);

for(var i = 0; i < names.length; i++) {

var item = cart[names[i]];

total += item.price;

}

return total;

}

 

function setPriceByName(cart, name, price) {

if(isInCart(cart, name)) {

var itemCopy = objectSet(cart[name], 'price', price);

return objectSet(cart, name, itemCopy);

} else {

return objectSet(cart, name, make_item(name, price));

}

}

We don’t have all of the tools we need to make these functions more straightforward. We’ll learn those tools in chapters 10 and 11. For now, we’ve got two more patterns to learn.

Pattern 3: Minimal interface

The third pattern we will look at to help guide our design sense is minimal interface. It asks us to consider where the code for new features belongs. By keeping our interfaces minimal, we avoid bloating our lower layers with unnecessary features. Let’s see an example.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • ImageMinimal interface**

    ** you are here

  • Image Comfortable layers

Marketing wants to give a discount for watches

The marketing department has a new campaign. They’d like to give anyone with a lot of items in their cart, along with a watch, a 10% discount.

Watch marketing campaign

If the shopping cart’s total is > $100**

and

the shopping cart contains a watch**

then

they get a 10% discount.

** implement this condition as a function that returns true or false

Image

Two choices for coding the marketing campaign

There are two ways we could implement this marketing campaign. The first is to implement it in the same layer as the abstraction barrier. The other is to implement it above the abstraction barrier. We can’t implement it below the barrier because then marketing can’t call it. Which should we choose?

Image
Choice 1: Part of the barrier

In the barrier layer, we can access the cart as a hash map. But we can’t call any function in the same layer:

function getsWatchDiscount(cart) {

var total = 0;

var names = Object.keys(cart);

for(var i = 0; i < names.length; i++) {

var item = cart[names[i]];

total += item.price;

}

return total > 100 && cart.hasOwnProperty("watch");

}

Choice 2: Above the barrier

Above the barrier, we can’t treat it like a hash map. We have to go through the functions that define the barrier:

function getsWatchDiscount(cart) {

var total  = calcTotal(cart);

var hasWatch = isInCart("watch");

return total > 100 && hasWatch;

}

Image Noodle on it

Which do you think is better? Why?

Implementing the campaign above the barrier is better

Implementing the campaign above the barrier (choice 2) is better for many interrelated reasons. First of all, choice 2 is more straightforward than choice 1, so it wins on pattern 1. Choice 1 increases the amount of low-level code in the system.

Choice 1

function getsWatchDiscount(cart) {

var total = 0;

var names = Object.keys(cart);

for(var i = 0; i < names.length; i++) {

var item = cart[names[i]];

total += item.price;

}

return total > 100 &&

cart.hasOwnProperty("watch");

}

Choice 2

function getsWatchDiscount(cart) {

var total  = calcTotal(cart);

var hasWatch = isInCart("watch");

return total > 100 && hasWatch;

}

Choice 1 doesn’t technically violate the abstraction barrier. The arrows don’t reach across any barriers. However, it violates the purpose behind the barrier. This function is for a marketing campaign, and the marketing team does not want to care about implementation details like for loops. Because choice 1 puts it below the barrier, the dev team has to maintain it. The marketing team will need to talk to the dev team to change it. Choice 2 doesn’t have these problems.

Straightforward implementations

  • • Choice 1
  • Image Choice 2

But there is a subtler problem. The functions that make up the abstraction barrier are part of a contract between the marketing team and the development team. Adding a new function to the abstraction barrier increases the size of the contract. If anything needs to change there, the change will be more expensive because it requires renegotiating the terms of the contract. It’s more code to understand. It is more details to keep in mind. In short, choice 1 dilutes the benefits of our abstraction barrier.

Abstraction barrier

  • • Choice 1
  • Image Choice 2

The minimal interface pattern states that we should prefer to write new features at higher levels rather than adding to or modifying lower levels. Luckily, this marketing campaign is a really clear case and we don’t need an extra function in the abstraction barrier layer. But there are many cases that are much muddier than this one. The minimal interface pattern guides us to solve problems at higher levels and avoid modifying lower levels. And the pattern applies to all layers, not just abstraction barriers.

Let’s look at a more challenging example that will tempt even the best designers among us to make the wrong decision. Hold fast to pattern 3 as you turn the page.

Minimal interface

  • • Choice 1
  • Image Choice 2

Marketing wants to log items added to the cart

The marketing team wants another feature. People are adding items to their carts, but then they abandon the carts without buying. Why? The marketing team wants more information to be able to answer the question so they can improve sales. Their request is to record a log of every time someone adds an item to the cart.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • ImageMinimal interface
  • Image Comfortable layers
Image

Jenna creates the database table and codes up an action to save the record to the database. You call it like this:

logAddToCart(user_id, item)

Now we just need to put it somewhere so that it logs all of them. Jenna suggests putting it in the add_item() function, like this:

function add_item(cart, item) {

logAddToCart(global_user_id, item);

return objectSet(cart, item.name, item);

}

Is this where we should put it? Let’s think about this like a designer. What are the advantages of putting it here? What are the disadvantages? Let’s walk through the consequences together.

The design consequences of code location

Of course, Jenna’s suggestion seems to make a lot of sense. We want to log a record every time a user adds an item to the cart, and add_item() is where that happens. It makes it easy to get that rule right because the function will do the logging for us. We won’t have to remember. We can ignore that detail (the detail of logging) while we work above this layer.

However, there are serious, problematic consequences of logging inside of add_item(). For one, logAddToCart() is an action. If we call an action from inside add_item(), it becomes an action itself. And then everything that calls add_item() becomes an action by the spreading rule. That could have serious consequences for testing.

Because add_item() is a calculation, we were always allowed to use it wherever and whenever we wanted to. Here’s an example:

function update_shipping_icons(cart) {

var buttons = get_buy_buttons_dom();

for(var i = 0; i < buttons.length; i++) {

var button = buttons[i];

var item = button.item;

var new_cart = add_item(cart, item);

if(gets_free_shipping(new_cart))

button.show_free_shipping_icon();

else

button.hide_free_shipping_icon();

}

}

we call add_item() when the user didn’t add it to the cart

we definitely don’t want to log that!

update_shipping_icons() uses add_item(), even when the user hasn’t added the item to the cart. This function gets called every time a product is displayed to the user! We don’t want to log those as if the user added them to their cart.

Finally, and most importantly, we have such a nice, clean set of functions—an interface—to the shopping cart. We should cherish that. It serves our needs. It allows us to ignore the appropriate details. This change does not make the interface better. The call to logAddToCart() should really go above the abstraction barrier. Let’s take a crack at that on the next page.

A better place to log adds to cart

We know two things for certain about the logAddToCart() function: It is an action and it should go above our abstraction barrier. But where should it go?

Again, this is a design decision, so there’s no answer that’s right in all contexts. However, the function add_item_to_cart() is a good choice. That’s the click handler we put on the add to cart button. That’s the place where we are sure we are capturing the intent of the user. It’s also already an action. It also looks like a dispatch of “here’s everything that needs to happen when the user adds an item to the cart.” Calling logAddToCart() is just one more thing to dispatch.

function add_item_to_cart(name, price) {

var item = make_cart_item(name, price);

shopping_cart = add_item(shopping_cart, item);

var total = calc_total(shopping_cart);

set_cart_total_dom(total);

update_shipping_icons(shopping_cart);

update_tax_dom(total);

logAddToCart();

}

add to cart button click handler

other actions that get called when the user clicks

we can add it here along with other stuff that needs to happen when users add items to cart

This isn’t the best possible design, but it is the right place to call this function for our particular design. Without completely redesigning our app, this is where it belongs. A better design would require re-architecting the whole application.

We almost put this function in the wrong place, but we got lucky. It’s lucky we thought about the action spreading rule and that we remembered that the call to add_item() is update_shipping_icons().

But we don’t want to rely on luck for good design. We want a principle that would have avoided this. That’s what the pattern of minimal interface gives us.

The pattern of minimal interface asks us to focus on the sense of a clean, simple, and reliable interface and to use it as a proxy for the unseen consequences in the rest of the code. The pattern guides us to protect our interface from unnecessary changes or expansion.

Pattern 3 Review: Minimal interface

We can think of the functions that define an abstraction barrier as an interface. They provide the operations through which we will access and manipulate a set of values. In stratified design, we find a dynamic tension between the completeness of the abstraction barrier and the pattern to keep it minimal.

There are many reasons to keep the abstraction barrier minimal:

  1. If we add more code to the barrier, we have more to change when we change the implementation.
  2. Code in the barrier is lower level, so it’s more likely to contain bugs.
  3. Low-level code is harder to understand.
  4. More functions in an abstraction barrier mean more coordination between teams.
  5. A larger interface to our abstraction barrier is harder to keep in your head.

Applying this pattern in practice means that if you can implement a function above a layer, using existing functions in that layer, you should. Think carefully about the purpose of the function and at what layer of abstraction it makes sense to implement it. In general, you should prefer higher layers on the graph.

Although the benefit of the minimal interface pattern is clearest in the case of abstraction barriers, it applies generally to all layers. In the ideal case, a layer should have as many functions as necessary, but no more. And, also ideally, the functions should not have to change, nor should you need to add functions later. The set should be complete, minimal, and timeless. This is the ideal to which the minimal interface pattern draws all layers.

Is this possible? Yes, we do see it happening, though not for every layer. We see the ideal achieved when we find a file of source code that has not been modified for years, yet the functions in it are used heavily throughout the codebase. This ideal is achievable at the lower layers of your call graph if they define a small set of operations that provide great power. But keep in mind that it is an ideal to strive toward, not a destination.

The key thing is to sharpen your sense of how well the functions in the layer serve their purpose. Do they do it well, with a small number of functions? Does your change really add to that purpose?

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

Pattern 4: Comfortable layers

The first three patterns have asked us to build our layers. They have given us guidance on how best to do that by striving for ideals. The fourth and final pattern, comfortable layers, asks us to consider the practical side.

It often feels really nice to build layers very tall. Look how powerful! Look at how few details I have to think about! However, it’s rarely so easy to come up with robust layers of abstraction. Often, over time, we realize that an abstraction barrier we built was not so helpful after all. It wasn’t complete. Or it was less convenient than not using it. We all have had the experience of building towers of abstractions too high. The exploration and subsequent failure is part of the process. It is hard to build very high.

It’s also the case that abstraction can mean the difference between an impossible task and a possible one. Look at the JavaScript language, which provides a nice abstraction barrier over machine code. Who thinks about the machine instructions when they’re coding in JavaScript? You can’t! JavaScript does too much and there are too many differences between implementations. How did such a useful layer get devised and built? Thousands of person-years of work over many decades to build robust parsers, compilers, and virtual machines.

As programmers working in the industry, presented with a problem to solve in software, we don’t have the luxury of such resources for finding and building great abstractions. They take too much time. The business can’t afford to wait.

The comfort pattern gives us a practical test of when to stop striving for the other patterns (and also when to start again). We ask ourselves, “Are we comfortable?” If we are comfortable working in the code, we can relax on design. Let the for loops go unwrapped. Let the arrows grow long and the layers meld into one another.

However, if we are uncomfortable with the details we have to keep in our heads or with how unclean the code feels, start applying the patterns again. No codebase reaches the ideal. There is constant tension between design and the need for new features. Let comfort guide you on when to stop. Basically, you and your team live in this code. You have to make it meet your needs as programmers and the needs of the business.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • Image Minimal interface
  • Image Comfortable layers**

    ** you are here

We’ve finished the four patterns of stratified design. Let’s summarize our patterns before we take a last look at the call graph to see how much information we can get from it.

Patterns of stratified design

We’ve reached the end, so just for reference, here are the four patterns we studied in these two chapters.

Pattern 1: Straightforward implementation

The layer structure of stratified design should help us build straightforward implementations. When we read a function with a straightforward implementation, the problem the function signature presents should be solved at the right level of detail in the body. Too much detail is a code smell.

Patterns

  • Image Straightforward implementations
  • Image Abstraction barrier
  • Image Minimal interface
  • Image Comfortable layers

Pattern 2: Abstraction barrier

Some layers in the graph provide an interface that lets us hide an important implementation detail. These layers help us write code at a higher level and free our limited mental capacity to think at a higher level.

Pattern 3: Minimal interface

As our system evolves, we want the interfaces to important business concepts to converge to a small, powerful set of operations. Every other operation should be defined in terms of those, either directly or indirectly.

Pattern 4: Comfort

The patterns and practices of stratified design should serve our needs as programmers, who are in turn serving the business. We should invest time in the layers that will help us deliver software faster and with higher quality. We don’t want to add layers for sport. The code and its layers of abstraction should feel comfortable to work in. If they do, we don’t need to improve the design for the sake of it.

Now we will look at the call graph abstractly to see what it can tell us about reusability, testability, and changeability. We want to keep these factors in mind as we add code around the different layers.

What does the graph show us about our code?

We have learned how to draw the call graph and to use it to help us improve our code. We spent a lot of time in the last chapter on how the graph can guide us to make our code more straightforward. We have also spent time learning other patterns for organizing the layers. But we haven’t talked much about how the structure of the call graph itself can reveal a lot about our code.

The call graph structure shows which functions call what. It’s pure facts. If we eliminate the names of the functions, we have an abstract view of just that structure.

Image

Believe it or not, this structure by itself can tell us a lot about three important nonfunctional requirements. Functional requirements are the things the software has to do work correctly. For example, it has to get the right answer when it does a tax calculation. Nonfunctional requirements (NFRs) are things like how testable, maintainable, or reusable the code is. These are often considered the main reasons for doing software design. They are often called ilities, as in testability, reusability, or maintainability. (No, I am not making this up.)

Let’s look at what the structure of the call graph can tell us about these three NFRs:

  1. Maintainability—What code is easiest to change when requirements change?
  2. Testability—What is most important to test?
  3. Reusability—What functions are easier to reuse?

By looking at just the structure of the call graph, without function names, we’ll see how the position in the call graph largely determines these three important NFRs.

Code at the top of the graph is easier to change

Given an abstract call graph diagram (no function names), can we figure out what code is easiest to change? Knowing that, we’ll know where we need to put the code that implements rapidly changing requirements (like business rules). We’ll also know where to put the code that changes the least. If we put things in the right places, we can drastically reduce the cost of maintenance.

Image
Image

Sarah is right. Code at the top of the graph is easier to change. If you change the function at the very top, you don’t have to think about what else is calling it since nothing is calling it. It can completely change behavior without affecting any calling code.

The longer the path from the top to a function, the more expensive that function will be to change.

Contrast that with functions at the bottom layer. Three levels of functions are relying on its behavior. If you change its external behavior, you change the behavior of everything on the path up to the top. That’s why it’s hard to change safely.

We want code at the bottom that we know implements timeless functions. That’s why we find copy-on-write functions way at the bottom. They can be done once correctly and never changed. When we extract functions into a lower layer (pattern 1) or add functions at higher layers (pattern 3), we are stratifying code into layers of change.

If we put code that changes frequently near or at the top, our jobs will be easier. Build less on top of things that change.

Testing code at the bottom is more important

Now let’s see what this graph can tell us about which code is more important to test. We might think “we should test everything,” but that doesn’t always happen. If we can’t test everything, what is the most important stuff we should test so that the time spent testing will pay off the most in the long term?

Image
Image
Image

If we’re doing it right, code at the top changes more frequently than code at the bottom.

Image

Testing takes work, and we want that work to pay off as much as possible. If we are doing it right, we have moved code that needs to change quickly up to the top and left the more stable code at the bottom. Since code at the top changes frequently, any tests we write for code at the top will also have to change to match the new behavior. Code at the bottom, however, changes very infrequently, so the tests will also change infrequently.

Image

Our patterns help us stratify code into layers of testability. As we extract functions into lower layers or build functions in higher layers, we are choosing how valuable their tests will be.

Testing code at the bottom benefits you more in the long run.

Code at the bottom is more reusable

We have seen that code at the top is easier to change and that code at the bottom is more important to test. Which code is easier to reuse? Reused code is code you didn’t have to write, test, or change twice. Code reuse saves time and money.

Image

We have already seen how stratifying our code can lead to serendipitous reuse. As we extracted functions into a lower layer, we found a lot of possible reuse. The lower layers are more reusable. As we apply the patterns of stratified design, we are stratifying our code into layers of reusability.

The more code below a function, the less reusable it is.

Summary: What the graph shows us about our code

We’ve just seen that the call graph can tell us a lot about the nonfunctional requirements (NFRs) of our code. Let’s review them and rephase them as rules of thumb.

Maintainability

Rule: The fewer functions on the path to the top of the graph, the easier a function is to change.

Image
  • A() is easier to change than B(). A() has one function above it. B() has two.
  • C() is easiest of all to change because it has no functions above it.

Bottom line: Put code that changes frequently near the top.

Testability

Rule: The more functions on the path to the top of the graph, the more valuable its tests will be.

Image
  • It is more valuable to test B() than A() since more code relies on it, having two functions above it.

Bottom line: Test code at the bottom more than code at the top.

Reusability

Rule: The fewer functions underneath a function, the more reusable it is.

Image
  • A() and B() are about the same reusability. They each have no functions under them.
  • C() would be the least reusable, since it has two levels of functions underneath it.

Bottom line: Extract functions into lower layers to make them reusable.

These properties emerge from the structure of our code. We should use these to find optimal layers for change, testing, and reuse. We’ll see a very practical application of this in chapter 16 when we explore the onion architecture.

Conclusion

Stratified design is a technique for organizing our functions into layers of abstraction, where each function is implemented in terms of functions defined in the layers below it. We follow our intuitions to guide our code to a more comfortable system for solving our business’s needs. The layer structure also shows us which code is more testable, changeable, and reusable.

Summary

  • The pattern of abstraction barrier lets us think at a higher level. Abstraction barriers let us completely hide details.
  • The pattern of minimal interface has us build layers that will converge on a final form. The interfaces for important business concepts should not grow or change once they have matured.
  • The pattern of comfort helps us apply the other patterns to serve our needs. It is easy to over-abstract when applying these patterns. We should apply them with purpose.
  • Properties emerge from the structure of the call graph. Those properties tell us where to put code to maximize our testability, maintainability, and reusability.

Up next…

This chapter marks the end of the first major leg of our journey. We have learned about actions, calculations, and data, and how they manifest in our code. We refactored quite a lot, yet there were some functions that resisted easy extractions. In the next chapter, we learn how to truly abstract the for loop. And so begins the next leg of the journey, in which code can be used like data.

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

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