Недавно я закончил читать прекрасную книгу под названием «Философия дизайна программного обеспечения», написанную Джоном Оустерхаутом. В этой статье я собираюсь обобщить все, чему научился за время своего путешествия, и попытаться применить это к экосистеме Vue.js.

Сложность

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

  1. Устраните сложность, написав более короткий и очевидный код
  2. Инкапсулируйте сложность в компоненты (или модули), чтобы другие разработчики, работающие над проектом, не подвергались ее воздействию одновременно.

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

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

Определение

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

Симптомы

Сложность программных систем имеет 3 основных симптома:

  1. Изменить усиление

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

Это означает, что в нашей системе много взаимосвязей, что порождает сложность.

  1. Познавательная нагрузка

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

  1. Неизвестные неизвестные

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

Причины

Итак, что на самом деле может вызвать эти симптомы в наших программных системах? следует учитывать два основных фактора:

  1. Зависимость

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

  1. Безвестность

Неясность возникает, когда важная информация не очевидна.

В проектах JavaScript/TypeScript несуществующий JSDoc или повсеместное использование типа любой — два примера неясности.

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

Рабочий код !достаточно

В этом мире есть 10 типов разработчиков. Те, кто может читать двоичный код, и те, кто не может. Шучу 😄.

Есть два типа разработчиков: тактические и стратегические.

Тактический

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

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

Стратегический

Программисты-стратеги понимают, что работающего кода недостаточно.

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

Стратегические программисты делают упреждающие или реактивные инвестиции. такой как:

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

Мы должны стремиться быть немного более стратегическими и немного менее тактическими в нашей карьере.

Глубина модуля

Каждый модуль или компонент состоит из двух частей: интерфейса и реализации.

Лучшие модули — это те, интерфейсы которых намного проще, чем их реализации.

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

Интерфейсы должны быть спроектированы так, чтобы максимально упростить использование общего регистра (KISS).

Сокрытие информации или ее утечка!

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

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

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

Другой слой, другая абстракция

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

Сквозные переменные

Представьте, что у вас есть длинная цепочка методов, и через нее проходит переменная. Эта переменная создает сложность, поскольку заставляет все звенья этой цепочки знать о ее существовании, даже если одно из них ее не использует.

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

Пример сквозных переменных в Vue.js — использование реквизита.

В приведенном выше примере мы передаем динамический URL-адрес, на который нам нужно отправлять полезную нагрузку всякий раз, когда мы проверяем что-то в нашем списке дел. Предположим, у нас есть доступ только к этому URL-адресу в корневом файле index.vue, поэтому мы не можем просто жестко закодировать его в файле TaskItem.vue, поэтому мы вынуждены передавать его вниз по дереву компонентов вот так.

Теперь представьте, если требования изменятся, в этом случае нам нужно изменить 3 места, чтобы выполнить одно дело!

Сквозные функции

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

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

Лучше вместе или лучше порознь

Один из самых фундаментальных вопросов в разработке программного обеспечения: учитывая две функциональные части, должны ли они быть реализованы вместе в одном месте или их реализации должны быть разделены?

Разделение компонентов может привести к:

  • Чем больше количество компонентов, тем сложнее отслеживать их все (больше хлопот из-за большего количества движущихся частей).
  • Дополнительный код для управления компонентами
  • Разделение затрудняет одновременный просмотр компонентов или даже осознание их существования (увеличение неизвестных неизвестных).
  • Дублирование кода

Вот несколько признаков того, что два фрагмента кода связаны:

  • Они делятся информацией
  • Они используются вместе (только убедительно, если двунаправленные)
  • Они пересекаются концептуально
  • Трудно понять один из фрагментов кода, не глядя на другой.

Мы должны:

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

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

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

Потяните сложность вниз

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

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

Определить несуществующие ошибки

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

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

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

Код, который не был выполнен, не работает.

Лучший способ устранить сложность обработки исключений — определить ваши API так, чтобы исключений не было.

Маскировка исключений

Сократите количество мест, где должны обрабатываться исключения.

Агрегация исключений

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

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

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

Просто крашитесь, когда это имеет смысл. Точно так же имеет смысл исключить и другие особые случаи.

Дизайн дважды

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

Написание лучших комментариев

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

  • Хороший код самодокументируется.
  • У меня нет времени писать комментарии.
  • Комментарии устаревают и вводят в заблуждение.
  • Комментарии, которые я видел, все бесполезны. Зачем беспокоиться?

Все эти оправдания развенчаны в книге.

Хорошая документация помогает с двумя из трех способов, которыми сложность проявляется в программных системах: когнитивная нагрузка и неизвестные неизвестные.

Комментарии должны уменьшить неизвестность

Учтите следующие моменты:

  • Определите правила комментирования.
  • Не повторяйте код. Комментарии на том же уровне детализации, что и код, бесполезны и являются тревожным сигналом. Задайте себе вопрос: «Может ли кто-то, кто никогда не видел код, написать комментарий, просто взглянув на код рядом с комментарием?». Если ответ положительный, комментарий бесполезен.
  • Комментарии дополняют код, предоставляя информацию на другом уровне детализации, комментарии низкого уровня добавляют точности, комментарии высокого уровня предлагают интуицию.
  • Если комментарии к интерфейсу должны также описывать реализацию, то класс/метод неглубокий.
  • Задержанные комментарии — это плохие комментарии, сначала напишите комментарии.
  • Комментарии — это инструмент дизайна. Комментарии служат канарейкой в ​​​​угольной шахте сложности. Если метод или переменная требует длинного комментария, это красный флаг того, что у вас нет хорошей абстракции.
  • Ранние комментарии — это забавные комментарии. Ранние комментарии на самом деле не слишком дороги.

Выбор имен

Хорошие имена — это форма документации/абстракции. Хорошие имена обладают двумя свойствами: точностью и согласованностью.

Последовательность

Всегда используйте общее имя для данной цели. Никогда не используйте общее имя для чего-либо, кроме данной цели. Убедитесь, что цель достаточно узкая, чтобы все переменные с таким именем имели одинаковое поведение.

Чем больше расстояние между объявлением имени и его использованием, тем длиннее должно быть имя.

Точность

Расплывчатые имена — это красный флаг. Можно использовать общие имена, такие как i и j в цикле. Но если петля становится такой длинной, что вы не можете увидеть ее всю сразу, тогда уместно более описательное имя. Также избегайте слишком конкретных имен. - Если вам трудно придумать точное, интуитивно понятное и не слишком длинное имя для конкретной переменной, это говорит о том, что переменная может не иметь четкого определения или назначения.

Последовательность

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

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

Не меняйте существующие соглашения. Наличие «лучшей идеи» не является достаточным оправданием для внесения несоответствий. Ценность согласованности над непоследовательностью почти всегда больше, чем ценность одного подхода над другим.

Как говорится:

Находясь в Риме, поступай, как римляне.

Код должен быть очевиден

Программное обеспечение должно быть разработано так, чтобы его было легко читать, а не писать.

Вещи, которые делают код более очевидным:

  • Выбор хороших имен (как упоминалось выше)
  • Консистенция (как указано выше)
  • Методы общего назначения, такие как пустое пространство или комментарии.

Вещи, которые делают код менее очевидным:

  • Программирование, управляемое событиями
  • Общие контейнеры
  • Различные типы для объявления и распределения
  • Код, который нарушает ожидания читателя

Одной из плохих практик Vue является обработка бизнес-логики и изменение состояния в наблюдателях. Наблюдатели или watchEffect должны быть реализованы по принципу «запустил и забыл». Кроме того, использование Provide и Inject потенциально может быть неочевидным. используйте эти инструменты с умом.

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

ТВЕРДЫЙ

1. S для «принципа единой ответственности»

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

Ваши компоненты должны делать одну вещь, и если вы хотите, чтобы происходило несколько вещей, вы должны скомпоновать их так, чтобы читатель понял: «О, это компонент, который собирает 3 других компонента, которые делают только одну вещь, поэтому он должен уметь делать только 3 вещи! ”

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

Прекрасным примером SRP является tailwind CSS. В первую очередь утилитарная CSS-инфраструктура, содержащая такие классы, как flex, pt-4, text-center и rotate-90, которые можно составить для создания любого дизайна прямо в вашей разметке.

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

2. O для «принцип открыт-закрыт»

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

Слоты — отличный пример того, как мы можем облегчить работу с расширениями в Vue. Слоты помогут нам расширять наши компоненты с помощью любого шаблона, который мы хотим, и в то же время сохраняя наше состояние внутри!

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

3. L для «принципа подстановки Лисков»

Лично я не думаю, что мы увидим много LSP в экосистеме Vue, потому что я предполагаю, что не так много компаний или команд, которые используют компоненты класса, но в любом случае, вот описание!

«Принцип подстановки Лисков» просто подразумевает, что когда экземпляр класса/компонента передается или расширяется до другого класса/компонента, наследующий класс должен иметь вариант использования для всех свойств и поведения унаследованного класса.

4. I за «Принцип разделения интерфейсов»

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

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

5. D для «инверсии зависимости»

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

Позвольте мне показать вам пример в Vue: слабая связь между внутренними API и свойствами компонентов.

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

Таким образом, если API или требования изменятся, вы измените только реализацию сопоставления, и все дерево компонентов останется нетронутым!

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

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

Вот и все! Спасибо за ваше время. Если у вас есть какие-либо предложения или идеи, пожалуйста, поделитесь ими со мной в разделе комментариев. Хотелось бы узнать ваше мнение на эту тему! ❤️