Open/Close Principle (OCP)
¿Qué es el Open/Close Principle? ¿Cómo se aplica en React?
En este artículo vamos a ver qué es el Open/Close Principle (OCP) que se corresponde con la O de SOLID. Este principio lo que viene a decir es que las entidades que tengamos en nuestro código (en el caso de React, entenderemos por entidades los componentes que formarán nuestras aplicaciones) tienen que estar abiertas para extender pero cerradas para ser modificadas.
Pero ¿qué es esto de estar abierto para extensión pero cerrado para modificación? Pues centrándonos en React vamos a verlo con un ejemplo y para ello pensemos en que tenemos un componente que ha de renderizar algo diferente de los que estaba haciendo hasta ahora el OCP lo que nos viene a indicar es que en lugar de tocar dentro del código del componente (lo cual podría provocar que cosas que están funcionando dejen de funcionar) lo que tenemos que hacer es que, de alguna forma, desde fuera del componente seamos capaces de extender la funcionalidad que tiene ese componente.
En lugar de modificar el código que tenemos en nuestro componente para añadir (extender su funcionalidad) lo que tenemos que lograr es que esta extensión se pueda llevar a cabo desde fuera sin necesidad de modificarlo desde dentro.
Esta idea, en el caso de React, se hace mucho con los children
puesto que su utilización es un caso clarísimo de extensión de un componente puesto que cuando estamos trabajando con un children
(pasándoselo como props) lo que estamos haciendo es que, independientemente de lo que el componente haga en sí mismo dentro, el children
que le estamos pasando si ya sabemos que va a contener lo que se renderizará lo estaremos extendiendo desde fuera (sea lo que sea que estemos pasando desde fuera del componente en el children
).
Vamos a intentar verlo con un ejemplo para que las cosas queden más claras. Supongamos que tenemos dos componente de React que tratan de renderizar un botón donde uno de ellos acepta recibir children
para poder renderizar el contenido que va dentro del botón y otro simplemente acepta un prop a la que llamamos title
donde podemos establecer el texto que se renderizará en el botón. Es decir, que tendríamos algo como lo siguiente:
<Button1>{children}</Button1>
<Button2 title={title} />
¿Cuál es aquí el problema? Pues que el componente Button1
por defecto es extensible y el Button2
no lo es puesto que para poderlo extender tenemos que modificar el componente por dentro llevándonos a hacer cosas como que, para que pueda permitir tener un icono tendríamos que definir una prop para ello, establecer el código que se encargue de renderizar este icono, etc.
<Button2 title={title} icon={icon} />
En el caso de Button1
esto no sería un problema puesto que si queremos añadir un icono a nuestro botón simplemente se lo podríamos pasar como parte del children
que recibe sin que tengamos que tocar en ningún momento el componente Button1
:
<Button1>
<Icon />
Click aquí
</Button1>
Como podemos ver hemos podido extender la funcionalidad de Button1
sin tener que tocar en ningún momento el código del mismo lo cual significa que en ningún momento hemos roto el contrato de este componente el cual es más que probable que se esté usando en un montón de sitios.
Nota: en el caso de React el uso de este principio en el uso de los
children
es una ejemplo más que claro del mismo pero tenemos que pensar que lo podemos llevar a más sitios.
Ejemplo
Vamos a ver un ejemplo más elaborado de OCP para lo cual vamos a partir del siguiente código:
type Props = {
title: string;
type: 'default' | 'withLinkButton' | 'withNormalButton';
href?: string;
buttonText?: string;
onClick?: () => void;
}
export const Title: React.FC<Props> = ({
title,
type,
href,
buttonText,
onClick
}) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h1>{title}</h1>
{ type === 'withLinkButton && (
<div>
<a href={href}>{buttonText}</a>
</div>
)}
{ type === 'withNormalButton' && (
<button onClick={onClick}>{buttonText}</button>
)}
</div>
)
En el código anterior se está definiendo el componete Title
el cual recibe una serie de props que están definidas en el tipo Props
. Ahora si nos fijamos en el código JSX del componente vemos que si la props type
tiene el valor withLinkButton
lo que hace es renderizar un <div>
el cual tiene a su vez una etiqueta <a>
y si el type
es withNormalButton
lo único que estaremos haciendo es mostrar un botón.
Nota: una situación como la que acabamos de describir también es bastante común cuando estamos trabajando con los paths de las rutas que tenemos definidas en nuestras aplicaciones. Así, tendríamos un código en el que se consultaría cuál es este path y en función del mismo renderizaríamos un componente u otro.
¿Esto es correcto? Pues en principio no. Si pensamos en el ejemplo que de las rutas que acabamos de describir en la nota anterior estará claro puesto que cada vez que añadamos una nueva ruta a nuestra aplicación deberíamos modificar el componente donde se definen los componentes que a su vez se renderizarán cuando estemos en dicha ruta y como hemos visto esto violará el principio OCP.
En el ejemplo que estamos siguiendo esto también pasará puesto ¿qué sucederá en el caso de que posteriormente añadamos un nuevo valor al tipo type
? Por ejemplo, si añadimos el tipo withIconButton
esto va a suponer que tengamos que modificar el código JSX del componente Title
para que recoja las modificaciones asociadas a este nuevo tipo de botón.
En ambos casos estamos diciendo que vamos a tener que modificar el componente y por lo tanto no estaremos cumpliendo el principio OCP porque recordemos que este decía que una entidad de software (un componente en el caso de React) deberá ser abierto a extensión pero cerrado a modificación.
¿Cómo podemos arreglarlo?
¿Qué podemos hacer para que nuestro componente Title
si que cumpla con el principio OCP? Pues la solución pasa en que vamos a tener que separar el código en varios componentes y nos vamos a basar en la estrategia que hemos explicado cuando hemos estado hablando de los children
. Así definiremos:
type TitleProps = {
children: React.ReactNode
title: string
}
export const Title: React.FC<TitleProps> = ({ title, children }) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h1>{title}</h1>
{children}
</div>
)
Lo que hemos hecho en el código anterior es quedarnos en el código JSX con totas aquellas partes que NO dependen de la prop type
y por lo tanto se renderizarán siempre con independencia del tipo del título que se vaya a crear. Además estamos haciendo uso de children
lo que supondrá que se le pasará el contendido que se quiere renderizar como parte de la utilización del componente. Dicho de otra manera, la parte del componente Title
que queremos que se pueda extender (es decir, el contenido que se renderizará como parte del título) lo definiremos con la props children
.
Siguiendo con nuestra estrategia de refactorización lo que vamos a hacer es crear un componente por cada uno de los tipos de botones que se pueden renderizar empezaremos definiendo el componente TitleWithLink
el cual recibirá como props únicamente aquellas que serán necesarias para poder renderizar dicho componente:
type TitleWithLinkProps = {
buttonText: string
href: string
title: string
}
export const TitleWithLink: React.FC<TitleWithLinkProps> = ({
buttonText
href,
title,
}) => (
<Title title={title}>
<div>
<a href={href}>{buttonText}</a>
</div>
</Title>
)
La estrategia de refactorización es más o menos clara puesto que lo que estamos haciendo es utilizar el componente Title
que hemos refactorizado anteriormente y definimos como children
el código JSX que se deberá mostrar en el caso de que vayamos a mostrar un botón con un enlace.
Esta misma estrategia es la que vamos a seguir a la hora de definir el componente TitleWithButton
pero teniendo en cuenta únicamente las props que serán necesarias cuando estamos renderizando un Title
que ha de contener dentro de un <button>
:
type TitleWithButton = {
buttonText: string
onClick: () => void
title: string
}
export const TitleWithButton: Reac.FC<TitleWithButtonProps> = ({
buttonText,
onClick,
title
}) => (
<Title title={title}>
<button onClick={onClick}>{buttonText}</button>
</Title>
)
de esta manera jugando con el valor que le asignaremos a esta prop podemos definir el TitleWithLink
y el TileWithButton
.
Nota: Con este refactor además hemos visto que podemos evitar pasarle a un componente (en el ejemplo
Title
) que inicialmente no necesitaba puesto eran propias del tipo de botón que posteriormente iba a ser renderizado.
¿Cómo utilizaremos ahora nuestro código?
Ahora, tras la refactorización, lo que podemos hacer es utilizar el componente TitleWithLink
o TitleWithButton
directamente en nuestro código JSX lo que además nos garantizará que tengamos que proporcionarle las props que espera recibir para su correcto funcionamiento. Así podríamos escribir directamente algo como:
<TitleWithButton
buttonText="Click me"
onClick={() => {}}
title="Open Close Principle"
/>
Extender Title
Ahora supongamos que tenemos el componente Title
en varios tipos de la aplicación y nos damos cuenta de que es necesario incorporar un nuevo tipo simplemente lo que haríamos sería definirlo y en el código JSX del mismo utilizar el componente Title
más o menos como sigue:
export const TitleSpecial = ({ ... }) => (
<Title ...>
// Aquí el JSX propio de este tipo de Title.
</Title>
)
De esta manera no estaremos rompiendo en ningún momento el componente Title
(lo que viene a verificar que este componente está cerrado para modificación) pero lo podemos utilizar fácilmente para crear nuevos tipos de títulos (lo que verifica que está abierto para extensión).
Componentes Compuesto (Compound Component Pattern)
Para poder evitar el tener que estar pasando tantas props a los componentes que hemos definido también habríamos podido usar el patrón Componente Compuesto (Compound Component Pattern). Aunque no vamos a entrar en detalle en cómo funciona este patrón puesto que es algo que se escapa de lo que queremos abordar en el este artículo la idea sería que en vez de utilizar el children
de Title
para lograr la extensión lo que podríamos hacer sería algo como:
<Title title="Open Close Principle" buttonText="Click me">
<Title.WithButton onClick={() => {}} />
</Title>
Lo malo al usar este patrón es que en última instancia nos puede traer otros problemas porque, por ejemplo, sin que hagamos nada más, no hay nada que nos impida que en nuestro Title
no podamos poder dos, tres, etc. Title.WithButton
:
<Title title="Open Close Principle" buttonText="Click me">
<Title.WithButton onClick={() => {}} />
<Title.WithButton onClick={() => {}} />
</Title>
Código completo
Tras todos los refactors que hemos descrito el código completo para nuestro componente Title
original que sí que cumplirá con el OCP es el que podemos ver a continuación:
type TitleProps = {
children: React.ReactNode
title: string
}
const Title: React.FC<TitleProps> = ({ title, children }) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h1>{title}</h1>
{children}
</div>
)
type TitleWithLinkProps = {
buttonText: string
href: string
title: string
}
export const TitleWithLink: React.FC<TitleWithLinkProps> = ({
buttonText
href,
title,
}) => (
<Title title={title}>
<div>
<a href={href}>{buttonText}</a>
</div>
</Title>
)
type TitleWithButton = {
buttonText: string
onClick: () => void
title: string
}
export const TitleWithButton: Reac.FC<TitleWithButtonProps> = ({
buttonText,
onClick,
title
}) => (
<Title title={title}>
<button onClick={onClick}>{buttonText}</button>
</Title>
)
Otro ejemplo (esta vez con rutas)
Aunque no vamos a entrar en su estudio en profundidad vamos a ver otro ejemplo en el que no se está cumpliendo el OCP y como posteriormente se refactorizará el código para que sí que lo cumpla. En concreto vamos a centrarnos en el otro escenario que hemos explicado anteriormente en el que se está haciendo uso del path de una ruta para renderizar un componente u otro:
const Header = () => {
const { pathname } = useRouter()
return (
<header>
<Logo />
<Actions>
{pathname === '/dashboard' && (
<Link to="/events/new">Create event</Link>
)}
{pathname === '/' && <Link to="/dashboard">Go to dashboard</Link>}
</Actions>
</header>
)
}
const HomePage = () => (
<>
<Header />
<OtherHomeStuff />
</>
)
const DashboardPage = () => (
<>
<Header />
<OtherDashboardStuff />
</>
)
¿Qué es lo que tenemos aquí? Pues centrándonos en lo que nos interesa podemos ver que en función del valor que tiene asignado pathname
se estará renderizando un componente Link
o toro que sabemos que viola el OCP porque cada vez que añadamos un nuevo pathname vamos a tener que modificar el componente Header
.
Para realizar la refactorización vamos a tener que apoyarnos una vez en el uso de children
pero además sabiendo que el componente HomePage
estará asociado con el pathname /
y que el componente DashboardPage
estará asociado con el pathname /dashboard
. Esto nos dejará una refactorización como la siguiente:
const Header = ({ children }) => (
<header>
<Logo />
<Actions>{children}</Actions>
</header>
)
const HomePage = () => (
<>
<Header>
<Link to="/dashboard">Go to dashboard</Link>
</Header>
<OtherHomeStuff />
</>
)
const DashboardPage = () => (
<>
<Header>
<Link to="/events/new">Create event</Link>
</Header>
<OtherDashboardStuff />
</>
)
Con este refactor al final podemos ver que el Header
estará abierto a extensión pero cerrado a modificación.
Notas finales
Aunque hemos visto dos casos con children
tenemos que tener presente que su uso pueda conllevar que el componente quede demasiado abierto y que, por ejemplo, en el caso del Header
que acabamos de ver que en vez de renderizarse un Link
se pueda acabar renderizando cualquier otra cosa que no tiene por qué ser válida.
Imaginemos, por lo que sea, que lo que queremos forzar es que siempre se tenga que renderizar un Link
pues en estos casos no tendrá tanto sentido haber abierto tanto el componente en sí y lo que tendríamos que hacer es que mediante las props de Header
se obtuviese la información necesaria para poder renderizar ese Link
.
Nota: en definitiva, los escenarios en los que se vaya a usar nuestro componente a la hora de decidir si se utilizará el
children
o las props para lograr el OCP será el elemento más importante que tendremos que tener en cuenta.Nota: si quieres saber cómo limitar los tipos que puede aceptar un componente puedes echarle un vistazo al artículo Trabajar con React children