در این مقاله قصد داریم تا نگاهی به پارادایم‌های برنامه نویسی بیندازیم، این موضوع عنوان جذابی برای توصیف روش‌ها یا سبک‌های رایج برای سازماندهی برنامه نویسی ما خواهد بود. سعی خواهیم کرد توضیح ساده‌ای از هر پارادایم ارائه کنیم. به این ترتیب می‌توانیم با مفهوم هر کدام از این پارادایم‌ها آشنا شویم. البته چند نمونه شبه کد و کد را نیز باهم بررسی خواهیم کرد.

منظور از پارادایم برنامه نویسی چیست؟

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

این سوال که چرا پارادایم‌های برنامه نویسی مختلف وجود دارد، مشابه این است که بپرسیم چرا زبان‌های برنامه نویسی زیادی وجود دارد. پارادایم‌های خاص برای انواع خاصی از مشکلات مناسب‌تر هستند، بنابراین استفاده از پارادایم‌های مختلف برای انواع مختلف پروژه‌ها منطقی‌تر است. همچنین، روش‌هایی که هر پارادایم را تشکیل می‌دهند در طول زمان توسعه یافته‌اند و به لطف پیشرفت‌های نرم‌افزاری و سخت‌افزاری، رویکردهای متفاوتی مطرح شده‌اند که قبلاً وجود نداشتند.

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

همه موارد به این واقعیت منجر می‌شود که امروزه وقتی می‌خواهیم برنامه‌ای را بنویسیم و ساختار دهیم، گزینه‌های زیادی برای انتخاب داشته باشیم.

چه چیزی پارادایم برنامه نویسی به حساب نمی‌آید؟

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

زبان‌های برنامه نویسی همیشه به یک پارادایم خاص گره‌خورده نیستند. زبان‌هایی هستند که با الگوی خاصی ساخته شده‌اند و دارای ویژگی‌هایی هستند که این نوع برنامه ‌نویسی را بیشتر از سایرین تسهیل می‌کنند.

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

چرا باید به این مفهوم بپردازیم؟

اگر بخواهیم به این سوال پاسخ کوتاه بدهیم، دلیل آن دانش عمومی است.

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

پارادایم‌های محبوب برنامه نویسی

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

برنامه نویسی imperative

برنامه نویسی imperative (دستوری) شامل مجموعه‌ای از دستورالعمل‌های دقیق است که به کامپیوتر داده می‌شود تا به ترتیب معین اجرا شوند. به آن “imperative” می‌گوییم زیرا ما به عنوان برنامه نویس دقیقاً آنچه را که کامپیوتر باید انجام دهد به روشی بسیار خاص دیکته می‌کنیم.

برنامه نویسی imperative بر توصیف نحوه عملکرد یک برنامه، به شکل مرحله به مرحله تمرکز دارد.

اگر بخواهیم مفهوم این روش برنامه نویسی را به‌طور دقیق‌تر درک کنیم آن را با یک مثال کیک‌پزی بررسی می‌کنیم:

۱- Pour flour in a bowl
۲- Pour a couple eggs in the same bowl
۳- Pour some milk in the same bowl
۴- Mix the ingredients
۵- Pour the mix in a mold
۶- Cook for 35 minutes
۷- Let chill

اما اگر بخواهیم یک مثال واقعی را بررسی کنیم، فرض کنید می‌خواهیم آرایه‌ای از اعداد را فیلتر کنیم تا فقط عناصر بزرگ‌تر از ۵ را نگه داریم. کد ما ممکن است به این صورت باشد:

const nums = [1,4,3,6,7,8,9,2]
const result = []

for (let i = 0; i < nums.length; i++) {
    if (nums[i] > 5) result.push(nums[i])
}

console.log(result) // Output: [ 6, 7, 8, 9 ]

در این قطعه کد به برنامه می‌گوییم که مقدار المنت آرایه را با ۵ مقایسه کند و اگر بزرگ‌تر از ۵ باشد آن را به یک آرایه جدید push کند، و این کار را روی تمام المنت‌های آرایه تکرار کند.

ما همه دستورالعمل‌ها را به شکل خیلی دقیق ذکر کرده‌ایم و این همان چیزی است که برنامه نویسی imperative انجام می‌دهد.

برنامه نویسی procedural

برنامه نویسی procedural (رویه‌ای) مشتق شده از برنامه نویسی imperative است که ویژگی توابع را به آن اضافه می‌کند (همچنین به عنوان “procedureها” یا “subroutineها” شناخته می‌شود).

در برنامه‌نویسی procedural، کاربر تشویق می‌شود تا به عنوان راهی برای بهبود ماژلار بودن و سازماندهی برنامه، اجرای آن را به توابع تقسیم کند.

به دنبال مثال کیکی که داشتیم، برنامه نویسی procedural ممکن است شبیه به مثال زیر باشد:

function pourIngredients() {
    - Pour flour in a bowl
    - Pour a couple eggs in the same bowl
    - Pour some milk in the same bowl
}

function mixAndTransferToMold() {
    - Mix the ingredients
    - Pour the mix in a mold
}

function cookAndLetChill() {
    - Cook for 35 minutes
    - Let chill
}

pourIngredients()
mixAndTransferToMold()
cookAndLetChill()

همانطور که می بینید با استفاده از توابع، اکنون می‌توانیم فقط سه فراخوانی تابع موجود در انتهای فایل را بخوانیم و ایده خوبی از عملکرد برنامه خود بدست بیاوریم.

ساده‌سازی و انتزاع یکی از مزایای برنامه نویسی procedural است. اما در داخل توابع همچنان همان کدهای ضروری قدیمی را داریم.

برنامه نویسی functional

برنامه نویسی functional (تابعی) مفهوم توابع را کمی فراتر می‌برد.

در برنامه نویسی functional، توابع به عنوان شهروندان درجه یک در نظر گرفته می‌شوند، به این معنی که می‌توان آن‌ها را به متغیرها نسبت داد، به عنوان آرگومان ارسال کرد و از توابع دیگر برگرداند.

مفهوم کلیدی دیگر ایده توابع pure است. تابع pure تابعی است که برای تولید نتیجه فقط به ورودی‌های خود متکی است و با توجه به ورودی یکسان، همیشه همان نتیجه را ایجاد می‌کند. علاوه بر این، هیچ side effectای (هر تغییری در خارج از محیط تابع) ایجاد نمی‌کند.

با در نظر گرفتن این مفاهیم، برنامه نویسی functional از برنامه‌هایی که عمدتاً با توابع نوشته شده‌اند حمایت می‌کند. همچنین از این ایده که ماژولار بودن کد و عدم وجود side effectها، شناسایی و تفکیک مسئولیت‌ها در پایگاه کد را آسان‌تر می‌کند دفاع می‌کند. در نتیجه این قابلیت، نگه‌داری کد را بهبود می‌بخشد.

با بازگشت به مثال فیلتر آرایه، می‌توانیم ببینیم که با پارادایم imperative ممکن است از یک متغیر خارجی برای ذخیره نتیجه تابع استفاده کنیم که می‌تواند یک side effect در نظر گرفته شود.

const nums = [1,4,3,6,7,8,9,2]
const result = [] // External variable

for (let i = 0; i < nums.length; i++) {
    if (nums[i] > 5) result.push(nums[i])
}

console.log(result) // Output: [ 6, 7, 8, 9 ]

برای تبدیل آن به برنامه نویسی functional، می‌توانیم این کار را به صورت زیر انجام دهیم:

const nums = [1,4,3,6,7,8,9,2]

function filterNums() {
    const result = [] // Internal variable

    for (let i = 0; i < nums.length; i++) {
        if (nums[i] > 5) result.push(nums[i])
    }

    return result
}

console.log(filterNums()) // Output: [ 6, 7, 8, 9 ]

کد بالا تقریباً همان کد قبلی است با این تفاوت که اینجا تکرار را در یک تابع قرار می‌دهیم که در آن آرایه result را نیز ذخیره می‌کنیم. به این ترتیب، می‌توانیم اطمینان حاصل کنیم که تابع چیزی خارج از محدوده خود را تغییر نمی‌دهد. فقط یک متغیر برای پردازش اطلاعات خود ایجاد می‌کند و پس از پایان اجرا، متغیر نیز از بین می‌رود.

برنامه نویسی declarative

برنامه نویسی declarative (اعلامی) به معنای پنهان کردن پیچیدگی و نزدیک کردن زبان‌های برنامه نویسی به زبان و تفکر انسان است. این رویکرد دقیقاً برعکس برنامه نویسی imperative است به این معنا که برنامه نویس دستورالعمل‌هایی در مورد نحوه اجرای کار توسط کامپیوتر نمی‌دهد، بلکه در مورد نتیجه مورد نظر را ارائه می‌کند.

این مفهوم با یک مثال بسیار واضح‌تر خواهد بود. به دنبال همان مثال فیلتر آرایه، رویکرد declarative به شکل زیر خواهد بود:

const nums = [1,4,3,6,7,8,9,2]

console.log(nums.filter(num => num > 5)) // Output: [ 6, 7, 8, 9 ]

همانطور که می‌بینید با تابع filter، ما صریحاً به کامپیوتر نمی‌گوییم که روی آرایه تکرار کند یا مقادیر را در یک آرایه جداگانه ذخیره کند. بلکه ما فقط آنچه می‌خواهیم انجام شود یعنی عمل filter شدن و شرطی که باید برآورده شود یعنی ” num > 5” را اعلام می‌کنیم.

نکته مثبت این است که خواندن و درک آن آسان‌تر است و اغلب نوشتن آن کوتاه‌تر می‌باشد. توابع filter، map، reduceو sortجاوااسکریپت نمونه‌های خوبی از کدهای declarative هستند.

مثال خوب دیگر فریمورک‌ها و یا کتابخانه‌های مدرن جاوااسکریپت مانند React هستند. به عنوان مثال کد زیر را در نظر بگیرید:

<button onClick={() => console.log('You clicked me!')}>Click me</button>

در اینجا ما یک المنت باتن داریم با یک event listener که هنگام کلیک روی باتن، یک تابع console.log را فعال می‌کند.

سینتکس JSX (آنچه که در React استفاده می‌شود) ترکیب HTML و جاوااسکریپت می‌باشد که نوشتن برنامه‌ها را آسان‌تر و سریع‌تر می‌کند. اما این چیزی نیست که مرورگرها می‌خوانند و اجرا می‌کنند. کد React بعداً به HTML و جاوااسکریپت معمولی تبدیل می‌شود و این همان چیزی است که مرورگرها در واقعیت اجرا می‌کنند.

JSX در واقع declarative است، یعنی هدف آن این است که به توسعه‌دهندگان یک رابط کاربری دوستانه و کارآمدتر برای کار با آن ارائه دهد.

نکته مهمی که در مورد برنامه نویسی declarative باید به آن توجه کنیم این است که به هر حال کامپیوتر این اطلاعات را به عنوان کد imperative پردازش می‌کند.

در ادامه‌ی مثال آرایه‌ای که داشتیم، کامپیوتر همچنان روی آرایه همانند یک حلقه for تکرار می‌شود، اما به عنوان برنامه‌ نویس نیازی به کدنویسی مستقیم نداریم. کاری که برنامه نویسی declarative انجام می‌دهد این است که این پیچیدگی را از دید مستقیم برنامه نویس پنهان می‌کند.

برنامه نویسی object-oriented

یکی از محبوب‌ترین پارادایم‌های برنامه نویسی، برنامه نویسی OOP (شی گرا) است.

مفهوم اصلی OOP این است که مسئولیت‌ها بین موجودیت‌هایی که به عنوان اشیاء کدگذاری می‌شوند، تفکیک شوند. هر موجودیت مجموعه‌ای از اطلاعات(ویژگی‌ها) و اقدامات(متدها) را که می‌تواند توسط آن موجودیت انجام شود، گروه‌بندی می‌کند.

OOP از کلاس‌ها استفاده زیادی می‌کند. اشیایی که از یک کلاس ایجاد می‌شوند، نمونه می‌نامیم.

به دنبال مثال آشپزی با شبه کد ما، اکنون فرض کنید در نانوایی خود یک آشپز اصلی (به نام فرانک) و یک دستیار آشپز (به نام آنتونی) داریم و هر یک از آن‌ها وظایف خاصی در فرآیند پخت خواهند داشت. اگر از OOP استفاده کنیم، برنامه ما ممکن است به این شکل باشد:

// Create the two classes corresponding to each entity
class Cook {
  constructor constructor (name) {
        this.name = name
    }

    mixAndBake() {
        - Mix the ingredients
    	- Pour the mix in a mold
        - Cook for 35 minutes
    }
}

class AssistantCook {
    constructor (name) {
        this.name = name
    }

    pourIngredients() {
        - Pour flour in a bowl
        - Pour a couple eggs in the same bowl
        - Pour some milk in the same bowl
    }
    
    chillTheCake() {
    	- Let chill
    }
}

// Instantiate an object from each class
const Frank = new Cook('Frank')
const Anthony = new AssistantCook('Anthony')

// Call the corresponding methods from each instance
Anthony.pourIngredients()
Frank.mixAndBake()
Anthony.chillTheCake()

مزیتی که برنامه نویسی OOP دارد این است که با تفکیک خوب مسئولیت‌ها درک برنامه را بسیار تسهیل می‌کند.

در این ویدیو کانال یوتیوب، مقایسه خوبی بین برنامه نویسی رویه‌ای، تابعی و شی ‌گرا انجام شده است که مطالعه آن می‌تواند بسیار مفید باشد.

جمع‌بندی

همانطور که دیدیم، پارادایم‌های برنامه نویسی راه‌های مختلفی هستند که در آن‌ها می‌توانیم با مشکلات برنامه نویسی مواجه شویم و کدهای خود را سازماندهی کنیم.

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