From God Objects to Graceful Code
Vitaliy Matiyash
You've seen a single class with > 3,000 lines of code?
You fixed one bug only to immediately break two others?
You've had to debug "Magic Numbers" (e.g., status == 7)?
You've seen an empty catch (Exception e) {} block?
"An antipattern is just like a pattern, except that instead of a solution, it gives something that looks superficially like a solution but isnโt one."
- Andrew Koenig
Common Java Anti-Patterns
"Happens when a single object or class tries to do too much, resulting in tight coupling and decreased maintainability."
"A 3,000+ line class violating the Single Responsibility Principle by handling every concern in the system."
public class AppManager {
public void createUser(User u) {
if (u.getName() == null) { ... } // Validation
db.execute("INSERT..."); // SQL Persistence
email.sendWelcome(u); // Notification
analytics.track("NEW_USER"); // Monitoring
if (isHoliday()) { ... } // Business Rules
}
// ... 200 unrelated methods follow ...
}
"Code where flow is tangled, logic is nested, and everything happens in one place."
public void processOrder(Order order) {
if (order != null) {
if (order.getItems() != null) {
if (order.getItems().size() > 0) {
// Deeply nested logic
for (Item item : order.getItems()) {
if (check(item)) { ... }
}
}
}
}
}
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
System.out.println("User:");
String u = s.nextLine();
// Logic mixed with I/O
if (u.equals("admin")) {
System.out.println("Pass:");
String p = s.nextLine();
// Hardcoded auth
if (p.equals("1234")) {
System.out.println("New User:");
String nu = s.nextLine();
// Nested update logic
if (!nu.isEmpty()) {
System.out.println("Updated");
}
}
}
}
"The lava flow anti-pattern occurs when code that is no longer needed is left in the codebase."
Like hardened lava, it solidifies because developers are afraid to delete it.
// @Deprecated
// public static void doOldThing() {
// // Is this used? Who knows.
// // Don't touch it, it might break prod.
// legacySystem.connect();
// }
public void activeMethod() {
// int temp = 0; // Leftover debug code
process();
}
"Bad programming practice in which numerical values are used in the source code without being properly named."
if (status == 7) {
// What is 7?
run();
}
"Embedding values or configuration into the programโs source code rather than storing it in a separate configuration file or database."
String dbUrl =
"jdbc:mysql://localhost:3306/db";
// Requires recompilation to change
"Occurs when source code is copied and pasted from one location to another instead of reused through abstraction."
public void exportPdf() {
connect();
formatData(); // IDENTICAL
saveFile(); // IDENTICAL
}
public void exportCsv() {
connect();
formatData(); // IDENTICAL
saveFile(); // IDENTICAL
}
Update bug in one, forget the other.
"Occurs when a solution to a problem is unnecessarily reinvented rather than using an existing solution or building on existing work."
// Why write 50 lines to parse JSON?
public class MyJsonParser {
public Object parse(String s) {
// Buggy, slow implementation
}
}
Just use Jackson or Gson.
"Optimizing before you have a profile is just guessing."
// Caching a list of 5 items
// "Just in case it grows"
if (complexCache.has(id)) {
return complexCache.get(id);
}
Cost of maintenance > Cost of re-fetching.
List<String> menu = List.of("Burger", "Fries");
// Thread fork/join overhead
// is 100x slower than the operation
menu.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
"Using a Ferrari to cross the street."
"Premature optimization is the root of all evil."
โ Donald Knuth
"Occurs when a solution to a problem is unnecessarily complex."
Using Microservices + Kubernetes + Kafka...
โฌ๏ธ
...to build a To-Do List app for 1 user.
Swallowing Exceptions
try {
dangerousOperation();
} catch (Exception e) {
// TODO: Fix this later
// e.printStackTrace(); // Commented out so logs are clean
}
The result? Silent failures in production that are impossible to debug.
The Antidote to Anti-Patterns
"The name of a variable should answer all the big questions."
// Good luck grepping for 't'
// in a million lines of code.
double t = 500.00;
// What is 7?
if (account.status == 7) ...
"Single-letter names and numeric constants are hard to locate across a body of text."
// Unique, searchable name
double transactionAmount = 500.00;
// Named constants are easy to find
int MAX_BLOCK_SIZE = 7;
if (account.status == MAX_BLOCK_SIZE)...
"Say what you mean. Mean what you say."
// Cultural Reference / Slang
public void whack() { ... }
public void eatMyShorts() { ... }
// "Member" Encodings (Unnecessary)
private String m_name;
// Clarity over Humor
public void save() { ... }
public void abort() { ... }
// Clean State
private String name;
"The first rule of functions is that they should be small."
"The second rule of functions is that they should be smaller than that."
- Robert C. Martin
Functions should do One Thing. They should do it well.
Extracting Abstractions
public static String testableHtml(
PageData pageData,
boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer buffer = new StringBuffer();
if (pageData.hasAttribute("Test")) {
if (includeSuiteSetup) {
WikiPage suiteSetup =
PageCrawlerImpl.getInheritedPage(
SuiteResponder.SUITE_SETUP_NAME, wikiPage);
if (suiteSetup != null) {
WikiPagePath pagePath =
suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName = PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName).append("\n");
}
}
// ... 50 more lines of append logic ...
}
return buffer.toString();
}
public static String renderPage(
PageData pageData,
boolean isSuite) throws Exception {
if (isTestPage(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite);
}
return pageData.getHtml();
}
Why is this better?
1. Fits on one screen.
2. Describes what it does, not how.
3. One level of abstraction.
"A long descriptive name is better than a short enigmatic name."
"Don't be afraid to make a name long... A long descriptive name is better than a long descriptive comment."
"The ideal number of arguments for a function is zero (niladic)."
includeSetupPage()fileExists("MyFile")makeCircle(double x, double y, double radius)makeCircle(Point center, double radius)"Side effects are lies. Your function promises to do one thing, but it also does other hidden things."
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
if (cypher.verify(password, user)) {
// SIDE EFFECT!
// Function name says "Check", code says "Initialize"
Session.initialize();
return true;
}
}
return false;
}
Temporal Coupling: This function can only be called when it is safe to initialize the session. If called elsewhere, you lose data.
"Error handling is 'One Thing'."
public void delete(Page page) {
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name);
} catch (Exception e) {
logger.log(e.getMessage());
}
}
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page p) {
deletePage(p);
registry.deleteReference(p.name);
configKeys.deleteKey(p.name);
}
"Code is the only source of truth. Don't explain it, express it."
// Check if we should proceed with deployment
// (No deploys on Friday after 4 PM)
if (date.getDay() == 5 &&
time.getHour() > 16) {
throw new DeploymentException();
}
if (!isDeploymentAllowed()) {
abortDeployment();
}
"Don't use a comment when you can use a function or a variable."
/*
* Changes:
* 11-Oct-2021: Fixed bug (Jim)
* 05-Nov-2021: Added feature (Pam)
*/
VERDICT: DELETE
"Source control (Git) does this better. These just clutter the file."
/** The Default Constructor */
public Account() {}
/** The day of the month */
private int dayOfMonth;
// See RFC-2045 Section 4.2.1...
VERDICT: DELETE
"Restating the obvious or dumping historical encyclopedias into code is just noise."
} // end while
} // end if
} // end method
// Added by Rick
VERDICT: REFACTOR
"If you need to mark the end of a function, your function is too big. Shorten it."
// InputStreamResponse response =
// new InputStreamResponse();
// response = (InputStreamResponse)
// ...
VERDICT: DELETE IMMEDIATELY
"Others will be afraid to delete it because they think it's important. It rots. Kill it."
// TODO: Fix this efficiency issue
// by 2018...
VERDICT: TOLERATE (BUT SCAN)
"TODOs are not an excuse to leave bad code. Scan them regularly or they become invisible."
Vertical Formatting & Density
The F.I.R.S.T. Rule
Fast: Tests should run quickly.
Independent: Should not depend on each other.
Repeatable: Run in any environment (QA, Prod, Laptop).
Self-Validating: Boolean output (Pass/Fail).
Timely: Written just before the production code (TDD).
Violating the abstraction barrier
public void testGetPageHierarchyAsXml() throws Exception {
// 1. Details of PathParser and Crawler exposed
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
// 2. Hard to see the "Act" step amidst the setup
request.setResource("root");
request.addInput("type", "xml");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
String xml = response.getContent();
// 3. Assertions mixed with low-level logic
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
}
Build-Operate-Check Pattern
public void testGetPageHierarchyAsXml() throws Exception {
// BUILD: Create the test data using a helper
makePages("PageOne", "PageOne.ChildOne");
// OPERATE: Perform the action
submitRequest("root", "type:xml");
// CHECK: Assert using Domain Specific Language
assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>",
"<name>ChildOne</name>"
);
}
If you have to explain "why" in a comment or chat, refactor the code instead.
Does this class have only one reason to change? If not, split it.
Always leave the file cleaner than you found it (rename one variable, fix one magic number).
Anti-patterns at quick glance
"Elegant code isn't just about adhering to patterns.
It's about empathy for the next developer who has to read it.
(Even if that developer is you, six months from now)."
Let's build better software.
Scan to connect or use the links below.
Please take 30 seconds to rate this session.
Or visit:
https://sfeedback.com/sk4if3Grab the deck and code examples.
Or visit:
https://bit.ly/java-anti-patterns