LSP (Liskov Substitution Principle) — SOLID এর তৃতীয় নীতি, যা অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিংয়ে সঠিক ইনহেরিটেন্সের শর্ত নির্ধারণ করে। নীতিটি বারবারা লিস্কভ 1987 সালে প্রণয়ন করেছিলেন এবং এভাবে আনুষ্ঠানিকভাবে উপস্থাপন করা হয়েছিল: যদি S, T এর একটি উপপ্রকার হয়, তাহলে T প্রকারের বস্তুগুলিকে S প্রকারের বস্তু দিয়ে প্রতিস্থাপন করা যেতে পারে প্রোগ্রামের বৈশিষ্ট্য পরিবর্তন না করেই। রবার্ট মার্টিনের বই Clean Architecture (2017) এ উল্লেখ করা হয়েছে, প্রতিস্থাপন নীতি দাবি করে যে উপশ্রেণীকে বেস ক্লাসের চুক্তি দুর্বল করা উচিত নয়।
মূল বিষয়
LSP (Liskov Substitution Principle) — প্রতিস্থাপন নীতি যা বারবারা লিস্কভ 1987 সালে OOPSLA সম্মেলনে প্রণয়ন করেছিলেন। আনুষ্ঠানিক সংজ্ঞা: ধরা যাক q(x) হল T প্রকারের বস্তু x এর একটি প্রমাণযোগ্য বৈশিষ্ট্য। তাহলে q(y) অবশ্যই S প্রকারের বস্তু y এর জন্য প্রমাণযোগ্য হতে হবে, যেখানে S, T এর একটি উপপ্রকার। সহজ ভাষায়: একটি উপশ্রেণীর বস্তুগুলিকে এমনভাবে আচরণ করতে হবে যাতে বেস ক্লাসের সাথে কাজ করা কোডটি উপশ্রেণীর সাথেও সঠিকভাবে কাজ করতে থাকে।
ব্যবহারিক ক্ষেত্রে, LSP মানে হল উপশ্রেণীকে বেস ক্লাসের চুক্তি লঙ্ঘন করা উচিত নয়। চুক্তিতে অন্তর্ভুক্ত রয়েছে পূর্বশর্ত (একটি পদ্ধতি কল করার জন্য কী প্রয়োজন), পরশর্ত (কলের পরে কী নিশ্চিত করা হয়), এবং ইনভেরিয়েন্ট (শর্ত যা বস্তুর জীবনকাল জুড়ে বজায় থাকে)। উপশ্রেণী পূর্বশর্ত শক্তিশালী করতে পারে বা পরশর্ত দুর্বল করতে পারে — এটিই LSP লঙ্ঘন।
LSP লঙ্ঘনের একটি চিরায়ত উদাহরণ হল একটি বর্গক্ষেত্র যা একটি আয়তক্ষেত্র থেকে উত্তরাধিকার সূত্রে প্রাপ্ত। আয়তক্ষেত্রের setWidth পদ্ধতি প্রস্থ নির্ধারণ করে, বর্গক্ষেত্রে এটি প্রস্থ এবং উচ্চতা উভয়ই নির্ধারণ করে। একজন ক্লায়েন্ট যে আয়তক্ষেত্রের আচরণ আশা করে (এক পাশ পরিবর্তন করলে অপর পাশ প্রভাবিত হয় না) একটি অপ্রত্যাশিত ফলাফল পায়। একটি বর্গক্ষেত্র আয়তক্ষেত্রের একটি বৈধ উপপ্রকার নয়।
LSP সঠিক ইনহেরিটেন্সের জন্য তিনটি শর্ত প্রতিষ্ঠা করে: উপশ্রেণীর পূর্বশর্ত বেস ক্লাসের পূর্বশর্তের চেয়ে শক্তিশালী হতে পারে না (উপশ্রেণী বেশি দাবি করে না), উপশ্রেণীর পরশর্ত বেস ক্লাসের পরশর্তের চেয়ে দুর্বল হতে পারে না (উপশ্রেণী কম নিশ্চিত করে না), এবং বেস ক্লাসের ইনভেরিয়েন্ট উপশ্রেণীতে সংরক্ষিত থাকতে হবে। এই শর্তগুলি বারট্রান্ড মেয়ারের চুক্তি দ্বারা নকশার নিয়ম নামে পরিচিত।
যদি অন্তত একটি শর্ত লঙ্ঘিত হয়, তাহলে পলিমরফিজম ব্যবহারকারী কোড ব্যর্থ হতে পারে। কম্পাইলার শব্দার্থিক চুক্তি পরীক্ষা করে না, কেবল সিনট্যাকটিক চুক্তি। তাই, LSP স্থির টাইপিংয়ের বিষয় নয়, বরং আর্কিটেকচারাল শৃঙ্খলার বিষয়।
LSP প্রক্রিয়া প্রকারের আচরণগত সামঞ্জস্যের উপর ভিত্তি করে। যদি শ্রেণী S শ্রেণী T থেকে উত্তরাধিকার সূত্রে প্রাপ্ত হয়, তাহলে ক্লায়েন্ট কোডকে S ব্যবহার করতে সক্ষম হতে হবে যেখানে T প্রত্যাশিত, তার আচরণ পরিবর্তন না করেই। এটি কেবল পদ্ধতির স্বাক্ষর নয়, তাদের শব্দার্থবিদ্যাও অন্তর্ভুক্ত করে।
LSP উপশ্রেণীকে নতুন আচরণ যোগ করতে নিষেধ করে না। কিন্তু বেস ক্লাসের জন্য লেখা কোডের প্রত্যাশা লঙ্ঘন করা নিষিদ্ধ। যদি বেস ক্লাস নিশ্চিত করে যে save পদ্ধতি ব্যতিক্রম নিক্ষেপ করে না, তাহলে উপশ্রেণী সেগুলি নিক্ষেপ করা উচিত নয়। যদি বেস ক্লাস একটি অ-ঋণাত্মক মান ফেরত দেয়, তাহলে উপশ্রেণী একটি ঋণাত্মক মান ফেরত দেওয়া উচিত নয়।
বাস্তব প্রকল্পে, LSP প্রায়শই উপশ্রেণীর পদ্ধতিতে শর্তসাপেক্ষ যুক্তি যোগ করার সময় লঙ্ঘিত হয়: «যদি শর্ত — ব্যতিক্রম নিক্ষেপ করুন», «যদি শর্ত — null ফেরত দিন»। এই প্রতিটি «আশ্চর্য» পলিমরফিজমকে দুর্বল করে এবং ক্লায়েন্ট কোডকে কল করার আগে বস্তুর প্রকার পরীক্ষা করতে বাধ্য করে — যা অবজেক্ট-ওরিয়েন্টেড ডিজাইনের মূল ধারণার বিরোধী।
মোবাইল প্রকল্পে, একটি সাধারণ LSP লঙ্ঘন ঘটে যখন বেস ViewModel তৈরি করা হয়। যদি BaseViewModel নিশ্চিত করে যে onCleared পদ্ধতি সমস্ত সম্পদ মুক্ত করে, এবং একটি উপশ্রেণী এই পদ্ধতিটিকে খালি হিসেবে ওভাররাইড করে — তাহলে পলিমরফিক onCleared কলের মাধ্যমে সম্পদ মুক্তির উপর নির্ভরশীল যেকোনো কোড ভুলভাবে কাজ করবে। LSP দাবি করে যে উপশ্রেণীকে super.onCleared() কল করতে হবে বা নিজেই একই কাজ সম্পাদন করতে হবে। LifecycleObserver এর মাধ্যমে কম্পোজিশন একটি বিকল্প যা জীবনচক্র ব্যবস্থাপনায় LSP লঙ্ঘন দূর করে।
LSP লঙ্ঘনের প্রধান সূচক এর মধ্যে রয়েছে: একটি পদ্ধতি কল করার আগে instanceof বা is এর মাধ্যমে বস্তুর প্রকার পরীক্ষা করা, খালি পদ্ধতি বাস্তবায়ন (স্টাব), NotImplementedError বা UnsupportedOperationException নিক্ষেপ করা, মানের পরিবর্তে null ফেরত দেওয়া। এই প্রতিটি প্যাটার্ন সংকেত দেয় যে উপশ্রেণীটি একটি বৈধ উপপ্রকার নয়।
আরেকটি সাধারণ লক্ষণ হল «হয়» (is-a) সম্পর্ক মডেল করার পরিবর্তে কোড পুনর্ব্যবহারের উদ্দেশ্যে ইনহেরিটেন্স। Bird শ্রেণীতে fly() পদ্ধতি রয়েছে। Penguin শ্রেণী Bird থেকে উত্তরাধিকার সূত্রে প্রাপ্ত এবং fly() কে খালি বা ব্যতিক্রম নিক্ষেপকারী হিসেবে ওভাররাইড করে। এটি LSP লঙ্ঘন: পেঙ্গুইন পাখির একটি বৈধ উপপ্রকার নয়।
মোবাইল ডেভেলপমেন্টে, স্টাব পদ্ধতি সহ বেস ViewHolder, Fragment বা ViewController ক্লাস তৈরি করার সময় LSP লঙ্ঘিত হয়। যদি একটি উপশ্রেণী বেস ক্লাসের অর্ধেক পদ্ধতি ব্যবহার না করে — তাহলে ইনহেরিটেন্স ভুলভাবে বেছে নেওয়া হয়েছে। কম্পোজিশন বা ইন্টারফেস পৃথকীকরণ সমস্যাটি আরও সঠিকভাবে সমাধান করে।
LSP যাচাই করার জন্য একটি সহজ পরীক্ষা: বেস ক্লাসের জন্য একটি ইউনিট টেস্ট লিখুন যা এর চুক্তি (ফেরত মান, ব্যতিক্রম, পার্শ্ব প্রতিক্রিয়া) যাচাই করে। প্রতিটি উপশ্রেণীর জন্য এই পরীক্ষাটি চালান। যদি পরীক্ষা ব্যর্থ হয় — LSP লঙ্ঘিত হয়েছে। এই পদ্ধতিটিকে «বেস ক্লাস চুক্তির মাধ্যমে পরীক্ষা» বলা হয়।
Android প্রকল্পে, এই ধরনের পরীক্ষা ViewModel এবং Repository এর জন্য উপযোগী। যদি BaseViewModel ত্রুটির আগে একটি Loading অবস্থা নিশ্চিত করে, এবং একটি উপশ্রেণী Loading ছাড়াই ত্রুটি নিক্ষেপ করে — পরীক্ষাটি CI স্তরে LSP লঙ্ঘন শনাক্ত করবে।
আসুন ClickListener হ্যান্ডলিং সহ একটি Android উদাহরণ দেখি। LSP লঙ্ঘন ঘটে যখন বেস বাস্তবায়ন কিছু নিশ্চিত করে এবং উপশ্রেণী তা লঙ্ঘন করে।
// গ্যারান্টি সহ বেস ক্লাস: onClick কল করা হবে
open class BaseClickListener {
open fun onClick(view: View) {
// মৌলিক হ্যান্ডলিং
}
}
// LSP লঙ্ঘন: উপশ্রেণী একটি শর্ত যোগ করে যা ব্যতিক্রম নিক্ষেপ করে
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// সঠিক সমাধান: চুক্তি লঙ্ঘিত হয়নি
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
DataSource প্রোটোকল সহ একটি iOS উদাহরণ ডেটার পরিবর্তে nil ফেরত দিয়ে LSP লঙ্ঘন প্রদর্শন করে:
// চুক্তি সহ প্রোটোকল: ডেটা বা ত্রুটি ফেরত দেয়
protocol DataProvider {
func fetchData() async throws -> [String]
}
// LSP লঙ্ঘন: ত্রুটি ছাড়াই nil ফেরত দেয়
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // ত্রুটির পরিবর্তে খালি অ্যারে
}
}
// সঠিক LSP মেনে চলা
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
একটি ব্যবহারিক নিয়ম: যদি একটি উপশ্রেণী বেস ক্লাসের চুক্তি পূরণ করতে না পারে, তাহলে এটি উপশ্রেণী হওয়া উচিত নয়। একটি বিকল্প হল ন্যূনতম চুক্তি সহ একটি ইন্টারফেস বের করা এবং প্রতিটি প্রকারে এটি নিজস্ব উপায়ে বাস্তবায়ন করা।
কম্পোজিশন সেই পরিস্থিতিতে ইনহেরিটেন্সের চেয়ে পছন্দনীয় যেখানে «হয়» (is-a) সম্পর্ক অস্পষ্ট বা শর্তসাপেক্ষ। একটি চিরায়ত উদাহরণ: Manager কি একজন Employee? হ্যাঁ। কিন্তু Square কি একটি বৈধ Rectangle? LSP বলে «না»। যদি আপনি ইনহেরিটেন্সের সঠিকতা নিয়ে সন্দেহ করেন — কম্পোজিশন বেছে নিন।
মোবাইল ডেভেলপমেন্টে, কম্পোজিশন প্রায়ই নির্ভরতা ইনজেকশন এর মাধ্যমে ব্যবহৃত হয়: বেস ক্লাস থেকে আচরণ উত্তরাধিকার সূত্রে পাওয়ার পরিবর্তে, একটি শ্রেণী এটি একটি কনস্ট্রাক্টরের মাধ্যমে গ্রহণ করে। ViewModel Repository থেকে উত্তরাধিকার সূত্রে প্রাপ্ত হয় না বরং এটিকে নির্ভরতা হিসেবে গ্রহণ করে। এটি সংজ্ঞা অনুসারে LSP লঙ্ঘন দূর করে — কোনও ইনহেরিটেন্স নেই, কোনও চুক্তি লঙ্ঘন নেই।
লক্ষণ যে ইনহেরিটেন্স কম্পোজিশন দ্বারা প্রতিস্থাপিত হওয়া উচিত: উপশ্রেণী বেস ক্লাসের কিছু পদ্ধতি ব্যবহার করে না, উপশ্রেণী খালি স্টাব দিয়ে পদ্ধতি ওভাররাইড করে, ক্লায়েন্ট কোড instanceof এর মাধ্যমে বস্তুর প্রকার পরীক্ষা করে। এই ক্ষেত্রে, ইনহেরিটেন্স ভুলভাবে বেছে নেওয়া হয়েছে এবং LSP লঙ্ঘিত হয়েছে।
ইন্টারফেস উত্তরাধিকার ছাড়াই LSP সমস্যার সমাধান করে: প্রতিটি প্রকার ঠিক সেই পদ্ধতিগুলি বাস্তবায়ন করে যা এর প্রয়োজন। fly() পদ্ধতি সহ একটি সাধারণ বেস Bird ক্লাস (যেখানে Penguin উড়তে পারে না) এর পরিবর্তে — একটি Flyable ইন্টারফেস যা কেবল উড়ন্ত পাখিরা বাস্তবায়ন করে। Penguin Bird কে fly() পদ্ধতি ছাড়াই বাস্তবায়ন করে — LSP লঙ্ঘিত হয় না।
Android আর্কিটেকচারে, এই পদ্ধতিটি পৃথকীকৃত UseCase ইন্টারফেস এর মাধ্যমে প্রয়োগ করা হয়: getAll, getById, save, delete পদ্ধতি সহ একটি বড় UseCase এর পরিবর্তে — পৃথক GetItemsUseCase, SaveItemUseCase ইন্টারফেস। একজন ক্লায়েন্ট কেবল প্রয়োজনীয় ইন্টারফেসের উপর নির্ভর করে, এবং সেই ইন্টারফেসটি বাস্তবায়নকারী যেকোনো শ্রেণী LSP দৃষ্টিকোণ থেকে সঠিক।
সচরাচর জিজ্ঞাসিত প্রশ্ন
ইনহেরিটেন্স একটি ভাষার প্রক্রিয়া; LSP হল সেই প্রক্রিয়ার সঠিক ব্যবহারের নিয়ম। ইনহেরিটেন্স স্বাক্ষর সামঞ্জস্য (সিনট্যাক্স) নিশ্চিত করে; LSP আচরণগত সামঞ্জস্য (শব্দার্থবিদ্যা) দাবি করে। LSP ছাড়া ইনহেরিটেন্স পলিমরফিজম দেয় যা রানটাইমে ভেঙে যায়।
যদি বেস ক্লাস অ-শূন্য ফেরত নিশ্চিত করে — হ্যাঁ। যদি চুক্তি null (ঐচ্ছিক মান) অনুমতি দেয় — না। LSP null নিষিদ্ধ করে না; এটি চুক্তি দুর্বল করা নিষিদ্ধ করে। বেস ক্লাসের ডকুমেন্টেশন অধ্যয়ন করুন এবং উপশ্রেণীর চুক্তি সামঞ্জস্যপূর্ণ কিনা পরীক্ষা করুন।
LSP প্রোটোকলের ক্ষেত্রে একইভাবে প্রযোজ্য যেমন ক্লাসের ক্ষেত্রে। একটি প্রোটোকল বাস্তবায়নকে শব্দার্থিক চুক্তি মেনে চলতে হবে: যদি একটি প্রোটোকল একটি পদ্ধতিকে non-throwing হিসাবে সংজ্ঞায়িত করে, তাহলে বাস্তবায়নকে ত্রুটি নিক্ষেপ করা উচিত নয়। Swift কম্পাইলার স্তরে এটি পরীক্ষা করে না — দায়িত্ব ডেভেলপারের উপর।
Kotlin এ Sealed class একটি বিশেষ ক্ষেত্রে কারণ শ্রেণীবিন্যাস বন্ধ এবং কম্পাইলারের জানা। LSP sealed class এর ক্ষেত্রে কম পরিমাণে প্রযোজ্য কারণ সমস্ত উপপ্রকার when অভিব্যক্তিতে স্পষ্টভাবে তালিকাভুক্ত। একটি সিলড উপশ্রেণীর ত্রুটি স্থানীয় হবে, লুকানো পলিমরফিক ত্রুটি নয়।
বেস ক্লাসের জন্য একটি প্যারামিটারাইজড টেস্ট লিখুন যা এর সমস্ত উপশ্রেণীর জন্য চলে। পরীক্ষাটি মূল আচরণগত চুক্তি যাচাই করে: ফেরত মান, ব্যতিক্রম, অবস্থা। যদি পরীক্ষা একটি উপশ্রেণীতে ব্যর্থ হয় — LSP লঙ্ঘিত হয়েছে। CI তে, এই ধরনের পরীক্ষা পলিমরফিক কোডের রিগ্রেশন প্রতিরোধ করে।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন