Перейти к содержанию
Educora
Университет25 мин58 / 59

Программная инженерия

Изучи жизненный цикл разработки ПО, гибкие методологии и Scrum, контроль версий с Git, уровни тестирования, принципы SOLID и код-ревью.

Проверь себя
В этом уроке ты узнаешь
  • Сравнивать каскадную и гибкую модели, объяснять роли и события Scrum
  • Работать с ветками в Git и безопасно отменять изменения
  • Выбирать уровни тестирования и вычислять цикломатическую сложность
  • Находить в коде нарушения принципов SOLID

Написать в одиночку скрипт на 200 строк — это программирование; а когда команда из 50 человек годами развивает миллионы строк кода, тестирует их и каждую неделю выпускает обновления, ничего не ломая, — это программная инженерия. Главная трудность здесь не алгоритмы, а сложность, меняющиеся требования и общение между людьми. В этом уроке мы узнаем, как профессиональные команды справляются с этими проблемами.

Жизненный цикл: от каскада к гибким методам

  1. 1
    Требования

    Что должна делать система (функциональные) и насколько хорошо (скорость, безопасность — нефункциональные)?

  2. 2
    Проектирование

    Архитектура, модули, база данных, интерфейсы.

  3. 3
    Реализация

    Написание кода, код-ревью, контроль версий.

  4. 4
    Тестирование

    Модульные, интеграционные, системные и приёмочные тесты.

  5. 5
    Развёртывание и сопровождение

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

КритерийКаскадная модельГибкая (agile)
этапыпоследовательно, один разповторяются в коротких итерациях
изменение требованийдорогоожидаемо и приветствуется
заказчик видит работающую программув концекаждые 1–4 недели
где уместнастабильные требования, строгое регулирование (медицина, авиация)неопределённый рынок, веб- и мобильные продукты

Манифест Agile 2001 года ставит на первое место работающий продукт, сотрудничество с заказчиком и готовность к изменениям. Самый популярный гибкий фреймворк — Scrum. В нём три роли: Product Owner (владелец продукта — управляет Product Backlog и приоритетами), Scrum Master (помогает процессу и устраняет препятствия) и Developers (команда, выполняющая работу). Работа идёт спринтами — отрезками фиксированной длины до одного месяца: Sprint Planning, ежедневный 15-минутный Daily Scrum, а в конце Sprint Review (показ результата) и Retrospective (улучшение процесса).

Пример 1: прогноз по скорости

В Product Backlog осталось 150 стори-пойнтов. За три последних спринта команда закрыла 16, 20 и 18 пойнтов. Спринт длится 2 недели. Сколько примерно осталось до релиза?

Показать решение
Средняя скорость (velocity): (16 + 20 + 18) / 3 = 54 / 3 = 18 пойнтов за спринт.
Число спринтов: ⌈150 / 18⌉ = ⌈8,33⌉ = 9 спринтов.
Время: 9 · 2 = 18 недель (около 4 месяцев).
Это прогноз, а не обещание: его обновляют после каждого спринта. Скорость нельзя сравнивать между командами — пойнты у каждой команды свои.
C = n(n − 1) / 2C = n(n − 1) / 2
где:
  • Cчисло парных каналов общения в команде
  • nчисло участников команды

5 человек → 10 каналов, 10 человек → 45 каналов. Этим объясняется закон Брукса: добавление людей в опаздывающий проект часто задерживает его ещё сильнее. Поэтому Scrum-команды держат небольшими (обычно до 10 человек).

Контроль версий с Git

Git — распределённая система контроля версий: на компьютере каждого разработчика хранится полный репозиторий со всей историей. Коммит — «снимок» проекта (изменения, автор, время, сообщение); коммиты образуют цепочку, и каждый назван хешем. Ветка (branch) — подвижный указатель на коммит: новую функцию делают в отдельной ветке, а когда она готова, отправляют на проверку через pull request и сливают (merge) в основную ветку.

Terminal
git switch -c feature/login          # create and switch to a new branch
git add login.py
git commit -m 'Validate the login form'
git push -u origin feature/login     # then open a pull request

git switch main
git pull                             # get the reviewed and merged work
git revert 3f2a9c1                   # safely undo a bad pushed commit
Типичный рабочий процесс. git revert создаёт новый коммит, отменяющий ошибочный, — история не переписывается, поэтому это безопасно для общих веток.

Тестирование

УровеньЧто проверяетСкорость и количество
Модульный (unit)одна функция или класс в изоляциимиллисекунды, тысячи
Интеграционныйсовместная работа модулей (код + база данных, API)секунды, сотни
Системный (end-to-end)вся система глазами пользователяминуты, десятки
Приёмочныйсоответствие требованиям заказчикаперед выпуском
«Пирамида тестов»: внизу много быстрых модульных тестов, наверху — немного медленных системных.
Python
def is_leap(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

def buggy_is_leap(year):
    return year % 4 == 0

cases = {2024: True, 2023: False, 1900: False, 2000: True}

def run_tests(func):
    failed = [y for y, expected in cases.items() if func(y) != expected]
    return 'all passed' if not failed else 'FAILED for ' + str(failed)

print('is_leap:', run_tests(is_leap))
print('buggy_is_leap:', run_tests(buggy_is_leap))
▸ Ожидаемый результат
is_leap: all passed
buggy_is_leap: FAILED for [1900]
Хорошие модульные тесты проверяют и граничные случаи: 1900 делится на 4, но, делясь на 100 и не делясь на 400, високосным не является. Наивная версия на обычных случаях выглядит верной, и ошибку ловит только этот тест. В реальных проектах то же делают pytest или unittest, запускаемые на CI-сервере при каждом коммите.
M = E − N + 2P (= число решений + 1)
где:
  • Mцикломатическая сложность Маккейба: число независимых путей
  • E, Nрёбра и узлы графа потока управления
  • Pчисло связных компонент (1 для одной функции)

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

Пример 2: цикломатическая сложность

is_leap возвращает A and (B or C), где A — «делится на 4», B — «не делится на 100», C — «делится на 400». Какова её цикломатическая сложность и почему в наших тестах 4 случая?

Показать решение
Каждый логический оператор с сокращённым вычислением (and, or) — точка решения: 2 решения → M = 2 + 1 = 3.
3 базовых пути: A ложно → 2023; A истинно, B истинно → 2024; A истинно, B ложно → результатом становится само C.
Третий путь возвращает C, поэтому проверяем оба его значения: 1900 (C ложно) и 2000 (C истинно).
Значит, M = 3 — минимум для покрытия путей, а 4 теста дают полное покрытие условий — это в точности наш словарь cases.

SOLID и код-ревью

ПринципСмысл
S — единственная ответственностьу класса должна быть лишь одна причина для изменения
O — открытости/закрытостиоткрыт для расширения, закрыт для изменения: новое поведение — новым кодом
L — подстановки Лисковобъект подкласса должен работать везде, где ожидается базовый класс
I — разделения интерфейсанесколько маленьких конкретных интерфейсов вместо одного большого
D — инверсии зависимостейзависеть от абстракций, а не от конкретных классов
Python
from abc import ABC, abstractmethod

class Discount(ABC):
    @abstractmethod
    def apply(self, price): ...

class NoDiscount(Discount):
    def apply(self, price):
        return price

class Percent(Discount):
    def __init__(self, p):
        self.p = p
    def apply(self, price):
        return round(price * (1 - self.p / 100), 2)

class StudentCard(Discount):
    def apply(self, price):
        return max(price - 5, 0)

def checkout(price, discount: Discount):
    return discount.apply(price)

for d in [NoDiscount(), Percent(20), StudentCard()]:
    print(type(d).__name__, checkout(50, d))
▸ Ожидаемый результат
NoDiscount 50
Percent 40.0
StudentCard 45
checkout зависит от абстракции Discount, а не от конкретных скидок (D). Чтобы добавить новый вид скидки, checkout менять не нужно — достаточно нового класса (O). Любой подкласс работает там, где ожидается Discount (L).

Код-ревью — проверка изменения другим разработчиком до того, как оно попадёт в основную ветку. Рецензент смотрит на корректность, тесты, читаемость, безопасность и дизайн; стиль же должны проверять автоматические инструменты (линтеры, форматтеры). Помимо поиска ошибок, ревью распространяет знания по команде: каждую часть системы знают минимум двое. Небольшие pull request'ы (меньше нескольких сотен строк) читают внимательнее.

Главное

  • Жизненный цикл: требования → проектирование → реализация → тестирование → развёртывание и сопровождение; гибкие методы повторяют его короткими итерациями.
  • Scrum: Product Owner, Scrum Master, Developers; спринты до месяца и четыре события.
  • Git: коммит — снимок, ветка — указатель; не переписывай историю общей ветки — используй revert.
  • Пирамида тестов: много модульных, мало системных; цикломатическая сложность M = решения + 1.
  • SOLID даёт устойчивый к изменениям дизайн; код-ревью ловит ошибки и распространяет знания; каналы общения растут как n(n − 1)/2.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Сколько парных каналов общения в команде из 8 человек?