Поиск по сайту:

Что такое компонуемый код и как его создать?


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

Компонуемость и инверсия управления

Компонуемый код часто является целью и следствием стратегий инверсии управления (IoC). Такие методы, как внедрение зависимостей, работают с автономными компонентами, которые передаются («внедряются») в те места, где они необходимы. Это пример IoC — внешняя среда отвечает за разрешение зависимостей более глубоких слоев кода, которые она вызывает.

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

Примеры компонуемости

Вот пример трех возможных функциональных блоков:

interface LogMessage {
    public function getMessage() : string;
}
 
interface Mailable {
    public function getEmailContent() : string;
}
 
interface RelatesToUser {
    public function getTargetUserId() : int;
}

Теперь давайте добавим в смесь реализацию регистратора:

interface Logger {
    public function log(LogMessage $message) : void;
}
 
final class SystemErrorLogMessage implements LogMessage, Mailable {
 
    public function __construct(
        protected readonly Exception $e) {}
 
    public function getMessage() : string {
        return "Unhandled error: " . $this -> e -> getMessage();
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
}

Давайте теперь рассмотрим другой тип сообщения журнала:

final class UserLoggedInLogMessage implements LogMessage, Mailable, RelatesToUser {
 
    public function __construct(
        protected readonly int $UserId) {}
 
    public function getMessage() : string {
        return "User {$this -> UserId} logged in!";
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
    public function getTargetUserId() : int {
        return $this -> UserId;
    }
 
}

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

Приведенные выше примеры написаны на PHP, но могут быть воспроизведены на любом объектно-ориентированном языке. Однако компонуемость не ограничивается ООП-кодом: это также основополагающий аспект функционального программирования. Здесь сложное поведение достигается путем объединения небольших функций. Функции могут принимать другие функции в качестве аргументов и в результате возвращать новую функцию более высокого порядка.

const square = x => (x * x);
const quadruple = x => (x * 4);
 
// 16
console.log(compose(square, quadruple)(2));

В этом минимальном примере JavaScript используется библиотека compose-function для объединения единиц square и quadruple в другую функцию, которая возводит в квадрат, а затем учетверяет входные данные. Вспомогательная функция compose() принимает другие функции для совместной работы; он возвращает новую функцию, которая последовательно вызывает цепочку входных данных.

Вы также столкнетесь с возможностью компоновки в современных компонентных средах разработки. Вот пример простого набора компонентов React:

const UserCard = ({user, children}) => (
    <div>
        <h1>{user.name}</h1>
        {children}
    </div>
);
 
const UserAvatar = ({user}) => {
    if (user.avatarId) {
        return <img src={`/avatars/${user.avatarId}.png`} />;
    }
    else return <img src={`/avatars/generic.png`} />;
};
 
const UserCardWithAvatar = ({user}) => (
    <UserCard user={user}>
        <UserAvatar user={user} />
    </UserCard>
);

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

Композиция против наследования

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

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

class LogMessage implements LogMessage {
    public function __construct(
        public readonly string $message) : void;
}
 
class LogMessageWithEmail extends LogMessage implements Mailable {
    public function getEmailContent() : string {
        return "New Log Message: {$this -> message}";
    }
}
 
class LogMessageWithUser extends LogMessage implements RelatedToUser {
 
    public function __construct(
        public readonly string $message,
        public readonly int $userId) {}
 
    public function getTargetUserId() : int {
        return $this -> userId;
    }
 
}

Эти занятия могут показаться полезными для начала. Теперь вам не нужны специальные реализации сообщений журнала, такие как наш класс UserLoggedInMessage. Однако есть одна большая проблема: если вам нужно написать сообщение журнала, которое относится к пользователю и отправляет электронное письмо, для этого нет класса. Вы можете написать LogMessageWithEmailAndUser, но вы пойдете по скользкому пути покрытия всех возможных перестановок с помощью «общих» конкретных реализаций классов.

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

Хорошее эмпирическое правило для наследования — использовать его, когда объект является чем-то другим. Композиция обычно является лучшим выбором, когда объект содержит что-то еще:

  • Журнал/электронная почта. Сообщение журнала по своей сути не является электронным письмом, но с ним может быть иметь связанное с ним содержимое электронной почты. Журнал должен включать содержимое электронной почты в качестве зависимости. Если не все журналы будут иметь компонент электронной почты, следует использовать композицию, как показано выше.
  • Пользователь/Администратор. Администратор наследует все действия пользователя и добавляет несколько новых. Это может быть хорошим примером наследования — Администратор расширяет права пользователя.

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

Краткое содержание

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

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




Все права защищены. © Linux-Console.net • 2019-2024