Advent of Code is Here!
Every year everyone, who does, looks forward to their advent calendar so that they can eat chocolates but us developers have our own! Advent of Code is a yearly event where you can solve puzzles and learn new things.
Very early in my career, I read the Clean Code by Robert C. Martin, and I recently found some notes on key principles I took at the time. Now I still follow these principles to this day, but in a different way. This journey down memory lane (or the ’note’ lane) made me realise that it’s important to learn new things from knowledgeable authors or teachers, but it’s always better to make it yours if you want to be consistent and successful over time.
I used to be someone who struggled to not have polarized opinions on things (I still do, but it’s a lot better), and people like Juan M. Barroso helped me quite a bit. They taught me not to follow teachings as a dogma but to rather consider them as the North Star or overall principles. Otherwise, you would get frustrated because it’s impossible to reach “perfection”, but you should always try to work towards it.
In other words, I’ve come to the conclusion that thinking like a Mandalorian (“This is the way”, and you have to accept it) is actually not the way if you want to understand why things are, and it’s better to ‘own’ the principles you defend and convince others.
Anyway, enough digression on my learning journey! Let me introduce the Clean Code principles I follow, with ‘my’ touch:
Photo by Lucrezia Carnelos on Unsplash
- Low details functions requires long-descriptive-names (Private methods)
- High details functions don’t require long descriptive names (Public methods)
When you name your variables, functions, classes, etc. the type of access (public, private, exported unexported, etc.) is not important. What’s important is that the name is self-explanatory enough, so that anyone understands what it does.
For example, in an API endpoint method, you can call it create customer
and that might be enough, but down the line,
deep in the business code, you might have something to create a specific type of customer, because it depends on the
payload your API received. In this case, you may prefer a more descriptive, but equally good, name
like create subscription based customer
One of the important things I do now is to not stress too much at first. I would definitely try to choose a good name, but more often than not, my peers would help me improve it, like I would when I review their code.
A function should do one thing and one thing only, and it should do it well. i.e. Function with a try-catch should do only the error handling as error handling is one thing
I think this is a very hard and really not that great thing to do when you follow it to the letter. What matters in my opinion is to try your best to reduce the scope of your methods to the minimum. This way it’s easier to give it a good name, it’s easier to review, and probably easier to reuse.
Parameters aren’t assets they are liabilities.
- 0 is great
- 1 is OK
- 2 is not too bad
- 3 you should not have to come to this point so often
Functions should not use booleans, but rather two different functions one for the true and another for the false
Here, a lot is going on, but it all boils down to a similar thing, trying your best to reduce the scope of your methods
to the minimum. The number of arguments matters, but I think these days I see less and less code with many arguments.
Instead, people use objects to represent what the set of arguments means, improving the readability of the code. Also,
sometimes the programming language requires several parameters. “GoLang” is a good example of why counting the number
of parameters is not super relevant. In this language, it’s idiomatic to have at least a context.Context
as the first
argument, so that you can, for example, track a request and cancel it when needed.
# Example of what not to do.
def create_customer(customer, is_subscription_based: bool):
if is_subscription_based:
create_subscription_based_customer(customer)
else:
create_one_time_customer(customer)
In terms of the boolean as arguments, I must admit I really don’t like them, as it often means the method is doing many things, and it’s harder to read. Instead, I prefer something like below:
def create_customer(customer):
if is_subscription_based(customer):
create_subscription_based_customer(customer)
else:
create_one_time_customer(customer)
def create_subscription_based_customer(customer):
# save the customer on the database
# ensure subscription is created for such customer
def create_usage_based_customer(customer):
# save the customer on the database
# ensure the system captures the customer's usage.
def is_customer_subscription_based(customer) -> bool:
# calculate if the customer is subscription-based and return the result.
return customer.subscription_based
Although the example is small and silly, it shows how you can have a similar outcome without passing booleans around to direct the flow of the code.
- No switch statements! “A switch statement is an unused option for polymorphism” Of course, there has to be one point where you decide which class you instantiate, but it has to be in the “main section” and leave the “app section” with independent deployability
- When A depends on B there’s a runtime dependency that means that for A to run B has to be there to execute. But, there’s also a Source-code dependency (import, use, whatever) that means that for A to compile, B has to be there.
The solution? Using interfaces A depends on Interface and B derives from Interface, so we maintain the runtime flow, and we maintain the module B independent
It’s a very wordy principle but, in short, this means to favour
the Dependency Inversion Principle. Where a component
with more context tells components with less context (but more details) what to do. You can use frameworks
for Dependency Injection
, but it’s not really needed, and in some languages like GoLang
, it’s quite standard to not
use such frameworks.
You can get into the implementation details like user interfaces and polymorphism, but they are just the implementation details. Not all languages support interfaces i.e. Ruby or not all languages support polymorphism i.e. Go. But the principle is the same.
- Every class should return his exceptions not reusing check exceptions
- Stack class should return Stack.Overflow, Stack.Underflow, etc
- You may write messages but a message means that you failed in showing your intentions
You should try to avoid reusing error definitions from other libraries or components. Instead, try to write your own and make them part of your component’s API. This way, you have better documentation and ensure that you aren’t creating too abstract errors. The principle is quite simple, Ensure components have a clear API specifying not only their inputs but the errors too
In my opinion, if one component A
depends on another component B
. The caller of component A
shouldn’t need to
know that component B
exists. Any failure from component A
to integrate properly with component B
should
be A's
responsibility.
- Code standards, Best code standard document is the code itself
- You may write comments but a comment means that you failed in showing your intentions
- Wide length, YOU SHOULD NEVER HAVE TO SCROLL TO THE RIGHT TO SEE THE WHOLE LINE!!!
- Average wide length 40
- Write cohesive classes! methods that change as many private variables as possible
Honestly, I don’t follow this too much. I think that by trying your best to reduce the scope of your methods to the minimum you should achieve much of the above without really thinking about it. Your lines would be short, and you would probably write comments when you must, which is exactly the right amount of comments. Although remember that not everyone has a 34inches monitor 🤣
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
Test code has to be treated as good as production code. Even so, it has to be treated even with more love and care is the one that is verifying that everything is working as expected and is the actual documentation of your project
This is one of the biggest points people keep focussing on and where the most polarized views exist. In my opinion, the goal is to ensure you write tests, and they are covering your use cases, that’s all that matters. So whatever way you pick to achieve it, that’s a personal preference.
To keep it simple, the principle is to ensure we have test coverage for our features making sure our tests cover our use cases (if you do a breaking change on a feature, a test should fail).
Something else that I’d like to add is that, in my opinion, when you fix a bug you must add a test for it. In other words without a test the bug does not exist nor is fixed.
To summarise, the top principles you may want to consider when writing codes are:
My comments on the topics above may seem too simplistic but, in reality, there are a lot of nuances. Remember that these are like the North Star, not rules, that should guide you on how to improve your code.
As always I’d like to thank my wife Marie Dziubich for the help on every post, I write so that you all get an easier-to-read article!