সফটওয়্যার ডেভেলপমেন্টের জগতে বড় প্রজেক্ট নিয়ে কাজ করার সময় সবচেয়ে বড় চ্যালেঞ্জ হলো কোডকে ক্লিন, মেইনটেইনেবল, ফ্লেক্সিবল এবং স্কেলেবল রাখা। একটি এলোমেলো বা টাইটলি কাপলড (tightly coupled) কোডবেসে নতুন ফিচার যোগ করা বা বাগ ফিক্স করা প্রায় অসাধ্য হয়ে পড়ে। এই সমস্যার সমাধান হিসেবে রবার্ট সি. মার্টিন (Uncle Bob) আমাদের দিয়েছেন SOLID Principles—যা অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিংকে (OOP) করে তোলে আরও শক্তিশালী, ক্লিন এবং রিইউজেবল।
সফটওয়্যার ডেভেলপমেন্টে কোড যেন সহজে বোঝা যায় এবং পরে পরিবর্তন করা যায়, তার জন্য SOLID প্রিন্সিপাল ব্যবহার করা হয়। নিচে প্রতিটি পয়েন্ট খুব সহজ করে এবং জাভা (Java) কোড দিয়ে বোঝানো হলো।
১. Single Responsibility Principle (SRP)
এই নীতি বলে যে, একটি ক্লাসের একটাই দায়িত্ব (Responsibility) থাকবে এবং ক্লাসটি পরিবর্তিত হওয়ার পেছনে কেবল একটিমাত্র কারণ থাকতে পারে।
ধরা যাক, একটি রেস্টুরেন্টে একজন ব্যক্তি একই সাথে রান্না করা, সার্ভিস দেওয়া এবং বিল হিসাব করার কাজ করছে। রান্নার রেসিপি বা বিলিং সিস্টেমে কোনো পরিবর্তন এলে ওই একই ব্যক্তিকে পুরো প্রক্রিয়ার সাথে মানিয়ে নিতে হবে, যা অত্যন্ত জটিল। SRP অনুসারে, প্রতিটি কাজের জন্য আলাদা দায়িত্বপ্রাপ্ত ব্যক্তি (যেমন: শেফ, ওয়েটার এবং ক্যাশিয়ার) থাকা উচিত।
Bad Example (SRP Violated):
class RestaurantStaff {
public void cookFood(String order) {
System.out.println("Cooking " + order);
}
public void serveCustomer(String order) {
System.out.println("Serving " + order);
}
public void calculateBill(double orderAmount) {
System.out.println("Bill is $" + orderAmount);
}
}এখানে RestaurantStaff ক্লাসটি একাধিক কাজ করছে। কোড মেইনটেইন করা কঠিন এবং এরর হওয়ার সম্ভাবনা বেশি।
Good Example (SRP Followed):
class Chef {
public void cookFood(String order) {
System.out.println("Cooking " + order);
}
}
class Waiter {
public void serveCustomer(String order) {
System.out.println("Serving " + order);
}
}
class Cashier {
public void calculateBill(double orderAmount) {
System.out.println("Bill is $" + orderAmount);
}
}এখন প্রতিটি ক্লাস একটি নির্দিষ্ট দায়িত্ব পালন করছে, যা কোডকে মডুলার এবং রিইউজেবল করে তুলেছে।
২. Open/Closed Principle (OCP)
এই নীতি বলে যে, সফটওয়্যার এন্টিটি (যেমন: ক্লাস বা মডিউল) এক্সটেনশন বা নতুন ফিচার যোগ করার জন্য উন্মুক্ত (Open) থাকবে, কিন্তু মডিফিকেশন বা পরিবর্তনের জন্য বন্ধ (Closed) থাকবে। অর্থাৎ, নতুন ফিচার যোগ করতে হলে পুরনো কোড পরিবর্তন করা যাবে না।
Bad Example (OCP Violated):
class Discount {
public double calculate(double amount, String type) {
if (type.equals("staff")) {
return amount * 0.8;
} else if (type.equals("student")) {
return amount * 0.9;
}
return amount;
}
}নতুন কোনো ডিসকাউন্ট যোগ করতে হলে মূল calculate মেথডটি বারবার পরিবর্তন করতে হবে, যা কোডকে টাইটলি কাপলড করে ফেলে।
Good Example (OCP Followed):
PHP
interface DiscountStrategy {
double calculate(double amount);
}
class StaffDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.8;
}
}
class HolidayDiscount implements DiscountStrategy {
@Override
public double calculate(double amount) {
return amount * 0.85;
}
}মূল লজিক মডিফাই না করেই নতুন ডিসকাউন্ট ক্লাস এক্সটেন্ড করে খুব সহজেই সিস্টেমে যুক্ত করা সম্ভব।
৩. Liskov Substitution Principle (LSP)
এই নীতি বলে যে, ডিরাইভড ক্লাস বা সাবক্লাস এমনভাবে তৈরি করতে হবে যাতে বেইস ক্লাসের জায়গায় তাকে ব্যবহার করলে প্রোগ্রামের আচরণ পরিবর্তিত না হয়।
Bad Example (LSP Violated):
class Vehicle {
public void startEngine() {
System.out.println("Engine started");
}
}
class Bicycle extends Vehicle {
@Override
public void startEngine() {
throw new UnsupportedOperationException("No engine to start!");
}
}Vehicle-এর সাবক্লাস হিসেবে Bicycle ব্যবহার করা হয়েছে, কিন্তু সাইকেলের ইঞ্জিন না থাকায় এটি বেইস ক্লাসের প্রত্যাশা ভঙ্গ করছে এবং এক্সেপশন থ্রো করছে।
Good Example (LSP Followed):
interface Vehicle {
void move();
}
class EngineVehicle implements Vehicle {
@Override
public void move() {
System.out.println("Vehicle is moving using engine");
}
}
class NonEngineVehicle implements Vehicle {
@Override
public void move() {
System.out.println("Vehicle is moving by pedaling");
}
}
class Car extends EngineVehicle {}
class Bicycle extends NonEngineVehicle {}এখন প্রতিটি সাবক্লাস তাদের প্যারেন্ট ক্লাসের বৈশিষ্ট্য সঠিকভাবে বহন করছে এবং প্রোগ্রামের আচরণ অপরিবর্তিত থাকছে।
৪. Interface Segregation Principle (ISP)
এই নীতি বলে যে, কোনো ক্লাসকে এমন কোনো ইন্টারফেস ইমপ্লিমেন্ট করতে বাধ্য করা যাবে না, যেখানে এমন মেথড রয়েছে যা ওই ক্লাসের কোনো দরকারই নেই।
Bad Example (ISP Violated):
interface Vehicle {
void startEngine();
void fly();
}
class Car implements Vehicle {
@Override
public void startEngine() {
System.out.println("Car engine started");
}
@Override
public void fly() {
throw new UnsupportedOperationException("Car cannot fly!");
}
}গাড়ির ওড়ার ক্ষমতা না থাকা সত্ত্বেও তাকে fly() মেথড ইমপ্লিমেন্ট করতে বাধ্য করা হয়েছে।
Good Example (ISP Followed):
interface EngineVehicle {
void startEngine();
}
interface Flyable {
void fly();
}
class Car implements EngineVehicle {
@Override
public void startEngine() {
System.out.println("Car engine started");
}
}
class Airplane implements EngineVehicle, Flyable {
@Override
public void startEngine() {
System.out.println("Airplane engine started");
}
@Override
public void fly() {
System.out.println("Airplane is flying");
}
}বড় ইন্টারফেসকে ভেঙে ছোট ও স্পেসিফিক ইন্টারফেসে রূপান্তর করায় কোনো ক্লাসকে অপ্রয়োজনীয় মেথড ইমপ্লিমেন্ট করতে হচ্ছে না।
৫. Dependency Inversion Principle (DIP)
এই নীতি বলে যে, হাই-লেভেল মডিউল কখনোই সরাসরি লো-লেভেল মডিউলের ওপর নির্ভর করবে না। বরং উভয়কেই অ্যাবস্ট্রাকশনের (Interface বা Abstract Class) ওপর নির্ভর করতে হবে। এই কনসেপ্টটি স্প্রিং বুট (Spring Boot) বা অন্যান্য ফ্রেমওয়ার্কে ডিপেন্ডেন্সি ইনজেকশনের মূল ভিত্তি।
Bad Example (DIP Violated):
class GmailService {
public void sendEmail(String to, String message) {
System.out.println("Sending Email via Gmail to " + to + ": " + message);
}
}
class NotificationService {
private GmailService gmail;
public NotificationService() {
this.gmail = new GmailService(); // direct depended (Tight Coupling)
}
public void notify(String to, String message) {
gmail.sendEmail(to, message);
}
}
এখানে NotificationService সরাসরি GmailService-এর ওপর নির্ভরশীল। নতুন কোনো ইমেইল প্রোভাইডার যুক্ত করতে চাইলে কোডে বড় ধরনের পরিবর্তন আনতে হবে।
Good Example (DIP Followed):
interface EmailService {
void sendEmail(String to, String message);
}
class GmailService implements EmailService {
@Override
public void sendEmail(String to, String message) {
System.out.println("Sending Email via Gmail to " + to + ": " + message);
}
}
class NotificationService {
private final EmailService emailService;
// Dependency on an interface via constructor injection
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}
public void notifyUser(String to, String message) {
emailService.sendEmail(to, message);
}
}এখন NotificationService যেকোনো ইমেইল প্রোভাইডারের (Gmail, Yahoo, Hotmail) সাথে কাজ করতে পারবে, শর্ত হলো প্রোভাইডারকে অবশ্যই EmailService ইন্টারফেস ইমপ্লিমেন্ট করতে হবে।
High-level বনাম Low-level মডিউল চেনার উপায়
High-level Module: এটি মূলত বিজনেস-লেভেল ফিচার। এটি নির্দেশ করে "কী করতে হবে"। Low-level Module: এটি কাজের ইমপ্লিমেন্টেশন ডিটেইলস সামলায়। এটি নির্দেশ করে কাজটা "কীভাবে করতে হবে"।
বাস্তব উদাহরণ:
PaymentService: High-level মডিউল নির্দেশ করে যে কাস্টমারের পেমেন্ট নিতে হবে। Low-level মডিউল (StripeGateway, PaypalGateway) নির্ধারণ করে কীভাবে পেমেন্ট প্রসেস হবে।
ReportGenerator: High-level মডিউল নির্দেশ করে রিপোর্ট তৈরি করতে হবে। Low-level মডিউল (PdfExporter, ExcelExporter) নির্ধারণ করে আউটপুট কোন ফরম্যাটে হবে।