Skip to content

Посты

4 практики для снижения количества дефектов

17 ноября 2015 г. • 8 мин чтения

4 практики для снижения количества дефектов

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

Можно ли избежать дефектов? NASA пытается, но они также проводят огромное количество тестирования. Их программное обеспечение буквально критично для миссии — это одноразовое дело. Для большинства организаций это не так, и большой объем тестирования дорог и непрактичен. Хотя нет замены тестированию, можно писать устойчивый к дефектам код без тестирования.

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

1. Пишите простой код

Простота должна быть легкой, но это не так. Написание простого кода — это сложно.

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

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

Microsoft определяет циклическую сложность как:

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

Что такое низкая циклическая сложность? Microsoft рекомендует держать циклическую сложность ниже 25.

Честно говоря, я считаю рекомендацию Microsoft по циклической сложности 25 слишком высокой. Для поддерживаемости и сложности я обнаружил, что идеальный размер метода составляет от 1 до 10 строк с циклической сложностью от 1 до 5.

Bill Wagner в Effective C#, Second Edition писал о размере метода:

Помните, что преобразование вашего кода C# в машинный код — это двухэтапный процесс. Компилятор C# генерирует IL, который доставляется в сборках. JIT-компилятор генерирует машинный код для каждого метода (или группы методов, когда задействовано встраивание) по мере необходимости. Небольшие функции значительно облегчают JIT-компилятору амортизировать эти затраты. Небольшие функции также с большей вероятностью будут кандидатами на встраивание. Дело не только в размере: простота потока управления имеет такое же значение. Меньше условных ветвей внутри функций облегчает JIT-компилятору регистрировать переменные. Это не просто хорошая практика писать более понятный код; это то, как вы создаете более эффективный код во время выполнения.

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

public string ComplexityOf12(int status)
{
    var isTrue = true;
    var myString = "Chuck";

    if (isTrue)
    {
        if (isTrue)
        {
            myString = string.Empty;
            isTrue = false;

            for (var index = 0; index < 10; index++)
            {
                isTrue |= Convert.ToBoolean(new Random().Next());
            }

            if (status == 1 || status == 3)
            {
                switch (status)
                {
                    case 3:
                        return "Bye";
                    case 1:
                        if (status % 1 == 0)
                        {
                            myString = "Super";
                        }
                        break;
                }

                return myString;
            }
        }
    }

    if (!isTrue)
    {
        myString = "New";
    }

    switch (status)
    {
        case 300:
            myString = "3001";
            break;
        case 400:
            myString = "4003";
            break;

    }

    return myString;
}

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

Предыдущая строка немного запутана. В самых простых терминах — сохранение простоты кода снижает количество дефектов.

2. Пишите тестируемый код

Исследования показали, что написание тестируемого кода без написания фактических тестов снижает количество дефектов. Это настолько важно и глубоко, что требует повторения: Написание тестируемого кода без написания фактических тестов снижает количество дефектов.

Это вызывает вопрос: что такое тестируемый код?

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

Написание тестируемого кода может показаться сложным, но на самом деле это легко при следовании принципам Инверсии управления (внедрение зависимостей) и S.O.L.I.D. Вы будете удивлены легкостью и будете удивляться, почему это заняло так много времени, чтобы начать писать таким образом.

3. Проверка кода

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

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

В книге Code Complete Steve McConnell приводит многочисленные тематические исследования преимуществ проверки кода:

  • Исследование организации AT&T с более чем 200 сотрудниками показало увеличение производительности на 14 процентов и снижение дефектов на 90 процентов после введения проверок в организации.
    • Компания Aetna Insurance обнаружила 82 процента ошибок в программе, используя проверки, и смогла сократить ресурсы разработки на 20 процентов.
    • В группе из 11 программ, разработанных одной и той же группой людей, первые 5 были разработаны без проверок. Остальные 6 были разработаны с проверками. После выпуска всех программ в производство первые 5 имели в среднем 4,5 ошибки на 100 строк кода. 6 проверенных имели в среднем только 0,82 ошибки на 100. Проверки сократили ошибки более чем на 80 процентов.

Если эти цифры вас не убедят принять проверку кода, то вы обречены дрейфовать в черную дыру, напевая Take This Job and Shove It Джонни Пейчека.

4. Модульное тестирование

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

Microsoft провела исследование Эффективности модульного тестирования. Они обнаружили, что кодирование версии 2 (версия 1 не имела тестирования) с автоматизированным тестированием немедленно снизило дефекты на 20%, но с дополнительными затратами в 30%.

Другое исследование рассматривало разработку, управляемую тестами (TDD). Они наблюдали увеличение качества кода более чем в два раза по сравнению с аналогичными проектами, не использующими TDD. Проекты TDD заняли в среднем на 15% больше времени для разработки. Побочным эффектом TDD было то, что тесты служили документацией для библиотек и API.

Наконец, в исследовании Покрытие тестами и дефекты после верификации:

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

Пример

Следующий код имеет циклическую сложность 4.

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        AgencySettingsRepository agencySettingsRepository = new AgencySettingsRepository();
        var agencySettings = agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                EmailService emailService = new EmailService();

                emailService.SendTemplate(new[] { newAuthAdmin.Email }, GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value =
                    string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }

Давайте рассмотрим тестируемость приведенного выше кода.

Это простой код?

Да, это так, циклическая сложность ниже 5.

Есть ли какие-либо зависимости?

Да. Есть 2 сервиса AgencySettingsRepository и EmailService.

Можно ли смоделировать сервисы?

Нет, их создание скрыто внутри метода.

Тестируемый ли этот код?

Нет, этот код не тестируемый, потому что мы не можем смоделировать AgencySettingsRepository и EmailService.

Пример рефакторинга кода

Как мы можем сделать этот код тестируемым?

Мы внедряем (используя внедрение через конструктор) AgencySettingsRepository и EmailService как зависимости. Это позволяет нам смоделировать их из теста и тестировать в изоляции.

Ниже приведена рефакторенная версия.

Обратите внимание, как сервисы внедряются в конструктор. Это позволяет нам контролировать, какая реализация передается в конструктор SendMail. Затем легко передать фиктивные данные и перехватить вызовы методов сервиса.

public class SendEmail
{
    private IAgencySettingsRepository _agencySettingsRepository;
    private IEmailService _emailService;


    public SendEmail(IAgencySettingsRepository agencySettingsRepository, IEmailService emailService)
    {
        _agencySettingsRepository = agencySettingsRepository;
        _emailService = emailService;
    }

    public void SendUserHadJoinedEmailToAdministrator(DataAccess.Database.Schema.dbo.Agency agency, User savedUser)
    {
        var agencySettings = _agencySettingsRepository.GetById(agency.Id);

        if (agencySettings != null)
        {
            var newAuthAdmin = agencySettings.NewUserAuthorizationContact;

            if (newAuthAdmin.IsNotNull())
            {
                _emailService.SendTemplate(new[] { newAuthAdmin.Email },
                GroverConstants.EmailTemplate.NewUserAdminNotification, s =>
                {
                    s.Add(new EmailToken { Token = "Domain", Value = _settings.Domain });
                    s.Add(new EmailToken
                    {
                        Token = "Subject",
                        Value = string.Format("New User {0} has joined {1} on myGrover.", savedUser.FullName(), agency.Name)
                    });
                    s.Add(new EmailToken { Token = "Name", Value = savedUser.FullName() });

                    return s;
                });
            }
        }
    }
}

Пример тестирования

Ниже приведен пример тестирования в изоляции. Мы используем фреймворк для создания макетов FakeItEasy.

    [Test]
    public void TestEmailService()
    {
        //Given

        //Using FakeItEasy mocking framework
        var repository = A<IAgencySettingsRepository>.Fake();
        var service = A<IEmailService>.Fake();

        var agency = new Agency { Name = "Acme Inc." };
        var user = new User { FirstName = "Chuck", LastName = "Conway", Email = "chuck.conway@fakedomain.com" }

        //When

        var sendEmail = new SendEmail(repository, service);
        sendEmail.SendUserHadJoinedEmailToAdministrator(agency, user);


        //Then
        //An exception is thrown when this is not called.
        A.CallTo(() => service.SendTemplate(A<Agency>.Ignore, A<User>.Ignore)).MustHaveHappened();

    }

Заключение

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

Подводя итог, Написание простого кода — это сохранение циклической сложности около 5 и небольшого размера метода. Написание тестируемого кода легко достигается при следовании принципам инверсии управления и S.O.L.I.D. Проверка кода помогает вам и команде понять область и написанный вами код — просто необходимость объяснить ваш код выявит проблемы. И наконец, Модульное тестирование может значительно улучшить качество вашего кода и предоставить документацию для будущих разработчиков.

Автор: Chuck Conway — инженер AI с почти 30-летним опытом разработки программного обеспечения. Он создает практические системы AI — конвейеры контента, агенты инфраструктуры и инструменты, которые решают реальные проблемы — и делится тем, что он узнает на этом пути. Свяжитесь с ним в социальных сетях: X (@chuckconway) или посетите его на YouTube и на SubStack.

↑ Вернуться в начало

Вам также может понравиться