rafael_del nero
Java Developer

How to write reusable Java code

how-to
23 Nov 202313 mins
JavaSoftware Development

Maximizing code reuse in your Java programs means writing code that is easy to read, understand, and maintain. Here are eight ways to get started.

How to write reusable code
Credit: PopTika/Shutterstock

Writing reusable code is a vital skill for every software developer, and every engineer must know how to maximize code reuse. Nowadays, developers often use the excuse that there is no need to bother with writing high-quality code because microservices are inherently small and efficient. However, even microservices can grow quite large, and the time required to read and understand the code will soon be 10 times more than when it was first written.

Solving bugs or adding new features takes considerably more work when your code is not well-written from the start. In extreme cases, I’ve seen teams throw away the whole application and start fresh with new code. Not only is time wasted when this happens, but developers are blamed and may lose their jobs.

This article introduces eight well-tested guidelines for writing reusable code in Java.

8 guidelines for writing reusable Java code

  1. Define the rules for your code
  2. Document your APIs
  3. Follow standard code naming conventions
  4. Write cohesive classes and methods
  5. Decouple your classes
  6. Keep it SOLID
  7. Use design patterns where applicable
  8. Don’t reinvent the wheel

Define the rules for your code

The first step to writing reusable code is to define code standards with your team. Otherwise, the code will get messy very quickly. Meaningless discussions about code implementation will also often happen if there is no alignment with the team. You will also want to determine a basic code design for the problems you want the software to solve.

Once you have the standards and code design, it’s time to define the guidelines for your code. Code guidelines determine the rules for your code:

  • Code naming
  • Class and method line quantity
  • Exception handling
  • Package structure
  • Programming language and version
  • Frameworks, tools, and libraries
  • Code testing standards
  • Code layers (controller, service, repository, domain, etc.)

Once you agree on the rules for your code, you can hold the whole team accountable for reviewing it and ensuring the code is well-written and reusable. If there is no team agreement, there is no way the code will be written to a healthy and reusable standard.

Document your APIs

When creating services and exposing them as an API, you need to document the API so that it is easy for a new developer to understand and use.

APIs are very commonly used with the microservices architecture. As a result, other teams that don’t know much about your project must be able to read your API documentation and understand it. If the API is not well documented, code repetition is more likely. The new developers will probably create another API method, duplicating the existing one.

So, documenting your API is very important. At the same time, overusing documentation in code doesn’t bring much value. Only document the code that is valuable in your API. For example, explain the business operations in the API, parameters, return objects, and so on.

Follow standard code naming conventions

Simple and descriptive code names are much preferred to mysterious acronyms. When I see an acronym in an unfamiliar codebase, I usually don’t know what it means.

So, instead of using the acronym Ctr, write Customer. It’s clear and meaningful. Ctr could be an acronym for contract, control, customer—it could mean so many things!

Also, use your programming language naming conventions. For Java, for example, there is the JavaBeans naming convention. It’s simple, and every Java developer should understand it. Here’s how to name classes, methods, variables, and packages in Java:

  • Classes, PascalCase: CustomerContract
  • Methods and variables, camelCase: customerContract
  • Packages, all lowercase: service

Write cohesive classes and methods

Cohesive code does one thing very well. Although writing cohesive classes and methods is a simple concept, even experienced developers don’t follow it very well. As a result, they create ultra-responsible classes, meaning classes that do too many things. These are sometimes also known as god classes.

To make your code cohesive, you must know how to break it down so that each class and method does one thing well. If you create a method called saveCustomer, you want this method to have one action: to save a customer. It should not also update and delete customers.

Likewise, if we have a class named CustomerService, it should only have features that belong to the customer. If we have a method in the CustomerService class that performs operations with the product domain, we should move the method to the ProductService class.

Rather than having a method that does product operations in the CustomerService class, we can use the ProductService in the CustomerService class and invoke whatever method we need from it.

To understand this concept better, let’s first look at an example of a class that is not cohesive:


public class CustomerPurchaseService {

    public void saveCustomerPurchase(CustomerPurchase customerPurchase) {
         // Does operations with customer
        registerProduct(customerPurchase.getProduct());
         // update customer
         // delete customer
    }

    private void registerProduct(Product product) {
         // Performs logic for product in the domain of the customer…
    }

}

Okay, so what are the issues with this class?

  • The saveCustomerPurchase method registers the product as well as updating and deleting the customer. This method does too many things.
  • The registerProduct method is difficult to find. Because of that, there is a good chance a developer will duplicate this method if something like it is needed.
  • The registerProduct method is in the wrong domain. CustomerPurchaseService shouldn’t be registering products.
  • The saveCustomerPurchase method invokes a private method instead of using an external class that performs product operations.

Now that we know what’s wrong with the code, we can rewrite it to make it cohesive. We will move the registerProduct method to its correct domain, ProductService. That makes the code much easier to search and reuse. Also, this method won’t be stuck inside the CustomerPurchaseService:


public class CustomerPurchaseService {

    private ProductService productService;

    public CustomerPurchaseService(ProductService productService) {
      this.productService = productService;
    }

    public void saveCustomerPurchase(CustomerPurchase customerPurchase) {
         // Does operations with customer
        productService.registerProduct(customerPurchase.getProduct());
    }

}

public class ProductService {

   public void registerProduct(Product product) {
         // Performs logic for product in the domain of the customer…
    }
}

Here, we’ve made the saveCustomerPurchase do just one thing: save the customer purchase, nothing else. We also delegated the responsibility to registerProduct to the ProductService class, which makes both classes more cohesive. Now, the classes and their methods do what we expect.

Decouple your classes

Highly coupled code is code that has too many dependencies, making the code more difficult to maintain. The more dependencies (number of classes defined) a class has, the more highly coupled it is.

The best way to approach code reuse is to make systems and code as minimally dependent on each other as possible. A certain coupling level will always exist because services and code must communicate. The key is to make those services as independent as possible.

Here’s an example of a highly coupled class:


public class CustomerOrderService {

  private ProductService productService;
  private OrderService orderService;
  private CustomerPaymentRepository customerPaymentRepository;
  private CustomerDiscountRepository customerDiscountRepository;
  private CustomerContractRepository customerContractRepository;
  private CustomerOrderRepository customerOrderRepository;
  private CustomerGiftCardRepository customerGiftCardRepository;

  // Other methods…
}

Notice that the CustomerService is highly coupled with many other service classes. Having so many dependencies means the class requires many lines of code. That makes the code difficult to test and hard to maintain.

A better approach is to separate this class into services with fewer dependencies. Let’s decrease the coupling by breaking the CustomerService class into separate services:


public class CustomerOrderService {

  private OrderService orderService;
  private CustomerPaymentService customerPaymentService;
  private CustomerDiscountService customerDiscountService;

  // Omitted other methods…
}

public class CustomerPaymentService {

  private ProductService productService;
  private CustomerPaymentRepository customerPaymentRepository;
  private CustomerContractRepository customerContractRepository;
  
  // Omitted other methods…
}

public class CustomerDiscountService {
  private CustomerDiscountRepository customerDiscountRepository;
  private CustomerGiftCardRepository customerGiftCardRepository;

  // Omitted other methods…
}

After refactoring, CustomerService and other classes are much easier to unit test, and they are also easier to maintain. The more specialized and concise the class is, the easier it is to implement new features. If there are bugs, they’ll be easier to fix.

Keep it SOLID

SOLID is an acronym that represents five design principles in object-oriented programming (OOP). These principles aim to make software systems more maintainable, flexible, and easily understood. Here’s a brief explanation of each principle:

  • Single Responsibility Principle (SRP): A class should have a single responsibility or purpose and encapsulate that responsibility. This principle promotes high cohesion and helps in keeping classes focused and manageable.
  • Open-Closed Principle (OCP): Software entities (classes, modules, methods, etc.) should be open for extension but closed for modification. You should design your code to allow you to add new functionalities or behaviors without modifying existing code, reducing the impact of changes and promoting code reuse.
  • Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, any instance of a base class should be substitutable with any instance of its derived classes, ensuring that the program’s behavior remains consistent.
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. This principle advises breaking down large interfaces into smaller and more specific ones so that clients only need to depend on the relevant interfaces. This promotes loose coupling and avoids unnecessary dependencies.
  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. This principle encourages using abstractions (interfaces or abstract classes) to decouple high-level modules from low-level implementation details. It promotes the idea that classes should depend on abstractions rather than concrete implementations, making the system more flexible and facilitating easier testing and maintenance.

By following these SOLID principles, developers can create more modular, maintainable, and extensible code. These principles help achieve code that is easier to understand, test, and modify, leading to more robust and adaptable software systems.

Use design patterns where applicable

Design patterns were created by experienced developers who have gone through many coding situations. When used correctly, design patterns help with code reuse.

Understanding design patterns also improves your ability to read and understand code—even code from the JDK is clearer when you can see the underlying design pattern.

Even though design patterns are powerful, no design pattern is a silver bullet; we still must be very careful about using them. For example, it’s a mistake to use a design pattern just because we know it. Using a design pattern in the wrong situation makes the code more complex and difficult to maintain. However, applying a design pattern for the proper use case makes the code more flexible for extension.

Here’s a quick summary of design patterns in object-oriented programming:

Creational patterns

  • Singleton: Ensures a class has only one instance and provides global access to it.
  • Factory method: Defines an interface for creating objects, but lets subclasses decide which class to instantiate.
  • Abstract factory: Provides an interface for creating families of related or dependent objects.
  • Builder: Separates the construction of complex objects from their representation.
  • Prototype: Creates new objects by cloning existing ones.

Structural patterns

  • Adapter: Converts the interface of a class into another interface that clients expect.
  • Decorator: Dynamically adds behavior to an object.
  • Proxy: Provides a surrogate or placeholder for another object to control access to it.
  • Composite: Treats a group of objects as a single object.
  • Bridge: Decouples an abstraction from its implementation.

Behavioral patterns

  • Observer: Defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.
  • Strategy: Encapsulates related algorithms and allows them to be selected at runtime.
  • Template method: Defines the skeleton of an algorithm in a base class, allowing subclasses to provide specific implementation details.
  • Command: Encapsulates a request as an object, allowing clients to be parameterized with different requests.
  • State: Allows an object to alter its behavior when its internal state changes.
  • Iterator: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
  • Chain of responsibility: Allows an object to pass a request along a chain of potential handlers until the request is handled.
  • Mediator: Defines an object that encapsulates how a set of objects interact, promoting loose coupling between them.
  • Visitor: Separates an algorithm from the objects it operates on by moving the algorithm into separate visitor objects.

There is no need to learn every design pattern by heart. Realizing that the patterns exist and having a sense of what each one does is enough. You will be able to choose the right design pattern for your programming scenario.

Don’t reinvent the wheel

Many companies still adopt the standard of using internal frameworks without a solid reason. Unless the company is a big tech company such as Google or Microsoft, there is no point in creating internal frameworks in most cases. A small or medium company can’t compete with open source or big tech companies to develop a better solution.

Instead of reinventing the wheel and creating unnecessary work, it’s better to simply use the available tools. That also helps developers in their careers because they won’t have to learn a framework that will never be used outside the company.

An example is the persistence framework, Hibernate, which is extremely well tested and commonly used. A company I worked for chose to use its own internal framework for persistence, even though it did not have all the features and robustness of Hibernate. Extending and maintaining the internal framework created extra work for no real benefit.

Therefore, I recommend using technologies and tools that are widely available and popular in the market. It’s impossible to develop a framework at the same level of quality as open source, where talented developers collaborate over many years to develop and improve the product. In many cases, big companies also support open-source projects, providing even more assurance that they work as they should.

Conclusion

Understanding and applying the main principles of code reusability is crucial for building efficient and maintainable software systems. By embracing abstraction, encapsulation, separation of concerns, standardization, and documentation, developers can create reusable components that save time, reduce redundancy, and improve code quality.

Design patterns play a significant role in code reusability, providing proven solutions to common design problems. Cohesion and low coupling ensure that components are self-contained and minimally dependent, increasing their reusability. Adhering to the SOLID principles promotes modular and extensible code that can be easily integrated into different projects.

By writing reusable code, developers can unlock numerous benefits, including increased productivity, enhanced collaboration, and accelerated development cycles. Reusable code allows for faster project iterations, easier maintenance, and the ability to leverage existing solutions. Ultimately, the main principles of code reusability empower developers to build scalable, adaptable, and future-proof software systems.

rafael_del nero
Java Developer

Rafael del Nero is a Java Champion and Oracle Ace, creator of the Java Challengers initiative, and a quiz master in the Oracle Dev Gym. Rafael is the author of "Java Challengers" and "Golden Lessons." He believes there are many techniques involved in creating high-quality software that developers are often unaware of. His purpose is to help Java developers use better programming practices to code quality software for stress-free projects with fewer bugs.

More from this author

Exit mobile version