Dans le monde du logiciel, il existe une obsession omniprésente pour le refactoring prématuré et la recherche de fausse réutilisabilité. Les développeurs, en particulier ceux qui débutent, apprennent souvent que la « réutilisabilité » est le Saint Graal. Mais la recherche de la réutilisabilité à tout prix aboutit souvent à des solutions sur-conçues, trop génériques, trop rigides et trop éloignées des besoins spécifiques du projet en cours. En fait, cela peut conduire à ce que nous appelons souvent l'« l'enfer de l'abstraction » : un scénario dans lequel rien ne fonctionne vraiment à moins que vous ne compreniez parfaitement comment et pourquoi chaque partie du système a été abstraite pour s'adapter à une interface générique.
Nous suggérons un changement de paradigme : Au lieu d'être obsédés par la réutilisabilité, concentrons-nous sur l'adaptabilité, l'extensibilité et la substitution.
Dans ce contexte, nous cessons d'essayer de prédire les besoins futurs de notre base de code (comme une voyante de bonne aventure prédisant l'avenir) et nous nous concentrons plutôt sur la création d'une base solide et flexible pour aujourd'hui qui a encore de la place pour se développer et évoluer à mesure que l'avenir se déroule.
Le problème avec le refactoring prématuré est qu'il vient de la conviction que tout ce que vous écrivez doit être réutilisable. Cela peut sembler un objectif noble. Cependant, la réutilisabilité conduit souvent à une complexité inutile et à des abstractions inutiles. Prenez, par exemple, l'idée de créer un adaptateur API universel qui fonctionne pour tous vos modèles. L'idéal est que cet adaptateur puisse gérer n'importe quel point de terminaison d'API, n'importe quel format de données et n'importe quelle condition de réseau. Mais en réalité, cela signifie que vous construisez un cadre pour un avenir incertain, sans résoudre efficacement les problèmes d’aujourd’hui.
Exemple :
Prenons nos précédentes classes BaseAdapter et APIAdapter :
export class BaseAdapter { constructor(modelClass) { this.modelClass = modelClass; } async get(id) { throw new Error("Method 'get' must be implemented."); } async *all() { throw new Error("Method 'all' must be implemented."); } async query(params = {}) { throw new Error("Method 'query' must be implemented."); } async create(payload) { throw new Error("Method 'create' must be implemented."); } async update(payload) { throw new Error("Method 'update' must be implemented."); } async delete(id) { throw new Error("Method 'delete' must be implemented."); } }
Dans le code ci-dessus, le BaseAdapter définit toutes les méthodes possibles, nous laissant les implémenter dans des sous-classes spécifiques (comme APIAdapter, LocalStorageAdapter, etc.). Il s'agit d'un modèle pour divers adaptateurs. Cela semble bien en théorie, non ? Un jour, si nous devons nous connecter à un nouveau service ou intégrer une nouvelle solution de stockage, nous pourrons simplement créer une autre sous-classe.
Mais soyons réalistes : Est-il vraiment réutilisable ? Ou va-t-il simplement devenir une grosse boule de complexité, rendant votre système plus difficile à maintenir, à comprendre et à étendre ? Construisez-vous vraiment quelque chose qui peut être réutilisé dans le monde réel, ou êtes-vous simplement en train de deviner l'avenir ?
Au lieu de rechercher une réutilisabilité prématurée, nous proposons de nous concentrer sur l'adaptabilité et l'extensibilité. Qu'est-ce que ça veut dire ?
Il ne s’agit pas de créer le code parfaitement réutilisable qui fonctionne pour tous les cas extrêmes aujourd’hui. Au lieu de cela, nous nous concentrons sur la construction d'une base solide sur laquelle vous pouvez construire, ajouter et modifier au fil du temps. La clé est la flexibilité, pas une optimisation prématurée.
Dans l'ancien temps de Java (et de nombreux autres langages à typage statique), l'accent était souvent mis sur la création d'interfaces et sur la nécessité de rendre votre code « évolutif ». L’idée était d’anticiper chaque scénario à l’avance et de concevoir en fonction de celui-ci.
Cependant, cette approche peut souvent aboutir à une sur-ingénierie : concevoir pour des choses qui pourraient ne jamais se produire ou construire des cadres abstraits autour de problèmes qui n'ont pas encore fait surface. Vous écrivez effectivement du code censé être « universel » sans comprendre les besoins concrets du système sur lequel vous travaillez.
En Java, les interfaces étaient utilisées pour définir les contrats. Mais et si nous changeions cette façon de penser de « définir des contrats » à simplement fixer des attentes pour le présent ? Une promesse claire et fiable pour le contexte immédiat, sans supposer ce qui se passera dans le futur.
Dans notre nouvelle approche, nous ne faisons pas de promesses sur l'avenir de l'application comme une diseuse de bonne aventure mystique. Au lieu de cela, nous fixons des promesses claires et fiables pour aujourd'hui, et veillons à ce que ces promesses puissent être étendues et adaptées facilement lorsque le besoin s'en fait sentir.
Pensez-y comme ceci : nous ne prédisons pas à quoi ressemblera le monde dans 5 ans ; nous veillons à ce que le code que nous écrivons aujourd'hui puisse évoluer et s'adapter à mesure que le monde change. C'est comme poser des fondations solides pour un bâtiment, en s'assurant qu'il est suffisamment solide pour résister à tous les changements qui surviennent.
La « promesse » que nous faisons est un engagement envers l’adaptabilité et l’extensibilité. Le but n'est pas de prédire l'avenir, mais de créer les outils qui permettront aux futurs développeurs (ou à vous-même) d'ajouter, de modifier ou d'étendre facilement des fonctionnalités selon vos besoins.
Reprenons notre exemple avec le BaseAdapter et l'APIAdapter. Au lieu de créer des méthodes super génériques qui tentent de gérer toutes les situations, nous nous concentrerons sur la nécessité de rendre le code adaptable et facilement extensible.
Voici une réarchitecture rapide de l'APIAdapter :
export class BaseAdapter { constructor(modelClass) { this.modelClass = modelClass; } async get(id) { throw new Error("Method 'get' must be implemented."); } async *all() { throw new Error("Method 'all' must be implemented."); } async query(params = {}) { throw new Error("Method 'query' must be implemented."); } async create(payload) { throw new Error("Method 'create' must be implemented."); } async update(payload) { throw new Error("Method 'update' must be implemented."); } async delete(id) { throw new Error("Method 'delete' must be implemented."); } }
Maintenant, au lieu de créer un tout nouveau BaseAdapter pour chaque nouveau type d'adaptateur, nous avons créé une base qui peut être facilement étendue et adaptée aux besoins futurs.
Exemple d'extension pour un nouveau point de terminaison d'API :
export class APIAdapter extends BaseAdapter { static baseURL; static headers; static endpoint; async *all(params = {}) { // Custom logic, but easily extensible if needed const url = `${this.baseURL}/${this.endpoint}`; const response = await API.get(url, { params, headers: this.headers }); return response.data; } async query(params = {}) { // Simplified for illustration const url = `${this.baseURL}/${this.endpoint}/search`; const response = await API.get(url, { params }); return response.data; } // Easily extendable for specific cases async customRequest(method, endpoint, params = {}) { const url = `${this.baseURL}/${endpoint}`; const response = await API[method](url, { params }); return response.data; } }
Dans ce scénario, si vous devez ajouter un comportement spécifique pour un point de terminaison d'API (par exemple, une gestion personnalisée des erreurs pour les commandes), vous pouvez remplacer ou étendre l'APIAdapter pour l'adapter à votre besoins sans refactoriser l'ensemble du système.
Dans ce nouveau paradigme, nous n’essayons pas de prédire tous les besoins ou problèmes futurs. Au lieu de cela, nous nous concentrons sur la construction d'une base solide et flexible qui s'adapte à mesure que les exigences changent et que de nouveaux défis surviennent. Nous n'abstrayons pas prématurément ni ne sur-concevons des solutions basées sur des problèmes hypothétiques. Au lieu de cela, nous créons des outils qui peuvent évoluer et être facilement adaptés à mesure que de nouveaux besoins apparaissent.
La clé n'est pas d'être à l'épreuve du temps comme une voyante de bonne aventure, mais de créer une fondation qui résistera de manière fiable à l'épreuve du temps, même si le monde change. C'est une promesse que vous pouvez faire à votre futur moi : le code est solide, adaptable et prêt à être étendu à mesure que de nouvelles exigences entrent en jeu.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!