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. 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 Define the rules for your code Document your APIs Follow standard code naming conventions Write cohesive classes and methods Decouple your classes Keep it SOLID Use design patterns where applicable 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. Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos