Contexte
Il y a six mois, notre entreprise a décidé de passer à des technologies plus récentes et plus à la mode. Pour cela, un groupe de spécialistes a été formé, qui devait: déterminer la pile technologique, faire un pont vers le code Legacy sur la base de cette pile, et, enfin, transférer certains des anciens modules vers de nouveaux rails. J'ai eu la chance de faire partie de ce groupe. La base de code client est d'environ un million de lignes de code. Nous avons choisi TypeScript comme langage. Ils ont décidé de créer un substrat GUI sur vue en collaboration avec
vue-class-component et
IoC .
Mais l'histoire n'est pas sur la façon dont nous nous sommes débarrassés de l'héritage du code, mais sur un petit incident qui a entraîné une véritable guerre de la connaissance. Peu importe, bienvenue au chat.
Connaître le problème
Quelques mois après le début, le groupe de travail s'est familiarisé avec la nouvelle pile et a réussi à lui transférer une partie de l'ancien code. Vous pourriez même dire qu'il y avait une masse critique de viande lorsque vous deviez vous arrêter, prendre une respiration et regarder ce que nous avions fait.
Comme vous le savez, les endroits qui nécessitaient une étude approfondie suffisaient. Mais de toutes les choses importantes, ironiquement, rien ne m'a attrapé. Pas en tant que développeur. Mais l'un des sans importance, au contraire, était obsédant. J'étais énervé au point de voir comment nous travaillons avec les données d'énumération. Il n'y a pas eu de généralisation. Vous rencontrerez une classe séparée avec un ensemble de méthodes requises, puis vous trouverez deux classes, pour la même, ou même quelque chose de mystérieux et magique. Et il n'y a personne à blâmer. Le rythme que nous avons pris pour nous débarrasser de l'héritage était trop grand.
Ayant soulevé la question des transferts entre collègues, j'ai reçu un soutien. Il s'est avéré que non seulement je n'étais pas satisfait de l'absence d'une approche unifiée pour travailler avec eux. À ce moment-là, il m'a semblé que dans quelques heures de codage, j'obtiendrais le résultat souhaité et me suis porté volontaire pour corriger la situation. Mais à quel point je me trompais alors ...
// , . 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. Décorateur
Par où commencer? Une seule chose m'est venue à l'esprit: prendre comme base une énumération de type Java. Mais comme je voulais montrer à mes collègues, j'ai décidé d'abandonner l'héritage classique. Utilisez plutôt un décorateur. De plus, le décorateur pourrait être appliqué avec des arguments afin de donner aux énumérations la fonctionnalité requise facilement et naturellement. Le codage n'a pas pris beaucoup de temps, et après quelques heures, j'avais déjà quelque chose de similaire à ceci:
Décorateur 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; }; }
Et là, j'ai subi le premier échec. Il s'est avéré que vous ne pouvez pas changer le type avec l'aide d'un décorateur. Microsoft a même un appel à ce sujet: la
classe Decorator Mutation . Quand je dis que vous ne pouvez pas changer le type, je veux dire que votre IDE ne saura rien à ce sujet et n'offrira aucun conseil ni auto-complétion adéquate. Et vous pouvez changer le type autant que vous le souhaitez, seulement le bien de celui-ci ...
2. Héritage
Comme je n'ai pas essayé de me persuader, mais j'ai dû revenir à l'idée de créer des transferts basés sur la classe générale. Oui, et qu'est-ce qui ne va pas? J'étais ennuyé par moi-même. Le temps presse, les gars du groupe, Dieu ne plaise, je passe du temps sur les décorateurs. Il était possible de déposer l'énumération en général en une heure et de passer à autre chose. Qu'il en soit ainsi. Jeta rapidement le code de la classe de base Enumerable et soupira, se sentant soulagé. J'ai jeté le brouillon dans le référentiel général et j'ai demandé à mon collègue de vérifier la solution.
Enumerable // : , - 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; } }
Mais la tragicomédie prenait de l'ampleur. La version 2.6.2 de TypeScript a été installée sur ma machine, la version dans laquelle il y avait un bug inestimable. Inestimable, car ce n'est pas un bug, mais un fitcha. Une voix de la pièce voisine a crié qu'il n'allait rien faire. Erreur lors de la compilation (
transpilation ). Je n'en croyais pas mes propres oreilles, car je montais toujours un projet avant une poussée, même si c'était un brouillon. Et la voix intérieure a chuchoté: c'est un fiasco, mon frère.
Après un court essai, j'ai réalisé que c'était une version TypeScript. Il s'est avéré que si le nom générique de la classe coïncidait avec le nom du générique spécifié dans la méthode statique, le compilateur le considérait comme un type. Mais quoi qu'il en soit, il fait maintenant partie de l'histoire de cette guerre pour la connaissance de TypeScript.
Conclusion: le problème des transferts tels qu'ils étaient et est resté. Ma douleur ...
Remarque:
je ne peux pas reproduire ce comportement moi-même maintenant avec 2.6.2, j'ai peut-être fait une erreur avec la version ou je n'ai pas ajouté quelque chose dans les cas de test. Et la demande pour le problème ci-dessus Autoriser les membres statiques à référencer les paramètres de type de classe a été rejetée.3. Fonction de coulée
Malgré le fait qu'il y avait une solution tordue, avec une indication explicite du type de la classe d'énumération dans les méthodes statiques, par exemple, State.valueOf <State> (), cela ne convenait à personne, et d'abord à moi. Pendant un moment, j'ai même mis de côté les putains de transferts et perdu confiance que je pouvais généralement résoudre ce problème.
Ayant retrouvé ma force morale, j'ai cherché sur Internet des astuces TypeScript, regardé qui souffrait de quoi, relu la documentation du langage, juste au cas où, et décidé, afin de l'éviter, de terminer le travail. Sept heures d'expériences continues, sans rien chercher, même pour le café, ont donné leur résultat. Une seule fonction, composée d'une ligne de code, met tout à sa place.
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; }
Et la déclaration d'une énumération de type Java ressemble maintenant à ceci:
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
Pas sans curiosité, avec une importation supplémentaire de IStaticEnum, qui n'est utilisé nulle part (voir l'exemple ci-dessus). Dans la version très malheureuse de TypeScript 2.6.2, vous devez le spécifier explicitement. Un bug sur le sujet
ici .
Total
Si vous souffrez longtemps, quelque chose ira bien. Lien vers github avec le résultat du travail effectué
ici . Pour moi, j'ai découvert que TypeScript est un langage avec de grandes fonctionnalités. Il y a tellement de ces opportunités que vous pouvez vous noyer en elles. Et qui ne veut pas couler, apprend à nager. Si vous revenez au sujet des transferts, vous pouvez voir comment les autres travaillent avec eux:
Écrivez sur votre travail, je pense que la communauté sera intéressée. Merci à tous pour votre patience et votre intérêt.