درس ۰۵: مفهوم شی‌گرایی

مفهوم شی گرایی (Object-Oriented Programming)

Photo by Lucas Benjamin

این درس به توضیح مفاهیم پایه «برنامه‌نویسی شی‌گرا» (Object-Oriented Programming) اختصاص یافته است و آخرین درس از سطح «پایه» در این کتاب می‌باشد. هدف از این درس آشنایی خوانندگان با مفاهیم عمومی شی‌گرایی بوده و نه آموزش آن؛ جزییات بیشتر از برنامه‌نویسی شی‌گرا به همراه آموزش پیاده‌سازی مفاهیم آن در زبان پایتون از درس هفدهم تا بیست و دوم به صورت کامل بررسی خواهد شد. البته در این درس کمی به ساختار اشیا و کلاس‌ها در زبان پایتون اشاره‌ شده است که پیش‌نیاز دروس آتی خواهد بود.

سطح: پایه



برنامه‌نویسی شی‌گرا

«برنامه‌نویسی شی‌گرا» (Object-Oriented Programming) یا به اختصار OOP یک الگو یا شیوه تفکر در برنامه‌نویسی است که برگرفته از دنیای واقعی بوده و از دهه ۱۹۶۰ میلادی مطرح گشته است. به زبانی که از این الگو پشتیبانی کند، «زبان شی‌گرا» گفته می‌شود؛ Simula 67 و Smalltalk نخستین زبان‌های برنامه‌نویسی شی‌گرا هستند. ایده شی‌گرایی در پاسخ به برخی از نیازها که الگوهای موجود پاسخ‌گو آن‌ها نبودند به وجود آمد؛ نیازهایی مانند: توانایی پیاده‌سازی مسائل پیچیده (Complex)، «پنهان‌سازی داده» (Data Hiding)، «قابلیت استفاده مجدد» (Reusability) بیشتر، وابستگی کمتر به توابع، انعطاف بالا و...

رویکرد برنامه‌نویسی شی‌گرا «از پایین به بالا» (Bottom-Up) است؛ یعنی ابتدا واحدهایی کوچک از برنامه ایجاد می‌شوند و سپس با پیوند این واحدها، واحدهایی بزرگتر و در نهایت شکلی کامل از برنامه به وجود می‌آید. برنامه‌نویسی شی‌گرا در قالب دو مفهوم «کلاس» (Class) و «شی» (Object) ارایه می‌گردد. هر کلاس واحدی از برنامه است که بخشی از عملیات‌ها و داده‌های یک برنامه را تعریف می‌کند و هر شی نیز یک نمونه پیاده‌سازی شده از یک کلاس است. می‌توان هر تعداد شی از یک کلاس ایجاد کرد. هر شی مستقل از دیگر اشیای ایجاد شده خواهد بود که می‌تواند مقادیر متفاوتی را در خود نگهداری کند (در چهارچوبی که توسط کلاس آن تعریف شده است). در برنامه‌نویسی شی‌گرا، تمامی عملیات‌های قابل انتظار یک برنامه از تعامل اشیای گوناگون با یکدیگر حاصل می‌شود.

برای مثال، کارخانه تولید یک مدل خودرو را می‌توانیم به شکل یک ساختار بزرگ در نظر بگیریم. بدیهی است که این کارخانه شامل بخش‌های تولیدی کوچکتری به مانند: سیستم الکتریکی، سیستم چرخ‌ها، سیستم سوخت، سیستم خنک کننده، موتور و... است که هر کدام نیز در نوع خود می‌تواند به واحدهایی کوچکتر تقسیم گردد. طراحی شی‌گرای این کارخانه به این صورت می‌شود که ابتدا می‌بایست کلاس‌های کوچکترین واحدها را ایجاد کرد که کاملا ایزوله از یکدیگر هستند. پس از آن به ترتیب کلاس‌هایی بزرگتر تعریف می‌گردند که در آنها بخشی از عملیات‌های مرتبط با یکدیگر هماهنگ می‌شوند، این کلاس‌ها در تعریف خود حاوی اشیای کلاس‌های کوچکتر خواهند بود. با گسترش این طرح به تعریف کلاس نهایی می‌رسیم که در اینجا همان کلاس کارخانه است. اکنون این کلاس با هماهنگ کردن اشیای واحدهای کوچکتر از خود و تعریف عملیات‌های مورد نیاز، آماده تولید نمونه‌های کامل و مورد انتظار خواهد بود. به این صورت که با ایجاد یک شی از آن، یک موجودیت اجرایی (خودرو) پدید می‌آید.

مفاهیم شی‌گرایی

گفته شد که هر کلاس ساختاری است که تعدادی داده و عملیات مرتبط را تعریف می‌کند. از دید برنامه‌نویسی، هر کلاس از دو بخش «اعضای داده» (Data Members) و «توابع عضو» (Member Functions) تشکیل شده است. اعضای داده در واقع همان متغیر‌های درون کلاس هستند که خصوصیات یا صفات شی را بیان می‌کنند و در شی‌گرایی با عنوان «فیلد» (Field) یا «صفت» (Attribute) از آن‌ها یاد می‌شود. توابع عضو نیز عملیات یا کارهایی هستند که یک شی از کلاس قادر به انجام آن‌ها می‌باشد؛ در شی‌گرایی به این توابع «متد» (Method) گفته می‌شود.

تا به اینجا با مفاهیم «کلاس»، «صفت»، «متد» و «شی» آشنا شده‌ایم؛ در ادامه به توضیح مفاهیم اصلی از برنامه‌نویسی شی‌گرا خواهیم پرداخت.

نمونه‌سازی

به هر شی از یک کلاس، یک نمونه (Instance) از آن کلاس گفته می‌شود و به عمل ایجاد شی از کلاس در شی‌گرایی «نمونه‌سازی» (Instantiation) گفته می‌شود. در این مفهموم، هدف نهایی از تعریف کلاس‌ها در برنامه، تنها تولید شی نیست. گاهی نیز وجود یک کلاس تنها برای درج تعاریف مشترک بین چند کلاس دیگر کاربرد پیدا می‌کند. بر همین اساس دو نوع کلاس در شی‌گرایی وجود دارد: ۱- کلاس‌های عادی که توانایی نمونه‌سازی دارند و به آن‌ها ”Concrete Class“ گفته می‌شود ۲- کلاس‌هایی که توانایی نمونه‌سازی ندارند و به آن‌ها ”Abstract Class“ گفته می‌شود.

همانطور که در دروس مربوط به پیاده‌سازی کلاس خواهید دید؛ با ایجاد هر نمونه از کلاس یک متد خاص در آن به صورت خودکار اجرا می‌گردد. این متد «سازنده» (Constructor) نام دارد و کار آن «مقداردهی اولیه» (Initialization) شی است. این کار موجب اطمینان از مقداردهی تمامی اعضای داده پیش از استفاده شی در برنامه می‌گردد.

برای مثال یک کلاس خودرو ساده را در نظر بگیرید که در آن صفات: رنگ بدنه، ظرفیت باک، بیشینه سرعت و متدهای: راندن، دریافت میزان سوخت، سوخت گیری، تنظیم سرعت، توقف تعریف شده است. در همان ابتدا یعنی زمان نمونه‌سازی می‌توانیم با تنظیم صفات توسط متد سازنده، اشیا هم نوع ولی با صفات متفاوتی را ایجاد نماییم. برای مثال: دو خودروی آبی با ظرفیت باک ۲۰ لیتر و بیشینه سرعت ۸۰ کیلومتر-ساعت یا یک خودروی صورتی با ظرفیت باک ۴۰ لیتر و بیشینه سرعت ۱۶۰ کیلومتر-ساعت که البته هر سه آن‌ها تمام متدهای کلاس را در خود دارند:

../_images/l05-car-class-sample.jpg ../_images/l05-car-class-object-sample.jpg

کپسوله‌سازی

یک مفهوم دیگر در برنامه‌نویسی شی‌گرا، «کپسوله‌سازی» (Encapsulation) است. کپسوله‌سازی به معنی پنهان‌سازی داده‌ها یا عملیات‌های درون یک شی است که با محدودسازی دسترسی به آن‌ها به دست می‌آید. در این شرایط اشیا باید بدون آگاهی از ساختار درونی و چگونگی انجام عملیات یکدیگر به تعامل بپردازند. این امر از پیچیدگی برنامه جلوگیری و تغییر را ساده‌تر می‌کند.

وراثت

وراثت (Inheritance) یکی از شکل‌های «قابلیت استفاده مجدد» کد بوده که برنامه‌نویس را قادر می‌سازد تا با ارث‌بری صفات و متدهای یک یا چند کلاس موجود، کلاس‌های جدیدی را ایجاد نماید.

برای نمونه فرض کنیم طراح کلاس خودروی پیش، قصد طراحی یک مدل خودرو جدید با رویکرد باربری دارد؛ بنابراین می‌بایست کلاسی جدید برای تولید آن تهیه نماید. ولی کلاس جدید علاوه‌بر صفات (ظرفیت بارگیری و..) و متدهای (انجام بارگیری، تخلیه بار و...) خاص خودش به صفات (رنگ بدنه، ظرفیت باک و...) و متدهای (راندن، سوخت گیری، توقف و...) مشابه در کلاس قبل هم نیاز دارد؛ در این حالت نیازی به تعریف مجدد آن‌ها نیست و می‌توان صفات و متدهای کلاس پیش را در کلاس جدید به ارث برد یا یک Abstract Class برای درج تعاریف مشترک هر دو کلاس ایجاد کرد که این دو مدل خورد می‌توانند آن را به ارث ببرند.

به کلاسی که از آن ارث‌بری می‌شود ”Parent Class“ یا ”Base Class“ (کلاس پایه) یا ”Superclass“ و به کلاسی که اقدام به ارث‌بری می‌کند ”Child Class“ (کلاس فرزند) یا ”Derived Class“ یا ”Subclass“ گفته می‌شود.

ارث‌بری توسط «نسبت هست-یک» (IS-A Relationship) بیان می‌شود؛ این نسبت می‌گوید کلاس فرزند یک نوع از چیزی است که کلاس پایه هست. کلاس A از کلاس B ارث‌بری دارد؛ در این حالت می‌گوییم: A is a type of B، یعنی درست است اگر بگوییم: «سیب» یک نوع «میوه» است یا «خودرو» یک نوع «وسیله نقلیه» است ولی توجه داشته باشید که این یک ارتباط یک‌طرفه از کلاس فرزند به کلاس پایه است و نمی‌توانیم بگوییم: «میوه» یک نوع «سیب» است یا «وسیله نقلیه» یک نوع «خودرو» است.

کلاس‌ها می‌توانند مستقل باشند ولی هنگامی که وارد رابطه‌های وراثت می‌شوند، یک ساختار سلسله مراتب (Hierarchy) به شکل درخت را تشکیل می‌دهند. برای نمونه به ساختار سلسله مراتب وراثت پایین که مربوط به برخی اشکال هندسی است توجه نمایید، پیکان‌ها نشانگر نسبت is-a هستند.

../_images/l05-Inheritance-Hierarchy-Sample.png

ترکیب

در برنامه‌نویسی شی‌گرا نسبت دیگری نیز با عنوان «نسبت دارد-یک» (HAS-A Relationship) وجود دارد که بیانگر مفهومی به نام «ترکیب» (Composition) است که شکل دیگری از قابلیت استفاده مجدد کد می‌باشد ولی مفهومی متفاوت با وراثت دارد. این نسبت زمانی بیان می‌شود که درون یک کلاس (مانند: C) از کلاس دیگری (مانند: D) نمونه‌سازی شده باشد؛ یعنی شی کلاس C درون خودش شی‌ای از کلاس D را داشته باشد؛ در این حالت می‌گوییم: C has a D. به یاد دارید خواندیم کلاس کارخانه خودروسازی با هماهنگ کردن اشیای واحدهای کوچکتر، عملیات خود را تعریف می‌کند. مثلا کلاس موتور - یعنی درون این کلاس یک شی از کلاس موتور ایجاد شده است، اکنون می‌توانیم بگوییم: «خودرو» یک «موتور» دارد.

../_images/l05-has-a-Sample.png

چندریختی

مفهوم چندریختی (Polymorphism) بیانگر توانایی کلاس فرزند در بازتعریف متدهایی است که در کلاس پایه موجود می‌باشند. برای نمونه دو کلاس «ماهی» و «گربه» را که هر دو آن‌ها از کلاسی به نام «حیوان» ارث‌بری دارند را در نظر بگیرید؛ در کلاس حیوان متدی با عنوان «غذا خوردن» که عملی مشترک در میان تمام حیوانات است وجود دارد ولی از آنجا که چگونگی انجام آن در ماهی و گربه متفاوت است، بنابراین هر دو این کلاس‌ها نیاز دارند تا متد «غذا خوردن» مخصوص خود را داشته باشند - در این جاست که این متد در کلاس‌های فرزند بازتعریف می‌شود، به این عمل ”Method Overriding“ گفته می‌شود. با Override کردن یک متد، متد کلاس پایه زیر سایه متد مشابه در کلاس فرزند قرار می‌گیرد و از نظر اشیا کلاس فرزند پنهان می‌شود.

تجرید

تجرید (Abstraction) در برنامه‌نویسی شی‌گرا به همراه چندریختی می‌آید و توسط دو مفهوم «کلاس‌های مجرد» (Abstract Classes) و «متدهای مجرد» (Abstract Methods) ارایه می‌گردد.

«کلاس مجرد» کلاسی است که شامل یک یا چند «متد مجرد» باشد و «متد مجرد» متدی است که اعلان (Declare) شده ولی بدنه آن ‌تعریف (Define) نشده است. کلاس‌های مجرد قابلیت نمونه‌سازی ندارند و نمی‌توان از آن‌ها شی ایجاد نمود؛ چرا که هدف از توسعه آن‌ها قرار گرفتن در بالاترین سطح (یا چند سطح بالایی) درخت وراثت، به عنوان کلاس پایه برای ارث‌بری کلاس‌های پایین‌تر می‌باشد. ایده طراحی کلاس مجرد در تعیین یک نقشه توسعه برای کلاس‌های فرزند آن است؛ تعیین صفات و متدهای لازم ولی واگذاردن تعریف متدها بر عهده کلاس‌های فرزند.

به عنوان نمونه سه کلاس «ماهی»، «گربه» و «کبوتر» را در نظر بگیرید. این کلاس‌ها جدا از رفتارهای خاص خود (مانند: «پرواز کردن» در کبوتر یا «شنا کردن» در ماهی)، در یک سری رفتار به مانند «نفس کشیدن»، «غذا خوردن» و... مشترک هستند. راه درستِ توسعه این کلاس‌ها تعیین یک «کلاس پایه» برای رفتارهای مشترک و ارث‌بری هر سه آن‌ها می‌باشد. ولی از آنجا که هر یک، این رفتارهای مشترک را به گونه‌ای دیگر انجام می‌دهد؛ راه درست‌تر آن است که یک «کلاس مجرد» به عنوان «کلاس پایه» آن‌ها در نظر بگیریم؛ در این حالت هر کدام از کلاس‌ها ضمن دانستن رفتارهای لازم می‌تواند آن‌‌ها را متناسب با خواست خود تعریف نمایند.

توجه

آنچه در ادامه این درس آورده شده است، چکیده‌ای از پیاده‌سازی برنامه‌نویسی شی گرا در پایتون است. شی گرایی در زبان برنامه نویسی پایتون به صورت کامل از درس هفدهم تا بیست و دوم به صورت کامل شرح داده می‌شود.

اشیا در پایتون

علاوه‌بر اینکه پایتون یک زبان برنامه‌نویسی شی‌گراست، ساختار آن نیز بر مبنای شی‌گرایی توسعه یافته است و اینطور بیان می‌شود که هر چیزی در پایتون یک شی است. اشیا، انتزاعِ پایتون برای ارایه «انواع داده‌» (Data Types) هستند. به بیان دیگر تمام داده‌های یک برنامه پایتونی یا به صورت مستقیم یک شی است یا از روابط بین اشیا ایجاد می‌گردد. برای نمونه: 56، "!Hello World"، توابع و... حتی خود کلاس‌ها نیز توسط یک نوع شی ارایه می‌شوند.

هر شی در پایتون حاوی یک «شناسه» (identity)، یک «نوع» (type) و یک «مقدار» (value) است.

  • «شناسه» در زمان ایجاد شی به آن اختصاص می‌یابد و غیر قابل تغییر است. تابع ()id شناسه شی را به صورت یک عدد صحیح برمی‌گرداند که این مقدار در CPython بیانگر نشانی (Address) شی در حافظه (Memory) است:

    >>> id(5)
    140468674877440
    
    >>> num = 0.25
    >>> id(num)
    140468676592120
    
    >>> msg = "Hello World!"
    >>> id(msg)
    140468675425264
    
  • هر شی در پایتون دارای یک «نوع» یا ”type“ است که در واقع معرف کلاسی است که شی از آن ایجاد گردیده است. نوع هر شی توسط تابع ()type قابل مشاهده است:

    >>> type(127)
    <class 'int'>
    
    >>> msg = "Hello World!"
    >>> type(msg)
    <class 'str'>
    
    >>> type(print)
    <class 'builtin_function_or_method'>
    

    ملاحظه

    تمام اعداد صحیح (Integers) در پایتون یک شی از نوع int می‌باشند. [با انواع آماده (Built-in) شی در پایتون توسط دروس آینده آشنا خواهید شد.]

  • «مقدار» برخی اشیا در پایتون قابل تغییر است که به این دسته از اشیا ”mutable“ (تغییر پذیر) گفته می‌شود؛ ولی مقدار برخی دیگر قابل تغییر نمی‌باشد (مانند اعداد: شی 127) که به آن‌ها اشیا ”immutable“ (تغییر ناپذیر) می‌گویند.

کلاس‌ها در پایتون

از نسخه 2.2 طراحی کلاس‌ها در پایتون تغییر کرده است. [New-style Classes] در ساختار جدید مفهوم ”type“ برابر مفهوم ”class“ طراحی شده است. در این ساختار هر کلاس، خود یک شی از کلاسی به نام type می‌باشد و همچنین تمامی کلاس‌ها از کلاسی به نام object ارث‌بری دارند:

>>> num = 3

>>> num.__class__
<class 'int'>

>>> type(num)
<class 'int'>

>>> type(type(num))
<class 'type'>

>>> type(num).__class__
<class 'type'>

>>> type(num).__bases__
(<class 'object'>,)

ملاحظه

صفت __class__ نام کلاس یک شی و صفت __bases__ نام کلاس‌های پایه یک کلاس را نمایش می‌دهد.



😊 امیدوارم مفید بوده باشه