The perspective of a designer

To bestow us with the perspective of a designer, let's explore a simple problem. We must construct an abstraction that allows users to give us two strings, a subject string and a query string. We must then calculate a count of the query strings found within the subject string.

So, consider the following query string:

"the"

And have a look at the following subject string:

"the fox jumped over the lazy brown dog"

We should receive a result of 2.

For our purposes as a designer, we care about the experience of those who must use our code. For now, we won't worry about our implementation; we will instead only consider the interface, as it is primarily the interface to our code that will drive our fellow programmers' experiences.

The very first thing we may do as a designer is to define a function with a carefully chosen name and a specific set of named arguments:

function countNeedlesInHaystack(needle, haystack) { }

This function accepts needle and haystack and will return Number, indicating the count of needle within haystack. The consumer of our code would make use of it like so:

countNeedlesInHaystack('abc', 'abc abc abc'); // => 3
We are using the popular idiom of needle-in-a-haystack to describe the problem of looking for a substring within another string. Considering popular idioms is a crucial part of designing code, but we must be wary of idioms being misunderstood.

The design of a piece of code should be defined by the problem domain we wish to solve and the user experience we wish to reveal. Another programmer, given the same problem domain, may have chosen a different solution. For example, they may have employed partial application to allow the following calling syntax:

needleCounter('app')('apple apple'); // => 2

Or perhaps they may have designed a more verbose syntax that involves invoking a Haystack constructor and calling its count() method like so:

new Haystack('apple apple'),count('app'); // => 2

This classical approach arguably has a nice semantic relationship between the object (Haystack) and the count method. It meshes well with the OOP concepts we've explored in previous chapters. That said, some programmers may find it to be overly verbose.

There's also the possibility of a more descriptive API where the arguments are defined within a configuration object (that is, a plain object literal passed as the sole argument):

countOccurancesOfNeedleInHaystack({
haystack: 'abc abc abc',
needle: 'abc'
}); // => 3

There's also the possibility that this counting functionality may form the part of a larger set of string-related utilities and, hence, can be incorporated into a larger custom-named module:

str('omg omg omg').count('omg'); // => 3

We may even consider it okay to modify the native String.prototype, even though it is inadvisable, so that we have a count method available on all strings:

'omg omg omg'.count('omg'); // => 3

In terms of our naming conventions as well, we may wish to avoid the needle-in-a-haystack idiom and, instead, use more descriptive names where perhaps there is less risk of misunderstanding, like the following:

  • searchableString and subString
  • query and content
  • search and corpus

The choices available to us, even within this very narrow problem domain, are overwhelming. You'll likely have many of your own strong opinions about which approach and naming conventions would have been superior here.

The fact that we can solve a seemingly simple problem with so many different approaches shows us how there is a need for a decision process. And this process is software design. Effective software design employs design patterns to encapsulate problem domains and provide familiarity and ease of comprehension to fellow programmers.

The intent with our exploration of the needle-in-a-haystack problem was not to find a solution, but rather to highlight the difficulty of software design, and to expose our minds to a more user-oriented perspective. It also reminds us that there is very rarely one ideal design.

A well-chosen design pattern, given any problem domain, can be said to have two basic characteristics:

  • It solves the problem well: A well-chosen design pattern will be well-suited to the problem domain so that we can fluidly express the nature of the problem and its solution easily.
  • It is familiar and usable: A well-chosen design pattern will be familiar to our fellow programmers. It'll be immediately obvious how can they can use it or make changes to the code.

Design patterns are useful in a variety of contexts and scales. We use them when we write individual operations and functions, but we also use them when structuring our entire code base. Design patterns, as such, are hierarchical. They exist on the macro and micro scale of a code base. A singular code base can easily contain many design pattern within.

In Chapter 2, Tenets of Clean Code, we spoke about familiarity as a crucial characteristic. A car mechanic opening the hood of a car will hope to see many familiar patterns: from the individual pieces of wiring and welding of Components to the larger construction of the cylinders, valves, and pistons. There is a certain layout they would expect to find and if it is not there, then they would be left scratching their heads, wondering how to approach whatever problem they're trying to solve.

Familiarity increases the maintainability and usability of our solutions. Consider the following directory structure and the displayed logger.js source code:

What design patterns can we observe here? Let's take a look at some examples:

  • The use of a top-level app/ directory to contain all the source code
  • The existence of Models, Views, and Controllers (MVC)
  • The separation of utilities into its own directory (utils/)
  • The camel case naming of files (for example,binarySearch.js)
  • The use of a Conventional Module pattern in logger.js (that is, exporting a plain object of methods)
  • The use of ... && msgs.length to confirm a nonzero (that is, truthy) length
  • Declaring constants at the top of a file (that is, const ALL_LOGS_LEVEL)
  • (Possibly others...)

Design patterns are not just large, lofty architectural structures. They can exist in every part of our code base: the directory structure, the naming of files, and the individual expressions of our code. At every level, our usage of common patterns can increase our ability to express the problem domain, and increase the familiarity of our code to newcomers. Patterns exist within patterns.

Using design patterns well can have beneficial effects on all of the tenets of clean code we covered previously—reliability, efficiency, maintainability, and usability:

  • Reliability: A good design pattern will suit the problem domain and allow you to easily express your desired logic and data structures without too much complexity. The familiarity of your adopted design patterns will also enable other programmers to easily understand and improve upon the reliability of your code over time.
  • Efficiency: A good design pattern will enable you to fuss less about how to structure your code base or your individual modules. It'll enable you to spend more time worrying about the problem domain. Well-selected design patterns will also aid in making the interfaces between different pieces of code streamlined and understandable.
  • Maintainability: A good design pattern allows for easy adaptation. If there is a change of specification or a bug that needs to be fixed, the programmer can easily find the desired area of change/insertion and make the change without hassle.
  • Usability: A good design pattern is easy to understand due to its familiarity. A fellow programmer can easily comprehend the flow of the code and quickly make correct assertions about how it works and how they can make use of it. A good design pattern will also create a pleasant user experience, whether expressed via a programmatic API or a GUI.

You can see that a lot of what makes design patterns useful is only actualized if we pick the right one. We'll be exploring a selection of popular design patterns and, for each, we'll discuss the types of situations they're suited to. This exploration should hopefully give you a good idea of what it means to select a good design pattern.

Be warned: just as good design proliferates via convention, so does bad design. We discussed the phenomenon of cargo culting in Chapter 3, The Enemies of Clean Code, and so we are not strangers to how such types of bad designs may spread, but it's important to remain mindful of these traps when employing design patterns.
..................Content has been hidden....................

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