Untangling Java, Part 4 — When Your Code Finally Feels Grown-Up
🧱 Untangling Java, Part 4 — When Your Code Finally Feels Grown-Up

🧶 Quick Flashback
Remember that chaotic login program from Part 1?
The one where everything lived in main() and the if-statements multiplied like rabbits?
By Part 3, we’d finally grown up a little.
We built a User class that could hold its own data, validate itself, and update its own username.
Now it’s time to see the real payoff:
Why structured code doesn’t just look cleaner — it saves your brain when testing and expanding your app.
⚙️ Step 1: Meet Our User (Again)
Here’s the hero from last time:
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public boolean isValid() {
return username.equals("admin") && password.equals("1234");
}
public String getUsername() {
return username;
}
public void updateUsername(String newUsername) {
if (!newUsername.isEmpty()) {
this.username = newUsername;
}
}
}
This already felt cleaner — our user could think for itself.
But right now, it only recognizes one sacred login: “admin” / “1234”.
That’s like a bouncer who only lets their best friend into the club.
Let’s fix that.
🧠 Step 2: Make It Reusable
We’ll tweak isValid() so each User validates against its own password instead of a hard-coded combo:
public boolean isValid(String inputPassword) {
return password.equals(inputPassword);
}
That one change quietly makes our code testable.
Each User can now prove whether they belong — no global hacks required.
💼 Step 3: Hire a Manager
Adding multiple users in main() quickly turns into chaos.
Time to bring in a LoginManager — a tiny class whose only job is to manage users.
import java.util.HashMap;
import java.util.Map;
public class LoginManager {
private Map<String, User> users = new HashMap<>();
// Hire (add) a new user
public void addUser(User user) {
users.put(user.getUsername(), user);
}
// Check credentials
public boolean authenticate(String username, String password) {
User user = users.get(username);
return user != null && user.isValid(password);
}
}
See what happened?
We didn’t rewrite validation logic — we reused it.
That’s separation of concerns in plain English: the manager manages; the user behaves.
🧪 Step 4: Test Without Crying
Early Java testing usually looks like this:
Run main(), type things, pray, repeat.
However, now that our User stands alone, we can test it directly.
public class UserTest {
public static void main(String[] args) {
User admin = new User("admin", "1234");
System.out.println(admin.isValid("1234")); // true
System.out.println(admin.isValid("wrong")); // false
}
}
If this prints true then false, congrats —
you’ve just written your first unit test (and kept your sanity).
🧾 Step 5: A Peek at Professional Testing (JUnit)
Eventually, you’ll meet JUnit, the grown-up way to test Java code.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class UserTest {
@Test
public void testValidPassword() {
User user = new User("admin", "1234");
assertTrue(user.isValid("1234"));
}
@Test
public void testInvalidPassword() {
User user = new User("admin", "1234");
assertFalse(user.isValid("wrong"));
}
}
You don’t need to memorize JUnit today.
Just remember this: clean classes invite good tests.
Messy code avoids them like sunlight.
🧱 Step 6: Add Features Without Fear
Let’s give our users a small upgrade — record when they last logged in.
In old procedural code, this would mean rewriting multiple functions.
Now, we just add a field and a method inside User.
import java.time.LocalDateTime;
public class User {
private String username;
private String password;
private LocalDateTime lastLogin;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public boolean isValid(String inputPassword) {
return password.equals(inputPassword);
}
public void updateLoginTime() {
this.lastLogin = LocalDateTime.now();
}
public LocalDateTime getLastLogin() {
return lastLogin;
}
}
Then, whenever a login succeeds:
user.updateLoginTime();
No drama, no spaghetti, no regressions.
That’s the quiet power of encapsulation.
👑 Step 7: A Tiny Taste of Inheritance
Now imagine admins who can reset other users’ passwords.
We could jam that logic into User, but then everyone becomes an admin — oops.
Instead, we extend User to create AdminUser:
public class AdminUser extends User {
public AdminUser(String username, String password) {
super(username, password);
}
public void resetUserPassword(User targetUser, String newPassword) {
targetUser.setPassword(newPassword);
}
}
Could we have done this inside the original class? Sure.
But inheritance lets us grant privileges without chaos.
Each class does what it’s meant to — no identity crises.
🧠 Why This Actually Matters
We started with a single class and ended up with a mini ecosystem:
- User — knows who it is and what it can do
- LoginManager — coordinates multiple users
- AdminUser — extends power responsibly
Every piece is testable on its own.
You can change one without breaking the rest.
That’s not just good code.
That’s peace of mind.
🧵 Wrapping Up: Your Code Just Grew Up
You’ve gone from “everything in main()” to code that behaves like a team:
each class with clear roles, clean boundaries, and mutual respect.
Add new features? Easy.
Write tests? Straightforward.
Sleep at night? Finally possible.
Clean code isn’t about perfection — it’s about confidence.
You can change things knowing what will (and won’t) break.
Next up in Part 5:
👉 Inheritance vs Composition — the great OOP debate.
We’ll learn when to extend, when to contain, and how to keep your Java clean as it grows.
🧱 Untangling Java, Part 4 — When Your Code Finally Feels Grown-Up was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More

