Author: Hayden Smith 2021
Why?
What?
Generally speaking, we can consider software well written if:
This lecture focuses on the design and development aspects of good 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
Purpose is to make things:
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.
"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
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
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:
We cover some of this later in the course
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.
Often, our default tendency is to write bad code. Why?
Bad code: Easy short term, hard long term
Good code: Hard short term, easy long term
"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
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)
How can we improve this?
import jwt
encoded_jwt = jwt.encode({'some': 'payload'}, 'applepineappleorange', algorithm='HS256')
print(jwt.decode(encoded_jwt, 'applepineappleorange', algorithms=['HS256']))
"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?
Example 1: Write a python function to generate a random string with up to 50 characters that consist of lowercase and uppercase characters
Example 2: Write a function that prints what day of the week it is today
Example 3: Handling command line arguments
python3 commit.py -m "Message"
python3 commit.py -am "All messages"
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.
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
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
Restructuring existing code without changing its external behaviour.
Typically this is to fix code or design smells and thus make code more maintainable
Software can be correct (tested), well designed, but still written like absolute garbage. We want to write nice code, because:
"Clean code" is often a language specific topic.
Clean code is generally defined by:
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 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 are functions that help as accomplish basic iterative tasks without the overhead of a loop setup
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: 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: 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
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
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:
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
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
Programmers will often go through 3 stages:
from datetime import datetime
def dateNow():
return datetime.now()
if __name__ == '__main__':
print(dateNow())
benign.py