Skip to main content

14 - Best Practices and Clean Code

Best Practices e Clean Code in Java Scrivere codice pulito significa creare software leggibile, manutenibile e professionale.

What do you think about this article?

At a Glance

Best Practices e Clean Code in Java Scrivere codice pulito significa creare software leggibile, manutenibile e professionale.

Best Practices and Clean Code in Java

Writing clean code means creating readable, maintainable, and professional software. SOLID principles and best practices guide toward robust and scalable design.

What You'll Learn

  • SOLID principles
  • Naming conventions
  • Code smells and refactoring
  • Proper error handling
  • Effective documentation

SOLID Principles

S - Single Responsibility Principle
// WRONG: class with too many responsibilities
class StudentManager {
    void saveStudent(Student s) { /* save to DB */ }
    void sendEmail(Student s) { /* send email */ }
    String generateReport(Student s) { /* generate report */ }
    void validateStudent(Student s) { /* validate data */ }
}

// CORRECT: one responsibility per class
class StudentRepository {
    void save(Student s) { /* save to DB */ }
    Student find(Long id) { /* search in DB */ }
}

class EmailService {
    void send(String to, String message) { /* send */ }
}

class StudentReportGenerator {
    String generate(Student s) { /* generate report */ }
}

class StudentValidator {
    boolean isValid(Student s) { /* validate */ }
}
O - Open/Closed Principle
// WRONG: modification needed for new types
class PaymentCalculator {
    double calculate(String type, double amount) {
        if (type.equals("CARD")) {
            return amount * 0.98;  // 2% fee
        } else if (type.equals("PAYPAL")) {
            return amount * 0.97;  // 3% fee
        }
        // Add new else-if for each type...
        return amount;
    }
}

// CORRECT: open for extension, closed for modification
interface PaymentMethod {
    double calculateFinalAmount(double amount);
}

class CardPayment implements PaymentMethod {
    public double calculateFinalAmount(double amount) {
        return amount * 0.98;
    }
}

class PayPalPayment implements PaymentMethod {
    public double calculateFinalAmount(double amount) {
        return amount * 0.97;
    }
}

// New method: just add a class
class CryptoPayment implements PaymentMethod {
    public double calculateFinalAmount(double amount) {
        return amount * 0.99;
    }
}
L - Liskov Substitution Principle
// WRONG: subclass violates the contract
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    public void setWidth(int w) {
        width = w;
        height = w;  // Violates expected behavior!
    }
}

// CORRECT: composition instead of problematic inheritance
interface Shape {
    int getArea();
}

class RectangleCorrect implements Shape {
    private final int width;
    private final int height;

    RectangleCorrect(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() { return width * height; }
}

class SquareCorrect implements Shape {
    private final int side;

    SquareCorrect(int side) {
        this.side = side;
    }

    public int getArea() { return side * side; }
}
I - Interface Segregation Principle
// WRONG: interface too large
interface Worker {
    void work();
    void eat();
    void sleep();
}

class Robot implements Worker {
    public void work() { /* ok */ }
    public void eat() { /* not applicable! */ }
    public void sleep() { /* not applicable! */ }
}

// CORRECT: segregated interfaces
interface Workable {
    void work();
}

interface LivingBeing {
    void eat();
    void sleep();
}

class Human implements Workable, LivingBeing {
    public void work() { /* implement */ }
    public void eat() { /* implement */ }
    public void sleep() { /* implement */ }
}

class RobotCorrect implements Workable {
    public void work() { /* implement only what's needed */ }
}
D - Dependency Inversion Principle
// WRONG: depends on concrete implementation
class NotificationService {
    private EmailSender emailSender = new EmailSender();

    void notify(String message) {
        emailSender.send(message);  // Tight coupling!
    }
}

// CORRECT: depends on abstraction
interface NotificationSender {
    void send(String message);
}

class EmailSenderImpl implements NotificationSender {
    public void send(String message) { /* email */ }
}

class SmsSender implements NotificationSender {
    public void send(String message) { /* sms */ }
}

class NotificationServiceCorrect {
    private final NotificationSender sender;

    // Dependency Injection
    NotificationServiceCorrect(NotificationSender sender) {
        this.sender = sender;
    }

    void notify(String message) {
        sender.send(message);
    }
}

Naming Conventions

Meaningful Names
// WRONG: unclear names
int d;  // what does it represent?
String s;
List<String> list;
void proc(int x);

// CORRECT: descriptive names
int daysUntilExpiration;
String studentName;
List<String> activeEmails;
void calculateGradeAverage(int studentId);

// Java conventions
class ClassName {}              // PascalCase
interface Calculable {}          // PascalCase
void methodName() {}              // camelCase
int localVariable;               // camelCase
static final int CONSTANT = 10;  // UPPER_SNAKE_CASE

// Booleans: prefix is/has/can
boolean isActive;
boolean hasPermission;
boolean canModify;

// Collections: plural
List<Student> students;
Map<String, Integer> gradesBySubject;

// Methods: verb + noun
void saveStudent(Student s);
Student findStudentById(Long id);
boolean isStudentActive(Student s);
List<Student> getActiveStudents();

Code Smells and Refactoring

Code Smell Examples
// CODE SMELL: Method too long
void processOrder(Order o) {
    // 50+ lines of code...
}

// REFACTORING: Extract methods
void processOrder(Order o) {
    validateOrder(o);
    calculateTotal(o);
    applyDiscounts(o);
    saveOrder(o);
    sendConfirmation(o);
}

// CODE SMELL: Magic numbers
if (age > 18 && score > 75) {
    // ...
}

// REFACTORING: Named constants
private static final int MINIMUM_AGE = 18;
private static final int PASSING_SCORE = 75;

if (age > MINIMUM_AGE && score > PASSING_SCORE) {
    // ...
}

// CODE SMELL: Boolean parameters
void createUser(String name, boolean admin, boolean active);

// REFACTORING: Builder or objects
class UserConfiguration {
    private boolean admin;
    private boolean active;
    // builder pattern...
}

// CODE SMELL: Comments explaining complex code
// Calculate weighted average considering credits
double r = 0;
int t = 0;
for (int i = 0; i < v.length; i++) {
    r += v[i] * c[i];
    t += c[i];
}
return r / t;

// REFACTORING: Self-documenting code
double calculateWeightedAverage(int[] grades, int[] credits) {
    double weightedSum = 0;
    int totalCredits = 0;

    for (int i = 0; i < grades.length; i++) {
        weightedSum += grades[i] * credits[i];
        totalCredits += credits[i];
    }

    return weightedSum / totalCredits;
}

Error Handling

Exception Best Practices
// WRONG: generic catch
try {
    // operation
} catch (Exception e) {
    // too generic handling
}

// CORRECT: specific catches
try {
    // operation
} catch (FileNotFoundException e) {
    // handle file not found
} catch (IOException e) {
    // handle other I/O errors
}

// WRONG: ignored exception
try {
    riskyOperation();
} catch (Exception e) {
    // silent - NEVER do this!
}

// CORRECT: at least log
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Error during operation", e);
    throw new ServiceException("Operation failed", e);
}

// Meaningful custom exceptions
class StudentNotFoundException extends RuntimeException {
    private final Long studentId;

    StudentNotFoundException(Long id) {
        super("Student not found with ID: " + id);
        this.studentId = id;
    }

    public Long getStudentId() {
        return studentId;
    }
}

// Use Optional instead of null
Optional<Student> findStudent(Long id);

// Caller
Student s = findStudent(id)
    .orElseThrow(() -> new StudentNotFoundException(id));

General Best Practices

Golden Rules

  • DRY: Don't Repeat Yourself
  • KISS: Keep It Simple, Stupid
  • YAGNI: You Aren't Gonna Need It
  • Fail Fast: Validate input early
  • Immutability: Prefer immutable objects
  • Composition > Inheritance: More flexible
  • Program to interfaces: Not implementations
  • Single Level of Abstraction: One level per method
Clean Code Example
// Well-structured class
public class StudentService {

    private final StudentRepository repository;
    private final EmailService emailService;
    private final StudentValidator validator;

    public StudentService(StudentRepository repository,
                          EmailService emailService,
                          StudentValidator validator) {
        this.repository = Objects.requireNonNull(repository);
        this.emailService = Objects.requireNonNull(emailService);
        this.validator = Objects.requireNonNull(validator);
    }

    public Student register(RegistrationRequest request) {
        validator.validate(request);

        Student student = createStudent(request);
        Student saved = repository.save(student);

        sendWelcomeEmail(saved);

        return saved;
    }

    private Student createStudent(RegistrationRequest request) {
        return Student.builder()
            .name(request.getName())
            .email(request.getEmail())
            .registrationDate(LocalDate.now())
            .build();
    }

    private void sendWelcomeEmail(Student student) {
        emailService.send(
            student.getEmail(),
            "Welcome " + student.getName()
        );
    }
}

Frequently asked questions

What are the main goals of writing clean code in Java?

The main goals of writing clean code in Java are to create readable, maintainable, and professional software.

What principles guide toward a robust and scalable design in Java programming?

SOLID principles guide toward a robust and scalable design in Java programming.

What is the purpose of following naming conventions in Java coding best practices?

The purpose of following naming conventions in Java coding best practices is to improve code readability.

What are some common issues that can be addressed through refactoring in Java code?

Code Smells and Refactoring can address common issues such as improving maintainability, reducing complexity, and enhancing overall code quality.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects