تفاوت پارادایم های برنامه نویسی شیءگرا، تابع‌گرا و رویه ای (قسمت دوم: شیء گرا)

تفاوت پارادایم های برنامه نویسی شیءگرا، تابع‌گرا و رویه ای (قسمت اول)

تفاوت پارادایم های برنامه نویسی شیگرا، تابع‌گرا و رویه ای (قسمت دوم: شی گرا)

در جلسه ی قبل پروژه ی ساده ی خود را به صورت رویه‌ای (procedural) نوشتیم و حالا می خواهیم این کد را به صورت شی گرا بنویسیم. برای این کار یک فایل جدید به نام oop.js می سازم و دستور script درون فایل HTML را نیز تغییر می دهم:

    <script src="oop.js" defer></script>

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

نمونه ای از ساختار برنامه نویسی شیء گرا
نمونه ای از ساختار برنامه نویسی شی گرا

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

  const user = {
    userName: enteredUsername,
    password: enteredPassword
  };

اولین کلاسی که باید تعریف کنیم، مربوط به input های فرم است. من نامش را UserInputForm می گذارم. همچنین کلاس دیگری به نام User نیاز داریم تا کاربران را ایجاد کند. نهایتا کلاس Validator را می خواهیم که اعتبارسنجی ورودی کاربر را بر عهده دارد:

class Validator { }

class User { }

class UserInputForm { }

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

هشدار: برای تعریف خصوصیت های مختلف درون کلاس های جاوا اسکریپت (نسخه ES6) اجازه ی تعریف آن ها را به صورت زیر نداریم:

class User {
    name;
}

متاسفانه جاوا اسکریپت ES6 مانند دیگر زبان های برنامه نویسی از این حالت استفاده نمی کند و اگر بخواهیم خصوصیات را تعریف کنیم باید آن ها را درون constructor و با کلیدواژه ی this به کار ببریم:

class UserInputForm {
    constructor() {
        this.form = document.getElementById('user-input');
        this.userNameInput = document.getElementById('username');
        this.passwordInput = document.getElementById('password');

        this.form.addEventListener('submit', this.signupHandler);
    }
}

در constructor بالا هر دو فیلد یوزرنیم و پسورد را به همراه خود فرم دریافت کرده ام و همانطور که گفتم حالا form و userNameInput و passwordInput از خصوصیات این کلاس هستند. احتمالا متوجه eventListener خط آخر شده اید. شما می توانید به جای روش بالا از یک تابع anonymous عادی استفاده کنید اما من می خواهم کدهایم تا حد ممکن شی گرا باشد بنابراین به جای تابعی عادی از یک متد متعلق به این کلاس استفاده می کنم.

بگذارید متد را تعریف کنیم:

class UserInputForm {
    constructor() {
        this.form = document.getElementById('user-input');
        this.userNameInput = document.getElementById('username');
        this.passwordInput = document.getElementById('password');

        this.form.addEventListener('submit', this.signupHandler);
    }

    signupHandler(event) {
        event.preventDefault();
        
    }
}

در حال حاضر کد ما مشکل کوچکی دارد. اگر بخواهیم درون signupHandler مقدار وارد شده توسط کاربر را دریافت کنیم باید بگوییم:

    signupHandler(event) {
        event.preventDefault();
        const enteredUserName = this.userNameInput.value;
        const enteredPassword = this.passwordInput.value;
    }

این کد اجرا نخواهد شد. چرا؟ به دلیل اینکه کلیدواژه ی this در این متد به کلاس ما اشاره نمی کند. زمانی که در حال اشاره به یک متد یا تابع هستید که بر اساس یک event کار می کند، کلیدواژه ی this به هدف آن event اشاره خواهد کرد. برای حل این مشکل باید از bind استفاده کنیم:

    constructor() {
        this.form = document.getElementById('user-input');
        this.userNameInput = document.getElementById('username');
        this.passwordInput = document.getElementById('password');

        this.form.addEventListener('submit', this.signupHandler.bind(this));
    }

با صدا زدن bind و پاس دادن this به آن مطمئن می شویم که کلیدواژه ی this درون متد signupHandler حتما مانند this درون constructor عمل می کند، یعنی همان نمونه ی ساخته شده بر اساس کلاس. حالا کار این کلاس تمام شده است.

من می خواهم کدهایم تا حد ممکن شی گرا باشد بنابراین به جای کپی کردن شرط های if از قسمت قبلی، یک کلاس دیگر به نام validator ساخته ایم و حالا می خواهیم آن را تکمیل کنیم. در کلاس validator یک متد استاتیک تعریف کنید:

class Validator {
    static validate(value, flag, validatorValue) {

    }
}

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

  • value: همان مقدار وارد شده توسط کاربر است.
  • flag: نوع اعتبار سنجی را مشخص می کند.
  • validatorValue: یک گزینه ی غیر الزامی است که به ما اجازه می دهد اعتبارسنجی را تنظیم کنیم.

شاید این مفاهیم در ابتدا گنگ باشند اما در عمل متوجه آن ها می شوید. من دو خصوصیت استاتیک هم به این کلاس اضافه می کنم تا بتوانیم از متدی که تعریف کرده ایم استفاده کنیم (بعدا متوجه می شوید چرا):

class Validator {
    static REQUIRED = 'REQUIRED';
    static MIN_LENGTH = 'MIN_LENGTH';

    static validate(value, flag, validatorValue) {

    }
}

منطق من این است که می خواهم متد validate بر اساس این دو خصوصیت استاتیک که flag های ما هستند، کار خاصی انجام دهد. من می گویم اگر flag ما برابر با required بود (یعنی input ما اجباری بود و کاربر حتما باید آن را پُر کند) مقدار true برگردانده شود:

class Validator {
    static REQUIRED = 'REQUIRED';
    static MIN_LENGTH = 'MIN_LENGTH';

    static validate(value, flag, validatorValue) {
        if (flag === this.REQUIRED) {
            return value.trim().length > 0;
        }
    }
}

در کد بالا اگر طول value.trim بیشتر از صفر باشد (کاربر فیلد را خالی نگذاشته باشد) مقدار True را برمی گردانیم اما اگر این شرط صحیح نباشد مقدار false برگردانده خواهد شد. سپس همین عملیات مشابه را با اندکی تفاوت برای MIN_LENGTH انجام می دهیم:

class Validator {
    static REQUIRED = 'REQUIRED';
    static MIN_LENGTH = 'MIN_LENGTH';

    static validate(value, flag, validatorValue) {
        if (flag === this.REQUIRED) {
            return value.trim().length > 0;
        }
        if (flag === this.MIN_LENGTH) {
            return value.trim().length > validatorValue;
        }
    }
}

یعنی اگر flag ما برابر با MIN_LENGTH بود باید بر اساس شرط داخل (بیشتر بودن طول کاراکترهای وارد شده توسط کاربر از validatorValue) مقدار true یا false برگردانده شود. منظور من از تنظیم اعتبار سنجی همین بود. ما خودمان در هنگام اعتبار سنجی می توانیم مقدار ValidatorValue را مشخص کنیم. حالا این کلاس نیز تکمیل شده است و می توانیم از این کدها در متد signupHandler در کلاس UserInputForm استفاده کنیم:

    signupHandler(event) {
        event.preventDefault();
        const enteredUserName = this.userNameInput.value;
        const enteredPassword = this.passwordInput.value;

        if (
            !Validator.validate(enteredUserName, Validator.REQUIRED) ||
            !Validator.validate(enteredPassword, Validator.MIN_LENGTH, 5)
        ) {
            alert(
                'Invalid input - username or password is wrong (password should be at least six characters).'
            );
            return;
        }
    }

در اینجا یک شرط if ساده را اضافه کرده ام (خود شرط را در دو خط نوشته ام که خوانا تر شود و کار خاصی انجام نداده ام). از آنجایی که متد validate یک متد استاتیک بود، می توانیم بدون نمونه سازی از کلاس Validator و به صورت بالا از آن استفاده کنیم. ما در شرط خود گفته ایم اگر متد validate برای enteredUserName یا enteredPassword مقدار false را برگرداند (به اپراتور! توجه کنید که شرط را برعکس می کند) یعنی داده های ورودی ما معتبر نیست بنابراین با یک alert ساده به کاربر می گوییم که یکی از فیلدهای یوزرنیم یا پسورد را اشتباه نوشته است. توجه داشته باشید که ما از flag های REQUIRED و MIN_LENGTH برای این کار استفاده کرده ایم و حداقل کاراکتر برای پسورد را 6 کاراکتر گذاشته ایم (بزرگ تر از 5 یعنی 6 و بالاتر). دستور return در انتها نیز برای این است که ادامه ی متد signupHandler اجرا نشده و از آن خارج شویم (می خواهیم در ادامه یک کاربر ایجاد کنیم).

مرحله ی آخر کار ما تعریف کلاس User است. من این کلاس را ساده می نویسم، یک constructor که خصوصیات username و password دریافتی را تعریف کند و یک متد ساده که این موارد را console.log کند:

class User {
  constructor(uName, uPassword) {
    this.userName = uName;
    this.password = uPassword;
  }

  greet() {
    console.log('Hi, I am ' + this.userName);
  }
}

حالا به متد signupHandler بروید و یک کاربر جدید بسازید:

    signupHandler(event) {
        event.preventDefault();
        const enteredUserName = this.userNameInput.value;
        const enteredPassword = this.passwordInput.value;

        if (
            !Validator.validate(enteredUserName, Validator.REQUIRED) ||
            !Validator.validate(enteredPassword, Validator.MIN_LENGTH, 5)
        ) {
            alert(
                'Invalid input - username or password is wrong (password should be at least six characters).'
            );
            return;
        }

        const newUser = new User(enteredUserName, enteredPassword);
        console.log(newUser);
        newUser.greet();
    }

بالاتر constructor کلاس User را طوری تعریف کردیم که دو پارامتر ورودی می گرفت بنابراین در هنگام نمونه سازی از این کلاس، باید این دو مقدار را پاس بدهیم. من کاربر جدیدی را که ساختیم، console.log می کنم و در نهایت متد greet را صدا می زنم. در حال حاضر تمام منطق کاری را درون UserInputForm است که یک کلاس است و تا زمانی که نمونه ای از آن ساخته نشود، اجرا نخواهد شد بنابراین باید یک نمونه از این کلاس را نیز بسازیم:

new UserInputForm();

یادتان باشد که کد بالا را در انتهای فایل و خارج از تمام کلاس ها قرار بدهید. حالا به مرورگر بروید و کدها را تست کنید. اگر ورودی هایتان خطا نداشته باشد باید نتیجه ی زیر را در کنسول مرورگر ببینید:

نتیجه ی موفقیت آمیز از کد های ما
نتیجه ی موفقیت آمیز از کدهای ما

(دانلود سورس کد این جلسه)

نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.