
Inversion of Control es un principio de programación bastante fácil de entender que, al mismo tiempo, puede mejorar significativamente su código. Este artículo demostrará cómo aplicar la Inversión de control en JavaScript y en Reactjs.
Si ya ha escrito código que se usa en más de un lugar, entonces está familiarizado con esta situación:
- Crea un fragmento de código reutilizable (puede ser una función, un componente React, un React hook, etc.) y lo comparte (para colaboración o publicación en código abierto).
- Alguien le pide que agregue nuevas funcionalidades. Su código no admite la funcionalidad propuesta, pero podría hacerlo si realizara un pequeño cambio.
- Agrega un nuevo argumento / prop / opción a su código y su lógica asociada para mantener esta nueva función funcionando.
- Repita los pasos 2 y 3 varias veces (o muchas, muchas veces).
- Ahora su código reutilizable es difícil de usar y mantener.
¿Qué hace exactamente que el código sea una pesadilla para usar y mantener? Hay varios aspectos que pueden hacer que su código sea problemático:
- Tamaño y / o rendimiento del paquete: solo más código para ejecutar en dispositivos puede conducir a un bajo rendimiento. A veces, esto puede hacer que las personas simplemente se nieguen a usar su código.
- Difícil de mantener: Anteriormente, su código reutilizable tenía solo unas pocas opciones, y estaba enfocado en hacer una cosa bien, pero ahora puede hacer un montón de cosas diferentes, y debe documentarlo todo. Además, las personas comenzarán a hacerle preguntas sobre cómo usar su código para ciertos casos de uso que pueden o no ser comparables con los casos de uso para los que ya ha agregado soporte. Incluso puede tener dos casos de uso casi idénticos que son ligeramente diferentes, por lo que deberá responder preguntas sobre qué es lo mejor para usar en una situación dada.
- Complejidad de implementación : cada vez que esto no es solo otra
if
, cada rama de la lógica de su código coexiste con las ramas existentes de la lógica. De hecho, las situaciones son posibles cuando intenta mantener una combinación de argumentos / opciones / accesorios que nadie usa, pero aún necesita considerar cualquier opción posible, ya que no sabe exactamente si alguien usará o no estas combinaciones. - API sofisticada : cada nuevo argumento / opción / accesorio que agregue a su código reutilizable hace que sea difícil de usar, porque ahora tiene un enorme archivo README o un sitio donde se documenta toda la funcionalidad disponible, y las personas tienen que aprender todo esto para un uso efectivo tu código Usarlo no es conveniente, porque la complejidad de su API penetra el código del desarrollador que lo usa, lo que complica su código.
Como resultado, todos sufren. Vale la pena señalar que la implementación del programa final es una parte esencial del desarrollo. Pero sería genial si pensáramos más en la implementación de nuestras abstracciones (lea sobre "programación AHA" ). ¿Hay alguna manera de que podamos reducir los problemas con el código reutilizable y aun así cosechar los beneficios del uso de abstracciones?
Inversión de control
La inversión del control es un principio que realmente simplifica la creación y el uso de abstracciones. Esto es lo que Wikipedia dice al respecto:
... en la programación tradicional, el código de usuario que expresa el propósito del programa se llama en bibliotecas reutilizables para resolver problemas comunes, pero con la inversión de control, es un entorno que llama a código personalizado o específico de la tarea,
Piénselo de esta manera: "Reduzca la funcionalidad de su abstracción y haga que sus usuarios puedan implementar la funcionalidad que necesitan". Esto puede parecer un completo absurdo, ya que utilizamos abstracciones para ocultar tareas complejas y repetitivas y, por lo tanto, hacer que nuestro código sea más "limpio" y "ordenado". Pero, como hemos visto anteriormente, las abstracciones tradicionales no siempre simplifican el código.
¿Qué es la inversión de gestión en el código?
Para comenzar, aquí hay un ejemplo muy artificial:
Ahora juguemos un típico "ciclo de vida de la abstracción" agregando nuevos casos de uso a esta abstracción y "mejorando sin pensar" para admitir estos nuevos casos de uso:
Por lo tanto, nuestro programa funciona con solo seis casos de uso, pero de hecho admitimos cualquier combinación posible de funciones, y hay hasta 25 combinaciones de este tipo (si calculé correctamente).
En general, esta es una abstracción bastante simple. Pero se puede simplificar. A menudo sucede que la abstracción en la que se agregó la nueva funcionalidad podría simplificarse en gran medida para los casos de uso que realmente admite. Desafortunadamente, tan pronto como la abstracción comience a admitir algo (por ejemplo, ejecutar { filterZero: true, filterUndefined: false }
), tenemos miedo de eliminar esta funcionalidad debido a que puede romper el código que se basa en ella.
Incluso escribimos pruebas para casos de uso que en realidad no tenemos, simplemente porque nuestra abstracción admite estos escenarios, y es posible que tengamos que hacer esto en el futuro. Y cuando estos u otros casos de uso se vuelven innecesarios para nosotros, no eliminamos su apoyo, ya que simplemente lo olvidamos, o pensamos que puede ser útil para nosotros en el futuro, o simplemente tenemos miedo de romper algo.
Bien, ahora escribamos una abstracción más elaborada para esta función y apliquemos el método de inversión de control para admitir todos los casos de uso que necesitamos:
Genial Resultó mucho más fácil. Acabamos de pasar el control sobre la función, pasando la responsabilidad de decidir qué elemento pertenece a la nueva matriz, desde la función de filter
a la función que llama a la función de filtro. Tenga en cuenta que la función de filter
sigue siendo una abstracción útil en sí misma, pero ahora es mucho más flexible.
¿Pero era tan mala la versión anterior de esta abstracción? Probablemente no. Pero desde que cambiamos el control, ahora podemos admitir casos de uso mucho más únicos:
filter( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], animal => animal.legs === 0, )
¿Imagínese si necesita agregar soporte para este caso de uso sin aplicar la inversión de control? Sí, eso sería simplemente ridículo.
API mala?
Una de las quejas más comunes que escucho de las personas con respecto a las API que usan la inversión de control es: "Sí, pero ahora es más difícil de usar que antes". Toma este ejemplo:
Sí, una de las opciones es claramente más fácil de usar que la otra. Pero uno de los beneficios de la inversión de control es que puede usar una API que usa la inversión de control para reimplementar su antigua API. Esto suele ser bastante simple. Por ejemplo:
function filterWithOptions( array, { filterNull = true, filterUndefined = true, filterZero = false, filterEmptyString = false, } = {}, ) { return filter( array, element => !( (filterNull && element === null) || (filterUndefined && element === undefined) || (filterZero && element === 0) || (filterEmptyString && element === '') ), ) }
Genial, ¿eh? De esta manera, podemos crear abstracciones en la parte superior de la API en la que se aplica la inversión de control, y así crear una API más simple. Y si nuestra API "más simple" no tiene suficientes casos de uso, nuestros usuarios pueden aplicar los mismos componentes básicos que utilizamos para crear nuestra API de alto nivel para desarrollar soluciones para tareas más complejas. No necesitan pedirnos que agreguemos una nueva función para filterWithOptions
y esperar a que se implemente. Ya tienen herramientas con las que pueden desarrollar independientemente la funcionalidad adicional que necesitan.
Y, solo para el fan:
function filterByLegCount(array, legCount) { return filter(array, animal => animal.legs === legCount) } filterByLegCount( [ {name: 'dog', legs: 4, mammal: true}, {name: 'dolphin', legs: 0, mammal: true}, {name: 'eagle', legs: 2, mammal: false}, {name: 'elephant', legs: 4, mammal: true}, {name: 'robin', legs: 2, mammal: false}, {name: 'cat', legs: 4, mammal: true}, {name: 'salmon', legs: 0, mammal: false}, ], 0, )
Puede crear una funcionalidad especial para cualquier situación que a menudo ocurra con usted.
Ejemplos de la vida real
Entonces, esto funciona en casos simples, pero ¿es este concepto adecuado para la vida real? Bueno, lo más probable es que estés usando constantemente la inversión de control. Por ejemplo, la función Array.prototype.filter
aplica control inverso. Como la función Array.prototype.map
.
Hay varios patrones con los que ya puede estar familiarizado, y que son solo una forma de inversión de control.
Aquí hay dos de mis patrones favoritos que muestran que estos son "Componentes Compuestos" y "Reductores de Estado" . A continuación se presentan breves ejemplos de cómo se pueden aplicar estos patrones.
Componentes compuestos
Suponga que desea crear un componente de Menu
que tenga un botón para abrir un menú y una lista de elementos de menú que se mostrarán cuando haga clic en el botón. Luego, cuando se selecciona el elemento, realizará alguna acción. Por lo general, para implementar esto, simplemente crean accesorios:
function App() { return ( <Menu buttonContents={ <> Actions <span aria-hidden>▾</span> </> } items={[ {contents: 'Download', onSelect: () => alert('Download')}, {contents: 'Create a Copy', onSelect: () => alert('Create a Copy')}, {contents: 'Delete', onSelect: () => alert('Delete')}, ]} /> ) }
Esto nos permite configurar muchas cosas en los elementos del menú. Pero, ¿qué sucede si queremos insertar una línea antes del elemento de menú Eliminar? ¿Deberíamos agregar una opción especial a los objetos relacionados con los items
? Bueno, no sé, por ejemplo: precedeWithLine
? Más o menos idea.
Tal vez cree un tipo especial de elemento de menú, por ejemplo {contents: <hr />}
. Creo que funcionaría, pero luego tendríamos que manejar casos donde no hay onSelect
. Y para ser honesto, esta es una API muy incómoda.
Cuando piense en cómo crear una buena API para las personas que intentan hacer algo un poco diferente, en lugar de alcanzar la declaración if
, intente invertir el control. ¿Qué pasa si transferimos la responsabilidad de la visualización del menú al usuario? Usamos uno de los puntos fuertes de la reacción:
function App() { return ( <Menu> <MenuButton> Actions <span aria-hidden>▾</span> </MenuButton> <MenuList> <MenuItem onSelect={() => alert('Download')}>Download</MenuItem> <MenuItem onSelect={() => alert('Copy')}>Create a Copy</MenuItem> <MenuItem onSelect={() => alert('Delete')}>Delete</MenuItem> </MenuList> </Menu> ) }
Un punto importante a tener en cuenta es que no hay estado de los componentes visibles para el usuario. El estado se comparte implícitamente entre estos componentes. Este es el valor central del patrón de componentes. Aprovechando esta oportunidad, le dimos cierto control sobre el renderizado al usuario de nuestros componentes, y ahora agregar una línea adicional (o algo más) es una acción simple e intuitiva. Sin documentación adicional, sin funciones adicionales, sin código adicional o pruebas. Todos ganan.
Puedes leer más sobre este patrón aquí . Gracias a Ryan Florence , quien me enseñó esto.
Reductor de estado
Se me ocurrió este patrón para resolver el problema de establecer la lógica del componente. Puede leer más sobre esa situación en mi blog , The State Reducer Pattern , pero el punto principal es que tengo una biblioteca de búsqueda / autocompletar / escribir llamada Downshift
, y uno de los usuarios de la biblioteca estaba desarrollando una versión de componentes de opción múltiple. , por lo que quería que el menú permaneciera abierto incluso después de seleccionar un elemento.
La lógica detrás de Downshift
sugirió que después de que se hizo la elección, el menú debería cerrarse. Un usuario de la biblioteca que necesitaba cambiar su funcionalidad sugirió agregar prop closeOnSelection
. Rechacé esta oferta, ya que una vez ya había recorrido el camino que conducía a un prolapso , y quería evitarlo.
En cambio, hice la API para que los propios usuarios pudieran controlar cómo ocurren los cambios de estado. Piense en el reductor de estado como el estado de una función que se llama cada vez que cambia el estado del componente, y le da al desarrollador de la aplicación la capacidad de influir en el cambio de estado que está por suceder.
Un ejemplo de uso de la biblioteca Downshift
para que no cierre el menú después de que el usuario haga clic en el elemento seleccionado:
function stateReducer(state, changes) { switch (changes.type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes,
Después de agregar este accesorio, comenzamos a recibir MUCHAS menos solicitudes para agregar nuevas configuraciones para este componente. El componente se ha vuelto más flexible y se ha vuelto más fácil para los desarrolladores configurarlo según lo necesiten.
Render Props
Vale la pena mencionar es el patrón "render props" . Este patrón es un ejemplo ideal del uso de la inversión de control, pero especialmente no lo necesitamos. Lea más sobre esto aquí: por qué ya no necesitamos Render Props .
Advertencia
La inversión del control es una excelente manera de sortear el problema de la idea errónea acerca de cómo se usará su código en el futuro. Pero antes de terminar, me gustaría darle algunos consejos.
Volvamos a nuestro ejemplo descabellado:
¿Qué pasa si esto es todo lo que necesitamos de la función de filter
? ¿Y nunca nos enfrentamos a una situación en la que necesitaríamos filtrar algo excepto null
e undefined
? En este caso, agregar la inversión de control para un solo caso de uso simplemente complicaría el código y no aportaría muchos beneficios.
¡Al igual que con cualquier abstracción, tenga cuidado de aplicar el principio de la programación de la AHA y evitar abstracciones apresuradas!
Conclusiones
Espero que el artículo te haya sido útil. Mostré cómo puedes aplicar el concepto de Inversión de Control en una reacción. Este concepto, por supuesto, se aplica no solo a React (como vimos con la función de filter
). La próxima vez que note que está agregando otra if
a la función coreBusinessLogic
de su aplicación, piense en cómo puede invertir el control y transferir la lógica a donde se usa (o, si se usa en varios lugares, puede crear una abstracción más especializada para esto). caso específico).
Si lo desea, puede jugar con un ejemplo de un artículo en CodeSandbox .
¡Buena suerte y gracias por su atención!
PS. Si te ha gustado este artículo, puedes disfrutar esta charla: youtube Kent C Dodds - Simply React