Journey of a Techie

Design Principles – 2

Advertisements

Object Oriented Programming

We will look at the important principles of object oriented programming.

  1. Abstraction
  2. Encapsulation
  3. Inheritance

Lets start by taking an example. Lets say we need to develop a program which is responsible for sending SMS. We would have integrated with multiple vendors and each vendor would have its own set of API requirements.

Lets create the Classes.

/**
 * The variables that are required by the vendor A
 */
public class VendorRequestA {

  String message;
  String uniqueReferenceNumber;
  String phoneNumber;
}


/**
 * The variables that are required by vendor B
 */
public class VendorRequestB {
  String message;
  String uniqueRefNo;
  String phoneNumber;
}

public class MessageInvoker {

  /**
   * The enums denote the status for the message.
   */
  public enum MessageStatus {
    SUCCESS,
    FAILURE,
    PENDING
  }

  /**
   * This method is invoked when a message needs to be sent.
   *
   * @param phoneNumber the phone number which would receive the message.
   * The number is passed on as is to vendor and no validations are performed.
   * @param message the message that needs to be sent to the number
   * @return the status of the message
   */
  public MessageStatus sendMessage(String phoneNumber, String message) {
    boolean sendThroughA = true; //Identify which vendor needs to be used. Assuming the invocation says to use A.
    if (sendThroughA) {
      return sendMessageThroughA(phoneNumber, message);
    } else {
      return sendMessageThroughB(phoneNumber, message);
    }
  }

  /**
   * Send the message through the vendorA
   *
   * @return the message status.
   */
  private MessageStatus sendMessageThroughA(String phoneNumber, String message) {
    VendorRequestA vendorRequestA = new VendorRequestA();
    vendorRequestA.phoneNumber = phoneNumber;
    vendorRequestA.message = message;
    vendorRequestA.uniqueReferenceNumber = "VendorA" + UUID.randomUUID();
    //Invoke the function that  transmits the information to the vendor. And return status from API.
    MessageStatus messageStatus = MessageStatus.SUCCESS;
    return messageStatus;
  }

  /**
   * Send the message through the vendorB
   *
   * @return the message status.
   */
  private MessageStatus sendMessageThroughB(String phoneNumber, String message) {
    VendorRequestB vendorRequestB = new VendorRequestB();
    vendorRequestB.phoneNumber = phoneNumber;
    vendorRequestB.message = message;
    vendorRequestB.uniqueRefNo = "VendorB" + UUID.randomUUID();
    //Invoke the function that  transmits the information to the vendor. And return status from API.
    MessageStatus messageStatus = MessageStatus.SUCCESS;
    return messageStatus;
  }
}

Prima face, the code does not seem to have any issues. Lets now see it from the view of OOP principles.

Abstraction

Abstraction in the terminology of OOPs mean hiding the internal details.

Lets work on hiding the inner details of sending the messages. Lets refactor the code so that we can abstract out the code for sending the messages.

What does it mean to abstract out the code for sending messages?

It means that the MessageInvoker.sendMessage should not be worry about how the objects for the communication are formed and sent over the wire.

Does this mean the interface should have multiple implementations?

No, abstraction simply implies we hide the workings.

Lets start by creating a interface which abstracts out the transmission of the message.

public interface MessageTransmitter {

  MessageStatus transmitMessage(String phoneNumber, String message);

}

Let us now create different classes that can deal with the transmission with respective vendors.

public class MessageTransmitterThroughA implements MessageTransmitter {

  /**
   * Send the message through the vendorA
   *
   * @return the message status.
   */
  public MessageStatus transmitMessage(String phoneNumber, String message) {
    VendorRequestA vendorRequestA = new VendorRequestA();
    vendorRequestA.phoneNumber = phoneNumber;
    vendorRequestA.message = message;
    vendorRequestA.uniqueReferenceNumber = "VendorA" + UUID.randomUUID();
    //Invoke the function that  transmits the information to the vendor. And return status from API.
    MessageStatus messageStatus = MessageStatus.SUCCESS;
    return messageStatus;
  }

}

public class MessageTransmitterThroughB implements MessageTransmitter{

  /**
   * Send the message through the vendorB
   *
   * @return the message status.
   */
  public MessageStatus transmitMessage(String phoneNumber, String message) {
    VendorRequestB vendorRequestB = new VendorRequestB();
    vendorRequestB.phoneNumber = phoneNumber;
    vendorRequestB.message = message;
    vendorRequestB.uniqueRefNo = "VendorB" + UUID.randomUUID();
    //Invoke the function that  transmits the information to the vendor. And return status from API.
    MessageStatus messageStatus = MessageStatus.SUCCESS;
    return messageStatus;
  }
}

Now its time to use the interface

 /**
   * This method is invoked when a message needs to be sent.
   *
   * @param phoneNumber the phone number which would receive the message.
   * The number is passed on as is to vendor and no validations are performed.
   * @param message the message that needs to be sent to the number
   * @return the status of the message
   */
  public MessageStatus sendMessage(String phoneNumber, String message) {
    MessageTransmitter messageTransmitter = new MessageTransmitterThroughA(); //Identify which vendor needs to be used. Assuming we go with A.
    return messageTransmitter.transmitMessage(phoneNumber, message);
  }

How do we identify abstraction?

Whenever we can use a interface without having to understand how it has been implemented, we say we have abstracted out the functionality. In the above example, we abstracted out the creation and transmission of data which could be vendor specific.

Issues with frameworks?

When using a DI, habitually an interface is created which is used. It is not an abstraction till the interface can cleanly hide the implementation. For example, the interface should clearly define if it throws any exception, or how it propagates responses. A response variable set inside the request object is not abstraction as you now know the inner working of the method that it sets a particular field.

Encapsulation

Encapsulation in OOPs means hiding information

In the above example, we see that all the variables of VendorRequestA and VendorRequestB have default access modifiers.

Why is this an issue?

The issue happens when we want to control how the values can be set or get. Lets assume that each of the vendor needs the unique reference number to begin with some specific pattern, in that case everywhere the unique reference number gets generated need to handle it.

How do we solve it?

public class VendorRequestA {

  private static final String VENDOR_A_PREFIX = "VendorA";
  private static final String INDIAN_PREFIX = "+91";

  private String message;
  private String uniqueReferenceNumber;
  private String phoneNumber;

  public String getReferenceNumber() {
    if (uniqueReferenceNumber == null) {
      uniqueReferenceNumber = VENDOR_A_PREFIX + UUID.randomUUID();
    }
    return uniqueReferenceNumber;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public void setPhoneNumber(String phoneNumber) {
    if (!phoneNumber.startsWith(INDIAN_PREFIX)) {
      this.phoneNumber = INDIAN_PREFIX + phoneNumber;
    } else {
      this.phoneNumber = phoneNumber;
    }
  }

  public String getPhoneNumber() {
    return phoneNumber;
  }
}

Lets look at the above code. This code hides the information that phone numbers would be stored with Indian prefix. It also hides how the unique reference number is generated. This helps keep customisation and the data responsibility at a single place and not throughout the code.

Issues with frameworks

With the usage of lombok, setters and getters are exposed blankly without taking into account why we need setters and getters and why were the fields not exposed publicly instead. Each field needs to have a reason for being exposed and it cannot be done blindly.

How do we know we are violating it?

The biggest violation occurs when you are manipulating the data for the object using setters and getters. For example, in the above if we fetched the unique reference number, generated new (in case of not generated before) and set it back, it would be in violation. Why? Because this class has now become aware of how the data needs to be managed for the other object.

Inheritence

Inheritance refers to the ability of an object deriving characteristics from other class.

By breaking the class of vendor request as follows.

public abstract class VendorRequest {

  private static final String INDIAN_PREFIX = "+91";

  private String message;
  private String phoneNumber;

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public void setPhoneNumber(String phoneNumber) {
    if (!phoneNumber.startsWith(INDIAN_PREFIX)) {
      this.phoneNumber = INDIAN_PREFIX + phoneNumber;
    } else {
      this.phoneNumber = phoneNumber;
    }
  }

  public String getPhoneNumber() {
    return phoneNumber;

  }

}

public class VendorRequestA extends VendorRequest {

  private static final String VENDOR_A_PREFIX = "VendorA";

  private String uniqueReferenceNumber;

  public String getReferenceNumber() {
    if (uniqueReferenceNumber == null) {
      uniqueReferenceNumber = VENDOR_A_PREFIX + UUID.randomUUID();
    }
    return uniqueReferenceNumber;
  }
}

We now say that VendorRequestA ‘is a’ specific type of VendorRequest.

Why is it needed

It helps in abstracting out the common properties and functionalities in a parent or base class.

Why can’t we use composition instead

Composition is favoured over inheritance. Composition provides a ‘has a’ relationship. Inheritance provides ‘is a’ relationship. In the above example, ‘VendorRequestA’ is a ‘VendorRequest’ and it has a ‘phoneNumber’ (here a string, could have been object as well).

How do we know we are violating it?

Try speaking the classes. If the ‘is a’ sounds absurd, you are in violation.

Notes :-

  1. There may be cases of collision even in cases of UUID. For all practical purposes, the chances of collision being very close to 0, it can be assumed for the UUID to provide unique reference numbers.
  2. To reduce the chances further, appending machine id, timestamp, could be done to the reference number which further reduces the chances of collision.
  3. There is a difference in function and procedure. The writing uses them without distinguishing the difference.
Advertisements

Advertisements