Friday, September 17, 2021

Best Practices & Principles in OOP

SOLID principles

Before even beginning to introduce all the essential OOP principles and patterns, keep in mind that:

  • Focus only on KISS ("Keep it simple and stupid") and OOP Principles first, then observe your code to see which pattern it might need for a code quality improvement -> apply patterns  

1. Single Responsibility 

"A class should have one, and only one, reason to change."

Ask yourself "what job should this class do?" before starting to code. Use one word or one sentence to describe the only responsibility of this class.
The class should have high cohesion, which means its contents have strong relationships to each other, and they would be changed together and stay together.

2. Open/Closed 

A class should be open for Extension, but closed for Modification, so that minimum modification is needed on existing code when implementing new functionalities.

3. Liskov Substitution 

If class a is a sub type of class b, then replacing b with a should not disrupt the function of the program. 


4. Interface Segregation 

Interfaces should only have one responsibility and only contains methods necessary for all class/interface that might implement it. For example, the interface car should not have turnOnEngine() method, because not every class/interface that implements car would even have an engine.

5. Dependency Inversion

Why is it called "inversion"?
Because in traditional software architecture, high-level entities are built upon low-level ones, so this dependency on low-level modules make it harder to reuse the code.

Therefore, we "invert" the dependency by letting high-level modules to depend on modules with even higher-level of abstractions (for example, an interface in Java)

How to?
"Entities(include both high and low-level modules) must depend on abstractions, not on concretions." 
"Abstractions should not depend on details. Details should depend on abstractions."

Example: 

public class Computer{
		private Monitor monitor;
		private Keyboard keyboard;
		
		public Computer(Monitor m, Keyboard k){
			monitor = m;
			keyboard = k;
		}
		
		public void printToMonitor() {
			//Do sth
		}
	}
The corresponding class diagram:

To achieve DI, we can introduce an abstraction to model the interaction between high-level (Computer) and low-level (Monitor and Keyboard) modules.

Let's check our criteria from the "how to?" section again.
"Entities (include both high-level ones, Computer, and low-level ones, Monitor and Keyboard) must depend on abstractions (IKeyboard and IMonitor), not on concretions." 
Let's review the most crucial concept for dependency inversion again:  
"Abstractions should not depend on details. Details should depend on abstractions."


GRASP pattern (General Responsibility Assignment Software Patterns)

Proposed by Craig Larman in 1997, which contains 9 OOP design principles. This article will introduce only 6 out of them.
Responsibility can be carried out by a single object OR a group or objects, and GRASP helps us to assign the responsibilities to class/object.

  • Information expert 
         Rule of thumb: Assign the responsibility to a class that has the most information to fulfill that responsibility.

  • Creator 

          Choose Class b to be the creator of class a (having the responsibility of creating an instance of class a) if one or more of following is true:

  1.  Class b aggregates or contains objects of class a (preferred!)
  2.  Class b records instances of A objects 
  3.  Class b closely uses objects of class a
  4.  Class b has the data needed to initialize class a when it is created
A class is coupled with its creator, therefore we should choose the creator properly to prevent establishing unnecessary coupling. 

see also: Factory Pattern ( I would introduce design patterns in a separate article.) 

  • Controller

          The responsibility of receiving and handling system events should be assign to classes with following characteristics:

  1. When the class represents the overall system or subsystem.
  2. When the class represents a use case scenario within which the system event occurs

  • Indirection 

          Consider assigning responsibility to build loose coupling by using "wrapper" modules.

There are three ways of implementing indirection.

  1. Behavioral extension: Add a module between client and target modules to provide loose coupling. See Also: Proxy and Decorator patterns.
  2. Interface modification: Changing a target interface to meet the clients needs. See also Adapter and Mediator patterns.
  3. Complexity encapsulation: Move the complex implementations to other layers. See also Facade pattern.

  • Low coupling

          Assign a responsibility so that coupling remains low. That means a class should have minimal dependency on other classes, so it wouldn't be easily affected by changes in other classes and is easier to understand, reuse and test.

Class A and B(class or interface) are coupled when:
  1. Class A is an attribute in B
  2. Class A call a method in B
  3. If A is a return value or parameter of a method in B
  4. A extends & implements B
If A and B not coupled yet, then we would not couple them until necessary. (i.e. A responsibility can not be carried out if A and B are not coupled.)

  • High cohesion 

          A class having low cohesion is undesirable, because it either contains unrelated information or methods (do too much work). This is actually a paraphrase for "Single Responsibility Principle".

References:

http://sharif.edu/~ramsin/index_files/pselecture6.pdf

https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)#Creator



Composition over Inheritance

Take Java for example, we prefer defining new interfaces and implementations of it instead of defining classes and extending them.

No comments:

Post a Comment