24.04.2023 10 min read

توسيع الأصناف الأساسية في JavaScript وTypeScript وC# .NET

بواسطة Mirza Leka

تعلّم كيفية إضافة دوال وخصائص مخصصة إلى أصناف JavaScript/TypeScript المدمجة باستخدام الوراثة القائمة على Prototype والأصناف، وطرق الامتداد في C#.

Extending Core Classes

سيعلّمك هذا المقال كيفية إضافة دوال وخصائص مخصصة إلى أصناف JavaScript/TypeScript المدمجة باستخدام الوراثة القائمة على Prototype والأصناف، وطرق الامتداد (Extension Methods) في C#.

سننشئ ثلاث دوال مخصصة يمكن تطبيقها على أي نوع من المجموعات، سواء مصفوفة أو Set أو Map:

  • isEmpty — تفحص طول/حجم المجموعة وتُعيد true أو false إذا كانت المجموعة فارغة أم لا
  • insert — تدفع عنصرًا جديدًا إلى مجموعة وتُعيد المجموعة الحالية
  • tap — تأخذ دالة استدعاء (callback) (من console.log إلى دالة مخصصة) يمكنها فعل أي شيء تقريبًا وتُعيد المجموعة الحالية

سيعمل هذا بشكل مشابه لـ طرق الامتداد (Extension Methods) في C#. سيتمكن أي مستهلِك للمجموعة من استخدام هذه الدوال إلى جانب الدوال الافتراضية.

عيب التنفيذ الأصلي هو أن أنواع المجموعات المختلفة تستخدم دوالًا مختلفة للتفاعل معها. الأمور التي نهتم بها هي كالتالي:

  • دفع عنصر جديد إلى المجموعة (المصفوفة: ()push، الـ set: ()add، الـ map: ()set)
  • فحص حجم المجموعة (المصفوفة: length.، الـ set والـ map: size.)

لذا بدلًا من الحاجة لتذكّر الخاصية التي تنطبق على أي مجموعة، سننشئ دوالًا موحّدة مخصصة تعمل مع كليهما.

تفصيل مهم آخر هو نوع القيمة المُعادة. إذا أعادت دالتنا المخصصة مجموعة موجودة، فهذا يعني أننا نستطيع إلحاق دوال أخرى من تلك المجموعة إليها:

const numbers = [1, 2, 3, 4, 5];

numbers
  .map(number => number * 5)
  .tap()
  // array continues
  .filter(...)

لكن إذا أعادت الدالة قيمة مختلفة، لن نتمكن بعد ذلك من إلحاق دالة مجموعة إليها.

const numbers = [1, 2, 3, 4, 5];

numbers
  .filter(number => number > 100)
  .isEmpty()
  // true or false

هذا سهل التنفيذ جدًا، وسنرى كيف يبدو مع نماذج مختلفة، مثل الوراثة القائمة على Prototype والأصناف، وبلغات برمجة مختلفة.

JavaScript

JavaScript

الوراثة القائمة على Prototype

الوراثة القائمة على Prototype هي آلية للبرمجة الكائنية التوجه في JavaScript تتيح للكائنات أن ترث الخصائص والدوال من كائنات أخرى. في JavaScript، يحتوي كل كائن على خاصية داخلية تُسمى prototype الخاص به، وهي مرجع لكائن آخر يعمل كنموذجه الأولي.

الـ prototype أشبه بشجرة حيث توجد جميع الخصائص والدوال على كائن معين. على سبيل المثال، إذا أنشأنا مصفوفة، يمكننا توسيع خاصية prototype لرؤية جميع الدوال والخصائص المتاحة على المصفوفات.

Array prototype preview

لإضافة دالة جديدة إلى هذا الـ prototype سنفعل شيئًا كهذا:

Class.prototype.nameOfTheMethod = function() {
    // do something
}

الشيء المهم الذي يجب ملاحظته هو أننا نستخدم عبارة دالة (function statement) بدلًا من دالة سهمية (arrow function). لماذا؟ - لأننا سنستخدم الكلمة المفتاحية this داخل الدالة وستشير this إلى مستهلِك الدالة.

Array.prototype.nameOfTheMethod = function() {
    // do something
    console.log(this) // array
}

سنستخدم نفس الصيغة أعلاه لتوسيع كل نوع من المجموعات (مصفوفة وset وmap) بكل من دوالنا المخصصة (isEmpty وinsert وtap).

# isEmpty

ستتحقق هذه الدالة من طول/حجم المجموعة، وتخبرنا ما إذا كانت فارغة أم لا.

Array.prototype.isEmpty = function () {
    return this.length === 0;
};
  
Set.prototype.isEmpty = function () {
    return this.size === 0;
};
  
Map.prototype.isEmpty = function () {
    return this.size === 0;
};

# insert

ستدفع هذه الدالة عنصرًا جديدًا إلى مجموعة.

Array.prototype.insert = function (value) {
  this.push(value);
  return this;
};
  
Set.prototype.insert = function (value) {
  this.add(value);
  return this;
};
  
Map.prototype.insert = function (key, value) {
  this.set(key, value);
  return this;
};

هذا هو الجزء الذي قلت فيه إننا سنحصل على حل موحّد. لأنه كما نرى:

  • تستخدم المصفوفات ()push لإلحاق عنصر جديد
  • تستخدم الـ sets ()add لإلحاق عنصر جديد
  • تستخدم الـ maps ()set لإلحاق عنصر جديد

والآن يمكن للثلاثة استخدام ()insert لفعل نفس الشيء لكل منها.

# tap

ستمر دالة tap عبر كل عنصر داخل مصفوفة وتستدعي دالة الاستدعاء لدينا. يمكن أن تكون دالة الاستدعاء هذه دالة إرسال إجراء (action dispatcher)، أو console.log/warn/error، أو أي دالة مخصصة.

Array.prototype.tap = function (callback) {
    this.forEach(item => callback(item))
    return this;
};
  
Set.prototype.tap = function (callback) {
    this.forEach(item => callback(item))
    return this;
};
  
Map.prototype.tap = function (callback) {
    for (const [key, value] of this.entries()) {
      callback(key, value);
    }
    return this;
};
  
// this.entries() method returns an iterable of 
// key, value pairs for every entry in the Map.

حالة استخدام:

const numbers = [1, 2, 3, 4, 5];

numbers
  .tap((number) => {
    // some logic
  })

الآن لنختبر كل دالة. بما أننا وسّعنا الـ prototype الأصلي، يمكننا استدعاء دوالنا الجديدة مباشرة على النسخ (instances).

const myArr = [];
console.log(myArr.isEmpty()); // True

myArr.insert('Hello');
myArr.tap(console.log); // Hello
console.log(myArr.isEmpty()); // False

//

const mySet = new Set();
console.log(mySet.isEmpty()); // True

mySet.insert('Hello');
mySet.tap(console.log); // Hello
console.log(mySet.isEmpty()); // False

//

const myMap = new Map();
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World');
myMap.tap(console.log); // Hello World
console.log(myMap.isEmpty()); // False

الوراثة القائمة على الأصناف (Class-based)

الفكرة الأساسية ستبدو هكذا. سيوسّع صنفنا المخصص الصنف الأساسي (Super).

class SubClass extends SuperClass {
    constructor() {
      super();
    }
    method1() {
      this.doSomething // where this the instance of the class
    }
    method2() {
      //
    }
}

عند توسيع مثل هذه الأصناف، من المهم أيضًا استدعاء ()super داخل باني الصنف (constructor) لصنف مشتق (Sub). هذا ضروري لأن الصنف المشتق يرث جميع خصائص ودوال صنفه الأب، وقد يحتوي الصنف الأب على منطق باني صنف خاص به يحتاج للتنفيذ إضافة إلى منطق باني الصنف المشتق.

سنكرر هذه العملية لكل من أصنافنا المستهدفة:

  • Array (ExtendedArray)
  • Set (ExtendedSet)
  • Map (ExtendedMap)

# Extended Array

class ExtendedArray extends Array {
    constructor() {
      super();
    }
    isEmpty() {
      return this.length === 0;
    }
    insert(value) {
      this.push(value);
      return this;
    }
    tap(callback) {
      this.forEach(item => callback(item))
      return this;
    }
}

# Extended Set

class ExtendedSet extends Set {
    constructor() {
      super();
    }
    isEmpty() {
      return this.size === 0;
    }
    insert(value) {
      this.add(value);
      return this;
    }
    tap(callback) {
      this.forEach(item => callback(item))
      return this;
    }
}

# Extended Map

class ExtendedMap extends Map {
    constructor() {
      super();
    }
    isEmpty() {
      return this.size === 0;
    }
    insert(key, value) {
      this.set(key, value);
      return this;
    }
    tap(callback) {
      for (const [key, value] of this.entries()) {
        callback(key, value);
      }
      return this;
    }
}

لاختبار هذا، سنحتاج لإنشاء نسخة جديدة من كل صنف من أصنافنا المخصصة بدلًا من العمل مع الأصناف الأساسية (كما فعلنا سابقًا).

سندفع عنصرًا جديدًا إلى مجموعة باستخدام دالة insert لدينا، ونفحص المجموعة باستخدام دالة tap، ثم نتحقق مما إذا كانت المجموعة فارغة باستخدام دالة isEmpty.

const myArr = new ExtendedArray();
console.log(myArr.isEmpty()); // True

myArr.insert('Hello')
myArr.tap(console.log); // Hello
console.log(myArr.isEmpty()); // False

//

const mySet = new ExtendedSet();
console.log(mySet.isEmpty()); // True

mySet.insert('Hello')
mySet.tap(console.log); // Hello
console.log(mySet.isEmpty()); // False

//

const myMap = new ExtendedMap();
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World')
myMap.tap(console.log); // Hello World
console.log(myMap.isEmpty()); // False

ونجح الأمر. الآن يمكننا استخدام هذه الدوال المُنشَأة حديثًا كلما عملنا مع هذه الأنواع من المجموعات. لنتعلّم كيفية تطبيق هذا في TypeScript أيضًا.

TypeScript

TypeScript

على جانب TypeScript، ستكون الأمور مشابهة إلى حد كبير لتلك الموجودة في JavaScript مع بعض الخطوات الإضافية.

الوراثة القائمة على الأصناف

لتطبيق منطق مشابه قائم على الأصناف في عالم TypeScript، نحتاج لإضافة أنواع عامة (generics) إلى أصناف TypeScript.

# Extended Array

class ExtendedArray<T> extends Array<T> { 
    constructor() {  // where T is type of data included in an array,
      super(); // e.g. string, number, custom class, etc.
    }
    isEmpty() {
      return this.length === 0;
    }
    insert(value: T) {
      this.push(value);
      return this;
    }
    tap(callback: (item: T) => void) {
      this.forEach(item => callback(item))
      return this;
    }
}

# Extended Set

class ExtendedSet<T> extends Set<T> { 
    constructor() {
      super();
    }
    isEmpty() {
      return this.size === 0;
    }
    insert(value: T) {
      this.add(value);
      return this;
    }
    tap(callback: (item: T) => void) {
      this.forEach(item => callback(item))
      return this;
    }
}

# Extended Map

class ExtendedMap<K, V> extends Map<K, V> {
    constructor() { // where K, V are generic types used when inserting data
      super(); // e.g. (string, string), (number, string), etc.
    }
    isEmpty() {
      return this.size === 0;
    }
    insert(key: K, value: V) {
      this.set(key, value)
      return this;
    }
    tap(callback: (key: K, value: V) => void) {
      for (const [key, value] of this.entries()) {
        callback(key, value);
      }
      return this;
    }
}

للتأكد من أن هذا يعمل مرة أخرى، سنختبره بنفس الطريقة.

const myArr = new ExtendedArray<string>(); // where T is string
console.log(myArr.isEmpty()); // True

myArr.insert('Hello')
myArr.tap(console.log); // Hello
console.log(myArr.isEmpty()); // False

//

const mySet = new ExtendedSet<string>(); // where T is string
console.log(mySet.isEmpty()); // True

mySet.insert('Hello')
mySet.tap(console.log); // Hello
console.log(mySet.isEmpty()); // False

//

const myMap = new ExtendedMap<string, string>(); // where K,V are strings
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World')
myMap.tap(console.log); // Hello World
console.log(myMap.isEmpty()); // False

الوراثة القائمة على Prototype في TypeScript

لكي تعمل الـ Prototypes مع TypeScript، نحتاج لإضافة وظائف إضافية. إليك السبب.

إذا حاولنا توسيع prototype الخاص بـ Array بنفس الطريقة التي فعلناها في JavaScript، سيشتكي مترجم TypeScript من أن دالة isEmpty غير موجودة على نوع المصفوفة.

Array.prototype.isEmpty = function () {
    return this.length === 0;
};
  
// Property 'isEmpty' does not exist on type 'any[]'.ts(2339)

للتغلب على هذه المشكلة، نحتاج لإنشاء ملف تصريح نوع (d.ts) حيث سنحدد قواعد للأنواع التي سنستخدمها. أساسًا، سننشئ دالة isEmpty لكل نوع. تعرّف على المزيد حول ملفات تصريح الأنواع.

// index.d.ts file
export {};

declare global {
  interface Array<T> { // where T is once again type included in an array
    isEmpty(): boolean;
    insert(value: T): this
    tap(callback: (item: T) => void): this
  // callback type is function that takes argument and returns void
  }
}

declare global {
  interface Set<T> {
    isEmpty(): boolean;
    insert(value: T): this
  // returning 'this' means we can append methods after it
    tap(callback: (item: T) => void): this
  }
}

declare global {
  interface Map<K, V> {
    isEmpty(): boolean;
    insert(key: K, value: V): this
    tap(callback: (key: K, value: V) => void): this
  // here callback takes two arguments to print key and value
  }
}

مع وجود هذا في جذر مشروعنا، يمكننا إنشاء ملف index.ts وتوسيع كل prototype. لاحظ أننا لا نستورد ملف d.ts.

// index.ts or any other .ts file

Array.prototype.isEmpty = function () {
  return this.length === 0;
};

Array.prototype.insert = function<T>(value: T) {
  this.push(value);
  return this;
};

Array.prototype.tap = function<T>(callback: (item: T) => void) {
  this.forEach((item: T) => callback(item))
  return this;
};

//

Set.prototype.isEmpty = function () {
  return this.size === 0;
};

Set.prototype.insert = function<T>(value: T) {
  this.add(value);
  return this;
};

Set.prototype.tap = function<T>(callback: (item: T) => void) {
  this.forEach((item: T) => callback(item))
  return this;
};

//

Map.prototype.isEmpty = function () {
  return this.size === 0;
};

Map.prototype.insert = function<K, V>(key: K, value: V) {
  this.set(key, value);
  return this;
};

Map.prototype.tap = function<K, V>(callback: (key: K, value: V) => void) {
  for (const [key, value] of this.entries()) {
    callback(key, value);
  }
  return this;
};

بوضع هذا موضع الاختبار، يجب أن نحصل على نفس النتائج.

const myArr = [];
console.log(myArr.isEmpty()); // True

myArr.insert('Hello');
myArr.tap(console.log); // Hello
console.log(myArr.isEmpty()); // False

//

const mySet = new Set();
console.log(mySet.isEmpty()); // True

mySet.insert('Hello');
mySet.tap(console.log); // Hello
console.log(mySet.isEmpty()); // False

//

const myMap = new Map();
console.log(myMap.isEmpty()); // True

myMap.insert('Hello', 'World');
myMap.tap(console.log); // Hello World
console.log(myMap.isEmpty()); // False

تجريد حزم الطرف الثالث باستخدام الامتدادات

Books

أحيانًا نريد إضافة وظيفة إلى كود من مكتبة طرف ثالث في قاعدة الكود واستخدامها بشكل متكرر، لكننا لا نريد تعلّم جميع الدوال والخصائص التي تكشفها المكتبة.

أساسًا، يمكننا إنشاء تجريد للمكتبة التي نستخدمها بحيث يمكن للمستهلِك/المطوّر استخدام دالة دون معرفة ما تفعله خلف الكواليس، وما هي المكتبات التي تستدعيها.

لنُعدّ مكتبة تواريخ JavaScript الشهيرة - Day.js، في المشروع.

> npm init -y
...
> npm install dayjs

سنستفيد من إضافة relative time المدمجة في Day.js للحصول على الفارق بين التاريخ المطلوب والحالي.

const dayjs = require('dayjs');
const relativeTime = require('dayjs/plugin/relativeTime');
dayjs.extend(relativeTime);

سنوسّع صنف Date في JavaScript بدالتنا المخصصة (getDateOffset) التي تجرّد استدعاء relative time في Day.js.

Date.prototype.getDateOffset = function() {
    return dayjs().to(dayjs(this));
}

هذا بشكل أساسي يضيف ميزة إضافية إلى صنف Date في JavaScript. لنختبر دالتنا.

const dateInThePast = new Date();
dateInThePast.setDate(dateInThePast.getDate() - 10);
console.log(dateInThePast.getDateOffset()) // 10 days ago

var dateInTheFuture = new Date();
dateInTheFuture.setDate(dateInTheFuture.getDate() + 10);
console.log(dateInTheFuture.getDateOffset()); // in 10 days

لتحقيق هذه الحيلة في C#، تأكد من استخدام حزمة NuGet الخاصة بـ Humanizer. إذا أردت معرفة المزيد عن Day.js، تأكد من قراءة مقالي: الدليل الكامل لـ Day.js.

طرق الامتداد في C#

C# and .NET

ذكرنا C# عدة مرات في هذا المقال، لذا من الأفضل أن نضيف بعض المحتوى هنا أيضًا.

لتوسيع الأصناف الأساسية كما فعلنا مع الـ prototype في JavaScript، سنستفيد من طرق الامتداد (Extension Methods) في C#. الفكرة الأساسية مشابهة، ننشئ صنفًا مخصصًا بدالة مخصصة (امتداد) ستؤدي المهمة التي نحددها لها.

public static class Utils
{
 public static string TheMethod (this string TheText)
 {
  return TheText.something();
 }

بعض الأمور التي يجب مراعاتها:

  • يجب أن يكون الصنف والدالة ثابتين (static)
  • يحدد المعامل الأول لدالة الامتداد النوع الذي ستُطبَّق عليه. هذا هو مستهلِك الامتداد.
  • يسبق المعامل الأول الكلمة المفتاحية this. هذا ما يخبر C# أن هذه الدالة امتداد للنوع المزوَّد (في هذه الحالة string).

مع وجود هذا، نستورد صنف Utils الخاص بنا في الملف الذي نرغب باستخدام الامتداد فيه ونطبّقه:

using Utils;
...
string name = "Player 1";

name.TheMethod(); // watch the magic happen

الآن لنطبّق هذا النمط على امتداداتنا المخصصة الثلاثة.

من المهم الإشارة هنا إلى أن C# تدعم أيضًا الوراثة القائمة على الأصناف، كما فعلنا سابقًا.

# isEmpty

سنبدأ بإنشاء دالة امتداد. جمال مجموعات C# هو أنها جميعًا تنفّذ واجهة IEnumerable.

إذا أنشأنا دالة تُعيد IEnumerable، لن يهم حقًا ما إذا كانت الدالة تُعيد Array أو List أو HashSet (Set في JavaScript)؛ في أي من هذه الحالات لن يشتكي المترجم.

public IEnumerable<string> TheMethod() {
    return new string[0];
}
  
public IEnumerable<string> TheMethod2() {
    return new List<string>();
}
  
public IEnumerable<string> TheMethod3() {
    return new HashSet<string>();
}
  
public IEnumerable<string> TheMethod4() {
    return new Stack<string>();
}
  
public IEnumerable<string> TheMethod5() {
    return new Queue<string>();
}

يعمل أيضًا مع القواميس (Dictionaries) (Map في JavaScript)، طالما نتابع باستخدام KeyValuePair<K, V>، لأن IEnumerable يقبل نوعًا واحدًا (مُغلَّفًا).

public IEnumerable<KeyValuePair<string, string>> TheMethod() {
    return new Dictionary<string, string>();
}

نعود إلى مثالنا. ستعمل دالة isEmpty مع المجموعات، لذا سيكون المعامل المُمرَّر إلى دالة الامتداد لدينا من نوع IEnumerable<T>. ثم نستدعي دالة .Any() على المجموعة المطلوبة. تُعيد هذه true أو false حسب ما إذا كانت المجموعة فارغة أم لا.

public static class Utils
{
 public static bool IsEmpty<T>(this IEnumerable<T> collection)
 {
  return collection != null && !collection.Any(); 
  // .Any() method is available on all IEnumerables
 }
}

لنختبر هذا:

using System;
using System.Collections.Generic;
using System.Linq;
using Utils;

public class Program
{
 public static void Main()
 {
     string[] myArr = new string[0];
     Console.WriteLine(myArr.IsEmpty()); // True
    
     myArr = new string[5]; // array of Nulls
     Console.WriteLine(myArr.IsEmpty()); // False

      //

     var myList = new List<string>();
     Console.WriteLine(myList.IsEmpty()); // True
    
     myList.Add("Hello");
     Console.WriteLine(myList.IsEmpty()); // False
       
      //
    
     var myDict = new Dictionary<string, string>();
     Console.WriteLine(myDict.IsEmpty()); // True
    
     myDict.Add("Hello", "World");
     Console.WriteLine(myDict.IsEmpty()); // False
 }
}

نجح الاختبار بامتياز.

# insert

لهذه الدالة - سنستخدم صنفي Stack وList.

هنا، لن نستخدم IEnumerable، لأننا لا نستطيع إدراج بيانات في IEnumerable. بدلًا من ذلك، سننشئ ونتجاوز (override) دالة يمكنها العمل مع List<T> وStack<T>.

public static class Utils
{
 
    public static List<T> Insert<T>(this List<T> list, T item)
    {
        list.Add(item); // adds item to the list
        return list;
    }

    public static Stack<T> Insert<T>(this Stack<T> stack, T item)
    {
        stack.Push(item); // pushes item to the stack
        return stack;
    }
}

يمكن الآن لمستهلِك أي من الصنفين استخدام دالة insert، دون معرفة التنفيذ الأساسي.

var existingStories = new List<string>();
existingStories.Insert("Destructuring JS");
existingStories.Insert("Day.js");
existingStories.Insert("CMD");
  
Console.WriteLine(string.Join(", ", existingStories));
  
var upcoming = new Stack<string>();
upcoming.Insert("Express JS");
upcoming.Insert("Angular");
upcoming.Insert("Rx.js");
    
Console.WriteLine(string.Join(", ", upcoming));

# tap

لإنشاء دالة استدعاء في C#، سنستفيد من مفوّض Action\<T\>. هنا سنستخدم List وHashSet (وهو ما يعادل مجموعة Set في Java/JavaScript).

public static class Utils
{
    public static IEnumerable<T> Tap<T>
    (
      this IEnumerable<T> collection, 
      Action<T> callback
    )
    {
        foreach (T item in collection)
        {
            callback(item);
        }
        return collection;
    }
}

الآن لنختبر هذا:

using System;
using System.Collections.Generic;
using Utils;
     
public class Program
{
 public static void Main()
 {
  
  HashSet<int> mySet = new HashSet<int> { 1, 2, 3 };
  mySet.Tap(Console.WriteLine); // 1, 2, 3
  
  List<int> myList = new List<int> { 1, 2, 3 };
  myList.Tap(Console.WriteLine); // 1, 2, 3
 }
}

يمكن تطبيق هذا على القواميس أيضًا:

public static class Utils
{

    public static Dictionary<TKey, TValue> Tap<TKey, TValue>
    (
      this Dictionary<TKey, TValue> dictionary, 
      Action<TKey, TValue> callback
    )
    {
        foreach (KeyValuePair<TKey, TValue> kvp in dictionary)
        {
            callback(kvp.Key, kvp.Value);
        }
        return dictionary;
    }
}
using System;
using System.Collections.Generic;
using Utils;
     
public class Program
{
 public static void Main()
 {
  Dictionary<string, int> myDictionary = new Dictionary<string, int> {
   { "foo", 1 },
   { "bar", 2 },
   { "baz", 3 }
  };

  myDictionary.Tap((key, value) => 
    Console.WriteLine($"Key: {key}, Value: {value}"));

  /*
    Key: foo, Value: 1
    Key: bar, Value: 2
    Key: baz, Value: 3
  */
 }
}

جمال إنشاء الامتدادات

Abacus

إذا لم أقنعك حتى الآن باستخدام الامتدادات، فهذا يجب أن يحسم الأمر. باستخدام الدوال التي أنشأناها أعلاه، سننشئ مصفوفة فارغة ونضيف عناصر إليها.

const colleciton = new Array();
colleciton
  .insert(1)
  .insert(5)
  .insert(3)
  .insert(5)
  .tap(console.log);

// 1, 5, 3, 5

الآن لنستبدل صنف Array بصنف Set.

const colleciton = new Set();
colleciton
  .insert(1)
  .insert(5)
  .insert(3)
  .insert(5)
  .tap(console.log);

// 1, 5, 3

وكل شيء يعمل كما هو متوقع مع صفر تغييرات في الكود. الفرق في المخرج هو أن مجموعة Set تُعيد فقط العناصر الفريدة.

صعوبات العمل مع الامتدادات

Danger Sign

لنتحدث عن المشكلات المحتملة عند إنشاء امتدادات مخصصة:

#1

أول شيء يجب الانتباه له هو أننا نحتاج لأن يكون جميع مستهلِكي الامتداد متوافقين مع دالة الامتداد التي يستدعونها. على سبيل المثال، إذا كنا نستخدم List<T> في C# وأردت دمجها مع Stack<T>، يمكننا فقط استخدام الدوال ضمن الامتدادات المتوفرة في كليهما.

وإذا لم يشترك الاثنان في لغة مشتركة، يمكننا حينها تنفيذ دالة لكل صنف كما فعلنا أعلاه لدالة insert.

#2

تغييرات اللغة المستقبلية. إذا أضفنا دالة مخصصة في الكود وقُدِّمت نفس الدالة من قبل فريق اللغة الأساسية (JS/C#) مستقبلًا، فقد يُنشئ ذلك غموضًا يؤدي إلى كسر الكود.

#3

خطر آخر لإنشاء prototypes مخصصة هو أنه يمكننا كسر الكود الذي يعتمد على السلوك الافتراضي للكائنات المدمجة. على سبيل المثال، إذا عدّلنا كائن String.prototype لإضافة دالة مخصصة، فقد يتأثر أي كود يعتمد على السلوك الافتراضي لكائن String وقد ينكسر.

بالمثل مع طرق الامتداد في C#. إذا تعارض اسم دالة الامتداد مع خاصية موجودة، فقد يتسبب ذلك في سلوك غير متوقع أو أخطاء.

الخلاصة

الامتدادات طريقة ممتعة لتقديم ميزات جديدة، وتجريد التنفيذ، وإنشاء حلول موحّدة، لكنها قد تسبب مشكلات إذا استُخدمت بتهوّر.

قراءات إضافية عرض الكل
الخطوة التالية

طبّق هذه الأفكار على منتجك

يساعد استوديو فالنس الفرق ذات المخاطر العالية على تحويل الوضوح الهندسي إلى تسليم منتج معياري.