آموزش مفاهیم شی گرایی در TypeScript
آموزش مفاهیم شیگرایی در TypeScript به توسعهدهندگان کمک میکند تا کدی ساختارمند، قابل نگهداری و مقیاسپذیر بنویسند. با بهرهگیری از اصول OOP، میتوان پیچیدگی پروژههای نرمافزاری را مدیریت کرده و به بهبود کیفیت کلی کد دست یافت. این زبان با ارائه قابلیتهای شیگرا، ابزاری قدرتمند برای مهندسی نرمافزار فراهم میکند. در دنیای پرشتاب توسعه نرمافزار امروز، نیاز به کدهای تمیز، ماژولار و قابل توسعه بیش از هر زمان دیگری احساس میشود. TypeScript، به عنوان یک ابرمجموعه از JavaScript که قابلیتهای تایپ استاتیک را ارائه میدهد، بستری ایدهآل برای پیادهسازی مفاهیم برنامهنویسی شیگرا (OOP) است. این زبان با ترکیب انعطافپذیری JavaScript و ساختار منظم OOP، به توسعهدهندگان امکان میدهد تا سیستمهای نرمافزاری پیچیده را با اطمینان و کارایی بالا طراحی کنند. یادگیری و تسلط بر این مفاهیم، گامی اساسی برای هر برنامهنویسی است که به دنبال ارتقاء مهارتهای خود و ساخت برنامههای قویتر است. آموزش TypeScript برای رسیدن به این هدف ضروری است و مجتمع فنی تهران با ارائه دورههای تخصصی در این زمینه، این مسیر را هموار میکند.
مروری بر برنامهنویسی شیگرا (OOP)
برنامهنویسی شیگرا یک پارادایم برنامهنویسی است که بر مفهوم “اشیاء” استوار است. این اشیاء میتوانند حاوی دادهها به شکل فیلدها (ویژگیها) و کد به شکل رویهها (متدها) باشند. هدف اصلی OOP، سازماندهی کد به گونهای است که بازتابدهنده دنیای واقعی باشد و بتواند تعاملات بین اجزای مختلف سیستم را به شکلی منطقی و قابل فهم مدیریت کند.
تفاوت برنامهنویسی رویهای و شیگرا
در برنامهنویسی رویهای، تمرکز اصلی بر روی توابع و دنبالهای از دستورات برای انجام یک وظیفه است. دادهها و توابع معمولاً از هم جدا هستند. این روش در پروژههای کوچک کاربردی است، اما با افزایش پیچیدگی سیستم، مدیریت و نگهداری کد دشوار میشود. در مقابل، برنامهنویسی شیگرا دادهها و توابع مرتبط با آنها را در قالب یک “شیء” در کنار هم قرار میدهد. این رویکرد به ایجاد کدهای ماژولارتر و قابل استفاده مجدد منجر میشود که مدیریت آنها در پروژههای بزرگ بسیار آسانتر است.
مفاهیم بنیادی: شیء (Object) و کلاس (Class)
در OOP، کلاس را میتوان به عنوان یک نقشه یا الگو برای ساخت اشیاء در نظر گرفت. کلاسها ساختار و رفتار مشترک اشیاء یک نوع خاص را تعریف میکنند، اما خودشان به طور مستقیم دادهای را نگهداری نمیکنند. به عنوان مثال، کلاس “Car” میتواند ویژگیهایی مانند “برند”، “مدل” و “رنگ” و متدهایی مانند “روشن کردن موتور” را تعریف کند. شیء (یا نمونه) در واقع یک موجودیت واقعی است که بر اساس یک کلاس ساخته میشود. هر شیء دارای مقادیر خاص خود برای ویژگیها و میتواند متدهای تعریف شده در کلاس خود را اجرا کند. برای مثال، “myCar” میتواند یک شیء از کلاس “Car” باشد که برند آن “BMW” و مدل آن “X5” است.
چهار رکن اصلی OOP
چهار رکن اصلی برنامهنویسی شیگرا عبارتند از:
- کپسولهسازی (Encapsulation): پنهانسازی جزئیات داخلی یک شیء و کنترل دسترسی به دادهها و توابع آن.
- ارثبری (Inheritance): امکان ایجاد کلاسهای جدید بر اساس کلاسهای موجود، به منظور استفاده مجدد از کد.
- چندریختی (Polymorphism): توانایی یک شیء برای گرفتن اشکال مختلف یا نمایش رفتارهای متفاوت در زمینههای گوناگون.
- انتزاع (Abstraction): تمرکز بر روی اطلاعات ضروری و پنهانسازی جزئیات پیادهسازی غیرضروری.
این اصول پایههای محکمی را برای طراحی نرمافزارهای قدرتمند و انعطافپذیر فراهم میکنند. کلاس آموزش TypeScript در مجتمع فنی تهران به صورت جامع به این مفاهیم میپردازد.
تسلط بر مفاهیم شیگرایی در TypeScript، نه تنها به بهبود کیفیت کد کمک میکند، بلکه راه را برای درک عمیقتر الگوهای طراحی و اصول SOLID هموار میسازد.
کلاسها و اشیاء: بلوکهای سازنده در TypeScript
TypeScript به طور کامل از مفاهیم کلاس و شیء پشتیبانی میکند که ستون فقرات برنامهنویسی شیگرا را تشکیل میدهند. این قابلیتها به برنامهنویسان کمک میکنند تا کدی سازمانیافته و قابل نگهداری ایجاد کنند. آموزش TypeScript مجتمع فنی تهران بر این اصول اساسی تاکید دارد.
تعریف کلاس در TypeScript
برای تعریف یک کلاس در TypeScript، از کلیدواژه class استفاده میشود. این کلاس میتواند شامل ویژگیها (Properties) و متدها (Methods) باشد که ساختار و رفتار اشیاء ساخته شده از آن کلاس را مشخص میکنند.
class Car { brand: string; model: string; year: number; constructor(brand: string, model: string, year: number) { this.brand = brand; this.model = model; this.year = year; } startEngine(): void { console.log(`${this.brand} ${this.model} engine started.`); } }
در این مثال، کلاس Car با سه ویژگی brand، model و year و یک متد startEngine() تعریف شده است.
ایجاد شیء (Instance)
برای ساخت یک شیء (یا نمونه) از یک کلاس، از کلیدواژه new استفاده میشود. این فرایند، یک نمونه جدید از کلاس ایجاد کرده و سازنده (Constructor) کلاس را فراخوانی میکند.
const myCar = new Car(“BMW”, “X5”, 2023); myCar.startEngine(); // خروجی: BMW X5 engine started.
myCar یک شیء از کلاس Car است که با مقادیر مشخصی مقداردهی اولیه شده است.
Constructor (سازنده)
Constructor یک متد ویژه در کلاس است که هنگام ایجاد یک شیء جدید از آن کلاس به طور خودکار فراخوانی میشود. نقش اصلی آن، مقداردهی اولیه ویژگیهای شیء است. میتوان پارامترهایی را به Constructor ارسال کرد تا ویژگیهای شیء با مقادیر اولیه مورد نظر تنظیم شوند. در داخل Constructor، از کلمه کلیدی this برای ارجاع به ویژگیهای شیء فعلی استفاده میشود.
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } const person1 = new Person(“Ali”, 30);
Properties (ویژگیها) و Methods (متدها)
ویژگیها (Properties) متغیرهایی هستند که دادههای مربوط به یک شیء را ذخیره میکنند و میتوانند از انواع دادهای مختلفی باشند. متدها (Methods) توابعی هستند که رفتار یک شیء را تعریف میکنند؛ آنها عملیاتی را روی دادههای شیء انجام میدهند یا کاری را با استفاده از آنها انجام میدهند.
class Bicycle { color: string; // ویژگی speed: number; // ویژگی constructor(color: string, speed: number) { this.color = color; this.speed = speed; } accelerate(amount: number): void { // متد this.speed += amount; console.log(`Current speed: ${this.speed}`); } } const myBike = new Bicycle(“Blue”, 10); myBike.accelerate(5); // خروجی: Current speed: 15
Access Modifiers (محدودکنندههای دسترسی)
TypeScript سه محدودکننده دسترسی (public, private, protected) را برای کنترل قابلیت دسترسی به ویژگیها و متدهای یک کلاس فراهم میکند:
- public: اعضای public از هر کجای برنامه قابل دسترسی هستند. این حالت پیشفرض است.
- private: اعضای private فقط در داخل کلاسی که تعریف شدهاند قابل دسترسی هستند.
- protected: اعضای protected در داخل کلاسی که تعریف شدهاند و همچنین در کلاسهای فرزند آن قابل دسترسی هستند.
class BankAccount { public accountNumber: string; private _balance: number; protected ownerName: string; constructor(accNum: string, initialBalance: number, owner: string) { this.accountNumber = accNum; this._balance = initialBalance; this.ownerName = owner; } deposit(amount: number): void { this._balance += amount; console.log(`New balance: ${this._balance}`); } }
استفاده صحیح از این محدودکنندهها برای اعمال کپسولهسازی و حفظ یکپارچگی دادهها حیاتی است.
Readonly Properties
ویژگیهای readonly ویژگیهایی هستند که فقط هنگام مقداردهی اولیه در Constructor یا در زمان تعریف قابل تنظیم هستند و پس از آن نمیتوان مقدار آنها را تغییر داد. این ویژگیها برای مقادیری که باید ثابت باقی بمانند، مفید هستند.
class ImmutablePoint { readonly x: number; readonly y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } const p = new ImmutablePoint(10, 20); // p.x = 30; // خطا: نمیتوان یک ویژگی readonly را تغییر داد
Static Members (اعضای استاتیک)
اعضای استاتیک (ویژگیها و متدها) به خود کلاس تعلق دارند، نه به نمونههای کلاس. این بدان معناست که برای دسترسی به آنها نیازی به ایجاد شیء از کلاس نیست و میتوان مستقیماً از طریق نام کلاس به آنها دسترسی پیدا کرد. اعضای استاتیک برای توابع ابزاری، ثابتها یا دادههایی که بین تمام نمونههای یک کلاس مشترک هستند، استفاده میشوند.
class MathUtils { static PI: number = 3.14159; static calculateCircleArea(radius: number): number { return this.PI radius radius; } } console.log(MathUtils.PI); // خروجی: 3.14159 console.log(MathUtils.calculateCircleArea(5)); // خروجی: 78.53975
در دوره آموزش TypeScript، تمامی این جزئیات به صورت عملی آموزش داده میشوند.
کپسولهسازی (Encapsulation): حفاظت از دادهها
کپسولهسازی یکی از ارکان اصلی برنامهنویسی شیگراست که به پنهانسازی جزئیات پیادهسازی داخلی یک شیء و کنترل دسترسی به دادههای آن میپردازد. این اصل، شیء را به یک واحد خودکفا تبدیل میکند که دادهها و منطق مربوط به آنها را در کنار هم نگهداری میکند.
تعریف کپسولهسازی
کپسولهسازی به معنای بستهبندی دادهها (ویژگیها) و متدهایی (توابع) که روی آن دادهها کار میکنند، در یک واحد به نام کلاس است. علاوه بر این، کپسولهسازی امکان پنهانسازی اطلاعات (Information Hiding) را فراهم میکند؛ یعنی جزئیات داخلی یک شیء از دنیای خارج پنهان میشوند و فقط از طریق یک رابط عمومی (Public Interface) مشخص میتوان با آن تعامل کرد. این کار باعث کاهش وابستگیها و افزایش انعطافپذیری کد میشود.
پیادهسازی با Access Modifiers
همانطور که قبلاً اشاره شد، محدودکنندههای دسترسی private و protected ابزارهای اصلی برای پیادهسازی کپسولهسازی در TypeScript هستند. با استفاده از private، میتوان ویژگیها یا متدهایی را تعریف کرد که فقط در داخل همان کلاس قابل دسترسی باشند و از دسترسی مستقیم از بیرون جلوگیری شود. این کار از تغییرات ناخواسته یا دسترسی غیرمجاز به دادههای حساس جلوگیری میکند.
class TemperatureSensor { private _currentTemp: number; constructor(initialTemp: number) { this._currentTemp = initialTemp; } // متد داخلی برای تنظیم دما private adjustReading(offset: number): void { this._currentTemp += offset; } // متد عمومی برای دریافت دما getTemperature(): number { // ممکن است در اینجا منطق تبدیل واحد یا اعتبارسنجی اضافه شود return this._currentTemp; } // متد عمومی برای تغییر دما به صورت کنترل شده updateTemperature(newReading: number): void { if (newReading >= -20 && newReading <= 50) { // اعتبارسنجی this._currentTemp = newReading; } else { console.error(“Invalid temperature reading.”); } } } const sensor = new TemperatureSensor(25); // console.log(sensor._currentTemp); // خطا: _currentTemp یک ویژگی private است console.log(sensor.getTemperature()); // خروجی: 25 sensor.updateTemperature(28); console.log(sensor.getTemperature()); // خروجی: 28
Getter و Setter (Accessor Properties)
Getter و Setter متدهای خاصی هستند که به شما امکان میدهند دسترسی به ویژگیهای خصوصی یک کلاس را کنترل کنید. Getter (با کلیدواژه get) برای خواندن مقدار یک ویژگی خصوصی و Setter (با کلیدواژه set) برای نوشتن مقدار یک ویژگی خصوصی استفاده میشود. این متدها امکان افزودن منطق اعتبارسنجی، تبدیل دادهها یا هر پردازش دیگری را قبل از خواندن یا نوشتن فراهم میکنند.
class Product { private _price: number; private _name: string; constructor(name: string, price: number) { this._name = name; this._price = price; } get price(): number { return this._price; } set price(value: number) { if (value >= 0) { this._price = value; } else { console.error(“Price cannot be negative.”); } } get name(): string { return this._name; } set name(value: string) { if (value.length > 2) { this._name = value; } else { console.error(“Product name must be at least 3 characters.”); } } } const item = new Product(“Laptop”, 1200); item.price = 1250; // فراخوانی setter console.log(item.price); // فراخوانی getter, خروجی: 1250 item.price = -100; // خطا: Price cannot be negative. item.name = “PC”; // خطا: Product name must be at least 3 characters.
اهمیت کپسولهسازی
کپسولهسازی چندین مزیت کلیدی دارد:
- کاهش پیچیدگی: با پنهانسازی جزئیات داخلی، اشیاء از دید کاربر سادهتر به نظر میرسند.
- افزایش امنیت: از دستکاری مستقیم و ناخواسته دادههای حساس جلوگیری میکند.
- سهولت نگهداری کد: تغییرات در پیادهسازی داخلی یک کلاس، تأثیری بر کدهایی که از آن کلاس استفاده میکنند، نخواهد داشت، به شرطی که رابط عمومی ثابت بماند.
- انعطافپذیری: امکان تغییر پیادهسازی داخلی بدون نیاز به تغییر کدهای وابسته را فراهم میکند.
دوره تایپ اسکریپت در مجتمع فنی تهران، تمرینات عملی زیادی را برای درک عمیق این مفهوم ارائه میدهد.
ارثبری (Inheritance): استفاده مجدد از کد
ارثبری مکانیزمی قدرتمند در برنامهنویسی شیگرا است که به یک کلاس امکان میدهد ویژگیها و متدهای کلاس دیگری را به ارث ببرد. این اصل، ستون فقرات استفاده مجدد از کد و ایجاد سلسله مراتب منطقی بین انواع مختلف اشیاء را فراهم میکند.
تعریف ارثبری
ارثبری به این معنی است که میتوانیم کلاسهای جدیدی را بر اساس کلاسهای موجود ایجاد کنیم. کلاس موجود که ویژگیها و متدهای خود را به ارث میگذارد، “کلاس والد”، “کلاس پایه” یا “Superclass” نامیده میشود. کلاسی که از کلاس والد به ارث میبرد، “کلاس فرزند”، “کلاس مشتق شده” یا “Subclass” نام دارد. کلاس فرزند تمام اعضای عمومی و محافظتشده (public و protected) کلاس والد را به ارث میبرد و میتواند ویژگیها و متدهای جدیدی را اضافه یا متدهای والد را بازنویسی (Override) کند.
کلیدواژههای extends و super()
در TypeScript، برای پیادهسازی ارثبری از کلیدواژه extends استفاده میشود. هنگامی که یک کلاس فرزند از کلاس والد ارث میبرد، Constructor کلاس فرزند باید حتماً Constructor کلاس والد را با استفاده از super() فراخوانی کند. این کار اطمینان میدهد که قسمت والد شیء به درستی مقداردهی اولیه شده است.
class Vehicle { manufacturer: string; model: string; constructor(manufacturer: string, model: string) { this.manufacturer = manufacturer; this.model = model; } start(): void { console.log(`${this.manufacturer} ${this.model} is starting.`); } } class Car extends Vehicle { // کلاس Car از Vehicle ارث میبرد numberOfWheels: number; constructor(manufacturer: string, model: string, numberOfWheels: number) { super(manufacturer, model); // فراخوانی Constructor کلاس والد this.numberOfWheels = numberOfWheels; } drive(): void { console.log(`${this.manufacturer} ${this.model} is driving with ${this.numberOfWheels} wheels.`); } } const myCar = new Car(“Toyota”, “Camry”, 4); myCar.start(); // متد start از Vehicle به ارث رسیده است myCar.drive();
مثال عملی: کلاس ElectricCar
فرض کنید میخواهیم یک کلاس ElectricCar ایجاد کنیم که علاوه بر ویژگیهای یک Car عادی، ویژگیهای خاص خود مانند ظرفیت باتری را نیز داشته باشد.
class ElectricCar extends Car { batteryCapacityKwh: number; constructor(manufacturer: string, model: string, batteryCapacityKwh: number) { super(manufacturer, model, 4); // Electric cars always have 4 wheels this.batteryCapacityKwh = batteryCapacityKwh; } charge(): void { console.log(`${this.manufacturer} ${this.model} is charging. Battery capacity: ${this.batteryCapacityKwh} kWh.`); } // متد start را بازنویسی میکنیم start(): void { console.log(`Silent start: ${this.manufacturer} ${this.model} electric engine started.`); } } const tesla = new ElectricCar(“Tesla”, “Model S”, 100); tesla.start(); // خروجی: Silent start: Tesla Model S electric engine started. (متد بازنویسی شده) tesla.drive(); // متد drive از Car به ارث رسیده است tesla.charge();
بازنویسی متدها (Method Overriding)
بازنویسی متد به این معنی است که یک کلاس فرزند میتواند یک متد را که در کلاس والد تعریف شده است، با پیادهسازی جدیدی ارائه دهد. برای فراخوانی متد والد در داخل متد بازنویسی شده در کلاس فرزند، از super.methodName() استفاده میشود. این کار به ما امکان میدهد تا رفتار پیشفرض والد را حفظ کرده و در عین حال عملکرد خاص خود را به آن اضافه کنیم.
class Animal { makeSound(): void { console.log(“Generic animal sound.”); } } class Dog extends Animal { makeSound(): void { // بازنویسی متد makeSound super.makeSound(); // فراخوانی متد والد console.log(“Woof woof!”); } } const myDog = new Dog(); myDog.makeSound(); // خروجی: Generic animal sound. n Woof woof!
محدودیتها و بهترین شیوهها
در حالی که ارثبری ابزاری قدرتمند است، استفاده بیش از حد یا نادرست از آن میتواند منجر به سلسله مراتب کلاسهای پیچیده و دشوار برای نگهداری شود. اصل “Composition over Inheritance” (ترکیب بر ارثبری) اغلب توصیه میشود، به این معنی که بهتر است به جای ایجاد سلسله مراتب عمیق ارثبری، از ترکیب اشیاء سادهتر برای ساخت اشیاء پیچیدهتر استفاده شود. در آموزش TypeScript پیشرفته، این اصول به دقت مورد بررسی قرار میگیرند.
چندریختی (Polymorphism): انعطافپذیری و توسعهپذیری
چندریختی یکی از مفاهیم کلیدی OOP است که به اشیاء اجازه میدهد تا در زمینههای مختلف، اشکال متفاوت به خود بگیرند یا به شیوههای مختلفی رفتار کنند. این اصل باعث افزایش انعطافپذیری و توسعهپذیری کد میشود.
تعریف چندریختی
کلمه “Polymorphism” از دو بخش یونانی “Poly” (به معنای زیاد یا چند) و “Morph” (به معنای شکل) تشکیل شده است. در برنامهنویسی، چندریختی به این معنی است که یک متد یا ویژگی میتواند در کلاسهای مختلف (معمولاً در یک سلسله مراتب ارثبری یا با اینترفیسهای مشترک) رفتارهای متفاوتی داشته باشد و ما بتوانیم با این اشیاء به صورت عمومی (از طریق نوع پایه یا اینترفیس) تعامل کنیم.
چندریختی با ارثبری
رایجترین شکل چندریختی، زمانی رخ میدهد که کلاسهای فرزند، متدهای کلاس والد خود را بازنویسی میکنند. در این حالت، میتوانیم یک متغیر از نوع کلاس والد را اعلام کنیم و یک شیء از کلاس فرزند را به آن اختصاص دهیم. سپس، هنگام فراخوانی متدی که بازنویسی شده است، متد مربوط به کلاس فرزند اجرا میشود.
class Animal { name: string; constructor(name: string) { this.name = name; } makeSound(): void { console.log(`${this.name} makes a sound.`); } } class Cat extends Animal { constructor(name: string) { super(name); } makeSound(): void { console.log(`${this.name} says Meow!`); } } class Dog extends Animal { constructor(name: string) { super(name); } makeSound(): void { console.log(`${this.name} says Woof!`); } } const animals: Animal[] = [new Cat(“Whiskers”), new Dog(“Buddy”), new Animal(“Generic Beast”)]; animals.forEach(animal => { animal.makeSound(); // هر شیء متد makeSound خاص خود را فراخوانی میکند }); // خروجی: // Whiskers says Meow! // Buddy says Woof! // Generic Beast makes a sound.
در این مثال، آرایهای از نوع Animal داریم که شامل اشیاء Cat و Dog (که از Animal ارث میبرند) است. وقتی متد makeSound() را روی هر عنصر آرایه فراخوانی میکنیم، TypeScript (در زمان اجرا) تشخیص میدهد که نوع واقعی شیء چیست و متد makeSound() مناسب را از کلاس فرزند مربوطه فراخوانی میکند. این ویژگی قدرتمند، امکان نوشتن کدهای عمومیتری را فراهم میکند.
چندریختی با اینترفیسها
اینترفیسها (Interfaces) راه دیگری برای دستیابی به چندریختی هستند. یک اینترفیس قراردادی از متدها و ویژگیها را تعریف میکند که کلاسهای مختلف میتوانند آن را پیادهسازی کنند. هر کلاسی که یک اینترفیس را پیادهسازی کند، متعهد میشود که تمام اعضای تعریف شده در آن اینترفیس را داشته باشد. این امر به ما اجازه میدهد تا با اشیاء از طریق اینترفیس مشترک تعامل کنیم، بدون اینکه به نوع واقعی آنها اهمیت دهیم.
interface Drivable { drive(): void; } class Car implements Drivable { drive(): void { console.log(“Car is driving.”); } } class Truck implements Drivable { drive(): void { console.log(“Truck is driving heavy loads.”); } } class Bicycle implements Drivable { drive(): void { console.log(“Bicycle is pedaling.”); } } function operateVehicle(vehicle: Drivable) { vehicle.drive(); } const myCar = new Car(); const myTruck = new Truck(); const myBicycle = new Bicycle(); operateVehicle(myCar); // خروجی: Car is driving. operateVehicle(myTruck); // خروجی: Truck is driving heavy loads. operateVehicle(myBicycle); // خروجی: Bicycle is pedaling.
در این حالت، تابع operateVehicle نیازی به دانستن نوع دقیق شیء ندارد؛ فقط میداند که شیء ارسالی اینترفیس Drivable را پیادهسازی میکند و دارای متد drive() است. این باعث میشود کد ما انعطافپذیر و توسعهپذیر باشد.
مزایای چندریختی
چندریختی مزایای متعددی دارد:
- افزایش انعطافپذیری: به شما امکان میدهد با انواع مختلف اشیاء به روشی یکسان تعامل کنید.
- کاهش جفتشدگی (Coupling): وابستگی بین اجزای سیستم را کاهش میدهد.
- سهولت توسعه: اضافه کردن انواع جدید به سیستم بدون نیاز به تغییر کدهای موجود را آسان میکند.
- کدهای خواناتر و ماژولارتر: با ایجاد رابطهای عمومی، کدها سازمانیافتهتر میشوند.
این سطح از انعطافپذیری و قابلیت توسعهپذیری در پروژههای بزرگ بسیار حیاتی است. دوره آموزش TypeScript مجتمع فنی تهران با مثالهای کاربردی به این مفاهیم عمیق میپردازد.
انتزاع (Abstraction): پنهانسازی پیچیدگیها
انتزاع یکی دیگر از ارکان برنامهنویسی شیگرا است که به تمرکز بر روی اطلاعات ضروری و پنهانسازی جزئیات پیادهسازی غیرضروری میپردازد. هدف آن، ارائه یک دیدگاه سادهسازی شده از یک سیستم پیچیده است.
تعریف انتزاع
انتزاع به معنای نشان دادن “آنچه” یک شیء انجام میدهد، به جای “چگونه” آن را انجام میدهد، است. این اصل به ما اجازه میدهد تا یک مدل سادهسازی شده از یک موجودیت پیچیده ارائه دهیم و جزئیات داخلی و پیچیدگیهای پیادهسازی را پنهان کنیم. انتزاع باعث میشود سیستمها آسانتر قابل درک، استفاده و نگهداری باشند.
Abstract Classes (کلاسهای انتزاعی)
کلاسهای انتزاعی، کلاسهایی هستند که نمیتوان به طور مستقیم از آنها شیء (instance) ایجاد کرد. این کلاسها با کلیدواژه abstract تعریف میشوند و میتوانند هم شامل متدهای عادی (با پیادهسازی) و هم متدهای انتزاعی (بدون پیادهسازی) باشند. متدهای انتزاعی نیز با کلیدواژه abstract مشخص میشوند و باید در تمام کلاسهای فرزندی که از کلاس انتزاعی ارث میبرند، پیادهسازی شوند.
abstract class Shape { name: string; constructor(name: string) { this.name = name; } abstract calculateArea(): number; // متد انتزاعی abstract calculatePerimeter(): number; // متد انتزاعی displayInfo(): void { // متد غیرانتزاعی console.log(`Shape: ${this.name}`); } } class Circle extends Shape { radius: number; constructor(radius: number) { super(“Circle”); this.radius = radius; } calculateArea(): number { return Math.PI this.radius this.radius; } calculatePerimeter(): number { return 2 Math.PI this.radius; } } class Rectangle extends Shape { width: number; height: number; constructor(width: number, height: number) { super(“Rectangle”); this.width = width; this.height = height; } calculateArea(): number { return this.width this.height; } calculatePerimeter(): number { return 2 (this.width + this.height); } } // const someShape = new Shape(“Generic”); // خطا: نمیتوان از یک کلاس انتزاعی شیء ساخت const myCircle = new Circle(5); myCircle.displayInfo(); // خروجی: Shape: Circle console.log(`Circle Area: ${myCircle.calculateArea()}`); // خروجی: 78.53… const myRectangle = new Rectangle(4, 6); console.log(`Rectangle Perimeter: ${myRectangle.calculatePerimeter()}`); // خروجی: 20
کلاس Shape یک قرارداد کلی برای اشکال هندسی تعریف میکند، اما پیادهسازی جزئیات محاسبه مساحت و محیط را به کلاسهای فرزند میسپارد.
Interfaces (اینترفیسها)
اینترفیسها در TypeScript قراردادی از اعضا (ویژگیها و متدها) را تعریف میکنند که یک کلاس یا شیء باید آنها را پیادهسازی کند. آنها فقط “چه چیزی باید باشد” را مشخص میکنند و “چگونه باید پیادهسازی شود” را تعریف نمیکنند. اینترفیسها راهی عالی برای تعریف رفتارهای مشترک بین کلاسهایی هستند که ممکن است در سلسله مراتب ارثبری مشترکی نباشند.
interface Loggable { log(message: string): void; } class ConsoleLogger implements Loggable { log(message: string): void { console.log(`[Console]: ${message}`); } } class FileLogger implements Loggable { filePath: string; constructor(path: string) { this.filePath = path; } log(message: string): void { // در دنیای واقعی، اینجا به فایل مینویسد console.log(`[File: ${this.filePath}]: ${message}`); } } function processData(logger: Loggable, data: string): void { logger.log(`Processing data: ${data}`); } const consoleLogger = new ConsoleLogger(); const fileLogger = new FileLogger(“app.log”); processData(consoleLogger, “User login event”); processData(fileLogger, “Database backup complete”);
تفاوتهای کلیدی و شباهتهای بین Abstract Classes و Interfaces
در نگاه اول، کلاسهای انتزاعی و اینترفیسها مشابه به نظر میرسند، اما تفاوتهای مهمی دارند که در جدول زیر آمده است:
| ویژگی | کلاس انتزاعی (Abstract Class) | اینترفیس (Interface) |
|---|---|---|
| امکان ساخت شیء | هرگز نمیتوان شیء مستقیم ساخت. | فقط یک قرارداد است و نمیتوان از آن شیء ساخت. |
| متدها | میتواند متدهای انتزاعی (بدون پیادهسازی) و متدهای عادی (با پیادهسازی) داشته باشد. | فقط امضای متدها (بدون پیادهسازی) را تعریف میکند (در TypeScript 3.7+ میتوان پیادهسازی پیشفرض داشت، اما هدف اصلی متفاوت است). |
| ویژگیها | میتواند ویژگیها را با مقدار اولیه تعریف کند. | فقط امضای ویژگیها را تعریف میکند. |
| ارثبری/پیادهسازی | فقط از یک کلاس انتزاعی میتوان ارث برد (extends). | میتواند چندین اینترفیس را پیادهسازی کند (implements). |
| هدف | تعریف یک کلاس پایه با برخی پیادهسازیهای مشترک و برخی متدهای انتزاعی برای تکمیل توسط فرزندان. | تعریف یک قرارداد رفتاری برای کلاسها بدون هیچ پیادهسازی. |
انتخاب بین این دو بستگی به نیاز طراحی دارد. اگر نیاز به اشتراکگذاری کد پیادهسازی و همچنین تعریف متدهای انتزاعی دارید، کلاس انتزاعی مناسب است. اگر فقط به تعریف یک قرارداد رفتاری نیاز دارید، اینترفیس گزینه بهتری است.
مباحث تکمیلی و بهترین شیوهها در آموزش TypeScript
پس از تسلط بر اصول بنیادین شیگرایی، ورود به مباحث پیشرفتهتر و درک بهترین شیوهها، به شما کمک میکند تا کدهای TypeScript خود را به سطح بالاتری ارتقا دهید. مجتمع فنی تهران با دورههای پیشرفته خود، شما را در این مسیر همراهی میکند.
Generic Classes (کلاسهای جنریک)
Generic ها (یا انواع عمومی) ابزاری قدرتمند در TypeScript هستند که به شما امکان میدهند تا اجزایی بسازید که میتوانند با انواع دادهای مختلفی کار کنند، بدون اینکه انعطافپذیری یا امنیت نوع خود را از دست بدهند. کلاسهای جنریک به شما اجازه میدهند تا کلاسهایی تعریف کنید که در زمان تعریف نوع دادههای آنها مشخص نیست، بلکه در زمان استفاده از کلاس تعیین میشود.
class Box { private _value: T; constructor(value: T) { this._value = value; } getValue(): T { return this._value; } setValue(newValue: T): void { this._value = newValue; } } const numberBox = new Box(123); console.log(numberBox.getValue()); // خروجی: 123 numberBox.setValue(456); // numberBox.setValue(“text”); // خطا: Type ‘string’ is not assignable to type ‘number’. const stringBox = new Box(“Hello”); console.log(stringBox.getValue()); // خروجی: Hello
در این مثال، کلاس Box میتواند مقادیری از هر نوعی (که با T مشخص میشود) را نگهداری کند و TypeScript اطمینان حاصل میکند که نوع داده در سراسر کلاس حفظ میشود.
Composition over Inheritance (ترکیب بر ارثبری)
این یک اصل طراحی است که به جای استفاده از ارثبری برای به اشتراکگذاری رفتار، استفاده از ترکیب (Composition) را تشویق میکند. به عبارت دیگر، به جای اینکه یک کلاس ویژگیهای یک کلاس دیگر را به ارث ببرد، بهتر است یک کلاس شامل نمونهای از کلاسهای دیگر باشد و از قابلیتهای آنها استفاده کند. این رویکرد به طراحی منعطفتر، کاهش وابستگیها و ایجاد کدهای قابل نگهداریتر منجر میشود، زیرا از مشکلات سلسله مراتب عمیق ارثبری جلوگیری میکند.
// به جای ارثبری، از ترکیب استفاده میکنیم class Engine { start(): void { console.log(“Engine started.”); } } class Wheels { rotate(): void { console.log(“Wheels rotating.”); } } class ModernCar { private engine: Engine; private wheels: Wheels; constructor() { this.engine = new Engine(); this.wheels = new Wheels(); } drive(): void { this.engine.start(); this.wheels.rotate(); console.log(“Car is driving.”); } } const modernCar = new ModernCar(); modernCar.drive();
SOLID Principles (اصول SOLID)
اصول SOLID مجموعهای از پنج اصل طراحی شیگرا هستند که هدف آنها ایجاد کدهای نرمافزاری قابل فهم، انعطافپذیر و قابل نگهداری است. این اصول به توسعهدهندگان کمک میکنند تا از مشکلات رایج در طراحی نرمافزار جلوگیری کنند و کدهایی بنویسند که به راحتی قابل توسعه و اصلاح باشند. مفاهیم OOP که در این مقاله پوشش داده شد، مستقیماً به رعایت این اصول کمک میکنند:
- Single Responsibility Principle (SRP): هر کلاس باید تنها یک دلیل برای تغییر داشته باشد (یک وظیفه).
- Open/Closed Principle (OCP): موجودیتهای نرمافزاری (کلاسها، ماژولها، توابع) باید برای توسعه باز و برای تغییر بسته باشند.
- Liskov Substitution Principle (LSP): اشیاء از یک کلاس پایه باید بتوانند با اشیاء از کلاسهای مشتق شده جایگزین شوند بدون اینکه عملکرد برنامه را تغییر دهند. (ارتباط نزدیک با چندریختی)
- Interface Segregation Principle (ISP): مشتریان نباید مجبور به پیادهسازی اینترفیسهایی باشند که از آنها استفاده نمیکنند. (ارتباط نزدیک با اینترفیسها و انتزاع)
- Dependency Inversion Principle (DIP): ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. هر دو باید به انتزاعات وابسته باشند. (ارتباط نزدیک با انتزاع)
درک عمیق این اصول و نحوه پیادهسازی آنها در TypeScript، شما را به یک توسعهدهنده نرمافزار حرفهای تبدیل میکند. مجتمع فنی تهران با ارائه دوره آموزش TypeScript جامع، به شما کمک میکند تا تمامی این مفاهیم را از پایه تا پیشرفته بیاموزید و در پروژههای واقعی خود به کار ببرید. با شرکت در کلاس آموزش TypeScript این مجتمع، گامی بزرگ در جهت تسلط بر برنامهنویسی شیگرا و توسعه کدهای با کیفیت بردارید. آموزش TypeScript مجتمع فنی تهران فرصتی بینظیر برای ارتقای مهارتهای برنامهنویسی شماست.
سوالات متداول
آیا TypeScript از مفهوم Mixins برای ترکیب رفتارها بین کلاسها پشتیبانی میکند و نحوه پیادهسازی آن چگونه است؟
بله، TypeScript از Mixins پشتیبانی میکند که به کلاسها اجازه میدهد رفتارها را از چندین منبع ترکیب کنند، معمولاً با استفاده از توابع کمکی که ویژگیها و متدهای یک کلاس را در کلاس دیگر کپی میکنند.
تفاوت اصلی بین کلاسهای انتزاعی و اینترفیسها در TypeScript چیست و چه زمانی باید از هر کدام استفاده کنیم؟
کلاس انتزاعی میتواند هم شامل پیادهسازی و هم متدهای انتزاعی باشد و فقط یک بار قابل ارثبری است، در حالی که اینترفیس فقط قراردادها را تعریف میکند و میتوان چندین اینترفیس را پیادهسازی کرد؛ کلاسهای انتزاعی برای اشتراکگذاری کد و تعریف اسکلت مشترک، و اینترفیسها برای تعریف قراردادهای رفتاری مستقل از سلسلهمراتب ارثبری کاربرد دارند.
چه زمانی باید از اعضای استاتیک (Static Members) در یک کلاس TypeScript استفاده کنیم و چه محدودیتهایی دارند؟
اعضای استاتیک برای ویژگیها و متدهایی استفاده میشوند که به خود کلاس تعلق دارند، نه به نمونههای آن، مانند ثابتها، توابع کمکی، یا شمارندههای مشترک؛ محدودیت اصلی آنها این است که نمیتوانند به اعضای نمونه (غیر استاتیک) دسترسی داشته باشند.
بهترین راه برای مدیریت وابستگیها و کاهش جفتشدگی (Coupling) در یک سیستم شیگرا با TypeScript چیست؟
بهترین راه برای مدیریت وابستگیها و کاهش جفتشدگی، استفاده از اصول SOLID، به ویژه اصل وارونگی وابستگی (DIP)، الگوهای طراحی مانند تزریق وابستگی (Dependency Injection) و ترجیح ترکیب بر ارثبری است.
چگونه میتوانیم از Constructor Overloading (سازندههای چندگانه) در TypeScript به بهترین نحو استفاده کنیم؟
TypeScript به طور مستقیم از Constructor Overloading پشتیبانی نمیکند، اما میتوان با تعریف امضاهای متعدد برای Constructor و یک پیادهسازی واحد که منطق را بر اساس نوع و تعداد آرگومانهای ورودی مدیریت میکند، به این قابلیت دست یافت.

