Styling Your Code: The conventional style guide for Clean Codes
I started programming by reading “Clean Code”, and I’ve almost always applied it in practice since then. By keeping my work clean, I’ve been able to maintain high readability and low maintenance overhead, and I dare say my colleagues have never failed to understand my code. I want to share my experiences and practices to help juniors and some seniors, especially in collaboration.
1. Don’t use values directly.
Instead of hard-coding values like seconds, minutes, and hours as numerical data, it’s better to define them as constants abstracted with meaning. For example, when you want to use 1 second in milliseconds, instead of directly using 1000, define it as a constant like this:
// original
Date d = new Date();
long t = d.getTime() / 1000;
// improved
final int SEC_MILLIS = 1000;
Date d = new Date();
long unixSec = d.getTime() / SEC_MILLIS;
This will help others understand the intention without needing to think why 1000 is being divided into the time.
2. Name functions or methods in verb forms.
Function or method names should be in the form of verbs. Functions fundamentally have input and output (excluding void return type). It means that they process input to produce output, signifying actions and hence should be named in verb forms. Here’s an example of a method formatting Date into a string:
public class DateFormatter {
public String format(Date d) {
…
}
}
However, there are exceptions based on conventions. Despite not being in verb form, there’s a common usage in the form of prepositions like “to…”. It’s primarily used when naming functions that convert values into different forms. Here’s another example of a method performing the same role:
public class DateFormatter {
public String toString(Date d) {
…
}
}
This is not so much a violation of the verb form rule as it is often used interchangeably with the same meaning, so it seems an abbreviation rather than breaking the rule.
3. Less than 3 arguments in functions.
It’s recommended to have three or fewer parameters or arguments in functions. If there are too many arguments, readability decreases as it becomes unclear what each argument’s role is, especially in languages where the order of arguments matters, significantly reducing a function’s scalability.
Especially in TypeScript, this becomes critical as it becomes difficult to use optional parameters. When there are multiple arguments, and some are optional parameters, not following the prescribed order will result in the following compilation error:
error TS1016: A required parameter cannot follow an optional parameter.
When there are four or more arguments, it’s preferable to use a configuration object. DTO is an excellent example of this.
// original
public class UserService {
void signup(String email, String uid, String phone, boolean consentsToMarket) {
…
}
}
// improved
public class SignupDto {
String email;
String uid;
String phone;
boolean consentsToMarket;
}
public class UserService {
void signup(SignupDto dto) {
}
}
4. A module should be responsible for one(SRP).
Modules like functions, methods, or objects should have only one responsibility. Functions and methods should perform only one action, and objects should have only one responsibility. It doesn’t mean simply performing a straightforward task like Math.add; it means having the narrowest and deepest level of abstraction. It’s about increasing cohesion and decreasing coupling.
These principles are called the Single Responsibility Principle (SRP), and the advantages of adhering to it versus not adhering to it are clear. Let’s look at an example of a REST API service:
public class UserService {
void signup(Signup dto) {}
void signin(Signin dto) {}
User findUserByEmail(String email) {}
String generateUuid() {}
void createUser(CreateUserDto dto) {}
}
In this service, there are roles such as signing up, checking for email duplicates, generating UUIDs, and creating user data. This UserService is dependent on at least three responsibilities. If signup and signin are responsibilities of UserService, createUser and findUserByEmail belong to the responsibility of accessing user data, and generateUuid belongs to the responsibility of UUID generation.
public class SessionManager {
String generateSessionId() {
UserService service = new UserService();
String sessionId = service.generateUuid();
…
}
}
Suppose, for example, in a class like SessionManager, you use the generateUuid method to create a session ID. Although it’s the same UUID, its purpose of generation is different. In other words, UserService is also responsible for generating UUIDs for session IDs. If you change the UUID version for generating unique user IDs from v4 to v1, the UUID version for session IDs also changes. This breaks cohesion and introduces coupling, violating the Single Responsibility Principle. Let’s improve the examples as follows:
public class UserService {
void signup(Signup dto) {
…
CommonModule module = new CommonModule();
String uid = module.generateUuid(1);
…
}
void signin(Signin dto) {}
}
public class UserRepository {
User findUserByEmail(String email) {}
void createUser(CreateUserDto dto) {}
}
public class SessionManager {
String generateSessionId() {
CommonModule module = new CommonModule();
String sessionId = module.generateUuid(4);
…
}
}
public class CommonModule {
String generateUuid(int version) {}
}
We separated data access objects within the service into a separate UserRepository class, and we separated the generateUuid method into a general module class while allowing the version to be specified as an argument to differentiate the purpose of usage. Using v1 for generating UID and v4 for generating session IDs doesn’t affect each other. The function only generates UUIDs based on the version. This way, the code becomes clearer.
public class CommonModule {
String generateUuidV1() {}
String generateUuidV4() {}
}
5. Don’t use boolean arguments in functions.
Avoid using boolean-type flag parameters in functions. Flags are used to branch the flow of logic, meaning the function does more than one thing. This violates the principle that “a function should do one thing.”
Instead, separate actions differentiated by flags into individual functions.
public class CommonModule {
// original
String generateUuid(boolean isForUid) {}
// better
String generateUid() {}
String generateSessionId() {}
}
6. Make your intentions clear.
Strive to express intentions and meanings as much as possible. Avoid using meaningless characters like ‘a’ and ‘b’ and refrain from using non-standard abbreviations. Even if the types are the same, prohibit reusing variables for different purposes. Consider the following example from the past experience of a senior programmer, which I tried to recreate from memory.
let _ret: any = do_something();
if (_ret === null) {
return null;
}
_ret = doAnother();
if (_ret === null) {
return null;
}
…
_ret.fromDoSomething.code … // Undefined Error!
First, let’s look at the variable _ret. It’s declared as any type using the let keyword. let likely signifies that this variable is mutable, and any type indicates that it can accept any type. It’s divided into three parts: in the first part, this variable is defined based on the return value of do_something function. After exception handling, it’s overwritten with the return value of doAnother in the second part. In the third part, it’s trying to access properties of the return value from the first function. And it results in an Undefined Error!
Of course, after the variable is overwritten by the return value of the second function, it’s trying to find the return value of the first function. This is a clear coding mistake. This wouldn’t have happened intentionally. It’s likely due to avoiding cumbersome variable names, which resulted in overlooking it. But you should never code like this. Code is written to be understood by computers, but ultimately, it’s written for humans. It should be understandable by colleagues and, more importantly, by yourself three months later. If you approach coding without a philosophy of expressing intentions, it becomes code that even your future self won’t recognize. If I were to take on that task, I would write it like this.
const resultFromDoSomething = doSomething();
if (resultFromDoSomething === null) {
throw new Error("Failed doSomething");
}
const resultFromDoAnother = doAnother();
if (resultFromDoAnother === null) {
throw new Error("Failed doAnother");
}
How is it? The intention is clearly expressed, and the code flows like a story rather than code.
7. Don’t explain codes with comments.
Avoid explaining code with comments. Contrary to the above principle, instead of explaining code with comments, put intentions into the code. The code itself should show intentions without the need for explanation. Let’s look at the following example.
int a = do(); // a is result of do
Though the code became shorter due to short variable names and function names, it actually became longer due to comments. If this repeats, it actually harms readability. Although not as extreme as the example above, there are cases where comments are written out of habit. It seems to be intended to add comments for readability where some parts are ambiguous. However, if you deliver intentions through variable naming, comments become unnecessary, but readability would be better.
I always strive to find ideal short names that deliver intentions for variable names, function names, and class names. But if unavoidable, I believe it’s better to be clear, even if it’s longer rather than using ambiguous intentions. Clear intention code makes collaboration smoother and maintenance easier.
8. Make it easy at first.
The most important part is to start simple and gradually improve the code. Trying to write perfect code from the start makes you miss the essence of implementation. The first priority of implementation is whether the function operates correctly as intended, the second is whether it adheres to security standards, and the last is the code style and readability. What you’re trying to implement and its details should be written first, then check if there are critical security issues, and then improve the code. When this becomes a habit, you’ll naturally apply it without needs considering priority.