Coding standards
I have been keeping an in-memory list for myself for a while now, containing the standards I apply to ensure quality code. Writing software requires craftsmanship. It is a trade where there is always something new to learn and where proven methods should be upheld. I wanted to write down the rules that I use successfully. It helps me to think about them again. Additionally, it may help others to deliver higher quality code.
I did not come up with everything on my own. I often learned from books and also learned a lot from colleagues. So, I don't want to give the impression that what follows is solely my achievement. My only merit is that I am sharing this summary with you.
Quality code, meaning readable, adaptable and bug-free code, preferably without Technical Debt, is valuable. Recent research shows that low-quality code produces 15x more bugs and takes up 42% of development time. The problem is that it's difficult for management to see what level of quality the code is at. Despite this, management should still be able to deal with setbacks and deliver software on time. Since it's difficult to see what level of quality the code is at, it's the responsibility of the software development team to proactively maintain high code quality. Often, corners are cut under time pressure, resulting in Technical Debt and decreased code quality. It's up to the development team to point out the impact of the choices that are made. However, few companies have a clear strategy for making wasted time visible outside the development team. Therefore, it's important for managers to work on that. What we as developers can do is to maintain high code quality.
Typing code is not where we as developers spend most of our time. Most of the time is spent understanding existing code. That's why readable code is extremely important. Developers spend about 60% of their time trying to understand existing code. Spaghetti code costs 40% more time.
In short, cutting corners and allowing Technical Debt will always result in higher development costs later on. The choice for quality code instead of cutting corners to meet an upcoming deadline is clear. Whatever choice you make, the effect of the choice will only be visible much later, but it will have a significant impact.
There are several reasons why it's important to write readable code:
- Maintainability: Readable code is easier to understand and maintain for both the original author and other developers who work with it later.
- Debugging: If code is readable, it's easier to detect and fix errors.
- Collaboration: If other developers can understand and work with your code, this can improve collaboration and efficiency.
- Scalability: Readable code can be more easily extended and adapted to changing requirements, keeping the code scalable.
- Sustainability: Readable code can be maintained for longer periods of time without requiring major changes.
Below is an overview of some aspects that should definitely be taken into account when practicing your craft. This is not an exhaustive list, as there are plenty of books written on the topic.
Naming
Prefer long clear names
Prefer a long clear member or variable name over an unclear short name. Short names always seem to be preferred, but long names have the disadvantage of making readability difficult. However, short names can be very meaningless. Find the balance between clarity and the length of the name.
Below is an example, not even a very short name. In this example, the name does not accurately describe the function. If the person is an employee, they are not removed. The name says nothing about that.
1class PersonScreen {
2 private deletePerson(id: number): void {
3 if (person.type === personType.employee) {
4 return;
5 }
6 personRepository.delete(id);
7 }
8}
A better name would be: deleteNonEmployee()
. But even better would be to optimize the function itself, as shown below:
1class PersonScreen {
2 private deleteNonEmployee(id: number): void {
3 if (person.type === personType.employee) {
4 throw new Error('It is not allowed to delete an employee');
5 }
6 personRepository.delete(id);
7 }
8}
Agree on patterns
Naming conventions can help make code more readable. It's important to agree on which convention to follow. For example:
- Functions always start with a verb:
show...
,validate...
- Events start with
on...
, such asonClick...
,onChange...
- If a function is asynchronous, add
Async
at the end, such asloadDataAsync()
(for TypeScript, C#)
Relateer naam aan de context
Relateer de namen aan je context. PersonRepository met een save()
functie, dan is het helder dat er een persoon wordt opgeslagen. De functie savePerson()
noemen is dus overbodig.
1class PersonRepository {
2 public save() {
3 ...
4 }
5}
In the example below, you will have to read the code of the save()
function to understand what is being saved. The name could be better.
1class MainScreen {
2 public save() {
3 ...
4 }
5}
To keep the names as short as possible, it is important that each class or function does one thing.
Comments
Why comments? Isn't the code readable without them? Think three times before adding comments. Comments can be useful to clarify the purpose of a class or function. However, once you start inserting comments regularly between lines of code, your code is likely unclear. Moreover, the disadvantage of this is that when changes are made to the code, the comments are sometimes forgotten, which makes poorly readable code even more confusing.
Take the following example:
1if (this.getAnswer(answer).trim().length < question.MinLength) {
2 // Als antwoordlengte kleiner dan verplichte minimum
3}
The code above is an example that I encountered in practice. You have to think about the line for a moment. The author of this piece also thought that and added comments. In this example, you read the comment, but you also have to check if it matches the if
clause above. Moreover, you still have to unravel the gist of the if
statement.
One solution is to incorporate comments into the code.
1const answerLength = this.getAnswer(answer).trim().length;
2const requiredAnswerLength = question.MinLength;
3const isAnswerTooShort = answerLength < requiredAnswerLength;
4if (isAnswerTooShort) {
5
6}
You see, it's more code, but still more readable. When reading the code, you can skip lines 1, 2, and 3 to understand what is happening. This reduces reading time because what is stated is clear.
Code organization
What follows is a standard that I have been using myself for a few years when setting up code for any type of application. It helps me with organizing and structuring the code properly. However, it can be very personal.
Amount of code per file
What I have noticed is that many software developers prefer to put a lot of code in a limited set of files rather than the other way around. I myself also unconsciously prefer to code in an existing file rather than just adding a new file. However, the latter helps me organize the code better:
What I do:
-
Each class in its own file.
-
Class name = file name
-
Rather more files than a lot of code per file.
-
This way of organizing helps prevent me from having to search longer for a class. In addition, it limits the size of the files, reducing the need for scrolling. However, some languages are not very suitable for this. Take Python, for example. A good IDE can help split things up into logical pieces.
Other readability enhancing measures
The following points mainly concern being explicit about certain things. Preferably explicit rather than implicit.
Type indication
In non-strong-typed languages such as Python and JavaScript, it is impossible to embed a type indication in your code. For example, the return value of a function or the type of a variable. Include this in inline comments. Most languages have good facilities for this.
The disadvantage of non-strong-typed languages is that if you omit all type indications, the emphasis will be on correct naming. And as mentioned, that is not always easy.
Handling statuses in your code. Think, for example, of the connectedStatus of an Android device: 'offline'
, 'wifi'
, 'mobileNetwork'
. Do not use strings to test or retrieve the status. Use enums
if possible or define the statuses as strings, but centrally.
Don't:
1if (status == 'offline') {
Do:
1public static STATUS_OFFLINE = 'offline';
2public static STATUS_WIFI = 'wifi';
3public static STATUS_MOBILE_NETWORK = 'mobileNetwork';
4
5...
6
7if (status == STATUS_OFFLINE) {
This way you prevent your software from failing due to a typo in a string. But again, use enums if the programming language provides them!
Accessibility
By accessibility, we mean the public
, private
, protected
designation of a variable, class, or function. Clearly indicating the accessibility of members is especially useful for programmers who are proficient in multiple languages. Depending on the language, the accessibility can differ if it is not indicated. For TypeScript, a member is always public
if it is not indicated. However, for C#, a member without designation is actually private
. Avoid confusion and be explicit.
Lambda functions
Lambda functies of arrow functies. These are anonymous functions that can be passed as a property value or parameter value of a method.
I often come across whole chunks of code in such anonymous functions. In short, a piece of code where nowhere, by a name, is indicated what it exactly does. That is obviously harmful to readability.
If you have such an expression with more than one line, wrap the code in a separate function with a clear name and call it in the expression.
2 examples of the above:
1var squaredNumbers = numbers.Select(x => x * x);
1<TouchableOpacity onPress={() => this.onClickFacebook()}>
2
3private onClickFacebook(): void {
4 ...
5}
Software organization
I do a number of things to keep the code well-organized at a higher level. My main goal is to prevent an application from becoming a big spaghetti mess.
Dependency injection is king
Whole books can be written about dependency injection. In short, it means injecting all the objects (instances of classes) that you need in your class into the class. The instantiation happens in a central place.
For example, if you have a logging function in your application and want to use it in the repository that retrieves people from the database, the Dependency class is the central place responsible for creating all dependencies. In this case, both the loggingService and the personRepository:
1class Dependencies {
2 private _loggingService: ILoggingService;
3 public loggingService(): ILoggingService {
4 if (_loggingService == null) {
5 _loggingService = new ConsoleLoggingService();
6 }
7 return _loggingService;
8 }
9
10 private _personRepository: IPersonRepository;
11 public personRepository(): IPersonRepository {
12 if (_personRepository == null) {
13 _personRepository = new PersonRepository(this.loggingService())
14 }
15 return _personRepository
16 }
17}
The dependencies here are constructed as singletons. With a singleton it is meant that if an instance does not exist, it is created. If it does exist, the instantiated version is used.
As you can see on line 13 above, the loggingService is injected in the constructor of the personRepository. The PersonRepository then looks like this:
1class PersonRepository {
2 private _loggingService: ILoggingService
3 public constructor(loggingService: ILoggingService) {
4 this._loggingService = loggingService;
5 }
6
7 ...
8 savePerson(person: Person) {
9 ...
10 this._loggingService.log('Person stored in DB');
11 }
12}
The constructor receives the loggingService as a parameter during instantiation. This is stored in _loggingService and can then be used to perform log actions.
In the first piece of code, the dependencies, you could see that the ConsoleLoggingService
implements the ILoggingService
interface. If, for example, we no longer want to log to the console but to a file, we can simply replace ConsoleLoggingService
with another implementation of ILoggingService
. If we want to build a unit test for the PersonRepository
, we can replace ConsoleLoggingService
with a dummy in the test. Thanks to this solution, we only test the code of the repository. If the repository ultimately has more dependencies, these can also be replaced during a test. Therefore, the use of mock libraries for tests seems like a quick fix for code that interconnects too directly. There are no plugs, only hard connections.
With Dependency Injection, the code is no longer spaghetti, but objects are neatly connected to each other without being directly dependent on each other. In other words, we have plugs and sockets instead of soldered wires. The Dependency
class is the patch panel.
Structure and Layers
After years of experience, I have found that the following structure works very well for me. In the past, every application was organized a little differently, depending on the situation. Do you recognize that? By using the structure below, there is a solid foundation. All my applications are organized the same way. And I have noticed that no matter what type of application it is, the structure always fits. And! If I am looking for certain logic, I can find it more quickly than ever.
- Functions of the application are grouped into:
- Business logic services & (Web) API services
- Converters
- Repositories
- Views
- Models and ViewModels
Business logic services & (Web) API services
All logic is included in services. If something needs to be translated, data needs to be retrieved from a web API, device data needs to be requested, a difficult calculation needs to be performed, there is a service for each function that performs one task. Each service is available in the dependencies and can be used by any other service.
I no longer use utility classes. My experience is that a forest of small classes is created in all sorts of corners of your application. That are also instantiated and used in all sorts of places. The service replaces that.
For each service, an interface is available. So replacing the logic is easy. If I first use the Google API to retrieve a static map, it is easy to replace it with a service that returns an OpenStreetMap map.
Converters
Converters ensure the conversion of one object to another object. The use of a multi-layered application has always been popular. In practice, however, you see that the separation of layers is not strict. Entities, data-carrying objects, are often used throughout all layers. This makes the independently intended layers strongly dependent on each other again. The layers communicate with the same messenger classes, which are used for both presentation and data storage. This data carrier becomes larger and larger because each layer has certain data needs. If an object in the data layer contains only a mortgage amount and interest, the presentation layer would like to include the results of calculations as well. And then we forget about property name changes for a particular layer. If you use a third-party service, the objects of that service suddenly become part of your application. All a bit problematic!
Converters provide the solution. The converter converts one object to another object. Especially when crossing a boundary in the application layers. If we extract a Person
object from the database, there is a converter that converts this Person
object to an object that can be used in the presentation layer, the PersonViewModel
. The converter allows a single object or a list of objects to be converted from one type to another and vica versa.
In the personViewModel, for example, we can now include a calculation for the person's age based on his birth date and the current date. The Person
object from the data layer is not loaded with this. If we load a person object to display on the screen, it is converted from Person
to PersonViewModel
. Want to save a PersonViewModel
, then the conversion goes the other way.
At first it seems like a lot of overhead. But once you start using this, you notice the difference. It's convenient. It shields one piece of logic from another. Replacing one or part of the application layers now becomes much easier.
Repositories
Repositories are responsible for retrieving and storing data in the data layer. A tried and true concept that you see a lot. A blueprint is available for each repository in the form of an interface.
Views
Views are part of the presentation layer. The screens of the application. These contain as little logic as possible. ViewModels are used here as data carriers.
Models and ViewModels
By Model and ViewModel classes we mean entity classes, data carriers, used in the application. Think of Person
, Customer
or Product
class. Models are part of the business and data layer. ViewModels are mainly used in the presentation layer of the app.
Models and ViewModels do NOT contain logic such as methods and functions. Once you start adding logic it does not do 1 thing but multiple things. So no save()
, delete()
, showScreen()
functions in this kind of classes.
Of course it is possible to add extra properties in which, for example, certain calculations are done that are strictly related to the model. Just keep in mind to keep these properties out of serialization.
Models are not suitable for Dependency Injection. You don't want to pull logic into a data carrier that way either.
A ViewModel and Model class have only 1 function: to represent Data.
Control at the gate
Assert, assert, assert. Every function should start with a verification of the input parameters. Doing no verification greatly increases the chances of bugs. Take the next example:
1class Calculator {
2 countTrays(bottles: number, traySize: number): number {
3 return bottles / traySize;
4 }
5}
Based on the number of bottles and size of crates, the function calculates how many crates are needed for a given number of bottles. The input of function is not checked. The first major problem that may arise is division by 0. In addition, we implicitly expect a positive number as the outcome of the function here. However, that is not enforced anywhere. You would expect an integer as the result, but this function does not necessarily return that. In short, this function seems OK thanks to some implicit assumptions. The trick is to make those precisely explicit.
1class Calculator {
2 countTrays(bottles: number, traySize: number): number {
3 if (bottles < 0) {
4 throw Error('Invalid number of bottles');
5 }
6 if (bottles == 0) {
7 return 0;
8 }
9 if (traySize <= 0) {
10 throw Error('Invalid tray size');
11 }
12
13 const totalTrays = bottles/traySize;
14 const roundUpTotalTrays = Math.ceiling(totalTrays);
15 return roundUpTotalTrays;
16 }
17}
In the code above, every input parameter is checked. Throw an Exception (Error) for any input you really don't expect. Especially don't go helping the user
of the function by giving a friendly
response. For example null
or something similar. This is because you run the risk of anticipating the rest of the code.
For the other input parameters, think about what the result of the function should be and return that immediately. So as soon as the input is checked and you know the result you return that. That's easy to read, the top part checks and cancels the function where necessary. The lower part contains the actual functionality.
Furthermore, the output is also normalized. If something goes wrong in the input, it is immediately clear in the rest of the application. Just as a plug has 2 poles and it is agreed that 230V comes out so you will have to enforce in your code what the rules are for the use of your code.
Force yourself to think about the implicit assumptions. You do that by going through each input variable and check what you expect and don't expect. Usually blacklisting is sufficient, all input values are OK except.... Whitelisting is rarely necessary, all input values are off except.... For output, ask yourself what the user expects as a response from the function. Limit the output accordingly.
Every part of the application does one thing.
Everything does one thing! This is a well-known but also a difficult topic. However, if you follow this rule, your code will be readable and understandable. How does one component only do one thing? They often do a lot of things. How can a function that calls other functions only do one thing? These are arguments that I still hear some programmers shout. Doing that one thing mainly has to do with the level at which the function or class functions. Often a service that is of one thing at a high level bundles a number of smaller "things" at a low level.
By the way, if each component does one thing, you automatically avoid duplicate code. Pieces, lines of code that you have in multiple places.
One of the signals that your function is doing multiple things is the if
else
construct. No, we have those for a reason. Still, if your function is doing one thing you usually only need the if
. The else
is the end of your function. An example in this Linked In Post.
One of the first steps with splitting your code can be by making sure your for
or while
loops contain only one line of code. The details go to a separate function. You call that function in your loop.
Another signal is a function with more than 20 lines of code. Keep functions short. 1 line is fine, 5 is OK, 20 is a lot. More than that? Think about it and reorganize your code.
One function does multiple things
The function below does 3 things instead of 1.
1public sendReceiptsToTenants(): void
2{
3 for (let tenant: Tenant of tenants) {
4 if (tenant.isPayDay()) {
5 const rent = tenant.calculateRentPrice();
6 tenant.sentRentalReceipt(rent);
7 }
8 }
9}
How to solve this?
1public sendReceiptsToTenants(): void
2{
3 for (let tenant: Tenant of tenants) {
4 this.sendRentalReceiptIfNecessary(tenant);
5 }
6}
7
8public sendRentalReceiptIfNecessary(tenant: Tenant): void {
9 if (tenant.isPayDay() == false) {
10 return
11 }
12 calculateAndSendRentalReceipt(tenant);
13}
14
15public calculateAndSendRentalReceipt(tenant: Tenant): void {
16 const rent = tenant.calculateRentPrice();
17 tenant.sentRentalReceipt(rent);
18}
Each function now does exactly one thing.
Split up
The code below is better broken down.
1class AppMain {
2 private onAppStateChanged(state: string) {
3 if (state == APP_STATE_FOREGROUND) {
4 this.loadSettings();
5 this.refreshUIData();
6 this.downloadUserInfoFromServer();
7 return;
8 }
9
10 this.saveSettings();
11 this.logout();
12 }
13}
Below, we limit the if function to 1 line. The real logic is in separate functions that also contain only a few lines.
1class AppMain {
2 private onAppStateChanged(state: string) {
3 if (state == APP_STATE_FOREGROUND) {
4 this.onAppToForeground();
5 return;
6 }
7 this.onAppToBackground()
8 }
9
10 private onAppToBackground(): void {
11 this.saveSettings();
12 this.logout();
13 }
14
15 private onAppToForeground(): void {
16 this.loadSettings();
17 this.refreshUIData();
18 this.downloadUserInfoFromServer();
19 }
20}
In a team
To work well together, it is important to agree on how to code. Create a consensus on the topics listed above. Agree with each other. If someone has a better idea, be open to it. Don't have an idea? Then comply with the agreements. Putting agreements in writing is OK, but don't make a book of them. Give developers time to grow into it. Above all, the agreements must be ingrained in practice. So code, code and more code, but not without reviews.
Code reviews
Review each other's code. Be critical of code illegibility and deviations from the standard. But do find the balance between your own opinion and the common good. Don't be afraid to be criticized. It may initially feel like you are not good at your job, but only if you are willing to learn will you get better. Disagree on the standard? Discussions are fine, but mandate one person to tie a knot.
Expand Application.
Add next component? Then first take 5 minutes to think about the right structure. Spaghetti is born that way. Classes that do more than what they are intended for. Functions that do 2 things instead of 1. Why do we tend to do that? It's faster and easier to modify existing code than to add a new structure. The barrier of creating a new file for code proves high in practice.
Why? I think the hassle around it: creating constructor, defining interface, giving dependencies a place etc, etc.... Sometimes it is just easier to insert a line of code. But in the long run, then refactoring or bugs can show up. Fight it. If you make something, make it right the first time. That's the fight we face every day.
Make it a drama? No, but if you don't think before you do something it can become a drama. It often happens slowly and very invisibly. We as developers still sometimes say, "We should build it from scratch with the knowledge of today." Often that is the result of a longer period of time, quick fixes and features. A patch panel without labels and all tangled cables. At the end, you can't do anything with it. Then you haven't done your job properly. A waste of all the productivity.
If you love your work then you want to be a professional right?
Conclusion
Is this now the holy grail? It's just a small piece of what could be better. These are the things that are the focus for me. It's a mix of best practices and my own experience. The best practices I didn't come up with myself. They are well established and supported by numerous books on the subject. I came across the application structuring once in an app at one of my clients. An "Eureka!" moment. Unfortunately, I do not know its developer. I would have loved to have a chat with him sometime.
You might be thinking, if I'm going to put all this into practice I'll have to write a lot more code. That's true, however, the reality is that writing code doesn't take the most time. Fixing bugs, as well as completely rewriting pieces of software takes up a lot of that time. That along with all the overhead such as consultations, stand-ups, reviews and the like that comes with it makes it better to do it right the first time. Moreover, experts have determined that "only" 1/6th of the time is spent writing code. If it takes you a little longer to develop a feature then that's unfortunate, but in the long run everyone will be happy.
Reading
Clean Code, Robert C Martin This book goes beyond this limited blog post. It contains numerous tips for taking your craftsmanship to the next level.