Принципы SOLID существовали всегда. С тех пор, как об этом появился «Дядя Боб», о нем говорили и использовали как одну из важных парадигм для рассмотрения программных решений и дизайна на многих языках. В этой статье давайте определим их и, самое главное, применим их при разработке компонентов React в реальном сценарии.

Проще говоря, нижеследующие принципы представляют собой ТВЕРДЫЕ Принципы, и на самом деле они соответствуют их названиям. Несмотря на то, что они достаточно информативны, давайте углубимся в них, пока мы рассмотрим наш реальный сценарий.

  1. Принцип единой ответственности
  2. Принцип открытия и закрытия
  3. Принцип замены Лискова
  4. Принцип разделения интерфейса
  5. Принцип инверсии зависимостей

Далее я представлю вам реальный сценарий, который мы вместе пройдем в процессе разработки, придерживаясь только что рассмотренных принципов.

Вариант использования следующий:

  1. Нам нужно создать компонент корзины покупок, который может отображать список товаров.
  2. Этот компонент корзины покупок будет иметь разные виды, такие как компактный и расширенный. Потребители могут импортировать и использовать представление по своему усмотрению. С меньшими усилиями мы сможем добавлять новые представления, если поступят запросы клиентов.
  3. В компактном представлении должна отображаться общая сумма и отображаться кнопка для перехода к расширенному представлению.
  4. В расширенном представлении должен отображаться список товаров, кнопки сохранения корзины и оформления заказа, чтобы инициировать эти потоки.
  5. Обо всех этих триггерах потребитель должен быть уведомлен, если он хочет действовать в соответствии с ним.
  6. У каждого потребителя может быть своя собственная UX-тема, поэтому должен быть способ передавать стили по своему усмотрению. Когда число потребителей вырастет и появятся новые темы, компонент должен легко расширяться для новых тем.

Может быть много неправильных способов сделать это, а также много правильных способов. Вместо того, чтобы пытаться охватить их все, я сосредоточусь на одном правильном способе и на том, как он согласуется с основными концепциями SOLID.

(Полную реализацию упомянутого сценария вы также можете найти на github)

Принцип единой ответственности

В качестве первого шага в нашем решении я хотел бы определить компоненты, которые мы собираемся создать, и их обязанности, что подводит нас к принципу единой ответственности. Принцип гласит: «У класса должна быть только одна причина для изменения». В нашем сценарии его можно применять как функцию, или функциональный компонент должен иметь только одну ответственность. Поэтому нам необходимо определить наши функции и обязанности.

Но перед этим давайте посмотрим, что нам действительно нужно сделать?

  • Нам нужно визуализировать контент
  • Нам нужно вызвать API
  • Нам нужно выполнить расчеты

Если вы хотите сделать это быстро и грязно, вполне возможно сделать все это в одном файле TypeScript. Может быть даже в пределах одного компонента. Но именно поэтому его становится сложнее поддерживать. Итак, все это разные обязанности, которые необходимо разделить на разные компоненты или функции, и которые можно определить следующим образом.

Ответственность за рендеринг контента может быть возложена на следующие компоненты.

//Responsibilities:
//Render base skeleton sharing common capabilities to all components
//Provide Total Amount to associated components

import {getTotalWithDiscount} from "../Utils/Calculations";
import IProduct from "../modals/IProduct";

const CartComponent = (props: ICartViewProps) => {
    const styles = {
        backgroundColor: props.consumerStyleProps.backgroundColour,
        fontFamily: props.consumerStyleProps.font,
        height: `${props.consumerStyleProps.height}px`
    };

    return (
        <div style={styles}>
            <h6> {props.cartName + ' : ' + props.clientId} </h6>
            {
                props.children &&
                props.children(getTotalWithDiscount(props.itemList, 0))
            }
        </div>
    );
}
export default CartComponent;
//Responsibilities:
//Render Total Amount
//Render Show Full Cart Button

import CartComponent from "./CartComponent";

const CartCompactView = (props: ICartCompactViewProps) => {
    return (
        <CartComponent {...props} >
            {
                (totalAmount: number) => {
                    return (
                        <div>
                            <span>Total: {totalAmount}</span>

                            <button onClick={() => console.log('You will be redirected to Full Cart View')}>Show Full
                                Cart
                            </button>
                        </div>
                    )
                }

            }
        </CartComponent>
    );
}
export default CartCompactView;
//Responsibilities:
//Render Total Amount
//Render Item list
//Render Save Cart Button
//Render Checkout Button

import CartComponent from "./CartComponent";
import CartApi from "../Utils/CartApi";


const CartExpandedView = (props: ICartExpandedViewProps) => {
    return (
        <CartComponent {...props} >
            {
                (totalAmount: number) => {
                    return (
                        <div>
                            <ol>
                                {
                                    props.itemList.map(product => {
                                        return (
                                            <li key={product.name}>
                                                {`${product.name} --- ${product.price}`}
                                            </li>
                                        );
                                    })
                                }
                            </ol>

                            <div>Total: {totalAmount}</div>

                            <button onClick={() => {
                                CartApi.saveCart(props.itemList)
                            }}>Save Cart
                            </button>
                            <button onClick={() => {
                                CartApi.checkoutCart(props.itemList)
                            }}>Checkout
                            </button>
                        </div>
                    )
                }

            }
        </CartComponent>
    );
}
export default CartExpandedView;

Ответственность за расчет общей суммы в соответствии со скидкой может быть передана другой функции.

//Responsibilities:
//Calculate total

import IProduct from "../modals/IProduct";

export const getTotalWithDiscount = (
    items: IProduct[],
    discountPercentage: number
): number => {
    const total = items.reduce((result, item) => {
        return result + item.price;
    }, 0)

    if (discountPercentage === 0) {
        return total;
    }

    return total * (discountPercentage / 100);
}

Ответственность за вызов API может быть возложена на следующие функции.

//Responsibilities:
//Core API functions

const post = (url: string, payload: object) => {
    return new Promise(resolve => {
        setTimeout(() => {
            alert(`Calling POST for ${url} with ${JSON.stringify(payload)}`);
            resolve('Successful!');
        }, 500);
    })
};
export default post;
//Responsibilities:
//Specialized API functions for Cart

import post from "./CoreApi";
import IProduct from "../modals/IProduct";

const CartApi = {
    saveCart: async (productList: IProduct[]) => {
        const URL = '/save-cart';

        const response = await post(URL, productList);
        console.log(`${URL}: ${response}`);
    },
    checkoutCart: async (productList: IProduct[]) => {
        const URL = '/checkout';

        const response = await post(URL, productList);
        console.log(`${URL}: ${response}`);
    }
}
export default CartApi;

Итак, если мы пройдемся по каждому из вышеупомянутых блоков, читая обязанности, упомянутые в комментариях, вы поймете, что каждому подразделению дается только одна ответственность высокого уровня, не перегружая их.

Принцип открытия и закрытия

Поскольку мы разделили обязанности, теперь давайте посмотрим, насколько приведенный выше код закрыт и его можно расширить. Принцип открытости и закрытости гласит, что «объекты программного обеспечения (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации».

Если вы внимательно изучали упомянутый код, возможно, вы уже заметили некоторые места, где применялся этот принцип. Но если вы этого не сделали, не волнуйтесь, я объясню вам это на нескольких примерах.

  • Предположим, вы хотите представить новое представление из-за требований клиента. Благодаря композиции, которую мы использовали между CartComponent и другими представлениями (CartCompactView и CartExpandedView), создать новое представление без внесения изменений очень просто. существующий код. Вам просто нужно будет использовать ту же композицию с новым представлением и объявить новый интерфейс реквизита, унаследованный от ICartViewProps.
const CartCompactView = (props: ICartCompactViewProps) => {
    return (
        <CartComponent {...props} >
            {

const CartExpandedView = (props: ICartExpandedViewProps) => {
    return (
        <CartComponent {...props} >
            {

//New View
const NewView = (props: ICartNewViewProps) => {
    return (
        <CartComponent {...props} >
            {
  • Кроме того, в случае, если придет новый потребитель и захочет использовать новую тему, интерфейс IConsumerStyleProps будет поддерживать это без изменения закрытого кода.
export interface IConsumerStyleProps {
    backgroundColour: string;
    font: string;
    height: number
}

Принцип замены Лискова

Когда мы смотрим на Принцип замены Лискова, он объясняется так: «Объекты супертипа должны быть заменены объектами его подтипа без нарушения работы приложения. Проще говоря, мы хотим, чтобы объекты нашего подтипа вели себя так же, как объекты нашего супертипа».

Что ж, код, который мы написали, также придерживается этого принципа. Я объясню как.

Хорошим примером является использование супертипа ICartViewProps и подтипов ICartCompactViewProps и ICartExpandedViewProps. Если вы посмотрите на следующий блок кода, то увидите, что мы передаем ICartExpandedViewProps в CartComponent, хотя типом его свойства является ICartViewProps. Это возможно только потому, что мы придерживаемся LSP при наследовании и реализации этих интерфейсов.

const CartComponent = (props: ICartViewProps) => {

const CartExpandedView = (props: ICartExpandedViewProps) => {
    return (
        <CartComponent {...props} >

Принцип разделения интерфейса

Согласно определению, Принцип разделения интерфейса означает, что «ни один код не должен зависеть от методов, которые он не использует, или реализовывать их». Это просто означает, что при определении интерфейсов мы должны убедиться, что возможности разделены на более мелкие интерфейсы, не перегружая один из них атрибутами, которые потенциально могут не использоваться всеми конкретными людьми, которые это реализуют.

Если вы посмотрите на объявление интерфейсов типа prop, вы заметите, что это очень хорошо используется в нашем коде.

export interface ICartViewProps {
    cartName: string;
    clientId: number;
    consumerStyleProps: IConsumerStyleProps;
    itemList: IProduct[];
    children?: Function
}

interface ICartCompactViewProps extends ICartViewProps {
    onExpandedViewTrigger: Function;
}

interface ICartExpandedViewProps extends ICartViewProps {
    onCheckoutCompleted: Function;
    onSaveCartCompleted: Function;
}

«Просмотр конкретных возможностей» разделен на отдельные интерфейсы без использования единого интерфейса. Если нам нужно ввести новое представление, все, что нам нужно сделать, это создать новый интерфейс, унаследованный от ICartViewProps. Таким образом, это также поддерживает принцип открытия и закрытия.

Принцип инверсии зависимостей

Этот принцип более важен для поддержки других принципов SOLID. Принцип инверсии зависимостей гласит: «Модули высокого уровня не должны ничего импортировать из модулей низкого уровня. И то, и другое должно зависеть от абстракций». Опять же, простыми словами: вам следует кодировать не для конкретных реализаций, а для абстракций или интерфейсов. Это помогает расширить зависимые модули с меньшими изменениями.

Если вы уже знакомы с нашим кодом, вы заметите, что мы сделали это повсюду.

  • Создание интерфейса IProduct и, в зависимости от этого, является одним из них. Если есть новые разновидности продуктов, мы можем наследовать их, и закрытые модули будут вести себя правильно, пока мы реализуем новые модули.
interface IProduct {
    name: string;
    price: number
}
  • В зависимости от интерфейсов высокого уровня для реквизита вместо конкретных реализаций.
export interface ICartViewProps {
}

interface ICartCompactViewProps extends ICartViewProps {
}

interface ICartExpandedViewProps extends ICartViewProps {
}
  • Использование интерфейса IConsumerStyleProps и в зависимости от этого вместо конкретных реализаций.
export interface IConsumerStyleProps {
    backgroundColour: string;
    font: string;
    height: number
}

Заключение

Итак, это все. Именно так мы реализовали компонент React, соответствующий принципам SOLID, который облегчит жизнь будущим разработчикам, работающим над тем же компонентом.

Невозможно применить один и тот же подход к каждому сценарию, с которым вы сталкиваетесь. Что важно понять из этой статьи, так это процесс создания наилучшей возможной архитектуры компонентов, соответствующей принципам SOLID, с осознанием всех плюсов и минусов.

(Полную реализацию упомянутого сценария вы можете посмотреть на GitHub)

Спасибо, что дочитали до конца. Пожалуйста, подумайте о том, чтобы подписаться на автора и эту публикацию. Посетите Stackademic, чтобы узнать больше о том, как мы демократизируем бесплатное образование в области программирования во всем мире.