Перейти к содержанию

История о рефакторинге

Укрощение сложности кодовой базы с помощью абстракции, издержки и преимущества

Введение

Программная инженерия — это в равной степени искусство и наука. Точнее говоря, чем более высокоуровневый язык мы используем, тем больше вероятность того, что основным потребителем вашего кода будет не машина, а человек. Ваш коллега-инженер или вы сами через полгода. Поэтому цель состоит не только в том, чтобы написать производительный код и сделать машину счастливой. Вы должны писать легко читаемый, сопровождаемый, подключаемый и т. д. код. В коде, который вы пишете, должно быть как можно меньше моментов, связанных с тем, что, черт возьми, происходит. Звучит просто?

Если вы давно занимаетесь разработкой программного обеспечения, вы знаете, что это не так.

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

Это определяющая характеристика сложной системы: взаимодействие между частями имеет гораздо большее значение, чем свойства части. Это основной тезис теории систем и полная противоположность так называемой редукционистской идее. Что такое редукционистская идея, спросите вы? Она заключается в том, что система — это не более чем сумма составляющих ее элементов.

Редукционизм

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

Переосмысление священного — Стюарт А. Кауффман

Я уже утомил уважаемого читателя своими философскими бреднями. Позвольте мне вернуться к теме и привязать вышеизложенные мысли к программной инженерии.

Подумайте о различных типах тестов: модульных, интеграционных и e2e. Это далеко не полный список, но достаточный, чтобы доказать мою точку зрения. Как? Если программные системы не являются сложными, зачем нам нужны интеграционные и/или e2e-тесты? Разве одни только модульные тесты не приведут нас к утопии и миру без ошибок?

Мы знаем, что это не так. Мы можем иметь 100-процентное покрытие юнит-тестами, но при этом наш продукт может быть непригоден для использования. Взаимодействие имеет гораздо большее значение, чем свойства конкретного блока. Это одна из причин, по которой программные системы становятся сложными.

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

История платежной системы

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

Именно платежная часть и является главным героем нашей истории.

К счастью, на момент написания статьи (2024 год) у нас есть множество поставщиков услуг, которые абстрагируют всю утомительную и рискованную логику. Итак, реализовать страницу оформления заказа/оплаты очень просто, верно? Я имею в виду, насколько это может быть сложно? Подростки создают приложения со страницей оформления заказа меньше чем за день после того, как приходят домой из школы. ChatGPT может создать ее и для вас. Так где же подвох?

Как появляется сложность? Как и всегда, она подкрадывается к вам шаг за шагом.

Простое начало

Наш путь начинается с того, что нам нужно реализовать только один способ оплаты: Кредитная карта. У вас есть только один способ оплаты, о котором вы должны заботиться.

Форма оплаты

Все просто. Нам не нужно учитывать различные пути, по которым могут пойти покупатели. Они могут ввести данные своей кредитной карты, нажать на кнопку "Оформить заказ" и все. После завершения загрузки спиннера (и лежащего в его основе сетевого запроса) мы видим страницу подтверждения. Конец истории.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const PaymentFormSimple = () => {
    React.useEffect(() => {
        initCreditCard();

        return () => {
            tearDownCreditCard();
        };
    }, []);

    return (
        <form
            onsubmit={() => {
                handleCreditCard();
            }}
        >
            <label>CreditCard</label>
            {creditCardInput()}
            <ErrorComponent />
            <PlaceOrderButton />
        </form>
    );
};

Потенциальный фрагмент кода для такого приложения может выглядеть следующим образом. Что здесь происходит? Позвольте мне быстро дать некоторые пояснения.

У нас есть функция useEffect, в которой мы запускаем любой хозяйственный код, который происходит во время монтирования и размонтирования нашего компонента.

Функция handleCreditCard отвечает за все, что происходит после нажатия кнопки "Place Order". Преобразование данных, любые сетевые запросы для авторизации платежа, проверка запасов, взаимодействие с системами управления заказами, аналитика, побочные эффекты — все, что угодно.

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

Главная идея — читабельность. Читабельность достигается тем, насколько лаконичны логика и элементы пользовательского интерфейса. Разработчикам не нужно отвлекаться на разборы. Почему или как это попадает на экран, или какую бизнес-логику оно выполняет. Все просто.

1
2
3
4
5
6
7
const handleCreditCard = async () => {
    await authorizeCreditCard();
    await capturePayment();
    await reserveInventory();
    await createOrderManagementEntry();
    await finalizePayment();
};

Возьмем для примера функцию handleCreditCard. Несмотря на то, что она делает очень много, она хорошо читается. Мы можем добавить больше логики и больше строк, но это не повлияет на ее читабельность (в значительной степени). Нет сложных проверок условий, чтобы определить, какую ветвь нам нужно выполнить. Нет перестановок, которые нужно отслеживать.

Одна из главных причин его простоты заключается в том, что вам не нужно держать много информации в голове. Не нужно прыгать туда-сюда и переключать контекст в голове.

Начинаем усложнять

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

Форма оплаты кредитной картой

Форма оплаты AfterPay

Форма оплаты PayPal

Наше приложение сильно изменилось. Многие из этих изменений малозаметны, но они есть.

Например:

  • Посмотрите на описание рядом с "AfterPay". Это необязательный текст, который отображается только при выборе "AfterPay".

  • Наша кнопка "Оформить заказ" при выборе "PayPal" имеет другой стиль и текст.

  • Отсутствие тела (три элемента ввода: номер карты, CVV и дата истечения срока действия), когда мы выбираем любой способ, кроме кредитной карты.

    кредитка

    Afterpay

  • Различные сообщения об ошибках в зависимости от методов.

Однако самое важное изменение — это то, что происходит, когда мы нажимаем на кнопку "Place Order", то есть логика отправки заказа. Если мы посмотрим на поток AfterPay, то он совсем не похож на обычное путешествие по кредитной карте.

Мы нажимаем на кнопку "Place Order" и в качестве следующего шага перенаправляемся на другой домен. Мы оказались на домене AfterPay. Там нам нужно ввести данные нашего кошелька и авторизовать транзакцию (счастливый путь). После успешной авторизации AfterPay перенаправит нас на наш домен.

Этот поток (который включает в себя передачу данных в какой-то момент) довольно распространен, он не является специфическим для AfterPay. Моя цель — продемонстрировать, насколько разнообразной может быть логика отправки, основанная на различных методах.

Сложность продолжается: разные поставщики

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

По крайней мере, если мы внедряем метод оплаты, скажем, кредитную карту для региона, то все готово. Для следующего региона, где есть кредитная карта, все будет в порядке, верно? Не совсем.

Существует множество различных реализаций, предоставляемых разными поставщиками. Они достаточно различны, чтобы затруднить создание целостной абстракции. Stripe, Adyen, Razorpay, множество их. Все они по-разному реализуют кредитные карты.

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

Функциональные флаги

Мы дробили все наши билеты. Каждый способ оплаты реализован. У нас есть соответствующие поставщики для каждого региона: несколько кредитных карт, PayPal и т.д.. Все хорошо, правда? Не совсем так.

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

Это кредитная карта, которую мы уже внедрили, но ее поток не похож на тот, что был раньше. Теперь у нас есть этап проверки.

Предположим, что один из регионов хочет иметь промежуточный шаг, прежде чем мы будем списывать деньги с карты пользователя. Это, в свою очередь, создает новую часть пользовательского интерфейса, совершенно новую ветвь в потоке обработчика отправки и дополнительный обработчик reviewComplete (то, что происходит, когда мы нажимаем кнопку Place Order на новом экране).

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

Разветвления, разветвления повсюду

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
export const PaymentForm = () => {
    React.useEffect(() => {
        switch (selectedPayment) {
            case PaymentType.PayPal: {
                switch (region) {
                    case Region.USA: {
                        paypalInitUS();
                        break;
                    }
                    case Region.UK: {
                        paypalInitUK();
                        break;
                    }
                    default:
                        break;
                }
            }
            case PaymentType.CreditCard: {
                initCreditCard();
                break;
            }
            case PaymentType.InjectedCreditCard: {
                initInjectedCreditCard();
                break;
            }
            default:
                break;
        }
        return () => {
            switch (selectedPayment) {
                case PaymentType.PayPal:
                    switch (region) {
                        case Region.USA:
                            return tearDownPayPalUS();
                        case Region.UK:
                            return tearDownPayPalUK();
                        default:
                            return;
                    }
                case PaymentType.CreditCard: {
                    tearDownCreditCard();
                    break;
                }
                case PaymentType.InjectedCreditCard:
                    tearDownInjectedCreditCard();
                    break;

                default:
                    return;
            }
        };
    }, []);

    const inputs = () => {
        switch (selectedPayment) {
            case PaymentType.CreditCard:
                switch (region) {
                    case Region.USA:
                        return paypalInputsUS();
                    case Region.UK:
                        return paypalInputUK();
                    default:
                        return;
                }
            case PaymentType.InjectedCreditCard:
                return injectedCreditCardInput();
            case PaymentType.CreditCard:
                return creditCardInput();
            default:
                return;
        }
    };

    const submitHandler = () => {
        switch (selectedPayment) {
            case PaymentType.PayPal:
                switch (region) {
                    case Region.USA:
                        return paypalSubmitHandlerUS();
                    case Region.UK:
                        return paypalSubmitHandlerUK();
                    default:
                        return;
                }
            case PaymentType.InjectedCreditCard:
                return handleInjectedCreditCard();
            case PaymentType.CreditCard:
                return handleCreditCard();
            default:
                return;
        }
    };

    const error = () => {
        switch (selectedPayment) {
            case PaymentType.PayPal:
                return <PaypalErrorComp />;

            case PaymentType.InjectedCreditCard:
                return <CreditErrorComp />;
            case PaymentType.CreditCard:
                return <CreditErrorComp />;
            default:
                return;
        }
    };

    return (
        <form onsubmit={submitHandler}>
            {paymentMethods.map((method) => {
                return (
                    <>
                        <label>{method}</label>
                        {inputs.map()}
                    </>
                );
            })}
            {error()}
        </form>
    );
};

Даже если бы не было комментариев, фрагмент кода говорил бы сам за себя. Это просто неправильно. Уследить за тем, что происходит в этом псевдокоде, довольно сложно, а возможный реальный производственный код может быть еще сложнее.

Так в чем же, собственно, проблема? Я бы сформулировал это так: одна возможная пользовательская история распространяется на множество операторов switch. (Вы можете заменить операторы switch на if/else, if или объектные отображения. Главное здесь — разветвление).

Например:

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

Проблема в том, что наш мозговой стек не является постоянным. Мы многое забываем. Переключение контекста и когнитивная нагрузка делают разбор подобной логики очень трудным. Очень трудно, если не невозможно, понять. Представьте себе нового разработчика. Вы входите в проект, и вам поручают тушить пожар. Удачи вам в этом. Но даже если вы знаете контекст кода, скорее всего, эти знания померкнут уже через полгода. Ваш будущий "я" будет в таком же замешательстве, как и любой новичок.

еще одно ветвление

Мы в бешенстве. И правильно сделали. Мы добавили все функции, но это оставило нас с очень шумной (британская вежливость) кодовой базой.

Управление сложностью с помощью абстракции

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

Что мы можем с этим сделать? Каждая кодовая база отличается от другой, поэтому сейчас нет универсального решения. На протяжении десятилетий появлялись паттерны проектирования, призванные уменьшить сложность такого рода.

Однако есть одна общая черта. Всегда есть паттерн или паттерны, которые возникают из проблемы (хаоса). На основе этого паттерна можно создать абстракцию. Создание абстракции требует написания большего количества кода, а значит, увеличивает сложность. Опять же.

Поэтому вопрос заключается в следующем: стоит ли создание абстракции того? Является ли сложность и код, который создает абстракция, достаточно ценным, чтобы оправдать дополнительную работу и обслуживание?

Если ответ положительный, то действуйте.

Видим ли мы здесь закономерность?

Мы определили основную болевую точку следующим образом.

Потенциальный путь пользователя растягивается между множеством операторов if (или switch). Это приводит к большой когнитивной нагрузке, сложному обслуживанию и другим подобным проблемам. Наша цель — снизить эту нагрузку и сделать код более удобным для человека.

форма

Взгляните на картинку. Выделены части страницы/приложения, измененные по ходу этой статьи. Для каждой из них мы ввели новую ветку. Некоторые элементы пользовательского интерфейса. Также изменилась логика отправки заявок.

Извлекаем паттерн

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const getPaymentPrimitives = (
    paymentType: PaymentType
) => {
    switch (paymentType) {
        case PaymentType.PayPal: {
            return {
                Label: () => <span>PayPal</span>,
                Button: PayPalButton,
                Body: null,
                Review: null,
            };
        }
        case PaymentType.CreditCard: {
            return {
                Label: () => <span>CreditCard</span>,
                Button: CreditCardButton,
                Body: CreditCardBody,
                Review: CreditCardReview,
            };
        }
        default: {
            throw new Error('Not a valid type');
        }
    }
};

Что, если перевернуть нашу логику с ног на голову: сделать один оператор switch и определить внутренний API, который мы возвращаем для каждой ветки? Мы создаем API, который должен возвращать такие элементы, как Button, Body и т. д.

Что, если нам придется возвращать элементы, примерно соответствующие тому, что мы имеем в нашем UI. Как это поможет нам?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const CreditCardBody = () => {
    const config = useConfig();

    React.useEffect(() => {
        // Can handle side effect local to the given component.
    }, []);

    const {
        onSubmission,
        onReviewConfirmed,
    } = React.useContext(CheckoutWrapperContext);

    onSubmission.current = async () => {
        // Normal API logic to save the payment info.
        // Can handle error handling and reporting.
    };

    onReviewConfirmed.current = async () => {
        if (config.features.review) {
            // Handle logic if/when review is expected.
        }
    };

    return (
        <>
            <input />
            <input />
            <input />
            <CreditCardErrorComponent />
        </>
    ); // Render JSX.
};

Давайте рассмотрим компонент <CreditCardBody />. Компонент <CreditCardBody /> теперь имеет дело только с тем, что нужно отобразить для кредитной карты. Внутри этого компонента каждая строка кода имеет дело с бизнес-логикой, связанной с кредитной картой.

  • Эффект useEffect запускает код, связанный с кредитной картой.
  • Вы возвращаете JSX только для кредитной карты.
  • У нас есть нечто, называемое onSubmission.current (я объясню, зачем нам нужен контекст, через минуту). Сейчас достаточно сказать, что эта функция будет включать логику отправки только для кредитной карты. Здесь вы определяете свою логику обработки.

Другими словами, если вы хотите узнать, как работает кредитная карта, вам нужно открыть этот файл, и никакой другой.

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

Ничего меньше и ничего больше.

Ничего меньше — это значит, что вам не нужно бегать за другими файлами, помещать их в мозговую стопку, разбирать их или ходить туда-сюда. Ничего не "растягивается" между ветвями, гораздо легче разбирать код.

Ничего лишнего, то есть код не просачивается из других частей приложения. Абстракция достаточно лаконична, чтобы не включать в себя код, которому здесь не место. Это очень важно, когда нам нужно расширить нашу кодовую базу (принцип "открыто-закрыто").

Контекстный API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
export const PaymentPage = () => {
    const {
        Label,
        Button,
        Body,
        Review,
    } = getPaymentPrimitives(currentPayment);

    return (
        <CheckoutProvider>
            {config.payments.map((payment) => {
                return (
                    <div key={payment}>
                        <div>
                            <input
                                type="radio"
                                value={payment}
                            />
                            <span>{payment}</span>
                            <Label />
                        </div>
                        {props.currentPayment === payment &&
                            props.children && (
                                <div>
                                    <Body />
                                </div>
                            )}
                    </div>
                );
            })}
            <Button />
            {Review && <Review />}
        </CheckoutProvider>
    );
};

const CheckoutWrapperContext = React.createContext<{
    onSubmission: React.MutableRefObject<() => void>;
    onReviewConfirmed: React.MutableRefObject<() => void>;
    loading: boolean;
    setLoading: React.Dispatch<
        React.SetStateAction<boolean>
    >;
    // other metadata
}>(null as any);

export const CheckoutProvider = (props) => {
    const onSubmission = React.useRef<() => void>(
        () => null
    );

    const onReviewConfirmed = React.useRef<() => void>(
        () => null
    );

    const [loading, setLoading] = React.useState(false);

    return (
        <CheckoutWrapperContext.Provider
            value={{
                onSubmission,
                onReviewConfirmed,
                loading,
                setLoading,
            }}
        >
            {props.children}
        </CheckoutWrapperContext.Provider>
    );
};

Наши элементы пользовательского интерфейса в PaymentPage.tsx, скорее всего, будут братьями и сестрами. Поэтому совместно использовать некоторые общие свойства и их сеттеры проще всего с помощью Context API. Этот CheckoutWrapperContext просто предоставляет нам возможность легко передавать метаданные между компонентами. То есть нам может понадобиться установить состояние загрузки в <Body />, но спиннер будет виден в компоненте <Button />.

onSubmission.current — это просто способ установить обработчик отправки без запуска другого цикла изменения пользовательского интерфейса. Теперь это также часть нашего API. Точно так же, как нам нужно возвращать определенные элементы пользовательского интерфейса из функции getPaymentPrimitives, нам нужно устанавливать и использовать логику отправки через наш контекст. Эти две функции (getPaymentPrimitives и CheckoutWrapperContext) вместе составляют основу нашего API.

Преимущества такого подхода

В чем ценность такого подхода? Мы увидели, как отображается пользовательский интерфейс и что представляет собой наш API (что нам нужно возвращать и с какими метаданными мы можем работать). Мы также затронули вопрос о том, что <CreditCardBody /> теперь касается только того, что происходит с кредитной картой.

Но в чем реальная польза от такого подхода? Во-первых, давайте четко определим, что такое площадь поверхности API:

  • <Button /> обязательный компонент
  • <Body /> необязательный компонент
  • <Description /> необязательный компонент
  • <Review /> необязательный компонент
  • onSubmissionHandler
  • onReviewConfirmed обработчик
  • различные props (например загрузка)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// PaymentFormSimple - Our form when we only had credit
export const PaymentFormSimple = () => {
    React.useEffect(() => {
        initCreditCard();

        return () => {
            tearDownCreditCard();
        };
    }, []);

    return (
        <form
            onsubmit={() => {
                handleCreditCard();
            }}
        >
            <label>CreditCard</label>
            {creditCardInput()}
            <ErrorComponent />
            <PlaceOrderButton />
        </form>
    );
};

// CreditCardBody
const CreditCardBody = () => {
    const config = useConfig();

    React.useEffect(() => {
        // Can handle side effect local to the given component.
    }, []);

    const {
        onSubmission,
        onReviewConfirmed,
    } = React.useContext(CheckoutWrapperContext);

    onSubmission.current = async () => {
        // Normal API logic to save the payment info.
        // Can handle error handling and reporting.
    };

    onReviewConfirmed.current = async () => {
        if (config.features.review) {
            // Handle logic if/when review is expected.
        }
    };

    return (
        <>
            <input />
            <input />
            <input />
            <CreditCardErrorComponent />
        </>
    ); // Render JSX.
};

// CreditCardButton
const CreditCardButton = () => {
    const { onSubmission } = React.useContext(
        CheckoutWrapperContext
    );

    return (
        <button
            onClick={() => {
                onSubmission.current();
            }}
        >
            Place order
        </button>
    );
};

Пришло время сравнить наш новый API с нашим оригинальным PaymentFormSimple, где у нас была только кредитка.

Наши две формы довольно похожи, и читаются они одинаково. Все элементы и обработчики находятся в компоненте формы, и мы можем легко их понять. Обе формы PaymentFormSimple (оригинал, только один метод), <CreditCardBody /> и <Button /> (новое рефакторинговое решение) имеют дело только с бизнес-логикой, связанной с кредитными картами. Оба они лишены каких-либо серьезных разветвлений, поэтому их легко понять.

Мы как бы собрали все самое лучшее из нашего простого состояния. Теперь нет необходимости жонглировать кодами. Конечно, теперь элемент абстрагирован и выводится условно. Но это часть нашего API. Главное: и в форме SimplePayment, и в рефакторизованной форме мы можем быть уверены: если мы нажмем на отрисованный элемент (т.е. <Body />), то попадем в компонент, где нам придется столкнуться только с логикой для данного метода, не меньше и не больше!

Создание нового метода

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

Давайте реализуем AliPay.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
export const getPaymentPrimitives = (
    paymentType: PaymentType
) => {
    switch (paymentType) {
        case PaymentType.AliPay: {
            return {
                Label: () => <span>AliPay 🌐</span>,
                Button: () => {
                    const {
                        onSubmission,
                    } = React.useContext(
                        CheckoutWrapperContext
                    );

                    return (
                        <button
                            onClick={() =>
                                onSubmission.current()
                            }
                        >
                            Place order
                        </button>
                    );
                },
                Body: () => {
                    const {
                        onSubmission,
                    } = React.useContext(
                        CheckoutWrapperContext
                    );

                    React.useEffect(() => {
                        // Load the script.
                        const script = document.createElement(
                            'script'
                        );

                        script.src =
                            '<https://ali.script.com>';

                        script.onload = () => {
                            // Render the UI.
                            window.Ali.render(
                                'placeholder'
                            );

                            window.Ali.setSubmissionHandler = (
                                amount,
                                metadata
                            ) => {
                                // do the usual things.
                            };
                        };

                        document.body.appendChild(script);
                        G;
                    }, []);

                    onSubmission.current = () => {
                        window.Ali.submit();
                    };

                    return <div id="placeholder" />;
                },
                Review: null,
            };
        }
        default: {
            throw new Error('Not a valid type');
        }
    }
};

Если мы знаем API, который мы создали, то добавить новый метод довольно просто. В этом примере мы объединили наши компоненты для большей наглядности. Все элементы имеют дело с соответствующей логикой. Элементы <Label /> и <Button /> довольно очевидны: они возвращают необходимые элементы пользовательского интерфейса для данного метода.

Компонент <Label /> отвечает лишь за возвращение необязательного описания рядом с нашей радиокнопкой. Вспомните случай с AfterPay. Компонент <Button /> отображает то, что мы увидим в нижней части формы. Мы просто добавляем обработчик onClick, который будет вызывать наш onSubmission.current. ( Это часть нашего контекстного API, и его можно установить в другом месте).

Кстати говоря, давайте посмотрим, как мы это установим.

Основная часть работы выполняется в компоненте <Body />. В данном конкретном примере мы предполагаем, что Ali Pay использует сторонний скрипт. Поэтому мы инициализируем его в useEffect. Мы назначаем наш обработчик onSubmission для вызова метода submit скрипта Ali Pay. Мы предполагаем, что он был прикреплен к объекту окна благодаря нашему useEffect. Мы также отображаем заполнитель, чтобы наш загруженный скрипт мог взять на себя ответственность и отобразить любые элементы DOM в качестве своих дочерних элементов.

Из этого шаблона видно, что требуется только один оператор switch. Написать, а затем прочитать или изменить эту логику довольно просто. По крайней мере, легко понять, что делает код. В коде нет ни ветвлений, ни переходов. Лаконичная абстракция над несколькими блоками, которые имеют четко определенные обязанности.

Какова же цена?

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

Этот интерфейс не является общеизвестным. Он не является частью API какой-либо библиотеки. Это означает:

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

документация

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

Чему мы научились?

Мы решили несколько проблем. По мере роста кодовых баз сложность будет расти. Это неизбежно. Мы не можем ее искоренить, только приручить. То, что мы здесь обсудили, — это история, которая сработала для нас в кодовой базе React в определенный момент времени. Будет ли она работать в каждом случае? Можете ли вы скопировать ее и применить вслепую? Конечно, нет.

Тогда в чем же ценность для вас, мой дорогой читатель?

Не поддавайтесь порыву создавать незрелые и ранние абстракции. Создавайте их только там, где есть реальная боль. Абстракции стоят дорого. Что еще важнее: они требуют паттернов. Распознавание паттернов требует времени.

Источник — https://commerce.nearform.com/blog/2024/tale-of-a-refactor/

Комментарии