When it comes to writing code, there are a lot of factors to consider. You want your code to be efficient, scalable, and easy to maintain. One way to achieve these goals is by using design patterns.
Design patterns are proven solutions to recurring problems in software design. They provide a template for solving a particular problem that can be adapted to different situations. Design patterns can help you write code that is more modular, easier to understand, and less prone to errors. In OOP Paradigm, Design patterns are reusable solutions to common software design problems. Object-oriented patterns are design patterns specifically geared towards object-oriented programming.
Object-oriented patterns can be divided into several categories based on their purpose and scope. Some common categories include:
Singleton Pattern – Ensures that only one instance of a class exists in the system and provides a global point of access to it.
class Singleton private constructor() { companion object { private var instance: Singleton? = null fun getInstance(): Singleton { if (instance == null) { instance = Singleton() } return instance as Singleton } } }
In this Kotlin implementation, the Singleton class has a private constructor so that it cannot be instantiated outside the class. The companion object
is used to hold the singleton instance. The getInstance()
method is a static method that provides access to the singleton instance. It checks if the instance has been created, and if it hasn’t, it creates a new instance. Then, it returns the instance.
You can use the Singleton pattern like this:
val s1 = Singleton.getInstance() val s2 = Singleton.getInstance() println(s1 === s2) // Output: true
In this example, s1
and s2
are both references to the same instance of the Singleton class. Because the getInstance
method always returns the same instance, the ===
operator returns true
, indicating that s1
and s2
are the same object.
Factory Pattern – Allows you to create objects without exposing the instantiation logic to the client.
interface Shape { fun draw() } class Circle : Shape { override fun draw() { println("Circle.draw") } } class Rectangle : Shape { override fun draw() { println("Rectangle.draw") } } class ShapeFactory { companion object { fun getShape(shapeType: String?): Shape? { return when (shapeType?.toLowerCase()) { "circle" -> Circle() "rectangle" -> Rectangle() else -> null } } } }
In this example, we have an interface called Shape
that defines a draw
method. We also have two classes that implement the Shape
interface: Circle
and Rectangle
. Finally, we have a ShapeFactory
class that creates instances of Shape
subclasses based on a given shapeType
string.
The ShapeFactory
class has a static getShape
method that takes a shapeType
string parameter and returns a corresponding Shape
instance. If the shapeType
is "circle"
, it returns a Circle
instance. If the shapeType
is "rectangle"
, it returns a Rectangle
instance. Otherwise, it returns null
.
You can use the ShapeFactory
like this:
val circle = ShapeFactory.getShape("circle") circle?.draw() // Output: Circle.draw val rectangle = ShapeFactory.getShape("rectangle") rectangle?.draw() // Output: Rectangle.draw val square = ShapeFactory.getShape("square") square?.draw() // Output: null
In this example, we use the ShapeFactory
to create instances of Circle
and Rectangle
objects by passing the corresponding shape type strings to the getShape
method. We then call the draw
method on each object to verify that the correct shape is drawn. Finally, we pass an invalid shape type string to the getShape
method, which returns null
.
Builder Pattern – Provides a flexible way to create complex objects by separating the construction of an object from its representation.
Example:
class Person private constructor( val firstName: String, val lastName: String, val age: Int, val address: String, val phoneNumber: String ) { class Builder { private var firstName: String = "" private var lastName: String = "" private var age: Int = 0 private var address: String = "" private var phoneNumber: String = "" fun setFirstName(firstName: String): Builder { this.firstName = firstName return this } fun setLastName(lastName: String): Builder { this.lastName = lastName return this } fun setAge(age: Int): Builder { this.age = age return this } fun setAddress(address: String): Builder { this.address = address return this } fun setPhoneNumber(phoneNumber: String): Builder { this.phoneNumber = phoneNumber return this } fun build(): Person { return Person(firstName, lastName, age, address, phoneNumber) } } }
In this example, we have a Person
class that represents a person’s details. The Person
class has a private constructor that takes five parameters: firstName
, lastName
, age
, address
, and phoneNumber
.
We also have a Builder
class that is nested inside the Person
class. The Builder
class has methods to set each of the Person
class’s fields and a build
method that constructs a new Person
instance with the given field values.
You can use the Builder
pattern like this:
val person = Person.Builder() .setFirstName("John") .setLastName("Doe") .setAge(30) .setAddress("123 Main St") .setPhoneNumber("555-1234") .build()
In this example, we use the Builder
to construct a new Person
instance with the given field values. We call each of the set
methods on the Builder
instance to set the corresponding field value. Finally, we call the build
method to create a new Person
instance with the given field values.
This pattern provides a clean and easy-to-use way to construct objects with many fields or optional fields, without requiring multiple constructors or constructor overloading.
Observer Pattern – Allows an object to notify other objects when its state changes, without having to know which objects need to be notified in advance.
The Observer pattern is a design pattern that establishes a one-to-many dependency between objects so that when one object changes its state, all its dependents are notified and updated automatically. In other words, it allows an object to notify other objects when its state changes, without having to know which objects need to be notified in advance.
The Observer pattern consists of two main components: the Subject (or Observable) and the Observer. The Subject maintains a list of its dependents (Observers) and notifies them automatically of any state changes, while the Observers register themselves with the Subject and receive notifications when the Subject’s state changes.
Here’s an example of the Observer pattern :
interface Observer { fun update(state: String) } interface Subject { fun registerObserver(observer: Observer) fun removeObserver(observer: Observer) fun notifyObservers() } class WeatherStation : Subject { private val observers = mutableListOf<Observer>() private var temperature: Float = 0.0f fun setTemperature(temperature: Float) { this.temperature = temperature notifyObservers() } override fun registerObserver(observer: Observer) { observers.add(observer) } override fun removeObserver(observer: Observer) { observers.remove(observer) } override fun notifyObservers() { val state = "Current temperature is $temperature degrees Celsius" observers.forEach { it.update(state) } } } class PhoneDisplay : Observer { override fun update(state: String) { println("Phone display updated: $state") } } class TabletDisplay : Observer { override fun update(state: String) { println("Tablet display updated: $state") } }
In this example, we have a WeatherStation
class that represents a weather station that measures and reports the temperature. The WeatherStation
implements the Subject
interface, which defines methods for registering, removing, and notifying observers.
We also have two classes that implement the Observer
interface: PhoneDisplay
and TabletDisplay
. These classes represent displays that show the current temperature reported by the WeatherStation
.
When the WeatherStation
updates its temperature, it notifies all of its observers by calling their update
method with the current temperature state. The observers then update their displays with the new temperature state.
Here’s an example usage of the Observer pattern with our WeatherStation
, PhoneDisplay
, and TabletDisplay
classes:
val weatherStation = WeatherStation() val phoneDisplay = PhoneDisplay() val tabletDisplay = TabletDisplay() weatherStation.registerObserver(phoneDisplay) weatherStation.registerObserver(tabletDisplay) weatherStation.setTemperature(25.0f) // Output: // Phone display updated: Current temperature is 25.0 degrees Celsius // Tablet display updated: Current temperature is 25.0 degrees Celsius weatherStation.removeObserver(phoneDisplay) weatherStation.setTemperature(20.0f) // Output: // Tablet display updated: Current temperature is 20.0 degrees Celsius
In this example, we create a WeatherStation
, a PhoneDisplay
, and a TabletDisplay
. We register both displays as observers of the WeatherStation
. When the WeatherStation
updates its temperature, it notifies both displays, which update their displays accordingly. We then remove the PhoneDisplay
observer from the WeatherStation
, and when the WeatherStation
updates its temperature again, only the TabletDisplay
observer is notified.
Strategy Pattern – Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.
Decorator Pattern – Allows you to add behavior to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
Adapter Pattern – Allows you to convert the interface of a class into another interface that clients expect.
These design patterns are just a few of the many that exist. By learning and applying these patterns to your code, you can write more efficient, scalable, and maintainable software.
In addition to these patterns, it’s important to remember that design patterns are not a silver bullet. They should be used judiciously and in the right context. Overuse of design patterns can lead to code that is overly complex and hard to understand.
Hi there, ChatGPT is very familiar and become the hottest technology generation now a days.…
ES6, also known as ECMAScript 2015, is a major update to the JavaScript language that…
In today's fast-paced business environment, the ability to quickly respond to changing market conditions and…
Sangat umum dijumpai bahwa developer tidak menggunakan arsitektur formal, tanpa arsitektur yang jelas dan terdefinisi…
Keunggulan development team adalah yang selalu update terhadap perkembangan teknologi, trend pengembangan terbaru dan pada…
Database Management System atau kerap kali disebutkan dengan DMBS ini adalah software/tools yang digunakan untuk…