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

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

Photo by A R on Unsplash

🧶 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