در این مقاله قصد داریم تا درمورد مفاهیم اصلی برنامه نویسی شی‌ گرا (OOP) صحبت کنیم و آن‌ها را در مثال‌های کاربردی با استفاده از زبان جاوااسکریپت بررسی کنیم.

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

مفهوم اصلی برنامه نویسی شی‌ گرا تفکیک موضوعات و مسئولیت‌ها و تخصیص آن‌ها به موجودیت‌ها است.

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

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

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

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

نحوه ایجاد آبجکت‌ها-کلاس‌ها

همانطور که می‌دانیم هر بازی ویدیویی به شخصیت‌پردازی نیاز دارد، و همه کاراکترها دارای ویژگی‌های خاصی مانند رنگ، قد، نام و غیره و توانایی‌ها(متدهایی) مانند پریدن، دویدن، مشت زدن و غیره هستند. آبجکت‌ها ساختار داده‌ای مناسب برای ذخیره این نوع اطلاعات به حساب می‌آیند.

فرض کنید ما ۳ «گونه» شخصیت مختلف در دسترس داریم و می‌خواهیم ۶ شخصیت مختلف را ایجاد کنیم یعنی از هر گونه ۲ تا بسازیم. یک راه برای ساخت کاراکترهای مورد نظر ما می‌تواند این باشد که آبجکت‌ها را به‌صورت دستی و با استفاده از object literals ایجاد کنیم. به این صورت که:

const alien1 = {
    name: "Ali",
    species: "alien",
    phrase: () => console.log("I'm Ali the alien!"),
    fly: () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}
const alien2 = {
    name: "Lien",
    species: "alien",
    sayPhrase: () => console.log("Run for your lives!"),
    fly: () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}
const bug1 = {
    name: "Buggy",
    species: "bug",
    sayPhrase: () => console.log("Your debugger doesn't work with me!"),
    hide: () => console.log("You can't catch me now!")
}
const bug2 = {
    name: "Erik",
    species: "bug",
    sayPhrase: () => console.log("I drink decaf!"),
    hide: () => console.log("You can't catch me now!")
}
const Robot1 = {
    name: "Tito",
    species: "robot",
    sayPhrase: () => console.log("I can cook, swim and dance!"),
    transform: () => console.log("Optimus prime!")
}
const Robot2 = {
    name: "Terminator",
    species: "robot",
    sayPhrase: () => console.log("Hasta la vista, baby!"),
    transform: () => console.log("Optimus prime!")
}

همانطور که می‌بینید همه کاراکترها دارای ویژگی nameو speciesو همچنین متد sayPhraseهستند. علاوه بر این، هر گونه متدی دارد که فقط به آن گونه تعلق دارد (مثلاً alienها متد flyدارند).

همچنین برخی از داده‌ها توسط همه کاراکترها به اشتراک گذاشته می‌شود، برخی از داده‌ها توسط هر گونه به اشتراک گذاشته می‌شود، و برخی از داده‌ها برای هر کاراکتر منحصربه‌فرد است.

این رویکرد به درستی کار می‌کند. به این صورت که می‌توانیم به ویژگی‌ها و متدها کاملا دسترسی داشته باشیم، به عنوان مثال:

console.log(alien1.name) // output: "Ali"
console.log(bug2.species) // output: "bug"
Robot1.sayPhrase() // output: "I can cook, swim and dance!"
Robot2.transform() // output: "Optimus prime!"

مشکلی که در این مثال وجود دارد این است که اصلا مقیاس‌بندی خوبی ندارد و مستعد خطا است. تصور کنید که بازی ما می‌تواند صدها کاراکتر داشته باشد. در این صورت باید به شکل دستی ویژگی‌ها و متدها را برای هر یک از آن‌ها تنظیم کنیم!

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

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

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

class Alien { // Name of the class
    // The constructor method will take a number of parameters and assign those parameters as properties to the created object.
    constructor (name, phrase) {
        this.name = name
        this.phrase = phrase
        this.species = "alien"
    }
    // These will be the object's methods.
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    sayPhrase = () => console.log(this.phrase)
}

class Bug {
    constructor (name, phrase) {
        this.name = name
        this.phrase = phrase
        this.species = "bug"
    }
    hide = () => console.log("You can't catch me now!")
    sayPhrase = () => console.log(this.phrase)
}

class Robot {
    constructor (name, phrase) {
        this.name = name
        this.phrase = phrase
        this.species = "robot"
    }
    transform = () => console.log("Optimus prime!")
    sayPhrase = () => console.log(this.phrase)
}

و سپس می‌توانیم کاراکترهای مورد نظر خود را از آن کلاس‌ها به صورت زیر نمونه‌سازی کنیم:

const alien1 = new Alien("Ali", "I'm Ali the alien!")
// We use the "new" keyword followed by the corresponding class name
// and pass it the corresponding parameters according to what was declared in the class constructor function

const alien2 = new Alien("Lien", "Run for your lives!")
const bug1 = new Bug("Buggy", "Your debugger doesn't work with me!")
const bug2 = new Bug("Erik", "I drink decaf!")
const Robot1 = new Robot("Tito", "I can cook, swim and dance!")
const Robot2 = new Robot("Terminator", "Hasta la vista, baby!")

سپس دوباره می‌توانیم به ویژگی‌ها و متدهای هر آبجکت مانند مثال زیر دسترسی داشته باشیم:

console.log(alien1.name) // output: "Ali"
console.log(bug2.species) // output: "bug"
Robot1.sayPhrase() // output: "I can cook, swim and dance!"
Robot2.transform() // output: "Optimus prime!"

مزیتی که این رویکرد، و به‌طور کلی استفاده از کلاس‌ها به همراه دارد این است که می‌توانیم از آن «طرح‌ها» برای ایجاد آبجکت‌های جدید استفاده کنیم و این موضوع نسبت به زمانی که این کار را به‌ صورت «دستی» انجام می‌دادیم هم سریع‌تر است و هم ایمن‌تر.

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

نکاتی که در مورد کلاس‌ها باید به خاطر داشته باشیم:

به بیان رسمی‌تر:

یک کلاس در یک برنامه، تعریفی از “type” ساختار داده سفارشی است که هم داده‌ها و هم رفتارهایی که بر روی آن داده‌ها عمل می‌کنند را شامل می‌شود. کلاس‌ها نحوه کارکرد چنین ساختار داده‌ای را تعریف می‌کنند، اما خود آن‌ها مقادیر مشخصی نیستند. برای به دست آوردن یک مقدار مشخص که می‌توانیم در برنامه خود از آن استفاده کنیم، باید از کلاس با استفاده از کلمه کلیدی new یک یا چند بار نمونه‌سازی کنیم.

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

OOP معمولاً با چهار اصل کلیدی توضیح داده می‌شود که نحوه عملکرد برنامه‌های OOP را بیان می‌کند که عبارتند از: inheritance(وراثت)، encapsulation(کپسوله‌سازی)، abstraction(انتزاع) و polymorphism(چندشکلی). در ادامه هر یک از این موارد را بررسی می‌کنیم.

inheritance

وراثت به معنی توانایی ایجاد کلاس‌ها بر اساس کلاس‌های دیگر است. یعنی با وراثت می‌توانیم یک کلاس parent(با ویژگی‌ها و متدهای خاص) ایجاد کنیم و سپس کلاس‌های child تعریف کنیم که تمام ویژگی‌ها و متدهایی که دارند را از کلاس parent به ارث می‌برند.

در ادامه یک مثال را باهم بررسی می‌کنیم. تصور کنید تمام کاراکترهایی که قبلا آن‌ها را تعریف کردیم دشمن کاراکتر اصلی ما خواهند بود و به عنوان دشمن، همگی دارای ویژگی “power” و متد “attack” خواهند بود.

یکی از راه‌های پیاده‌سازی آن اضافه کردن همان ویژگی‌ها و متدها به تمام کلاس‌هایی است که داشتیم، مانند:

...

class Bug {
    constructor (name, phrase, power) {
        this.name = name
        this.phrase = phrase
        this.power = power
        this.species = "bug"
    }
    hide = () => console.log("You can't catch me now!")
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}

class Robot {
    constructor (name, phrase, power) {
        this.name = name
        this.phrase = phrase
        this.power = power
        this.species = "robot"
    }
    transform = () => console.log("Optimus prime!")
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}

const bug1 = new Bug("Buggy", "Your debugger doesn't work with me!", 10)
const Robot1 = new Robot("Tito", "I can cook, swim and dance!", 15)

console.log(bug1.power) //output: 10
Robot1.attack() // output: "I'm attacking with a power of 15!"

اما همانطور که می‌بینید ما کد را تکرار می‌کنیم، و این اتفاق اصلا بهینه نیست. یک راه بهتر این است که یک کلاس “Enemy” پرنت تعریف کنیم که این کلاس قرار است توسط همه گونه‌های دشمن extend پیدا کند، مانند:

class Enemy {
    constructor(power) {
        this.power = power
    }

    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power) {
        super(power)
        this.name = name
        this.phrase = phrase
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    sayPhrase = () => console.log(this.phrase)
}

...

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

در کلاس‌های child، از کلمه کلیدی extendsبرای تعریف کلاس parentای که می‌خواهیم از آن ارث‌بری کنیم، استفاده می‌کنیم. سپس در متد سازنده باید پارامتر “power” را تعریف کنیم و از تابع superبرای نشان دادن اینکه ویژگی در کلاس parent تعریف شده است استفاده کنیم.

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

const alien1 = new Alien("Ali", "I'm Ali the alien!", 10)
const alien2 = new Alien("Lien", "Run for your lives!", 15)

alien1.attack() // output: I'm attacking with a power of 10!
console.log(alien2.power) // output: 15

اکنون فرض کنید می‌خواهیم یک کلاس parent جدید اضافه کنیم که همه کاراکترهای ما را اعم از گروه دشمن و غیر دشمن را گروه‌بندی کند. همینطور می‌خواهیم یک ویژگی “speed” و یک متد “move” تنظیم کنیم. این کار را به‌صورت زیر می‌توانیم انجام دهیم:

class Character {
    constructor (speed) {
        this.speed = speed
    }

    move = () => console.log(`I'm moving at the speed of ${this.speed}!`)
}

class Enemy extends Character {
    constructor(power, speed) {
        super(speed)
        this.power = power
    }

    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(power, speed)
        this.name = name
        this.phrase = phrase
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    sayPhrase = () => console.log(this.phrase)
}

ابتدا کلاس parent جدید به نام “Character” را تعریف می‌کنیم. سپس آن را روی کلاس Enemy توسعه می‌دهیم. و در نهایت پارامتر “speed” جدید را بهconstructorو توابع superدر کلاس Alien خود اضافه می‌کنیم.

ما همانند همیشه پاس دادن پارامترها را نمونه‌سازی می‌کنیم و دوباره می‌توانیم از کلاس “grandparent” به ویژگی‌ها و متدها دسترسی داشته باشیم.

const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)
const alien2 = new Alien("Lien", "Run for your lives!", 15, 60)

alien1.move() // output: "I'm moving at the speed of 50!"
console.log(alien2.speed) // output: 60

اکنون که اطلاعات بیشتری درباره وراثت داریم، کد خود را مجدداً اصلاح می‌کنیم که بتوانیم تا حد امکان از تکرار شدن آن جلوگیری کنیم:

class Character {
    constructor (speed) {
        this.speed = speed
    }
    move = () => console.log(`I'm moving at the speed of ${this.speed}!`)
}

class Enemy extends Character {
    constructor(name, phrase, power, speed) {
        super(speed)
        this.name = name
        this.phrase = phrase
        this.power = power
    }
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

class Bug extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "bug"
    }
    hide = () => console.log("You can't catch me now!")
}

class Robot extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "robot"
    }
    transform = () => console.log("Optimus prime!")
}


const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)
const alien2 = new Alien("Lien", "Run for your lives!", 15, 60)
const bug1 = new Bug("Buggy", "Your debugger doesn't work with me!", 25, 100)
const bug2 = new Bug("Erik", "I drink decaf!", 5, 120)
const Robot1 = new Robot("Tito", "I can cook, swim and dance!", 125, 30)
const Robot2 = new Robot("Terminator", "Hasta la vista, baby!", 155, 40)

همانطور که می‌بینید کلاس‌های گونه‌های ما اکنون بسیار کوچک‌تر به نظر می‌رسند، دلیل این اتفاق این است که همه ویژگی‌ها و متدهای مشترک را به یک کلاس parent مشترک منتقل کردیم. این همان نوع ارث‌بری سودمند است که می‌تواند به ما کمک کند.

نکاتی که درمورد وراثت باید به خاطر داشته باشیم

به عنوان مثال:

// This works:
class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

// This throws an error:
class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        this.species = "alien" // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
        super(name, phrase, power, speed)
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

برای مثال، در کد قبلی ما، کلاس Alien کلاس Enemy را extend می‌کند و متد attackرا به ارث می‌برد و بنابراین I'm attacking with a power of ${this.power}!را چاپ می‌کند:

class Enemy extends Character {
    constructor(name, phrase, power, speed) {
        super(speed)
        this.name = name
        this.phrase = phrase
        this.power = power
    }
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)
alien1.attack() // output: I'm attacking with a power of 10!

اکنون فرض کنید می‌خواهیم متد attackدر کلاس Alien کار متفاوتی انجام دهد. برای این کار می‌توانیم آن را به شکل زیر دوباره تعریف کنیم:

class Enemy extends Character {
    constructor(name, phrase, power, speed) {
        super(speed)
        this.name = name
        this.phrase = phrase
        this.power = power
    }
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    attack = () => console.log("Now I'm doing a different thing, HA!") // Override the parent method.
}

const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)
alien1.attack() // output: "Now I'm doing a different thing, HA!"

encapsulation

کپسوله‌سازی یکی دیگر از مفاهیم کلیدی برنامه نویسی شی‌ گرا است و مخفف ظرفیت یک آبجکت برای «تصمیم‌گیری» می‌باشد که چه اطلاعاتی را در معرض دسترسی قرار دهد و چه اطلاعاتی را نه. این مفهوم از طریق ویژگی‌ها و متدهای public و private قابلیت اجرا شدن را دارد.

در جاوااسکریپت تمام ویژگی‌ها و متدهای آبجکت‌ها به‌طور پیش‌فرض public هستند. “public” فقط به این معنی است که ما می‌توانیم به ویژگی و یا متد یک آبجکت از خارج از بدنه آن دسترسی داشته باشیم:

// Here's our class
class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

// Here's our object
const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)

// Here we're accessing our public properties and methods
console.log(alien1.name) // output: Ali
alien1.sayPhrase() // output: "I'm Ali the alien!"

برای روشن‌تر شدن این موضوع، ویژگی‌ها و متدهای private را هم باهم بررسی می‌کنیم.

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

class Alien extends Enemy {
    #birthYear // We first need to declare the private property, always using the '#' symbol as the start of its name.

    constructor (name, phrase, power, speed, birthYear) {
        super(name, phrase, power, speed)
        this.species = "alien"
        this.#birthYear = birthYear // Then we assign its value within the constructor function
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    howOld = () => console.log(`I was born in ${this.#birthYear}`) // and use it in the corresponding method.
}
    
// We instantiate the same way we always do
const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50, 10000)

سپس می‌توانیم به متد howOldبه شکل زیر دسترسی داشته باشیم:

alien1.howOld() // output: "I was born in 10000"

اما اگر بخواهیم مستقیماً به این ویژگی دسترسی پیدا کنیم، با خطا مواجه می‌شویم. حتی اگر آبجکت را console.log کنیم باز هم ویژگی private را نخواهیم دید.

console.log(alien1.#birthYear) // This throws an error
console.log(alien1) 
// output:
// Alien {
//     move: [Function: move],
//     speed: 50,
//     sayPhrase: [Function: sayPhrase],
//     attack: [Function: attack],
//     name: 'Ali',
//     phrase: "I'm Ali the alien!",
//     power: 10,
//     fly: [Function: fly],
//     howOld: [Function: howOld],
//     species: 'alien'
//   }

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

abstraction

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

polymorphism

پس از انتزاع مفهوم چندشکلی مطرح می‌شود. polymorphism به معنای «شکل‌های متعدد» است و در واقع یک مفهوم ساده به ‌شمار می‌آید. این مفهوم توانایی یک متد برای برگرداندن مقادیر مختلف براساس شرایط خاص را نشان می‌دهد.

مثلاً دیدیم که کلاس Enemy متد sayPhraseرا دارد، و تمام کلاس‌های گونه‌ها از کلاس Enemy ارث‌بری می‌کنند. پس در نتیجه همه آن‌ها نیز متد sayPhraseرا دارند.

اما باید به این موضوع توجه کنیم وقتی که متد را روی گونه‌های مختلف فراخوانی می‌کنیم، نتایج متفاوتی دریافت می‌کنیم:

const alien2 = new Alien("Lien", "Run for your lives!", 15, 60)
const bug1 = new Bug("Buggy", "Your debugger doesn't work with me!", 25, 100)

alien2.sayPhrase() // output: "Run for your lives!"
bug1.sayPhrase() // output: "Your debugger doesn't work with me!"

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

نوع دیگری از چندشکلی وجود دارد که مبتنی بر وراثت است و به زمانی اشاره می‌کند که ما یک کلاس parent داریم که یک متد را تنظیم می‌کند و کلاس child آن متد را لغو می‌کند تا به نحوی آن را تغییر دهد. مثالی که قبلا دیدیم در اینجا نیز کاملاً کاربرد دارد:

class Enemy extends Character {
    constructor(name, phrase, power, speed) {
        super(speed)
        this.name = name
        this.phrase = phrase
        this.power = power
    }
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
    attack = () => console.log("Now I'm doing a different thing, HA!") // Override the parent method.
}

const alien1 = new Alien("Ali", "I'm Ali the alien!", 10, 50)
alien1.attack() // output: "Now I'm doing a different thing, HA!"

مثال بالا یک پیاده‌سازی چند شکلی است زیرا اگر متد attackرا در کلاس Alien کامنت کنیم، همچنان می‌توانیم آن را روی آبجکت فراخوانی کنیم:

alien1.attack() // output: "I'm attacking with a power of 10!"

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

Object composition

Object composition تکنیکی است که به عنوان جایگزینی برای وراثت عمل می‌کند.

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

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

اکنون می‌خواهیم قابلیت پرواز را به کاراکترهای bug اضافه کنیم. همانطور که در کد دیدیم، فقط alienها متد flyدارند. بنابراین یک گزینه می‌تواند تکرار همان متد در کلاس Bug باشد:

class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}

class Bug extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "bug"
    }
    hide = () => console.log("You can't catch me now!")
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!") // We're duplicating code =(
}

گزینه دیگر این است که متد flyرا به کلاس Enemy منتقل کنید، بنابراین این متد می‌تواند توسط هر دو کلاس Alien و Bug به ارث برسد. اما در این صورت این متد برای کلاس‌هایی که به آن نیاز ندارند، مانند Robot نیز در دسترس قرار خواهد گرفت.

class Enemy extends Character {
    constructor(name, phrase, power, speed) {
        super(speed)
        this.name = name
        this.phrase = phrase
        this.power = power
    }
    sayPhrase = () => console.log(this.phrase)
    attack = () => console.log(`I'm attacking with a power of ${this.power}!`)
    fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
}


class Alien extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "alien"
    }
}

class Bug extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "bug"
    }
    hide = () => console.log("You can't catch me now!")
}

class Robot extends Enemy {
    constructor (name, phrase, power, speed) {
        super(name, phrase, power, speed)
        this.species = "robot"
    }
    transform = () => console.log("Optimus prime!")
  // I don't need the fly method =(
}

همانطور که مشاهده می‌کنید، ارث‌بری زمانی باعث ایجاد مشکل می‌شود که برنامه‌ای که در ابتدا برای کلاس‌های خود داشتیم، تغییر کند(این موضوع در دنیای واقعی تقریباً همیشه اتفاق می‌افتد). در این شرایط Object composition رویکردی را پیشنهاد می‌کند که در آن آبجکت‌ها فقط ویژگی‌ها و متدهایی را دریافت می‌کنند که به آن‌ها نیاز دارند.

اکنون در این مثال، می‌توانیم یک تابع ایجاد کنیم که تنها مسئولیت آن اضافه کردن متد flyبه هر آبجکتی است که آن را به عنوان پارامتر دریافت می‌کند:

const bug1 = new Bug("Buggy", "Your debugger doesn't work with me!", 25, 100)

const addFlyingAbility = obj => {
    obj.fly = () => console.log(`Now ${obj.name} can fly!`)
}

addFlyingAbility(bug1)
bug1.fly() // output: "Now Buggy can fly!"

و به این ترتیب می‌توانیم برای هر قدرت یا توانایی که می‌خواهیم کاراکترهای موردنظرمان داشته باشند، توابع مشابهی تعریف کنیم.

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

جمع‌بندی

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

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