Top 5 OO tips with Java examples
Tip #1: Tightly encapsulate your classes. A class generally contains data as well as methods, and is responsible for the integrity of its own data. The standard way to protect the data is to make it private, so that no other class can get direct access to it, and then write a couple of public methods to get the data and set the data.
In the example below,
package com.oo; import java.math.BigDecimal; import org.apache.commons.lang.StringUtils; public class Employee { private String name; private int age; private BigDecimal salary; public String getName() { return name; } public void setName(String name) { if(StringUtils.isEmpty(name)){ throw new IllegalArgumentException("Invalid name !!!"); } this.name = name; } public int getAge() { return age; } public void setAge(int age) { if(age < 0 || age > 100) { throw new IllegalArgumentException("Invalid age !!!"); } this.age = age; } public BigDecimal getSalary() { return salary; } public void setSalary(BigDecimal salary) { if(salary.compareTo(BigDecimal.ZERO) < 0){ throw new IllegalArgumentException("Invalid salary !!!"); } this.salary = salary; } }Data: name, age, and salary are made private. So, you can't directly set these values from out side like
Employee employee1 = new Employee(); employee1.age = -20; //Illegal. Compile Error
The data is encapsulated, and only access is possible via the public methods. The public methods fail-fast by validating the input data. It won't allow any negative ages by throwing an "IllegalArgumentException" at run time. So, your data is protected via compile-time and run time checks. The other fields like name and salary are encapsulated as well from illegal use.
Employee employee1 = new Employee(); employee1.setAge(20); // okay
Tip #2: Hide non-essential details through abstraction. A good OO design should hide non-essential details through abstraction. Encapsulation is about hiding the implementation details whereas, abstraction is about providing a generalization.
For example, say you want to capture employment type like part-time, full-time, casual, semi-casual, and so on, it is a bad practice to define them as classes as shown below.
package com.oo; public class PartTimeEmployee extends Employee { }
package com.oo; public class FullTimeEmployee extends Employee { }
You will end up creating new classes for each new employment type, making your code more rigid and tightly coupled. The better approach is to abstract out the employmentType as a field. This way, instead of creating new classes for every employment type, you will be just creating new objects at run time with different employmentType.
public class Employee { enum EmploymentType {PART_TIME, FULL_TIME, CASUAL, SEMI_CASUAL} private String name; private int age; private BigDecimal salary; private EmploymentType employmentType; // ... getters and setters }
Another detailed example on abstraction Java OO interview Questions and Answers
Tip #3: Loosely couple your classes. There are basically three relationships between classes
- is a relationship. Also known as an inheritance and in UML terms generalization
- has a relationship. Also known as a composition and in UML terms association.
- uses a relationship. Also known as a delegation in UML terms.
Definition 1:
package com.oo; import java.math.BigDecimal; public interface SalaryProcessor { BigDecimal processSalary(Employee employee); }
Definition 2:
package com.oo; import java.math.BigDecimal; public interface SalaryProcessor { BigDecimal processSalary(String name, BigDecimal salary); }
The Definition 1 is more loosely coupled. If in the future the method processSalary(...) requires employmentType to process the salary, adding a new parameter to the method processSalary(...) can break all the classes that depends on it. But if you were to pass the Employee object as an argument, you don't have to change the processSalary(...) definition, but just change the implementation to use the employmentType.
Even if the processSalary(..) method needed a new parameter like say leaveLoading, you don't have to change the signature of the processSalary( ) method, and just add the new field to the Employee class, and include that in the implementation. The interfaces provide the contract, and you need to make minimal changes. The implementations can change as they don't break the contract.
public class Employee { enum EmploymentType {PART_TIME, FULL_TIME, CASUAL, SEMI_CASUAL} private String name; private int age; private BigDecimal salary; private EmploymentType employmentType; private BigDecimal leaveLoading; //.....getters and setters }
With the advent of IoC frameworks like Spring, you can increase loose coupling through dependency injection. Use the Dependency Inversion principle for loose coupling.
Being loosely coupled goes hand in hand with the idea of Separation of Concerns (SoC), which is the process of breaking a program into distinct features in order to reduce overlapping functionalities.
For example, the following Employee class tries to do too many things like being a model class, data access logic, and validation logic.
public class Employee { enum EmploymentType {PART_TIME, FULL_TIME, CASUAL, SEMI_CASUAL} private String name; private int age; private BigDecimal salary; private EmploymentType employmentType; private BigDecimal leaveLoading; public List<employee> loadEmployees() { //sql logic to load emplyees } public boolean validateEmployeeSalary() { //validation logic } //getters and setters }The data access logic can be extracted out to a separate class.
public interface EmployeeDao { abstract ListloadEmployees() ; }
public class EmployeeDaoImpl implements EmployeeDao { @Override public List<employee> loadEmployees() { // TODO ..................... return null; } }
Similarly, a Validator interface can be used for validation.
Tip #4: Favor composition over inheritance to get code reuse.
- With composition, you will have full control of your implementations. i.e., you can expose only the methods you intend to expose.
- With composition, it's easy to change behavior on the fly with Dependency Injection / Setters. Inheritance is more rigid as most languages do not allow you to derive from more than one type.
This does not mean that you can't use inheritance. Apply "Liskov Substitution & Interface Segregation Principles" to ensure that it makes sense to use inheritance.
In a more simpler terms, when you use inheritance to reuse code from the super class, rather than to override methods and define another polymorphic behavior, it's often an indication that you should use composition instead of inheritance. A typical example is that
In geometry a "Square is a rectangle". But in programming, a square is not a rectangle as explained in detail here.
A template-method design pattern is a good example for using inheritance, which is often used in frameworks. The Gang of Four (GoF) design patterns favor composition over inheritance.
Tip #5: Use polymorphism.
So, use
instead of
As a List can take different forms like ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, and Vector as they all implents the List interface. So, you can easily swith to CopyOnWriteArrayList if you want thread-safety, etc.
Finally.....be aware of the OO design principles and judiciously apply them where appropriate.
All these principles are explained with code in "Core Java Career Essentials. Design is always a trade-off, and there is no black and white answers.
In geometry a "Square is a rectangle". But in programming, a square is not a rectangle as explained in detail here.
A template-method design pattern is a good example for using inheritance, which is often used in frameworks. The Gang of Four (GoF) design patterns favor composition over inheritance.
Tip #5: Use polymorphism.
So, use
List<Emplyee> employees = new ArrayList<Employee>();
instead of
ArrayList<Emplyee> employees = new ArrayList<Employee>();
As a List can take different forms like ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, and Vector as they all implents the List interface. So, you can easily swith to CopyOnWriteArrayList if you want thread-safety, etc.
Finally.....be aware of the OO design principles and judiciously apply them where appropriate.
- DRY (Don’t repeat yourself): Don’t write duplicate code, instead use abstraction to abstract common things in one place.
- Open Closed Principle(OCP): The Open Close Principle states that the design and writing of the code should be done in a way that new functionality should be added with minimum changes in the existing code. The design should be done in a way to allow the adding of new functionality as new classes, keeping as much as possible existing code unchanged.
- Single Responsibility Principle (SRP): If you put more than one functionality in one Class in Java it introduce coupling between two functionality. This is also aknown as the SoC (Separation of Concerns) they was discussed earlier with EmployeeDao example.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
- Interface Segregation Principle (ISP): a client should not implement an interface if it doesn’t use that. So, don't have fat interface with 10+ methods. Segregate them into separate interfaces.
- Liskov Substitution Principle (LSP): This is related to Single Responsibility Principle and Dependency Inversion Principle. Subtypes must be substitutable for super type. When you use inheritance to reuse code from the super class, rather than to override methods and define another polymorphic behavior, it's often an indication that you should use composition instead of inheritance as it breaks the LSP.
All these principles are explained with code in "Core Java Career Essentials. Design is always a trade-off, and there is no black and white answers.
Labels: OO
0 Comments:
Post a Comment
Subscribe to Post Comments [Atom]
<< Home