حرب TypeScript أو Enum Conquest

الخلفية


قبل نصف عام ، قررت شركتنا التحول إلى تقنيات أحدث وأكثر عصرية. لهذا ، تم تشكيل مجموعة من المتخصصين ، والتي تهدف إلى: تحديد المكدس التكنولوجي ، وإنشاء جسر إلى رمز Legacy على أساس هذا المكدس ، وأخيرًا ، نقل بعض الوحدات القديمة إلى القضبان الجديدة. كنت محظوظاً لأنني دخلت هذه المجموعة. تتكون قاعدة رمز العميل من مليون سطر تقريبًا من التعليمات البرمجية. لقد اخترنا TypeScript كلغة. قرروا جعل الركيزة واجهة المستخدم الرسومية على vue بالتزامن مع مكون vue-class و IoC .

لكن القصة ليست حول كيفية التخلص من إرث المدونة ، ولكن حول حادثة صغيرة واحدة أدت إلى حرب حقيقية للمعرفة. من يهتم ، مرحبا بكم في القطط.

التعرف على المشكلة


بعد شهرين من البداية ، شعرت مجموعة العمل بالراحة مع المكدس الجديد وتمكنت من نقل جزء من الرمز القديم إليها. يمكنك حتى أن تقول أنه كانت هناك بعض الكتلة الحرجة من اللحم عندما كنت بحاجة للتوقف ، وأخذ نفسًا وتنظر في ما قمنا به.

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

بعد أن أثارت موضوع التحويلات بين الزملاء ، تلقيت الدعم. اتضح أنني لم أكن راضيًا فقط عن عدم وجود نهج موحد للعمل معهم. في تلك اللحظة ، بدا لي أنه في غضون ساعتين من الترميز ، سأحقق النتيجة المرجوة وتطوعت لتصحيح الوضع. ولكن كم كنت مخطئا بعد ذلك ...

//    ,        . import {Enum} from "ts-jenum"; @Enum("text") export class State { static readonly NEW = new State("New"); static readonly ACTIVE = new State("Active"); static readonly BLOCKED = new State("Blocked"); private constructor(public text: string) { super(); } } //   console.log("" + State.ACTIVE); // Active console.log("" + State.BLOCKED); // Blocked console.log(State.values()); // [State.NEW, State.ACTIVE, State.BLOCKED] console.log(State.valueOf("New")); // State.NEW console.log(State.valueByName("NEW")); // State.NEW console.log(State.ACTIVE.enumName); // ACTIVE 

1. ديكور


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

ديكور
 export function Enum(idProperty?: string) { // tslint:disable-next-line return function <T extends Function, V>(target: T): T { if ((target as any).__enumMap__ || (target as any).__enumValues__) { const enumName = (target as any).prototype.constructor.name; throw new Error(`The enumeration ${enumName} has already initialized`); } const enumMap: any = {}; const enumMapByName: any = {}; const enumValues = []; // Lookup static fields for (const key of Object.keys(target)) { const value: any = (target as any)[key]; // Check static field: to be instance of enum type if (value instanceof target) { let id; if (idProperty) { id = (value as any)[idProperty]; if (typeof id !== "string" && typeof id !== "number") { const enumName = (target as any).prototype.constructor.name; throw new Error(`The value of the ${idProperty} property in the enumeration element ${enumName}. ${key} is not a string or a number: ${id}`); } } else { id = key; } if (enumMap[id]) { const enumName = (target as any).prototype.constructor.name; throw new Error(`An element with the identifier ${id}: ${enumName}.${enumMap[id].enumName} already exists in the enumeration ${enumName}`); } enumMap[id] = value; enumMapByName[key] = value; enumValues.push(value); Object.defineProperty(value, "__enumName__", {value: key}); Object.freeze(value); } } Object.freeze(enumMap); Object.freeze(enumValues); Object.defineProperty(target, "__enumMap__", {value: enumMap}); Object.defineProperty(target, "__enumMapByName__", {value: enumMapByName}); Object.defineProperty(target, "__enumValues__", {value: enumValues}); if (idProperty) { Object.defineProperty(target, "__idPropertyName__", {value: idProperty}); } //  values(), valueOf     ,    -. Object.freeze(target); return target; }; } 

وهنا عانيت من الفشل الأول. اتضح أنه لا يمكنك تغيير النوع بمساعدة الديكور. مايكروسوفت لديها حتى نداء حول هذا الموضوع: Class Decorator Mutation . عندما أقول أنه لا يمكنك تغيير النوع ، أعني أن IDE الخاص بك لن يعرف أي شيء عن هذا ولن يقدم أي تلميحات وإكمال تلقائي مناسب. ويمكنك تغيير النوع بقدر ما تريد ، فقط خيره ...

2. الميراث


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

لا تحصى
 // :     ,  -     export class Enumerable<T> { constructor() { const clazz = this.constructor as any as EnumStore; if (clazz.__enumMap__ || clazz.__enumValues__ || clazz.__enumMapByName__) { throw new Error(`It is forbidden to create ${clazz.name} enumeration elements outside the enumeration`); } } static values<T>(): ReadonlyArray<T> { const clazz = this as any as EnumStore; if (!clazz.__enumValues__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } return clazz.__enumValues__; } static valueOf<T>(id: string | number): T { const clazz = this as any as EnumStore; if (!clazz.__enumMap__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } const value = clazz.__enumMap__[id]; if (!value) { throw new Error(`The element with ${id} identifier does not exist in the $ {clazz.name} enumeration`); } return value; } static valueByName<T>(name: string): T { const clazz = this as any as EnumStore; if (!clazz.__enumMapByName__) { throw new Error(`${clazz.name} enumeration has not been initialized. It is necessary to add the decorator @Enum to the class`); } const value = clazz.__enumMapByName__[name]; if (!value) { throw new Error(`The element with ${name} name does not exist in the ${clazz.name} enumeration`); } return value; } get enumName(): string { return (this as any).__enumName__; } toString(): string { const clazz = this.constructor as any as EnumStore; if (clazz.__idPropertyName__) { const self = this as any; return self[clazz.__idPropertyName__]; } return this.enumName; } } 

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

بعد تجربة قصيرة ، أدركت أنها نسخة TypeScript. اتضح أنه إذا تزامن الاسم العام للفئة مع اسم النوع المحدد في الطريقة الثابتة ، فإن المترجم يعتبر هذا نوعًا واحدًا. ولكن مهما كانت ، فقد أصبحت الآن جزءًا من تاريخ تلك الحرب لمعرفة TypeScript.

المحصلة النهائية: مشكلة التحويلات كما كانت وبقيت. حزني ...

ملاحظة: لا يمكنني إعادة هذا السلوك بنفسي الآن باستخدام 2.6.2 ، ربما أخطأت في الإصدار أو لم أضيف شيئًا في حالات الاختبار. كما تم رفض طلب المشكلة المذكورة أعلاه السماح للأعضاء الساكنين بالإشارة إلى معلمات نوع الفئة .

3. وظيفة الصب


على الرغم من وجود حل معوج ، مع إشارة صريحة إلى نوع فئة العد في الأساليب الثابتة ، على سبيل المثال ، State.valueOf <State> () ، إلا أنها لم تناسب أي شخص ، وقبل كل شيء ، أنا. لفترة من الوقت ، حتى وضعت جانبا عمليات النقل اللعينة وفقدت الثقة بأنني أستطيع عموما حل هذه المشكلة.

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

 export function EnumType<T>(): IStaticEnum<T> { return (<IStaticEnum<T>> Enumerable); } //  IStaticEnum : export interface IStaticEnum<T> { new(): {enumName: string}; values(): ReadonlyArray<T>; valueOf(id: string | number): T; valueByName(name: string): T; } 

ويظهر الإعلان عن تعداد يشبه جافا الآن كما يلي:

 import {Enum, EnumType, IStaticEnum} from "ts-jenum"; @Enum("text") export class State extends EnumType<State>() { static readonly NEW = new State("New"); static readonly ACTIVE = new State("Active"); static readonly BLOCKED = new State("Blocked"); private constructor(public text: string) { super(); } } //   console.log("" + State.ACTIVE); // Active console.log("" + State.BLOCKED); // Blocked console.log(State.values()); // [State.NEW, State.ACTIVE, State.BLOCKED] console.log(State.valueOf("New")); // State.NEW console.log(State.valueByName("NEW")); // State.NEW console.log(State.ACTIVE.enumName); // ACTIVE 

لا يخلو من فضول ، مع استيراد إضافي لـ IStaticEnum ، والذي لا يستخدم في أي مكان (انظر المثال أعلاه). في إصدار TypeScript 2.6.2 سيئ الحظ ، تحتاج إلى تحديده بشكل صريح. خلل في الموضوع هنا .

المجموع


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


اكتب عن عملك ، أعتقد أن المجتمع سيكون مهتمًا. شكرا لكم جميعا على صبركم واهتمامكم.

Source: https://habr.com/ru/post/ar420041/


All Articles