Connect with us

Разработка

Расширенные архитектурные правила в SwiftLint: часть 1

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

Опубликовано

/

     
     

«Вы забыли сделать ‘self’ weak».

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

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

Расширенные архитектурные правила в SwiftLint: часть 1

Ваш Senior разработчик после очередного PR в 100+ файлов с вызовами базы данных в основном потоке

Обзоры кода — отличный способ научить младших коллег правилам и обычаям архитектуры, но зачастую они включают в себя одни и те же моменты, которые повторяются снова и снова. Возможно, когда-нибудь ИИ станет решением этой проблемы, но сейчас размер даже небольшого программного проекта быстро опережает контекст коммерческих ИИ. «Я смерджил его, потому что ИИ-ассистент Gemini одобрил мой PR», — никто и никогда (по крайней мере, пока) не скажет так. Если вы не можете четко опредеить свою архитектуру, то и ИИ будет испытывать трудности.

Но что значит определить архитектуру, и как мы можем использовать программу для обеспечения руководства архитектурой — и защитных ограждений — для минимизации бремени проверки кода?

Проще говоря, как дать возможность Senior инженерам один раз высказать свое мнение об архитектуре в интересах всей команды разработчиков и сократить до минимума время, затрачиваемое на проверку и комментирование кода?

Нам нужны Архитектурные линтеры.

Для чего вообще нужен линтинг?

Линтеры появились в конце 1970-х годов, когда был представлен первый инструмент для языка программирования C. В выразительных современных языках линтеры могут обеспечивать соблюдение стиля и синтаксиса. Сегодня линтеры существуют для каждого популярного языка, будь то ESLint (JavaScript), RuboCop (Ruby), ktlint и Detekt (Kotlin) или SwiftLint (Swift) (в свое время был даже очень хороший Objective Clean для Objective-C).

Для современной iOS-разработки SwiftLint является доминирующим — с почти 20 тыс. звезд, изменениями, вносимыми еженедельно, и руководствами по стилю от крупных компаний, это одна из самых надежных библиотек Swift, не относящихся к Apple, которая все еще находится в активной разработке.

Но насколько она полезна?

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

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

Два примера, приведенные на странице SwiftLint в Github, либо легко исправить с помощью трассировки стека (использование принудительного приведения), либо чисто эстетически (отсутствие пробелов после двоеточия). И главное продуктовое обещание SwiftLint — «чистый код» — конечно получится с точки зрения синтаксиса, но «чистый код», как его обычно понимают инженеры, — это гораздо больше, это паттерн, популяризированный Бобом Мартином и наиболее часто визуализируемый так:

Расширенные архитектурные правила в SwiftLint: часть 1

Мы любим эту диаграмму в нашей компании и часто ее используем

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

Спойлер: непросто и практически никто из них.

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

Расширенные архитектурные правила в SwiftLint: часть 1

Линтеры в 2025 про архитектуру

Обеспечение чистоты кода

Представьте, что у вас есть приложение на Swift с 4 слоями, показанными на картинке выше — UI, ViewModels, Domain или UseCase и Repositories. Частью эффективного Чистого кода является обеспечение того, что каждый слой взаимодействует только с нижележащим слоем. Таким образом, если слой UI будет взаимодействовать с чем-либо, кроме слоя ViewModel, это будет нарушением данной архитектуры.

Из-за того, как работают переходные зависимости пакетов в Swift, если ваш слой UI имеет зависимость от ViewModel, а ваш пакет ViewModel имеет зависимость от Domain, а ваш пакет Domain имеет зависимость от Repositoriу, то ваш слой UI может вызвать import Repositories и начать взаимодействовать с объектами слоя Repository напрямую.

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

Вот простой пример:

 pss_viewmodels_no_import_repositories:
      included: "SwiftPackages\\/.*ViewModel\\.swift"
      regex: "import (Repositories)"
      name: "ViewModels may not import repositories; violation of separation of concerns."
      severity: error

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

Можем ли мы гарантировать, что все наши Logic классы (aka Domain/UseCase) выставляют только одну публичную функцию?

Кастомные правила, которые понимают контекст

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

class\s+\w+\s*{(?:(?!\bclass\b)[\s\S])*?\bpublic\s+func\b(?:(?!\bclass\b)[\s\S])*?\bpublic\s+func\b

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

Расширенные архитектурные правила в SwiftLint: часть 1

Наши разработчики, когда мы попросили их написать пользовательское правило SwiftSyntaxRule

import SwiftSyntax

@SwiftSyntaxRule
struct UseCaseExposedFunctionsRule: Rule {
    var configuration = SeverityConfiguration<Self>(.error)

    init() {}

    let message: String = "A UseCase should only expose one public function"

    static let description = RuleDescription(
        identifier: "usecase_exposed_functions",
        name: "UseCaseExposedFunctionsRule",
        description: "A UseCase should only expose one public function",
        kind: .style,
        nonTriggeringExamples: UseCaseExposedFunctionsRuleExamples.nonTriggeringExamples,
        triggeringExamples: UseCaseExposedFunctionsRuleExamples.triggeringExamples
    )
}

private extension UseCaseExposedFunctionsRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override var skippableDeclarations: [any DeclSyntaxProtocol.Type] {
            .allExcept(ClassDeclSyntax.self, ProtocolDeclSyntax.self, StructDeclSyntax.self)
        }

        override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
            if node.isLogicClass && node.memberBlock.nonPrivateFunctions.count > 1 {
                violations.append(node.positionAfterSkippingLeadingTrivia)
            }

            return .skipChildren
        }

        override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
            if node.isLogicProtocol && node.memberBlock.nonPrivateFunctions.count > 1 {
                violations.append(node.positionAfterSkippingLeadingTrivia)
            }

            return .skipChildren
        }

        override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
            if node.isLogicStruct && node.memberBlock.nonPrivateFunctions.count > 1 {
                violations.append(node.positionAfterSkippingLeadingTrivia)
            }

            return .skipChildren
        }
    }
}

private extension ClassDeclSyntax {
    // Check that it is a logic class
    var isLogicClass: Bool {
        name.text.hasSuffix("Logic") || name.text.hasSuffix("UseCase")
    }
}

private extension StructDeclSyntax {
    // Check that it is a logic struct
    var isLogicStruct: Bool {
        name.text.hasSuffix("Logic") || name.text.hasSuffix("UseCase")
    }
}

private extension ProtocolDeclSyntax {
    // Check that it is a logic struct
    var isLogicProtocol: Bool {
        name.text.hasSuffix("Logic") || name.text.hasSuffix("UseCase")
    }
}

private extension MemberBlockSyntax {
    var nonPrivateFunctions: [MemberBlockItemListSyntax.Element] {
        members.filter { member in
            guard let function: FunctionDeclSyntax = member.decl.as(FunctionDeclSyntax.self) else { return false }

            return function.modifiers.isEmpty ||
                function.modifiers.first?.name.text == "public"
        }
    }
}

Что такое AST?

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

Судя по приведенному выше коду, неудивительно, что мало кто из нашей команды пытался пополнить этот список правил, и поэтому наша коллекция правил линтинга на основе AST оставалась бездействующей в течение целого года. Тем не менее, наш список правил линтинга на основе регулярных выражений продолжал пополняться, по несколько штук в месяц, пока однажды в 2023 году Android-проект, в котором не было аналога линтера на основе регулярных выражений и, соответственно, никакой значимой коллекции пользовательских правил, внезапно не начал мчаться вперед…

Далее

В следующем посте мы расскажем о вдохновении от Android и представим Harmonize, архитектурный линтер для Swift.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: