COMP1531
🐶 Software Engineering
5.1 - Design - Writing good software
Author: Hayden Smith 2021
In this lecture
Why?
- Writing good software makes your team members happy, and makes your code less likely to break
What?
- What makes good software
- Elements of good design
- Elements of clean code
Writing good software
Generally speaking, we can consider software well written if:
- (Testing) It's correctness is verifiable in an automated way
- (Design) It is planned, modular, and resistant to breaking changes
- (Development) It is clean and easy to work with
This lecture focuses on the design and development aspects of good software.
Well designed software
Something happens between writing tests and finishing code: Design
The design of software happens when you know what problem you're trying to solve with code, but want to think about the best way to solve the problem before you finish the code.
Testing
Design
Development
Design Principles
Purpose is to make things:
- Reusable / Modular
- Maintainable
- Robust to changes
Design approaches that do not make things better are called design smells
Generally speaking, well designed code is simple, clear, and resists the tendency to break as the software changes or grows.
This is more critical than ever with the rapidly iterative nature of modern web development.
Why is well designed software important?
"Poor software quality costs more than $500 billion per year worldwide" – Casper Jones
Systems Sciences Institute at IBM found that it costs four- to five-times as much to fix a software bug after release, rather than during the design process
Where is "design"?
The term design is thrown around very broadly. But we can think of it in two different lens:
1. Before coding: Design means thoughtfulness in planning
2. During coding: Design means robustness in coding
Design: Thoughtful planning
Thinking hard before you code is a great tactic to ensure that your time coding is efficient and that you carry useful documentation.
Examples include:
- Writing pseudocode
- Flow diagrams
- Component diagrams
- State diagrams
- etc
We cover some of this later in the course
Design: Robust Coding
Just because you plan software well, it doesn't mean when you come to write it that it will turn out well designed in it's execution.
Why do we write bad code?
Often, our default tendency is to write bad code. Why?
-
It's quicker not to think too much about things
- Good code requires thinking not just about now, but also the future
- Pressure from business we're working for
Bad code: Easy short term, hard long term
Good code: Hard short term, easy long term
DRY
"Don't repeat yourself" (DRY) is about reducing repetition in code. The same code/configuration should ideally not be written in multiple places.
Defined as:
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system"
Why do we care?
When you repeat yourself less, a change in one module is unlikely to break entire systems
DRY
How can we clean this up?
import sys
if len(sys.argv) != 2:
sys.exit(1)
num = int(sys.argv[1])
if num == 2:
for i in range(10, 20):
result = i ** 2
print(f"{i} ** 2 = {result}")
elif num == 3:
for i in range(10, 20):
result = i ** 3
print(f"{i} ** 3 = {result}")
else:
sys.exit(1)
DRY
How can we improve this?
import jwt
encoded_jwt = jwt.encode({'some': 'payload'}, 'applepineappleorange', algorithm='HS256')
print(jwt.decode(encoded_jwt, 'applepineappleorange', algorithms=['HS256']))
KISS
"Keep it Simple, Stupid" (KISS) principles state that a software system works best when things are kept simple. It is the believe that complexity and errors are correlated.
Your aim should often be to use the simplest tools to solve a problem in the simplest way.
"Every line of code you don't write is bug free"
Why do we care?
- Complicating things with boutique solutions means more code to maintain (more likely to break)
- More code written means more code to test
KISS
Example 1: Write a python function to generate a random string with up to 50 characters that consist of lowercase and uppercase characters
KISS
Example 2: Write a function that prints what day of the week it is today
KISS
Example 3: Handling command line arguments
python3 commit.py -m "Message"
python3 commit.py -am "All messages"
Minimal Coupling
Coupling is the degree of interdependence between software components.
The more software components are connected, the more changes and alterations to one component may break another (either at compile time or runtime)
Excessive coupling can lead to spaghetti code.
Top-down thinking
Similar to "You aren't gonna need it" (YAGNI) that says a programmer should not add functionality until it is needed.
Top-down thinking says that when building capabilities, we should work from high levels of abstraction down to lower levels of abstraction.
Why do we care?
Removes unnecessary code, less to maintain, less likely to cause problems
Top-down thinking
Question 1: Given two Latitude/Longitude coordinates, find out what time I would arrive at my destination if I left now. Assume I travel at the local country's highway speed
Refactoring
Restructuring existing code without changing its external behaviour.
Typically this is to fix code or design smells and thus make code more maintainable
Finding a balance
- Don't over-optimise to remove design smells
- Don't apply principles when there are no design smells - unconditional conforming to a principle is a bad idea, and can sometimes add complexity back in
Well developed software
Software can be correct (tested), well designed, but still written like absolute garbage. We want to write nice code, because:
- It's easier for future you to read and understand
- It's easier for others to read and understand
- It's easier to find and spot errors now and in future
"Clean code" is often a language specific topic.
- Some things we talk about are universal
- Some things only work in a handful of languages
- Some things apply to only python specifically
What is clean code?
Clean code is generally defined by:
- Being as simple as possible
- Following standard & understood conventions
Being Pythonic
On the topic of following standard conventions: Within python, being "Pythonic" means that your code generally follows a set of idioms agreed upon by the broader python community.
"When a veteran Python developer calls portions of code not “Pythonic”, they usually mean that these lines of code do not follow the common guidelines and fail to express its intent in what is considered the most readable way. On some border cases, no best way has been agreed upon on how to express an intent in Python code, but these cases are rare."
Hitchhiker's guide to python (read more on this)
Some of these include destructuring, enumerate
Docstrings
Docstrings are an important way to document code and make clear to other programmers the intent and meaning behind what you're writing. We are somewhat different on the formatting, but we want it to include 1) Description, 2) Parameters, 3) Returns
def string_find(str1, str2):
""" Returns whether str2 can be found within str1
Parameters:
str1 (str): The haystack
str2 (str): The needle
Returns:
(bool): Whether or not str2 could be found in str1
"""
docstring.py
Map, Reduce, Filter
Map, reduce, filter are functions that help as accomplish basic iterative tasks without the overhead of a loop setup
- Map: creates a new list with the results of calling a provided function on every element in the given list
- Reduce: executes a reducer function (that you provide) on each member of the array resulting in a single output value
- Filter: creates a new array with all elements that pass the test implemented by the provided function
Map
Map: creates a new array with the results of calling a provided function on every element in the calling array
def shout(string):
return string.upper() + "!!!!"
if __name__ == '__main__':
tutors = ['Simon', 'Teresa', 'Kaiqi', 'Michelle']
angry_tutors = list(map(shout, tutors))
print(angry_tutors)
map.py
Filter
Filter: creates a new array with all elements that pass the test implemented by the provided function
from functools import reduce
if __name__ == '__main__':
marks = [ 65, 72, 81, 40, 56 ]
passing_marks = list(filter(lambda m: m >= 50, marks))
total = reduce(lambda a, b: a + b, passing_marks)
average = total/len(passing_marks)
print(average)
filter.py
Reduce
Reduce: executes a reducer function (that you provide) on each member of the array resulting in a single output value
from functools import reduce
def custom_sum(first, second):
return first + second
if __name__ == '__main__':
studentMarks = [ 55, 43, 34, 23, 22, 10, 44 ]
total = reduce(lambda a, b: a + b, studentMarks)
print(total)
reduce.py
Combined
from functools import reduce
if __name__ == '__main__':
marks = [ 39, 43.2, 48.6, 24, 33.6 ] # Marks out of 60
normalised_marks = map(lambda m: 100*m/60, marks)
passing_marks = list(filter(lambda m: m >= 50, normalised_marks))
total = reduce(lambda a, b: a + b, passing_marks)
average = total/len(passing_marks)
print(average)
allthree.py
Exceptions > Error Codes
C-style programming follows a principle of methodical process of using return values to denote particular errors. Whilst this makes programs more easy to reason with, it convolutes codea nd makes it hard to understand. For this reason, we prefer exceptions.
def sqrt(num):
if num < 0:
return None
return num ** 0.5
myNum = int(input())
if sqrt(myNum) is not None:
print(sqrt(myNum))
early.py
The problems though are:
- Often we can only use "None" or some arbitrary return (-1) to signify that it didn't work
- It's harder to check for a client using it
Exceptions > Early Returns
Using exceptions. And we can make our own.
class SqrtException(Exception):
pass
def sqrt(num):
if num < 0:
raise SqrtException("Number cannot be < 0")
return num ** 0.5
try:
print(sqrt(int(input())))
except SqrtException as e:
print(e)
early.py
Multi-line strings
Someones strings need to exist over multiple lines, there are two good approaches for this
if __name__ == '__main__':
text1 = """hi
this has lots of space
between chunks"""
text2 = (
"This is how you can break strings "
"into multiple lines "
"without needing to combine them manually"
)
print(text1)
print(text2)
multiline.py
Meaningful Abstractions
Programmers will often go through 3 stages:
- (Bad) Not appreciating abstraction
-
(Bad) Over-appreciating abstraction
- Creating benign-abstractions
- (Good) Appreciating abstraction appropriately
from datetime import datetime
def dateNow():
return datetime.now()
if __name__ == '__main__':
print(dateNow())
benign.py
Feedback
COMP1531 21T3 - 5.1 - SDLC Design - Graceful Programming
By haydensmith
COMP1531 21T3 - 5.1 - SDLC Design - Graceful Programming
- 1,969