COMP1531
Software Design: Maintainability & Robustness
Presented by Atlassian

In this lecture

- The role of software maintenance in the SDLC
- How to write maintainable and robust software using the core Software Engineering Design Principles.
What's your catchline?
The problems we are discussing today will remain with you for your entire software career.
Your relationship to them will change as you mature, but the principles themselves have remained the same since the early 2000s.
Previously, on COMP1531 (with Nick)
Good software design is all table setting.
When it comes to building software, sometimes you go in already having won or lost the fight.

The way you decide to write your software determines how you set the table.
Should I write it in TypeScript or JavaScript?
Should I use a slow algorithm that is quick to implement or a fast algorithm that is harder to implement?
These types of decisions will determine the quality of your software.

To understand why good design is important, we need to understand maintenance

Brittle Software

Good software is malleable and robust like gold. If you want to change it to be used in a different way, it is easy to change and doesn't break easily.
Brittle software will break when put under pressure.
This pressure comes in the form of changing requirements.
These requirements can be functional (e.g. a new feature) or non-functional (e.g. more requests to an API, increasing the load on the software).
When your code feels like Jenga

Why do we need software to be malleable, robust and maintainable?
Reason (1) There are many unknowns when we build software.
So we can't design everything up-front correctly the first time. We need to be able to adapt our software to unknowns as they arise.
Reason (2) The requirements for our software evolve rapidly.
These can be functional (e.g. a new feature) or non-functional (e.g. more requests to an API, increasing the load on the software.
Web development in particular is very fast-paced as deployment cycles are very short. Agile software development necessitates being able to adjust rapidly.
"All Software Development is maintenance" - Jeff Sutherland (one of the fathers of modern Agile).
Why do we need software to be malleable, robust and maintainable?
Reason (3) Different people will work on the software over time.
Having one person understand all the software is a bad idea. What if they leave? What if a new person joins the team?
Code always grows, it rarely shrinks.
So you need to start thinking not only about whether your code will work now, but also whether it will continue to work later.

The Eternal Problem: Now vs Later

Writing maintainable software includes thoughtfully planning your code, making code well-structured and writing robust tests.
These things all take time, and you will be incentivised to cut corners to save time in the short term.
This will cause problems later down the track.
Slow is smooth. Smooth is fast.
Spending just a little bit more time now will help you later.
It is always much harder to "go back and fix it later". Adding a unit test, or refactoring some code usually takes just a few minutes.
The same is true at the level of an entire business.
A good software company balances the desire to ship as many new features as fast as possible with the need to spend less time shipping features now so the code is maintainable.
This is a constant tug-of-war that has to be worked out to ensure software delivers immediate business value but also works in the long term.
At Atlassian, we aim to build with heart and balance.

How do we make code maintainable?
The Software Maintenance step of the SDLC is a product of all of the previous steps in the cycle.
- Software testing - code that is well tested (statically/dynamically) is more robust and less prone to regressions.
-
Software design - which can be broken down into:
-
System design - this is table setting.
- How is your system put together? How is it broken down into components? How do these components interact?
- Choices such as what language you will use (TypeScript? JavaScript?) are included here.
- Covered in the TS lecture, and in the System Modelling lecture
-
Code design
- We'll talk about this one today!
-
System design - this is table setting.
- Conceptually, making code maintainable is about two things: thoughtful planning and robust coding.
Design Principles
Lots of people have thought about how to write good, robust software before.
You could spend an entire career re-learning the hard way what others have already learned before. Instead, stand on the shoulders of giants.
We'll discuss six of them today.
1. Don't Repeat Yourself (DRY)
2. Keep it Simple, Stupid! (KISS)
3. You Ain't Gonna Need It (YAGNI)
4. Aim for low coupling, high cohesion
5. Follow established patterns, standards and conventions
6. Reduce state where possible
7. Follow your nose (Design Smells)

#1: Don't Repeat Yourself (DRY)
Question to ask: is there a single source of truth for this?
Let's look at an example.
#1: Don't Repeat Yourself (DRY)
import { argv } from 'process';
if (argv.length !== 3) {
process.exit(1);
}
const num = parseInt(argv[2], 10);
if (num === 2) {
for (let i = 10; i < 20; i++) {
const result = Math.pow(i, 2);
console.log(`${i} ** 2 = ${result}`);
}
} else if (num === 3) {
for (let i = 10; i < 20; i++) {
const result = Math.pow(i, 3);
console.log(`${i} ** 3 = ${result}`);
}
} else {
process.exit(1);
}
#2: Keep it simple, stupid!
Question to ask: is this as simple as it could be?
Example: Write a program to generate a random string with 50 characters with a mix of upper/lowercase characters.
#2: Keep it simple, stupid!
Question to ask: is this as simple as it could be?
The more code you write, the more likely you are to introduce errors.
Every line of code you don't write is bug free.
Additionally, the more complex your code is, the more likely you are to introduce errors.
Clear code is better than clever code.
Someone who is not you should ideally be able to understand your code just by reading it.
The sick principle: you should be able to understand your code when you're sick.
#3: You ain't gonna need it
Question to ask: is over-designed or under-designed?
If we over design things, we have complex abstractions to maintain for trivial changes.
YAGNI - only write the code that you know you will need.
A good way to do this is to program top-down / pyramid programming.
Start with the overarching problem, break it down into a series of functions. Then for each of those functions, break those down into further functions until each function performs a single job.

#3: You ain't gonna need it
Question to ask: is over-designed or under-designed?
Example: Write a function that takes in a latitude and a longitude and calculates the time taken to get to the nearest city.
#3: You ain't gonna need it
Programmers' journey - (early) under-design things, then (amateur) over-design, then move towards (mature) good design.

#4: Aim for low coupling and high cohesion
Question to ask: is related code grouped together in distinct places?
Example: Sunny, who is a new member of your COMP1531 group has said that to make your team's code easier to read you are going to move all of the code into a single file.
What do you think about this approach in relation to the question?

#4: Aim for low coupling and high cohesion
Question to ask: is related code grouped together in distinct places?
Example: Sunny, who is a new member of your COMP1531 group has said that your authentication functions need to be split so that each function has its own file and follows the structure (auth_1.ts, auth_2.ts, auth_3.ts, etc.)
What do you think about this approach in relation to the question?

#4: Aim for low coupling and high cohesion
Question to ask: is related code grouped together in distinct places?
Coupling: the extent to which software modules rely on each other.
If you change one area of the code, what is the chance that another area of the code will break?
Tightly coupled code is highly brittle. "Spaghetti code" is a common term for this.
If related code isn't grouped together, it is spread across the codebase and couples components together.
We want to aim for low coupling when writing code.

#4: Aim for low coupling and high cohesion
Question to ask: is related code grouped together in distinct places?
Cohesion: the extent to which related code is grouped together.
Your current COMP1531 project file structure (auth.ts, question.ts, etc.) has good cohesion. Within those files, if code is scattered all over the place then cohesion can be improved.
We want to have high cohesion when building software.

#5: Follow established patterns and conventions
Question to ask: has the problem I am solving already been solved before in a certain way?
Example: A simple express server
#5: Follow established patterns and conventions
Question to ask: has the problem I am solving already been solved before in a certain way?
Following established patterns and conventions ensures that someone who is reading your code for the first time can understand it more easily. It is then easier to reason about, debug and test.
There are many programming patterns (also known as design patterns) - these are ways of solving a problem with an established structure. Following these patterns will mean that you don't reinvent the wheel and you are leveraging the rich wisdom of the past to solve your problem.

#6: Follow your nose
Question to ask: does something "smell" funny about this code?
Example: A few import statements

#6: Follow your nose
Question to ask: does something "smell" funny about this code?
When writing code you will start to notice if there is problematic code just by how it looks. These are called code smells (or design smells). This is an indication that one or more design principles is being violated.
Often the immediate cause/design problem won't be immediately apparent, and you have to understand the code better to be able to fix it.
Sometimes fixing the smell doesn't fix the underlying problem.

#7: Reduce state where possible
All bugs in our code are due to bad manipulations of state.
Most bugs within this are because of the fact we write code imperatively.
What's wrong with this code?
function squareRoot(n: number): number {
let answer = n;
while (answer * answer > n) {
answer = (answer + n / answer) / 2;
}
}
#7: Reduce state where possible
function getValidPasswords(passwords: string[]): string[] {
return passwords.filter((password) => password.length >= 8);
}
function getValidPasswords2(passwords: string[]): string[] {
const validPasswords: string[] = [];
for (const password of passwords) {
if (password.length >= 8) {
validPasswords.push(password);
}
}
return validPasswords;
}
#7: Reduce state where possible
function getValidPasswords(passwords: string[]): string[] {
return passwords.filter((password) => password.length >= 8);
}
function getValidPasswords2(passwords: string[]): string[] {
const validPasswords: string[] = [];
for (const password of passwords) {
if (password.length >= 8) {
validPasswords.push(password);
}
}
return validPasswords;
}
Declarative programming structures that give you less control over state means that you make less mistakes.
This is the beauty of functional-style structures like map, filter, reduce.
#7: Reduce state where possible
Which is better, Code A or Code B?

Refactoring
Refactoring is the process of restructuring existing code without changing its external behaviour.
Often this is to fix design problems to make things more maintainable.
The don't make things worse principle: if you're trying to fix something, prioritise not making it worse.
Refactoring without a strong test suite can introduce bugs.
Sometimes you'll be asked to refactor code, but keep the tests passing.

COMP1531 25T1: Software Design - Maintainability & Robustness
By npatrikeos
COMP1531 25T1: Software Design - Maintainability & Robustness
- 180