Software development is an increasingly complex business. While it’s always been about solving problems, there are now more problems to solve and more ways to solve them than ever before. A proliferation of platforms, languages, frameworks, patterns, and practices means that there is always something new to discover.
As much as we might love writing code, trying to keep up with new technology can cause stress, anxiety and self-doubt. No one can reasonably be expected to have deep knowledge of every language, framework or tool that we end up using on a project. This means that almost every development task we are faced with is in some way a learning experience.
Our development teams will always be composed of people of varying skill levels, and equalising the skill levels of all team members may never be practical or possible. What can we do to enable effective learning within our teams, thereby helping the whole team to be more productive?
The magical number seven, plus or minus two
Cognitive psychologist George A. Miller proposed in 1956 that there are definable limits to our capacity to process information. His seminal paper gave rise to the idea that at any one time we can only hold in our working memory the magical number of 7 ± 2 objects. This has implications for our ability to perform complex tasks and particularly our ability to learn.
In the 1980s, John Sweller, an educational psychologist, built on these ideas to develop cognitive load theory. He took the idea that we have a limited working memory and began to research the factors that affect the load we impose on it. That research has suggested ways that we can manage cognitive load for more effective learning experiences and encourage expertise.
Cognitive load theory defines three types of load that are imposed when we undertake a learning or problem-solving experience: intrinsic, extraneous, and germane. Each contributes to the total cognitive load imposed on our working memory. In this article I’ll explain some simple ways that we can manage each type of load using techniques that most development teams may already be familiar with.
Quite simply, the complexity of a concept to be learned, or problem to be solved defines the intrinsic load imposed. In development terms, the more complicated a feature is to develop, the more intrinsic load the person implementing it must handle. As a consequence we can only manage intrinsic load by breaking high load tasks into smaller, less complex ones.
Less is more
One of the fundamental principles of most agile software development methodologies is reducing the complexity and size of tasks in order to enable an adaptable incremental development process with short iterations.
You may have come across the concept of user stories and story pointing. While story pointing is intended as an aid to estimating the amount of time a task will take to complete, it also allows us to quantify the intrinsic load of a development task.
It’s accepted that bigger features or tasks are more difficult to estimate accurately. So, when estimating stories, it is beneficial to break larger features into more manageable tasks with fewer story points each. This is also exactly what we should be doing to manage the intrinsic load of the tasks and features we work on.
Unless you’re well practiced in working with them, design patterns can also contribute to the intrinsic load of implementation. Not only do you have to contend with the complexity of developing widget X to satisfy requirement Y, but also having to write your code to conform with pattern or architecture Z.
Depending on the experience of your team, consider whether you should impose a set of architectural patterns right from the start. It may be a better learning experience to allow a project to evolve until it hits a clear sticking point which a particular pattern will solve. Members of the team who aren’t as experienced will be given valuable real world exposure to problem and solution, which puts them in the position of being able to make an informed choice in future.
Self-doubt and uncertainty about our implementation methodology can hold us back from further learning. If we’re not sure that the understanding we have is correct then we can never fully commit that understanding to long-term memory. If we’re unsure we’ve made the best decision, we’ll be held back in the state of a learner looking for validation. This state of constantly checking and re-checking what we’ve already done imposes a continuing intrinsic load on our working memory.
Code reviews help validate and embed understanding. We want to attain a state where we promote something called automaticity, where we are using learned skills unconsciously without taxing our working memory. This is similar to the way “muscle memory” assists pianists in coordinating the movement of their fingers.
Achieving this state takes many cycles of repetition, so it is worth doing regular code reviews to make sure team members are getting appropriate feedback and validation.
Extraneous or irrelevant load is imposed by factors that aren’t directly relevant to the problem we are trying to solve or the information we are trying to assimilate.
Imagine trying to comprehend a mathematics textbook with a brass band band playing show tunes outside the window. The content of the textbook is the intrinsic load, the show tunes are an extraneous load assaulting your senses, and it takes mental effort to filter this out and focus on the relevant input.
Less is still more
I suspect every developer is familiar with the problem of context-switching. Perhaps someone interrupts your train of thought with a question, or an urgent bug arises on another project. When you try to switch context back to what you were doing, it can take a little while to get going again.
This type of distraction can occur inside our codebases too; every framework and tool will contribute to the extraneous load as you switch from the context of one language or configuration system to another.
I‘m not saying these tools are as useless as a brass band playing show tunes would be to a mathematician; but how often do we stop to consider whether they are absolutely necessary and adding value to the particular project we’re working on? For example, in a corporate environment where you’re serving an intranet web application over Gigabit ethernet, do we really need to worry about minification or obfuscation?
Are the tool-chains that experienced developers can accrue entirely necessary for a new project, or should we be growing them on an as-needed basis each time? If we introduce components one at a time (as far as it’s possible to do so) we reduce extraneous load on less experienced developers who may not be familiar with those tools. This also allows us to step away from any issues of toolchain dogma and gives a valuable opportunity to reconsider what tool might best solve a problem at the point that we encounter it as a team.
Cues and Signals
Not knowing exactly where to focus your attention can also impose extraneous load. Without clear signs to cue you with the most relevant points or signal the boundaries of distinct concepts we cannot prioritise. With no choice but to assign equal relevance to everything in front of us, we can be overwhelmed trying to assimilate it all at once.
Large bodies of text with no paragraphs or punctuation? No headings or subtitles? No capital letters? It would be unthinkable to publish an article without any of these common cues and signals to the reader.
Paragraphs signal a new point or idea, headers and sub-titles cue the reader with structural information, capital letters cue the reader to expect the start of a sentence or a proper noun.
We should approach writing code with a similar mindset. By directing attention to specific areas we reduce the extraneous load we impose on team members who have to review, integrate with or maintain what we’ve written.
Comments as cues
Good comments are cues to the developer that a section of code needs additional care or consideration. Code will never be able to tell you the business reasoning behind a certain algorithm or order of execution, but a well-written comment can. Comments alleviate uncertainty where complex code is unavoidable and cue a developer to tread cautiously where all might not be as straightforward as it seems.
Paragraphs of code
Just as we split the text of an article into paragraphs to convey each point separately, we can and should do the same with our code. Although we should usually strive to avoid anti-patterns like the “God Method”, it’s not uncommon in procedural languages to find a method with more than one distinct task.
Perhaps a method might start by checking its parameters for existence and type, then it might do processing of some sort after which it might do further checking and return a formatted result.
If these tasks all ran together without any whitespace we might struggle to differentiate between them and our brains are likely to read it as a single task on first viewing. By introducing line breaks and appropriate white space, we signal to the reader where each distinct task begins, reducing the load by enabling each task to be focused on individually.
Germane cognitive load is possibly the most difficult to manage as it is imposed by materials and tasks that are beneficial to the overall goal of the learner. In other words, we want to impose germane load, but need to consider the degree to which we do so.
For example, when a new developer is assigned mid-project, overall learning goals might be to gain an understanding of both the problem domain and the existing codebase. It would be typical to assign such a developer simple tasks in the beginning to allow them to get a feel for the problem domain and the patterns and architecture in use. The choice of tasks we assign in attempting to bring someone up to speed is one way of managing their germane load.
It’s never a good idea to silo developers in their own corner of a code-base. If all we ever work with is an area of the codebase that does one thing, we may become proficient at understanding that narrowly defined area but will fail to see broader applications of our knowledge, and as Abraham Maslow famously commented, “if all you have is a hammer, everything looks like a nail”.
We want to promote broader understanding of the problem domain and encourage our team to develop flexible, transferrable and consistent skills. We definitely want to avoid the situation where an issue arises in a part of our codebase that only one person understands. If we vary the tasks that our team members are assigned we impose positive germane load towards that goal of broader understanding.
While this variation is beneficial, germane load still taxes our working memory and there can definitely be too much of a good thing. Assigning varied tasks can also impose a high context switching burden if the tasks are in completely different libraries or services. Look for commonality between tasks if possible. Any exposure to new patterns, architecture or areas of the problem domain could be useful germane load, but try not to introduce too much that is unfamiliar with each successive task or the burden could become too great and productivity could take a hit.
For the overall learning goal of gaining proficiency in the languages and tools we employ, task variation can only take us so far. Tasks are rarely so prescriptive as to specify that a developer use a particular pattern or language feature and so can do little to encourage learning in those directions.
Pairing junior developers with more experienced developers gives the less experienced developer a more diverse experience of writing code which can promote a more flexible, transferrable skillset. The potential germane load here is in any exposure to a new algorithm or language feature that they may not have come across before, or an alternative explanation of something not fully understood which can help cement understanding.
Learning from a peer can be enormously beneficial but it does carry a risk if the skill gap between developers is too great. There may be much to learn from a more seasoned developer but too much may be overwhelming and ultimately unproductive!
Cognitive load theory is a powerful tool to help us understand the way we process information and manage learning experiences. Viewing development as a learning experience allows us to look at everyday methods and practices with an alternate perspective, giving us a richer understanding of why they are important.
We’ve seen that we can manage the load we impose on our teams by considering the three components of cognitive load individually:
- We can manage intrinsic load by reducing task size and complexity while we encourage automaticity and understanding within our team through regular code review.
- Extraneous load can be reduced through minimising the number of distractions and moving parts. We can also help focus attention through writing our code to be read by human beings not just the computer.
- Managing germane load is about guiding the learning processes of our team with respect to broader goals such as domain knowledge or language acquisition. We can achieve this through varying the tasks that we assign and encouraging sharing of knowledge between team members through pair programming.
An awareness of cognitive load theory could get us all thinking about the load we impose upon our teams through the processes we devise, the tools we choose and the code we write.