I have interviewed dozens of programmers and I must say that I am really shocked how more than 80% of the prospective employees fail to answer some of the most basic questions about OOP. Some folks have been programming years but fail to answer questions like what is the difference between a class and an object, what is the difference between == and equals() methods and much more. The below hopefully serves as a quick introduction to some of the fundamentals of OOP:
- EncapsulationEncapsulation enforces modularity.
Encapsulation refers to the creation of self-contained modules that bind processing functions to the data. These user-defined data types are called "classes," and one instance of a class is an "object." For example, in a payroll system, a class could be Manager, and Mikalai and Volha could be two instances (two objects) of the Manager class. Encapsulation ensures good code modularity, which keeps routines separate and less prone to conflict with each other.
- InheritanceInheritance passes "knowledge" down.
Classes are created in hierarchies, and inheritance allows the structure and methods in one class to be passed down the hierarchy. That means less programming is required when adding functions to complex systems. If a step is added at the bottom of a hierarchy, then only the processing and data associated with that unique step needs to be added. Everything else about that step is inherited. The ability to reuse existing objects is considered a major advantage of object technology.
- PolymorphismPolymorphism takes any shape.
Object-oriented programming allows procedures about objects to be created whose exact type is not known until runtime. For example, a screen cursor may change its shape from an arrow to a line depending on the program mode. The routine to move the cursor on screen in response to mouse movement would be written for "cursor," and polymorphism allows that cursor to take on whatever shape is required at runtime. It also allows new shapes to be easily integrated.
- Open Closed Principle (OCP)Classes should be open for extension but closed for modification.
The Open Closed Principle (OCP) is undoubtedly the most important of all the class category principles. In fact, each of the remaining class principles are derived from OCP. OCP states that we should be able to add new features to our system without having to modify our set of preexisting classes. One of the benefits of the object-oriented paradigm is to enable us to add new data structures to our system without having to modify the existing system's code base.
One of the principle of OCP is to reduce the coupling between classes to the abstract level. Instead of creating relationships between two concrete classes, we create relationships between a concrete class and an abstract class, or in Java, between a concrete class and an interface. When we create an extension of our base class, assuming we adhere to the public methods and their respective signatures defined on the abstract class, we essentially have achieved OCP.
- Liskov Substitution Principle (LSP)Subclasses should be substitutable for their base classes.
We mentioned that OCP is the most important of the class category principles. We can think of the Liskov Substitution Principle (LSP) as an extension to OCP. In order to take advantage of LSP, we must adhere to OCP because violations of LSP also are violations of OCP, but not vice versa. In its simplest form, LSP is difficult to differentiate from OCP, but a subtle difference does exist. OCP is centered around abstract coupling. LSP, while also heavily dependent on abstract coupling, is in addition heavily dependent on preconditions and postconditions, which is LSP's relation to Design by Contract, where the concept of preconditions and postconditions was formalized.
A precondition is a contract that must be satisfied before a method can be invoked. A postcondition, on the other hand, must be true upon method completion. If the precondition is not met, the method shouldn't be invoked, and if the postcondition is not met, the method shouldn't return. The relation of preconditions and postconditions has meaning embedded within an inheritance relationship that isn't supported within Java, outside of some manual assertions or nonexecutable comments. Because of this, violations of LSP can be difficult to find.
- Dependency Inversion Principle (DIP)Depend upon abstractions. Do not depend upon concretions.
The Dependency Inversion Principle (DIP) formalizes the concept of abstract coupling and clearly states that we should couple at the abstract level, not at the concrete level. In our own designs, attempting to couple at the abstract level can seem like overkill at times. Pragmatically, we should apply this principle in any situation where we're unsure whether the implementation of a class may change in the future. But in reality, we encounter situations during development where we know exactly what needs to be done. Requirements state this very clearly, and the probability of change or extension is quite low. In these situations, adherence to DIP may be more work than the benefit realized.
At this point, there exists a striking similarity between DIP and OCP. In fact, these two principles are closely related. Fundamentally, DIP tells us how we can adhere to OCP. Or, stated differently, if OCP is the desired end, DIP is the means through which we achieve that end. While this statement may seem obvious, we commonly violate DIP in a certain situation and don't even realize it.
Abstract coupling is the notion that a class is not coupled to another concrete class or class that can be instantiated. Instead, the class is coupled to other base, or abstract, classes. In Java, this abstract class can be either a class with the abstract modifier or a Java interface data type. Regardless, this concept actually is the means through which LSP achieves its flexibility, the mechanism required for DIP, and the heart of OCP.
- Interface Segregation Principle (ISP)Many specific interfaces are better than a single, general interface.
Any interface we define should be highly cohesive. In Java, we know that an interface is a reference data type that can have method declarations, but no implementation. In essence, an interface is an abstract class with all abstract methods. As we define our interfaces, it becomes important that we clearly understand the role the interface plays within the context of our application. In fact, interfaces provide flexibility: They allow objects to assume the data type of the interface. Consequently, an interface is simply a role that an object plays at some point throughout its lifetime. It follows, rather logically, that when defining the operation on an interface, we should do so in a manner that doesn't accommodate multiple roles. Therefore, an interface should be responsible for allowing an object to assume a SINGLE ROLE, assuming the class of which that object is an instance implements that interface.
- Composite Reuse Principle (CRP)Favor polymorphic composition of objects over inheritance.
The Composite Reuse Principle (CRP) prevents us from making one of the most catastrophic mistakes that contribute to the demise of an object-oriented system: using inheritance as the primary reuse mechanism.Inheritance can be thought of as a generalization over a specialization relationshipю That is, a class higher in the inheritance hierarchy is a more general version of those inherited from it. In other words, any ancestor class is a partial descriptor that should define some default characteristics that are applicable to any class inherited from it.
Any time we have to override default behavior defined in an ancestor class, we are saying that the ancestor class is not a more general version of all of its descendents but actually contains descriptor characteristics that make it too specialized to serve as the ancestor of the class in question. Therefore, if we choose to define default behavior on an ancestor, it should be general enough to apply to all of its descendents.
In practice, it's not uncommon to define a default behavior in an ancestor class. However, we should still accommodate CRP in our relationships.
- Principle of Least Knowledge (PLK)For an operation O on a class C, only operations on the following objects should be called: itself, its parameters, objects it creates, or its contained instance objects.
The Principle of Least Knowledge (PLK) is also known as the Law of Demeter. The basic idea is to avoid calling any methods on an object where the reference to that object is obtained by calling a method on another object. Instead, this principle recommends we call methods on the containing object, not to obtain a reference to some other object, but instead to allow the containing object to forward the request to the object we would have formerly obtained a reference to. The primary benefit is that the calling method doesn't need to understand the structural makeup of the object it's invoking methods upon.
The obvious disadvantage associated with PLK is that we must create many methods that only forward method calls to the containing classes internal components. This can contribute to a large and cumbersome public interface. An alternative to PLK, or a variation on its implementation, is to obtain a reference to an object via a method call, with the restriction that any time this is done, the type of the reference obtained is always an interface data type. This is more flexible because we aren't binding ourselves directly to the concrete implementation of a complex object, but instead are dependent only on the abstractions of which the complex object is composed. This is how many classes in Java typically resolve this situation.
- Package DependencyIf the contents of package A are dependent on the contents of package B, then A has a dependency on B; and if the contents of B change, this impact may be noticeable in A. Therefore, the relationships between packages become more apparent, and we can conclude the following:
If an element in package A uses an element in package B, then package A depends on package B.
- Release Reuse Equivalency Principle (REP)The granule of reuse is the granule of release.
Whenever a client class wishes to use the services of another class, we must reference the class offering the desired services. If the class offering the service is in the same package as the client, we can reference that class using the simple name. If, however, the service class is in a different package, then any references to that class must be done using the class' fully qualified name, which includes the name of the package.
Any Java class may reside in only a single package. Therefore, if a client wishes to utilize the services of a class, not only must we reference the class, but we must also explicitly make reference to the containing package. Failure to do so results in compile-time errors. Therefore, to deploy any class, we must be sure the containing package is deployed. Because the package is deployed, we can utilize the services offered by any public class within the package. While we may presently need the services of only a single class in the containing package, the services of all classes are available to us. Consequently, our unit of release is our unit of reuse, resulting in the Release Reuse Equivalency Principle (REP). This leads us to the basis for this principle, and it should now be apparent that the packages into which classes are placed have a tremendous impact on reuse. Careful consideration must be given to the allocation of classes to packages.
- Common Closure Principle (CCP)Classes that change together, belong together.
The basis for the Common Closure Principle (CCP) is rather simple. Adhering to fundamental programming best practices should take place throughout the entire system. Functional cohesion emphasizes well-written methods that are more easily maintained. Class cohesion stresses the importance of creating classes that are functionally sound and don't cross responsibility boundaries. And package cohesion focuses on the classes within each package, emphasizing the overall services offered by entire packages.
During development, when a change to one class may dictate changes to another class, it's preferred that these two classes be placed in the same package. Conceptually, CCP may be easy to understand; however, applying it can be difficult because the only way that we can group classes together in this manner is when we can predictably determine the changes that might occur and the effect that those changes might have on any dependent classes. Predictions often are incorrect or aren't ever realized. Regardless, placement of classes into respective packages should be a conscious decision that is driven not only by the relationships between classes, but also by the cohesive nature of a set of classes working together.
- Common Reuse Principle (CReP)Classes that aren't reused together should not be grouped together.
If we need the services offered by a class, we must import the package containing the necessary classes. As we stated previously in our discussion of REP (Release Reuse Equivalency Principle), when we import a package, we also may utilize the services offered by any public class within the package. In addition, changing the behavior of any class within the service package has the potential to break the client. Even if the client doesn't directly reference the modified class in the service package, other classes in the service package being used by clients may reference the modified class. This creates indirect dependencies between the client and the modified class that can be the cause of mysterious behavior. Цe can state the following:
If a class is dependent on another class in a different package, then it is dependent on all classes in that package, albeit indirectly.
This principle has a negative connotation. It doesn't hold true that classes that are reused together should reside together, depending on CCP. Even though classes may always be reused together, they may not always change together. In striving to adhere to CCP, separating a set of classes based on their likelihood to change together should be given careful consideration. Of course, this impacts REP because now multiple packages must be deployed to use this functionality. Experience tells us that adhering to one of these principles may impact the ability to adhere to another. Whereas REP and Common Reuse Principle (CReP) emphasize reuse, CCP emphasizes maintenance.
- Acyclic Dependencies Principle (ADP)The dependencies between packages must form no cycles.
Cycles among dependencies of the packages composing an application should almost always be avoided. Packages should form a directed acyclic graph (DAG).
If we do identify cyclic dependencies, the easiest way to resolve them is to factor out the classes that cause the dependency structure. Factoring out the classes that caused the cyclic dependencies has a positive impact on reuse:
Acyclic Dependencies Principle (ADP) is important from a deployment perspective. Along with packages being reusable and maintanable, the should also be deployable as well. Just as in the class design, the package design should have defined dependencies so that it is deployment-friendly.
- Stable Dependencies Principle (SDP)Depend in the direction of stability.
Stability implies that an item is fixed, permanent, and unvarying. Attempting to change an item that is stable is more difficult than inflicting change on an item in a less stable state.
Aside from poorly written code, the degree of coupling to other packages has a dramatic impact on the ease of change. Those packages with many incoming dependencies have many other components in our application dependent on them. These more stable packages are difficult to change because of the far-reaching consequences the change may have throughout all other dependent packages. On the other hand, packages with few incoming dependencies are easier to change. Those packages with few incoming dependencies most likely will have more outgoing dependencies. A package with no incoming or outgoing dependencies is useless and isn't part of an application because it has no relationships. Therefore, packages with fewer incoming, and more outgoing dependencies, are less stable.
Stability doesn't provide any implication as to the frequency with which the contents of a package change. Those packages having a tendency to change more often should be the packages that are less stable in nature. On the other hand, packages unlikely to experience change may be more stable, and it's in this direction that we should find the dependency relations flowing. Combining the concepts of stability, frequency of change, and dependency management, we're able to conclude the following:
- Packages likely to experience frequent change should be less stable, implying fewer incoming dependencies and more outgoing dependencies.
- Packages likely to experience infrequent change may be more stable, implying more incoming dependencies and fewer outgoing dependencies.
- Stable Abstractions Principle (SAP)Stable packages should be abstract packages.
One of the greatest benefits of object orientation is the ability to easily maintain our systems. The high degree of resiliency and maintainability is achieved through abstract coupling. By coupling concrete classes to abstract classes, we can extend these abstract classes and provide new system functions without having to modify existing system structure. Consequently, the means through which we can depend in the direction of stability, and help ensure that these more depended-upon packages exhibit a higher degree of stability, is to place abstract classes, or interfaces, in the more stable packages. We can state the following:
- More stable packages, containing a higher number of abstract classes, or interfaces, should be heavily depended upon.
- Less stable packages, containing a higher number of concrete classes, should not be heavily depended upon.
Any packages containing all abstract classes with no incoming dependencies are utterly useless. On the other hand, packages containing all concrete classes with many incoming dependencies are extremely difficult to maintain.