Object-oriented design (OOD) principles make up the core of object-oriented programs and systems (OOPS). It is the process of planning a system of interacting objects for the purpose of solving a software problem. An object contains encapsulated data and procedures grouped together to represent an entity. The object interface, or how the object can be interacted with, is also defined. An object-oriented program is described by the interaction of these objects. Object-oriented design is the discipline of defining the objects and their interactions to solve a problem that was identified and documented during object-oriented analysis.
There are innumerable ways that programmers use object-oriented design in software development. Many of these programmers tend to have a very one-sided view of OOD; especially when working with Java. Programmers often chase design patterns like Singleton, Decorator, or Observer without giving enough attention to object-oriented analysis, or following the core principles of using OOD with Java. There are developers and programmers of all skill levels who have never learned the correct design principles, do not fully understand the benefits a particular design principle offers, or are unsure of how to properly apply these principles when coding.
The most important thing when working with OOD in Java is to strive for cohesion in any solution, code, or design. Taking the time to look over successful applications of OOD in Java coding can be very helpful, and examples of open source Java OOD can be found all over the Internet. Apache and Sun offer excellent views of how Java coding should be tackled using OOD.
These examples are helpful, as was stated before, but they can never replace the importance of real world experience. This type of learning teaches not only the basics of successful design, but also gives users an idea of what happens when these design principles are violated. Mistakes teach valuable lessons in life and programming, so novice designers and those with little experience will benefit greatly from seeing firsthand what works and what does not.
Principles of OOD in Java
The list that follows defines and describes some core principles that developers must keep in mind when working with OOD in Java coding. These principles represent some information that all programmers need to know to be successful when working in this medium. These principles are:
DRY: DRY, or don’t repeat yourself, means just what it says; never write duplicate code. Use abstraction to group common things together in one place. For example, if a hardcoded value is used more than one time consider making it a public final constant, or if a block of code is located in more than one place try making it a separate method. This is a general rule of thumb, but, as with all things, there will be exceptions. The benefits from doing things in this way can be clearly seen when it is time for code maintenance.
Encapsulate What Varies: Change is the only constant in software development. This is a truism and leads to the next design principle; encapsulate what varies. The code expected, or suspected, of being changed in the future should be encapsulated. Encapsulation of code allows for easier testing and maintenance. In Java coding, it is best to make variables and methods private by default and increase access step-by-step from private to protected. The Factory design pattern in Java uses encapsulation on object creation code to provide flexibility when releasing new products. The code, due to its encapsulation, is not impacted by this change, so the programmer does not need to rewrite anything.
Open Closed: Classes, functions, or methods should be open for extension, or new functionality, and closed for modification. This is an excellent principle for preventing changes being made to functional, tested code by anyone other than the developer.
Single Responsibility: This principle is based on the assumption that a class should never have more than one reason to change, and should always handle single functionality. In Java coding, it is unwise to put more than one function into a class. By doing so, coupling between the two functions is impossible to prevent, so if one function needs to be changed there is a good chance that the coupling will be broken. This will require another round of testing to safeguard against any unforeseen problems cropping up in a production environment.
Dependency Injection or Inversion: The framework will provide the dependency, so that is not an issue for programmers. Spring framework achieves this beautifully. Any class that is injected by DI framework is easy to test with mock objects, and easier to maintain due to the fact that the object creation code is centralized in the framework, and the client code is entirely free of this clutter. There are multiple ways to implement dependency injection.
Composition over Inheritance: Whenever possible, it always best to favor composition over inheritance. This point is highly debatable, but composition seems to allow more flexibility than inheritance. Composition gives the option of changing the behavior of a class at runtime by setting the property, and using interfaces to compose a class using polymorphism. This provides the flexibility to upgrade implementation at any time.
Liskov Substitution: This principle states that subtypes must be substitutable for super type. It is closely related to both the single responsibility principle and the interface segregation principle that will be discussed later. In order to follow the Liskov substitution principle, all derived classes and subclasses must enhance functionality; not reduce it.
Interface Segregation: The interface segregation principle states that a client should not implement an interface if it is not necessary. The most common problem that arises from ignoring this principle happens when an interface contains more than one function, but the client only needs one of them. This is one of the reasons that interface design is so difficult. Once the interface is released from development, it cannot be changed without breaking all implementation. In Java, interfaces with only one function mean fewer methods to implement, and an overall easier task to complete.
Program for Interface not Implementation: Always program for interface instead of for implementation. This will lead to more flexible code that can coexist with any new implementation or interface. It is best to use interface type on variables, return types of method, and argument types of method when working with Java.
Delegation: This principle basically means that it is never a good idea to try to accomplish all the necessary steps alone. Delegate everything possible to respective class. Equals and hashcode methods in Java allow users to ask class itself to do a comparison of two objects for equality. The greatest benefits of this principle are ease of behavior modification and minimizing code duplication.
Final Thoughts
The listed principles of working with OOD in Java will help users write flexible, higher-quality code by maximizing cohesion and minimizing coupling. As in all things, theory is only the first step. Developing the knowledge necessary to properly apply these principles takes real-world experience and trial-and-error. This will give the programmer the tools to know when they are violating a key principle, and compromising the flexibility of the code. These principles are not fool-proof, and like all things in life they are to be used as guides and not blindly followed. It is beyond question that when working with OOD in Java these core principles will allow developers to make fewer mistakes, and get the upmost out of their coding work.