অনেক TypeScript টিম abstract class এবং interface-কে একই জিনিসের বিকল্প হিসেবে বিবেচনা করে। কিন্তু তারা আসলে এক নয়। ভুলটি বেছে নেওয়ার ফলে বড় ধরনের সমস্যা তৈরি হতে পারে: ডুপ্লিকেট বিজনেস লজিক, রিফ্যাক্টর করা কঠিন এমন অনমনীয় ক্লাস ট্রি, এবং সূক্ষ্ম রানটাইম এরর যা কোনো একটি বেস ক্লাসের কারণে ঘটে যা কেউ নিরাপদ বলে ধরে নিয়েছিল। এর সমাধান কোনো নিয়ম মুখস্থ করা নয়। বরং আপনার প্রকৃত প্রয়োজনীয়তাগুলো বোঝা এবং সেই অনুযায়ী সঠিক টুলটি বেছে নেওয়া।
ইন্টারফেস হলো বিশুদ্ধ চুক্তি (Pure Contracts)
একটি ইন্টারফেস হলো একটি কম্পাইল-টাইম বাউন্ডারি। এটি বর্ণনা করে একটি অবজেক্ট দেখতে কেমন হবে এবং এটি কী কী করতে সক্ষম হবে, কিন্তু এতে কোনো ইমপ্লিমেন্টেশন থাকে না। TypeScript যখন JavaScript-এ কম্পাইল হয়, তখন ইন্টারফেসটি সম্পূর্ণভাবে অদৃশ্য হয়ে যায়। এটি কোনো কনস্ট্রাক্টর, প্রোটোটাইপ চেইন বা আপনার বান্ডেলে কোনো অতিরিক্ত বাইট যোগ করে না।
কল্পনা করুন আপনি একটি নোটিফিকেশন সিস্টেম তৈরি করছেন। আপনি একটি ইন্টারফেস সংজ্ঞায়িত করলেন:
interface Notifier {
send(message: string): void;
readonly channel: string;
}
যে কোনো ক্লাস, এমনকি একটি সাধারণ অবজেক্ট লিটারেলও যদি সেই শেপ বা কাঠামো মেনে চলে, তবে সেটি একটি বৈধ Notifier হিসেবে গণ্য হবে। একটি EmailNotifier এটি ইমপ্লিমেন্ট করতে পারে। একইভাবে SmsNotifier, SlackNotifier, অথবা একটি ইউনিট টেস্টের জন্য ব্যবহৃত মক অবজেক্টও এটি করতে পারে। যেহেতু ইন্টারফেসে কোনো কোড থাকে না, তাই প্রতিটি ইমপ্লিমেন্টেশন তার নিজস্ব send মেথড একদম শুরু থেকে লেখে। যখন ইমপ্লিমেন্টেশনগুলোর মধ্যে পাবলিক ইন্টারফেস ছাড়া আর কিছু শেয়ার করার প্রয়োজন নেই, তখন আপনি ঠিক এটাই চান।
অ্যাবস্ট্রাক্ট ক্লাস বাস্তব কোড বহন করে
একটি অ্যাবস্ট্রাক্ট ক্লাস হলো একটি পূর্ণাঙ্গ ক্লাস যা সরাসরি ইনস্ট্যানশিয়েট (instantiate) করা নিষিদ্ধ। এটি ফিল্ড, লজিকসহ কনক্রিট মেথড এবং কনস্ট্রাক্টর লজিক সংজ্ঞায়িত করতে পারে। এটি সাবক্লাসগুলোকে অ্যাবস্ট্রাক্ট মেথডের মাধ্যমে ফাঁকা জায়গাগুলো পূরণ করতে বাধ্য করে, তবে একই সাথে এটি তাদের উত্তরাধিকারসূত্রে প্রাপ্ত (inherited) আচরণও প্রদান করে যা তাদের নিজে থেকে লিখতে হয় না।
একটি ডেটা অ্যাক্সেস লেয়ারের কথা ভাবুন। আপনার অ্যাপ্লিকেশনের প্রতিটি রিপোজিটরির একটি আইডেন্টিফায়ার পার্স করা, একটি স্কিমার বিপরীতে সেটি যাচাই করা এবং তারপর স্টোরেজ-নির্দিষ্ট কুয়েরি চালানো প্রয়োজন। একটি ইন্টারফেস এই শেয়ার করা সিকোয়েন্সটি ধারণ করতে পারে না কারণ ইন্টারফেসে কোনো এক্সিকিউটেবল কোড থাকতে পারে না। কিন্তু একটি অ্যাবস্ট্রাক্ট ক্লাস পারে:
abstract class BaseRepository<T> {
protected validateId(id: string): boolean {
return /^[a-z0-9\-]+$/.test(id);
}
abstract fetchById(id: string): Promise<T | null>;
async findById(id: string): Promise<T | null> {
if (!this.validateId(id)) throw new Error("Invalid ID format");
return this.fetchById(id);
}
}
BaseRepository কাঠামো নিশ্চিত করে। এটি সাবক্লাসগুলোকে fetchById ইমপ্লিমেন্ট করতে বাধ্য করে। তবে এটি কার্যকর লজিকও সরবরাহ করে। সাবক্লাসগুলো বিনামূল্যে গার্ডরেইল এবং বয়লারপ্লেট কোড পেয়ে যায়। আপনি যদি এটিকে একটি ইন্টারফেস দিয়ে প্রতিস্থাপন করার চেষ্টা করেন, তবে আপনাকে প্রতিটি রিপোজিটরিতে validateId এবং findById কপি করতে হবে। এটি অ্যাবস্ট্রাকশন নয়; এটি হলো মেইনটেন্যান্সের বোঝা (maintenance tax)।
একটি প্রশ্ন যা সিদ্ধান্ত দেবে
আপনার সিদ্ধান্ত সবসময় একটি মাত্র প্রশ্নের ওপর নির্ভর করা উচিত: এই চুক্তিতে (contract) কি শেয়ার করা কোড পাঠানোর প্রয়োজন আছে?
উত্তর যদি 'না' হয়, তবে ইন্টারফেস ব্যবহার করুন। উত্তর যদি 'হ্যাঁ' হয়, তবে অ্যাবস্ট্রাক্ট ক্লাস ব্যবহার করুন।
এটি ভুল করার তাৎক্ষণিক ফলাফল রয়েছে। আপনি যদি এমন জায়গায় অ্যাবস্ট্রাক্ট ক্লাস ব্যবহার করেন যেখানে কেবল একটি শেপ বা কাঠামো থাকলেই চলত, তবে আপনি প্রতিটি ইমপ্লিমেন্টেশনকে একটি ইনহেরিটেন্স চেইনে বাধ্য করছেন। ইউনিট টেস্টগুলোর জন্য হঠাৎ করেই একটি আসল ক্লাসের অদ্ভুত মকিং বা পার্শিয়াল স্টাব (partial stub) প্রয়োজন হয়ে পড়বে। থার্ড-পার্টি এক্সটেনশনগুলো কেবল একটি শেপ অনুসরণ করার পরিবর্তে আপনার বেস ক্লাসকে সাবক্লাস করতে বাধ্য হবে। আপনি একটি সাধারণ চুক্তিকে একটি বিশাল পাহাড় বানিয়ে ফেললেন।
অন্যদিকে, যদি এমন জায়গায় ইন্টারফেস ব্যবহার করেন যেখানে শেয়ার করা আচরণ বা বিহেভিয়ার প্রয়োজন, তবে আপনি আপনার কোডবেস জুড়ে একই লজিকের কপি ছড়িয়ে ফেলবেন। যখন সেই লজিকে কোনো বাগ থাকবে, তখন আপনি সেটি এক জায়গায় ঠিক করতে পারবেন না। আপনাকে দশটি ইমপ্লিমেন্টেশনের মধ্যে খুঁজতে হবে এবং আশা করতে হবে যেন একাদশটি বাদ না পড়ে।
বাস্তবে এদের মধ্যে পার্থক্য কী
দার্শনিক পার্থক্যের বাইরেও, তিনটি ব্যবহারিক ব্যবধান এদের আলাদা করে।
পারফরম্যান্স এবং বান্ডেল সাইজ। ইন্টারফেসগুলো কম্পাইল করার সময় মুছে ফেলা হয়। এগুলো আপনার JavaScript আউটপুটে কোনো ওজন যোগ করে না এবং রানটাইমে কোনো মেমরি খরচ করে না। অ্যাবস্ট্রাক্ট ক্লাসগুলো আসল কনস্ট্রাক্টর ফাংশন এবং প্রোটোটাইপ চেইনে রূপান্তরিত হয়। আপনি যে কয়টি সংজ্ঞায়িত করবেন, সেগুলো আপনার বান্ডেলের আকার বাড়াবে এবং ইনস্ট্যানশিয়েট করার সময় মেমরিতে থাকবে। হাজার হাজার ইনস্ট্যান্স হ্যান্ডেল করা সার্ভার বা সূক্ষ্মভাবে পর্যবেক্ষণ করা ফ্রন্টএন্ড বান্ডেলের ক্ষেত্রে এই পার্থক্যটি তাত্ত্বিক নয়।
কম্পোজিশনের নমনীয়তা। একটি ক্লাস একসাথে অনেকগুলো ইন্টারফেস ইমপ্লিমেন্ট করতে পারে। আপনি হয়তো একটি FileCache তৈরি করতে পারেন যা একই সাথে Cache, Disposable, এবং EventEmitter ইমপ্লিমেন্ট করে। TypeScript এতে খুশি কারণ ইন্টারফেস কোনো হায়ারার্কি বা শ্রেণিবিন্যাস চাপিয়ে দেয় না। তবে, একটি ক্লাস কেবল একটি অ্যাবস্ট্রাক্ট ক্লাসকে এক্সটেন্ড করতে পারে। জাভাস্ক্রিপ্টের প্রোটোটাইপ চেইন মাল্টিপল ইনহেরিটেন্স সমর্থন করে না। আপনি যদি অ্যাবস্ট্রাক্ট ক্লাসের ওপর খুব বেশি নির্ভর করেন, তবে আপনি শেষ পর্যন্ত দুটি বেস ক্লাস মার্জ করার ক্লাসিক দ্বিধার সম্মুখীন হবেন যে দুটির মধ্যেই রয়েছে
