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.
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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
