ডেকরেটর ডিসাইন প্যাটার্ন - Decorator Design Pattern

(ডিসেম্বর , ৪, ২০১৪, বৃহস্পতিবার)
ডেকরেটর ডিসাইন প্যাটার্ন:


জাভার উপর যশুয়া ব্লক’র লেখা খুবই জনপ্রিয় একটা বইয়ের নাম: ইফেক্টিভ জাভা। জাভার প্রতিষ্ঠাতা জেমস গসলিং নিজেও বইয়ের প্রশংসা করে বলেছেন যে অনেকেই মনে করে তাঁর কোনো জাভার বইয়ের দরকার নেই, কিন্তু তাঁর এই বইটির দরকার এবং খুব ভালো হত যদি তিনি আরো ১০ বছর আগে বইটি পেতেন!! [১]


ইফেক্টিভ জাভা বইটির বর্তমান মুদ্রণে ৭০ টি’র বেশি ‘আইটেম’ আছে। প্রতি আইটেম ২-৩ পৃষ্ঠা করে লেখা। আর কী ভাবে আরো ভালো জাভা কোড লেখা যায়, এক একটি আইটেমে তাই-ই লেখা আছে।


একটা আইটেমর ভাষ্য : জাভা কোড লেখার সময় ‘ইনহেরিটেন্স’ বাদ দিয়ে পারলে ‘কম্পসিশন’ ব্যবহার করা উচিত। কেন তা উদাহরণসহ খুব সুন্দর করে ব্যাখ্যা করা আছে।  মজার ব্যাপার হলো, কম্পসিশন ব্যবহার করার উপায় যেটা বলা আছে সেটা  আর কিছুই না: ডেকরেটর ডিসাইন প্যাটার্ন!


এই লেখায় প্রথমে ‘ইনহেরিটেন্স’ আর ‘কম্পসিশন’ কি তা সামান্য একটু আলাপ করেই আইটেমের কথায় আসব আর সমস্যার সমাধানে ডেকোরেটর ডিসাইন প্যাটার্ন কিভাবে কাজে আসে, সেইটা  দেখে নিব - এক ঢিলে দুই পাখি মারা আর কি!


‘ইনহেরিটেন্স’ হলো ‘extend’ কী-ওয়ার্ড  ব্যবহার করে যখন একটা ক্লাস( বা ইন্টারফেস) আরেকটা ক্লাস (বা ইন্টারফেস’র) সব প্রপার্টি, মেথড নিজের মধ্যে নিয়ে আসে। আর ‘কম্পসিশন’ হলো যখন এক ক্লাস আরেক ক্লাস’র রেফারেন্স নিজের মধ্যে ভ্যারিয়েবল হিসাবে রাখে। নিচে একটা উদাহরন দিলাম:


ইনহেরিটেন্স
কম্পসিশন
public class B extends A {
    // …
}
public class B {
     A obj;
}


প্রথেমে একটা উদাহরণ দিয়ে ইনহেরিটেন্স ব্যবহারের অসুবিধা বা সমস্যা কী হতে পারে দেখে নেই [২]
ধরা যাক, আমরা ‘HashSet’ ক্লাস ব্যবহার করব। এই ক্লাসে ‘size ()’ নামে একটা মেথড আছে যা HashSet-এ কয়টা এলিমেন্ট আছে তা বলে দেয়।  HashSet -এর বৈশিষ্টই হচ্ছে unique এলিমেন্ট রাখা। অর্থাথ, কেউ যদি একই এলিমেন্ট দুই বা তার চেয়ে বেশি বার রাখতে চায় তবে তা কেবল একটাই রাখবে। ধরা যাক, আমরা জানতে চাই আমাদের HashSe-এ শুরু থেকে এখন পর্যন্ত কয়টা এলিমেন্ট রাখার চেষ্টা করা হয়েছে (খেয়াল করুন কয়টা রাখা আছে তা কিন্তু না।)। এরজন্য আমরা কী করতে পারি?


সহজ উত্তর হচ্ছে আমাদের নিজেদের একটা ক্লাস তৈরী করব যা কিনা HashSet কে ইনহেরিট করবে আর সেই ক্লাস-এ একটা ভ্যারিয়েবল ‘count’ রেখে দিব। আর HashSet-এর ‘add’ বা ‘addAll’ মেথড গুলো ওভাররাইড করব যাতে যখনি কেউ add করতে চাইবে তখন  আমরা ‘count’ বাড়িয়ে নিব। আর একটা পাবলিক মেথড ‘getCount’ লিখব যার কাজ হবে ‘count’ রিটার্ন করা।  ব্যস, হয়ে যাওয়ার কথা !!

কোডের চেহারা অনেকটা নিচের মত হবে:


import java.util.HashSet;
public class CountingHashSet<E> extends HashSet<E> {
 private int count = 0;
 @Override public boolean add(E e) {
      count += 1;
      return super.add(e);
 }
 @Override public boolean addAll(Collection<? extends E> c) {
      count += c.size();
      return super.addAll(c);
 }
 public int getCount() { return count;}
 // constructors, ...
}


UML ডায়াগ্রাম  আঁকলে তা হবে এই রকম:


বেশ সোজা কোড।  কোনো কিছু add করার আগে আমরা শুধু ‘count’ বাড়িয়ে নিচ্ছি।আর আসল add এর কাজ HashSet-ই তো করছে। আপাত ভাবে মনে হচ্ছে সব ঠিকই আছে, কোথাও কোনো ঝামেলা হবার কথা না। কিন্তু আসলে অনেক গুলো ঝামেলা হবে।


আর তার মূল কারণ হচ্ছে HashSet ক্লাস-এর addAll মেথড।
HashSet-এর  addAll মেথড আসলে add মেথড বার বার কল করেই সবগুলো এলিমেন্ট add করে।  তাই, যখন আমাদের CountingHashSet ক্লাস-এর addAll মেথড  কল হয়, আমরা প্রথমে count বাড়িয়ে নেই, তারপর যখন সুপারক্লাস-এর ‘addAll’ কল করি তখন  তা আবার ‘add’ মেথড কল করে। তাতেও সমসস্যা ছিল না, কিন্তু ‘dynamic dispatch’ হয়ে সেই কল আসলে আমাদের CountingHashSet -এর ‘add ‘ মেথডের কাছে চলে আসে, ফলে  আমরা  count আরেকবার বাড়িয়ে নেই।  শেষমেষ, আমরা count  দিগুণ বার বাড়াই!


HashSet -এর addAll মেথড যে এইকাজ করে তা তো আমাদের জানার কথা না!! আর ডকুমেন্টেশনও কিন্তু এই ইমপ্লিমেন্টেশন -এর কথা বলে না।  কাজেই আমাদের দোষ দিয়ে তো লাভ নেই।  


এখন এই সমস্যা সমাধানের কী উপায়?


যেহেতু আমরা এখন ইমপ্লিমেন্টেশন ব্যাপারটা জেনেই ফেললাম, কাজেই আমরা আমাদের CountingHashSet  থেকে addAll  মেথড বাদ দিয়ে দিলেই কিন্তু ঝামেলা মিটে যায়! আমাদের ক্লাস-এর বাপ HashSet-ই  না হয় addAll  করলো।


কিন্তু তাতেও ঝামেলা পুরোপুরি শেষ হয় না।  ভবিষ্যতে যদি HashSet তার addAll মেথডের ইমপ্লিমেন্টেশন বদলিয়ে ফেলে তাহলে কিন্তু আবার আমরা count  কম গুনব - ঠিক না?


আরো ঝামেলা আছে।  কী হবে যদি ভবিষ্যতে HashSet  আবার addAtFront  টাইপ কোনো মেথড নিজের ক্লাস-এ যোগ করে বসে।  তখন আমাদেরও কিন্তু তা ওভাররাইড করতে হবে।  


আরো একটা ঝামেলা আছে।  ধরা যাক, আমাদের CountingHashSet ক্লাস-এ আমরা অন্য কিছু মেথড লিখলাম যা এখনকার HashSet  ক্লাস-এ নাই।  কিন্তু ভবিষ্যতে যদি HashSet  ক্লাস সেই একই  নামের অন্য রিটার্ন টাইপ-এর মেথড  যোগ করে বসে, তাহলে আমাদের কোড কম্পাইল করবে না !!


মোটকথা হচ্ছে, আমাদেরকে সবসময়ই HashSet -এর উপর নজর রাখতে হবে। কখন কী যে বদলে যায়, সেই ভয়ে থাকতে হবে।  এইটা কোড মেইনটেনেন্স বা ব্যবস্থাপনার দুঃস্বপ্ন!


আসল সমাধান টা  কী তাহলে?


উত্তরটা হচ্ছে কম্পসিশন ব্যবহার করা। যেহেতু আমরা HashSet -এ একটা নতুন ফীচার/বৈশিষ্ট যোগ করতে চাচ্ছি, সুতরাং HashSet কে কোনভাবে  ডেকরেট বা  অলংকরণ করতে হবে।  আর যেহেতু ইনহেরিট করলে অনেক ঝামেলা, তাই HashSet  আর আমাদের CountingHashSet কে আলাদা করতে হবে।  একই ইনহেরিটেন্স ট্রি -এর ভেতর রাখা যাবে না।  আগে সমাধানের UML  ডায়াগ্রাম তা দেখে নেই:


UML ডায়াগ্রামে ডায়মন্ড চিহ্ন দিয়ে কম্পসিশন, আর ত্রিভুজ চিহ্ন দিয়ে ইনহেরিটেন্স বোঝায়। প্রথমেই খেয়াল করুন যে HashSet  আর আমাদের CountingHashSet  এখন আর সরাসরি ইনহেরিটেন্স (বাপ -ছেলে ) সম্পর্কে নাই।


আমরা আরেকটা ক্লাস, SetForward যোগ করেছি যা কিনা HashSet  যেই ইন্টারফেস (Set ) ইমপ্লেমেন্ট করে, সেও একই ইন্টারফেস ইমপ্লেমেন্ট করে।  SetForward একটা প্রাইভেট রেফারেন্স ভ্যারিয়েবল s  রাখে যার টাইপও Set . আর এই প্রাইভেট রেফারেন্স ভ্যারিয়েবল ব্যবহার করাই কম্পসিশন !


আমাদের CountingHashSet  ক্লাস SetForward কে extend করে।  আর আমাদের  add অথবা addAll  মেথড, তাদের কাজ করার (অর্থাথ count  বাড়ানোর) আগে বা পরে s  ব্যাবহার করে অন্য ক্লাস-এর add বা addAll  মেথড কল করবে। এক্ষেত্রে তা HashSet -এর মেথড কল করবে। আর যেহেতু CountingHashSet  এখন আর HashSet কে ইনহেরিট করে না, তাই ‘dynamic dispatch ‘ হয়ে কোনো মেথড কল আর (CountingHashSet -এ ) ফিরে আসবে না।


এবার কোড টা দেখি, আশা করি তাহলে ব্যপারটা পুরোপুরি বোঝা যাবে:




public class SetForward<E> implements Set<E> {
     private final Set<E> s;                // will refer to HashSet, re-usable for any Set
     public SetForward(Set<E> s) {  this.s = s; } // Constructor that sets private field
     public boolean addAll(Collection<? extends E> c) {  return s.addAll(c); }
     public boolean add( E c) {  return s.add(c); }
     // … more methods
 }  
public class CountingHashSet<E> extends SetForward<E> {
      private int addCount = 0;                       // does the actual counting
      public CountingHashSet(Set<E> s) {  super(s); }
      public boolean addAll(Collection<? extends E> c) {
   s.addAll(c);
   count += c.size();
      }
      public boolean add(E c) { s.add(c); count++;   }
      // … more methods
 }


আর সব শেষে আমাদের যা করতে হবে তা হচ্ছে, (main মেথডে) HashSet -এর একটা অবজেক্ট তৈরী করে আমাদের CountingHashSet - ক্লাস-এ পারামিটার হিসাবে পাস করতে হবে।  অনেকটা এইভাবে:


Set hashSetObj = new HashSet();
CountingHashSet ourObj = new CountingHashSet(hashSetObj);


(যেহেতু আমাদের SetForward  ক্লাস আর HashSet  ক্লাস দুইজনই Set  ইমপ্লেমেন্ট করে, আবার আমাদের CountingHashSet  ক্লাস SetForward -কে extend  করে, তাই আমাদের CountingHashSet তার Constructor -এ HashSet  অবজেক্ট নিতে পারবে)
ব্যস, সব সমস্যার সমাধান হয়ে গেল।  এখন HashSet অবজেক্ট ভবিষ্যতে যতই পরিবর্তিত হোক, আমাদের কোড-এ আর ঝামেলা হবে না।  
আর এই যে আমরা HashSet কে ডেকরেট বা অলঙ্করণ করলাম, এইটাই ডেকরেট ডিসাইন প্যাটার্ন। যেকোনো অবজেক্ট কে কম্পসিশন ব্যবহার করে (Runtime -এ) অলঙ্করণ করাই এর মূল ভাষ্য।
মজার ব্যাপার হচ্ছে, আমরা অনেকই হয়ত এরই মধ্যে না জেনেই ডেকরেট ডিসাইন প্যাটার্ন ব্যবহার করেছি।  java.io  প্যাকেজে অনেক ক্লাস ডেকরেট ডিসাইন প্যাটার্ন ব্যবহার করে [৩]।  কোনো টেক্সট ফাইল পড়ার সময় আমরা না জেনেই ওই ক্লাস গুলো ব্যবহার করি।  যেমন:


FileReader fr = new FileReader(“input.txt”);
BufferedReader txtReader = new BufferedReader(fr);


এখানে কিন্তু FileReader অবজেক্ট fr কে BufferedReader  ডেকরেট  করছে! কিছুই না, BufferedReader  শুধুমাত্র বাফারিং বৈশিষ্ট FileReader -এ যোগ করছে।
ডেকরেট সহ অন্যান্য ডিসাইন প্যাটার্ন আরো ভালো ভাবে বোঝার জন্য দুর্দান্ত একটা বই হচ্ছে হেড ফার্স্ট ডিসাইন প্যাটার্ন।  আরো সহজ করে অনেক ছবি দিয়ে কমিক স্টাইল -এ প্রতিটা ডিসাইন প্যাটার্ন বইটাতে বোঝানো হয়েছে।


সবশেষে কিছু ক্রেডিট দিয়ে দেই:
[১] মূল উদাহরণ  ইফেক্টিভ জাভা বইটি থেকে নেয়া।
[২] কোড সহ অনেক ব্যাখ্যাই আমি আমার সুপারভাইসর  ড. ক্রিস্টফ চালনার -এর স্লাইড থেকে নিয়েছি।
[৩] java.io -এর ব্যবহার হেড ফার্স্ট ডিসাইন প্যাটার্ন বই থেকে জেনেছি।

ধন্যবাদ। -- ইশতিয়াক হোসেন, সিএসই , ডিইউ , সপ্তম ব্যাচ।

1 comment:

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম  ৩ ফেব্রুয়ারি, শনিবার, ২০২৪ রেগুলার এক্সপ্রেশন (Regular Exp...