Quality code brief guide: How to improve your coding skills
What is quality code?
There are a bunch of ways to measure code quality. Of course, the first thing to assure is that the intended functionality is doing well enough to be a good experience for the final user. Don't ever forget that programming is a tool for doing something, so the final user should always be in your mind while writing code. But that is the very beginning.
Good code is also clean and readable, making it understandable for future developers. Should be easy to extend and maintain, adding new features or improving existing ones shouldn't mean refactoring the whole thing. As if that were not enough, the code needs to be testable and, of course, have the corresponding tests.
Why quality code is important?
Code quality has an important impact on software, both for users and developers. Its importance lies in facilitating the work of other developers. It is so difficult to maintain bad code! Even facing the most common problems in code can be annoying and take a lot of time.
Take for example a long method of 200 lines of code with a bug, it will be necessary to understand most of it to fix it, even if the final change is a pair of lines. Imagine you found a good solution, but it means changing a lot of code. Before starting writing you look for the tests and the worst-case scenario occurs, there are no tests! It can't be worse, changing a lot of untested code can lead to a lot of bugs. Now you need to write tests that meet the expected behavior and, before that, start fixing it. A lot less work if tests were already there.
As you can see, having quality code makes it easier for developers to keep working on it. The code is a live entity, it exists to mutate when necessary, so keep in mind you are writing not only for yourself but also for others.
Code clean basics
If you don't know Robert C. Martin (Uncle Bob) and his book Clean Code, then you must read it, it will become your bible of good practices for readable code. Below, I present two points that you must be aware of. The book covers many more aspects that will undoubtedly improve your coding.
Meaningful names
When naming, you should use pronounceable names that are descriptive and unambiguous. Avoid encodings in the names, it can seem obvious for a person but not for others, and you can even forget your encodings.
If the names can't give a clear hint of what is happening, developers will need to check for the surrounding logic to understand. We are developers, we know the logic but will be easier if the names give a proper guideline.
Another thing that makes a lot of sense when naming is to use verbs for actions and nouns for subjects. Translating this to code, in general, you will have that classes should use noun names and method verb names.
Look at this code and try to understand what is doing.
def func(x):
lst = []
for i in range(x):
if i % 2 == 0:
lst.append(i)
return lst
It's not impossible, but there is no hint of what is happening. Now look what happens with good naming.
def get_even_numbers(limit):
even_numbers = []
for number in range(limit):
if number % 2 == 0:
even_numbers.append(number)
return even_numbers
You don't even need to read all the code, just with the method name you can have a good understanding of expected functionality.
Keep it small and simple
When creating or updating a class or method, you must avoid finishing with a bloater code, a complex code that is difficult to follow. If this is your case, then refactor it, and break it into little logical and readable pieces of code.
Long methods are, in most cases, bad code. Often the code does too much, and mentally keeping a general idea of what is happening is time-wasting. The method should be doing one thing, if it needs to do more, it can delegate the work instead of complicating it by doing it all in the same place. The same idea goes for classes.
To delegate work, you can break long methods into a bunch of smaller ones. You will finish with groups of methods that are closely related to them, encapsulate each of those groups in its class and use it! Don't be afraid of creating classes, a lot of small, simple and readable classes are better than one that does everything.
Software design basics
In my own words, software design is about making the best decisions to create the solution for a problem. But maintaining the ability of minimum effort for changes, because the perfect solution does not exist.
Software design is a pre-step of software development, you need to get a general idea about how your solution will work before programming it. If you are not doing some massive application, you can start without thinking about it too much. The important part is to never forget to refactor when it starts looking like a spaghetti code.
Don't repeat yourself (DRY)
For getting closer to a good design, the very first step is following this rule, pretty simple, don't repeat the code. There is no need to get it to the extreme and try to make a method to iterate over every possible array or list (you will fail). When having the same logic for two different processes, then you should put it into another class and use it from there in the two places that required it.
Contrary to expected, this process can lead to more lines of code than before, which is fine! It happens because adding a new class can be verbose enough, even more, if you divide the logic into little methods. Don't worry about anything about that. It's better to have one source in the code doing something that the same is scattered everywhere.
SOLID principles
These principles are a very solid guide to writing good code, but they are not easy to reach. The acronym helps to memorize and keep in mind five important ideas that can help to create maintainable code. Don't worry if it isn't clear what they mean or how to do it possible, these are something that can take a long time to learn how to apply them.
Single responsibility principle. A class or method should have only one reason to change.
Open/closed principle. It should be able to extend the behavior of a class without changing the class itself.
Liskov substitution principle. A superclass could change with some of its subclasses without breaking the program.
Interface segregation principle. Many specific interfaces are better than one generic interface.
Dependency inversion principle. High-level classes or modules should depend on abstractions and not on concrete implementations.
Thinking in the basics, you should follow, at least, the single responsibility principle (SRP). Having a class doing too much can be hard to follow, if it needs a lot of operations to reach what it needs, then it should delegate some of that to another class. If possible, reorder the methods and put together in a new class those that have a common goal. Now the new class and the refactored one should be a lot easier to understand!
It's difficult to organize the code right out when creating it. If a class grows too much, it could stop following the single responsibility principle and needs some refactoring.
Let's see how to apply this basic principle. Look at the following class.
class UserManager:
def __init__(self, username, password):
self.username = username
self.password = password
def create_user(self, username, password):
# Code for creating a user in the database
pass
def delete_user(self, username):
# Code for deleting a user from the database
pass
def send_notification(self, username, message):
# Code for sending a notification to a user
pass
def change_password(self, username, new_password):
# Code for changing a user's password in the database
pass
The UserManager
class is responsible for multiple tasks such as user creation, deletion, notification, and password management. It violates the SRP by having multiple reasons to change and performing too many operations.
Let's refactor the code to adhere to the SRP and improve code organization
class User:
def __init__(self, username, password):
self.username = username
self.password = password
class UserDB:
def create_user(self, user):
# Code for creating a user in the database
pass
def delete_user(self, user):
# Code for deleting a user from the database
pass
def change_password(self, user, new_password):
# Code for changing a user's password in the database
pass
class NotificationManager:
def send_notification(self, user, message):
# Code for sending a notification to a user
pass
In the refactored code, we have separated the concerns into three classes:
User
represents a user object with its attributes.UserDB
is responsible for handling database-related operations such as creating, deleting, and changing user information.NotificationManager
focuses on sending notifications to users.
By breaking down the responsibilities into separate classes, we adhere to the SRP, ensuring that each class has a single reason to change. This also improves code organization and readability.
Testing basics
First time I was doing tests, the least required coverage looks unnecessary, 80% of coverage. Where I work, they call for 90% of coverage; not too pleasant the first time doing it. But in a long way, it's surprising how it makes things work better.
When you have tested code, you don't worry too much about creating bugs, if one change breaks something else, you have a florescent red warning about it. Nowadays, I appreciate having tests. With tests, I don't need to know every detail about everything around the code that changes, it is enough to get the general idea.
Unit tests
The most simple type of testing is unit tests, these provide an expected behavior for the tested class/method. Having the behavior of a class written helps to understand what is expected to do, and prevent that functionality to break. The idea is to create unit tests for the smallest testable parts of software, the methods, and via testing every method of a class, then the class itself.
There are lots of test types and unit testing doesn't cover all. For example, they can't test how well performed the application under big stress. But, you should do it, especially if you expect to launch a service or webpage.
There are different methodologies for unit testing, one of them is test-driven development (TDD). In this case, you write a test battery for your class before writing the class itself. The tests should represent the expected functionality, if you run the tests, all should fail, after that, start the real development.
If you are not used to writing tests, this can seem pretty difficult. It's even harder if it's a new project, where everything is new and changing. But, after all, it's a method, not a rule. If you can't start with the tests for any reason, then finished with them! The crucial aspect is that you have completed a test battery that guards the functionality of your code.
Let's see the TDD methodology in action! Imagine we need to create a shopping cart for an e-commerce web application. The expected functionalities are to add and remove items from the cart and have the total value for the items in it. So, we start writing the tests.
import unittest
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
def test_add_item(self):
item = Item("Apple", 1.0)
self.cart.add_item(item)
self.assertEqual(len(self.cart.items), 1)
def test_remove_item(self):
item = Item("Apple", 1.0)
self.cart.add_item(item)
self.cart.remove_item(item)
self.assertEqual(len(self.cart.items), 0)
def test_calculate_total(self):
item1 = Item("Apple", 1.0)
item2 = Item("Banana", 2.0)
item3 = Item("Orange", 0.5)
self.cart.add_item(item1)
self.cart.add_item(item2)
self.cart.add_item(item3)
total = self.cart.calculate_total()
self.assertEqual(total, 3.5)
if __name__ == '__main__':
unittest.main()
If we run the tests will all fail because the shopping cart class is not yet implemented. Now we create the class and get the tests to pass.
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def remove_item(self, item):
if item in self.items:
self.items.remove(item)
def calculate_total(self):
total = 0
for item in self.items:
total += item.price
return total
class Item:
def __init__(self, name, price):
self.name = name
self.price = price
This process ensures that the functionality of the shopping cart is tested thoroughly and that any changes made in the future can be validated against the existing tests.
Conclusion
Creating quality code is hard, and it appears to be slower and inefficient at first sight. Remember, code is there to change, if little changes get hard, trying to add a new feature can be a real nightmare!
From this brief guide, I tried to cover the basic aspects to reach a good code. Always remember, little methods and classes are better than gigantic ones. Try to don't repeat your code, reuse it! Make that each class and method have only one responsibility. And add tests for, at least, the core functionality.
Never stop looking for improvement around your code. Follow this simple advice, and it will be a lot more friendly and readable, your partners and your future self will be happy.
Happy code!
Subscribe to my newsletter
Read articles from Nicolás Freudenberg directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nicolás Freudenberg
Nicolás Freudenberg
Hi there! I'm a software developer from Chile. I'm 26 years old, and I love to code, but not any code, just a huge fan of clean, readable and reusable code. Also, very interested in artificial intelligence, with knowledge on deep neural networks and different types of trainings methods.