Анализ задания и предметная область

Одна и та же формулировка может описывать совершенно разные задачи. Прежде чем писать код, нужно понять контекст: для кого мы решаем, какие данные считаются корректными, что означает «правильный ответ». Здесь мы разберём несколько примеров, где наивная реализация формально верна, но фактически бесполезна.

Постановка задачи Контекст Модель данных

Программист решает не «задачу», а задачу в контексте

В учебниках формулировки задач даны в вакууме. В реальной работе любая задача приходит с контекстом: где будет использоваться программа, кто её заказчик, какие данные на входе, что считается ошибкой. Разберём три типичных примера.

🎯

Формулировка обманчиво проста

«Решить линейное уравнение» — понятно каждому школьнику. Но что если это уравнение с комплексными числами или с матрицами? Постановка меняется — меняется и код.

🏠

Реальный объект ≠ примитивный тип

Адрес — это не «одна строка». Время года на Марсе отличается от земного. Программа, которая этого не учитывает, формально работает, а по сути — нет.

📋

Правильный ответ зависит от контекста

Уравнение 0 · x = b имеет бесконечно много решений при b = 0 и ни одного при b ≠ 0. Наивная программа скажет «x = b / 0» и упадёт.

Пример 1: «решить линейное уравнение»

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

Написать программу, которая решает линейное уравнение. Формулировка из учебного задания

Пример 2: «адрес» — это не строка

Кажется очевидным: адрес — это текст. Но в реальных задачах адрес состоит из многих частей и используется по-разному.

Создать программу для учёта клиентов. Для каждого клиента хранится ФИО, адрес и телефон. Учебное задание по структурам

❌ Наивно: адрес — одна строка

struct Client {
    string name;
    string address;   // ← "г. Москва, ул. Пушкина, д. 10, кв. 5"
    string phone;
};

// Проблемы:
// 1. Нельзя вывести всех клиентов из
//    конкретного города.
// 2. Нельзя проверить корректность
//    индекса или номера дома.
// 3. Сортировка по городу/улице
//    невозможна без парсинга строки.
// 4. Дубли домов и корпусов
//    приходится обрабатывать
//    вручную.
// 5. Нет проверки, что
//    адрес вообще существует.

✓ Продуманно: адрес — составной объект

struct Address {
    string country;
    string region;
    string city;
    string street;
    int    house;
    string building;   // корпус, литера
    int    apartment;
    string postal_code;
};

struct Client {
    string  name;
    Address address;
    string  phone;
};

// Что это даёт:
// 1. Можно отфильтровать по городу:
//    if (c.address.city == "Москва")
// 2. Проверить корректность
//    индекса и номера дома.
// 3. Сортировка по любому полю.
// 4. Легко добавить новые поля
//    (например, «район»).

Почему это важно на практике

В банковском приложении, в доставке, в госуслугах адрес используется каждый день: «найти всех клиентов в этом городе», «проверить, что индекс соответствует региону», «отсортировать по улице». Если адрес — одна строка, всё это превращается в хрупкий парсинг с регулярками — источник багов.

Проектирование структуры данных — это первый шаг решения задачи, и часто он важнее, чем сам алгоритм.

Как это связано с лабораторными. В ЛР 3 семестра 2 (классы) многие варианты просят хранить ФИО, адрес, телефон. Именно там удобно сразу проектировать составные типы: struct Address, struct FullName. Преподаватель обычно это приветствует.

Пример 3: «вывести время года по номеру месяца»

Классическая задача для отработки switch. Но если планета — не Земля, всё меняется. А на Земле ответ зависит от полушария.

Пользователь вводит номер месяца (1–12). Вывести время года. Учебная задача по switch

❌ Наивно: только для Земли и северного полушария

switch (month) {
    case 12: case 1: case 2:
        cout << "Зима"; break;
    case 3: case 4: case 5:
        cout << "Весна"; break;
    case 6: case 7: case 8:
        cout << "Лето"; break;
    case 9: case 10: case 11:
        cout << "Осень"; break;
}

// Работает ровно для одного случая:
// Земля, Северное полушарие.
// В Австралии — неверно.
// На Марсе — неверно.
// На Венере — бессмысленно.

✓ Продуманно: с учётом полушария и планеты

// Сезон = функция от планеты и полушария
Season getSeason(Planet p, Hemisphere h, int month) {
    if (p == VENUS) return NO_SEASONS;

    // Для простоты — сдвиг на полгода
    // для южного полушария
    int m = (h == SOUTH) ? ((month + 5) % 12) + 1 : month;

    // Длины времён года зависят от планеты:
    // на Марсе наклон оси и орбита другие.
    return planetSeasonTable[p][m];
}

// Что даёт:
// • Корректный результат на Земле везде.
// • Легко добавить Марс, Титан и другие.
// • Легко объяснить заказчику.

Что скрывается за простой формулировкой

Полушарие. На Земле южное полушарие имеет противоположные сезоны: январь — лето, июль — зима.

Широта. На экваторе нет четырёх сезонов — есть сезон дождей и сухой сезон. В полярных областях — полярный день и полярная ночь.

Планета. Времена года определяются наклоном оси вращения и эксцентриситетом орбиты. У Венеры наклон оси всего 3° — времён года нет. У Урана ось наклонена почти на 98° — там экстремальные сезоны по 42 года.

Учебный смысл. Даже учебная задача на switch может быть поставлена так, что нужно задуматься о контексте — а не просто «переписать формулу из учебника».

Что ещё прячется за простыми формулировками

Ещё несколько примеров, где наивный подход ломается о реальность.

«Найти среднее значение»

Среднее арифметическое? Медиана? Взвешенное? Среднее по времени (с учётом timestamp)? Для финансовых данных — скользящее среднее. Для отчёта по ошибкам — медиана устойчивее к выбросам. В каждой области своё «среднее».

«Найти расстояние между точками»

На плоскости — теорема Пифагора. На сфере (Земля) — формула гаверсинусов. В метрике такси — сумма модулей разностей координат. В пространстве Минковского — с другим знаком у времени. Выбор метрики определяется задачей.

«Проверить, является ли число простым»

Для учебной задачи — перебор делителей до √n. Для криптографии с числами в 2048 бит — вероятностные тесты Миллера-Рабина. Для 1 — не простое по определению. Для отрицательных — не определено.

«Отсортировать список студентов»

По алфавиту фамилии? С учётом «ё» и регистра? По среднему баллу? По дате зачисления? Что если две фамилии совпадают — какой второй критерий? Стабильная сортировка или нет?

«Найти максимальный элемент»

Если массив пуст? Если несколько максимумов — вернуть первый, последний или все? Если элементы — числа с плавающей точкой, то как сравнивать NaN? Есть ли отрицательные значения?

«Прочитать число с клавиатуры»

Целое или вещественное? Может ли быть отрицательным? Что делать, если пользователь ввёл букву? Разделитель — точка или запятая? Максимальное значение?

Чек-лист перед началом работы

Задайте эти вопросы — сначала себе, потом (если есть возможность) заказчику. Ответы часто меняют структуру программы.

Кто пользователь? Школьник, бухгалтер, инженер, программа-робот? От этого зависит интерфейс, формат ввода, точность вычислений.
Где будут использоваться данные? Только в этой программе, в общей базе, в отчёте, в другой стране? От этого зависит модель.
Какие граничные случаи? Пустой ввод, ноль, максимальное значение, отрицательное число, очень большое число.
Что считается ошибкой? Ввести ноль в знаменателе, ввести букву вместо числа, превысить лимит попыток.
Есть ли стандарт в предметной области? Единицы измерения (СИ или другие), форматы дат (ISO 8601), форматы адресов (КЛАДР/ФИАС), коды стран.
Что будет дальше с результатом? Печать, сохранение в файл, отправка в API, ручная проверка человеком. Формат результата зависит от этого.
Есть ли ограничения по точности? Числа с плавающей точкой дают 0.1 + 0.2 ≠ 0.3. Для денег нужны целые копейки или decimal.
Кто будет поддерживать программу? Если вы сами через год — делайте проще. Если команда — документируйте и придерживайтесь общих соглашений.
Не все вопросы имеют ответ сразу. Иногда полезно написать «я предполагаю, что…» и явно зафиксировать это в комментарии к коду. Это лучше, чем молчаливое допущение.

Пример: как продуманная структура упрощает код

Сравним две реализации задачи «найти всех клиентов из заданного города».

Адрес — строка: хрупкий код
// Простая структура
struct Client {
    string name;
    string address;   // "г. Москва, ул. Пушкина, д. 10"
};

// Поиск клиентов по городу
vector<Client> fromCity(
    const vector<Client>& clients,
    const string& city
) {
    vector<Client> result;
    for (const auto& c : clients) {
        // Ищем подстроку «г. Москва» — но что если
        // пользователь ввёл без «г.» или с «город Москва»?
        if (c.address.find(city) != string::npos) {
            result.push_back(c);
        }
    }
    return result;
}

// Сломалось, если:
// • другой формат записи;
// • город — часть названия улицы;
// • опечатка в исходных данных.
Адрес — структура: надёжный код
struct Address {
    string country;
    string city;
    string street;
    int house;
};

struct Client {
    string  name;
    Address address;
};

// Поиск клиентов по городу
vector<Client> fromCity(
    const vector<Client>& clients,
    const string& city
) {
    vector<Client> result;
    for (const auto& c : clients) {
        if (c.address.city == city) {   // точно и надёжно
            result.push_back(c);
        }
    }
    return result;
}

// Работает независимо от порядка слов,
// регистра, формата ввода.
// Можно легко добавить поиск по региону,
// по улице, по индексу.
Правило проектирования. Если объект реального мира состоит из нескольких частей, и эти части используются отдельно — делайте отдельный тип. Это защищает от ошибок и упрощает код во всех местах, где объект используется.

Задание

  1. Возьмите одну задачу из курсовых или лабораторных и выпишите ответы на все вопросы чек-листа. Обсудите с преподавателем.
  2. Для задачи «найти среднее значение чисел» определите, что именно нужно: среднее арифметическое, медиана, взвешенное или что-то ещё. Обоснуйте выбор.
  3. Спроектируйте модель данных для «ФИО» так, чтобы учитывать ситуации с отсутствующим отчеством, двойной фамилией, фамилией с дефисом.
  4. Расширьте пример с временами года: добавьте поддержку Луны (для неё сезонов нет — только освещённость) и Титана (спутник Сатурна, с methane seas, но с seasons).
  5. Найдите в интернете два разных формата адресов (например, российский и американский). Какие поля нужны для каждого? Спроектируйте общую модель, которая поддерживает оба.
  6. Придумайте формулировку задачи, где слово «среднее» означает три разных вещи в зависимости от контекста, и покажите на примерах, как меняется ответ.