- Сравнивать каскадную и гибкую модели, объяснять роли и события Scrum
- Работать с ветками в Git и безопасно отменять изменения
- Выбирать уровни тестирования и вычислять цикломатическую сложность
- Находить в коде нарушения принципов SOLID
Написать в одиночку скрипт на 200 строк — это программирование; а когда команда из 50 человек годами развивает миллионы строк кода, тестирует их и каждую неделю выпускает обновления, ничего не ломая, — это программная инженерия. Главная трудность здесь не алгоритмы, а сложность, меняющиеся требования и общение между людьми. В этом уроке мы узнаем, как профессиональные команды справляются с этими проблемами.
Жизненный цикл: от каскада к гибким методам
- 1Требования
Что должна делать система (функциональные) и насколько хорошо (скорость, безопасность — нефункциональные)?
- 2Проектирование
Архитектура, модули, база данных, интерфейсы.
- 3Реализация
Написание кода, код-ревью, контроль версий.
- 4Тестирование
Модульные, интеграционные, системные и приёмочные тесты.
- 5Развёртывание и сопровождение
Выпуск, мониторинг, исправление ошибок и новые требования — самая долгая часть жизни системы.
| Критерий | Каскадная модель | Гибкая (agile) |
|---|---|---|
| этапы | последовательно, один раз | повторяются в коротких итерациях |
| изменение требований | дорого | ожидаемо и приветствуется |
| заказчик видит работающую программу | в конце | каждые 1–4 недели |
| где уместна | стабильные требования, строгое регулирование (медицина, авиация) | неопределённый рынок, веб- и мобильные продукты |
Манифест Agile 2001 года ставит на первое место работающий продукт, сотрудничество с заказчиком и готовность к изменениям. Самый популярный гибкий фреймворк — Scrum. В нём три роли: Product Owner (владелец продукта — управляет Product Backlog и приоритетами), Scrum Master (помогает процессу и устраняет препятствия) и Developers (команда, выполняющая работу). Работа идёт спринтами — отрезками фиксированной длины до одного месяца: Sprint Planning, ежедневный 15-минутный Daily Scrum, а в конце Sprint Review (показ результата) и Retrospective (улучшение процесса).
В Product Backlog осталось 150 стори-пойнтов. За три последних спринта команда закрыла 16, 20 и 18 пойнтов. Спринт длится 2 недели. Сколько примерно осталось до релиза?
Показать решениеСкрыть решение
Число спринтов: ⌈150 / 18⌉ = ⌈8,33⌉ = 9 спринтов.
Время: 9 · 2 = 18 недель (около 4 месяцев).
Это прогноз, а не обещание: его обновляют после каждого спринта. Скорость нельзя сравнивать между командами — пойнты у каждой команды свои.
- Cчисло парных каналов общения в команде
- nчисло участников команды
5 человек → 10 каналов, 10 человек → 45 каналов. Этим объясняется закон Брукса: добавление людей в опаздывающий проект часто задерживает его ещё сильнее. Поэтому Scrum-команды держат небольшими (обычно до 10 человек).
Контроль версий с Git
Git — распределённая система контроля версий: на компьютере каждого разработчика хранится полный репозиторий со всей историей. Коммит — «снимок» проекта (изменения, автор, время, сообщение); коммиты образуют цепочку, и каждый назван хешем. Ветка (branch) — подвижный указатель на коммит: новую функцию делают в отдельной ветке, а когда она готова, отправляют на проверку через pull request и сливают (merge) в основную ветку.
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 commitgit revert создаёт новый коммит, отменяющий ошибочный, — история не переписывается, поэтому это безопасно для общих веток.Тестирование
| Уровень | Что проверяет | Скорость и количество |
|---|---|---|
| Модульный (unit) | одна функция или класс в изоляции | миллисекунды, тысячи |
| Интеграционный | совместная работа модулей (код + база данных, API) | секунды, сотни |
| Системный (end-to-end) | вся система глазами пользователя | минуты, десятки |
| Приёмочный | соответствие требованиям заказчика | перед выпуском |
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]
pytest или unittest, запускаемые на CI-сервере при каждом коммите.- Mцикломатическая сложность Маккейба: число независимых путей
- E, Nрёбра и узлы графа потока управления
- Pчисло связных компонент (1 для одной функции)
M — хорошая оценка минимального числа тестов, нужных для покрытия всех ветвей. Функции с M > 10 обычно разбивают на меньшие.
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 — инверсии зависимостей | зависеть от абстракций, а не от конкретных классов |
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.