Fundamentals 44 min read

Why Refactoring Matters: Eliminate Code Smells with SOLID Principles and Design Patterns

This article explains the importance of refactoring, identifies common code smells, introduces SOLID principles and design patterns, and provides practical techniques—including method extraction, guard clauses, polymorphism, and composition—to improve code readability, maintainability, and testability.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
Why Refactoring Matters: Eliminate Code Smells with SOLID Principles and Design Patterns

About Refactoring

Why Refactor

During continuous project evolution, code accumulates and becomes chaotic if no one takes responsibility for its quality, eventually making maintenance more expensive than rewriting the whole system.

Typical causes include:

Lack of effective design before coding

Cost‑driven, feature‑stacking development

Missing code‑quality supervision mechanisms

The industry solution is continuous refactoring to remove "bad smells".

What Is Refactoring

Martin Fowler defines refactoring as:

Refactoring (noun): a change to the internal structure of software to improve understandability without changing observable behavior. Refactoring (verb): applying a series of techniques to adjust structure without altering observable behavior.

Refactoring can be large‑scale (system, module, architecture) or small‑scale (class, method, variable). Large‑scale refactoring involves design principles, patterns, and architecture changes and carries higher risk; small‑scale refactoring focuses on coding conventions, duplicate removal, and is performed during feature development, bug fixing, or code review.

Code Smells

Common smells include duplicated code, long methods, large classes, scattered logic, excessive coupling, data clumps, inappropriate inheritance, excessive conditionals, long parameter lists, many temporary variables, confusing temporary fields, pure data classes, poor naming, and over‑commenting.

Problems of Bad Code

Hard to reuse due to excessive coupling

Hard to change because a single change ripples throughout the system

Difficult to understand because of messy naming and structure

Hard to test because of many branches and dependencies

What Is Good Code

Quality is judged by readability, maintainability, flexibility, elegance, and simplicity, with maintainability, readability, and extensibility being the most important.

Writing high‑quality code requires solid object‑oriented design, design principles, patterns, coding standards, and refactoring techniques.

How to Refactor

SOLID Principles

Single Responsibility Principle

A class should have only one reason to change.

It improves cohesion and reduces coupling, but over‑splitting can hurt cohesion.

Open‑Closed Principle

Extend behavior by adding new modules, classes, or methods rather than modifying existing code.

It encourages minimal changes for new features.

Liskov Substitution Principle

Subtypes must be usable wherever their base type is expected without altering program correctness.

Overridden methods must preserve the contract of the base method.

Interface Segregation Principle

Clients should not depend on interfaces they do not use; keep interfaces small and focused.

Dependency Inversion Principle

High‑level modules should depend on abstractions, not concrete implementations.

Law of Demeter

Objects should know as little as possible about other objects.

Composition Over Inheritance

Prefer composition/aggregation to inheritance to avoid fragile base‑class problems.

Combining the above principles leads to high‑cohesion, low‑coupling designs.

Design Patterns

Design patterns are reusable solutions to common software design problems.

Creational : solve object creation, e.g., Singleton, Factory.

Structural : decouple functionality via composition, e.g., Adapter, Bridge, Facade.

Behavioral : manage object interaction, e.g., Observer, Strategy, Template Method.

Code Layering

Module Structure Explanation

server_main: configuration layer (Maven, resources)

server_application: entry layer (RPC, messaging, scheduled tasks) – no business logic

server_biz: core business layer (use cases, domain entities)

server_irepository: resource interface layer

server_repository: resource proxy layer – isolates changes, focuses on data access

server_common: utilities, VO, etc.

Follow layer conventions and respect dependencies.

Naming Conventions

A good name must accurately describe its purpose and follow common conventions.

General Rules

Project name: lowercase, hyphen‑separated (e.g., spring-cloud)

Package name: lowercase (e.g., com.alibaba.fastjson)

Class/Interface: PascalCase (e.g., ParserConfig)

Variable: camelCase (e.g., userName)

Constant: UPPER_SNAKE_CASE (e.g., CACHE_EXPIRED_TIME)

Method: camelCase verb phrase (e.g., getById)

Class Naming

Abstract classes: start with Abstract or Base (e.g., BaseUserService)

Enums: suffix Enum (e.g., GenderEnum)

Utility classes: suffix Utils (e.g., StringUtils)

Exception classes: suffix Exception (e.g., RuntimeException)

Implementation classes: InterfaceNameImpl (e.g., UserServiceImpl)

Design‑pattern related: Builder, Factory, etc.

Method Naming

Use lower‑camelCase, start with a verb or predicate (e.g., isValid, ensureCapacity, calculate).

Refactoring Techniques

Extract Method

When methods are long, duplicate, or mix abstraction levels, extract them into smaller, focused methods.

Intent‑Driven Programming

Separate what needs to be done from how it is done.

Decompose a problem into functional steps.

Organize steps first, then implement each method.

/**
 * 1. Transaction info starts as an ASCII string.
 * 2. Convert to token array.
 * 3. Normalize tokens.
 * 4. Large transactions (>150 tokens) use a different algorithm.
 * 5. Return true on success, false on failure.
 */
public class Transaction {
    public Boolean commit(String command) {
        Boolean result = true;
        String[] tokens = tokenize(command);
        normalizeTokens(tokens);
        if (isALargeTransaction(tokens)) {
            result = processLargeTransaction(tokens);
        } else {
            result = processSmallTransaction(tokens);
        }
        return result;
    }
}

Replace Function with Function Object

Encapsulate related variables into an object and split the large function into smaller methods within that object.

Introduce Parameter Object

When a method has many parameters, wrap them into a single object.

Remove Parameter Assignment

public int discount(int inputVal, int quantity, int yearToDate) {
    if (inputVal > 50) inputVal -= 2;
    if (quantity > 100) inputVal -= 1;
    if (yearToDate > 10000) inputVal -= 4;
    return inputVal;
}

public int discount(int inputVal, int quantity, int yearToDate) {
    int result = inputVal;
    if (inputVal > 50) result -= 2;
    if (quantity > 100) result -= 1;
    if (yearToDate > 10000) result -= 4;
    return result;
}

Separate Queries from Modifications

Methods that return values should not have side effects.

Avoid writing in conversion methods.

Cache query results when appropriate.

Remove Unnecessary Temporaries

Eliminate variables used only once or whose computation is cheap.

Introduce Explaining Variables

if ((platform.toUpperCase().indexOf("MAC") > -1) &&
    (browser.toUpperCase().indexOf("IE") > -1) &&
    wasInitialized() && resize > 0) {
    // do something
}

final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
    // do something
}

Guard Clauses Instead of Nested Conditionals

public void getHello(int type) {
    if (type == 1) return;
    if (type == 2) return;
    if (type == 3) return;
    setHello();
}

Replace Conditionals with Polymorphism

Encapsulate each branch in a subclass and make the original method abstract.

public interface Operation { int apply(int a, int b); }
public class Addition implements Operation { public int apply(int a, int b) { return a + b; } }
public class OperatorFactory {
    private static final Map<String, Operation> operationMap = new HashMap<>();
    static { operationMap.put("add", new Addition()); /* ... */ }
    public static Operation getOperation(String op) { return operationMap.get(op); }
}
public int calculate(int a, int b, String operator) {
    Operation op = OperatorFactory.getOperation(operator);
    if (op == null) throw new IllegalArgumentException("Invalid Operator");
    return op.apply(a, b);
}

Use Exceptions Instead of Error Codes

public void withdraw(int amount) {
    if (amount > balance) throw new IllegalArgumentException("amount too large");
    balance -= amount;
}

Introduce Assertions

Assert conditions that must always be true; do not use for regular validation.

Introduce Null Object or Special Object

public class OperatorFactory {
    private static final Map<String, Operation> operationMap = new HashMap<>();
    static { operationMap.put("add", new Addition()); /* ... */ }
    public static Optional<Operation> getOperation(String op) {
        return Optional.ofNullable(operationMap.get(op));
    }
}
public int calculate(int a, int b, String operator) {
    Operation op = OperatorFactory.getOperation(operator)
        .orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
    return op.apply(a, b);
}

Extract Class

public class Person {
    private String name;
    private String officeAreaCode;
    private String officeNumber;
    // getters/setters
}

public class TelephoneNumber {
    private String areaCode;
    private String number;
    public String getTelephoneNumber() { return "(" + areaCode + ")" + number; }
    // getters/setters
}

Prefer Composition Over Inheritance

Use a private field that references an existing class instead of subclassing it.

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    // delegate all Set methods to s
}

public class InstrumentedHashSet<E> extends ForwardingSet<E> {
    private int addCount = 0;
    public InstrumentedHashSet(Set<E> s) { super(s); }
    @Override public boolean add(E e) { addCount++; return super.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); }
    public int getAddCount() { return addCount; }
}

Inheritance vs Composition Decision

Use inheritance only when a true "is‑a" relationship exists.

Prefer composition for most internal relationships.

Interface Over Abstract Class

Interfaces allow multiple inheritance and are easier to evolve with default methods, while abstract classes are limited by single inheritance.

Prefer Generics

Generics provide compile‑time type safety and avoid unchecked casts.

public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
    T max = x;
    if (y.compareTo(max) > 0) max = y;
    if (z.compareTo(max) > 0) max = z;
    return max;
}

Static Nested Classes Over Non‑Static

Use static nested classes when the nested class does not need access to the outer instance.

Use Template/Utility Classes

Encapsulate common scenarios into reusable templates to reduce duplication.

Separate Object Creation from Use

Inject dependencies via constructors, factories, or DI frameworks instead of creating them inline.

public class BusinessObject {
    private final Service service;
    public BusinessObject(Service service) { this.service = service; }
    public void actionMethod() { service.doService(); }
}

Minimize Accessibility

Prefer private or package‑private visibility; expose only what is necessary.

Minimize Mutability

Make classes immutable when possible: private final fields, no setters, final class, defensive copies.

Quality Assurance

Test‑Driven Development (TDD)

TDD places tests at the center of development: write a failing test, write just enough code to pass, then refactor.

The goal is clean, working code backed by an automated test suite.

TDD Cycle

Add test → run all tests (fail) → write code to pass → run all tests (pass) → refactor.

Basic Principles

Write only enough code to make the failing test pass.

Refactor to eliminate duplication before writing the next test.

Layered Testing

Test Type

Goal

Verification

DAO

Validate MyBatis config, mapper, handler

In‑memory DB, assert

Adapter

Validate external interactions and converters

Depends on environment, manual verification

Repository

Validate internal calculations and conversion logic

Mock external deps, assert

Biz Layer

Validate business logic

Isolate external deps, multiple scenario tests, assert

Application Layer

Validate entry‑parameter handling and internal flow

Isolate external deps, parameter‑driven scenarios, debugging, no detailed logic verification

References

Refactoring – Improving the Design of Existing Code

Design Patterns

Effective Java

Agile Software Development Best Practices

Implementation Patterns

Test‑Driven Development

Source: https://juejin.cn/post/6954378167947624484

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design Patternscode qualityrefactoringTDDSOLID
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

0 followers
Reader feedback

How this landed with the community

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.