Diseño de arquitectura front-end web a gran escala: introducción a la programación abstracta

Autor: svenzeng, ingeniero de desarrollo front-end de Tencent PCG

La programación orientada a abstracciones es un principio de referencia muy importante para construir un sistema a gran escala. Pero para muchos estudiantes de front-end, la comprensión de la programación abstracta no es muy profunda. El hábito de la mayoría de los estudiantes es comenzar a escribir la interfaz de la interfaz de usuario después de obtener la lista de demanda y el borrador del diseño, qué botones de la interfaz de usuario deben ajustarse, qué métodos, y luego escribir estos métodos, rara vez consideran la reutilización. Cuando los requisitos cambian un día, se descubre que el código actual es difícil de adaptar a estos cambios y solo se puede reescribir. Día tras día, así que ciclo.

Cuando vea por primera vez la frase "separar la abstracción de la realización concreta", puede resultar difícil entender lo que significa. ¿Qué es abstracción y qué es realización concreta? Para entender este pasaje, seamos pacientes, veamos primero un pequeño ejemplo hipotético y recordemos qué es la programación orientada a la implementación específica.

Supongamos que estamos desarrollando un programa similar a "Los Sims" y creamos a Xiao Ming. Para que tenga una vida normal todos los días, establecemos la siguiente lógica en su programa principal:

1、8点起床
2、9点吃面包
3、17点打篮球

Después de un mes, Xiao Ming estaba cansado de la constante repetición de la vida, después de levantarse una mañana, de repente quiso comer papas fritas en lugar de pan. Por la noche, quería jugar al fútbol en lugar de al baloncesto, así que tuvimos que modificar el código fuente:

 1、8点起床
 2、9点吃面包 -> 9点吃薯片
 3、17点打篮球 -> 17点踢足球

Después de un tiempo, Xiao Ming espera jugar al fútbol los días 3 y 5 de la semana y al bádminton el domingo. En este momento, para satisfacer la demanda, se pueden agregar muchas declaraciones if y else a nuestro programa.

Para satisfacer el cambio de la demanda, es muy similar al mundo real, necesitamos profundizar en el código fuente central y hacer muchos cambios. Ahora piensa en tu propio código, ¿hay muchas escenas familiares?

Este es un ejemplo de programación orientada a la implementación específica. Aquí, las acciones de comer pan, comer papas fritas, jugar baloncesto y jugar al fútbol pertenecen a implementaciones específicas. Asignados al programa, son un módulo, una clase o una función, que incluyen Con algún código específico, ser responsable de una cosa específica.

Una vez que queremos cambiar estas implementaciones en el código, debemos ser forzados a profundizar y modificar el código fuente central. Cuando los requisitos cambian, por un lado, si hay una gran cantidad de implementaciones específicas en el código central, la carga de trabajo de reescribir todas estas implementaciones específicas es enorme, por otro lado, modificar el código siempre traerá incógnitas. Cuando la conexión entre módulos está indisolublemente vinculada, debe tener cuidado de modificar cualquier módulo, de lo contrario es muy probable que se corrija un error y se produzcan tres errores más.

Extrae características comunes

Medios abstractos: extraer características comunes y esenciales de algunas cosas.

Si siempre escribimos código para implementaciones específicas, como en el ejemplo anterior, escribimos pan difícil de comer a las 9 en punto, o escribimos papas fritas difíciles de comer a las 9 en punto. Como resultado, en el proceso de desarrollo empresarial e iteración del sistema, el sistema se volverá rígido y difícil de modificar. Los requisitos del producto siempre cambian. Necesitamos mantener el código fuente central estable y no modificado en un entorno cambiante.

El método consiste en extraer las características generales de "comer pan a las 9 en punto" y "comer papas fritas a las 9 en punto". Aquí, "desayunar a las 9 en punto" se puede utilizar para representar esta característica general. De la misma forma, extrajimos las características generales de "jugar al baloncesto a las 17:00" y "jugar al fútbol a las 17:00" y las reemplazamos por "jugar a las 17:00". Entonces deje que este código fuente central se base en estas "características generales abstractas" en lugar de depender de la "realización concreta" de "comer pan" o "desayunar".

Escribimos este código como:

  1、 8点起床
  2、 9点吃早餐
  3、17点做运动

De esta manera, este código fuente central se ha vuelto relativamente estable. No importa lo que Xiao Ming quiera comer por la mañana, no es necesario cambiar este código. Siempre que el programa externo "desayune" o "coma papas" en la etapa posterior, Se puede inyectar "Piece".

Ejemplo real

Justo ahora era un ejemplo virtual. Ahora mire un fragmento de código real. Este código es todavía muy simple, pero puede ilustrar bien los beneficios de la abstracción.

En un cierto código de negocio central, localstorge debe usarse para almacenar cierta información de operación del usuario. El código se escribe rápidamente:

import ‘localstorge’ from 'localstorge';

class User{
    save(){
        localstorge.save('xxx');
    }
}

const user = new User();
user.save();

Este código originalmente funcionó bien, pero un día, descubrimos que la cantidad de datos relacionados con la información del usuario era demasiado grande, excediendo la capacidad de almacenamiento de localstorge. En este momento pensamos en indexdb, parece que sería más razonable usar indexdb para almacenamiento.

Ahora tenemos que reemplazar localstorge con indexdb, por lo que tenemos que profundizar en la clase User y cambiar el lugar donde se llama a localstorge para llamar a indexdb. Parece que hemos regresado a la escena familiar. Descubrimos que en el programa, en la profundidad de muchas lógicas comerciales centrales, no solo uno, sino cientos de miles de lugares llamados localstorge, esta simple modificación se ha convertido en un desastre.

Por lo tanto, todavía necesitamos extraer la parte abstracta común de localstorge e indexdb. Obviamente, la parte abstracta común de localstorge e indexdb es proporcionar a sus consumidores un método de guardado. Como consumidor, el código lógico central en el negocio, no le importa si es localstorge o indexdb Este asunto puede ser determinado por otros códigos externos después de que el programa sea posterior.

Podemos declarar una interfaz con un método de guardado:

interface DB{
   save(): void;
}

Luego, deje que el usuario del módulo empresarial principal solo confíe en esta interfaz:

import DB from 'DB';

class User{
    constructor(
        private db: DB
   ){

    }
    save(){
        this.db.save('xxx');
    }
}

Luego deje que Localstorge e Indexdb implementen la interfaz DB respectivamente:

class Localstorge implements DB{
    save(str:string){
        ...//do something
    }
}

class Indexdb implements DB{
    save(str:string){
        ...//do something
    }
}

const user = new User( new Localstorge() );
//or
const user = new User( new Indexdb() );

userInfo.save();

De esta manera, el módulo de Usuario ha pasado de depender de las implementaciones específicas de Localstorge o Indexdb a depender de la interfaz de DB. El módulo de Usuario se ha convertido en un módulo estable. No importa si usamos Localstorge o Indexdb en el futuro, el módulo de Usuario no estará obligado a seguir. Para hacer cambios.

Mantenga las modificaciones alejadas del código fuente central

Algunos estudiantes pueden tener preguntas. Aunque ya no necesitamos modificar el módulo de Usuario, todavía tenemos que elegir si usar Localstorge o Indexdb. Tenemos que cambiar el código en alguna parte. ¿Cuál es la diferencia entre esto y el código del módulo de Usuario? ?

De hecho, de lo que estamos hablando de programación orientada a abstracciones suele ser para módulos comerciales centrales. El módulo de usuario pertenece a nuestra lógica empresarial central y esperamos que sea lo más estable posible. No quiero cambiar el módulo de usuario solo porque elige usar Localstorge o Indexdb. Porque una vez que la lógica empresarial central del módulo de usuario se cambia accidentalmente, afectará a miles de módulos externos que dependen de él.

Si el módulo de usuario ahora se basa en la interfaz de base de datos, es mucho menos probable que se modifique. No importa cómo se desarrolle el almacenamiento local en el futuro, siempre que sigan proporcionando funciones de guardado externas, el módulo de usuario no cambiará debido a cambios en el almacenamiento local.

En relación con el comportamiento específico, la interfaz siempre es relativamente estable, porque una vez que se modifica la interfaz, significa que la implementación específica debe modificarse en consecuencia. Por el contrario, cuando se modifica el comportamiento específico, normalmente no es necesario modificar la interfaz.

En cuanto a la opción de usar Localstorge o Indexdb, hay muchas formas de hacerlo. Por lo general, lo colocamos en un lugar que es más fácil de modificar, es decir, el módulo externo alejado de la lógica empresarial central, por nombrar algunas. Un ejemplo:

* 在main函数或者其他外层模块中生成Localstorge或者Indexdb对象,在User对象被创建时作为参数传给User
* 用工厂方法创建Localstorge或者Indexdb
* 用依赖注入的容器来绑定DB接口和它具体实现之间的映射

Dependencias internas, externas y unidireccionales

Colocar el sistema en capas es como si un arquitecto divide un edificio en muchas capas, cada una con su propio diseño y función únicos, que es la base para construir una arquitectura de sistema a gran escala. Además de la arquitectura en capas obsoleta de mvc, los métodos de capas que se utilizan comúnmente en la actualidad incluyen arquitectura de cebolla (arquitectura ordenada), arquitectura DDD (diseño controlado por dominio), arquitectura hexagonal (arquitectura de adaptador de puerto), etc. No presentaremos cada uno Un modelo en capas, pero ya sea una arquitectura de cebolla, una arquitectura DDD o una arquitectura hexagonal, sus capas se dividirán de forma relativa y dinámica en capas externas e internas.

También hemos mencionado los conceptos de capas internas y externas varias veces (en la mayoría de los libros, en los negocios reales, qué módulos corresponden a la capa interna y qué módulos deben colocarse en la capa externa. ¿Qué regla es para decidir?

Primero observe la siguiente naturaleza: la tierra gira alrededor del Sol. Creemos que el sol es la capa interna y la tierra es la capa externa. Una vez que los ojos reciben la luz, se toman imágenes del cerebro. Creemos que el cerebro es la capa interna y los ojos la capa externa. Por supuesto, las capas interna y externa aquí no están determinadas por la ubicación física, sino en función de la estabilidad del módulo, es decir, los módulos que son más estables y difíciles de modificar deben colocarse en la capa interna, y los módulos que son más volátiles y con mayor probabilidad de ser modificados deben colocarse en la capa interna. Se coloca en la capa exterior. Al igual que cuando se construye una casa con bloques de construcción, debemos colocar los bloques de construcción más fuertes debajo.

Esta configuración de reglas es muy significativa, porque un sistema de capas maduro cumplirá estrictamente las dependencias unidireccionales.

Veamos la siguiente imagen:

marca

Suponiendo que el sistema se divide en cuatro capas: A, B, C y D, entonces A es la capa relativamente más interna, y la capa externa es B, C y D en secuencia. En un sistema de dependencia estrictamente unidireccional, la relación de dependencia siempre puede apuntar solo desde la capa externa a la capa interna.

Esto se debe a que si se modifica el módulo A más interno, los módulos B, C y D que dependen del módulo A estarán todos implicados. En un lenguaje de tipado estático, estos módulos deben recompilarse debido a cambios en el módulo A, y si hacen referencia a una variable en el módulo A o llaman a un método en el módulo A, es probable que el módulo A los modifique. Debe modificarse en consecuencia. Así que esperamos que el módulo A sea el más estable, lo mejor es no cambiar nunca.

Pero, ¿y si se modifica el módulo exterior? Por ejemplo, después de que se modifica el módulo D, porque está en la capa más externa y ningún otro módulo depende de él, solo se afecta a sí mismo. Los módulos A, B y C no necesitan preocuparse por ningún impacto en ellos, por lo que cuando la capa externa Cuando se modifica el módulo, el daño al sistema es relativamente pequeño.

Si los módulos que son fáciles de cambiar y que a menudo cambian con los requisitos del producto se colocan cerca de la capa interna desde el principio, significa que a menudo tenemos que ajustar o probar otros módulos en el sistema que dependen de él debido a cambios en estos módulos.

Se puede imaginar que el Creador también puede establecer el universo y la naturaleza basándose en el principio de dependencia unidireccional. Por ejemplo, los planetas dependen de las estrellas. Sin la tierra, el sol no tendría mucho impacto, y si el sol se perdiera, la tierra naturalmente no existiría. Los ojos dependen del cerebro. Si el cerebro está roto, los ojos pierden naturalmente su función, pero las otras funciones del cerebro aún se pueden utilizar si los ojos están rotos. Parece que la tierra es solo un complemento del sol y los ojos son solo un complemento del cerebro.

Volviendo al desarrollo comercial específico, la lógica comercial central es generalmente relativamente estable, y cuanto más cerca de la entrada y salida del usuario (cuanto más cerca del gerente y diseñador del producto, como la interfaz de interfaz de usuario), más inestable. Por ejemplo, al desarrollar un software de negociación de acciones, las reglas básicas de la negociación de acciones rara vez cambian, pero la interfaz del sistema puede cambiar fácilmente. Por lo tanto, generalmente colocamos la lógica comercial central en la capa interna y colocamos los módulos cerca de la entrada y salida del usuario en la capa externa.

En el negocio de documentos de Tencent, la lógica empresarial central se refiere al cálculo de los datos de entrada del usuario a través de ciertas reglas y convertirlos en datos de documentos. Estas reglas de conversión y procesos de cálculo específicos son la lógica comercial central de Tencent Docs. Son muy estables. Desde Microsoft Office hasta Google Docs y Tencent Docs, no ha habido demasiados cambios en más de 30 años. Deben colocarse en la capa interna del sistema. Por otro lado, si esta lógica empresarial central se ejecuta en el lado del navegador, terminal o nodo, no deberían cambiar. La capa de red, la capa de almacenamiento, la capa fuera de línea y la interfaz de usuario son cambiables. En el entorno de terminal, la implementación de la capa de interfaz de usuario de terminal y la capa web son completamente diferentes. En el lado del nodo, la capa de almacenamiento puede eliminarse directamente del sistema, porque en el lado del nodo, solo necesitamos usar el módulo de lógica empresarial central para realizar algunos cálculos en la función. De la misma manera, en las pruebas unitarias o de integración, es posible que la capa fuera de línea y la capa de almacenamiento no sean necesarias. En estas situaciones volátiles, debemos colocar la lógica comercial no esencial en la capa externa para que puedan modificarse o reemplazarse en cualquier momento.

Por lo tanto, seguir el principio de dependencia unidireccional puede mejorar en gran medida la estabilidad del sistema y reducir el daño al sistema cuando cambian los requisitos. Cuando diseñamos cada módulo, dedicamos mucho tiempo al nivel de diseño, la división de módulos y las dependencias entre niveles y módulos. A menudo decimos "divide y vencerás". "División" se refiere al nivel y módulo. Cómo dividir, clasificar, etc., "gobernanza" se refiere a cómo conectar razonablemente los niveles, módulos y clases divididos. Estos diseños son más importantes que los detalles de codificación específicos.

Principio de reversión de la dependencia

La idea central del principio de inversión de dependencia es: los módulos internos no deben depender de los módulos externos, todos deben depender de la abstracción.

Aunque dedicaremos mucho tiempo a considerar qué módulos se colocan en las capas interna y externa, intente asegurarse de que estén en una dependencia unidireccional. Sin embargo, en el desarrollo real, siempre hay muchos escenarios en los que los módulos internos deben depender de los módulos externos.

Por ejemplo, en los ejemplos de Localstorge e Indexdb, el módulo de usuario, como la lógica empresarial central de la capa interna, se basa en los módulos volátiles Localstorge e Indexdb de la capa externa, lo que hace que el módulo de usuario se vuelva inestable.

import ‘localstorge’ from 'localstorge';

class User{
    save(){
        localstorge.save('xxx');
    }
}

const user = new User();
user.save();

Falta una foto

Para resolver el problema de estabilidad del módulo de usuario, introdujimos la interfaz abstracta DB, que es relativamente estable. El módulo de usuario cambió para depender de la interfaz abstracta DB, de modo que el usuario se convirtió en un módulo estable.

Interface DB{
   save(): void;
}

Luego, deje que el usuario del módulo empresarial principal solo confíe en esta interfaz:

import DB from 'DB';

class User{
    constructor(
        private db: DB
   ){

    }
    save(){
        this.db.save('xxx');
    }
}

Luego deje que Localstorge e Indexdb implementen la interfaz DB respectivamente:

class Localstorge implements DB{
    save(str:string){
        ...//do something
    }
}

La dependencia se convierte en: imagen faltante

Usuario -> DB <- Localstorge

De la Figura 1 y la Figura 2, el módulo de usuario ya no depende explícitamente de Localstorge, sino de la interfaz DB estable. ¿Qué es DB? Localstorge o Indexdb serán inyectados por otros módulos externos más adelante en el programa. Aquí La relación de dependencia parece invertida, este enfoque se denomina "inversión de dependencia".

Encuentre cambios, abstraiga y encapsule

Nuestro tema "programación orientada a abstractos", en muchos casos se refiere en realidad a "programación orientada a interfaces". La programación orientada a abstractos se destaca desde una perspectiva más macro del diseño de sistemas y nos guía cómo construir un sistema débilmente acoplado, mientras que la programación orientada a interfaces nos dice Nuestro método de realización concreta. El principio de inversión de dependencias nos dice cómo usar la "programación orientada a la interfaz" para que las dependencias sean siempre de afuera hacia adentro, apuntando a módulos más estables en el sistema.

Es fácil de conocer y difícil de hacer Aunque la programación abstracta no es difícil de entender conceptualmente, no siempre es fácil en la implementación real. Qué módulos deben abstraerse, qué dependencias deben revertirse y cuántas capas de abstracción son razonables para introducir en el sistema, no existen respuestas estándar a estas preguntas.

Cuando recibimos un requerimiento y realizamos un diseño modular en él, primero debemos analizar si este módulo puede ser reemplazado con cambios en los requerimientos, o ser modificado y reconstruido a gran escala. Cuando descubrimos que puede haber cambios, debemos encapsular estos cambios y dejar que los módulos que dependen de ellos se basen en estas abstracciones.

Por ejemplo, Localstorge e indexdb en el ejemplo anterior, los programas experimentados pensarán fácilmente que es posible que deban reemplazarse entre sí, por lo que es mejor diseñarlos para que sean abstractos desde el principio.

De la misma manera, nuestra base de datos también puede cambiar. Quizás estemos usando mysql hoy, pero puede ser reemplazado por oracle el próximo año. Entonces nuestras aplicaciones no deberían depender de mysql u oracle, sino dejar que dependan de mysql y oracle. Abstracción pública.

Para otro ejemplo, a menudo usamos ajax en nuestros programas para transmitir datos de entrada del usuario, pero un día es posible que deseemos reemplazar ajax con solicitudes de websocket, por lo que la lógica empresarial central también debe depender de la abstracción común de ajax y websocket.

Cambios de empaque y patrones de diseño

De hecho, los 23 módulos de diseño comunes se resumen desde la perspectiva de los cambios de paquete. Tomemos el modo de creación como ejemplo. Crear un objeto es un comportamiento abstracto, y el objeto específico creado se puede cambiar. El propósito del modo de creación es encapsular los cambios del objeto creado. El modelo estructural encapsula la combinación de objetos. Los patrones de comportamiento encapsulan los cambios de comportamiento de los objetos.

Por ejemplo, el modelo de fábrica, al encapsular los cambios del objeto creado en la fábrica, de modo que el negocio principal no necesite depender de clases de implementación específicas y no necesite comprender demasiados detalles de implementación. Cuando el objeto creado cambia, solo necesitamos cambiar la implementación de la fábrica, sin afectar la lógica central del negocio.

Por ejemplo, el patrón del método del módulo encapsula la secuencia del proceso de ejecución. La subclase hereda la función de plantilla de la clase principal y la ejecuta de acuerdo con las reglas de proceso establecidas por la clase principal. Los detalles de implementación de la función específica son implementados por la propia subclase.

Al encapsular los cambios, las partes estables e inmutables del sistema pueden aislarse de las partes que cambian fácilmente. En la evolución del sistema, solo las partes que son fáciles de cambiar necesitan ser reemplazadas o modificadas, si estas partes ya están encapsuladas, son relativamente fáciles de reemplazar. Esto puede maximizar la estabilidad del programa.

Evite la abstracción excesiva

Aunque la abstracción mejora la escalabilidad y la flexibilidad del programa, la abstracción también introduce una capa adicional de indirección, que aporta complejidad adicional. Originalmente, un módulo depende de otro módulo, esta dependencia es la más simple y directa, pero cada vez que agregamos una capa de abstracción en el medio, significa que siempre debemos prestar atención y mantener esta capa de abstracción. La adición de estas capas abstractas al sistema aumentará inevitablemente el nivel y la complejidad del sistema.

Si juzgamos que algunos módulos son relativamente estables y no cambiarán durante mucho tiempo, entonces no hay necesidad de hacerlos abstractos en primer lugar.

Por ejemplo, la clase String en java es muy estable, por lo que no hay abstracción de String.

Por ejemplo, algunos métodos de herramientas, similares a utils.getCookie (), difícilmente puedo imaginar qué reemplazará a las cookies en 5 años, así que prefiero escribir getCookie directamente.

Por ejemplo, el modelo de datos de Tencent document excel, que pertenece al core en el core, como los huesos y meridianos de todo el cuerpo, se ha integrado en cada lógica de aplicación. La posibilidad de que sea reemplazado es muy pequeña y la dificultad también es muy alta. Escriba un documento de Tencent en Excel, por lo que no es necesario abstraer demasiado el modelo.

Conclusión

Hay dos mayores beneficios de la programación abstracta.

Por un lado, la programación orientada a lo abstracto puede encapsular las partes del sistema que cambian con frecuencia en abstracción para mantener estable el módulo principal.

Por otro lado, la programación orientada a lo abstracto puede liberar a los desarrolladores de módulos centrales de los detalles de implementación de los módulos no centrales y dejar los detalles de implementación de estos módulos no centrales en una etapa posterior o dejarlo en manos de otros.

La discusión real en este artículo se centra principalmente en el primer punto, a saber, los cambios de paquete. Los cambios de empaque son la clave para construir un sistema débilmente acoplado.

Este artículo, como introducción a la programación abstracta, espera ayudar a algunos estudiantes a darse cuenta de los beneficios de la programación abstracta y dominar algunos métodos básicos de programación abstracta.

Supongo que te gusta

Origin blog.csdn.net/Tencent_TEG/article/details/112210677
Recomendado
Clasificación