Nur Amin Sifat
6 min readOct 8, 2022

--

পাইথন দিয়ে SOLID Principles

SOLID হচ্ছে পাঁচটি অবজেক্ট ওরিয়েন্টেড ডিজাইন প্রিন্সিপালের সংক্ষিপ্তরূপ যেগুলোর সাথে Robert C. Martin (Uncle bob) সর্বপ্রথম পরিচয় করিয়ে দিয়েছিলো ২০০০ সালের দিকে। মূলতঃ এই প্রিন্সিপাল গুলো উদ্দেশ্য হচ্ছে যথাপোযুক্ত ভাবে ক্লাসকে ডিজাইন করা, ক্লাস গুলোকে সুসংগঠিত করা। কেননা এগুলো এমন কিছু ভালো প্র‍্যাকটিসের সাথে আমাদের পরিচয় করিয়ে দেয় যেগুলো আমাদের ক্লাসের গঠনকে আরো সুন্দর ও সুশৃঙ্খল করে। অন্যদিকে এগুলো আমাদের মাঝারি বা বড় বড় প্রজেক্টকে মেইনটেইন, এক্সটেন্ড করতে খুবই গুরুত্বপূর্ণ ভূমিকা পালন করে।

এসব প্রিন্সিপালের আরেকটা গুরুত্বপূর্ণ দিক হচ্ছে এগুলো আমাদের বিভিন্ন ডিজাইন প্যাটার্ন এবং সফটওয়্যার আর্কিটেকচার বুঝতে ব্যাপক ভাবে সহয়তা করে, তাই প্রত্যেকটা ডেভেলপার বা সফটওয়্যার ইঞ্জিনিয়ারদের উচিত এই প্রিন্সিপাল গুলো সম্পর্কে গভীর জ্ঞান রাখা।

এখানে আমি SOLID প্রিন্সিপাল গুলোর গুরুত্ব, কেন ব্যবহার করা উচিত ইত্যাদি বুঝানোর সর্বাত্মক চেষ্টা করবো, তাছাড়া কিভাবে নিজের প্রজেক্টে ব্যবহার করতে পারেন সে বিষয় থাকবে কিছু কথা। তবে তার আগে জেনে নেওয়া যাক SOLID এর পূর্নরূপঃ

S — Single Responsibility Principle(SRP)

O — Open Close Principle

L — Liscov Substitution Principle

I — Interface Segregation Principle

D — Dependency Inversion Principle

এখন উপরের প্রত্যেকটা প্রিন্সিপাল আস্তে আস্তে ব্যাখ্যা করার চেষ্টা করবো, প্রথমে শুরু করা যাকঃ-

Single Responsibility Principle(SRP):

এই প্রিন্সিপাল অনুসারে, একটা ক্লাসকে শুধু মাত্র একটা কারণে পরিবর্তন করা যাবে বা একটা ক্লাস শুধু মাত্র এক ধরণের কাজে নিয়োজিত থাকতে পারবে। যদি এই ব্যাপারটা আরেকটু খোলাশা করি সেটা হচ্ছে একটা ক্লাসের উদ্দেশ্য হবে একমুখী, বহুমুখী নয়। যদি কোন ক্লাস একের অধিক দায়িত্বে থাকে সেক্ষেত্রে ক্লাসটা coupled হয়ে যায়, আর যেটা এই প্রিন্সিপালের পুরোপুরি পরিপন্থী। ব্যাপারটা নিচের উদাহরণ এর সাথে ব্যাখ্যা করা হলে ভালো বুঝতে পারবেনঃ

class Animal:
def __init__(self, name: str):
self.name = name

def get_name(self) -> str:
pass

def save(self, animal):
pass

এখানে উপরের Animal ক্লাসটি SRP মেনে চলে নি। SRP থেকে জানি একটা ক্লাস একটা কাজে নিয়োজিত থাকবে কিন্তু এখানের Animal ক্লাসটি এক সাথে দু ধরণের কাজ করছে, একটা হচ্ছে সে তার প্রপার্টিকে ম্যানেজ করছে, আরেকটায় সে তার ডাটা গুলোকে ম্যানেজ করছে সেভ করার মাধ্যমে। এখন মাথায় প্রশ্ন আসতে পারে এটা এমন কি সমস্যা, কিন্তু ব্যাপারটা সত্যিই সমস্যার হতে পারে যখন ক্লাসের কোন একটা পরিবর্তনে save() মেথডে ইফেক্ট ফেলতে পারে, আর এই পরিবর্তন রিয়েল লাইফ সিনারিওতে একেক ধরণের আসতে পারে, আর সে অনুযায়ী ইফেক্ট পড়তে পারে save() মেথডে। আর যেটাকে Domino effect এর সাথে তুলনা করতে পারেন। এই সমস্যা সমাধানের ক্ষেত্রে আমরা আলাদা একটা ক্লাস তৈরি করতে পারি এবং দায়িত্ব ভাগাভাগি করে দেওয়ার মাধ্যমে সিংগেল রিস্পনসেবল করতে পারি নিম্নরুপ ভাবেঃ

class Animal:
def __init__(self, name: str):
self.name = name

def get_name(self):
pass

class AnimalDB:
def get_animal(self) -> Animal:
pass

def save(self, animal: Animal):
pass

এক্ষেত্রে Animal, AnimalDB নিজ নিজ কাজে নিয়োজিত থাকবে এবং কাজ অনেকটা decouple হয়ে যাবে।

Open-Close Principle(OCP):

OCP অনুসারে, object বা entity গুলো extension করা যাবে, modification করা যাবে না — অর্থাৎ ক্লাসকে অবশ্যই বর্ধিত করা যাবে কিন্তু ক্লাস নিজেকে মডিফাই করতে পারবে না। এখানে ব্যাপারটা বুঝার আগে মডিফাই এবং এক্সটেনশন ব্যাপারটা বুঝতে হবে। কোন existing কোডকে পরিবর্তন করাকে মডিফিকেশন এবং কোন একটা নতুন ফিচার যুক্ত করাকে extension বলে। সার্বিকভাবে অর্থ দাঁড়ায় কোন একটা existing কোড বেইজে নতুন ফাংশনালিটি যুক্ত করা উচিত, কোন রকম কোড পরিবর্তন করা ছাড়া। কিন্তু কিভাবে সম্ভব existing কোডে কোন রকম হাত দেওয়া ছাড়া এক্সটেনশন করা? সেটাই ব্যাখ্যা করবো কোডের মাধ্যমেঃ

class Animal:
def __init__(self, name: str):
self.name = name

def get_name(self) -> str:
pass

def animal_sound(animals: list):
for animal in animals:
if animal.name == 'lion':
print('roar')

elif animal.name == 'mouse':
print('squeak')

animals = [ Animal('lion'), Animal('mouse')]
animal_sound(animals)

উপরের কোডে animals, Open-Close principle মেনে চলে নি। কিভাবে? কেননা এটা নতুন ধরনের animal এর জন্য ক্লোজ না, যদি আমরা নতুন কোন animal অব্জেক্ট যুক্ত করি সেক্ষেত্রে আমাকে অবশ্যই এই ক্লাসকে মডিফাই করতে হবে। তারমানে প্রতিটা ভিন্ন animal এর ক্ষেত্রে animal_sound পরিবর্তন হতে থাকবে, কিন্তু এটা তো কথা ছিলো না? তাহলে আমরা এখন যা করতে পারিঃ

class Animal(ABC):

@abstractmethod
def get_name(self) -> str:
pass

@abstractmethod
def make_sound(self):
pass

class Lion(Animal):
def __init__(self, name):
self.name = name

def get_name(self) -> str:
return self.name

def make_sound(self):
return 'roar'

class Mouse(Animal):
def __init__(self, name):
self.name = name

def get_name(self) -> str:
return self.name

def make_sound(self):
return 'squeak'

class Snake(Animal):
def __init__(self, name):
self.name = name

def get_name(self) -> str:
return self.name

def make_sound(self):
return 'hiss'

def animal_sound(animals: list):
for animal in animals:
print(animal.make_sound())

animals = [
Lion("Lion"),
Mouse("Mouse"),
Snake("Snake")
]
animal_sound(animals)

এটা সমাধান করেছি আমরা ইন্টারফেস ক্লাসের মাধ্যমে — অব্জেক্ট ওয়ারিয়েন্টেড প্রগ্রামিংয়ের ভাষায় ইন্টারফেস হচ্ছে এমন একটা ক্লাস যেখানে সব ফাংশনের ডেফিনেশন থাকে, ইম্পলিমেন্টেশন থাকে না, তবে যে ক্লাসে এক্সটেন্ড হবে সে ক্লাসে ইম্পলিমেন্টেশন থাকে। পাইথন যেহেতু আমাদের ইন্টারফেস দেয় না সে কারণে Animal নামের একটা abstract ক্লাস তৈরি করেছি তারপর সেটা Lion, Mouse, Snake এ ইনহেরিট করে মেথড গুলো ইম্পলিমেন্ট করেছি। animals list কে ইটারেট করা মাধ্যমে খুব সহজেই অবেজেক্ট গুলো পাচ্ছি, এক্ষেত্রে আমাদের animal_sound কে কোন ধরনের পরিবর্তন করতে হয় নি, শুধু মাত্র নতুন animal যুক্ত করেছি মাত্র।

Liskov Substitution Principle (LSP):

LSP এর মতে সাব-ক্লাস অবশ্যই প্যারেন্ট ক্লাসের মাধ্যমে পরিবর্তন যোগ্য। অর্থাৎ সাব-ক্লাস তার সুপার বা প্যারেন্ট ক্লাসের জায়গা নিতে পারে কোন রকম অপ্রীতিকর আউটপুট ছাড়া।

উপরের উদাহরণের সাথে যদি যুক্ত করি:

def animal_leg_count(animals: list):
for animal in animals:
if isinstance(animal, Lion):
print(lion_leg_count(animal))
elif isinstance(animal, Mouse):
print(mouse_leg_count(animal))
elif isinstance(animal, Pigeon):
print(pigeon_leg_count(animal))

এখানে animal_leg_count ফাংশনটি LSP অনুসরণ করছে না, একে আমরা কিভাবে LCP তে আনতে পারি? ফাংশনটার দিকে লক্ষ্য করলে দেখতে পাচ্ছি কন্ডিশনের মাধ্যমে যাচাই করার চেষ্টা করছি যে animal টা কোন ক্লাসের অবেজক্ট, তারপর সে অনুযায়ী <animal_nam>_leg_count কল করছি বারবার। এক্ষেত্রে আমরা এখন leg_count নামের একটা মেথড সুপারক্লাস Animal এ ডিফাইন করতে পারি নিচের মত করে,

class Animal(ABC):

@abstractmethod
def get_name(self) -> str:
pass

@abstractmethod
def make_sound(self):
pass

@abstractmethod
def leg_count(self):
pass

তারপর সেগুলোকে চাইল্ড বা সাব-ক্লাস গুলোতে ইম্পলিমেন্ট করতে পারি।

class Lion(Animal):
def __init__(self, name):
self.name = name

def get_name(self) -> str:
return self.name

def make_sound(self):
return 'roar'

def leg_count(self):
return 4

class Mouse(Animal):
def __init__(self, name):
self.name = name

def get_name(self) -> str:
return self.name

def make_sound(self):
return 'squeak'

def leg_count(self):
return 4
def get_animal(animal: Animal):
return animal.leg_count()

একটা বিষয় লক্ষ্য করুন এখানে কিন্তু আমাদের আলাদা ভাবে যাচাই করতে হচ্ছে না কোন অবজেক্ট কোন ক্লাসের। তাছাড়া প্রত্যেকটা Animal সাব-ক্লাস দেখতে প্রায় হবুহু Animal এর মত। আর Animal এর জায়গায় সাব-ক্লাস গুলো রিপ্লেস করা যাচ্ছে।

Interface Segregation Principle(ISP):

Segregation এর অর্থ হচ্ছে কোন বিষয় বা কিছুকে আলাদা রাখা। আর এখানে ISP এর মানে হচ্ছে ইন্টারফেসকে আলাদা করা। ইতোমধ্যে ইন্টারফেস কি সেটা বলেছি, এখানে আর সেই বিষয় নিয়ে আলোচনা করবো না। ISP প্রিন্সিপালের মতে, ক্লাইন্ট এমন কোন ইন্টারফেস তৈরি করতে ফোর্স করতে পারবে না যেটার কোন প্রয়োজনীয়তা নেই বা সেরকম ইন্টারফেস ক্রিয়েট করতে বলবে না যেটার ব্যবহার আদৌতে নেই। এই প্রিন্সিপাল মুলত বড় ইন্টারফেস তৈরির খারাপ দিক গুলো নিয়ে আলোচনা করে। নিচে কোডের মাধ্যমে বুঝানো হলো ব্যাপারতাঃ

class IShape:
def draw_square(self):
raise NotImplementedError

def draw_rectangle(self):
raise NotImplementedError

def draw_circle(self):
raise NotImplementedError

আমরা ইন্টারফেসে দেখতে পাচ্ছি তিনটা মেথড ডিফাইন করা আছে draw_square, draw_circle(), draw_rectan() এবং সেগুলো নিচের কোডে ইম্পলিমেন্ট করা হয়েছে।

class Circle(IShape):
def draw_square(self):
pass

def draw_rectangle(self):
pass

def draw_circle(self):
pass

class Square(IShape):
def draw_square(self):
pass

def draw_rectangle(self):
pass

def draw_circle(self):
pass

class Rectangle(IShape):
def draw_square(self):
pass

def draw_rectangle(self):
pass

def draw_circle(self):
pass

উপরের কোডে একটা বিষয় হাস্যকর এবং লক্ষ্যনীয় যে Circle ক্লাসেও draw_rectan ইম্পলিমেন্ট করা হয়েছে। এখন যদি নতুন একটা মেথড draw_triangle মেথড ইন্টারফেসে ডিফাইন করি সেটা সব গুলো ক্লাসে কাজে না লাগলেও ইম্পলিমেন্ট করতে হবে, যদি না করি মেথড ইম্পলিমেন্টেশনের একটা ইরোর আসবে। তারমানে কি দাড়াচ্ছে? ISP পুরোপুরি ব্যর্থ এখানে।

তা এখন কি করতে পারি আমরা, কিভাবে ব্যাপারটা হ্যান্ডেল করতে পারি? এখন আমরা কোডটাকে নিচের মত করে রিফ্যাক্টরিং করতে পারি -

class IShape:
def draw(self):
raise NotImplementedError

class Circle(IShape):
def draw(self):
pass

class Square(IShape):
def draw(self):
pass

class Rectangle(IShape):
def draw(self):
pass

এখনে আমরা কোডকে segrete করেছি, আর প্রত্যেকটা ক্লাস তারা নিজের draw মেথড ইম্পলিমেন্ট করে নিয়েছে নিজেদের মত করে।

Dependency Inversion Principle(DIP):

Objects অবশ্যই abstract এর উপর নির্ভরশীল হবে কোন ধরণের কনক্রিট ক্লাসের উপর নয়। কোন হাই লেভেল মডিউল কোন ভাবে লো-লেভেল মডিউলের উপর নির্ভর করতে পারবে না, তবে abstract এর উপর নির্ভর করতে পারবে। এই প্রিন্সিপালের মূল উদ্দেশ্য ক্লাসকে ডি-কাপলিং করা। Uncle Bob এই ব্যাপারটাকে এভাবে ব্যাখ্যা করেছে,

“যদি Open-close principle ব্যাখা করে oop এর আর্কিটেকচার, তাহলে Dependency inversion principle oop এর প্রাথমিক ম্যাকানিজমকে ব্যাখা করে”!

কোডের মাধ্যমে ব্যাপারটা বুঝতে আরো সহজ হবে:

class Apple:
def eat(self):
print(f"Eating apple. Transferring {5} unit energy to brain.")

class Chocolate:
def eat(self):
print(f"Eating chocolate")

class Robot:
def get_energy(self, eatable: str):
if eatable == "Apple":
apple = Apple()
apple.eat()
elif eatable == "Chocolate":
chocolate = Chocolate()
chocolate.eat()

একটা Robot ক্লাস, এর মধ্যে মেথড হচ্ছে get_energy(), প্যারামিটারের মাধ্যমে যাচাই করছি Apple নাকি Chocolate ক্লাসকে কল করা হবে। এখন যদি আরো নতুন কোন খাবার ক্লাস ডিফাইন করি অবশ্যই এখানে মডিফাই করে কন্ডিশন সেট করতে হবে নিচের মত করে ঃ

class Robot:
def get_energy(self, eatable: str):
if eatable == "Apple":
apple = Apple()
apple.eat()
elif eatable == "Chocolate":
chocolate = Chocolate()
chocolate.eat()
elif eatable == "IceCream":
ice_cream = IceCream()
ice_cream.eat()

তার মানে OCP মেনে চলছে না, আমাদের ক্লাসকে মডিফাই করতে হচ্ছে বারবার।

এই জন্য আমরা একটা Etable() ইন্টারফেস ক্রিয়েট করতে পারি এবং সেটা Apple এবং Chocolate এ ইনহেরিট করতে এবং মেথড গুলো ইম্পলিমেন্ট করতে পারি এবং Robot এর মেথড চেঞ্জ করতে পারি নিম্নরূপভাবেঃ

class Eatable(ABC):
def eat(self):
return NotImplemented

class Apple(Eatable):
def eat(self):
print(f"Eating apple. Transferring {5} unit energy to brain.")

class Chocolate(Eatable):
def eat(self):
print(f"Eating chocolate")

class Robot:
def get_energy(self, eatable: Eatable):
eatable.eat()

আশাকরি সকলের কিছু হলেও উপকারে আসবে এটা। যদি কোন রকম আরো গভীর প্রশ্ন থাকে, কমেন্ট করতে পারেন। আশা করি আলোচনা করা যাবে।

code reference: dmmeteo

--

--

Nur Amin Sifat

I'm a Associate Software Engineer at Brain Station 23 and Data Security learner as well.