Рассмотрим 5 принципов SOLID на примере React-компонентов
1. Принцип Единой Ответственности (Single Responsibility Principle)
Компонент должен делать только одну вещь
Пример нарушения принципа единой ответственности в React-компоненте:
import { Button, Stack, Typography } from "@mui/joy";import axios from "axios";
interface CellProps { title: string; subtitle: string; blogPostId: string;}
// Ячейка таблицы для просмотра информации о посте и удаления поста в блогеexport default function Cell({ title, subtitle, blogPostId }: CellProps) {
// Удаление поста const handleDeleteClick = async () => { const res = await axios.delete(`/api/posts/${blogPostId}`); if (res.data.success) { location.reload(); } };
return <Stack> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> <Button onClick={handleDeleteClick} variant="soft">Удалить</Button> </Stack>}Вынесем код удаления поста из компонента. Будем прокидывать его в колбэк:
import { Button, Stack, Typography } from "@mui/joy";import axios from "axios";import React from "react";
interface CellProps { title: string; subtitle: string; onDeleteClick: (e: React.SyntheticEvent) => void;}
// Ячейка таблицы для просмотра информации о посте и удаления поста в блогеexport default function Cell({ title, subtitle, onDeleteClick }: CellProps) { return <Stack> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> <Button onClick={onDeleteClick} variant="soft">Удалить</Button> </Stack>}2. Принцип открытости-закрытости (Open-close Principle)
Предусматривайте возможность добавления функционала минимальными изменениями в уже написанном коде компонента
import { Button, Stack, Typography } from "@mui/joy";import React from "react";
interface CellProps { title: string; subtitle: string; canEdit: boolean; onEditClick: (e: React.SyntheticEvent) => void; canDelete: boolean; onDeleteClick: (e: React.SyntheticEvent) => void;}
// Ячейка таблицы для просмотра информации о посте в блогеexport default function Cell({ title, subtitle, canEdit, onEditClick, canDelete, onDeleteClick}: CellProps) { return <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> {canEdit ? <Button onClick={onEditClick} variant="soft">Редактировать</Button> : null} {canDelete ? <Button onClick={onDeleteClick} variant="soft">Удалить</Button> : null} </Stack>;}Создадим пропс endBlock и будем прокидывать туда все кнопки, которые захотим
import { Stack, Typography } from "@mui/joy";import React from "react";
interface CellProps { title: string; subtitle: string; endBlock?: React.ReactNode;}
// Ячейка таблицы для просмотра информации о посте в блогеexport default function Cell({ title, subtitle, endBlock,}: CellProps) { return <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> {endBlock} </Stack>;}3. Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
«Отнаследованный» компонент не должен изменять контракт, унаследованный от родителя, кроме добавления новых пропсов
import { Button, Stack, Typography } from "@mui/joy";import React from "react";
interface CellProps { title: string; subtitle: string; date: Date; endBlock?: React.ReactNode;}
// Ячейка таблицы для просмотра информации о посте в блогеfunction Cell({ title, subtitle, date, endBlock,}: CellProps) { return <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> <Typography level="body-sm">{date.toLocaleDateString()}</Typography> {endBlock} </Stack>;}
interface AdminCellProps { title: string; subtitle: string; date: string; endBlock?: React.ReactNode; onEditClick: () => void; onDeleteClick: () => void;}
// Ячейка таблицы для администратораfunction AdminCell({ title, subtitle, date, endBlock, onEditClick, onDeleteClick}: AdminCellProps) { return <Cell title={title} subtitle={subtitle} date={new Date(date)} endBlock={endBlock || <> <Button onClick={onEditClick} variant="soft">Редактировать</Button> <Button onClick={onDeleteClick} variant="soft">Удалить</Button> </>} />;}Мы не сможем подставить AdminCell вместо Cell из-за изменения типа пропса date
import { Button, Stack, Typography } from "@mui/joy";import React from "react";
interface CellProps { title: string; subtitle: string; date: Date; endBlock?: React.ReactNode;}
// Ячейка таблицы для просмотра информации о посте в блогеfunction Cell({ title, subtitle, date, endBlock,}: CellProps) { return <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md">{subtitle}</Typography> <Typography level="body-sm">{date.toLocaleDateString()}</Typography> {endBlock} </Stack>;}
interface AdminCellProps extends CellProps { onEditClick: () => void; onDeleteClick: () => void;}
// Ячейка таблицы для администратораfunction AdminCell({ title, subtitle, date, endBlock, onEditClick, onDeleteClick}: AdminCellProps) { return <Cell title={title} subtitle={subtitle} date={date} endBlock={endBlock || <> <Button onClick={onEditClick} variant="soft">Редактировать</Button> <Button onClick={onDeleteClick} variant="soft">Удалить</Button> </>} />;}4. Принцип разделения интерфейса (Interface Segregation Principle)
В пропсы нужно прокидывать только то, что нужно
import { Stack, Typography } from "@mui/joy";
interface User { id: string; name: string; role: string;}
interface CellProps { title: string; subtitle: string; author: User;}
// Ячейка таблицы с заголовком, подзаголовком и именем автораexport default function Cell({ title, subtitle, author }: CellProps) { return <> <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md" color="neutral">{subtitle}</Typography> <Typography level="body-md">{author.name}</Typography> </Stack> </>;}В пропс передается сущность User, которая имеет поля id, role, которые не используются в компоненте.
Заменим пропс author на пропс authorName:
import { Stack, Typography } from "@mui/joy";
interface CellProps { title: string; subtitle: string; authorName: string;}
// Ячейка таблицы с заголовком, подзаголовком и именем автораexport default function Cell({ title, subtitle, authorName }: CellProps) { return <> <Stack direction="row" spacing={2}> <Typography level="title-md">{title}</Typography> <Typography level="body-md" color="neutral">{subtitle}</Typography> <Typography level="body-md">{authorName}</Typography> </Stack> </>;}5. Принцип инверсии зависимостей (Dependency Inversion Principle)
- Нельзя импортить низкоуровневые модули внутри высокоуровневых
- Низкоуровневый модуль должен реализовывать интерфейс, находящийся на высоком уровне. Высокоуровневый модуль должен работать с любым модулем переданным в аргументы/пропсы, реализующим этот интерфейс
import { useEffect, useState } from "react";// Мы не сможем использовать компонент с блогами из другого источникаimport { getBlogsRequest } from "./Blogs.webapi";import Cell from "./Cell";
// Список блоговexport default function BlogsPage() { const [blogs, setBlogs] = useState<{ title: string; subtitle: string; }[]>([]);
const sendGetBlogsPostRequest = async () => { const blogsFromWebApi = await getBlogsRequest(); setBlogs(blogsFromWebApi); };
useEffect(() => { sendGetBlogsPostRequest(); }, []);
return <> {blogs?.map(blog => <Cell title={blog.title} subtitle={blog.subtitle} />)} </>;}Мы не сможем использовать этот компонент с блогами из другого источника
import { useEffect, useState } from "react";// Избавились от ключевого слова import// import { getBlogsRequest } from "./Blogs.webapi";import Cell from "./Cell";
interface Blog { title: string; subtitle: string;}
interface IGetBlogsRequest { (): Promise<Blog[]>;}
interface BlogsPageProps { getBlogsRequest: IGetBlogsRequest;}
// Список блогов// прокидываем getBlogsRequest в пропс при вызове компонента// getBlogsRequest реализует интерфейс IGetBlogsRequest// Любой модуль, реализующий интерфейс IGetBlogsRequest, можно будет прокинуть в этот пропсexport default function BlogsPage({ getBlogsRequest }: BlogsPageProps) { const [blogs, setBlogs] = useState<{ title: string; subtitle: string; }[]>([]);
const sendGetBlogsPostRequest = async () => { const blogsFromWebApi = await getBlogsRequest(); setBlogs(blogsFromWebApi); };
useEffect(() => { sendGetBlogsPostRequest(); }, []);
return <> {blogs?.map(blog => <Cell title={blog.title} subtitle={blog.subtitle} />)} </>;}