সলিড ডিজাইনের নীতিগুলি হল সবচেয়ে গুরুত্বপূর্ণ ডিজাইনের নীতিগুলি যা আপনাকে পরিষ্কার কোড লিখতে জানতে হবে৷ সলিড নীতিগুলির উপর একটি শক্ত কমান্ড থাকা যে কোনও প্রোগ্রামারের জন্য একটি অপরিহার্য দক্ষতা। তারা ভিত্তি যার উপর অন্যান্য নকশা নিদর্শন বিকশিত হয়.
এই নিবন্ধে, আমরা বাস্তব জীবনের কিছু উদাহরণ ব্যবহার করে সলিড ডিজাইনের নীতিগুলি মোকাবেলা করব এবং তাদের গুরুত্ব বুঝতে পারব।
পলিমরফিজম, বিমূর্ততা এবং উত্তরাধিকারের সাথে একত্রে, উদ্দেশ্য-ভিত্তিক প্রোগ্রামিংয়ে ভাল হওয়ার জন্য সলিড নীতিগুলি সত্যিই গুরুত্বপূর্ণ।
সলিড নীতিগুলি একাধিক কারণে গুরুত্বপূর্ণ:
আসুন এখন বাস্তব-বিশ্বের উদাহরণ সহ প্রতিটি সলিড নীতিগুলি বিশদভাবে অন্বেষণ করি।
SOLID নীতিতে S এর অর্থ একক দায়িত্ব নীতি। একক দায়বদ্ধতার নীতি বলে যে একটি শ্রেণীর পরিবর্তনের শুধুমাত্র একটি কারণ থাকা উচিত। এটি আমাদের প্রকল্পে অতিরিক্ত প্রয়োজনীয়তাগুলি অন্তর্ভুক্ত করার সময় আমাদের পরিবর্তন করতে হবে এমন স্থানের সংখ্যা সীমিত করে৷
প্রতিটি ক্লাস পরিবর্তন করার জন্য ঠিক একটি কারণ থাকা উচিত।
উদাহরণ স্বরূপ, ধরা যাক আমরা জাভাতে একটি ব্যাঙ্কিং অ্যাপ্লিকেশন ডিজাইন করছি যেখানে আমাদের একটি SavingsAccount
ক্লাস রয়েছে যা ডেবিট, ক্রেডিট এবং sendUpdates
এর মত মৌলিক ক্রিয়াকলাপগুলিকে অনুমতি দেয়৷ sendUpdate
পদ্ধতিটি NotificationMedium
নামে একটি enum নেয় (যেমন ইমেল, SMS, ইত্যাদি) এবং উপযুক্ত মাধ্যমে আপডেট পাঠায়। আমরা নীচে দেখানো হিসাবে এটির জন্য কোড লিখব।
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }
public enum NotificationMedium { SMS, EMAIL }
এখন, আপনি যদি উপরের SavingsAccount
ক্লাসটি দেখেন, এটি একাধিক কারণে পরিবর্তন হতে পারে:
SavingsAccount
ক্লাসের মূল যুক্তিতে (যেমন debit
, credit
ইত্যাদি) কোনো পরিবর্তন হলে।
যদি ব্যাঙ্ক একটি নতুন বিজ্ঞপ্তি মাধ্যম চালু করার সিদ্ধান্ত নেয় (আসুন WhatsApp বলি)।
এটি সলিড নীতিতে একক দায়িত্ব নীতির লঙ্ঘন। এটি ঠিক করতে, আমরা একটি পৃথক ক্লাস করব যা বিজ্ঞপ্তি পাঠাবে।
চলুন উপরের কোডটিকে সলিড নীতি অনুসারে রিফ্যাক্টর করি
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }
public enum NotificationMedium { SMS, EMAIL }
public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }
এখন, যেহেতু আমরা কোডটি রিফ্যাক্টর করেছি, যদি NotificationMedium
বা ফরম্যাটে কোনো পরিবর্তন হয়, আমরা Sender
ক্লাস পরিবর্তন করব। যাইহোক, SavingsAccount
এর মূল যুক্তিতে যদি পরিবর্তন হয়, তাহলে SavingsAccount
ক্লাসে পরিবর্তন হবে।
এটি লঙ্ঘনটি ঠিক করে যা আমরা প্রথম উদাহরণে লক্ষ্য করেছি।
ওপেন ক্লোজ নীতি বলে যে আমাদের ক্লাসগুলি এমনভাবে ডিজাইন করা উচিত যাতে সেগুলি এক্সটেনশনের জন্য উন্মুক্ত থাকে (অতিরিক্ত বৈশিষ্ট্য যুক্ত করার ক্ষেত্রে) কিন্তু পরিবর্তনের জন্য বন্ধ থাকে। পরিবর্তনের জন্য বন্ধ থাকা আমাদের দুটি উপায়ে সাহায্য করে:
ওপেন ক্লোজ নীতির একটি উদাহরণ দেখতে, আসুন একটি শপিং কার্ট (যেমন ই-কমার্স ওয়েবসাইটগুলিতে প্রয়োগ করা হয়) এর উদাহরণটি দেখুন।
আমি Cart
নামে একটি ক্লাস তৈরি করতে যাচ্ছি যাতে Item
(গুলি) এর একটি তালিকা থাকবে যা আপনি এতে যোগ করতে পারেন। আইটেমের প্রকারের উপর নির্ভর করে এবং এর উপর ট্যাক্সেশন আমরা একটি পদ্ধতি তৈরি করতে চাই যা Cart
শ্রেণীর ভিতরে মোট কার্টের মান গণনা করে।
ক্লাস এক্সটেনশনের জন্য খোলা এবং পরিবর্তনের জন্য বন্ধ করা উচিত
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }
@Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
public enum ItemType { ELECTRONIC, GIFT }
উপরের উদাহরণে, calculateCartValue
পদ্ধতি কার্টের ভিতরে থাকা সমস্ত আইটেমের উপর পুনরাবৃত্তি করে এবং আইটেমের প্রকারের উপর ভিত্তি করে যুক্তি প্রয়োগ করে কার্টের মান গণনা করে।
যদিও এই কোডটি সঠিক দেখায়, এটি সলিড নীতিগুলি লঙ্ঘন করে৷
ধরা যাক কার্ট মান গণনা করার সময় আমাদের একটি ভিন্ন ধরনের আইটেমের জন্য একটি নতুন নিয়ম যোগ করতে হবে (মুদিখানা বলুন)। সেক্ষেত্রে, আমাদের মূল ক্লাস Cart
সংশোধন করতে হবে এবং এর ভিতরে শর্ত else if
অন্য একটি লিখতে হবে যা Grocery
টাইপের আইটেমগুলির জন্য পরীক্ষা করে।
যাইহোক, সামান্য রিফ্যাক্টরিংয়ের মাধ্যমে, আমরা কোডটিকে খোলা/বন্ধ নীতি মেনে চলতে পারি। দেখা যাক কিভাবে।
প্রথমে, আমরা Item
ক্লাসটিকে বিমূর্ত করব এবং নীচের মত বিভিন্ন ধরণের Item
(গুলি) এর জন্য কংক্রিট ক্লাস করব।
public abstract class Item { protected double price; public double getValue() { return price; } }
public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }
public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }
public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }
GroceryItem
, GiftItem
, এবং ElectronicItem
এর মতো প্রতিটি কংক্রিট আইটেম ক্লাসের ভিতরে getValue()
পদ্ধতি প্রয়োগ করে যা ট্যাক্সেশন এবং মান গণনার জন্য ব্যবসায়িক যুক্তি ধারণ করে।
এখন, আমরা Cart
ক্লাসকে অ্যাবস্ট্রাক্ট ক্লাস Item
উপর নির্ভর করব এবং নীচে দেখানো প্রতিটি আইটেমের জন্য getValue()
পদ্ধতি চালু করব।
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }
এখন, এই রিফ্যাক্টর কোডে, এমনকি যদি নতুন ধরনের Item
(গুলি) চালু করা হয়, Cart
শ্রেণী অপরিবর্তিত থাকে। পলিমরফিজমের কারণে, items
ArrayList
এর ভিতরে Item
আসল ধরন যাই হোক না কেন, সেই ক্লাসের getValue()
পদ্ধতিটি চালু করা হবে।
লিসকভের প্রতিস্থাপন নীতি বলে যে একটি প্রদত্ত কোডে, এমনকি যদি আমরা একটি সুপারক্লাসের অবজেক্টকে চাইল্ড ক্লাসের একটি বস্তু দিয়ে প্রতিস্থাপন করি, কোডটি ভাঙা উচিত নয়। অন্য কথায়, যখন একটি সাবক্লাস একটি সুপারক্লাসের উত্তরাধিকারী হয় এবং তার পদ্ধতিগুলিকে ওভাররাইড করে, তখন এটি সুপার ক্লাসে পদ্ধতির আচরণের সাথে সামঞ্জস্য বজায় রাখা উচিত।
উদাহরণস্বরূপ, যদি আমরা নিম্নলিখিত শ্রেণীগুলিকে Vehicle
এবং দুটি শ্রেণীর Car
এবং Bicycle
শ্রেণী করি। এখন, ধরা যাক আমরা গাড়ির ক্লাসের মধ্যে startEngine()
নামে একটি পদ্ধতি তৈরি করি, এটি Car
ক্লাসে ওভাররাইড করা যেতে পারে, তবে এটি Bicycle
ক্লাসে অসমর্থিত হবে কারণ Bicycle
একটি ইঞ্জিন নেই (নীচে কোড নমুনা দেখুন)
মেথড ওভাররাইড করার সময় সাবক্লাসের সুপারক্লাসের আচরণের সাথে সামঞ্জস্য বজায় রাখা উচিত।
class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }
এখন, ধরা যাক এমন কিছু কোড আছে যা টাইপ গাড়ির একটি বস্তুর প্রত্যাশা করে এবং startEngine()
পদ্ধতির উপর নির্ভর করে। যদি, কোডের টুকরোটিকে কল করার সময় Vehicle
টাইপের একটি বস্তুকে পাস করার পরিবর্তে আমরা একটি Bicycle
অবজেক্টকে পাস করি, এটি কোডে সমস্যা সৃষ্টি করবে। যেহেতু startEngine()
মেথড কল করা হলে Bicycle
(গুলি) ক্লাসের পদ্ধতি একটি ব্যতিক্রম ছুঁড়ে দেবে। এটি সলিড নীতির লঙ্ঘন হবে (লিসকভের প্রতিস্থাপন নীতি)
এই সমস্যাটি সমাধান করার জন্য, আমরা MotorizedVehicle
এবং NonMotorizedVehicle
দুটি ক্লাস তৈরি করতে পারি এবং MotorizedVehicle
ক্লাস থেকে Car
ইনহেরিট এবং NonMotorizedVehicle
থেকে Bicycle
ইনহেরিট পেতে পারি।
class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }
সলিড প্রিন্সিপলে "I" মানে ইন্টারফেস সেগ্রিগেশন প্রিন্সিপল।
ইন্টারফেস সেগ্রিগেশন নীতিটি বলে যে বড় ইন্টারফেস থাকার পরিবর্তে যা প্রয়োগকারী ক্লাসগুলিকে অব্যবহৃত পদ্ধতিগুলি প্রয়োগ করতে বাধ্য করে, আমাদের ছোট ইন্টারফেস থাকা উচিত এবং ক্লাসগুলি প্রয়োগ করা উচিত। এইভাবে, ক্লাসগুলি শুধুমাত্র প্রাসঙ্গিক পদ্ধতি প্রয়োগ করে এবং পরিষ্কার থাকে।
আপনার ইন্টারফেসগুলিকে একটি বড় ইন্টারফেসের পরিবর্তে একাধিক ছোট ইন্টারফেসে ভাগ করুন।
উদাহরণস্বরূপ, জাভাতে বিল্ট-ইন কালেকশন ফ্রেমওয়ার্ক দেখি। অন্যান্য ডেটা স্ট্রাকচারের মধ্যে, Java LinkedList
এবং ArrayList
ডেটা স্ট্রাকচারও প্রদান করে,
ArrayList
ক্লাস নিম্নলিখিত ইন্টারফেস (গুলি) প্রয়োগ করে: Serializable
, Cloneable
, Iterable
, Collection
, List
এবং RandomAccess
।
LinkedList
ক্লাস Serializable
, Cloneable
, Iterable
, Collection
, Deque
, List
এবং Queue
প্রয়োগ করে।
যে ইন্টারফেস বেশ অনেক!
এতগুলি ইন্টারফেস থাকার পরিবর্তে, জাভা বিকাশকারীরা Serializable
, Cloneable
, Iterable
, Collecton
, List
, এবং RandomAccess
একটি ইন্টারফেসে একত্রিত করতে পারত, ধরা যাক IList
ইন্টারফেস। এখন, ArrayList
এবং LinkedList
উভয় ক্লাসই এই নতুন IList
ইন্টারফেসটি বাস্তবায়ন করতে পারত।
যাইহোক, যেহেতু LinkedList
এলোমেলো অ্যাক্সেস সমর্থন করে না, এটি RandomAccess
ইন্টারফেসে পদ্ধতিগুলি প্রয়োগ করতে পারে এবং কেউ এটিকে কল করার চেষ্টা করলে UnsupportedOperationException
ফেলে দিতে পারে।
যাইহোক, এটি সলিড প্রিন্সিপলে ইন্টারফেস সেগ্রিগেশন নীতির লঙ্ঘন হবে কারণ এটি লিঙ্কডলিস্ট ক্লাসকে RandomAccess
ইন্টারফেসের অভ্যন্তরে পদ্ধতিগুলি প্রয়োগ করতে "বাধ্য" করবে যদিও প্রয়োজন নেই৷
অতএব, সাধারণ আচরণের উপর ভিত্তি করে ইন্টারফেসটিকে বিভক্ত করা এবং প্রতিটি ক্লাসকে একটি বড় ইন্টারফেসের পরিবর্তে অনেকগুলি ইন্টারফেস প্রয়োগ করতে দেওয়া ভাল।
ডিপেনডেন্সি ইনভার্সন প্রিন্সিপল বলে যে উপরের স্তরের ক্লাসগুলি সরাসরি নীচের স্তরের ক্লাসের উপর নির্ভর করবে না। এটি দুটি স্তরের বি/ডব্লিউ একটি টাইট কাপলিং ঘটায়।
এর পরিবর্তে, নিম্ন শ্রেণীর একটি ইন্টারফেস প্রদান করা উচিত যার উপর উচ্চ-স্তরের শ্রেণীগুলি নির্ভর করবে।
ক্লাসের চেয়ে ইন্টারফেসের উপর নির্ভর করুন
উদাহরণ স্বরূপ, চলুন আমরা উপরে Cart
উদাহরণটি দেখেছি তা চালিয়ে যাই এবং কিছু অর্থপ্রদানের বিকল্প যোগ করতে এটিকে উন্নত করি। ধরা যাক আমাদের কাছে DebitCard
এবং Paypal
দুই ধরনের পেমেন্ট অপশন আছে। এখন, Cart
ক্লাসে, আমরা placeOrder
একটি পদ্ধতি যোগ করতে চাই যা কার্টের মান গণনা করবে এবং সরবরাহকৃত অর্থপ্রদানের ভিত্তিতে অর্থপ্রদান শুরু করবে। পদ্ধতি
এটি করার জন্য, আমরা Cart
ক্লাসের ভিতরে ক্ষেত্র হিসাবে দুটি অর্থপ্রদানের বিকল্প যোগ করে উপরে Cart
উদাহরণে নির্ভরতা যোগ করতে পারতাম। যাইহোক, এটি DebitCard
এবং Paypal
ক্লাসের সাথে Cart
ক্লাসকে শক্তভাবে সংযুক্ত করবে।
এর পরিবর্তে, আমরা একটি Payment
ইন্টারফেস তৈরি করব এবং DebitCard
এবং Paypal
উভয় ক্লাসেই Payment
ইন্টারফেসগুলি বাস্তবায়ন করব। এখন, Cart
শ্রেণী Payment
ইন্টারফেসের উপর নির্ভর করবে, ব্যক্তিগত অর্থপ্রদানের প্রকারের উপর নয়। এটি ক্লাসগুলিকে ঢিলেঢালাভাবে সংযুক্ত রাখে।
নীচের কোড দেখুন.
public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }
আপনি যদি অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং, GoF ডিজাইন প্যাটার্ন এবং নিম্ন-স্তরের ডিজাইন ইন্টারভিউ সম্পর্কে আরও জানতে আগ্রহী হন, তাহলে আমার উচ্চ রেট দেওয়া কোর্স অবজেক্ট ওরিয়েন্টেড প্রোগ্রামিং + জাভা ডিজাইন প্যাটার্নস দেখুন