Single Responsability Principle (SRP)

DevJoseManuelDevJoseManuel
9 min read

Este principio se corresponde con la S de SOLID.

En este artículo vamos a centrarnos en ver cómo lo podemos aplicar a la hora de construir nuestros componentes de React permitiéndonos pasar los denominados "supercomponentes" (componentes que hacen muchas cosas) a una serie de componentes mucho más pequeños que serán mucho más mantenibles y reutilizables.

¿Qué es lo que dice el SPR?

Pues que un componente de software ha de tener una y solamente una responsabilidad o, dicho de otra manera, un componente solamente deberá hacer una cosa y solamente una.

¿Qué beneficios nos va a reportar?

  1. Reducir la complejidad de nuestras aplicaciones puesto que estas están formadas por componentes pequeños esto se traducirá en que serán mucho más sencillos de entender y facilitará nuestro trabajo con ellos.

  2. Estaremos apostando por la reutilización de los componentes puesto que al estar todos ellos enfocados a la realización de una tarea concreta será mucho más sencillo que los volvamos a elegir para realizar la misma tarea en diferentes partes de la aplicación e incluso en aplicaciones diferentes.

  3. Facilitaremos el testing ya que al tratarse de componentes pequeños con funcionalidades bien definidas se traducirá en que el número de casos a testear será menor.

  4. Mejorará el mantenimiento puesto que en el momento en el que aparezca un bug será mucho más fácil aislarlo en el componente en el que se está produciendo o al menos será mucho más sencillo acotar el conjunto de componentes donde se produce.

Ejemplo

Vamos a mostrar un componente que viola el SRP con el fin de irlo refactorizando hasta lograr que sí que lo cumpla. El componente en cuestión es ProductsPage que se encarga de renderiza la página en la que se listan todos los productos de una aplicación.


import { useQuery } from '@tanstack/react-query'
import { Product } from './types/product'

export default function ProductsPage() {
  const {
    data: products,
    isFetching,
    error
  } = useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const responde = await fetch('https://fakestoreapi.com/products')
      const data = await response.json()
      returns data as Product[]
    }
  })

  return (
    <div>
      <h1>Products Page</h1>
      { isFetching && <p>Loading...</p> }
      { error && <p>Something went wrong</p> }
      { products && && (
        <div>
          { products.map(product => (
              <div key={product.id}>
                <h2>{product.name}</h2>
                <p>{product.price}</p>
                <div>
                  <h3>Seller</h3>
                  <p>{product.seller.name}</p>
                </div>
              </div>
          ))}
        </div>
      )}
    </div>
  )
}

Este componente está violando el SRP puesto que está haciendo muchas cosas:

  • hace una llamada a una API para obtener la lista de todos los productos que se han de renderizar en la página haciendo uso del hook useQuery.

  • en el código JSX el componente decide qué es lo que se ha de renderizar en función del estado de la query anterior definiendo el código JSX que se encargará de renderizar en cada uno de los casos.

Obtención de la información de la API

Centrándonos en el código que va a permitir obtener la información de la API aquí tenemos que entender que no es responsabilidad de ProductsPage saber cómo obtener todos los productos sino que simplemente querrá consumir esta información sin que le importe si esta información se obtiene con react-query o utilizando cualquier otra librería.

Así para lograr que ProductsPage siga el SRP lo que vamos a hacer es definir un custom hook que se encargue de realizar el proceso de obtención de la información de los productos y luego pasar a utilizarlo en el componente.

import { useQuery } from '@tanstack/react-query'
import { Product } from '../types/product'

export const useFetchProducts = () => {
  return useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const responde = await fetch('https://fakestoreapi.com/products')
      const data = await response.json()
      returns data as Product[]
    }
  })
}

Este hook retorna el resultado la invocación de useQuery de tal manera que pueda ser consumido donde se necesite dentro de la aplicación.

Pero... un momento ¿este custom hook no está haciendo a su vez dos cosas? Si es así no estará siguiendo el SRP... y así es puesto que por una parte está definiendo la clave con la que se almacenará la query gracias a react-query pero es que además se define la función gracias a la cual se obtendrán los datos. Esto quiere decir que tendremos que refactorizarlo:

import { useQuery } from '@tanstack/react-query'
import { Product } from '../types/product'

const fetchProducts = async (): Promise<Product[]> => {
  const responde = await fetch('https://fakestoreapi.com/products')
  const data = await response.json()
  returns data as Product[]
}

export const useFetchProducts = () => {
  return useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts
  })
}

Nota: se escribe la función fetchProducts dentro del mismo archivo que tiene el custom hook pero en un aplicación real no debería ser así ya que deberíamos situarlo dentro de una carpeta api o similar.

Ahora ya podemos pasar a utilizar este custom hook en el código de ProductsPage garantizando que el proceso de obtención de la información sigue el SRP.

import { useFetchProducts } from './hooks/useFetchProducts'

export default function ProductPage() {
  const { data: products, isFetching, error } = useFetchProducts()

  return (
    <div>
      <h1>Products Page</h1>
      { isFetching && <p>Loading...</p> }
      { error && <p>Something went wrong</p> }
      { products && && (
        <div>
          { products.map(product => (
              <div key={product.id}>
                <h2>{product.name}</h2>
                <p>{product.price}</p>
                <div>
                  <h3>Seller</h3>
                  <p>{product.seller.name}</p>
                </div>
              </div>
          ))}
        </div>
      )}
    </div>
  )
}

Refactorizar el JSX

En lo primero que tenemos que pensar cuando estamos mirando el código JSX es que el componente ProductPage está renderizando el mensaje mientras se están obteniendo los datos de la API (cuando isLoading es true), cuando se produce un error o bien cuando se tienen los datos de los productos. Esto en principio es así puesto que es el componente quien tiene que decidir qué se ha de renderizar en cada caso pero no es su responsabilidad determinar qué se ha de renderizar en cada caso.

Así en caso del estado isLoading vamos a tener que crear un nuevo componente para mostrar el mensaje cuando se está obteniendo la información de la API.

export default function LoadingDisplay() {
  return <p>Loading...</p>
}

Y siguiendo esta misma filosofía vamos a crear el componente que se encargará de mostrar el mensaje de error cuando sea necesario:

export default function ErrorDisplay() {
  return <p>Something went wrong...</p>
}

Ahora ya podemos utilizar estos dos nuevos componentes dentro de ProductsPage delegando la responsabilidad de mostrar esta información a los nuevos componentes:

import ErrorDisplay from './components/ErrorDisplay'
import LoadingDisplay from './components/LoadingDisplay'
import { useFetchProducts } from './hooks/useFetchProducts'

export default function ProductPage() {
  const { data: products, isFetching, error } = useFetchProducts()

  return (
    <div>
      <h1>Products Page</h1>
      { isFetching && <LoadingDisplay /> }
      { error && <ErrorDisplay /> }
      { products && && (
        <div>
          { products.map(product => (
              <div key={product.id}>
                <h2>{product.name}</h2>
                <p>{product.price}</p>
                <div>
                  <h3>Seller</h3>
                  <p>{product.seller.name}</p>
                </div>
              </div>
          ))}
        </div>
      )}
    </div>
  )
}

Con esta aproximación la ventaja que tenemos es que ahora tenemos dos nuevos componentes que se encargarán de los mensajes de carga de la información y de mostrar los mensajes de error que seguramente vayamos a utilizar más de una vez en toda nuestra aplicación. En este ejemplo estamos tratando de mantener las cosas lo más simples posibles pero en una aplicación real lo normal es que a tranto los mensajes de carga como de error estén mucho más elaborados que lo que acabamos de describir.

Vamos a centrarnos ahora con la parte de los productos donde tenemos que pensar en que el componente ProductsPage no tiene la responsabilidad de saber cómo se han de renderizar múltiples productos en la UI. El componente sí que tiene que saber que se han de renderizar los productos pero se ha de delegar esta funcionalidad a otro componente que se encargue del renderizado.

Por lo tanto vamos a crear el componente ProductList cuyo código sería algo como lo siguiente:


import { Product } from '../types/product'

type ProductListProps = {
  products: Product[]
}

export default function ProductList({ products }: ProductListProps) {
  return (
    <div>
      { products.map(product => (
          <div key={product.id}>
            <h2>{product.name}</h2>
            <p>{product.price}</p>
            <div>
              <h3>Seller</h3>
              <p>{product.seller.name}</p>
            </div>
          </div>
      ))}
    </div>
  )
}

y ya podemos pasar a utilizarlo en el componente ProductList denjándonos el código como sigue:

import ProductList from './components/ProductList'
import ErrorDisplay from './components/ErrorDisplay'
import LoadingDisplay from './components/LoadingDisplay'
import { useFetchProducts } from './hooks/useFetchProducts'

export default function ProductPage() {
  const { data: products, isFetching, error } = useFetchProducts()

  return (
    <div>
      <h1>Products Page</h1>
      { isFetching && <LoadingDisplay /> }
      { error && <ErrorDisplay /> }
      { products && && <ProductList products={products} /> }
    </div>
  )
}

Como podemos ver ahora mismo el componente ProductPage nos ha quedado bastante pequeño y con un código que es simple de mantener y que se entiende muy fácilmente.

Pero no podemos quedarnos aquí puesto que el componente ProductList que hemos definido anteriormente no está siguiendo el SRP puesto que sí que está renderizando la lista de todos los componentes pero además está asumiendo la responsabilidad de renderizar la UI para cada uno de los productos individuales por lo que deberemos crear un neuvo componente que sea el encargado de renderizar la información que se corresponde a cada uno de los elementos individuales.

type ProductCardProps = {
  product: Product
}

export default function ProductCard({ product }: ProductCardProps ) {
  return (
    <div key={product.id}>
      <h2>{product.name}</h2>
      <p>{product.price}</p>
      <div>
        <h3>Seller</h3>
        <p>{product.seller.name}</p>
      </div>
    </div>
  )
}

Podríamos seguir aplicando el SRP dentro del componente ProductCard puesto que el vendedor es a su vez una persona que no tiene que ver son el producto en sí mismo y así deberíamos hacerlo (por ejemplo, definiendo el componente UserCard donde se renderizaría el avatar del usuario, nombre, etc.). Pero con el fin de mantener las cosas simples de cara a la explicación vamos a suponer que el vendedor forma parte del producto.

Ahora tendremos que ir al componente ProductList y simplemente pasaremos a utilizar ProductCard como sigue logrando también que siga el SRP puesto que su responsabilidad es renderizar una lista de productos delegando la responsabilidad de renderizar la información de cada uno de los productos a otro componente.

import { Product } from '../types/product'

type ProductListProps = {
  products: Product[]
}

export default function ProductList({ products }: ProductListProps) {
  return (
    <div className='flex gap-4'>
      { products.map(product => <ProductCard key={product.id} product={product} /> }
    </div>
  )
}

Nota: hemos añadido los estilos de Tailwind necesarios para renderizar la lista de los productos puesto que es responsabilida del componente ProductList definir cómo se renderiza esa lista de los componentes.

Resúmen

Para saber si un componente está violando o no el SRP deberemos hacernos la siguiente pregunta ¿cuál es la responsabilidad única de este componente? El problema aquí es que la respuesta no tiene por qué ser obvia y muchas veces queda a criterio del desarrollador.

En el ejemplo de ProductsPage podemos ver de forma intuitiva que se está violando el SPR puesto que no solamente estaba obteniendo la información de los productos sino que además pintaba la lista de todos ellos además de controlar lo que se renderizaría en los estados para obtener la información.

El SRP nos viene a decir que deberemos crear nuestros componente pensando en que únicamente han de hacer una cosa y cuando necesitemos añadir nuevas funcionalidades a los mismos la estrategia consistirá en crear un nuevo componente que la ofrezca para posteriormente vincularlos logrando el objetivo que se está persiguiendo.

0
Subscribe to my newsletter

Read articles from DevJoseManuel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

DevJoseManuel
DevJoseManuel

Desarrollando código desde los 80s desde que en los 80 vi por primera vez un Amstrad en un escaparate de una tienda de electrónica en mi ciudad natal, lo que me llevó a insistirles a mis padres para que me apuntaran a clases de informática donde aprendí cosas como Basic y ensamblador del x86. A lo largo de mi carrera profesional he pasado por varias revoluciones (y crisis) que han hecho que me haya tenido que ir reinventando para poder seguir desarrollando mi trabajo de la mejor forma posible. Creo que el desarrollo de software tiene una parte de ciencia pero también una de artesanía (y creatividad) que lo convierten en algo apasionante. Eternamente contagiado con el Síndrome del Impostor.