2.12 – Как разработать свою первую программу
Теперь, когда вы узнали некоторые основы программирования, давайте более внимательно рассмотрим процесс создания программы.
Когда вы садитесь писать программу, обычно у вас есть какая-то идея, для которой вы хотите эту программу написать. Программистам-новичкам часто сложно понять, как преобразовать эту идею в реальный код. Но оказывается, что многие из необходимых вам навыков решения задач у вас уже есть, и они получены из повседневной жизни.
Самое важное, что нужно помнить (и самое сложное), – это разработать свою программу до того, как вы начнете писать код. Во многих отношениях программирование похоже на архитектуру. Что произойдет, если вы попытаетесь построить дом, не следуя архитектурному плану? Скорее всего, если вы не очень талантливы, вы получите дом, в котором будет много проблем: кривые стены, протекающая крыша и т.д. Точно так же, если вы попытаетесь программировать до того, как у вас будет хороший план действий, вы, вероятно, обнаружите, что в вашем коде много проблем, и вам придется потратить много времени на исправление проблем, которых можно было бы избежать, немного подумав заранее.
Небольшое предварительное планирование сэкономит вам время и нервы в долгосрочной перспективе.
В этом уроке мы изложим обобщенный подход к преобразованию идей в простые функциональные программы.
Шаг разработки 1. Определение цели
Чтобы написать успешную программу, вам сначала нужно определить, какова ваша цель. В идеале вы должны уметь сформулировать ее в одном-двух предложениях. Часто бывает полезно выразить ее как результат, ориентированный на пользователя. Например:
- Дать пользователю возможность организовать список имен и связанных номеров телефонов.
- Создавать случайные подземелья, которые будут создавать интересные лабиринты.
- Составить список рекомендаций по акциям с высокими дивидендами.
- Смоделировать, сколько требуется времени, чтобы мяч, упавший с башни, ударился об землю.
Хотя этот шаг кажется очевидным, он также очень важен. Худшее, что вы можете сделать, – это написать программу, которая на самом деле не выполняет то, что вы (или ваш начальник) хотели бы!
Шаг разработки 2. Определение требований
Хотя определение задачи помогает определить, какой результат вам нужен, но всё еще остаются неясности. Следующий шаг – подумать о требованиях.
Требования – это модное слово для обозначения как ограничений, которым должно соответствовать ваше решение (например, бюджет, временная шкала, пространство, память и т.д.), так и возможностей, которые программа должна проявлять, чтобы удовлетворять потребности пользователей. Обратите внимание, что ваши требования должны быть сосредоточены на том, «что», а не «как».
Например:
- Номера телефонов должны сохраняться, чтобы их можно было позже извлечь.
- В рандомизированном подземелье всегда должен быть путь от входа к выходу.
- Рекомендации по акциям должны использовать исторические данные о ценах.
- Пользователь должен иметь возможность ввести высоту башни.
- Нам нужна версия с тестовым периодом 7 дней.
- Программа должна выдать результаты в течение 10 секунд после отправки запроса пользователем.
- Программа должна давать сбой менее чем в 0,1% пользовательских сеансов.
Одна задача может привести к множеству требований, и решение не будет «готово», пока оно не удовлетворит все из них.
Шаг разработки 3. Определение инструментов, целей и плана резервного копирования
Если вы опытный программист, на этом этапе обычно нужно выполнить множество разных шагов, в том числе:
- Определение целевой архитектуры и/или ОС, на которой будет работать ваша программа.
- Определение того, какой набор инструментов вы будете использовать.
- Определение того, будете ли вы писать свою программу в одиночку или в составе команды.
- Определение вашей стратегии тестирования / обратной связи / релиза.
- Определение способа резервного копирования кода.
Однако для начинающего программиста ответы на эти вопросы обычно просты: вы пишете программу для собственного использования, самостоятельно, на своей системе, используя приобретенную или загруженную IDE, и ваш код, вероятно, никем не используется, кроме вас. Это упрощает задачу.
Тем не менее, если вы собираетесь работать над чем-то нетривиально сложным, у вас должен быть план резервного копирования вашего кода. Недостаточно просто заархивировать или скопировать каталог в другое место на вашем компьютере (хотя это лучше, чем ничего). Если ваш компьютер выйдет из строя, вы потеряете всё. Хорошая стратегия резервного копирования предполагает полное удаление копии кода из вашей системы. Есть много простых способов сделать это: заархивируйте его и отправьте по электронной почте самому себе, скопируйте в Dropbox или другой облачный сервис, отправьте его по FTP на другой компьютер, скопируйте на другой компьютер в вашей локальной сети или используйте систему контроля версий, которая находится на другом компьютере или в облаке (например, github). Системы контроля версий имеют дополнительное преимущество, заключающееся в том, что они не только могут восстанавливать файлы, но и возвращать их к предыдущей версии.
Шаг разработки 4. Разбейте сложные задачи на простые
В реальной жизни нам часто приходится выполнять очень сложные задачи. Попытка понять, как выполнять эти задачи, может быть очень сложной. В таких случаях мы часто используем метод решения задач сверху вниз. То есть вместо решения одной сложной задачи мы разбиваем ее на несколько подзадач, каждую из которых легче решить по отдельности. Если эти подзадачи всё еще слишком сложны для решения, их можно разбить снова. Постоянно разделяя сложные задачи на более простые, вы можете в конечном итоге достичь точки, когда каждая отдельная задача станет управляемой, если не тривиальной.
Давайте посмотрим на это на примере. Допустим, мы хотим навести порядок в доме. Наша иерархия задач в настоящее время выглядит так:
- Уборка дома
Уборка всего дома – довольно сложная задача, чтобы выполнить ее за один присест, поэтому давайте разберем ее на подзадачи:
- Уборка дома
- Пропылесосить ковры
- Уборка в ванной
- Уборка на кухне
Это более управляемо, поскольку теперь у нас есть подзадачи, на которых мы можем сосредоточиться по отдельности. Однако мы можем разбить некоторые из них еще дальше:
- Уборка дома
- Пропылесосить ковры
- Уборка в ванной
- Вымыть унитаз (фу!)
- Вымыть раковину
- Уборка на кухне
- Очистить столешницы
- Почистить раковину
- Вынести мусор
Теперь у нас есть иерархия задач, ни одна из которых не особо сложна. Выполнив каждый из этих относительно управляемых подпунктов, мы можем выполнить более сложную общую задачу по уборке дома.
Другой способ создать иерархию задач – сделать это снизу вверх. В этом методе мы начнем со списка простых задач и построим иерархию, сгруппировав их.
Например, многим людям нужно ходить на работу или в школу в будние дни, поэтому, допустим, мы хотим решить задачу «пойти на работу». Если вас спросят, какие задачи вы выполняли утром, от вставания с постели до похода на работу, вы могли бы составить следующий список:
- Выбрать одежду
- Одеться
- Съесть завтрак
- Поездка на работу
- Почистить зубы
- Встать с постели
- Приготовить завтрак
- Сесть в свою машину
- Принять душ
Используя метод снизу вверх, мы можем организовать их в иерархию элементов, ища способы группировать элементы, имеющие сходство:
- Встать с постели, чтобы пойти на работу
- Дела в спальне
- Встать с постели
- Выбрать одежду
- Одеться
- Дела в ванной
- Принять душ
- Почистить зубы
- Дела для завтрака
- Подготовить кашу
- Съесть кашу
- Дела , связанные с транспортом
- Сесть в свою машину
- Поехать на работу
- Дела в спальне
Как оказалось, эти иерархии задач чрезвычайно полезны в программировании, потому что, получив иерархию задач, вы, по сути, определили структуру всей программы в целом. Задача верхнего уровня (в данном случае «Уборка дома» или «Пойти на работу») становится main()
(потому что это основная задача, которую вы пытаетесь решить). Подпункты становятся функциями в программе.
Если окажется, что один из элементов (функций) реализовать слишком сложно, просто разделите этот элемент на несколько подпунктов/подфункций. В конце концов, вы должны достичь точки, в которой каждая функция в вашей программе станет тривиальной для реализации.
Шаг разработки 5. Определите последовательность событий
Теперь, когда ваша программа имеет структуру, пришло время определить, как связать все задачи вместе. Первый шаг – определить последовательность событий, которые будут выполняться. Например, когда вы встаете утром, в каком порядке вы выполняете указанные выше задачи? Это могло бы выглядеть так:
- Дела в спальне
- Дела в ванной
- Дела для завтрака
- Дела, связанные с транспортом
Если бы мы писали калькулятор, мы могли бы делать что-то в следующем порядке:
- Получить от пользователя первое число
- Получить от пользователя математическую операцию
- Получить от пользователя второе число
- Рассчитать результат
- Напечатать результат
На данный момент мы готовы к реализации.
Шаг реализации 1. Описание функции main
Теперь мы готовы приступить к реализации. Вышеупомянутые последовательности можно использовать для описания вашей основной программы. Пока не беспокойтесь о входных и выходных данных
int main()
{
// doBedroomThings(); // Дела в спальне
// doBathroomThings(); // Дела в ванной
// doBreakfastThings(); // Дела для завтрака
// doTransportationThings(); // Дела , связанные с транспортом
return 0;
}
Или в случае с калькулятором:
int main()
{
// Получить от пользователя первое число
// getUserInput();
// Получить от пользователя математическую операцию
// getMathematicalOperation();
// Получить от пользователя второе число
// getUserInput();
// Рассчитать результат
// calculateResult();
// Напечатать результат
// printResult();
return 0;
}
Обратите внимание: если вы собираетесь использовать этот метод создания «каркаса» для построения своих программ, ваши функции не будут компилироваться, потому что определения еще не существуют. Закомментируйте вызовы функций до тех пор, пока вы не будете готовы реализовать определения этих функций, – это один из способов решения этой проблемы (и способ, который мы покажем здесь). В качестве альтернативы, чтобы ваша программа скомпилировалась, вы можете создать заглушки для своих функций (создать функции с пустыми телами).
Шаг реализации 2. Реализация каждой функции
На этом этапе для каждой функции вы выполните три вещи:
- определите прототип функции (входные и выходные данные);
- напишите функцию;
- протестируйте функцию.
Если ваши функции достаточно детализированы, каждая функция должна быть довольно простой и понятной. Если какая-либо функция по-прежнему кажется слишком сложной для реализации, возможно, ее необходимо разбить на подфункции, которые можно будет реализовать более легко (или возможно, вы сделали что-то в неправильном порядке, и вам нужно пересмотреть последовательность событий).
Давайте выполним первую функцию из примера калькулятора:
#include <iostream>
// Полная реализация функции getUserInput
int getUserInput()
{
std::cout << "Enter an integer ";
int input{};
std::cin >> input;
return input;
}
int main()
{
// Получить от пользователя первое число
// Обратите внимание, что мы включили сюда код для проверки возвращаемого значения!
int value{ getUserInput() };
std::cout << value;
// Получить от пользователя математическую операцию
// getMathematicalOperation();
// Получить от пользователя второе число
// getUserInput();
// Рассчитать результат
// calculateResult();
// Напечатать результат
// printResult();
return 0;
}
Во-первых, мы определили, что функция getUserInput
не принимает аргументов и возвращает вызывающему объекту значение типа int
. Это отражается в прототипе функции с возвращаемым значением int
и без параметров. Затем мы написали тело функции, которое представляет собой четыре простых инструкции. Наконец, мы внедрили временный код в функцию main
, чтобы проверить правильность работы функции getUserInput
(включая возвращаемое ею значение).
Мы можем запустить эту программу много раз с разными входными значениями и убедиться, что программа ведет себя так, как мы ожидаем в этот момент. Если мы обнаружим, что что-то не работает, значит, проблема в только что написанном коде. Убедившись, что программа работает должным образом, мы можем удалить временный тестовый код и перейти к реализации следующей функции (функция getMathematicalOperation
).
Помните: не реализуйте всю программу за один раз. Работайте над этим поэтапно, проверяя каждый шаг, прежде чем продолжить.
Шаг реализации 3. Заключительное тестирование
После того, как ваша программа будет «завершена», последний шаг – протестировать всю программу целиком и убедиться, что она работает так, как задумано. Если работает не так, то исправить это.
Советы по написанию программ
Делайте свои программы для начала простыми. Часто у начинающих программистов есть грандиозное видение всего, что они хотят от своей программы. «Я хочу написать ролевую игру с графикой, звуком, случайными монстрами и подземельями, с городом, который вы можете посетить, чтобы продать предметы, которые вы найдете в подземелье». Если вы попытаетесь написать что-то слишком сложное для начала, в итоге вы можете быть ошеломлены и разочарованы из-за отсутствия прогресса. Вместо этого сделайте свою первую цель как можно проще, что-то, что определенно в пределах вашей досягаемости. Например, «Я хочу иметь возможность отображать на экране двумерное поле».
Со временем добавляйте новые функции. Когда ваша простая программа работает и работает хорошо, вы можете добавлять в нее функции. Например, как только вы можете отобразить поле, добавьте персонажа, который может ходить. Как только вы научитесь ходить, добавьте стены, которые могут помешать движению. Когда у вас будут стены, постройте из них простой город. Когда у вас будет город, добавьте торговцев. Постепенно добавляя каждую функцию, ваша программа будет постепенно усложняться, в процессе не перегружая вас.
Сосредоточьтесь на одной области за раз. Не пытайтесь писать код для всего сразу и не разбивайте внимание на несколько задач. Сосредоточьтесь на одной задаче за раз. Намного лучше иметь одну рабочую задачу и пять еще не начатых, чем шесть частично работающих задач. Если вы рассредоточите свое внимание, вы с большей вероятностью сделаете ошибки и забудете важные детали.
Проверяйте в процессе работы каждый фрагмент кода. Начинающие программисты часто пишут всю программу за один заход. Затем, когда они компилируют ее в первый раз, компилятор сообщает о сотнях ошибок. Это может не только напугать: если ваш код не работает, вам может быть трудно понять, почему. Вместо этого напишите фрагмент кода, а затем сразу скомпилируйте и протестируйте его. Если он не заработает, вы будете точно знать, в чем проблема, и ее легко исправить. Убедившись, что код работает, переходите к следующему фрагменту и повторите описанные действия. Написание кода может занять больше времени, но когда вы закончите, всё должно работать, и вам не придется тратить вдвое больше времени, пытаясь понять, почему что-то не работает.
Не вкладывайтесь в совершенствование раннего кода. Первый набросок функции (или программы) редко бывает хорош. Более того, программы имеют тенденцию со временем развиваться, поскольку вы добавляете возможности и находите лучшие способы структурировать вещи. Если вы слишком рано вкладываете усилия в доработку своего кода (добавление большого количества документации, полное соответствие передовым практикам, оптимизацию), вы рискуете потерять все эти вложения, когда потребуется изменение кода. Вместо этого прорабатывайте свои функции минимально, а затем двигайтесь дальше. Когда будете уверены в своих решениях, тогда и отшлифовывайте их. Не стремитесь к совершенству – нетривиальные программы никогда не бывают идеальными, и всегда есть что-то еще, что можно было бы сделать для их улучшения. Лучшее враг хорошего, поэтому двигайтесь дальше.
Большинство начинающих программистов сокращают многие из этих шагов и предложений (потому что это кажется большой работой и/или не так весело, как написание кода). Однако для любого нетривиального проекта выполнение этих шагов в долгосрочной перспективе определенно сэкономит вам много времени. Небольшое предварительное планирование избавляет от большого количества отладки в конце.
Хорошая новость заключается в том, что как только вы освоитесь со всеми этими концепциями, они начнут восприниматься вами более естественно. В конце концов вы дойдете до того момента, когда сможете писать целые функции без какого-либо предварительного планирования.