Don't Just Code. Make It Readable, Maintainable.
Overview
Mostly, Object-Oriented Programming makes programming simple. ODD bring classes and objects into the picture. Each object has its own state and behaviors. Class is a blueprint. The program could be complex or easy. Unstructured Code is one of the key reasons for a complex program. One can reduce the complexity of the program by following naming conventions, writing proper comments in code, and trying to do unit testing of each line of code.
But still, the program can be still complex, and we can find out that while maintaining it or implementing new functionalities. So, Robert C. Martin (Uncle Bob) introduced SOLID principles to avoid these complexities.
Single Responsibility Principle
The class can have multiple behaviors but creating classes with similar behaviors means single responsibility will be good practice.
If you change anything in this class, it will affect only one particular behavior of the software. That makes the code more robust because there will be fewer side effects. If a class had two responsibilities and you changed anything, the risk would be high that you also break the logic for the second behavior.
E.g. If a class is responsible for fetching comments and returning the string in CSV format of the comment list. Here, the class has 2 reasons to change the class code.
public class ResponsibleClass {
private List<Comment> getPostComments(Double num) {
// fetch comments from db}
// return comment list
return comments;
}
private String parseToCSV(List<Comment> comments) {
// parse comments to csv string
return commentsAsCSV;
}
If in the future,
i. There can be a requirement to change either the data source or the way comments are fetched.
ii. There can be a requirement to parse comments into another format.
Open-Closed Principle
Principle:
“classes should be open for extension but closed for modification”
public class Area{
public Double calculateDefaultArea(Shape shape){
if( shape instanceof Circle)
// formula: 2 * pi * radios
return 2 * 3.14 * 4;
else if( shape instanceof square){
// formula: length * length
return 4.0 * 4.0;
}
throw new NoSuchShapeFound();
}
}
Here, Area class can handle only 2 shapes. Let's say in the future, if there is a requirement to handle rectangle shape then the developer will modify the logic of calculateDefaultArea(). Changing the current behavior of a class will affect all systems that use that class.
A better approach can be:
interface Area {
Double calculateDefaultArea();
}
class Circle implements Area {
@Override
public Double calculateDefaultArea() {
// formula: 2 * pi * radios
return 2 * 3.14 * 4;
}
}
class Square implements Area {
@Override
public Double calculateDefaultArea() {
// formula: length * length
return 4.0 * 4.0;
}
}
Now, the requirement of calculating the default area of rectangle shape can be handled by extending the functionality of Area interface and the developer does not need to change other code.
class Rectanlge implements Area {So risk will be less. It will also remove conditions from the code and the code will be more robust.
@Override
public Double calculateDefaultArea() {
// formula: length * width
return 4.0 * 10.0;
}
}
Liskov Substitution Principle
LSP extends the Open/Closed Principle by focusing on the behavior of a superclass and its subtypes.
A principle in simple words,
An object of the Super(Parent) Class should be replaceable with the object of its Derived(Child) class.
class Message{
boolean message(String message){
// body
}
boolean deleteMessage(String message){
// body
}
}
class Group extends Message{
@Override
public boolean message(String message){
// body
}
@Override
public boolean deleteMessage(String message){
// body
}
}
class BroadCast extends Message{
@Override
public boolean message(String message){
// body
}
@Override
public boolean deleteMessage(String message){
// not supported
}
}
class Watsapp {
public boolean confidentialMessage(Message message){
message.message("message");
// wait for 10 second
message.deleteMessage("message");
}
}
Here, If confidentialMessage() is called by BroadCast obj then there will be RuntimeException because BroadCast does not support functionality to delete messages. Basically, Message obj can not be replaced by its child. To fix this design we developer can add conditions but that violates SRP.
A better approach can be:
interface Message{
boolean message(String message);
}
class Group implements Message{
@Override
public boolean message(String message){
// body
}
public boolean deleteMessage(String message){
// body
}
}
class BroadCast implements Message{
@Override
public boolean message(String message){
// body
}
}
class Watsapp {
public boolean confidentialMessage(Message message){
message.message("message");
// logic
}
}
Now Message obj can be replaced by any of the sub-class obj. This code will not throw RuntimeException.
Interface Segregation Principle
ISP suggests developers should create client-specific interfaces rather than general interfaces.
If code has a general interface then it will force its clients to implement unused behaviors as well.
interface Content{
boolean like(String message);
String comment(String message);
boolean share(String message);
}
class Post implements Content{
@Override
public boolean like(String message) {}
@Override
public String comment(String message) {}
@Override
public boolean share(String message) {}
}
class Reel implements Content{
@Override
public boolean like(String message) {}
@Override
public String comment(String message) {}
@Override
public boolean share(String message) {}
}
/**
* user can not like, and comment story
*/
class Story implements Content{
// @Override
// public boolean like(String message) {}
//
// @Override
// public String comment(String message) {}
@Override
public boolean share(String message) {}
}
But the better approach can be to create a separate interface for share().
interface Content{
boolean like(String message);
String comment(String message);
}
interface ShareableContent{
boolean share(String message);
}
class Post implements Content, ShareableContent{
@Override
public boolean like(String message) {}
@Override
public String comment(String message) {}
@Override
public boolean share(String message) {}
}
class Reel implements Content, ShareableContent{
@Override
public boolean like(String message) {}
@Override
public String comment(String message) {}
@Override
public boolean share(String message) {}
}
class Story implements ShareableContent{
@Override
public boolean share(String message) {}
}
The story feature is sharable, so Story class should only implement ShareableContent interface.
Dependency Inversion Principle
Classes should not depend on concrete details. Classes should depend on abstraction so that classes become loosely coupled.
class PetrolEngine{
boolean start(){
// body
}
}
class Car{
String name;
PetrolEngine engine;
}
Here, Car class is tightly coupled with PetrolEngine class.
Let's say Car could have Diesel Engine and that is a new requirement. The developer will update Car class to fulfill the requirement as Car class depends on concrete detail.
A better approach could be
interface Engine {
boolean start();
}
class PetrolEngine implements Engine {
@Override
boolean start() {
// body
}
}
class DieselEngine implements Engine {
@Override
boolean start(){
// body
}
}
class Car{
String name;
Engine engine;
}
April 23, 2022
Tags :
Engineer
Subscribe by Email
Follow Updates Articles from This Blog via Email
4 Comments
Like the simplicity of the language & domain knowledge of the blog.
Reply DeleteNicely explain easy to understand
Reply DeleteGood explanation
Reply DeleteWrite clean and maintainable code is much required skills.
Reply DeleteGood work
Please comment here...