How to Write Clean and Maintainable Code: Naming, Classes, Functions, and Testing
This article explains why clean code is essential for team development and provides practical guidelines on naming, class design, function composition, and testing—including SOLID principles, meaningful identifiers, single‑responsibility classes, short functions, and test‑driven development—to improve readability, extensibility, and overall software quality.
After many years of work, I increasingly realize that code cleanliness is crucial, especially in team development; writing elegant, tidy code makes colleagues more willing to cooperate.
Below, we will make our code clean through four chapters: naming, classes, functions, and testing.
1. Why keep code clean?
When code becomes messy over time, productivity drops, leading to problems such as:
Code that is hard to extend or that causes other issues when extended
Program crashes
Overtime work
Increased company costs (hiring) and even possible bankruptcy
1.1 Keep it clean from the start
Therefore, write clean code from the beginning; if messy code appears, refactor it immediately. Never adopt a "fix it later" mindset.
later equal neverThink of it this way: how many "later" or "fix later" items have you postponed?
If something must be done, do it early!
1.2 How to define clean code?
A clean code should have:
High readability – code should read like prose, conveying intent at a glance
No duplicated code
Compliance with design‑pattern principles, i.e., the SOLID rules:
2. Naming
Good naming improves readability, lets people understand code instantly, reduces comprehension cost, and cuts overtime.
2.1 Bad naming examples
Names with no meaning:
public interface Animal {
void abc();
}Calling abc() gives no clue about its purpose.
Meaningful naming:
public interface Animal {
void cry();
}Now the method name clearly indicates its function.
Inconsistent naming – the same behavior is called query in one place and find in another:
public interface StudentRepository extends JpaRepository
{
Student findOneById(@Param("id") String id);
List
queryAllStudent();
}After standardisation:
public interface StudentRepository extends JpaRepository
{
Student findOneById(@Param("id") String id);
List
findAll();
}2.2 Redundant naming – avoid unnecessary words such as "Variable" in a variable name or "Table" in a table name:
// Single‑object method prefix
getXxx();
// Multiple‑object method prefix
listXxx();3. Classes
A clean class should satisfy:
Single Responsibility
Open/Closed
High Cohesion
3.1 Single Responsibility
Classes should be small and have only one reason to change. Benefits include reduced complexity, better readability, easier maintenance, and lower risk of changes.
Determine if a class is too large by checking whether its name can accurately describe its whole responsibility. If not, the class likely does too much.
Example of a class with multiple responsibilities:
public abstract class Sql {
// SQL operation
public abstract void insert();
// Statistics operation
public abstract void countInsert();
}Refactor by extracting the statistics part into a separate class:
public abstract class CountSql {
public abstract void countInsert();
}3.2 Open/Closed Principle
Software should be open for extension but closed for modification. Adding new logic should not require changing existing code.
Non‑compliant example:
public abstract class Sql {
public abstract void insert();
public abstract void update();
public abstract void delete();
}When a new query operation is needed, the class must be modified, violating the principle.
Refactored version using separate subclasses:
public abstract class Sql {
public abstract void generate();
}
public class CreateSql extends Sql {
@Override
public void generate() { /* ... */ }
}
public class UpdateSql extends Sql {
@Override
public void generate() { /* ... */ }
}3.3 Cohesion
High cohesion means that a class’s methods operate on a common set of variables, forming a logical whole. If cohesion is low, split the class into smaller ones.
4. Functions
To keep functions clean, ensure:
Do one thing only
Have meaningful names
Use clean parameters
Pay attention to return values
4.1 Do one thing
Functions should be short and focused on a single responsibility.
Example of a long function (≈135 lines) that mixes validation, compression, and result handling:
public String upload() {
// validate image – 80 lines
// compress image – 50 lines
// return status – 5 lines
return "0";
}Refactored version delegating each step to its own method:
public String upload() {
check();
compress();
return "0";
}4.2 Function naming
Names should be self‑descriptive. Prefer longer, clear names over cryptic ones.
Bad example:
public String addCharacter(String originString, char ch);It is unclear whether the character is added at the beginning, end, or a specific position.
Better examples:
// Append to the end
public String appendCharacter(String originString, char ch);
// Insert at a specific position
public String insertCharacter(String originString, char ch, int insertPosition);4.3 Parameters
Fewer parameters are better. If more than three are needed, consider encapsulating them into a value object.
public List
findStudent(int age, String name, String country, int gender);
// Refactored
public List
findStudent(Student criteria);Avoid Boolean flag parameters because they split a method into two responsibilities. Split into two separate methods instead.
// Flag parameter
render(Boolean isSuite);
// Refactored
renderForSuite();
renderForSingleTest();Do not use output parameters. They increase cognitive load. Prefer returning a result directly.
public void findStudent() {
Student student = new Student();
doSomething(student);
return student;
}
// Refactored
student.doSomething();4.4 Return values
Separate commands from queries. A method that adds an element should not also return a success flag.
// Bad – mixes command and query
public Boolean addElement(Element element);
// Refactored
public void addElement(Element element);
public Boolean isAdd(Element element);Prefer throwing exceptions instead of returning error codes, which reduces nesting and clarifies failure paths.
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
// ...
throw new DeviceShutDownError("Invalid handle for:" + id.toString());
}4.5 How to write such functions?
No one writes perfect code at first. Write functional code, then refactor immediately—"later equal never".
4.6 Code quality scanning tools
Tools like SonarLint help discover issues and provide remediation suggestions, covering problems such as date‑type misuse, duplicated code, potential null‑pointer exceptions, and deep nesting. With static analysis, metrics like bug rate and duplication ratio become measurable.
5. Testing
Testing is essential to verify that code works correctly, and test code itself should be clean.
5.1 TDD (Test‑Driven Development)
TDD stands for Test‑Driven Development, a core agile practice that drives design by writing failing tests before production code.
Advantages: at any development point you have a runnable product with minimal bugs.
Disadvantages: test code can be twice or more the size of production code, but it saves debugging time.
5.2 FIRST principle
Guidelines for good unit tests:
Fast – tests run quickly
Independent – tests do not depend on each other
Repeatable – tests can run anywhere without external dependencies
Self‑validating – tests assert automatically without manual inspection
Timely – tests are written before the corresponding production code
5.3 Test code pattern
Use the given‑when‑then pattern to improve readability.
Given – create mock data
When – execute the code under test
Then – verify the outcome
Example:
@Test
public void shouldReturnItemNameInUpperCase() {
// Given
Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
when(itemRepository.findById("it1")).thenReturn(mockedItem);
// When
String result = itemService.getItemNameUpperCase("it1");
// Then
verify(itemRepository, times(1)).findById("it1");
assertThat(result, is("ITEM 1"));
}5.4 Automatic test generation
Two IDEA plugins can generate unit tests automatically:
SquareTest (paid)
TestMe (free)
6. Conclusion
Writing clean code improves readability and makes the codebase easier to extend.
Source: cnblogs.com/liuboren/p/17017421.html
Backend‑focused technical group
Build a high‑quality technical community; developers, recruiters, and anyone interested in sharing job referrals are welcome to join and help each other improve.
Speak civilly, focusing on technical exchange, job referrals, and industry discussion.
Advertisements are not allowed; do not trust private messages to avoid scams.
Join the group by adding me as a friend.
Click the "❤" below to support us—thank you!
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.