Skip to content

Посты

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Билл Вагнер в 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 Стива МакКоннелла, Стив приводит многочисленные тематические исследования о преимуществах ревью кода:

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

Если эти цифры не убеждают вас принять ревью кода, то вы обречены дрейфовать в черную дыру, напевая Johnny Paycheck’s 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. Ревью кода помогает вам и команде понять предметную область и код, который вы написали — просто необходимость объяснить ваш код выявит проблемы. И наконец, Модульное тестирование может кардинально улучшить качество вашего кода и предоставить документацию для будущих разработчиков.

Автор: Чак Конвей специализируется на разработке программного обеспечения и генеративном ИИ. Свяжитесь с ним в социальных сетях: X (@chuckconway) или посетите его на YouTube.

↑ Наверх

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