Behavioral Design Patterns মূলত বিভিন্ন object এর মধ্যে যোগাযোগ এবং interaction এর পদ্ধতি নির্ধারণ করে। এই patterns গুলো objects গুলোর মধ্যে দায়িত্ব বন্টন, কাজ ভাগ করে দেওয়া, এবং কীভাবে তারা একে অপরের সাথে কাজ করবে সেটা ঠিক করতে সাহায্য করে।
প্রথম পার্টে ৫ টি Behavioral Design Patterns নিয়ে কথা বলবো। এগুলো হলো -
১। Strategy Design Pattern
২। Null Object Design Pattern
৩। Command Design Pattern
৪। Iterator Design pattern
৫। Template Method Design Pattern
Strategy Design Pattern
একটা সমস্যা কল্পনা করা যাক। ধরি একটি class আছে যার নাম৷ Vehicle। সেই ক্লাসে একটি drive method আছে। এই ক্লাস হচ্ছে একটি এবস্ট্রাকট বা সুপার ক্লাস। এর তিনটি সাব ক্লাস আছে। ১. PassengerVehicle ২. OffRoadVehicle ৩. SportsVehicle
এখন PassengerVehicle এর তার সুপার ক্লাস এর drive মেথড কে অভাররাইড করতে হয় না। কিন্তু OffRoadVehicle এবং SportsVehicle কে তার সুপার ক্লাসের drive মেথড কে অভাররাইড করতে হয়।
// Base class
class Vehicle {
drive() {
console.log("Driving at a speed of 60 km/h");
}
}
// Subclass 1: PassengerVehicle
class PassengerVehicle extends Vehicle {
// No need to override the drive() method as the base implementation is suitable
}
// Subclass 2: OffRoadVehicle
class OffRoadVehicle extends Vehicle {
drive() {
console.log("Driving off-road at a speed of 30 km/h");
}
}
// Subclass 3: SportsVehicle
class SportsVehicle extends Vehicle {
drive() {
console.log("Driving at a high speed of 120 km/h");
}
}
// Creating instances
const passengerVehicle = new PassengerVehicle();
const offRoadVehicle = new OffRoadVehicle();
const sportsVehicle = new SportsVehicle();
// Calling the drive method
passengerVehicle.drive(); // Output: Driving at a speed of 60 km/h
offRoadVehicle.drive(); // Output: Driving off-road at a speed of 30 km/h
sportsVehicle.drive(); // Output: Driving at a high speed of 120 km/h
এই পর্যন্ত সমস্যা নেই।
খন ধরি আমাদের PassengerVehicle এবং SportsVehicle এর একটি কমন মেথড দরকার যেমন changeFuelToElectric()। কিন্তু এইটা আমরা সুপার ক্লাসে রাখতে পারবো না যেহেতু OffroadVehicle এই মেথড ব্যাবহার করবে না। এখন যদি আমরা আরো ২০ টা সাব ক্লাস বানাই এবং তাদের এই মেথড লাগে তাহলে প্রত্যেক ক্লাসে এই মেথড লিখতে হবে, যা কোড ডুপ্লিকেট এবং স্কেলেবল সলুশন না।
এখানে আসে Strategy Pattern।
এই ক্ষেত্রে আমরা প্রথমে স্ট্র্যাটেজি ক্লাস বানাবো।
// Strategy interface
class FuelStrategy {
changeFuel() {
throw new Error("This method should be overridden");
}
}
// Concrete strategy for electric fuel
class ElectricFuelStrategy extends FuelStrategy {
changeFuel() {
console.log("Changing fuel to electric...");
}
}
এরপর আমাদের সুপার ক্লাসে performChangeFuel() মেথড ইম্পলিমেন্ট করবো, এবং কন্সট্রাক্টরে আমাদের স্ট্র্যাটেজি পাস করবো।
// Vehicle superclass
class Vehicle {
constructor(fuelStrategy = null) {
this.fuelStrategy = fuelStrategy;
}
drive() {
console.log("Driving at 60 km/h");
}
performChangeFuel() {
if (this.fuelStrategy) {
this.fuelStrategy.changeFuel();
} else {
console.log("This vehicle does not support changing fuel.");
}
}
}
এখন আমাদের সাব ক্লাস বানানোর সময় যেই ক্লাসে এই মেথড দরকার সেখানে স্ট্র্যাটেজিকে পাস করবো।
// Strategy interface
class FuelStrategy {
changeFuel() {
throw new Error("This method should be overridden");
}
}
// Concrete strategy for electric fuel
class ElectricFuelStrategy extends FuelStrategy {
changeFuel() {
console.log("Changing fuel to electric...");
}
}
// Vehicle superclass
class Vehicle {
constructor(fuelStrategy = null) {
this.fuelStrategy = fuelStrategy;
}
drive() {
console.log("Driving at 60 km/h");
}
performChangeFuel() {
if (this.fuelStrategy) {
this.fuelStrategy.changeFuel();
} else {
console.log("This vehicle does not support changing fuel.");
}
}
}
// Subclass for PassengerVehicle
class PassengerVehicle extends Vehicle {
constructor() {
super(new ElectricFuelStrategy()); // Injecting the electric fuel strategy
}
}
// Subclass for SportsVehicle
class SportsVehicle extends Vehicle {
constructor() {
super(new ElectricFuelStrategy()); // Injecting the electric fuel strategy
}
drive() {
console.log("Driving at 200 km/h");
}
}
// Usage
const passengerVehicle = new PassengerVehicle();
passengerVehicle.drive(); // Output: Driving at 60 km/h
passengerVehicle.performChangeFuel(); // Output: Changing fuel to electric...
const sportsVehicle = new SportsVehicle();
sportsVehicle.drive(); // Output: Driving at 200 km/h
sportsVehicle.performChangeFuel(); // Output: Changing fuel to electric...
কখন ব্যাবহার করবো?
১. যখন ক্লাস এ multiple behaviour থাকবে, এবং আমরা if else logic ব্যাবহার করতে চাই না। উপরোক্ত সমস্যা যদিও if else দিয়ে সমাধান করা যেত, কিন্তু অনেক বেশি মেথড হলে তা কম্পলেক্স হয়ে যেত।
২. যখন রানটাইমে ক্লাসের বিহেইভিওর চেইঞ্জ করতে চাই। যেমন setStrategy নামে আরেকটি মেথড এড করে আমরা ক্লাস ইনিসিয়ালাইজ এর পরেও স্ট্রাটেজি চেইঞ্জ করতে পারে।
৩. যখন SOLID এর Open/Closed প্রিন্সিপাল অর্থাৎ class is open for extension, closed for modification মেইন্টেইন করতে চাই।
Null Object Design Pattern
ধরাযাক একটি Class আছে যার নাম হচ্ছে Main। এই Main ক্লাসের একটি static method আছে যার নাম হচ্ছে print। এই মেথড এর কাজ হচ্ছে ইনপুট হিসেবে একটি Vehicle এর নাম নেওয়া এবং সেই অনুযায়ী VehicleFactory class এর সাহায্যে নতুন Vehicle Instance তৈরি করা এবং এরপর নিজের আরেকটি মেথড printVehicleDetails(vehicleClass) কে কল করা।
// Vehicle Interface
class Vehicle {
getSeatingCapacity() {
throw new Error("This method should be overridden");
}
getTankCapacity() {
throw new Error("This method should be overridden");
}
}
// Concrete Vehicle Classes
class Car extends Vehicle {
getSeatingCapacity() {
return 5;
}
getTankCapacity() {
return 50;
}
}
class Bike extends Vehicle {
getSeatingCapacity() {
return 2;
}
getTankCapacity() {
return 15;
}
}
// Vehicle Factory
class VehicleFactory {
static createVehicle(type) {
if (type === "Car") {
return new Car();
} else if (type === "Bike") {
return new Bike();
} else {
return null;
}
}
}
// Main Class
class Main {
static print(vehicleType) {
const vehicleClass = VehicleFactory.createVehicle(vehicleType);
this.printVehicleDetails(vehicleClass);
}
static printVehicleDetails(vehicleClass) {
if (vehicleClass) {
console.log(`Seating Capacity: ${vehicleClass.getSeatingCapacity()}`);
console.log(`Tank Capacity: ${vehicleClass.getTankCapacity()}`);
} else {
console.log("Invalid vehicle type provided.");
}
}
}
// Example usage
Main.print("Car"); // Output: Seating Capacity: 5, Tank Capacity: 50
Main.print("Bike"); // Output: Seating Capacity: 2, Tank Capacity: 15
Main.print("Truck"); // Output: Invalid vehicle type provided.
কিন্তু এখানে একটা সমস্যা হচ্ছে, ক্লাইন্ট যদি car অথবা বাইলের পরিবর্তে অন্য স্ট্রিং দিয়ে দেয় তাহলে Main.print(“ any other string”) তাহলে ইরোর আসবে, কারন print method e vehicleClass null হবে এবং this.printVehicleDetails() এ vehicleClass ও null হবে এবং null.getTankCapacity() error throw করবে। এরজন্য কল করার আগে আমাদের vehicleClass null কিনা তা চেক করতে হয়। এখন যত যায়গায়ই এর ইন্সট্যান্স তৈরি ও ব্যাবহার হবে সব জায়গায় null চেক করতে হবে।
এক্ষেত্রে একটা সমাধান হচ্ছে Null object pattern অর্থাৎ null return এর পরিবর্তে Null object রিটার্ন করলেই error throw করবে না।
// Vehicle Interface
class Vehicle {
getSeatingCapacity() {
throw new Error("This method should be overridden");
}
getTankCapacity() {
throw new Error("This method should be overridden");
}
}
// Concrete Vehicle Classes
class Car extends Vehicle {
getSeatingCapacity() {
return 5;
}
getTankCapacity() {
return 50;
}
}
class Bike extends Vehicle {
getSeatingCapacity() {
return 2;
}
getTankCapacity() {
return 15;
}
}
// Null Object Class
class NullVehicle extends Vehicle {
getSeatingCapacity() {
return 0; // Default or "do-nothing" behavior
}
getTankCapacity() {
return 0; // Default or "do-nothing" behavior
}
}
// Vehicle Factory
class VehicleFactory {
static createVehicle(type) {
if (type === "Car") {
return new Car();
} else if (type === "Bike") {
return new Bike();
} else {
return new NullVehicle(); // Returning Null Object instead of null
}
}
}
// Main Class
class Main {
static print(vehicleType) {
const vehicleClass = VehicleFactory.createVehicle(vehicleType);
this.printVehicleDetails(vehicleClass);
}
static printVehicleDetails(vehicleClass) {
console.log(`Seating Capacity: ${vehicleClass.getSeatingCapacity()}`);
console.log(`Tank Capacity: ${vehicleClass.getTankCapacity()}`);
}
}
// Usage
Main.print("Car");
// Output:
// Seating Capacity: 5
// Tank Capacity: 50
Main.print("Bike");
// Output:
// Seating Capacity: 2
// Tank Capacity: 15
Main.print("Truck");
// Output:
// Seating Capacity: 0
// Tank Capacity: 0
কখন ব্যাবহার করবো?
১. যখন বার বার null চেক করতে চাই না।
২. যখন ডিফোল্ট কোনো বিহেইভিয়ার সেট করতে চাই। এবং কোডে কন্সিটেন্সি আনতে চাই।
Command Design Pattern
চিন্তা করুন আপনার একটি AC এর রিমোট আছে। এবং এটি কিছু কাজ করে যেমন turnOn, turnOff, setTemperature। যদি এটাকে একটি ক্লাসের মাধ্যমে রিপ্রেজেন্ট করি তাহলে দারায়-
class AirConditioner {
constructor() {
this.isOn = false; // Initial state of the AC is OFF
this.temperature = 24; // Default temperature
}
turnOn() {
this.isOn = true;
console.log("The AC is now ON.");
}
turnOff() {
this.isOn = false;
console.log("The AC is now OFF.");
}
setTemperature(temp) {
this.temperature = temp;
console.log(`The temperature is set to ${temp}°C.`);
}
}
// Example usage
const myAC = new AirConditioner();
myAC.turnOn(); // Output: The AC is now ON.
myAC.setTemperature(22); // Output: The temperature is set to 22°C.
myAC.turnOff(); // Output: The AC is now OFF.
এখানে সমস্যা হচ্ছে, ক্লাইন্টকে এই মেথড গুলোর নাম জানতে হবে। এবং এখানে undo, redo করা সম্ভব না। Command Design Pattern এর মাধ্যমে এই সমস্যা সমাধান করা সম্ভব।
এর জন্য প্রথমে একটি কমান্ড ইন্টারফেস বানাবো
// Command Interface
class Command {
execute() {
throw new Error("This method should be overridden");
}
undo() {
throw new Error("This method should be overridden");
}
}
এরপর কনক্রিট কমান্ড ক্লাস লিখবো
// Concrete Command Class to turn on the Air Conditioner
class TurnOnCommand extends Command {
constructor(ac) {
super();
this.ac = ac; // The air conditioner instance to control
}
execute() {
this.ac.turnOn(); // Execute the command to turn on the AC
}
undo() {
this.ac.turnOff(); // Undo the command to turn off the AC
}
}
// Concrete Command Class to turn off the Air Conditioner
class TurnOffCommand extends Command {
constructor(ac) {
super();
this.ac = ac; // The air conditioner instance to control
}
execute() {
this.ac.turnOff(); // Execute the command to turn off the AC
}
undo() {
this.ac.turnOn(); // Undo the command to turn on the AC
}
}
// Concrete Command Class to set the temperature of the Air Conditioner
class SetTemperatureCommand extends Command {
constructor(ac, temperature) {
super();
this.ac = ac; // The air conditioner instance to control
this.temperature = temperature; // The desired temperature to set
this.prevTemperature = ac.temperature; // Store the previous temperature for undo
}
execute() {
this.ac.setTemperature(this.temperature); // Execute the command to set the temperature
}
undo() {
this.ac.setTemperature(this.prevTemperature); // Undo by resetting to the previous temperature
}
}
এরপর ইনভোকার ক্লাস লিখব
// Invoker Class for executing commands
class RemoteControl {
constructor() {
this.commandStack = []; // Stack to keep track of executed commands
}
// Set the command to be executed
setCommand(command) {
this.command = command;
}
// Execute the command and store it in the stack
pressButton() {
this.command.execute(); // Execute the current command
this.commandStack.push(this.command); // Store the executed command for undo
}
// Undo the last executed command
pressUndo() {
if (this.commandStack.length > 0) {
const lastCommand = this.commandStack.pop(); // Get the last command from the stack
lastCommand.undo(); // Call the undo method of the last executed command
} else {
console.log("Nothing to undo."); // Message if there's nothing to undo
}
}
}
এবং ব্যাবহার করবো
// Main execution
const ac = new AirConditioner();
const remote = new RemoteControl();
// Create command instances
const turnOn = new TurnOnCommand(ac);
const turnOff = new TurnOffCommand(ac);
const setTemp = new SetTemperatureCommand(ac, 22);
// Use the remote to execute commands
remote.setCommand(turnOn);
remote.pressButton(); // Output: The AC is now ON.
remote.setCommand(setTemp);
remote.pressButton(); // Output: The temperature is set to 22°C.
remote.setCommand(turnOff);
remote.pressButton(); // Output: The AC is now OFF.
// Using the undo functionality
remote.pressUndo(); // Output: The AC is now ON.
remote.pressUndo(); // Output: The temperature is set to 24°C.
remote.pressUndo(); // Output: The AC is now OFF.
এই প্যাটার্ন বিভিন্ন ক্ষেত্রে ইউজ করা হতে পারে বিশেষ করে Undo/redo functionality এর প্রয়োজন হলে।
Iterator Design Pattern
এই ডিজাইন প্যাটার্ন ব্যাবহার করে elements এর উপর ইটারেট করা সম্ভব হয়। জাবাস্ক্রিপ্ট এও এই প্যাটার্ন ব্যাবহার হয়। এর সাহায্যে one by one elements এর এক্সেস পাওয়া যায়। জাবাস্ক্রিপ্ট এ ইটারেটর এর উদাহরণ -
const array = [10, 20, 30]; // Create an array
const iterator = array[Symbol.iterator](); // Get the iterator for the array
// Iterate through the array using the iterator
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
এখানে জাবাস্ক্রিপ্ট তার এরে কে কোন ডাটা স্ট্রাকচার এ ইন্টারনালি রেখেছে বা কিভাবে .next() ইম্পলিমেন্ট করেছে তার জ্ঞান ছাড়াই আমরা এটি ব্যাবহার করতে পারি। আরো কিছু উদাহরণ -
// Using an iterator with a string
const str = "hello"; // Create a string
const stringIterator = str[Symbol.iterator](); // Get the iterator for the string
console.log(stringIterator.next()); // { value: 'h', done: false }
console.log(stringIterator.next()); // { value: 'e', done: false }
// Using an iterator with a Map
const map = new Map(); // Create a Map
map.set('a', 1); // Set key-value pairs
map.set('b', 2);
const mapIterator = map[Symbol.iterator](); // Get the iterator for the Map
console.log(mapIterator.next()); // { value: [ 'a', 1 ], done: false }
console.log(mapIterator.next()); // { value: [ 'b', 2 ], done: false }
এই built in iterator, Iterator deisgn pattern ইউজ করেই ইম্পলিমেন্ট করা হয়েছে। এই প্যাটার্ন এর চারটি কম্পনেন্ট আছে। এই প্যাটার্ন এর চারটি কম্পনেন্ট আছে।
১ . Abstract Iterator
২. Concrete Iterator
৩. Aggregate Class
৪. Concrete Aggregate
// Abstract Iterator
class Iterator {
next() {
throw new Error("Method 'next()' must be implemented.");
}
hasNext() {
throw new Error("Method 'hasNext()' must be implemented.");
}
}
// Concrete Iterator
class ConcreteIterator extends Iterator {
constructor(collection) {
super();
this.collection = collection;
this.index = 0; // Start from the first element
}
next() {
if (this.hasNext()) {
return this.collection[this.index++];
}
return null; // Return null if no next element
}
hasNext() {
return this.index < this.collection.length; // Check if more elements exist
}
}
// Abstract Aggregate Class
class Aggregate {
createIterator() {
throw new Error("Method 'createIterator()' must be implemented.");
}
}
// Concrete Aggregate Class
class ConcreteAggregate extends Aggregate {
constructor(collection) {
super();
this.collection = collection; // Store the collection
}
createIterator() {
return new ConcreteIterator(this.collection); // Create a ConcreteIterator for the collection
}
}
// Creating a collection
const numbers = [1, 2, 3, 4, 5];
// Creating a Concrete Aggregate instance
const aggregate = new ConcreteAggregate(numbers);
// Getting the iterator
const iterator = aggregate.createIterator();
// Traversing the collection
while (iterator.hasNext()) {
console.log(iterator.next()); // Output: 1, 2, 3, 4, 5
}
Template Method Design Pattern
এটা খুবই কমনলি ইউজ প্যাটার্ন যেখানে সুপার ক্লাসে এটি blue print methods গুলো দিয়ে দেয় এবং এর সাব ক্লাসকে সেগুলো নিজেদের মত ইম্পলিমেন্টেশন করতে এলাও করে।
// Abstract class
class Payment {
processPayment() {
this.validate();
this.debit();
this.calculateFee(); // This will call the overridden method
this.credit();
}
validate() {
console.log("Validating payment...");
}
debit() {
console.log("Debiting amount...");
}
// This method will be overridden by subclasses
calculateFee() {
throw new Error("Method 'calculateFee()' must be implemented.");
}
credit() {
console.log("Crediting amount...");
}
}
// Subclass for peer-to-peer payment
class PayToFriend extends Payment {
calculateFee() {
console.log("No fees for friends.");
}
}
// Subclass for merchant payment
class PayToMerchant extends Payment {
calculateFee() {
console.log("Calculating fee: 2% of the amount.");
}
}
// Usage
const friendPayment = new PayToFriend();
friendPayment.processPayment();
// Output:
// Validating payment...
// Debiting amount...
// No fees for friends.
// Crediting amount...
const merchantPayment = new PayToMerchant();
merchantPayment.processPayment();
// Output:
// Validating payment...
// Debiting amount...
// Calculating fee: 2% of the amount.
// Crediting amount...
এই হচ্ছে behavioral pattern গুলোর মধ্যে অন্যতম ৫ টি প্যাটার্ন।
Real World Usage :-
Strategy Design Pattern : Payment processing systems , Sorting algorithms, File compression . যখন একটা মেথড এর ডিফারেন্ট ডিফারেন্ট ইমপ্লিমেন্টেশন থাকতে পারে এবং সব জায়গায় নাও লাগতে পারে তখন এই স্ট্রাটেজি ব্যাবহার করা যায়।
Null Object Design Pattern : যখনই Null check এভয়েড করতে চাই এবং ডিফোল্ট বিহেইভিয়র সেট করতে চাই ।
Command Design Pattern : Undo/redo functionality, Remote control operations , এমন কোনো সিচুয়েসন যেখানে কমান্ড এক্সিকিউট স্টোর অথবা রিভার্স করার অপসন থাকা লাগবে ।
Iterator Design Pattern : যখন ইন্টারনাল ডিটেইলস এক্সপোজ না করে কোনো কালেকশন এ ইটারেট ইমপ্লিমেন্ট করতে হবে ।
Template Method Design Pattern : যখন এমন একটি মেথড ইমপ্লিমেন্ট করতে বলা হয় যেখানে কিছু method fixed থাকে এবং বাকি method sসাবক্লাস দ্বারা ওভাররাইড করা যাবে।
