Design Patterns (Very simple examples)
Aalishan Ansari
SDE-2 @TESCO | Android | Kotlin | Java | MVVM | Coroutines | Rx Android | Architecture components | Jetpack Compose | Product base | Startups | Individual & Open Source Contributor | UI/UX Designer | Android Blogger
In the software industry, most common problems have common solutions, and that solution follows common types of patterns, The answers are called?design patterns.
Many of us encounter this situation where we must deal with object creation, structure management, and behaviour observation of problems. So in order to solve these correctly, we have?design patterns
There are 3 categories into which all the design patterns fall.
Let’s talk about common design patterns in detail
Creational Design Pattern
1. Singleton Pattern:
The singleton pattern is a design pattern that restricts the instantiation of a class to one object.
When we want to create only one instance of our class’s object we need to use a singleton design pattern.
In this, we create only 1 instance of our class throughout the lifecycle of our application
Such as we need to create a DB connection, some config, 3rd party APIs, etc.
Let’s see a simple example of a singleton design pattern.
public class Singleton {
//This is the variable where we will store instance
// This is static because we want only one instance
private static Singleton instance = null;
//This is private class because we dont want someone to create //direct object of this class
private Singleton() {
System.out.println("new object created of Singleton()");
}
//This is the factory method by which someone can access the //object
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Now let’s see if the above class is singleton or not,
1. Create a duplicate object by using reflection:
Singleton obj1= Singleton.getInstance();
Class clazz = Class.forName("com.package.Singleton");
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);Singleton obj2= constructor.newInstance();System.out.println("HashCode of obj1: " + obj1.hashCode());
System.out.println("HashCode of obj2: " + obj2.hashCode());
2. Create a duplicate object by using?Serialization:
Singleton obj1= Singleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("loction/save"));
outputStream.writeObject(singletone);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("loction/save"));
Singleton obj2 = (Singleton) objectInputStream.readObject();System.out.println("HashCode of obj1: " + obj1.hashCode());
System.out.println("HashCode of obj2: " + obj2.hashCode());
3.?Create an object by Cloning the object:
Singleton obj1 = Singleton.getInstance();SingletonDesign obj2 = (Singleton) obj1.clone();
System.out.println("HashCode of obj1: " + obj1.hashCode());
System.out.println("HashCode of obj2: " + obj2.hashCode());
4. Create an object by Thread:
ExecutorService service = Executors.newFixedThreadPool(2);
service.submit(SingletonTest::useSingleton);
service.submit(SingletonTest::useSingleton);
service.shutdown();
static void useSingleton(){
Singleton singleton = Singleton.getInstance();
System.out.println("HashCode of obj: " + obj.hashCode());
}
Now let’s try to create a fully secure singleton
public class Singleton {
private static volatile Singleton mInstance;
private Singleton() {
// To prevent reflection we need to do not null check
if (mInstance != null) {
throw new RuntimeException("Please use getInstance()");
}
System.out.println("Singleton Constructor");
}
public final static Singleton getInstance() {
// Make thread-safe Singleton by using Double Check Locking
if (mInstance == null) {
// 1- check
synchronized (Singleton.class) {
if (mInstance == null) {
// 2- check
mInstance = new Singleton();
}
}
}
return mInstance;
}
//Prevent from Serialization
protected Object readResolve() throws ObjectStreamException {
//return the mInstance or throw the Exception
return mInstance;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// instead of super.clone() return the mInstance or throw the Exception
return mInstance;
}
}
2. Builder Design Pattern:
Builder design pattern says to create a complex object step by step and the final step will return the object.
The process of constructing an object should be generic so that it can be used to create different representations of the same object.
If we don’t want to give all the values required we can leave it, By default value will be assigned to it.
Let's see the example
Take an example of phone class A Phone can have these 4 properties which we provide in the constructor
public class Phone {
private String name;
private String brand;
private int prize;
private int ram;
public Phone(String name, String brand, int prize, int ram) {
this.name = name;
this.brand = brand;
this.prize = prize;
this.ram = ram;
}
@Override
public String toString() {
return "Phone{" +
"name='" + name + '\'' +
", brand='" + brand + '\'' +
", prize=" + prize +
", ram=" + ram +
'}';
}
}
We want to create a phone by using a builder pattern so we need to have PhoneBuilder so that we can provide the values required value to it
public class PhoneBuilder {
private String name;
private String brand;
private int prize;
private int ram;
public String getName() {
return name;
}
public PhoneBuilder setName(String name) {
this.name = name;
return this;
}
public String getBrand() {
return brand;
}
public PhoneBuilder setBrand(String brand) {
this.brand = brand;
return this;
}
public int getPrize() {
return prize;
}
public PhoneBuilder setPrize(int prize) {
this.prize = prize;
return this;
}
public int getRam() {
return ram;
}
public PhoneBuilder setRam(int ram) {
this.ram = ram;
return this;
}
public Phone getPhone() {
return new Phone(name, brand, prize, ram);
}
}
In the above phone builder, we have a setter of each method that will return the same object after initializing the value to the property.
public class PhoneConsumer {
public static void main(String[] args) {
Phone phone = new PhoneBuilder().setBrand("iPhone")
.setName("Apple").getPhone();
System.out.println(phone);
}
}
Here we have the PhoneConsumer class which will provide values to the PhoneBuider according to their requirements.
3. Factory Design Pattern:
In the Factory pattern, we create objects without exposing the creation logic to the client and refer to newly created objects using a common interface.
The factory design pattern says that define an interface ( A java interface or an abstract class) and let the subclasses decide which object to instantiate.
Example:
Let’s say we want to create a shape of type Circle or Ractangal or Triangle, We don't want to hardcode the implementation of all shapes so let's say we want to create a shape of type Circle or Triangle or Rectangle.
In order to create a shape by factory method, we need the following things
- Interface in which factory method will be there.
- Classes of what all types of shapes we want to create.
- A Factory will be responsible for creating the object of shape.
- The consumer will decide which type of shape is required.
This is the Shape Interface
public interface Shape {
void draw();
}
Here we have 2 classes that will implement the Shape Interface and provide the implementation to it.
Circle class:
public class Circle implements Shape{
@Override
public void draw() {
System.out.println("Circle");
}
}
Rectangle Class:
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("Rectangle");
}
}
Now we need a shape factory that will have the object creation logic
public class ShapeFactory {
public Shape getShape(String type) {
if (type.equalsIgnoreCase("rectangle")) {
return new Rectangle();
} else if (type.equalsIgnoreCase("circle")) {
return new Circle();
}
return null;
}
}
Now we need a Shape Consumer
public class ShapeConsumer {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
Shape s = shapeFactory.getShape("Circle");
s.draw();
}
}
4. Abstract Factory Design Pattern:
This is similar to the factory pattern but it increases 1 more layer of abstraction in object creation.
Abstract Factory patterns work around a super-factory which creates other factories.
Let's see the example:
Here we have 2 interfaces
Color Interface:
public interface Color {
void fill();
}
Shape Interface:
public interface Shape {
void draw();
}
Abstract factory:
领英推è
public abstract class AbstractFactory {
abstract Color getColor(String colorType);
abstract Shape getShape(String shapeType);
}
Color Factory:
From the?Color?factory we will return null for?Shape
public class ColorFactory extends AbstractFactory {
@Override
public Color getColor(String colorType) {
if (colorType.equalsIgnoreCase("red")) {
return new Red();
} else if (colorType.equalsIgnoreCase("black")) {
return new Black();
}
return null;
}
@Override
public Shape getShape(String shapeType) {
return null;
}
}
Shape Factory:
From the?Shape?factory, we will return null for?Color
public class ShapeFactory extends AbstractFactory {
@Override
public Color getColor(String colorType) {
return null;
}
@Override
public Shape getShape(String shapeType) {
if (shapeType.equalsIgnoreCase("ractangle")) {
return new Ractangle();
} else if (shapeType.equalsIgnoreCase("circle")) {
return new Circle();
}
return null;
}
}
Now we will need a?factory producer?which will return the factory of the type given by the user(Color or Shape)
public class FactoryProducer {
public static AbstractFactory getFactory(String type) {
if (type.equalsIgnoreCase("Shape")) {
return new ShapeFactory();
} else if (type.equalsIgnoreCase("Color")) {
return new ColorFactory();
}
return null;
}
}
Now we need a consumer
public class ShapeAndColorConsumer {
public static void main(String[] args) {
AbstractFactory abstractFactory=FactoryProducer.getFactory("Color");
Color color=abstractFactory.getColor("red");
color.fill();
}
}
Structural Pattern
1. Facade Pattern:
It hides the complexities of the system and provides an interface to the client from where the client can access the system.
In the Facade pattern, there will be a layer that will have the system information and we communicate from this layer.
Let's take an example
You go to the mobile shop and you ask for the Samsung price Shopkeeper will give you the information which he will have
Same for the other phone as well let's say iPhone
Here the layer is Shopkeeper from which we are getting the information about the products.
See in action:
There is an interface?Phone?which has 2 methods
public interface Phone {
void modelName();
void modelPrice();
}
There are 2 classes currently(Can be more) that are implementing this interface
1 is?iPhone
public class Iphone implements Phone {
@Override
public void modelName() {
System.out.println("Iphone");
}
@Override
public void modelPrice() {
System.out.println("6000");
}
}
2nd is?Samsung
public class Samsung implements Phone {
@Override
public void modelName() {
System.out.println("Samsung");
}
@Override
public void modelPrice() {
System.out.println("1000");
}
}
Here will be a shopkeeper who will have all the information about the products
public class ShopKeeper {
Iphone iphone;
Samsung samsung;
public ShopKeeper() {
this.iphone = new Iphone();
this.samsung = new Samsung();
}
public void getIphoneDetail(){
iphone.modelName();
iphone.modelPrice();
}
public void getSamsungDetail(){
samsung.modelName();
samsung.modelPrice();
}
}
Customers want information about products from?shopkeepers
public class Customer {
public static void main(String[] args) {
ShopKeeper shopKeeper = new ShopKeeper();
shopKeeper.getIphoneDetail();
}
}
2. Composite Pattern:
Composite Pattern says to structure objects in such a way as a tree where common properties can be used from top to bottom.
In a company, all the employees will have some common structures like
empId, salary, Department. etc
Let's see the example:
We will have an?Employee?class where we have a common structure of A Employee
public interface Employee {
String getName();
int getId();
String getDepartment();
}
Now we can have?HRs
public class HRs implements Employee {
private String name;
private int id;
private String department;
public HRs(String name, int id, String department) {
this.name = name;
this.id = id;
this.department = department;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDepartment() {
return this.department;
}
@Override
public String toString() {
return "HRs{" +
"name='" + name + '\'' +
", id=" + id +
", department='" + department + '\'' +
'}';
}
}
We can have the Engineers
public class Engineer implements Employee {
private String name;
private int id;
private String department;
public Engineer(String name, int id, String department) {
this.name = name;
this.id = id;
this.department = department;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDepartment() {
return this.department;
}
@Override
public String toString() {
return "Engineer{" +
"name='" + name + '\'' +
", id=" + id +
", department='" + department + '\'' +
'}';
}
}
We can have the salesPerson
public class SalesMan implements Employee {
private String name;
private int id;
private String department;
public SalesMan(String name, int id, String department) {
this.name = name;
this.id = id;
this.department = department;
}
@Override
public String getName() {
return this.name;
}
@Override
public int getId() {
return this.id;
}
@Override
public String getDepartment() {
return this.department;
}
@Override
public String toString() {
return "SalesMan{" +
"name='" + name + '\'' +
", id=" + id +
", department='" + department + '\'' +
'}';
}
}
Now in the Company, we have a list of employees as below:
public class Company {
public static void main(String[] args) {
List<Employee> employees = new ArrayList();
Employee eng = new Engineer("Aalishan", 1, "Tech");
Employee sales = new SalesMan("Santosh", 2, "Sale");
Employee hr = new HRs("Abc", 3, "Hr");
employees.add(eng);
employees.add(sales);
employees.add(hr);
for (Employee e : employees
) {
System.out.println(e.toString());
}
}
}
Behavioural Pattern
- Observer pattern:
Sometimes we want to receive a notification when some event happens someplace. so for that, we need to register ourselves to get notified, In simple words, we want to observe some data at some event occurrence.
Suppose we are building a cricket app that notifies viewers about information such as current score, run rate, etc. Suppose we have made two display elements CurrentScoreDisplay and AverageScoreDisplay. CricketData has all the data (runs, bowls, etc.), and whenever data changes the display elements are notified with new data and they display the latest data accordingly.
Let's take an example of the Observer pattern
We have a?Subject?that will have:
public interface Subject {
void register(Observer observer);
void unRegister(Observer observer);
void notifyMessage(Message message);
}
We have?Observer?Interface
public interface Observer {
void updateMessage(Message message);
}
We have?Message?to publish
public class Message {
private final String message;
Message(String msg) {
message = msg;
}
public String getMessage() {
return message;
}
}
We have a?MessagePublisher
public class MessagePublisher implements Subject {
// Collection of observers
private Set<Observer> observers = new HashSet<>();
@Override
public void register(Observer observer) {
observers.add(observer);
}
@Override
public void unRegister(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyMessage(Message message) {
for (Observer one : observers
) {
one.updateMessage(message);
}
}
}
Now 1st?Subscriber
public class FirstSubscriber implements Observer {
@Override
public void updateMessage(Message message) {
System.out.println(message.getMessage());
}
}
Now 2nd?Subscriber
public class SecondSubscriber implements Observer {
@Override
public void updateMessage(Message message) {
System.out.println(message.getMessage());
}
}
Now we need a test class which will have:
public class ObserverTest {
public static void main(String[] args) {
FirstSubscriber firstMessageSubscriber = new FirstSubscriber();
SecondSubscriber secondMessageSubscriber = new SecondSubscriber();
ThirdSubscriber thirdMessageSubscriber = new ThirdSubscriber();
//Creating Message Publisher
MessagePublisher messagePublisher = new MessagePublisher();
//Register two Subscribers to get notifications on any update
messagePublisher.register(firstMessageSubscriber);
messagePublisher.register(secondMessageSubscriber);
messagePublisher.register(thirdMessageSubscriber);
//firstMessageSubscriber and secondMessageSubscriber will receive the update
messagePublisher.notifyMessage(new Message("This is First Message"));
System.out.println("---------------------------------------------------------------");
}
}
============================================================
SD3 - Android at FloBiz | Ex-ZebPay | Ex-OpenxCell | Building myBillBook at FloBiz
1 å¹´Insightful