A Simple And Powerful Git Version Control Workflow
May 31, 2021What’s New In Tech?
June 25, 2021Introduction
In this post I would like to share some principles and concepts that I have learnt, and hopefully this guide can aid you into expanding your programming knowledge and become a better dev from it.
I think we as Devs often are guilty of writing code that works and is quick to type up, rather than considering writing clearer code thats more focused and easier to read. The guide is not to go through Design Principles and Architecture or to explain programming paradigms such as OOP vs Functional vs Procedural programming but rather these guidelines are merely to offer some knowledge in how you can structure your code to be cleaner for yourself and any team member who comes across your code and has to expand on your developments.
the following is a summary of what I have learnt in the Udemy Course by
Maximilian Schwarzmüller - Academind
Clean Code Course
Body
What is Clean Code?
its not about whether code works or not, its about whether code is easy to read and understand. A vast majority of our time as developers is spent reading and understanding code.so reading and understanding code should be easy.
Here are some pointers that outline what is "Clean Code"
- Should be readable and meaningful
- Should reduce cognitive load
- Should be concise and "to the point"
- Should avoid unintuitive names, complex nesting and big code blocks
- Should follow common best practices and patterns
- Should be fun to write and to maintain
You code is like an essay and you are the author, write it such that it's fun and easy to read and understand.
Key Pain Points
- Names
- Variables
- Functions
- Classes
- Structure
- Code formatting
- Good & Bad Comments
- Functions/Methods
- Length
- Parameters
- Conditionals & Error Handling
- Deep Nesting
- Missing Error Handling
- Classes & Data Structures
- Missing Distinction
- Bloated Classes
Solutions are
- Rules and Concepts
- Patterns & Principles
- Test-Driven Development
Naming
Why Good Names Matter
Names should be meaningful, and should transport what a variable or method/function should do.
Here is an example of a poorly names piece of code:
const us = new MainEntity(); us.process(); if(login){ //... }
as you can see nothing really makes sense from the get go. nothing is straight forward and this code will mean that you will have to dig deeper into each function and variable to understand its meaning. Take a look at an example where we refactor the above into code that is more easy to understand.
class User { save() {} } const isLoggedIn = true; const user = new User(); user.save(); if (isLoggedIn) { // ... }
as you can see, it's the same code, but way easier to read and understand.
in conclusion to this, well-named "things" allow readers to understand your code without going through it in detail
1.How to name things correctly
variable & Constants
Use nouns or short phrases with adjectives e.g
const userData = {...}
or
const isValid = ...
Functions & Methods
use verbs or short phrases with adjectives e.g
sendData();
inputIsValid();
Classes
use nouns or short phrases with nouns
class User {...}
class RequestBody {...}
Name Casings
snake_case | camelCase | PascalCase | kebab-case |
---|---|---|---|
is_valid | isValid; SendResponse | AdminRole; UserRepository | <side-drawer> |
e.g Python, PHP | Java, Javascript | Python, Java, Javascript | e.g HTML |
Variables, functions, methods | Variables, functions, methods | Classes | Custom HTML elementsL |
2. Code Structure, Comments & Formatting
Bad Comments include the following:
- Redudent information
- Dividers/Blockers
- Misleading information
- Commented out code.
Good Comments include the following:
- Legal Information
- Explanation which can’t be replaced by good meaning
- Warnings
- Documentation
Code Formatting
Code Formatting improves Readability & Transports Meaning
There are two areas of formatting to consider
- Vertical Formatting
- Horizontal Formatting
Vertical Formatting
- Vertical Space Between Lines
- Grouping of Code
Horizontal Formatting
- Indentation
- Space Between Code
Functions & Methods
What makes up a function?
Calling the function should be readable and working with the function should be easy
therefore the number and order of arguments matter and the length of the function body matters.
Minimize the number of parameters
why? Because the more parameters a function uses the more difficult it becomes to use.
A function with no parameters is the simplest function to call and read and this should always be the goal when creating a function.
of course there will be situations where you will need to use parameters and from there obviously 1 parameters is the next best thing to no parameters.
2 Parameters starts to become more tricky to understand as it requires more information in order to get the function to run, a function with 2 parameters is acceptable but should be used with caution.
3 Arguments is really difficult to understand and is usually challenging to call and should be avoided if possible. It's usually difficult to determine the order of arguments and that inherently becomes more complex and difficult to understand.
any function with more than 3 arguments should be avoided at all costs because it's extremely difficult to understand and maintain. there are solutions to handle functions that require so many arguments.
Refactoring Functions that have multiple params.
function saveUser(email, password){
const user = {
id: Math.random().toString(),
email:email,
password: password
}
db.insert('users', user);
}
saveUser('user@test.com' , '123');
This is not terrible code and does make sense when reading it, however it can be refactored to use less parameters. Take the following code as an example where we outsouce the user argument and parse in a single argument to the function.
function saveUser(user){
db.insert('users',user)
}
the above is better because it only takes in one argument for the function to work, and that greatly simplifies the calling of the function. The better solution would be to lift this functionality into a class based approach, such as the following:
class User{
constructor(userData){
this.email = userData.email;
this.password = userData.password;
this.id = Math.random().toString();
}
save(){
db.insert('users',this);
}
}
const user = new User({email: 'test.user.com', password: '123'})
user.save();
This is much better as its very easy to read and you can immediately see what is expected to get a user object.
functions should be small and do one thing.
As developers we often get carried away with writing huge chunks of code and often forget to split our functions into smaller and more meaningful functions.
in general a function can have code naming and formatting, but a monolithic function that handles multiple things all at once is still considered bad code. It's okay to write more code if it adds readability and clarity to what it is that you are trying to achieve
Understanding levels of abstraction
So when it comes to abstraction you have a range of levels.
- High Level
- Low Levels
High levels can be thought of as user defined functions e.g
isEmail(email)
Low Level abstractions is usually language or programme specific low level api and methods.
e.g
email.includes('@);
in High Level operations we don't control how the email is validated - we just want it to be validated. So
in Low level operations, we control how the email is validated.
Something else to note is that high level abstractions usually are easy to read - there is no room for interpretation, whereas low level operations might be technically clear but the interpretation must be added by the reader
Functions should do work that's one level of abstraction below their name
Okay what does that mean?
take a look at the example below:
function emailIsValid(email){
return email.includes('@');
};
as you can tell the name of the function expands/describes the low level implementation of what this function is doing, its important to understand that a function should be clear as to what its purpose is. the function its self is a high level abstraction of the low level code that is within the function, and that makes it easier to understand and interpret.
take a look at the below and see how one should not use low level abstractions in a high level function.
function saveUser(email){
if(email.includes('@')){
//...
}
}
as you can tell the function name does not really interpret the low level code within the function. Yes it makes sense that function like this should have a step for validation but having the validation directly in this function is not clear and requires some additional thinking as to what is the purpose of the function.
a function like this should orchestrate all the steps that are required to save a user.
Try not to mix levels of Abstraction
take a look at the following:
if(!email.includes('a')){
console.log('Invalid email!');
}else{
const user = new User(email);
user.save();
}
as you can see we are clearly mixing levels of abstraction here, and this forces the reader to read and understand and interpret each block of code to fully understand each step
where as the following makes things a lot clearer and the reader only has to read the steps to understand the code.
if(!isEmail(email)){
showError('Invalid Email');
}else{
saveNewUser(email)
}
Keeping functions short
here are two rules of thumb that can help you identify when to split code or when to merge code.
take a look at the following:
user.setAge(21);
user.setName('Mitchell');
a rule of thumb
Extract code that works on the same functionality.
we can simplify the above to a single function.
user.update({name: 'Mitchell', age: 21 });
the second rule of thumb.
Extract code that requires more interpretation than the surrounding code
for e.g
if(!email.includes('@')){...}
saveNewUser(email);
then we can easily see that this email.includes check requires more interpretation from our side than having a look at saveNewUser.
There it's directly obvious what this does and why we're doing this. For email.includes, we have to add the extra meaning of email validation being done here.
So changing it to this code to the following would bring it all back onto the same level of abstraction and on the same level of required interpretation, you could say.
if(!isValid(email)){...}
saveNewUser(email)
Why Unit tests matter
Unit Tests matter because it forces you to write slim and focused functions which are easier to test and typically read and understand.
The next step to mastering writing clean code
This blog post was really an introduction to understanding clean code and what it really is about. the next step after reading this post is to go deeper into Control Structures, Object and Classes, and to Learn the SOLID principles.
these topics are quite broad for this post but defiantly comment if you would like to see a follow up post on these topics.