Structural Design Patterns
Structural Design Patterns এমন Design Pattern যা বিভিন্ন class এবং object এর মধ্যে সম্পর্ক গঠন এবং তাদের সহজে কাজ করার উপায় নিয়ে কাজ করে। এর মূল উদ্দেশ্য হলো objects এবং classes এর মধ্যে সহজে সংযোগ স্থাপন করা, যাতে code গুলো আরো flexible, reusable এবং maintainable হয়। Structural patterns system এর structure এবং organization কে optimize করে।
Structural Design Patterns এর ধরন:
১। Adapter Design Pattern
২। Bridge Design Pattern
৩। Composite Design Pattern
৪। Decorator Design Pattern
৫। Facade Design Pattern
৬। Flyweight Design Pattern
৭। Proxy Design Pattern
Adapter Design Pattern
Adapter Design Pattern হল Structural Design Pattern-এর একটি ধরন। এই প্যাটার্নটি ব্যবহার করা হয় যখন আমরা এমন দুটি incompatible interfaces এর মধ্যে সংযোগ স্থাপন করতে চাই, যেগুলো সরাসরি একে অপরের সাথে কাজ করতে পারে না। Adapter মূলত দুটি ভিন্ন interface-কে একসাথে কাজ করতে সাহায্য করে, যাতে কোনো বড় পরিবর্তন ছাড়াই তাদের মধ্যে compatibility আনা যায়।
ধরুন, আপনি একটি নতুন third-party library ব্যবহার করতে চান, কিন্তু সেই library-এর interface আপনার বিদ্যমান কোডের সাথে মেলে না। পুরো কোড নতুন করে লেখার বদলে, আপনি Adapter Design Pattern ব্যবহার করে সহজে এই সমস্যার সমাধান করতে পারেন।
// Step 1: পুরোনো interface বা class
class OldPrinter {
public print(text: string): void {
console.log(`Printing: ${text}`);
}
}
// Step 2: নতুন interface
interface AdvancedPrinter {
printColor(text: string, color: string): void;
}
// Step 3: নতুন class, যা নতুন feature handle করে
class ColorPrinter implements AdvancedPrinter {
public printColor(text: string, color: string): void {
console.log(`Printing in ${color}: ${text}`);
}
}
// Step 4: Adapter class, যা OldPrinter এবং AdvancedPrinter এর মধ্যে bridge হিসাবে কাজ করবে
class PrinterAdapter implements AdvancedPrinter {
private oldPrinter: OldPrinter;
constructor(oldPrinter: OldPrinter) {
this.oldPrinter = oldPrinter;
}
public printColor(text: string, color: string): void {
// Adapter পুরনো print method কে call করছে
console.log(`Converting color print to normal print...`);
this.oldPrinter.print(text);
}
}
// Step 5: Client code
const oldPrinter = new OldPrinter();
const adapter = new PrinterAdapter(oldPrinter);
// পুরনো printer interface নতুন Printer interface এর সাথে কাজ করছে
adapter.printColor("Hello, World!", "Red");
Output
Converting color print to normal print...
Printing: Hello, World!
উদাহরণে, আমরা একটি পুরোনো OldPrinter class দেখছি, যা শুধু সাধারণ text print করতে পারে। তবে আমাদের নতুন requirement অনুযায়ী, আমরা color print করতে চাই। এর জন্য আমরা AdvancedPrinter interface এবং ColorPrinter class তৈরি করেছি। তবে OldPrinter class পরিবর্তন না করেই PrinterAdapter class ব্যবহার করেছি, যা OldPrinter এবং AdvancedPrinter এর মধ্যে bridge তৈরি করছে। এই adapter নতুন printColor method কে পুরোনো print method এর সাথে মেলাচ্ছে।
Adapter প্রয়োজন:
- যখন দুটি ভিন্ন interface একসাথে কাজ করতে পারে না।
- যখন বিদ্যমান code পরিবর্তন না করে নতুন behavior যোগ করতে হয়।
- যখন কোনো third-party library বা API বিদ্যমান কোডের সাথে কাজ করে না।
উপকারিতা:
- কোড পুনর্লিখন ছাড়াই বিদ্যমান class এর সাথে কাজ করা যায়।
- বিদ্যমান কোডের সাথে নতুন functionality যোগ করা সহজ হয়।
- Client code পরিবর্তন না করেই ভিন্ন interface এর সাথে কাজ করা যায়।
Limitations:
- অতিরিক্ত adapter class কোডবেসকে কিছুটা জটিল করতে পারে।
- পারফরম্যান্সে সামান্য overhead যোগ করতে পারে।
Bridge Design Pattern
এটি abstraction এবং implementation কে স্বাধীন করতে সাহায্য করে। সহজভাবে বললে, abstraction এবং এর implementation কে আলাদা করা হয়, যাতে তারা একে অপরের উপর নির্ভরশীল না থাকে এবং আলাদাভাবে পরিবর্তন করা যায়।
এটি abstraction এবং implementation এর মধ্যে স্বাধীনতা তৈরি করতে সাহায্য করে। সহজভাবে বললে, abstraction এবং এর implementation কে আলাদা করা হয়, যাতে তারা একে অপরের উপর নির্ভরশীল না থাকে এবং আলাদাভাবে পরিবর্তন করা যায়।
// Step 1: Implementor interface যা implementation এর জন্য কাজ করবে
interface Color {
applyColor(): void;
}
// Step 2: Concrete Implementations যা বিভিন্ন color implement করবে
class Red implements Color {
public applyColor(): void {
console.log("Applying red color.");
}
}
class Blue implements Color {
public applyColor(): void {
console.log("Applying blue color.");
}
}
// Step 3: Abstraction class যা Color এর instance ব্যবহার করবে
abstract class Shape {
protected color: Color;
constructor(color: Color) {
this.color = color;
}
abstract draw(): void;
}
// Step 4: Refined Abstraction যা Shape এর বিভিন্ন ধরনের subclass তৈরি করবে
class Circle extends Shape {
public draw(): void {
console.log("Drawing Circle...");
this.color.applyColor(); // Color apply হচ্ছে
}
}
class Square extends Shape {
public draw(): void {
console.log("Drawing Square...");
this.color.applyColor(); // Color apply হচ্ছে
}
}
// Step 5: Client code
const red = new Red();
const blue = new Blue();
const redCircle = new Circle(red);
redCircle.draw(); // Red color সহ circle আঁকা হবে
const blueSquare = new Square(blue);
blueSquare.draw(); // Blue color সহ square আঁকা হবে
output
Drawing Circle...
Applying red color.
Drawing Square...
Applying blue color.
উদাহরণে, আমরা একটি Shape নামে abstraction class তৈরি করেছি, যেখানে Circle এবং Square এর মতো বিভিন্ন shape রয়েছে। তবে Shape class-এ color সরাসরি যোগ না করে, আমরা Color interface ব্যবহার করেছি, যাতে color এর implementation আলাদাভাবে control করা যায়। Red এবং Blue হলো Color interface-এর concrete implementation, যেগুলো বিভিন্ন রঙ apply করে। Shape এবং এর subclass গুলোর মধ্যে আমরা color inject করেছি, এবং draw method-এ সেই color apply করেছি।
কখন Bridge ব্যবহার করবেন:
- যখন abstraction এবং এর implementation আলাদাভাবে পরিবর্তন করতে হবে।
- যখন class hierarchy বড় হয়ে গেলে তা manage করা কঠিন হয়ে যায়।
- যখন inheritance এর বদলে composition ব্যবহার করে code structure সহজ রাখা প্রয়োজন।
উপকারিতা:
- Abstraction এবং Implementation আলাদাভাবে পরিবর্তন করা যায়।
- এটি কোডের জটিলতা কমায় এবং সহজে maintainable করে।
- নতুন class তৈরি না করেই বিভিন্ন implementation handle করা যায়।
Limitations:
- Bridge Pattern implement করা কিছুটা জটিল হতে পারে।
- অতিরিক্ত abstraction কোড বোঝার ক্ষেত্রে সমস্যা তৈরি করতে পারে।
Composite Design Pattern
এটি এমন একটি pattern যেখানে objects গুলোকে tree structure আকারে represent করা হয়, যেখানে individual objects এবং তাদের composition (group) একইভাবে treat করা যায়। অর্থাৎ, Composite pattern এর মাধ্যমে আমরা একটি single object এবং group of objects কে একভাবে handle করতে পারি।
ধরি, আপনি একটি file system implement করতে চাচ্ছেন যেখানে file এবং folder আছে। File এবং Folder উভয়কেই কিছু operations এর জন্য একভাবে handle করতে হবে, কিন্তু তারা একে অপরের থেকে আলাদা। এই সমস্যা সমাধানের জন্য আমরা Composite Design Pattern ব্যবহার করতে পারি।
// Step 1: Component interface যা file এবং folder এর জন্য common operations define করবে
interface FileSystemComponent {
showDetails(indent: string): void;
}
// Step 2: Leaf class যা actual files represent করবে
class FileLeaf implements FileSystemComponent {
private name: string;
constructor(name: string) {
this.name = name;
}
public showDetails(indent: string = ''): void {
console.log(`${indent}File: ${this.name}`);
}
}
// Step 3: Composite class যা folder represent করবে এবং এর ভিতরে files বা অন্যান্য folders রাখতে পারবে
class FolderComposite implements FileSystemComponent {
private name: string;
private components: FileSystemComponent[] = [];
constructor(name: string) {
this.name = name;
}
public addComponent(component: FileSystemComponent): void {
this.components.push(component);
}
public removeComponent(component: FileSystemComponent): void {
const index = this.components.indexOf(component);
if (index !== -1) {
this.components.splice(index, 1);
}
}
public showDetails(indent: string = ''): void {
console.log(`${indent}Folder: ${this.name}`);
this.components.forEach(component => component.showDetails(indent + ' '));
}
}
// Step 4: Client code
const file1 = new FileLeaf("File1.txt");
const file2 = new FileLeaf("File2.txt");
const file3 = new FileLeaf("File3.txt");
const folder1 = new FolderComposite("Folder1");
const folder2 = new FolderComposite("Folder2");
folder1.addComponent(file1);
folder1.addComponent(file2);
folder2.addComponent(file3);
folder2.addComponent(folder1); // Folder এর ভিতরে আরেকটি folder যোগ করা হচ্ছে
// Root folder এ সব কিছু দেখানো হবে
const rootFolder = new FolderComposite("Root");
rootFolder.addComponent(folder1);
rootFolder.addComponent(folder2);
rootFolder.showDetails(); // Full file system structure print করা হবে
output
Folder: Root
Folder: Folder1
File: File1.txt
File: File2.txt
Folder: Folder2
File: File3.txt
Folder: Folder1
File: File1.txt
File: File2.txt
উপরের উদাহরণে, আমরা একটি FileLeaf class তৈরি করেছি যা individual files represent করে এবং একটি FolderComposite class তৈরি করেছি যা folder represent করে। FolderComposite এর ভিতরে multiple FileLeaf এবং অন্য FolderComposite রাখা যায়, অর্থাৎ folder এর ভিতরে folder রাখা সম্ভব। showDetails method এর মাধ্যমে আমরা পুরো tree structure (folder এবং তার ভিতরে থাকা files এবং folders) দেখাতে পারি।
প্রয়োজনে Composite ব্যবহার:
- যখন object গুলো hierarchical structure এ থাকে এবং আপনি individual objects এবং তাদের groups কে একইভাবে handle করতে চান।
- যখন system গুলো recursive structure এ থাকে, যেমন file systems, menus ইত্যাদি।
- যখন client কে abstract structure এর উপর কাজ করতে দিতে হয়, যেন তারা single object বা group এর পার্থক্য না বুঝে।
উপকারিতা:
- Tree structure সহজে represent করা যায়।
- Client এর দিক থেকে single object এবং group object একইভাবে কাজ করে।
- System recursive composition সহজে manage করতে পারে।
Limitations:
- Structure বড় হয়ে গেলে এটি জটিল হয়ে যেতে পারে।
- Leaf এবং Composite এর behavior এর মধ্যে পার্থক্য থাকলে সেটা manage করতে কঠিন হতে পারে।
Decorator Design Pattern
এটি dynamic ভাবে একটি object এর behavior বা functionality পরিবর্তন বা extend করার জন্য ব্যবহৃত হয়, মূল class এর পরিবর্তন না করেই। অর্থাৎ, মূল class এর code modify না করে তার behavior বাড়ানো যায়।
ধরি, আপনার কাছে একটি coffee class আছে এবং আপনি বিভিন্ন ধরনের coffee (যেমন, milk coffee, sugar coffee) তৈরি করতে চান। এখন, আপনি যদি প্রতিটি combination এর জন্য আলাদা class তৈরি করেন, তাহলে class এর সংখ্যা অনেক বেড়ে যাবে। এই সমস্যার সমাধানে আমরা Decorator Design Pattern ব্যবহার করতে পারি।
// Step 1: Component interface যা coffee এর জন্য common operations define করবে
interface Coffee {
cost(): number;
description(): string;
}
// Step 2: Concrete component class যা সাধারণ coffee represent করবে
class SimpleCoffee implements Coffee {
public cost(): number {
return 5;
}
public description(): string {
return "Simple Coffee";
}
}
// Step 3: Decorator class যা Coffee interface implement করবে এবং Coffee object কে wrap করবে
class CoffeeDecorator implements Coffee {
protected coffee: Coffee;
constructor(coffee: Coffee) {
this.coffee = coffee;
}
public cost(): number {
return this.coffee.cost();
}
public description(): string {
return this.coffee.description();
}
}
// Step 4: Concrete decorators যা Coffee এর behavior modify করবে
class MilkDecorator extends CoffeeDecorator {
public cost(): number {
return this.coffee.cost() + 2; // Milk এর জন্য অতিরিক্ত খরচ
}
public description(): string {
return this.coffee.description() + ", Milk";
}
}
class SugarDecorator extends CoffeeDecorator {
public cost(): number {
return this.coffee.cost() + 1; // Sugar এর জন্য অতিরিক্ত খরচ
}
public description(): string {
return this.coffee.description() + ", Sugar";
}
}
// Step 5: Client code
let myCoffee: Coffee = new SimpleCoffee();
console.log(`${myCoffee.description()} Cost: $${myCoffee.cost()}`);
// Adding milk decorator
myCoffee = new MilkDecorator(myCoffee);
console.log(`${myCoffee.description()} Cost: $${myCoffee.cost()}`);
// Adding sugar decorator
myCoffee = new SugarDecorator(myCoffee);
console.log(`${myCoffee.description()} Cost: $${myCoffee.cost()}`);
output
Simple Coffee Cost: $5
Simple Coffee, Milk Cost: $7
Simple Coffee, Milk, Sugar Cost: $8
উপরের উদাহরণে, আমরা একটি SimpleCoffee class তৈরি করেছি, যা একটি সাধারণ coffee represent করে। এরপরে আমরা দুটি decorators তৈরি করেছি: MilkDecorator এবং SugarDecorator, যেগুলো coffee এর উপরে অতিরিক্ত functionality যোগ করে, যেমন দাম বাড়ানো এবং description এ নতুন উপাদান যোগ করা। CoffeeDecorator class মূল coffee object কে wrap করে এবং তার behavior extend করতে সাহায্য করে। Client code এ আমরা প্রথমে simple coffee তৈরি করেছি, এরপর তা MilkDecorator এবং SugarDecorator দিয়ে wrap করে নতুন behavior যোগ করেছি।
প্রয়োজনে Decorator ব্যবহার:
- যখন আপনি class এর behavior পরিবর্তন বা extend করতে চান, কিন্তু inheritance ব্যবহার করতে চান না।
- যখন আপনি object গুলোকে dynamic ভাবে modify করতে চান।
- যখন class গুলোর combination এর কারণে subclass এর সংখ্যা বেড়ে যাওয়ার সম্ভাবনা থাকে।
উপকারিতা:
- Decorator গুলো dynamic ভাবে object এর behavior extend করতে পারে।
- Inheritance এর চেয়ে বেশি flexibility দেয়।
- একাধিক decorators একসাথে ব্যবহার করে nested বা chained structure তৈরি করা যায়।
Limitations:
- Decorator এর structure অনেক nested হলে এটি complex হয়ে যেতে পারে।
- Client এর জন্য nested decorators বুঝা এবং manage করা কিছুটা কঠিন হতে পারে।
Facade Design Pattern
এটি ব্যবহার করে আমরা একটি জটিল subsystem বা API এর সরল interface প্রদান করতে পারি। Facade Design Pattern এর মূল উদ্দেশ্য হচ্ছে client এর জন্য একটি simple interface তৈরি করা, যাতে সে সহজেই জটিল subsystem এর বিভিন্ন কাজ handle করতে পারে।
ধরি, আপনি একটি movie streaming system তৈরি করেছেন, যেখানে অনেক subsystem রয়েছে যেমন: video encoding, user authentication, payment processing, এবং content delivery। এখন, একজন user যদি এই services গুলো individually ব্যবহার করতে চায়, তাহলে তার জন্য প্রতিটি subsystem এর জটিল logic বুঝতে হবে। এটি অনেক complex হয়ে যাবে। এই সমস্যার সমাধানে আমরা Facade Design Pattern ব্যবহার করতে পারি, যা বিভিন্ন subsystem এর উপরে একটি simple interface প্রদান করে।
// Step 1: Complex subsystems
class VideoEncoder {
public encodeVideo(video: string): void {
console.log(`Encoding video: ${video}`);
}
}
class UserAuthenticator {
public authenticateUser(username: string, password: string): boolean {
console.log(`Authenticating user: ${username}`);
return true;
}
}
class PaymentProcessor {
public processPayment(amount: number): void {
console.log(`Processing payment of $${amount}`);
}
}
class ContentDelivery {
public deliverContent(video: string): void {
console.log(`Delivering video content: ${video}`);
}
}
// Step 2: Facade class যা subsystems এর কাজগুলো encapsulate করে
class MovieStreamingFacade {
private encoder: VideoEncoder;
private authenticator: UserAuthenticator;
private payment: PaymentProcessor;
private delivery: ContentDelivery;
constructor() {
this.encoder = new VideoEncoder();
this.authenticator = new UserAuthenticator();
this.payment = new PaymentProcessor();
this.delivery = new ContentDelivery();
}
public streamMovie(username: string, password: string, video: string, paymentAmount: number): void {
if (this.authenticator.authenticateUser(username, password)) {
this.payment.processPayment(paymentAmount);
this.encoder.encodeVideo(video);
this.delivery.deliverContent(video);
console.log("Movie streaming started!");
} else {
console.log("Authentication failed. Unable to stream movie.");
}
}
}
// Step 3: Client code
const movieStreamingFacade = new MovieStreamingFacade();
movieStreamingFacade.streamMovie("john_doe", "password123", "Inception", 10);
output
Authenticating user: john_doe
Processing payment of $10
Encoding video: Inception
Delivering video content: Inception
Movie streaming started!
উপরের উদাহরণে, আমরা অনেকগুলো subsystem তৈরি করেছি: VideoEncoder, UserAuthenticator, PaymentProcessor, এবং ContentDelivery, প্রতিটির আলাদা কাজ আছে। তারপর আমরা একটি MovieStreamingFacade class তৈরি করেছি, যা এই সব subsystems এর কাজগুলো encapsulate করে এবং একটি সরল interface প্রদান করে। Client আর প্রতিটি subsystem এর সাথে আলাদাভাবে কাজ করতে হয় না; বরং, Facade class এর মাধ্যমে একটি method call করেই movie streaming শুরু করতে পারে।
প্রয়োজনে Facade ব্যবহার:
- যখন আপনার কাছে একটি জটিল system বা API থাকে এবং আপনি তার জন্য একটি সরল interface তৈরি করতে চান।
- যখন আপনি একটি বড় system এর multiple subsystems বা components কে সহজভাবে manage করতে চান।
- যখন client এর জন্য subsystem গুলোর সব কাজ না দেখিয়ে শুধু গুরুত্বপূর্ণ কাজগুলো দেখাতে চান।
উপকারিতা:
- Simple interface: Client এর জন্য সহজ এবং understandable interface প্রদান করে।
- Subsystem গুলোকে loose coupling করে: Facade ব্যবহার করে, client এবং subsystem এর মধ্যে dependency কমে যায়।
- Code readability: Facade implementation এর ফলে code অনেক সহজ এবং clean হয়।
Limitations:
- Facade system এর abstraction তৈরি করে, কিন্তু underlying subsystems এখনও complex থাকতে পারে।
- Subsystems গুলো পরিবর্তন হলে Facade এর code ও পরিবর্তন করতে হতে পারে।
Flyweight Design Pattern
এটি object তৈরি করার সময় memory optimization এর জন্য ব্যবহৃত হয়। Flyweight Pattern এর মূল উদ্দেশ্য হল একই ধরনের অনেক object এর মধ্যে common state share করা, যাতে আমরা কম object তৈরি করতে পারি এবং memory তে কম space লাগে।
ধরি, আমরা একটি graphics application তৈরি করছি যেখানে অনেক গাছ এবং পাথর আছে। যদি আমরা প্রতিটি গাছ এবং পাথরের জন্য আলাদা object তৈরি করি, তাহলে অনেক memory ব্যবহার হবে, কারণ গাছ এবং পাথরের common attributes আছে। Flyweight Pattern ব্যবহার করে, আমরা এই common attributes গুলোকে share করে memory কমাতে পারি।
// Step 1: Flyweight interface যা common method define করবে
interface Tree {
render(x: number, y: number): void; // Extrinsic state হিসেবে position (x, y)
}
// Step 2: Concrete Flyweight class যা shared state store করবে
class TreeType implements Tree {
private name: string;
private color: string;
private texture: string;
constructor(name: string, color: string, texture: string) {
this.name = name;
this.color = color;
this.texture = texture;
}
// Tree কে নির্দিষ্ট position এ render করবে
public render(x: number, y: number): void {
console.log(`Rendering a ${this.name} tree at (${x}, ${y}) with color ${this.color} and texture ${this.texture}`);
}
}
// Step 3: Flyweight Factory class যা Flyweight objects তৈরি এবং manage করবে
class TreeFactory {
private static treeTypes: { [key: string]: TreeType } = {};
// Flyweight object create or return করবে (যদি আগে থেকেই তৈরি থাকে)
public static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}_${color}_${texture}`;
if (!this.treeTypes[key]) {
this.treeTypes[key] = new TreeType(name, color, texture);
}
return this.treeTypes[key];
}
}
// Step 4: Client code যা Tree objects তৈরি এবং render করবে
class Forest {
private trees: { x: number, y: number, type: TreeType }[] = [];
public plantTree(x: number, y: number, name: string, color: string, texture: string): void {
const type = TreeFactory.getTreeType(name, color, texture);
this.trees.push({ x, y, type });
}
public render(): void {
this.trees.forEach(tree => tree.type.render(tree.x, tree.y));
}
}
// Step 5: Client Forest এ গাছ গুলো plant করছে এবং render করছে
const forest = new Forest();
forest.plantTree(10, 20, 'Oak', 'Green', 'Rough');
forest.plantTree(15, 25, 'Pine', 'Dark Green', 'Smooth');
forest.plantTree(10, 20, 'Oak', 'Green', 'Rough'); // একই ধরনের tree share করা হচ্ছে
forest.render();
output
Rendering a Oak tree at (10, 20) with color Green and texture Rough
Rendering a Pine tree at (15, 25) with color Dark Green and texture Smooth
Rendering a Oak tree at (10, 20) with color Green and texture Rough
উপরের উদাহরণে, আমরা একটি TreeType class তৈরি করেছি, যা গাছের shared attributes (name, color, texture) store করে। TreeFactory class এর মাধ্যমে আমরা Flyweight objects তৈরি বা reuse করছি। যখনই একই ধরনের গাছ তৈরি করা হয়, Flyweight Factory আগের object কে reuse করে। Forest class এর মধ্যে আমরা গাছের position (x, y) store করছি, যা extrinsic state, এবং TreeType object কে reuse করছি shared state হিসেবে।
প্রয়োজনে Flyweight ব্যবহার:
- যখন প্রচুর সংখ্যক objects তৈরি করতে হয়, কিন্তু অনেকগুলোর মধ্যে common attributes থাকে।
- যখন memory ব্যবহার কমানোর জন্য objects এর shared state optimize করতে হয়।
- যখন আপনার system performance এর উপর memory impact বেশি থাকে।
উপকারিতা:
- Memory optimization: Flyweight objects এর shared state এর মাধ্যমে অনেক কম memory ব্যবহার হয়।
- Object reuse: একই ধরনের objects কে বারবার তৈরি না করে reuse করা যায়।
- Performance improvement: কম objects তৈরি হওয়ার ফলে system এর performance ভালো হয়।
Limitations:
- Object গুলোর মধ্যে intrinsic এবং extrinsic state আলাদা করতে হবে, যা design এবং code জটিল করতে পারে।
- Shared objects এর কারণে এক object এর পরিবর্তন অন্য object কে affect করতে পারে।
Proxy Design Pattern
এই pattern টি ব্যবহার করা হয় যখন আমরা কোনো object এর উপর control বা access limit করতে চাই, অর্থাৎ, object এর কাজকে নির্দিষ্ট নিয়মের মাধ্যমে পরিচালনা করা। Proxy মূলত মূল object এর পরিবর্তে কাজ করে এবং মূল object এর কাজগুলোকে নিয়ন্ত্রণ করে। Proxy কে ব্যবহার করে আমরা মূল object কে directly access না করেও তার কাজ সম্পন্ন করতে পারি।
ধরি আমাদের একটি বড় Image file লোড করতে হবে। এখন যদি আমরা সরাসরি image load করি, তাহলে হয়তো performance এর সমস্যা হতে পারে, কারণ বড় ফাইল লোড হতে সময় লাগতে পারে। কিন্তু যদি আমরা শুধু তখনই image load করি যখন সেটার দরকার, অর্থাৎ, lazy load করি, তাহলে performance improve হবে। এক্ষেত্রে আমরা Proxy Design Pattern ব্যবহার করতে পারি। Proxy class মূল Image class এর পরিবর্তে কাজ করবে এবং তখনই মূল Image টি load করবে যখন সত্যিই দরকার।
// Step 1: Interface বা Abstract class তৈরি করা
interface Image {
display(): void;
}
// Step 2: মূল class তৈরি করা যা বড় resource লোড করবে
class RealImage implements Image {
private fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
this.loadFromDisk();
}
private loadFromDisk(): void {
console.log(`Loading ${this.fileName} from disk...`);
}
public display(): void {
console.log(`Displaying ${this.fileName}`);
}
}
// Step 3: Proxy class তৈরি করা যা মূল object কে manage করবে
class ProxyImage implements Image {
private realImage: RealImage | null = null;
private fileName: string;
constructor(fileName: string) {
this.fileName = fileName;
}
public display(): void {
// Proxy তখনই মূল Image load করবে যখন সেটা প্রয়োজন
if (this.realImage === null) {
this.realImage = new RealImage(this.fileName);
}
this.realImage.display();
}
}
// Step 4: Client code
const image = new ProxyImage("test_image.jpg");
image.display(); // প্রথম বার Image load হবে এবং display হবে
image.display(); // দ্বিতীয় বার Image সরাসরি display হবে, নতুন করে load হবে না
output
Loading test_image.jpg from disk...
Displaying test_image.jpg
Displaying test_image.jpg
উপরের উদাহরণে, RealImage class মূল object হিসাবে কাজ করছে যেটা বড় resource (image file) load করে। কিন্তু ProxyImage class শুধু তখনই মূল Image টি load করে যখন display function call করা হয়। এর ফলে performance improve হয় কারণ বড় ফাইল শুরু থেকেই load না করে, দরকার হলে load করা হয়।
প্রয়োজনে Proxy ব্যবহার:
- যখন কোনো object তৈরি করা ব্যয়বহুল বা resource intensive হয়।
- যখন আমরা মূল object এর access control করতে চাই।
- যখন আমরা caching বা lazy initialization করতে চাই।
উপকারিতা:
- এটি মূল object এর উপর নিয়ন্ত্রণ প্রদান করে।
- Performance optimization এ সাহায্য করে।
- Resource-intensive operations defer করা যায়।
Limitations:
- Proxy implement করতে কিছুটা জটিলতা বৃদ্ধি পায়।
- Proxy নিজেই নতুন class যোগ করে যা overall codebase এ overhead বাড়াতে পারে।
