OOP - آیا جاوااسکریپت نوع اینترفیس دارد (مثل 'interface' در جاوا)؟
من دارم یاد میگیرم که چگونه برنامهنویسی شیءگرا با جاوااسکریپت انجام دهم. آیا جاوااسکریپت مفهوم اینترفیس را دارد (مانند interface در جاوا)؟
پس من قادر خواهم بود که یک شنونده ایجاد کنم...
پاسخ شماره ۱
هیچ ایدهای از «این کلاس باید این توابع را داشته باشد» وجود ندارد (یعنی، خودِ رابطها وجود ندارد)، زیرا:
- وراثت در جاوااسکریپت بر اساس اشیاء است، نه کلاسها. تا زمانی که متوجه شوید:
- جاوااسکریپت یک زبان بسیار دارای نوع داینامیک است -- شما میتوانید یک شیء با متدهای مناسب ایجاد کنید که آن را با رابط وفق دهد، و سپس تمام چیزهایی که باعث تطابق آن شده بودند را حذف کنید. دور زدن سیستم نوع آن خیلی آسان است -- حتی به صورت تصادفی! -- به طوری که اصلا ارزش تلاش برای ایجاد یک سیستم نوع را ندارد.
در عوض، جاوااسکریپت از چیزی استفاده میکند که به آن نوعگذاری اردکی گفته میشود. (اگر مثل یک اردک راه برود و مثل یک اردک صدا کند، تا جایی که جاوااسکریپت اهمیت میدهد، یک اردک است.) اگر شیء شما متدهای quack()، walk() و fly() داشته باشد، کد میتواند از آن هر جا که انتظار دارد شیءای باشد که بتواند راه برود، صدای اردک دربیاورد و پرواز کند استفاده کند، بدون نیاز به پیادهسازی رابطی به نام «Duckable». رابط دقیقاً مجموعهای از توابعی است که کد از آنها استفاده میکند (و مقادیر بازگشتی آن توابع)، و با نوعگذاری اردکی، شما این را بهصورت رایگان دریافت میکنید.
حالا، این بدین معنا نیست که کد شما وسط کار خراب نمیشود، اگر سعی کنید some_dog.quack() را فراخوانی کنید؛ یک TypeError دریافت خواهید کرد. صادقانه بگویم، اگر به سگها میگویید قَدَق کنند، شما مشکلاتی کمی بزرگتر دارید؛ duck typing بهترین کارایی را دارد وقتی که همه چیز مرتب و منظم باشد، به اصطلاح همه اردکهایتان را در ردیف خود نگه دارید و اجازه ندهید سگها و اردکها با هم مخلوط شوند مگر اینکه آنها را به عنوان حیوانات عمومی در نظر بگیرید. به عبارت دیگر، حتی اگر رابط کاربری انعطافپذیر باشد، هنوز هم وجود دارد؛ اغلب خطا است که سگی را به کدی بدهیم که انتظار دارد آن قَق و پرواز کند.
اما اگر مطمئن هستید که کار درست را انجام میدهید، میتوانید مسئلهی سگ صداکننده را با بررسی وجود یک متد خاص قبل از تلاش برای استفاده از آن دور بزنید. چیزی شبیه
if (typeof(someObject.quack) == "function")
{
// This thing can quack
}
بنابراین میتوانید قبل از استفاده از آنها، تمام روشهایی که میتوانید استفاده کنید را بررسی کنید. هرچند نحو آن کمی زشت است. یک روش کمی زیباتر وجود دارد:
Object.prototype.can = function(methodName)
{
return ((typeof this[methodName]) == "function");
};
if (someObject.can("quack"))
{
someObject.quack();
}
این جاوااسکریپت استاندارد است، بنابراین باید در هر مفسر JS قابل استفاده کار کند. همچنین مزیت اضافی آن این است که مانند زبان انگلیسی قابل خواندن است.
برای مرورگرهای مدرن (یعنی، تقریباً هر مرورگری به جز IE 6-8)، حتی راهی وجود دارد تا از نمایش این ویژگی در for...in جلوگیری شود:
Object.defineProperty(Object.prototype, 'can', {
enumerable: false,
value: function(method) {
return (typeof this[method] === 'function');
}
}
مشکل این است که اشیاء IE7 اصلاً .defineProperty را ندارند، و در IE8، ظاهراً فقط روی اشیاء میزبان (یعنی عناصر DOM و غیره) کار میکند. اگر سازگاری مهم است، نمیتوانید از .defineProperty استفاده کنید. (حتی به IE6 اشاره هم نمیکنم، زیرا خارج از چین دیگر چندان مرتبط نیست.)
یک مسئله دیگر این است که برخی سبکهای کدنویسی دوست دارند فرض کنند که همه کدهای بد مینویسند و از اصلاح Object.prototype جلوگیری میکنند در صورتی که کسی بخواهد بدون فکر از for...in استفاده کند. اگر به این موضوع اهمیت میدهید، یا در حال استفاده از کدی هستید که (به نظر من شکسته) چنین کاری میکند، نسخه کمی متفاوتی را امتحان کنید:
function can(obj, methodName)
{
return ((typeof obj[methodName]) == "function");
}
if (can(someObject, "quack"))
{
someObject.quack();
}
پاسخ شماره ۲
یک نسخه از «الگوهای طراحی جاوااسکریپت» اثر داستین دیاز بردارید. چند فصل به پیادهسازی رابطهای جاوااسکریپت از طریق Duck Typing اختصاص دارد. همچنین مطالعهی خوبی است. اما نه، هیچ پیادهسازی بومی زبانی برای یک رابط وجود ندارد، شما باید از Duck Typing استفاده کنید.
// example duck typing method
var hasMethods = function(obj /*, method list as strings */){
var i = 1, methodName;
while((methodName = arguments[i++])){
if(typeof obj[methodName] != 'function') {
return false;
}
}
return true;
}
// in your code
if(hasMethods(obj, 'quak', 'flapWings','waggle')) {
// IT'S A DUCK, do your duck thang
}
پاسخ شماره ۳
جاوااسکریپت (نسخه ECMAScript 3) دارای یک کلمه رزرو شده implements است که برای استفاده در آینده نگه داشته شده است. فکر میکنم این دقیقاً برای همین منظور در نظر گرفته شده باشد، با این حال، در عجله برای انتشار مشخصات، زمانی برای تعریف نحوه استفاده از آن نداشتند، بنابراین در زمان حاضر، مرورگرها هیچ کاری انجام نمیدهند جز این که بگذارند همانجا بماند و گهگاهی اگر سعی کنید از آن برای چیزی استفاده کنید، شکایت کنند.
این امکان وجود دارد و در واقع به اندازه کافی آسان است که متد Object.implement(Interface) خود را با منطقی ایجاد کنید که هر زمان مجموعه خاصی از ویژگیها/توابع در یک شیء داده شده پیادهسازی نشده باشند، از ادامه کار خودداری کند.
من مقالهای دربارهٔ برنامهنویسی شیگرا نوشتم که از نشانهگذاری خودم به شرح زیر استفاده میکنم:
// Create a 'Dog' class that inherits from 'Animal'
// and implements the 'Mammal' interface
var Dog = Object.extend(Animal, {
constructor: function(name) {
Dog.superClass.call(this, name);
},
bark: function() {
alert('woof');
}
}).implement(Mammal);
راههای زیادی برای مواجهه با این مسئله خاص وجود دارد، اما این منطقی است که من برای پیادهسازی رابط خودم استفاده کردم. من متوجه شدم که این روش را ترجیح میدهم و خواندن و استفاده از آن آسان است (همانطور که در بالا میبینید). این به معنای اضافه کردن یک متد 'implement' به Function.prototype است که ممکن است برخی افراد با آن مشکل داشته باشند، اما من میبینم که بهطور عالی کار میکند.
Function.prototype.implement = function() {
// Loop through each interface passed in and then check
// that its members are implemented in the context object (this).
for(var i = 0; i < arguments.length; i++) {
// .. Check member's logic ..
}
// Remember to return the class being tested
return this;
}
پاسخ شماره ۴
رابطهای جاوااسکریپت:
اگرچه جاوااسکریپت نوع interface را ندارد، اما اغلب اوقات به آن نیاز است. به دلایلی مرتبط با ماهیت پویا جاوااسکریپت و استفاده از ارثبری نمونهای (Prototypical-Inheritance)، تضمین رابطهای سازگار در کلاسها دشوار است - با این حال، امکانپذیر است؛ و اغلب شبیهسازی میشود.
در این مرحله، روشهای محدودی برای شبیهسازی اینترفیسها در جاوااسکریپت وجود دارد؛ تنوع در رویکردها معمولاً برخی نیازها را برآورده میکند، در حالی که برخی دیگر بیپاسخ میمانند. اغلب، محکمترین روش بیش از حد دست و پا گیر است و اجرای آن توسط توسعهدهنده را دچار مشکل میکند.
در اینجا یک رویکرد برای رابطها/کلاسهای انتزاعی ارائه شده است که بسیار دست و پا گیر نیست، توضیحی است، حداقل پیادهسازیها را در داخل انتزاعات نگه میدارد و فضای کافی برای روشهای پویا یا سفارشی باقی میگذارد:
function resolvePrecept(interfaceName) {
var interfaceName = interfaceName;
return function curry(value) {
/* throw new Error(interfaceName + ' requires an implementation for ...'); */
console.warn('%s requires an implementation for ...', interfaceName);
return value;
};
}
var iAbstractClass = function AbstractClass() {
var defaultTo = resolvePrecept('iAbstractClass');
this.datum1 = this.datum1 || defaultTo(new Number());
this.datum2 = this.datum2 || defaultTo(new String());
this.method1 = this.method1 || defaultTo(new Function('return new Boolean();'));
this.method2 = this.method2 || defaultTo(new Function('return new Object();'));
};
var ConcreteImplementation = function ConcreteImplementation() {
this.datum1 = 1;
this.datum2 = 'str';
this.method1 = function method1() {
return true;
};
this.method2 = function method2() {
return {};
};
//Applies Interface (Implement iAbstractClass Interface)
iAbstractClass.apply(this); // .call / .apply after precept definitions
};
شرکتکنندگان
حلکننده اصول
تابع resolvePrecept یک تابع کمکی و ابزار کاربردی برای استفاده در داخل کلاس انتزاعی شما است. وظیفه آن اجازه دادن به پیادهسازی سفارشی برای مدیریت پیشفرضها (دادهها و رفتار) محصور شده است. این تابع میتواند خطا ایجاد کند یا هشدار دهد -- و -- یک مقدار پیشفرض به کلاس پیادهکننده اختصاص دهد.
کلاسانتزاعی
کلاس iAbstractClass اینترفیس قابل استفاده را تعریف میکند. رویکرد آن مستلزم یک توافق ضمنی با کلاس Implementor خود است. این اینترفیس هر precept را به همان فضای نام precept دقیق اختصاص میدهد -- یا -- به هر چیزی که تابع Precept Resolver بازمیگرداند. با این حال، توافق ضمنی به یک context حل میشود -- یک فراهمی از Implementor.
مجری
پیادهکننده به سادگی با یک اینترفیس (iAbstractClass در اینجا) 'موافقت' میکند و آن را با استفاده از Constructor-Hijacking اعمال میکند: iAbstractClass.apply(this). با تعریف دادهها و رفتار بالا، و سپس تصاحب کردن کانستراکتور اینترفیس -- انتقال زمینه پیادهکننده به کانستراکتور اینترفیس -- ما میتوانیم اطمینان حاصل کنیم که بازنویسیهای پیادهکننده اضافه خواهند شد و اینترفیس هشدارها و مقادیر پیشفرض را بهطور صریح ارائه خواهد داد.
این یک روش بسیار غیرسنگین است که برای من و تیمم در طول زمان و پروژههای مختلف بسیار مفید بوده است. با این حال، این روش دارای برخی معایب و کاستیها نیز میباشد.
معایب
اگرچه این به اجرای انسجام در سرتاسر نرمافزار شما به میزان قابل توجهی کمک میکند، رابطهای واقعی را اجرا نمیکند -- بلکه آنها را شبیهسازی میکند. اگرچه تعاریف، پیشفرضها و هشدارها یا خطاها مشخص میشوند، توضیح استفاده توسط توسعهدهنده اجرا و تأیید میشود (همانطور که در بسیاری از توسعههای جاوااسکریپت مشاهده میشود).
این به نظر میرسد بهترین روش برای "رابطها در جاوااسکریپت" باشد، با این حال، دوست دارم موارد زیر حل شوند:
- ادعاهای نوع بازگشتی
- اظهارات امضاها
- مسدود کردن اشیاء از اقدامات
حذف - ادعاهای هر چیز دیگر رایج یا مورد نیاز در خصوصیت جامعه جاوااسکریپت
با این اوصاف، امیدوارم این به شما به همان اندازه که به من و تیمم کمک کرده است کمک کند.
پاسخ شماره ۵
رابط انتزاعی مانند این
const MyInterface = {
serialize: () => {throw "must implement serialize for MyInterface types"},
print: function() { console.log(this.serialize()) }
}
یک نمونه ایجاد کنید:
function MyType() {
this.serialize = () => "serialized "
}
MyType.prototype = MyInterface
و از آن استفاده کن
let x = new MyType()
x.print()
پاسخ شماره ۶
امیدوارم که هر کسی که هنوز به دنبال پاسخ است، این را مفید بیابد.
شما میتوانید استفاده از یک پروکسی را امتحان کنید (این از زمان ECMAScript 2015 استاندارد است): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
latLngLiteral = new Proxy({},{
set: function(obj, prop, val) {
//only these two properties can be set
if(['lng','lat'].indexOf(prop) == -1) {
throw new ReferenceError('Key must be "lat" or "lng"!');
}
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(prop == 'lat' && !(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
//longitude is in range between 0 and 180
else if(prop == 'lng' && !(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
obj[prop] = val;
return true;
}
});
سپس میتوانید بهراحتی بگویید:
myMap = {}
myMap.position = latLngLiteral;
اگر میخواهید از طریق instanceof بررسی کنید (پرسیده شده توسط @Kamaffeather)، میتوانید آن را در یک شیء به این صورت بپیچید:
class LatLngLiteral {
constructor(props)
{
this.proxy = new Proxy(this, {
set: function(obj, prop, val) {
//only these two properties can be set
if(['lng','lat'].indexOf(prop) == -1) {
throw new ReferenceError('Key must be "lat" or "lng"!');
}
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(prop == 'lat' && !(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
//longitude is in range between 0 and 180
else if(prop == 'lng' && !(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
obj[prop] = val;
return true;
}
})
return this.proxy
}
}
این کار میتواند بدون استفاده از Proxy انجام شود، بلکه با استفاده از getter و setter های کلاسها:
class LatLngLiteral {
#latitude;
#longitude;
get lat()
{
return this.#latitude;
}
get lng()
{
return this.#longitude;
}
set lat(val)
{
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//latitude is in range between 0 and 90
if(!(0 < val && val < 90)) {
throw new RangeError('Position is out of range!');
}
this.#latitude = val
}
set lng(val)
{
//the dec format only accepts numbers
if(typeof val !== 'number') {
throw new TypeError('Value must be numeric');
}
//longitude is in range between 0 and 180
if(!(0 < val && val < 180)) {
throw new RangeError('Position is out of range!');
}
this.#longitude = val
}
}
