Escribir componentes reutilizables respetando SOLID

¡Hola a todos! Mi nombre es Roma, soy una interfaz en Ya Tutorial. Hoy te diré cómo evitar la duplicación de código y escribir componentes reutilizables de alta calidad. El artículo fue escrito basado en (¡pero solo por motivos!) Del informe de Y. Subbotnik - hay un video al final de la publicación. Si está interesado en comprender este tema, bienvenido bajo cat.



El artículo contiene un análisis más detallado de los principios y ejemplos detallados de la práctica que no encajan en el informe. Recomiendo leerlo si desea profundizar en el tema y aprender cómo escribimos componentes reutilizables. Si desea familiarizarse con el mundo de los componentes reutilizables en términos generales, entonces, en mi opinión, la grabación del informe es más adecuada para usted.






Todo el mundo sabe que la duplicación de código es mala porque a menudo se habla de ella: en un libro sobre su primer lenguaje de programación, en cursos de programación, en libros sobre cómo escribir código de calidad como Perfect Code y Clean Code.



Veamos por qué es tan difícil evitar la duplicación en la interfaz y cómo escribir componentes reutilizables correctamente. Y los principios de SOLID nos ayudarán.



¿Por qué es difícil dejar de duplicar código?



Parecería que el principio suena simple. Y al mismo tiempo, es fácil verificar si se está respetando: si no hay duplicación en el código base, entonces todo está bien. ¿Por qué es tan difícil en la práctica?



Analicemos un ejemplo con una librería de componentes Ya. Tutorial. Érase una vez, el proyecto fue un monolito. Más tarde, por conveniencia, los desarrolladores decidieron mover los componentes reutilizables a una biblioteca separada. Uno de los primeros en llegar fue un componente de botón. El componente ha evolucionado, con el tiempo, han aparecido nuevas "habilidades" y configuraciones para el botón, el número de personalizaciones visuales ha aumentado. Después de un tiempo, el componente se volvió tan sofisticado que resultó inconveniente usarlo para nuevas tareas y expandirse aún más.



Y así, en la siguiente iteración, apareció una copia del componente: Button2. Esto sucedió hace mucho tiempo, nadie recuerda las razones exactas de la aparición. Sin embargo, se creó el componente.







Parecería que está bien, que haya dos botones. Después de todo, es solo un botón. Sin embargo, en realidad, tener dos botones en un proyecto tenía consecuencias a largo plazo muy desagradables.



Cada vez, cuando era necesario actualizar los estilos, no estaba claro en qué componente hacerlo. Tuve que verificar dónde se usa qué componente para no romper accidentalmente estilos en otros lugares. Cuando apareció una nueva versión de la pantalla de botones, decidimos cuál de los componentes ampliar. Cada vez que veíamos una nueva función, pensábamos en cuál de los botones usar. Y a veces, en un lugar, necesitábamos varios botones diferentes, y luego importábamos dos componentes de botón en un componente de proyecto a la vez.



A pesar de que a la larga la existencia de los dos componentes del botón resultó ser dolorosa, no comprendimos de inmediato la gravedad del problema y logramos hacer algo similar con los íconos. Creamos un componente, y cuando nos dimos cuenta de que no era muy conveniente para nosotros, hicimos Icon2, y cuando resultó inadecuado para nuevas tareas, escribimos Icon3.



Casi todo el conjunto de efectos negativos de la duplicación de botones se repitió en los componentes del icono. Fue un poco más fácil porque los íconos se usan con menos frecuencia en el proyecto. Aunque, para ser honesto, todo depende de la función. Además, tanto para el botón como para el icono, el componente antiguo no se eliminó al crear uno nuevo, porque la eliminación requería mucha refactorización con la posible aparición de errores en todo el proyecto. Entonces, ¿qué tienen en común los casos de botones e íconos? El mismo esquema para la aparición de duplicados en el proyecto. Nos costó reutilizar el componente actual, adaptarlo a las nuevas condiciones, así que creamos uno nuevo.



Al crear un duplicado de un componente, complicamos nuestra vida futura. Queríamos ensamblar una interfaz a partir de bloques prefabricados, como un constructor. Para hacer esto de manera conveniente, necesita componentes de calidad que simplemente pueda tomar y usar. La raíz del problema es que el componente que planeábamos reutilizar estaba escrito incorrectamente. Fue difícil extenderlo y aplicarlo en otros lugares.



Un componente reutilizable debe ser lo suficientemente versátil y simple al mismo tiempo. Trabajar con él no debería causar dolor y parecerse a disparar a un gorrión desde un cañón. Por otro lado, el componente debe ser lo suficientemente personalizable como para que con un pequeño cambio en el script, no quede claro que es más fácil escribir "Component2".



SÓLIDO hacia componentes reutilizables



Para escribir componentes de calidad, necesitamos un conjunto de reglas detrás del acrónimo SOLID. Estas reglas explican cómo combinar funciones y estructuras de datos en clases y cómo las clases deben combinarse entre sí.



¿Por qué exactamente SOLID y no cualquier otro conjunto de principios? Las reglas SOLID le dicen cómo diseñar correctamente su aplicación. Para que puedas desarrollar de forma segura el proyecto, agregar nuevas funciones, cambiar las existentes y al mismo tiempo no romper todo. Cuando traté de describir lo que, en mi opinión, debería ser un buen componente, me di cuenta de que mis criterios se acercan a los principios de SOLID.



  • S es el principio de responsabilidad exclusiva.
  • O - el principio de apertura / cercanía.
  • L es el principio de sustitución de Liskov.
  • I - el principio de separación de interfaces.
  • D - Principio de inversión de dependencia.


Algunos de estos principios funcionan bien para describir componentes. Otros parecen más inverosímiles en un contexto de interfaz. Pero todos juntos describen bien mi visión de un componente de calidad.



Seguiremos los principios fuera de orden, pero de simple a complejo. Primero, veamos las cosas básicas que pueden ser útiles en una gran cantidad de situaciones, y luego, las más poderosas y específicas.



El artículo proporciona ejemplos de código en React + TypeScript. Elegí React como el marco con el que más trabajo. En su lugar puede haber cualquier otro marco que le guste o le convenga. En lugar de TS, puede haber JS puro, pero TypeScript le permite describir explícitamente contratos en código, lo que simplifica el desarrollo y uso de componentes complejos.



Básico



El principio de apertura / cierre



Las entidades de software deben estar abiertas para extensión y cerradas para modificación. En otras palabras, deberíamos poder extender la funcionalidad con un nuevo código sin cambiar el existente. ¿Por qué es importante? Si cada vez que tiene que editar un montón de módulos existentes para agregar una nueva funcionalidad, todo el proyecto se volverá inestable. Habrá muchos lugares que pueden romperse debido al hecho de que se cambian constantemente.



Consideremos la aplicación del principio en el ejemplo de un botón. Hemos creado un componente de botón y tiene estilos. Hasta ahora todo ha ido bien. Pero luego aparece una nueva tarea y resulta que en un lugar específico para este botón, se deben aplicar diferentes estilos.





El botón está escrito de tal manera que no se puede cambiar sin editar el código.



Para aplicar diferentes estilos en la versión actual, deberá editar el componente del botón. El problema es que el componente no se puede personalizar. No consideraremos la opción de escribir estilos globales, ya que no es confiable. Cualquier cosa puede romperse con cualquier edición. Las consecuencias son fáciles de imaginar si coloca algo más complejo en lugar del botón, por ejemplo, un componente selector de fecha.



De acuerdo con el principio de apertura / cierre, debemos escribir el código para que al agregar un nuevo estilo, no tengamos que reescribir el código del botón. Todo saldrá bien si parte de los estilos del componente se pueden lanzar al exterior. Para hacer esto, crearemos un accesorio en el que pasaremos la clase requerida para describir los nuevos estilos del componente.



//    ,    
import cx from 'classnames';

//    — mix
const Button = ({ children, mix }) => {
  return (
    <button
      className={cx("my-button", mix)}
    >
      {children}
    </button>
}

      
      





Listo, ahora no es necesario editar su código para personalizar un componente.







Este método bastante popular le permite personalizar la apariencia de un componente. Se llama mezcla porque la clase adicional se mezcla con las clases propias del componente. Tenga en cuenta que pasar una clase no es la única forma de diseñar un componente desde el exterior. Puede pasar un objeto con propiedades CSS a un componente. Puede utilizar soluciones CSS-in-JS, la esencia no cambiará. Las mezclas son utilizadas por muchas bibliotecas de componentes, por ejemplo: MaterialUI, Vuetify, PrimeNG y otras.



¿Qué conclusión se puede sacar sobre las mezclas? Son fáciles de implementar, versátiles y le permiten personalizar de manera flexible el aspecto de sus componentes con un mínimo esfuerzo.



Pero este enfoque también tiene sus inconvenientes. Permite mucha libertad, lo que puede generar problemas con la especificidad de los selectores. También rompe la encapsulación. Para generar el selector de CSS correcto, necesita conocer la estructura interna del componente. Esto significa que dicho código puede romperse al refactorizar un componente.



Variabilidad de componentes



Un componente tiene partes que son su núcleo. Si los cambiamos, obtenemos un componente diferente. Para un botón, es un conjunto de estados y comportamientos. Los usuarios distinguen un botón de, por ejemplo, una casilla de verificación, gracias a su efecto de desplazamiento y clic. Existe una lógica general de trabajo: cuando el usuario hace clic, se activa el controlador de eventos. Este es el núcleo del componente, lo que hace que un botón sea un botón. Sí, hay excepciones, pero así es como funciona en la mayoría de los casos de uso.



También hay partes en el componente que pueden cambiar según el lugar de uso. Los estilos pertenecen a este grupo. Quizás necesitemos un botón de diferente tamaño o color. Con un trazo y un empalme diferentes, o con un efecto de desplazamiento diferente. Todos los estilos son parte modificable del componente. No queremos reescribir o crear un nuevo componente cada vez que el botón se ve diferente.



Lo que cambia con frecuencia debe modificarse sin cambiar el código. De lo contrario, nos encontraremos en una situación en la que es más fácil crear un nuevo componente que personalizar y agregar uno antiguo, que resultó no ser lo suficientemente flexible.



Tematización



Volvamos a personalizar el componente visual usando el ejemplo de un botón. La siguiente forma es aplicar temas. Por temas, me refiero a la capacidad de un componente de aparecer en múltiples modos, de manera diferente en diferentes lugares. Esta interpretación es más amplia que la tematización en el contexto de temas claros y oscuros.



El uso de temas no excluye el método anterior con mezclas, sino que lo complementa. Decimos explícitamente que un componente tiene varios métodos de visualización y, cuando se usa, requiere que especifique el deseado.



import cx from 'classnames';
import b from 'b_';

const Button = ({ children, mix, theme }) => (
  <button
    className={cx(
     b("my-button", { theme }), mix)}
  >
    {children}
  </button>
)

      
      





La tematización te permite evitar el zoológico de estilos, cuando, por ejemplo, tienes 20 botones en tu proyecto y todos se ven un poco diferentes debido al hecho de que los estilos de cada botón se establecen en el lugar de aplicación. El enfoque se puede aplicar a todos los componentes nuevos sin temor a la sobreingeniería. Si comprende que un componente puede tener un aspecto diferente, es mejor utilizar el tema explícitamente desde el principio. Esto simplificará el desarrollo adicional de componentes.



Pero también hay un inconveniente: el método solo es adecuado para personalizar lo visual y no permite influir en el comportamiento del componente.



Anidamiento de componentes



No he enumerado todas las formas de evitar cambiar el código del componente al agregar nuevas funciones. Otros se demostrarán examinando el resto de los principios. Aquí me gustaría mencionar los componentes secundarios y las ranuras.



La página web es una jerarquía de componentes en forma de árbol. Cada componente decide por sí mismo qué y cómo renderizar. Pero no siempre es así. Por ejemplo, un botón le permite especificar qué contenido se renderizará internamente. En React, la herramienta principal son los accesorios para niños y render. Vue tiene un concepto más poderoso de tragamonedas. No hay problemas al escribir componentes simples utilizando estas capacidades. Pero es importante no olvidar que incluso en componentes complejos, puede utilizar el lanzamiento de algunos de los elementos que el componente debería mostrar desde arriba.



Avanzado



Los principios que se describen a continuación son adecuados para proyectos más grandes. Las técnicas correspondientes dan más flexibilidad, pero aumentan la complejidad del diseño y desarrollo.



Principio de responsabilidad única



El principio de responsabilidad única significa que un módulo debe tener una y solo una razón para cambiar.



¿Por qué es importante? Las consecuencias de violar el principio incluyen:

  • Riesgo de romper otra al editar una parte del sistema.
  • Malas abstracciones. El resultado son componentes que pueden realizar varias funciones, lo que dificulta comprender qué debe hacer exactamente el componente y qué no.
  • Trabajo inconveniente con componentes. Es muy difícil realizar mejoras o corregir errores en un componente que hace todo a la vez.


Volvamos al ejemplo de la tematización y veamos si se respeta el principio de responsabilidad única. Ya en su forma actual, la tematización hace frente a sus tareas, pero esto no significa que la solución no tenga problemas y no se pueda mejorar.





Un módulo es editado por diferentes personas por diferentes razones.



Digamos que ponemos todos los estilos en un archivo css. Puede ser editado por diferentes personas por diferentes motivos. Resulta que se ha violado el principio de responsabilidad exclusiva. Alguien puede refactorizar los estilos y otro desarrollador ajustará la nueva función. Para que puedas romper algo fácilmente.



Echemos un vistazo a cómo se vería la temática compatible con SRP. La imagen perfecta: tenemos un botón y, por separado, un conjunto de temas para ello. Podemos aplicar un tema a un botón y obtener un botón temático. Como beneficio adicional, me gustaría poder ensamblar un botón con varios temas disponibles, por ejemplo, para colocarlo en una biblioteca de componentes.





Pintura deseada. Un tema es una entidad separada y se puede aplicar a un botón. Un



tema envuelve un botón. Este es el enfoque utilizado en Lego, nuestra biblioteca de componentes internos. Usamos HOC (High Order Components) para envolver el componente base y agregarle nuevas características. Por ejemplo, la capacidad de mostrar con un tema.



HOC es una función que toma un componente y devuelve otro componente. Un HOC con un tema puede lanzar un objeto con estilos dentro del botón. A continuación se muestra una opción bastante educativa, en la vida real puede usar soluciones más elegantes, por ejemplo, agregar una clase al componente, cuyos estilos se importan al HOC, o usar soluciones CSS-in-JS.



Un ejemplo de un HOC simple para tematizar un botón:



const withTheme1 = (Button) =>
(props) => {
    return (
        <Button
            {...props}
            styles={theme1Styles}
        />
    )
}

const Theme1Button = withTheme1(Button);
      
      





El HOC solo puede aplicar estilos si se especifica un tema específico; de lo contrario, no hace nada. Entonces podemos ensamblar un botón con un conjunto de temas y activar el que necesitamos especificando el prop del tema.



Usando múltiples HOC para recopilar un botón con los temas deseados:



import "./styles.css";
 
//   .     
const ButtonBase = ({ style, children }) => {
 console.log("styl123e", style);
 return <button style={style}>{children}</button>;
};
 
const withTheme1 = (Button) => (props) => {
 // HOC  ,     "theme1"
 if (props.theme === "theme1") {
   return <Button {...props} style={{ color: "red" }} />;
 }
 
 return <Button {...props} />;
};
 
const withTheme2 = (Button) => (props) => {
 // HOC  ,     "theme2"
 if (props.theme === "theme2") {
   return <Button {...props} style={{ color: "green" }} />;
 }
 
 return <Button {...props} />;
};
 
// -      HOC
const compose = (...hocs) => (BaseComponent) =>
 hocs.reduce((Component, nextHOC) => nextHOC(Component), BaseComponent);
 
//  ,    
const Button = compose(withTheme1, withTheme2)(ButtonBase);
 
export default function App() {
 return (
   <div className="App">
     <Button theme="theme1">"Red"</Button>
     <Button theme="theme2">"Green"</Button>
   </div>
 );
}

      
      





Y aquí llegamos a la conclusión de que debemos dividir las áreas de responsabilidad. Incluso si parece que tiene un componente, piense: ¿es realmente así? Quizás debería dividirse en varias capas, cada una de las cuales será responsable de una función específica. En casi todos los casos, la capa visual se puede desacoplar de la lógica del componente.



La separación de un tema en una entidad separada da ventajas a la usabilidad del componente: puede colocar un botón en una biblioteca con un conjunto básico de temas y permitir que los usuarios escriban los suyos si es necesario; los temas se pueden buscar a tientas entre proyectos. Esto le permite preservar la consistencia de la interfaz y no sobrecargar la biblioteca original.



Existen diferentes opciones para implementar la división en capas. El ejemplo anterior fue con HOC, pero la composición también es posible. Sin embargo, creo que en el caso de la tematización, los HOC son más apropiados, ya que el tema no es un componente independiente.



No es solo lo visual lo que se puede llevar a una capa separada. Pero no planeo considerar en detalle la transferencia de la lógica empresarial al HOC, porque la pregunta es muy holística. Mi opinión es que puede hacer esto si comprende lo que está haciendo y por qué lo necesita.



Componentes compuestos



Pasemos a componentes más complejos. Tomemos Select como ejemplo y veamos cuál es el uso del principio de responsabilidad única. Se puede pensar en Select como una composición de componentes más pequeños.







  • Contenedor: comunicación entre otros componentes.
  • Campo: el texto para la selección habitual y la entrada para el componente CobmoBox, donde el usuario ingresa algo.
  • Icono: un icono tradicional en el campo para seleccionar.
  • El menú es un componente que muestra una lista de elementos para seleccionar.
  • El elemento es un elemento separado del menú.


Para cumplir con el principio de responsabilidad única, debe tomar todas las entidades en componentes separados, dejando a todos con un solo motivo para editar. Cuando cortamos el archivo, surgirá la pregunta: ¿cómo personalizar ahora el conjunto de componentes resultante? Por ejemplo, si necesita establecer un tema oscuro para el campo, amplíe el icono y cambie el color del menú. Hay dos maneras de lograr esto.



Anulaciones



La primera forma es sencilla. Mueva todos los ajustes de los componentes anidados a los accesorios del original. Sin embargo, si aplica la solución "de frente", resulta que la selección tiene una gran cantidad de accesorios, que son difíciles de entender. Es necesario organizarlos de alguna manera convenientemente. Aquí es donde entra en juego la anulación. Esta es una configuración que se reenvía a un componente y le permite personalizar cada uno de sus elementos.



<Select
  ...
  overrides={{
    Field: {
      props: {theme: 'dark'}
    },
    Icon: {
      props: {size: 'big'},
    },
    Menu: {
      style: {backgroundColor: '#CCCCCC'}
    },
  }}
/>

      
      





Di un ejemplo simple en el que anulamos los accesorios. Pero la anulación se puede considerar como una configuración global: configura todo lo que admiten los componentes. Puede ver cómo funciona esto en la práctica en la biblioteca BaseWeb .



Con todo, con la anulación puede personalizar de forma flexible los componentes compuestos, y este enfoque también se adapta bien. Contras: las configuraciones para componentes complejos resultan ser muy grandes y el poder de anulación tiene una desventaja. Tenemos un control total sobre los componentes internos, lo que nos permite hacer cosas extrañas y exponer configuraciones no válidas. Además, si no usa bibliotecas, pero desea implementar el enfoque usted mismo, tendrá que enseñar a los componentes a comprender la configuración o escribir envoltorios que la leerán y configurarán los componentes correctamente.



Principio de inversión de dependencia



Para comprender la alternativa para anular configuraciones, pasemos a la letra D en SOLID. Este es el principio de inversión de dependencia. Argumenta que el código que implementa la política de alto nivel no debería depender del código que implementa los detalles de bajo nivel.



Volvamos a nuestra selección. El contenedor es responsable de la comunicación entre otras partes del componente. De hecho, es la raíz la que controla el renderizado del resto de bloques. Para hacer esto, debe importarlos.



Así es como se verá la raíz de cualquier componente complejo, si no usa la inversión de dependencia:



import InputField from './InputField';
import Icon from './Icon';
import Menu from './Menu';
import Option from './Option';
      
      





Analicemos las dependencias entre los componentes para comprender qué puede salir mal. Ahora, la selección de nivel superior depende del menú de nivel inferior, porque lo importará a sí mismo. El principio de inversión de dependencia está roto. Esto crea problemas.

  • Primero, si cambia el menú, tendrá que editar Seleccionar.
  • En segundo lugar, si queremos utilizar un componente de menú diferente, también tenemos que editar el componente de selección.




No está claro qué hacer cuando necesita Seleccionar con un menú diferente.



Necesita expandir la dependencia. Haga que el componente del menú dependa de la selección. La inversión de dependencias se realiza mediante inyección de dependencias: Select debe aceptar un componente de menú como uno de los parámetros, props. Aquí es donde teclear resulta útil. Indicaremos qué componente espera el Select.



//     Select      
const Select = ({
  Menu: React.ComponentType<IMenu>
}) => {
  return (
    ...
    <Menu>
      ...
    </Menu>
    ...
  )
...
}

      
      





Así es como declaramos que la selección necesita un componente de menú cuyos accesorios satisfagan una determinada interfaz. Luego, las flechas apuntarán en la dirección opuesta, como dicta el principio DI.





La flecha se expande, así es como funciona la inversión de dependencia.



Hemos resuelto el problema de la dependencia, pero un poco de azúcar sintáctico y herramientas auxiliares son bienvenidas aquí.



Cada vez, colocar todas las dependencias en un componente en la ubicación de renderizado es tedioso, pero la biblioteca bem-react tiene un registro de dependencias y un proceso de composición. Con su ayuda, puede empaquetar dependencias y configuraciones una vez, y luego simplemente usar el componente listo para usar.



import { compose } from '@bem-react/core'
import { withRegistry, Registry } from '@bem-react/di'

const selectRegistry = new Registry({ id: cnSelect() })

...

selectRegistry.fill({
    'Trigger': Button,
    'Popup': Popup,
    'Menu': Menu,
    'Icon': Icon,
})

const Select = compose(
    ...
    withRegistry(selectRegistry),
)(SelectDesktop)
      
      





El ejemplo anterior muestra parte del ensamblaje del componente utilizando el ejemplo de bem-react. El código de ejemplo completo y la caja de arena se pueden encontrar en el libro de cuentos de la interfaz de usuario de Yandex .



¿Qué obtenemos al usar la inversión de dependencia?



  • Control total: la libertad de personalizar todos los componentes de un componente.
  • Encapsulación flexible: la capacidad de hacer que los componentes sean muy flexibles y totalmente personalizables. Si es necesario, el desarrollador anulará todos los bloques que componen el componente y obtendrá lo que quiere. En este caso, siempre hay una opción para crear componentes ya configurados y listos para usar.
  • Escalabilidad: este método funciona bien para bibliotecas de cualquier tamaño.


Nosotros en Yandex.Tutorial escribimos nuestros propios componentes usando DI. La biblioteca interna de componentes de Lego también adopta este enfoque. Pero tiene un inconveniente importante: un desarrollo mucho más complejo.



Dificultades para desarrollar componentes reutilizables.



¿Cuál es la dificultad para desarrollar componentes reutilizables?



Primero, un diseño largo y cuidado. Debe comprender de qué partes están hechos los componentes y qué partes pueden cambiar. Si hacemos que todas las partes sean mutables, terminamos con una gran cantidad de abstracciones que son difíciles de entender. Si hay muy pocas piezas intercambiables, el componente no será lo suficientemente flexible. Será necesario mejorarlo para evitar problemas de reutilización en el futuro.



En segundo lugar, los altos requisitos de los componentes. Entiende en qué partes constarán los componentes. Ahora debe escribirlos para que no sepan nada el uno del otro, pero se puedan usar juntos. Es más difícil que desarrollar sin tener en cuenta la reutilización.



En tercer lugar, una estructura compleja como consecuencia de los puntos anteriores. Si necesita una personalización seria, tendrá que reconstruir todas las dependencias del componente. Para hacer esto, necesita comprender profundamente en qué partes consta. Una buena documentación es esencial en el proceso.



El Tutorial tiene una biblioteca de componentes internos donde se encuentran los mecanismos educativos, una parte de la interfaz con la que los niños interactúan mientras resuelven problemas. Y luego hay una biblioteca compartida de servicios educativos. Allí ponemos los componentes que queremos reutilizar entre diferentes servicios.



La transferencia de una mecánica lleva varias semanas, siempre que ya tengamos un componente en funcionamiento y no agreguemos nueva funcionalidad. La mayor parte de este trabajo consiste en dividir el componente en partes independientes y permitir que se compartan.



Principio de sustitución de Liskov



Los principios anteriores trataban de qué hacer y los dos últimos trataban de qué no romper.



Comencemos con el principio de sustitución de Barbara Liskov. Él dice que los objetos en un programa deberían ser reemplazables con instancias de sus subtipos sin romper la ejecución correcta del programa.



No solemos escribir componentes como clases y no usamos herencia. Todos los componentes son intercambiables listos para usar. Esta es la base de la interfaz moderna. La escritura fuerte lo ayuda a evitar errores y mantener la compatibilidad.



¿Cómo se puede romper la reemplazabilidad lista para usar? El componente tiene una API. Por API, me refiero a un conjunto de accesorios para un componente y mecanismos integrados en el marco, como un mecanismo de manejo de eventos. La escritura fuerte y la interferencia en el IDE pueden resaltar la incompatibilidad en la API, pero el componente puede interactuar con el mundo exterior y omitir la API:



  • leer y escribir algo en la tienda global,
  • interactuar con la ventana,
  • interactuar con las cookies,
  • leer / escribir almacenamiento local,
  • realizar solicitudes a la red.






Todo esto es inseguro, porque el componente depende del entorno y puede romperse si lo mueves a otro lugar oa otro proyecto.



Para cumplir con el principio de sustitución de Liskov, necesita:



  • utilizar capacidades de mecanografía,
  • evitar la interacción sin pasar por la API del componente,
  • Evite los efectos secundarios.


¿Cómo evitar la interacción sin API? Coloque todo lo que depende del componente en la API y escriba un contenedor que reenviará los datos del mundo exterior a los accesorios. Por ejemplo, así:

const Component = () => {
   /*
       ,     ,       .
      ,          .
          ,   ,   .
   */
   const {userName} = useStore();
 
   //     ,       ( ,       ).
   const userToken = getFromCookie();
 
   //  —   window      .
   const {taskList} = window.ssrData;
 
   const handleTaskUpdate = () => {
       //    API .      .
       fetch(...)
   }
 
   return <div>{'...'}</div>;
  };
 
/*
          .
      ,         .
*/
const Component2 = ({
   userName, userToken, onTaskUpdate
}) => {
   return <div>{'...'}</div>;
};

      
      





Principio de segregación de interfaz



Muchas interfaces de propósito especial son mejores que una interfaz de propósito general. No pude transferir el principio a los componentes frontales de manera tan inequívoca. Así que lo entiendo como una necesidad de vigilar la API.



Es necesario transferir la menor cantidad de entidades posible al componente y no transferir datos que no sean utilizados por este. Una gran cantidad de accesorios en un componente es motivo de desconfianza. Lo más probable es que viole los principios SOLID.



¿Dónde y cómo reutilizamos?



Hemos analizado los principios que le ayudarán a escribir componentes de calidad. Ahora veamos dónde y cómo los reutilizamos. Esto le ayudará a comprender qué otros problemas puede encontrar.



El contexto puede ser diferente: necesita usar un componente en otra parte de la misma página o, por ejemplo, desea reutilizarlo en otros proyectos de la empresa; son cosas completamente diferentes. Destaco varias opciones:



Aún no se requiere reutilización.Ha escrito un componente, cree que es específico y no planea usarlo en ningún otro lugar. No es necesario realizar esfuerzos adicionales. Y puedes seguir unos sencillos pasos que te serán útiles si aún quieres volver a él. Entonces, por ejemplo, puede verificar que el componente no esté demasiado vinculado al entorno y que las dependencias estén empaquetadas. También puede hacer una reserva para la personalización para el futuro: agregar temas o la capacidad de cambiar la apariencia de un componente desde el exterior (como en el ejemplo con un botón); no lleva mucho tiempo.



Reutilizar en el mismo proyecto.Ha escrito un componente y está bastante seguro de que querrá reutilizarlo en otro lugar de su proyecto actual. Todo lo escrito arriba es relevante aquí. Solo que ahora es imperativo eliminar todas las dependencias en contenedores externos y es muy deseable poder personalizar desde el exterior (temas o mezclas). Si un componente contiene mucha lógica, debe pensar si es necesario en todas partes o si debe modificarse en algunas partes. Para la segunda opción, considere la posibilidad de personalización. También es importante aquí pensar en la estructura del componente y dividirlo en partes si es necesario.



Reutilizar en una pila similar.Entiende que el componente será útil en un proyecto vecino que tenga la misma pila que usted. Aquí es donde todo lo anterior se vuelve obligatorio. Además, le aconsejo que supervise de cerca las dependencias y tecnologías. ¿El proyecto vecino utiliza exactamente las mismas versiones de las bibliotecas que usted? ¿SASS y TypeScript usan la misma versión?



También me gustaría destacar la reutilización en otro entorno de ejecución , por ejemplo, en SSR. Decida si su componente puede y debe poder procesarse en SSR. Si es así, asegúrese de que se muestre como se esperaba de antemano. Recuerde que existen otros tiempos de ejecución como deno o GraalVM. Considere sus características si las usa.



Bibliotecas de componentes



Si es necesario reutilizar componentes entre varios repositorios y / o proyectos, deben trasladarse a la biblioteca.



Apilar



Cuantas más tecnologías se utilicen en los proyectos, más difícil será resolver los problemas de compatibilidad. Es mejor reducir el zoológico y minimizar la cantidad de tecnologías utilizadas: marcos, lenguajes, versiones de grandes bibliotecas. Si comprende que realmente necesita mucha tecnología, tendrá que aprender a vivir con ella. Por ejemplo, puede usar envoltorios sobre componentes web, recopilar todo en JS puro o usar adaptadores para componentes.



El tamaño



Si el uso de un componente simple de su biblioteca agrega un par de megabytes al paquete, no está bien. Dichos componentes no deben reutilizarse, porque la perspectiva de escribir su propia versión ligera desde cero parece justificada. Puede resolver el problema utilizando herramientas de control de tamaño, por ejemplo, límite de tamaño.



No se olvide de la modularidad: un desarrollador que quiera usar su componente debería poder tomar solo él y no arrastrar todo el código de la biblioteca al proyecto.



Es importante que la biblioteca modular no se compile en un solo archivo. También debe realizar un seguimiento de la versión de JS a la que va la biblioteca. Si crea la biblioteca en ES.NEXT y los proyectos en ES5, habrá problemas. También debe configurar correctamente el ensamblado para versiones anteriores de navegadores y asegurarse de que todos los usuarios de la biblioteca sepan en qué se trata. Si esto es demasiado complicado, existe una alternativa: configurar sus propias reglas de construcción de bibliotecas en cada proyecto.



Actualizar



Piense de antemano en cómo actualizará la biblioteca. Es bueno si conoce a todos los clientes y sus scripts personalizados. Esto le ayudará a pensar mejor sobre las migraciones y los cambios importantes. Por ejemplo, a un equipo que utilice su biblioteca le resultará extremadamente frustrante conocer una actualización importante con cambios importantes antes del lanzamiento.



Al mover componentes a una biblioteca que está usando otra persona, pierde la facilidad de refactorización. Para evitar que la carga de la refactorización se vuelva abrumadora, le aconsejo que no arrastre nuevos componentes a las bibliotecas. Es probable que cambien, lo que significa que tendrá que dedicar mucho tiempo a actualizar y mantener la compatibilidad.



Diseño y personalización



El diseño no afecta la reutilización, pero es una parte importante de la personalización. En nuestro Tutorial, los componentes no viven por sí mismos, su apariencia está diseñada por diseñadores. Los diseñadores tienen un sistema de diseño. Si un componente se ve diferente en el sistema y en el repositorio, los problemas no se pueden evitar. Los diseñadores y desarrolladores no tienen las mismas ideas sobre la apariencia de la interfaz, lo que puede conducir a malas decisiones.



Escaparate de componentes



El escaparate de componentes ayudará a simplificar la interacción con los diseñadores. Una de las soluciones de escaparate más populares es Storybook . Con esta u otra herramienta adecuada, puede mostrar los componentes del proyecto a cualquier persona ajena al desarrollo.



Agregue interactividad al escaparate: los diseñadores deben poder interactuar con los componentes y ver cómo se muestran y funcionan con diferentes parámetros.



No olvide configurar el escaparate para que se actualice automáticamente cuando actualice componentes. Para hacer esto, necesita mover el proceso a CI. Ahora los diseñadores siempre pueden ver qué componentes prefabricados hay en el proyecto y usarlos.







Sistema de diseño



Para un desarrollador, un sistema de diseño es un conjunto de reglas que gobiernan la apariencia de los componentes en un proyecto. Para evitar que el zoológico de componentes crezca, puede limitar la personalización a sus límites.



Otro punto importante es que el sistema de diseño y el tipo de componentes del proyecto a veces difieren entre sí. Por ejemplo, cuando hay un gran rediseño y no todo se puede actualizar en el código, o hay que ajustar un componente, pero no hay tiempo para hacer cambios en el sistema de diseño. En estos casos, le conviene tanto a usted como a los diseñadores sincronizar el sistema de diseño y el proyecto tan pronto como surja la oportunidad.



El último consejo universal y obvio: comunicar y negociar. No es necesario percibir a los diseñadores como aquellos que se mantienen al margen del desarrollo y solo crean y editan diseños. Trabajar en estrecha colaboración con ellos lo ayudará a diseñar e implementar una interfaz de calidad. En última instancia, esto beneficiará a la causa común y deleitará a los usuarios del producto.



conclusiones



El código duplicado genera dificultades en el desarrollo y una disminución en la calidad de la interfaz. Para evitar las consecuencias, debe controlar la calidad de los componentes y los principios de SOLID ayudan a escribir componentes de calidad.



Es mucho más difícil escribir un buen componente con una reserva para el futuro que uno que resuelva rápidamente el problema aquí y ahora. Al mismo tiempo, los buenos "ladrillos" son solo una parte de la solución. Si trae componentes a la biblioteca, debe trabajar con ellos de manera conveniente y también deben estar sincronizados con el sistema de diseño.



Como puede ver, la tarea no es fácil. Es difícil y requiere mucho tiempo desarrollar componentes reutilizables de alta calidad. ¿Vale la pena? Creo que todos responderán esta pregunta por sí mismos. Para proyectos pequeños, los gastos generales pueden ser demasiado altos. Para proyectos donde no se planifica el desarrollo a largo plazo, invertir esfuerzos en la reutilización del código también es una decisión controvertida. Sin embargo, habiendo dicho "no necesitamos esto ahora", es fácil pasar por alto cómo termina en una situación en la que la falta de componentes reutilizables traerá muchos problemas que podrían no haber sucedido. ¡Así que no repitas nuestros errores y no te repitas a ti mismo!



Mira el informe



All Articles