Heart of a Java application is classes and their methods. Designing classes and methods correctly is very important to design an application that satisfies quality attributes such as robustness, maintainability, modularity etc. While designing an application, we design so many methods. But what are thumb rules, best practices or guidelines of Java method design? As this is core of an application, we must consider all aspects of method design. In this post, let us discuss important practical aspects of method designing. It may not be the case that all of these are applicable to a method at a time. But it is good to consider these in design process.
Method Design Requirement
For our discussion, let us pick up a very simple requirement statement (from huge requirement document) – User saves customer.
How to Identify a Method?
Method is a behavior, which is identified, mostly by looking at verb in requirements. Actor does an operation by calling a method to achieve certain goal. Identify method that does something for an Actor. One can also have private methods that are doing something definite for another method.
If we look at our requirement above, we can see verb “save”. We can have “save” or “saveCustomer” method. Let us take “saveCustomer” method forward.
Method is a Behavior of Class
Method is performing certain operation, and is also behavior of a class. Hence it has to be in line with the behavior of class, inclusive in the class. Many times anything that is common is pushed to an abstract class of certain type or group of classes. But it is important to check if this method behavior is behavior of this group of classes? If not then the method needs to be pushed to appropriate class instead of abstract class, even a Helper or Utility class can be looked at.
Above save customer method can be included in a class that encapsulates all such customer operations. The class name can be CustomerBusiness, CustomerOperations, CustomerService etc. depending on layers identified in architecture.
What is Responsibility/Purpose of a Method?
Each method must have a definite purpose, clearly defined responsibility. Methods with distributed responsibility lead to coupling with other methods, resulting in maintenance challenges.
In our example, the saveCustomer() method of business tier does everything that is need in saving customer e.g. all business validations, generation of customer id, and getting customer saved in database by calling say a DAO class method. It may call private methods for each individual operation.
Exceptions in Method Signature
Mostly, in application architecture, there will be guidelines for the exceptions that will be thrown by methods in a layer. Follow these guidelines. Do not throw those exceptions which have been handled to certain extent inside method.
“saveCustomer” method can throw an application specific exception e.g. ServiceException which is wrapper on all exceptions originating during save operation. If database save operation results in any SQLException, it may not be propagated to the callers of “saveCustomer” method as is. The clients of this method may not be interested in knowing details of Sql statement execution failure.
Naming Convention for Methods
- Crisp and telling everything about the method’s responsibility
- Name in such a way that AOP point cut can be defined for certain group of methods. E.g. in service layer all methods starting with word “save” will require transaction, all methods starting with “search” will be Read Only.
We have already named saveCustomer() method using certain convention. If there are other services for Product, Order etc. and save methods of these entities are named following same convention as “saveProduct”, “saveOrder”, we can add a AOP point cut using this naming convention “save*”.
Each Method should do a Unit Operation
Each method must cater to a unit requirement than doing too many things. It makes design modular and subsequently maintainable.
“saveCustomer()” can involve many unit operations e.g. validation of customer data, creating customer id, saving customer in database etc. This wrapper method can call individual methods e.g. validateCustomer, generateCustomerId, and save method in CustomerDAO. Each method is doing a unit operation, and this wrapper on those is getting it done.
What should be Input Values for Method?
- Having bean wrapping related attributes as input makes addition of new attributes easy. New attributes can be added without having to change clients of this method.
- API methods can have very specific input parameters
Let us take a look at two of the possible signatures of the save customer method. If we want to add an additional parameter to the input, to cater requirement of one of the callers, in first signature, there will not be any change to other callers, while in case of second signature all clients will be impacted.
– saveCustomer(Customer customer)
– saveCustomer(String firstName, String lastName, …)
If we want to provide an API to check if the customer is active, the signature will be using individual parameter – boolean isCustomerActive(Long customerId)
Abstraction in Method Design
Many times it is possible to abstract common behavior of method to a parent class method, so that all children of the parent class can reuse it, extend from it. Good to look for such opportunities.
Inheritance in Method Design
Before designing a method, see if there is already complete or partial desired functionality implementation available in parent class. If yes, try to reuse it, if required through extension.
Follow open close principle while designing method as well. Method should be open for functional extension, but closed for modification. Can other classes, child classes, extend the method? If not, then mark it final. This is done when behavior of the method being implemented is not supposed to change.
Avoid Duplication of Logic/Code in Method Design
- Attempt should be to not let method to duplicating any logic
- If there is any logic that is getting repeatedly used inside same method, it can be reused by identifying a private method again following these guidelines.
Application of Design Patterns in Method Design
There are design patterns to address different design problems. E.g. coarse grain methods definitions are used for service layer methods implementing façade pattern. Similarly factory, template, etc. design patterns target method designing in different scenarios. Identify pattern that suites scenario in hand and implement it.
“saveCustomer(Customer customer) throws ServiceException” is following façade pattern. Customer object will contain customer basic information, address information, etc. All this is saved through a single call from clients, instead of finer calls to save each of the child entities separately. If clients are supposed to make expensive remote call to the service class, then the cost of call is reduced by this pattern.
What Should Be Return Value of Method
- If a method is similar to a method in any of Java API, then follow the convention. E.g. delete method or remove methods return number of rows deleted.
- Make sure, void return type is used, when the method is definitely not returning any value. It may be possible that the return type is void but a non-final input parameter is getting updated in method, and it is referred later. Good to avoid it.
- Avoid multiple returns e.g. return in definition and through input parameter
What would be return value after customer save? It will be database image of customer object, i.e. input Customer object updated with generated database ids, timestamps etc. Hence the signature will be – Customer saveCustomer(Customer customer) throws ServiceException
Static or Non – Static Methods
Does method require container class instance? Is there any behavior change expected with different instances? If not, the method can be a static method. It will help in reducing memory footprint of application and subsequently garbage collection activity.
Cohesion in Methods
Is the method strongly related to the class or module? The answer must be yes! If there is strong cohesion, then related functionality is grouped together. This is opposite of coupling. Higher cohesion reduces coupling. Further uniform interface to this cohesive functionality reduces coupling amongst modules as well.
Defining Scope of Methods
Thumb rule is going for minimum scope i.e. private. If required, increase scope. Aim avoiding public, chance is that method will end up with minimum required scope. It allows creation of unnecessary coupling from clients.
As we are going to expose customer save operation to entire world the method signature will be –
public Customer saveCustomer(Customer customer) throws ServiceException