برنامهنویسی شیگرا (OOP) چیست؟ راهنمای کامل با مثالهای ساده و کاربردی
برنامهنویسی شیگرا چیست، چرا مهندسان نرمافزار از OOP استفاده میکنند و کلاس، شیء، وراثت، پلیمورفیسم و کپسولهسازی چطور کار میکنند؟ راهنمای عملی با
اولین برخورد جدی من با برنامهنویسی شیگرا جایی بود که یک کلاس ۴۰۰ خطی را باز کردم و فهمیدم هیچکدام از متدها بدون بقیه کار نمیکنند؛ همه به هم چسبیده بودند و هر تغییر کوچک، سه باگ جدید میساخت. آن روز فهمیدم OOP (Object-Oriented Programming) فقط یک سبک کدنویسی نیست؛ یک راه فکر کردن به مسئله است. از آن تجربه به بعد، هر بار پروژهای را شروع میکنم، اول به این فکر میکنم که مسئله به چند موجودیت مستقل شکسته میشود، نه به چند تابع.
برنامهنویسی شیگرا چیست؟
برنامهنویسی شیگرا یک پارادایم (Paradigm) برنامهنویسی است که در آن نرمافزار بهجای مجموعهای از توابع و متغیرهای پراکنده، به شکل مجموعهای از موجودیتهای مستقل به نام شیء (Object) سازماندهی میشود. هر شیء دادههای خودش و رفتار خودش را در یک واحد بههمپیوسته نگه میدارد. برای تعریف دقیقتر میتوانید صفحه Object-oriented programming در ویکیپدیا را ببینید.
اگر بخواهم این تعریف را به زبان روزمره بگویم: در برنامهنویسی رویهای (Procedural Programming) به کامپیوتر میگوییم این مراحل را به ترتیب انجام بده، ولی در OOP به کامپیوتر میگوییم این موجودیتها را داری؛ هرکدام را وادار کن کار خودش را انجام بدهد. این تفاوت ظاهراً ساده، پیامدهای عمیقی دارد. وقتی کد را حول محور موجودیتها سازماندهی میکنیم، مرزهای مسئولیت روشن میشوند و تغییر در یک بخش، کمتر به بخشهای دیگر نشت میکند.
در پروژههای بزرگ — بهویژه آنهایی که بیش از یک توسعهدهنده رویشان کار میکند — این جداسازی، تفاوت میان یک کد قابلنگهداری و یک کد پوسیده است. اگر تازه با مفاهیم پایه آشنا هستید، پیشنهاد میکنم قبل از ادامه، آموزش php از صفر برای مبتدیان را مرور کنید تا پیشزمینه ذهنی روشنی از توابع و متغیرها داشته باشید.
OOP یک سبک نوشتن نیست؛ یک شیوه دیدن مسئله است. تا وقتی به مسئله مثل لیستی از کارها نگاه میکنید، هر زبانی که استفاده کنید کد رویهای مینویسید.
چرا OOP در دهههای اخیر غالب شده است؟
سه دلیل اصلی برای محبوبیت OOP وجود دارد. اول، رشد اندازه پروژهها: زمانی که یک برنامه به دهها هزار خط کد میرسد، سازماندهی وظیفهمحور از کنترل خارج میشود. دوم، رشد تیمهای توسعه: وقتی پنج نفر روی یک پایگاه کد کار میکنند، مرزهای روشن میان ماژولها حیاتی میشود. سوم، شباهت ذهنی OOP به جهان واقعی: انسان بهطور طبیعی با اشیاء و موجودیتها فکر میکند، نه با لیست دستورات.
کلاس و شیء: اولین قدم عملی
پیش از هر چیز باید تفاوت کلاس و شیء را روشن کنیم، چون این دو در گفتگوهای روزمره اشتباه گرفته میشوند. کلاس (Class) یک نقشه یا قالب است که تعیین میکند اشیاء چه ویژگیها و چه رفتارهایی خواهند داشت. شیء (Object) یک نمونه واقعی و ساختهشده از آن کلاس است. کلاس مثل نقشه معماری یک ساختمان است و شیء مثل ساختمانی که با آن نقشه ساخته شده.
به مثال زیر در PHP دقت کنید:
class User {
public string $name;
public string $email;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
public function greet(): string {
return "سلام، " . $this->name;
}
}
$user = new User("علی", "ali@example.com");
echo $user->greet();
در این مثال، User یک کلاس است و $user یک شیء از آن. متد __construct سازنده کلاس (Constructor) است و هنگام ساخت شیء بهطور خودکار اجرا میشود. کلمه کلیدی $this در داخل متد به شیء جاری اشاره میکند. اگر میخواهید شیءگرایی را در PHP عمیقتر یاد بگیرید، آموزش شی گرایی در php مسیر کاملتری را پیش میگذارد.
همین مفهوم در Python به این شکل نوشته میشود:
class User:
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
def greet(self) -> str:
return f"سلام، {self.name}"
user = User("علی", "ali@example.com")
print(user.greet())
تفاوت ظاهری مهم است: در Python از self استفاده میشود که صریحاً به عنوان پارامتر اول متد دریافت میشود، در حالی که در PHP $this بهطور خودکار در دسترس است. یادگیری این تفاوتها در شی گرایی در پایتون با جزئیات بیشتری پوشش داده شده است.
چهار ستون اصلی OOP
OOP بر چهار مفهوم بنیادین استوار است که معمولاً به آنها چهار ستون یا چهار پایه میگویند. این چهار پایه عبارتند از کپسولهسازی، وراثت، پلیمورفیسم و انتزاع. درک هر یک از این مفاهیم، پیشنیاز نوشتن کد شیگرای واقعی است، نه فقط استفاده از کلمه کلیدی class. اگر میخواهید هر چهار اصل را در یک منبع متمرکز مرور کنید، اصول چهارگانه OOP و کاربرد آنها را جداگانه بررسی کردهام.
۱. کپسولهسازی (Encapsulation)
کپسولهسازی یعنی پنهانکردن جزئیات درونی یک شیء از دنیای بیرون. بیرون فقط باید از طریق رابط عمومی (Public Interface) با شیء تعامل کند. در عمل این یعنی فیلدهای داخلی را private نگه داریم و برای دسترسی به آنها، متدهای مشخص تعریف کنیم. این کار مزیتهای مهمی دارد: میتوانیم بدون شکستن کد بیرونی، پیادهسازی داخلی را تغییر دهیم؛ میتوانیم هنگام مقداردهی اعتبارسنجی انجام دهیم؛ و میتوانیم از دسترسی غیرمجاز جلوگیری کنیم.
class BankAccount {
private float $balance = 0.0;
public function deposit(float $amount): void {
if ($amount <= 0) {
throw new InvalidArgumentException("مبلغ باید مثبت باشد");
}
$this->balance += $amount;
}
public function getBalance(): float {
return $this->balance;
}
}
توجه کنید که فیلد balance از بیرون قابل تغییر مستقیم نیست؛ هر تغییر باید از مسیر deposit عبور کند و در آن مسیر، اعتبارسنجی صورت میگیرد. همین الگو در زبانهای دیگر هم وجود دارد. در TypeScript، کلمات private، protected و public کنترل دقیقتری روی سطح دسترسی میدهند که در کلاس در تایپ اسکریپت توضیح دادهام.
۲. وراثت (Inheritance)
وراثت مکانیزمی است که به یک کلاس اجازه میدهد ویژگیها و رفتارهای کلاس دیگری را به ارث ببرد. کلاس فرزند (Child Class) میتواند هم از رفتارهای والد استفاده کند و هم رفتارهای جدید اضافه کند یا رفتارهای قبلی را بازنویسی (Override) کند. وراثت، ابزار اصلی برای حذف تکرار کد است — اما باید با احتیاط استفاده شود، چون در بسیاری از پروژهها وراثتهای چندلایه به بنبست نگهداری میرسند.
class Animal {
public function __construct(protected string $name) {}
public function speak(): string {
return "...";
}
}
class Dog extends Animal {
public function speak(): string {
return "واق واق";
}
}
class Cat extends Animal {
public function speak(): string {
return "میو";
}
}
در این مثال، Dog و Cat هر دو از Animal ارث میبرند و رفتار speak را به شکل مخصوص خودشان پیادهسازی میکنند. این بازنویسی، پایه پلیمورفیسم است که در بخش بعدی توضیح میدهم. توجه کنید که در PHP و TypeScript از extends استفاده میشود، در Python از پرانتز بعد از نام کلاس و در Java از همان extends.
۳. پلیمورفیسم (Polymorphism)
پلیمورفیسم به معنی چندریختی است: یک رابط واحد، چند پیادهسازی متفاوت. وقتی یک متغیر از نوع کلاس والد داریم، میتوانیم شیء کلاس فرزند به آن بدهیم و رفتار مخصوص آن فرزند اجرا شود. این امکان، انعطافپذیری فوقالعادهای در طراحی میسازد: کد پایهای که با Animal کار میکند، بدون تغییر با Dog و Cat هم کار میکند.
function letAnimalSpeak(Animal $animal): void {
echo $animal->speak() . PHP_EOL;
}
letAnimalSpeak(new Dog("رکس"));
letAnimalSpeak(new Cat("پیشی"));
تابع letAnimalSpeak هیچوقت نمیداند چه نوع حیوانی به آن میرسد؛ فقط از رابط عمومی speak استفاده میکند. اگر فردا کلاس Bird هم اضافه شود، این تابع بدون تغییر کار میکند. این همان چیزی است که در طراحی نرمافزار به آن Open/Closed Principle میگویند: باز برای گسترش، بسته برای تغییر.
۴. انتزاع (Abstraction)
انتزاع یعنی تمرکز بر چیستی یک رفتار، نه چگونگی آن. وقتی یک کلاس انتزاعی یا یک اینترفیس تعریف میکنیم، فقط امضا (Signature) متدها را مشخص میکنیم و پیادهسازی واقعی را به کلاسهای فرزند میسپاریم. کاربر این کلاسها فقط میداند چه متدهایی وجود دارد؛ اما نمیداند در پشت صحنه چه اتفاقی میافتد. این جداسازی، کد را از جزئیات غیرضروری آزاد میکند و وابستگی میان لایهها را کم میکند.
انتزاع یعنی بتوانید بدون دانستن چگونگی، از یک قابلیت استفاده کنید. اگر برای استفاده از یک کلاس مجبورید بدانید درونش چه خبر است، آن کلاس انتزاع موفقی نساخته است.
اینترفیس و کلاس انتزاعی: تفاوتها و کاربردها
دو ابزار اصلی برای پیادهسازی انتزاع در زبانهای شیگرا وجود دارد: اینترفیس (Interface) و کلاس انتزاعی (Abstract Class). هر دو اجازه میدهند بدون پیادهسازی کامل، یک قرارداد تعریف کنیم، اما تفاوتهای مهمی دارند.
| ویژگی | اینترفیس | کلاس انتزاعی |
|---|---|---|
| پیادهسازی متد | معمولاً ندارد | میتواند داشته باشد |
| وراثت چندگانه | پشتیبانی میشود | معمولاً نه |
| فیلد و حالت | ندارد | دارد |
| کاربرد معمول | قرارداد بین لایهها | پایه مشترک چند کلاس |
قاعده عملی من این است: اگر نیاز به یک قرارداد دارم که چند کلاس نامرتبط باید پیاده کنند، اینترفیس را انتخاب میکنم. اگر چند کلاس رابطه is-a دارند و منطق مشترکی بینشان وجود دارد، کلاس انتزاعی مناسبتر است. در TypeScript این تفاوتها با مفاهیم type و interface گره خورده است که در اینترفیس در تایپ اسکریپت بهطور دقیق توضیح دادهام.
ترِیت و ترکیب: جایگزین وراثت در دنیای مدرن
یکی از بزرگترین درسهایی که در طول سالها پروژه گرفتهام این است که وراثت، ابزار اصلی نیست؛ ابزار خاص است. در بسیاری از موارد، ترکیب (Composition) راهحل بهتری است. ترکیب یعنی یک کلاس بهجای ارثبری از کلاس دیگر، شیئی از آن کلاس را بهعنوان فیلد داخلی نگه میدارد. شعار معروفی که در این زمینه هست میگوید Composition را به Inheritance ترجیح بده.
در PHP، ویژگیهایی به نام ترِیت (Trait) وجود دارد که راه سومی ارائه میدهد. ترِیت مانند یک بسته کد است که میتوان آن را در چند کلاس مختلف inject کرد، بدون آنکه آنها در یک سلسلهمراتب وراثتی مشترک باشند. این مکانیزم، برای اشتراک کدی که بهطور منطقی از یک والد مشترک نمیآید، بسیار مفید است.
trait Logger {
public function log(string $message): void {
echo "[" . date("Y-m-d H:i") . "] " . $message;
}
}
class OrderService {
use Logger;
}
$service = new OrderService();
$service->log("سفارش جدید ثبت شد");
این الگو در پروژههای واقعی بسیار رایج است و بخش مهمی از بهترین روشهای PHP مدرن را میسازد. استفاده بیجا از وراثت عمیق، شکنندگی و پیچیدگی میآورد؛ ترکیب و ترِیت، جایگزینهای سبکتری هستند.
مثالهای عملی در زبانهای مختلف
حالا بیایید یک مثال واقعیتر را در سه زبان مختلف بررسی کنیم. فرض کنید میخواهیم یک سیستم پرداخت طراحی کنیم که چند درگاه پرداخت مختلف را پشتیبانی میکند.
نسخه PHP: اینترفیس بهعنوان قرارداد
interface PaymentGateway {
public function charge(float $amount): bool;
public function refund(string $transactionId): bool;
}
class ZarinPalGateway implements PaymentGateway {
public function charge(float $amount): bool {
// ارتباط با API زرینپال
return true;
}
public function refund(string $transactionId): bool {
// استرداد وجه
return true;
}
}
حالا کد مصرفکننده فقط به اینترفیس وابسته است، نه به پیادهسازی خاص. این الگو در وردپرس هم جایگاه مهمی دارد؛ مخصوصاً وقتی با سیستمهای افزونهای و درگاههای پرداخت سر و کار داریم. اگر روی پروژهای کار میکنید که با این نوع طراحی آمیخته است، PHP در وردپرس: از مبتدی تا حرفهای را مطالعه کنید تا ببینید این مفاهیم در اکوسیستم وردپرس چگونه بهکار میروند.
نسخه JavaScript: کلاسهای مدرن ES6
class Cart {
#items = [];
add(product, quantity = 1) {
this.#items.push({ product, quantity });
}
total() {
return this.#items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
);
}
}
const cart = new Cart();
cart.add({ name: "کتاب", price: 150000 }, 2);
console.log(cart.total());
در این مثال، #items یک فیلد خصوصی است که در JavaScript مدرن با علامت # مشخص میشود. هر شیء Cart، سبد خرید مستقل خودش را دارد. اگر با شیگرایی در JavaScript آشنا نیستید، شی گرایی در جاوااسکریپت مسیر یادگیری کاملی را پیشنهاد میدهد.
نسخه TypeScript: ترکیب Generics و Interface
interface Repository<T> {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<void>;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
// جستجو در دیتابیس
return null;
}
async save(user: User): Promise<void> {
// ذخیره
}
}
این الگو به نام Repository Pattern شناخته میشود و در پروژههای بزرگ بسیار پرکاربرد است. اگر Generics در TypeScript برایتان تازه است، جنریک در تایپ اسکریپت را ببینید. همچنین همین الگو پایهای است که در ORMها (Object-Relational Mapping) هم استفاده میشود؛ توضیح کامل ORM را در ORM چیست و چگونه کار با دیتابیس را ساده میکند آوردهام.
اشتباهات رایج در OOP
در بازبینی کدهای تیمهای مختلف، الگوهای تکراری از خطا را دیدهام که تقریباً همیشه به یک چیز برمیگردند: سوءاستفاده از یک ابزار بهجای ابزار مناسب. فهرست زیر خلاصه این خطاها است.
- وراثت بهجای ترکیب: زمانی که یک کلاس برای استفاده از یک قابلیت، از کلاس دیگری ارث میبرد فقط به این دلیل که راحتتر است، در حقیقت یک گره اضافی به سلسلهمراتب اضافه کرده است.
- کلاسهای همهکاره: کلاسی که هم لایه دیتابیس را مدیریت میکند، هم منطق کسبوکار و هم رندر HTML را، در واقع هیچ مسئولیتی را بهطور کامل انجام نمیدهد.
- نقض قانون دیمیتر: وقتی یک شیء با جزئیات داخلی شیء دیگر حرف میزند، کوپلینگ ناخواسته ساخته میشود.
- استفاده بیمورد از Setter: اگر همه فیلدها Setter عمومی دارند، کپسولهسازی فقط ظاهری است.
- سلسلهمراتب عمیق: وراثت بیش از دو یا سه لایه، تقریباً همیشه به یک پروژه غیرقابل نگهداری میرسد.
هر بار که مجبور میشوید یک متد را در والد تغییر دهید و بعد در چند فرزند دیگر آن را تصحیح کنید، یعنی وراثت شما از ابزار به بدهی تبدیل شده است.
OOP در برابر برنامهنویسی تابعی
یکی از سوالاتی که در جلسات فنی زیاد مطرح میشود این است که آیا OOP با برنامهنویسی تابعی (Functional Programming) در تضاد است یا میتوان آنها را ترکیب کرد. پاسخ کوتاه: در تضاد نیستند، بلکه دو ابزار متفاوت برای مسائل متفاوتاند. OOP برای مدلسازی موجودیتهای دارای حالت (State) و رفتارهای مرتبط عالی است؛ FP برای تبدیل دادهها، پردازش موازی و توابع بدون عوارض جانبی (Side Effects) بینظیر است.
در دنیای واقعی، پروژههای بزرگ تقریباً همیشه ترکیبی از هر دو هستند. مثلاً یک لایه سرویس میتواند شیگرا طراحی شود، در حالی که پردازش دادههای درون آن با توابع خالص (Pure Functions) انجام میگیرد. اگر میخواهید در این زمینه دید عمیقتری پیدا کنید، برنامهنویسی تابعی چیست و چه مزایایی دارد را ببینید.
چگونه OOP در معماریهای بزرگ ظاهر میشود؟
در معماریهای نرمافزاری مدرن مثل MVC (Model-View-Controller)، Clean Architecture و Hexagonal Architecture، مفاهیم شیگرا سنگبنای طراحی هستند. هر لایه یک یا چند کلاس دارد که مسئولیت مشخصی بر عهده دارند و از طریق اینترفیس با لایههای دیگر حرف میزنند. برای نمونه، اگر با الگوی MVC آشنا نیستید، الگوی MVC را با مثال واقعی یاد بگیرید تصویر روشنی از این معماری میدهد.
پرسشهای پرتکرار درباره برنامهنویسی شیگرا
آیا برای شروع، یادگیری OOP ضروری است یا میتوان بدون آن هم پروژه ساخت؟
برای پروژههای کوچک و شخصی، میتوانید بدون OOP هم کار کنید. اما به محض اینکه اندازه پروژه از چند فایل بگذرد یا تیم شما به بیش از یک نفر برسد، نداشتن ساختار شیگرا هزینههای سنگینی بههمراه دارد. تجربه من نشان داده که بازنویسی یک پروژه رویهای به شیگرا، معمولاً گرانتر از یادگیری درست از ابتدا است.
چند کلاس در یک پروژه منطقی است؟
عدد جادویی وجود ندارد. اما اگر یک کلاس بیش از ۲۰۰ خط کد دارد، احتمالاً بیش از یک مسئولیت بر دوشش افتاده است. اگر یک پروژه کوچک بیش از ۵۰ کلاس دارد، به احتمال زیاد abstractionهای بیمورد ساختهاید. مرز درست، همان جایی است که هر کلاس بتواند با یک جمله توضیح داده شود.
آیا وراثت چندگانه (Multiple Inheritance) ایده خوبی است؟
خیر. تقریباً همه زبانهای مدرن وراثت چندگانه کلاسها را محدود کردهاند، چون باعث ابهام در زمان حل نامها (Diamond Problem) میشود. راهحلهای جایگزین مانند Interface در Java و C#، و Trait در PHP و Scala دقیقاً برای رفع همین مشکل بهوجود آمدهاند.
کلاس انتزاعی و اینترفیس از نظر عملکرد فرقی دارند؟
در عمل تفاوت عملکردی محسوسی ندارند. انتخاب بین این دو، یک تصمیم طراحی است نه یک تصمیم کارایی. اگر نیاز به فیلد یا پیادهسازی مشترک دارید، کلاس انتزاعی؛ اگر فقط قرارداد میخواهید، اینترفیس را انتخاب کنید.
OOP در زبانهای اسکریپتی مثل JavaScript و PHP چقدر جدی گرفته میشود؟
بسیار جدی. از PHP 5 به بعد و از ES6 در JavaScript، پشتیبانی از کلاسها و شیءگرایی به شکل مدرن اضافه شده است. امروز کتابخانههای مهمی مانند Laravel در PHP و React در JavaScript کاملاً بر پایه مفاهیم شیگرا ساخته شدهاند.
آیا OOP همیشه بهتر از برنامهنویسی تابعی است؟
نه. اگر مسئله شما پردازش دادههای حجیم، همزمانی و توابع بدون حالت است، برنامهنویسی تابعی معمولاً سادهتر و مطمئنتر عمل میکند. بسیاری از زبانهای مدرن مثل Scala، Kotlin و TypeScript هر دو پارادایم را ترکیب کردهاند و انتخاب را به برنامهنویس واگذار میکنند.
چگونه تشخیص دهم یک کلاس بیش از یک مسئولیت دارد؟
یک روش عملی این است که به توصیف کلاس در یک جمله گوش کنید. اگر برای توضیح آن مجبورید از کلمه و استفاده کنید، احتمالاً بیش از یک مسئولیت دارد. مثلاً کلاسی که هم مدیریت کاربر و هم ارسال ایمیل را انجام میدهد، باید به دو کلاس تجزیه شود.
سخن پایانی
برنامهنویسی شیگرا یک ابزار قدرتمند است که اگر در جای درست بهکار برود، کد شما را خواناتر، آزمونپذیرتر و قابلنگهداریتر میکند. اما همانقدر که میتواند مفید باشد، اگر افراطی یا نادرست بهکار رود، به یک بدهی فنی سنگین تبدیل میشود. چهار ستون اصلی — کپسولهسازی، وراثت، پلیمورفیسم و انتزاع — چارچوب فکریای هستند که به شما کمک میکنند تصمیم بگیرید کدام بخش از کد به کدام کلاس تعلق دارد.
تجربه شخصی من این است که کیفیت یک طراحی شیگرا، بیش از آنکه به تعداد کلاسها یا پیچیدگی سلسلهمراتب وابسته باشد، به دقت در تعریف مسئولیتها بستگی دارد. کلاسی که فقط یک کار را انجام میدهد، اما آن کار را کامل و شفاف انجام میدهد، هزار بار ارزشمندتر از کلاسی است که ده کار نصفهنیمه انجام میدهد.
اگر این مطلب برایتان مفید بود، تجربه خودتان را از پروژههایی که در آنها از OOP استفاده کردهاید بنویسید؛ مخصوصاً اگر با موقعیتهایی روبرو شدهاید که وراثت یا کپسولهسازی بهجای کمک، دردسر ایجاد کرده است. این تجربههای واقعی برای خواننده بعدی که در حال طراحی یک سیستم است، از هر مثال کتابی ارزشمندتر هستند. 🧩