অবজেক্ট ওরিয়েন্টেড প্রোগ্রামিং পরিচিতি (শেষ ভাগ)

Last updated 3 months ago

গত পর্বে আমরা দেখেছিলাম ক্লাস ডিফাইন করে কিভাবে, অবজেক্ট তৈরি করে কিভাবে, অবজেক্ট এর মেথড, অ্যাট্রিবিউট সেট করা এবং তা Call করা। আজকে আমরা বেশ কিছু জিনিস সংক্ষিপ্ত আকারে দেখব। আজকে যা আলোচনা করা হবে:

  • Encapsulation

  • Constructor

  • Inheritance

  • Polymorphism

Encapsulation:

এর অপর নাম Data hiding বা Data protectionOOP এর একটি জনপ্রিয় ফিচার এবং সফটওয়্যার ডিজাইন ও বিল্ডিংয়ের জন্যও গুরুত্বপূর্ণ। সফটওয়্যার ব্যবহারকারীর জানার দরকার নেই সফটওয়্যারটি কীভাবে কাজ করে (যারা নন ডেভলপার) কাজ চললেই এবং মনমত পার্ফর্মেন্স পেলেই হল। সেদিক থেকে Encapsulation খুবই জরুরি। কোন ক্লাসের Attribute গুলোকে সাধারণত আমরা Encapsulated করে থাকি। আমরা আগেই দেখেছি Object এর Attribute পরিবর্তন করা কোন ব্যাপার না। ধরা যাক, Person একটি Class যার Attribute গুলো হল name (string type) এবং age (int type)।তাহলে আমরা Person এর একটা object তৈরি করে সহজেই তার নাম পরিবর্তন করতে পারি:

#include <iostream>
using namespace std;
class Person
{
public:
string name;
int age;
};
int main()
{
Person idiot;
idiot.age = 0;
idiot.name = "Idiot";
cout << idiot.age << endl;
cout << idiot.name << endl;
}

উপরের কোডটি ঠিকঠাক চলবে কিন্তু নিচেরটা চলবেই না। পরিবর্তন কোথায়? public এর বদলে private বসিয়েছি :)

#include <iostream>
using namespace std;
class Person
{
private:
string name;
int age;
};
int main()
{
Person idiot;
idiot.age = 0;
idiot.name = "Idiot";
cout << idiot.age << endl;
cout << idiot.name << endl;
}

তারমানে এভাবে আমরা Class কে Encapsulated করলাম! আর এতে সুবিধা হল ইউজার ডিরেক্টলি তার Attribute এ অ্যাক্সেস পেল না।

এখানে আরেকটি গুরুত্বপূর্ণ ব্যাপার, যদি আমরা Attribute এ অ্যাক্সেস না পাই তাহলে সেটা দিয়ে আমাদের কি লাভ? এখানে আবার OOP সর্বোপরি C++ এর চমক। Private বা Protected ডেটা আমরা ডিরেক্টলি অ্যাক্সেস করতে পারি না বটে, কিন্তু আমরা public ফাংশনের মাধ্যমে private ডেটায় অ্যাক্সেস পেতে পারি! তার মানে হল, আমার ক্লাসে যদি একটি পাব্লিক মেম্বার ফাংশন থাকে এবং সেটা এমন একটি Attribute নিয়ে Deal করে যেটা কিনা Private/Protected তারপরও আমরা ওই Attribute এ অ্যাক্সেস পাব। নিচের উদাহরণটি দেখা যাক:

#include <iostream>
using namespace std;
class Person
{
public:
void setNameAndAge(string n, int a);
void showNameAndAge();
private:
string name;
int age;
};
int main()
{
Person A;
A.setNameAndAge("My Name", 20);
A.showNameAndAge();
}
void Person::setNameAndAge(string n, int a)
{
name = n;
age = a;
}
void Person::showNameAndAge()
{
cout << "The name: " << name << endl;
cout << "The age: " << age << endl;
}

setNameAndAge এবং showNameAndAge দুইটা যেহেতু পাব্লিক তাই আমরা Object দ্বারা ডিরেক্টলি অ্যাক্সেস পাচ্ছি।আবার যেহেতু এই দুইটি মেথড ওই ক্লাসের মধ্যে অবস্থিত তাই ওই মেথডগুলো নিজের ক্লাসের সকল Private/Public/Protected Attribute গুলোতে অ্যাক্সেস পাবে। এখানে ব্যাপারটা ঠিক “কান টানলে মাথা আসে” মাথা প্রাইভেট তাই আমরা ডিরেক্টলি মাথা টানতে পারব না, আমাদের হাতে যেহেতু কান আছে (setNameAndAge এবং showNameAndAge) তাই ওইটা দিয়েই টানতে পারব :D

এইখানে আরেকটি প্রশ্ন আসে, আমিতো নিজের ক্লাসের মেথড দিয়ে ডেটা পরিবর্তন করছি তাহলে Encapsulation এর দরকার কোথায়? Encapsulation দরকার কারণ Public Data পুরাই Public আর আমি অন্য কোন Class এর Object দ্বারা Access পেতে পারি।

#include <iostream>
using namespace std;
class Person
{
public:
string name;
int age;
};
class dataChanger
{
public:
dataChanger(Person &p)
{
p.name = "I Changed it";
p.age = 10;
}
};
int main()
{
Person person;
person.name = "The Name has been set";
person.age = 99;
cout << person.name << endl;
cout << person.age << endl;
dataChanger danger(person);
cout << person.name << endl;
cout << person.age << endl;
}

আউটপুট:

The Name has been set
99
I Changed it
10

বিগিনারদের জন্য প্রোগ্রামটি একটু জটিল হতে পারে, এখানে দুইটি ক্লাস ব্যবহার করা হয়েছে, একটি হল Person এবং আরেকটি হল dataChangerdataChanger এর মেথড একটাই এবং সেটা তার কনস্ট্রাক্টর*। dataChanger এ আমরা একটি Person Class এর Object এর রেফারেন্স পাঠাই এবং তার Attribute গুলো পরিবর্তন করে দেয়। তাহলে দেখা যাচ্ছে public অ্যাট্রিবিউটগুলো আসলেই Public এবং তা অন্যান্য ক্লাসের Method দ্বারা Accessible!

আমরা যদি শুধু এই পরিবর্তনটা করি তাহলে অন্য ক্লাসের মেথড আর Access পাবে না:

class Person
{
private:
string name;
int age;
};

যেসব মেথড ও অ্যাট্রিবিউট Encapsulated করতে চান সেগুলোকে Access Modifier দ্বারা Modify করতে হবে। C++ এ কোন এক্সেস Modifier না ব্যবহার করলে ডিফল্ট Modifier হিসেবে private কাজ করবে। আপাতত Encapsulation নিয়ে এতটুকুই। এরপরে আমরা Constructor নিয়ে আলোচনা করব।

Constructor:

কোন ক্লাসের Constructor একটি ফাংশন ছাড়া কিছুই নয়। কিন্তু এর একটি বিশেষত্ব আছে। Constructor এর প্রধান বিশেষত্ব হল একে Call করা লাগে না। Object তৈরি হওয়ার সাথে সাথে এই ফাংশনে যতগুলো স্টেটমেন্ট থাকবে সেগুলো অন্যান্য সাধারণ ফাংশনের মত Execute করতে থাকে এবং এটি একাধিক আর্গুমেন্ট নিতে পারে কিংবা নাও নিতে পারে, আরেকটি বৈশিষ্ট্য হল এটি কিছু Return করে না। তাহলে একনজরে Constructor এর বৈশিষ্ট্যগুলো হল:

  • একে Call করা লাগে না, অবজেক্ট তৈরি হওয়ার সাথে সাথেই রান হয়

  • এক বা একাধিক Argument থাকতে পারে কিংবা আর্গুমেন্ট নাও থাকতে পারে

  • কোন কিছু Return করে না

  • Constructor যেহেতু ফাংশন তাই এই ফাংশনের নাম অবশ্যই সংশ্লিষ্ট ক্লাসের নাম হতে হবে

একটি উদাহরণ দেখা যাক:

#include <iostream>
using namespace std;
class Person
{
public:
string name;
int age;
Person(string string_as_argument, int int_as_argument)
{
name = string_as_argument;
age = int_as_argument;
}
};
int main()
{
Person A("Mr. A", 5);
cout << "My name is: " << A.name << endl;
cout << "My age: " << A.age << endl;
}

আউটপুট:

My name is: Mr. A
My age: 5

প্রোগ্রামটিতে কনস্ট্রাক্টর কোনটি? আগেই বলেছি, কনস্ট্রাক্টর হল সেই ফাংশন যার নাম আর ক্লাসের নাম একই এবং যেহেতু এটি কিছু রিটার্ন করে না তাই তার আগে কোনপ্রকার Return Type এর কিওয়ার্ড থাকবে না। এখানে আমাদের ক্লাস হল Person এবং Person নামের একটি ফাংশন দেখা যাচ্ছে যার প্রথম আর্গুমেন্ট string টাইপ এবং দ্বিতীয় আর্গুমেন্ট int টাইপ। এখন চোখ বন্ধ করে বলে দেওয়া যায় কনস্ট্রাক্টর আসলে কোনটি!

Person কনস্ট্রাক্টরটি কি করছে আসলে? এটি দুইটি আর্গুমেন্ট নিয়ে অবজেক্ট এর ভ্যারিয়েবলে তার মান বসাচ্ছে। অর্থাৎ আমরা যদি main ফাংশনের দিকে লক্ষ্য করি তাহলে দেখা যাবে আমরা Object তৈরি করার সময় দুইটা আর্গুমেন্ট পাস করেছি। একটি “Mr. A” এবং আরেকটি 5Constructor আর্গুমেন্ট দুটো নিয়ে ক্লাসের name ও age ভ্যারিয়েবলে বসিয়ে দিল। আদৌ বসালো কিনা তা দেখার জন্য আমরা A.nameA.age দিয়ে অ্যাট্রিবিউটগুল‌ো চেক করলাম এবং আউটপুটে দেখলাম সেটা ঠিকঠাক আউটপুট দেখাচ্ছে।

আশা করি কিছুটা হলেও বুঝা গেল, Constructor যেহেতু ফাংশন তাই আমরা এটাকে নিচের মত করে বাইরে Declare করতে পারি!

class Person
{
public:
string name;
int age;
Person(string, int);
};
Person::Person(string n, int a)
{
name = n;
age = a;
}

কনস্ট্রাক্টরের কিভাবে ব্যবহার করে তা নিয়ে দেখা গেল! এটি কতটা গুরুত্বপূর্ণ?

Constructor এর প্রয়োজনীয়তা:

কনস্ট্রাক্টরের মাধ্যমে আমরা অবজেক্ট এর Initial ভ্যালু বসিয়ে দিতে পারি, অন্য অর্থে আমরা যদি চাই যে একটি অবজেক্ট যখন তৈরি হবে তখন সেটা কিছু Initial Value নিয়ে তৈরি হোক। যাতে করে আমার সেই ক্লাসের ভ্যারিয়েবল ধরে ধরে ভ্যালুগুলো না বসানো লাগে। এবং সেই ভ্যালুগুলো ইনিশিয়ালি প্রসেসও করা যায় কনস্ট্রাক্টরের সাহায্যে।

আমরা দেখব Arduino তে LiquidCrystal লাইব্রেরি যখন আমরা ব্যবহার করব তখন কনস্ট্রাক্টরে কতগুলো পিন নাম্বার বসিয়ে দেব। ওই পিন নাম্বারগুলো অবশ্যই একটি নির্দিষ্ট Order এ দিতে হবে। সাধারণ Liquid Crystal Display চালানোর জন্য সাধারণত ১৪-১৬ পিন ব্যবহার করা হয়। এখানকার কিছু নির্দিষ্ট পিন আমরা Arduino এর সাথে কানেক্ট করি এবং ওই কানেক্ট করা পিনগুলো Order অনুসারে কনস্ট্রাক্টরে বসাই। বাকি সব কাজ হল LiquidCrystal লাইব্রেরির! অর্থাৎ ওই পিন অনুযায়ী LCD তে ReadWrite mode সেট করা, ক্যারেক্টার শো করা ইত্যাদি সব সেটিংস কনস্ট্রাক্টরের মাধ্যমেই ঠিক হয়ে যায়। আমাদের নতুন কোন ফাংশন কল করা লাগে না কিংবা সেটা নিয়ে ঘাঁটাঘাঁটি করা লাগে না।

Inheritance:

ইনহেরিট্যান্স বলতে যা বুঝায় কাজে আসলেই তাই। আমরা প্রত্যেকেই আমাদের নিজ নিজ পিতামাতার কিছু বৈশিষ্ট্যধারণ করে থাকি। একই বংশের লোকজনদের মধ্যে সাধারণত কিছু Common বৈশিষ্ট্য থাকা অস্বাভাবিক কিছু নয়। দোষগুণ যা আছে তা বংশানুক্রমে চলতে থাকে। OOP এর আরেকটি চমৎকার ফিচার Inheritance! পুরনো কোড ব্যবহার করা এবং আপনার নতুন ক্লাসকে আরও কিছু Method ও Attribute যোগ করে আরও শক্তিশালী করে তোলা Inheritance এর অন্যতম কাজ।

আমরা Animal ক্লাসে লক্ষ্য করি। Animal ক্লাসের সদস্যরা কি করে? খায়দায়, ঘুমায়, শিকার করে, চলাফেরা করে আরও অনেক কিছু করে। তাহলে আমরা সুবিধার্থে অল্প কিছু ফাংশন ও অ্যাট্রিবিউট দিয়ে Animal Class টি ডিফাইন করি:

class Animal
{
public:
void eat() { cout << "eating" << endl;}
void sleep() {cout << "sleeping" << endl;}
void hunt() {cout << "hunting" << endl;}
};

এখন যদি আমাকে বলা হয়, Animal ক্লাস বানাইলা ঠিকাছে, এবার Bird নামের আরেকটি ক্লাস তৈরি কর।

Bird যেহেতু Animal এর মধ্যেই পড়ে এবং তার eat, sleep ও hunt এই মেথডগুলোও রয়েছে তাই আমি আগের ক্লাসের মেথডগুলো কপি করে পেস্ট করে দিতে পারি আর যেহেতু এসব কাজের পাশাপাশি Bird এর আরেকটি Method আছে আর সেটা হল Fly বা উড়া। :)

তাহলে ক্লাসটি দাঁড়াবে:

class Bird
{
public:
void eat() {cout << "eating" << endl;}
void sleep() {cout << "sleeping" << endl;}
void hunt() {cout << "hunting" << endl;}
void fly() {cout << "flying" << endl;}
};

কাজটা কি সুবিধাজনক হল? মোটেই না, যদি শখানেক মেথড ও অ্যাট্রিবিউট থাকে তাহলে কপি করতে করতেই অবস্থা খারাপ হয়ে যাবে! :P

তাহলে উপায় কী? Inheritance Apply করা :)

আমরা যদি এখন বলি Bird inherits Animal তাহলে Animal class এর eat, sleep ও hunt মেথডগুলো অটোমেটিক` চলে আসবে এবং তার জন্য আমার আর কিছুই করতে হবে না। যেটা অতিরিক্ত লাগবে সেটা শুধু অ্যাড করে দেব।

Inheritance Apply করার পরে কোডটি হবে:

#include <iostream>
using namespace std;
class Animal
{
public:
void eat() { cout << "Eating" << endl;}
void sleep() {cout << "Sleeping" << endl;}
void hunt() {cout << "Hunting" << endl;}
};
class Bird : public Animal
{
public:
void fly() { cout << "Flying" << endl; }
// Calling the functions
void activities()
{
eat();
sleep();
hunt();
fly();
}
};
int main()
{
Bird b; // Creating an object
b.eat();
b.sleep(); // Calling the functions individually
b.hunt();
b.fly();
cout << "----------" << endl;
b.activities(); // Calling the 'activities' function which calls other functions
}

আউটপুট:

Eating
Sleeping
Hunting
Flying
----------
Eating
Sleeping
Hunting
Flying

Bird এর ক্লাস থেকে আমরা দেখতে পাচ্ছি সেখানে eat(), sleep(), hunt() এর কোনটারই Definition দেওয়া নেই কিন্তু তারপরও ফাংশনগুলো আমরা Call করতে পেরেছি। এই ফাংশনগুলো এসেছে Animal ক্লাসের থেকে। Animal ক্লাসে ফাংশনগুলোর পুরোপুরিভাবে ব্যাখ্যা করা হয়েছে তাই Inherit করার সময় আমাদেরকে ফাংশনগুলো নিয়ে মাথা ঘামাতে হয় নি, ডিরেক্টলি কল করেই কাজ করা গেছে।

ইনহেরিট্যান্স তাহলে প্রয়োগ করে কিভাবে?

class derivedClass : public baseClass { /* Put your attributes and methods here!*/ };
// derivedClass is the inherited one
// class[keyword] your_class_Name :[colon] public/private/protected[access modifier] base_Class

ইনহেরিট্যান্স ও বেশ বড় একটি টপিক, তাই অনেক কিছুই আলোচনা করা হল না। যতটুকু এখানে আলোচনা করা হল সেটুকুও এই কোর্সে লাগবে না আশাকরি।

Polymorphism:

Poly মানে বহু সেটা আমরা আগেই জানি। Polymorphism কে সেই হিসেবে বহুরূপতাও বলা যেতে পারে। একই জিনিসের বিভিন্ন রূপ থাকা মানেই সেই জিনিসটি বহুরূপী। OOP তেও বহুরূপী আছে :P

পলিমর্ফিজমও বিশাল একটি টপিক। আগেই বলে রাখা ভাল, Function Overloading, operator overloading কে সরাসরি Polymorphism বলা যাবে না। পলিমর্ফিজমের সংজ্ঞায় Function/Method overloading পড়ে না কিন্তু একে পলিমর্ফিজমের একটি অংশমাত্র হিসেবে বলতে ক্ষতি নেই। সবকিছু সিম্পল রাখার জন্য আমি এখানে Function Overloading টা দেখাব এবং আপাতত একেই Polymorphism হিসেবে বলব। তারপরও আবারও দ্রষ্টব্য, Function overloading মানেই কিন্তু Polymorphism নয়।

আলোচনা শুরুর আগে নিচের প্রোগ্রামটি দেখা যাক:

#include<iostream>
using namespace std;
class Shape
{
public:
float calcArea(int height, int width); // Calculate area for Rectangular shape
float calcArea(double base, double height); // Calculate area for Triangular Shape
float calcArea(int side); // Calculate area for Square shape
};
float Shape::calcArea(int height, int width)
{
return height * width;
}
float Shape::calcArea(double base, double height)
{
return 0.5 * base * height;
}
float Shape::calcArea(int side)
{
return side * side;
}
int main()
{
Shape rectangle, triangle, square;
cout << "Area of rectangle is: " << rectangle.calcArea(5,3) << endl;
cout << "Area of triangle is: " << rectangle.calcArea(2.0,5.0) << endl;
cout << "Area of square is: " << square.calcArea(5) << endl;
}

আউটপুট:

Area of rectangle is: 15
Area of triangle is: 5
Area of square is: 25

দেখা যাচ্ছে ক্লাস Shape এ আমরা তিনটা মেথড তৈরি করেছি একই নামের calcArea যার কাজ হল ক্ষেত্রফল বের করা। ভাল করে লক্ষ্য করলে দেখা যাবে তিনটি ফাংশনের নাম এক হতে পারে কিন্তু প্রত্যেকটার সাথে প্রত্যেকটার পার্থক্য বিদ্যমান। পার্থক্যগুলো হল, প্রথম ফাংশনটির আর্গুমেন্ট ২ টা এবং ২টাই int টাইপের। পরের ফাংশনটিরও আর্গুমেন্ট ২টা কিন্তু ২টার টাইপ হল double এবং শেষের ফাংশনটির আর্গুমেন্ট কেবল ১টি।

নাম এক হলেও আর্গুমেন্টের ভিত্তিতে ফাংশনগুলো আলাদা। কিন্তু কম্পাইলার কিভাবে বুঝবে আমি আসলে কোন ফাংশনটা Call করছি? C++ কম্পাইলার এতটাই স্মার্ট যে সে শুধু আপনার দেওয়া আর্গুমেন্ট টাইপ বা কয়টা আর্গুমেন্ট দিয়েছেন সেটা দেখেই বুঝতে পারবে আসলে আপনি কোন ফাংশনটি Call করছেন। এই পদ্ধতির কেতাবি নাম Function / Method overloading। আমরা আপাতত একেই পলিমর্ফিজম হিসেবে চিহ্নিত করছি।

আর এখানেই অবজেক্ট ওরিয়েন্টেড প্রোগ্রামিংয়ে ইতি টানলাম। OOP এর অনেক ফিচার বাদ পড়েছে ও জিনিসগুলো সাধারণ রাখার জন্য অনেক ভুলভাল কথা লিখেছি। আর্ডুইনো প্রোগ্রামিংয়ে OOP এর ব্যবহার যদিও আমরা কদাচিৎ দেখব তারপরও এই বিষয়গুলোতে ধারণা রাখা জরুরি। পরের পরিচ্ছদে আমরা সিরিয়াল কম্যুনিকেশন সম্পর্কে মোটামুটি জানার চেষ্টা করব। সেখানে অবজেক্ট ওরিয়েন্টেড প্রোগ্রামিংয়ের সব ব্যাপার না থাকলেও সাধারণ ধারণা থাকলে বুঝতে অসুবিধা হবে না। তাছাড়া পরবর্তীতে Header ফাইল তৈরি করা ও এর ব্যবহারও দেখানো হবে, তখন OOP জানলে কোড অনেকাংশে সংক্ষিপ্ত ও ReadableOrganized করা সম্ভব।