Декомпозиция задачи

Декомпозиция - это разбиение большой задачи на маленькие понятные части. Каждую часть проще описать, реализовать, проверить и потом соединить с остальными.
Зачем это нужно
Сложная задача пугает, если пытаться решить ее одной большой программой. Например: "робот должен ехать по линии, объезжать препятствия, подавать сигнал, запоминать ошибки и останавливаться по кнопке". Если писать все сразу, легко запутаться: где чтение датчиков, где выбор режима, где управление моторами, где отладка.
Декомпозиция помогает разложить поведение на части. Вместо одной огромной задачи появляются небольшие подзадачи: прочитать датчики, определить ситуацию, выбрать действие, задать скорость моторов, вывести сигнал. Каждую подзадачу можно проверить отдельно.
Главная идея
Большой алгоритм можно представить как набор связанных блоков.
сложная задача -> подзадача 1 + подзадача 2 + подзадача 3

Декомпозиция не означает "разрезать как попало". Хорошая часть имеет понятную роль: получает входные данные, выполняет одно осмысленное действие и выдает результат.
Подзадача - это часть общей задачи, которую можно описать отдельно. Например, "считать датчики линии" или "рассчитать скорость моторов".
Как делить задачу
Удобный способ - задавать вопросы о потоке данных и действий.
| Вопрос | Пример ответа для робота |
|---|---|
| Какие данные нужны? | датчики линии, кнопка, расстояние |
| Какие решения принимает программа? | ехать, повернуть, остановиться |
| Какие действия выполняет робот? | задать скорость моторов, включить сигнал |
| Что можно проверить отдельно? | датчики, моторы, индикацию |
| Что повторяется? | чтение датчиков и выбор действия |
Для робота, который едет по линии, разбиение может быть таким:
прочитать датчики линии
определить положение линии
выбрать направление
задать скорость моторов
показать состояние светодиодом
Каждый блок можно сначала описать словами, потом превратить в код.
Функции как способ декомпозиции
В программировании подзадачи часто оформляют функциями. Функция - это именованный фрагмент программы, который можно вызвать из другого места.
Псевдокод:
прочитать_датчики()
выбрать_движение()
управлять_моторами()
показать_состояние()
В блок-схемах вызов функции или подпрограммы можно показывать прямоугольником с дополнительными вертикальными линиями по краям.

Название функции должно объяснять действие. Если функция называется doStuff, по ней ничего не понятно. Если функция называется readLineSensors, уже видно, что она читает датчики линии.
Функция - это отдельная часть программы с именем. Ее можно вызвать, чтобы выполнить заложенные в нее команды.
Функция не обязана быть большой. Часто лучше несколько коротких функций с ясными ролями, чем одна огромная функция, где перемешаны датчики, моторы и логика.
Пример: робот едет по линии
Разложим задачу "ехать по линии" на части.
| Подзадача | Вход | Выход |
|---|---|---|
| Считать датчики | сигналы датчиков | значения левый, правый |
| Определить ситуацию | значения датчиков | линия слева, справа, по центру или потеряна |
| Выбрать действие | ситуация | ехать прямо, повернуть, искать |
| Управлять моторами | действие | скорости левого и правого мотора |
| Показать состояние | ситуация или ошибка | цвет светодиода |
Такой подход помогает тестировать. Можно отдельно проверить, правильно ли читаются датчики. Потом отдельно проверить моторы. Потом соединить их через блок выбора действия.

В главном цикле эти части вызываются по очереди, а затем алгоритм возвращается к новому чтению датчиков.

Если робот едет не туда, декомпозиция сужает поиск. Ошибка может быть в датчиках, выборе действия или моторах. Когда все смешано в одном куске кода, найти место ошибки сложнее.
Уровни детализации
Одна и та же задача может быть описана крупно или подробно.
Крупно:
ехать по линии
Подробно:
прочитать левый датчик
прочитать правый датчик
сравнить значения с порогом
выбрать направление
задать скорость моторов
На блок-схеме можно оставить один крупный блок для общего плана или раскрыть его до конкретных действий.

Для планирования удобно начать крупно, а затем раскрывать блоки. Для программирования нужен такой уровень детализации, где каждую команду можно реализовать.
Как понять, что разбиение получилось
Хорошая декомпозиция помогает отвечать на вопросы без чтения всей программы. Если робот не видит линию, мы идем к блоку чтения датчиков. Если видит линию, но поворачивает не туда, проверяем блок выбора действия. Если решение верное, а колеса крутятся неправильно, смотрим блок управления моторами.
Для каждого блока полезно записать короткий контракт:
| Блок | Что получает | Что должен выдать |
|---|---|---|
| Чтение датчиков | сигналы с пинов | значения датчиков |
| Выбор действия | значения датчиков | команда движения |
| Управление моторами | команда движения | скорости моторов |
Контракт не обязан быть сложным. Он просто фиксирует ожидания. Если входы и выходы блока невозможно описать, блок слишком расплывчатый.
Что запомнить про декомпозицию
Декомпозиция делает сложную задачу управляемой.
| Хороший блок | Плохой блок |
|---|---|
| имеет понятное имя | называется слишком общо |
| выполняет одну роль | делает все сразу |
| можно проверить отдельно | работает только внутри огромной программы |
| имеет понятные входы и выходы | берет данные непонятно откуда |
| помогает читать алгоритм | прячет смысл |
Если блок сложно назвать одним коротким действием, он, вероятно, слишком большой. Его стоит разделить еще раз.
Практика
Задание 1
Разбейте задачу "робот объезжает препятствие" на 5 подзадач. Для каждой подзадачи запишите, какие данные она получает и какой результат выдает.
Задание 2
У вас есть большой блок "ехать по линии". Раскройте его на более мелкие шаги так, чтобы их можно было нарисовать в блок-схеме.
Задание 3
Придумайте названия функций для трех частей робота:
- чтение датчика расстояния;
- управление моторами;
- вывод состояния светодиодом.
Проверьте себя
- Что такое декомпозиция?
- Почему одну большую программу сложнее проверять?
- Чем подзадача отличается от случайного куска кода?
- Зачем функции нужны понятные имена?
- Как декомпозиция помогает искать ошибку в роботе?
Сначала ответьте без подсказки. Ответ можно считать полным, если вы:
- формулируете основную мысль своими словами;
- называете важные условия, ограничения или меры безопасности;
- для схемы, кода или расчёта показываете ход решения и ожидаемый результат.
Если один из пунктов объяснить не получается, найдите соответствующую главу статьи, перечитайте её и повторите ответ.
Словарь статьи
- Декомпозиция - разбиение сложной задачи на более простые части.
- Подзадача - отдельная часть общей задачи.
- Функция - именованный фрагмент программы, который можно вызвать.
- Вход функции - данные, которые функция получает для работы.
- Выход функции - результат, который функция возвращает или создает.
- Модуль - относительно самостоятельная часть программы или устройства.
- Тестирование - проверка, что часть работает как ожидается.
Связанные темы
- Простая программа - как подзадача превращается в последовательность команд.
- Ввод-вывод - как блоки получают данные и выдают результат.
- Ветвление - как отдельный блок выбирает действие.
- Функции в Arduino C++ - как оформлять части программы в коде.
Источники
- Python Tutorial. Defining Functions: https://docs.python.org/3/tutorial/controlflow.html#defining-functions
- Python Tutorial. More Control Flow Tools: https://docs.python.org/3/tutorial/controlflow.html
- Arduino Reference. Functions: https://www.arduino.cc/reference/en/language/functions/
- Arduino Language Reference: https://docs.arduino.cc/language-reference/