Fundamentals 19 min read

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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
How to Write Clean and Maintainable Code: Naming, Classes, Functions, and Testing

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 never

Think 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!

unit testingsoftware designrefactoringclean codeNaming Conventions
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.