Статья
Версия для печати
Обсудить на форуме (8)
Отзыв на статью «Hello world в embedded исполнении»

© Dimka 8.12.2011


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

1. Критический анализ

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

1.1. Роль индивидуальности

Вообще этот (я бы даже сказал деликатный) вопрос не очень уместен при обсуждении текстов технической тематики. Однако автор не стал себя ограничивать сухим, чётким и лаконичным техническим языком. С одной стороны, живость слога всегда украшает литературное творчество, за что стоит поблагодарить автора. Но с другой стороны, именно лирические отступления в тексте превращают сугубо техническую статью в литературное произведение несколько иного жанра. У автора, на мой взгляд, вышла повесть дидактического характера с элементами панегирика самому себе.
Как известно всем причастным, среди столпов технической культуры и точных наук есть и объективность, и беспристрастность. Именно по этой причине стиль технических текстов сух, лаконичен и безличен, а содержание инженерных работ, как правило, включает в себя обзор и анализ альтернатив. К сожалению, я вынужден констатировать, что автор в ряде существенных моментов пренебрёг этими ценными качествами. Именно это небрежение, во-первых, заставило меня помимо прочего коснуться и личности автора, а во-вторых, избрать для отзыва отнюдь не лаконичную и не вполне сухую форму.
Излагаемый автором метод достигает своего дидактического апофеоза в пятой части его работы.

Цитата
Как отличить настоящего мастера от неумехи, который лишь старается выглядеть мастером? По инструментам? Нет, среди моих знакомых есть несколько, которые питают страсть к коллекционированию дорогих и действительно хороших инструментов, но ни разу не пустили их в дело, наслаждаясь самим фактом обладания красивой игрушкой. По словам и амбициям? Тем более не вариант. По конечному результату? В принципе верно, но откровенная халтура может быть прекрасно оформлена внешне, и чтобы понять это, потребуется некоторое время.
Если есть возможность, взгляните на рабочее место мастера. Если на верстаке вперемешку разбросаны инструменты, готовые детали, заготовки, обрезки, чертежи, справочники и много всякой прочей всячины, образуя миниатюрную модель первозданного хаоса, в котором невозможно что-то быстро найти, и не оставляя места для работы, вывод один — о качестве здесь не может быть и речи. Конечно же, и при работе мастера рабочее место неизбежно захламляется, без этого никак; однако мастер не дает беспорядку распространяться бесконтрольно, приводя рабочее место в порядок после каждой операции.
Давным-давно, когда двигатели внутреннего сгорания были еще карбюраторными, мне посчастливилось видеть работу одного карбюраторщика, настоящего артиста своего дела. Меня просто поразила стерильная чистота его рабочего места, хотя карбюратор видавшего виды авто — отнюдь не самая чистая штука. А уж обилию и состоянию его инструментов мог бы позавидовать, пожалуй, иной нейрохирург (равно как и умению с ними обращаться). Стоит ли говорить, что автомобиль преображался после посещения мастера, его невозможно было узнать — он просто радовался дороге, как застоявшийся в стойле конь.
К сожалению, это был единственный случай столь виртуозного мастерства в долгой практике моего вынужденного знакомства с автосервисами.

Здесь мы видим, во-первых, откровенное раскрытие идеалов автора, во-вторых, сожаление о несовершенстве земных реалий и, в-третьих, определённый намёк читателю. Конечно, идеал труднодостижим, но, к счастью, есть люди, которые к нему немного ближе прочих. Равнение на этих не побоюсь этого слова титанов — священный долг всякого человека, претендующего на гордое звание Профессионала. Правильно сориентировавшийся читатель в ряду этих титанов конечно выделит скромное место и для автора.
Стоило бы простить автору маленькие человеческие слабости, если бы этот панегирик не содержал дидактических наставлений, таким образом превращаясь в некотором роде проповедь пастыря к пастве с рассказом о мире горнем.
Объективность рассмотрения какого-либо вопроса требует и некоторой рефлексии — в том числе и самокритики к избранной точке зрения. Объективность — ценное профессиональное качество инженера, поскольку такой специалист работает с предметами, а не с чувствами и мнениями. В паре «железо»-специалист лишь последний хочет и может познать первое. Нет никакого проку в ожидании чуда, что «железо» когда-нибудь поймёт и угадает чаяния специалиста. Посему инженер, стремящийся достичь результата, вынужден оставить все надежды «договориться» и вплотную заняться объектом — быть объективным. Беспристрастность же стоит несколько особняком. С одной стороны, хорошо, когда специалист загорается идеей — это его мотивирует в работе, с другой стороны, плохо, когда страсть мешает объективному взгляду на реальность. Поэтому во избежание слепой веры в идеалы автора мне хочется обратить внимание и самого автора и других читателей на некоторые сопутствующие этому идеалу явления. Обсуждение этих явлений присутствует во всех трёх разделах критического анализа.
Прежде всего стоит признать тот объективный факт, что профессионализм — это скорее общественная оценка деловых качеств, нежели субъективное отношение к каким-то характеристикам личности. Эта оценка высока, когда специалист являет обществу значимые результаты, делает это эффективно и высококачественно. Коротко говоря, профессионализм — это определённая репутация специалиста в обществе. Например, мне известен замечательный профессионал, трудящийся на той же ниве, что и автор, — разработка embedded-решений, — имеющий солидное портфолио работ в разных предметных областях вплоть до авиационных систем. Вот его рабочее место.



Для всех очевидно — оно являет собой полную противоположность обрисованному автором идеалу. Конечно же автор выразит сомнение в профессионализме владельца такого стола, и я не буду бороться с этими сомнениями, предъявлять какие-либо доказательства, устраивать сравнения. Я лишь хочу обратить внимание на сам факт существования серьёзных разногласий в оценке профессионализма. Ведь последствия таких разногласий весьма значительны.
Сам же автор признаёт: титаны, подобные ему, — настолько редкое в природе явление, что за свою немалую жизнь автор лишь единожды в автосервисе встретил «родственную душу». Столь же прискорбное для автора положение дел наблюдается и в его профильной сфере деятельности, о чём он сразу заявляет ещё в первой части своей работы.

Цитата
Специалисты в области электроники, не имеющие навыков профессионального программирования, обычно смелее (ибо предрассудок, что программировать может каждый, весьма распространен) и находят в себе достаточно мужества, чтобы взяться за разработку кода; однако, если задача нетривиальна, этот код оказывается весьма далеким от совершенства, поскольку программная инженерия — сложная дисциплина, овладеть которой без специальной подготовки невозможно.

На практике это означает, что автор более склонен работать индивидуально, нежели в команде. Собрать команду из двух таких людей менеджеру будет архисложно, а из трёх и более — почти невозможно. В команде из одного Профессионала и прочих людей попроще, разумеется, Профессионал будет претендовать на руководящие позиции. И с этих позиций он, разумеется, установит личный порядок, который прочие члены команды должны будут либо соблюдать, либо следовать на выход с вещами.
Когда я читал, с какой любовью, тщательностью и аккуратностью автор создаёт папки проекта, пишет в них readme-файлы и делится с читателем своим чувством глубокого удовлетворения, меня охватывали совсем другие чувства. Скорее это были чувства скуки, тоски и уныния. (Опрос нескольких человек, читавших статью, показал, что я по крайней мере не одинок в этом впечатлении.) Посему, попади я под начало такого Профессионала, мне бы следовало сразу собирать вещи. И вовсе не потому, что я отрицаю некие технологии разработки, а исключительно из-за различных представлений об идеальном.
Представить же команду из двух равно амбициозных Профессионалов с несовпадающими идеалами просто страшно. В лучшем случае их совместная работа будет подобна противостоянию супердержав времён Холодной войны с разделом сфер влияния, а в худшем это будут битвы вроде Сталинградской.
Вопрос о том, что лучше: высокопрофессиональные одиночки или профессиональные команды — это вопрос, конечно, непростой. Он возвращает нас к дилемме времён промышленной революции. Тогда ремесленные цехи активно сопротивлялись внедрению промышленных способов, ибо разве могут сравниться в мастерстве и качестве работы опытный ремесленник и толпа в недавнем прошлом крестьян, нанятых рабочими на мануфактуру? Никакая мануфактура не способна создать шедевра, но никакой ремесленник не способен достичь производительности труда мануфактуры, а значит дешевизны продукции, несмотря на её более низкое качество. Личные симпатии к тому или иному классу тружеников у каждого свои, но история свидетельствует, что ремесленники-одиночки уступили место массовому производству.
Мне кажется, что ценными личностными качествами стоит признать умение работать как в одиночку, так и в команде. В последнем случае это требует совсем другого мировосприятия, нежели демонстрирует нам автор. В слаженной команде нужны и понимание, и признание чужих индивидуальностей, их уважение, и учёт альтернативных мнений, и диалог, и совместная выработка приемлемых для всех рабочих процедур и порядка. Чтобы общение специалистов было плодотворным, вовсе неуместны безапелляционные оценки хорошо-плохо, такие оценки всегда должны дополняться контекстом, включающим в себя и объект, и граничные условия рассматриваемой ситуации. И, разумеется, совсем неуместны попытки морального осуждения окружающих, считая свои собственные предпочтения мерилом всех профессиональных качеств.

1.2. Роль метода

Другой небезынтересный аспект статьи «Hello world в embedded исполнении» касается методологии в философском смысле. Если попытаться кратко охарактеризовать результат, полученный автором, пожалуй, самым точным было бы выражение «из пушки по воробьям». В данном случае мой тезис таков: это не столько свойство применённого автором метода, сколько своеобразное отношение автора к методу. Поэтому на вполне обоснованное беспокойство читателей: всегда ли TDD и прочие технологические приёмы разработки приводят к таким неприглядным последствиям? — я могу ответить: конечно, нет. Почему же получилось так, как получилось?
Судя по пафосу как этой статьи, так и других работ, складывается общее впечатление о методологических предпочтениях автора. Кратко говоря, он верит в свой метод точно так же, как охотник верит в своё ружьё, рыбак — в свои снасти, и всякий другой ремесленник — своему проверенному работой и временем инструменту. Такое отношение можно назвать приматом метода над задачей. Для этого типа специалистов задачи преходящи, а методы вечны. В самом деле, если у нас в распоряжении есть прекрасный метод, мы с его помощью сможем решить массу задач, и что это за задачи, становится не таким уж важным вопросом. Именно метод, доскональное его знание, свободное владение им в сознании такого специалиста тесно связано с понятием профессионализма. Такой специалист постоянно совершенствуется во владении методами, чтобы никакая новая задача не имела бы шанса оказаться неожиданной. Сам специалист всегда пребывает во всеоружии и готовности. Для него самая страшная ситуация — столкнуться с задачей, против которой весь арсенал его методов оказывается бессилен; такая ситуация рассматривается как профессиональное поражение, профессиональная несостоятельность.
Вследствие этих предпочтений, подобные автору специалисты чаще всего посвящают всю свою жизнь труду в какой-то одной определённой области, где они и достигают профессиональных высот. Их любопытство к другим областям ограничивается опасениями сесть в лужу, поэтому они стараются дистанцироваться от всего, выходящего за рамки их мастерства: не обсуждать, не высказываться, отбиваться от попыток втянуть их в неизведанные ими пространства, и если чем-нибудь интересоваться, то очень осторожно, без энтузиазма. Замечательной литературной иллюстрацией такого типа специалистов является всем известный Шерлок Холмс, не знавший теории Коперника и хранивший в голове только нужные для дела сведения, зато в идеальном порядке — блестящий специалист своего дела, Профессионал.
Священным граалем подобных специалистов является некий универсальный метод, с помощью которого они были бы способны справиться с максимально широким классом задач, и справиться не как-нибудь, а наверняка. Естественно, разнообразие и сложность задач оказывают влияние на метод их решения, и этот метод становится сложным — со множеством опций, тонкостей и нюансов его применения. Чем сложнее выходит метод, тем ценнее выглядит тот мастер, который им владеет в совершенстве.
Вот так же и наш автор попытался продемонстрировать нам красоту его любимого метода. Однако предчувствуя, что полное и развёрнутое изложение слишком затянется, он решил выбрать задачу попроще — «Hello world» — в надежде, что именно задача послужить ограничителем на размер излагаемого материала. При этом чувствуется: его терзали сожаления, что задача не позволяет раскрыть всю глубину и богатство метода.
Со временем некоторые из таких мастеров имеют тенденцию становиться «гуру», набирать себе учеников и создавать собственную школу мастерства. Такие школы мы можем наблюдать повсеместно: и в религиозной среде, и среди ремесленников, и у врачей, и в науке, и в конструкторских бюро, и в балете и прочих искусствах. Специалисты этого типа — та самая «соль земли», хранящая определённый уклад, традиции, устои.
Однако есть и иное отношение к методу, которое можно назвать приматом задачи над методом. В этом случае важна задача и эффективность её решения, а метод — это предмет выбора или даже разработки, он вторичен. Людей с такими методологическими предпочтениями подчас даже трудно назвать специалистами, поскольку они не любят замыкаться в рамках какой-то специальности. Среди них есть и экстремистская группа, исповедующая принцип «цель оправдывает средства», но обычно они предпочитают соотносить затрачиваемые усилия с достигаемым результатом и всеми побочными последствиями — стремятся к гармонии между действием и производимым им эффектом.
Таких людей объединяет любовь к целесообразности, к осмысленности действий, к значимости решаемых ими задач. Эти люди берутся за очередную задачу не потому, что хорошо умеют её решать и способны мастерски с ней справится, а потому, что считают решение этой задачи более важным делом, нежели решение каких-то других. Здесь мы вряд ли найдём выстроенные школы и профессиональные династии. Такие люди предпочитают свободные профессии, предпринимательство, педагогику, политику, журналистику, менеджмент и любые виды деятельности, где есть простор для творчества. Также нередко они встречаются среди инженеров, конструкторов и архитекторов — именно такие люди предлагают новое, делают прорывы в науке и технике, двигают прогресс.
Именно для таких людей «творческий беспорядок» на рабочем месте — необходимое условие для плодотворного труда. Регламентация деятельности их сковывает, мешает поиску эффективного метода, aпробациям различных вариантов, отсеву неподходящих и отбору перспективных. Такой стиль работы с трудом сочетается с планомерным строительством решения проверенным методом.
Питая симпатии ко второй категории людей, я во второй части отзыва постараюсь раскрыть их альтернативный подход к этой же самой задаче «Hello world», а затем в третьей части сопоставить оба подхода.
Нельзя отрицать пользу TDD в разработке embedded-приложений, но также я считаю неуместным относиться к этому (а равно и всякому другому) методу с тем пиететом, который мы наблюдаем у автора. Явленная нам сложность совершенно не соответствует задаче, и в результате заметную долю статьи автор, к сожалению, посвящает не исследованию структуры проблемы, а героическому преодолению им же самим для себя воздвигнутых препятствий. Сюда относятся и вечное несовершенство постоянно переписываемых конфигурационных файлов среды, и проблемы с применением эмулятора:

Цитата
VMLAB требует, чтобы все проектные файлы находились в одной директории. Если бы мы не использовали TDD, наша программа уместилась бы в одном файле, и проблема с деревом иерархии файлов проекта ушла бы сама собой. Однако я считаю это слишком высокой ценой за отказ от TDD. В конце концов, оттестированная программная модель на инструментальной системе внушает мне куда больше доверия, чем вроде бы работающая симуляция на VMLAB.

На мой взгляд, автор напрасно увязывает между собой TDD и иерархию файлов проекта. Скорее здесь речь идёт о нежелании ломать красоту оформления и некогда придуманного автором порядка, нежели о том, что TDD запрещает хранить файлы в одной папке. Просто автор не считает возможным хоть на йоту отступить от собственных привычек — например, заменить иерархию папок на длинные имена файлов с суффиксами или префиксами. Похоже, для него такие отступления от привычного — чуть не экстремизм. Ведь метод проверен и результат гарантирован, двигаться нужно строго по фарватеру от маяка к маяку и ни в коем случае не заплывать за буйки. Плавание без компаса и карты — это если не самоубийство, то точно глупость, которая к ни к чему хорошему не приведёт. Конечно, по известным картам новых Америк не открыть, но, видимо, где-то в глубине души автор уверен, что для профессионала это — лишнее.
Хочется напомнить автору слова Э. Дейкстры из книги «Дисциплина программирования»:

Цитата
Наверняка разочаруются все те, кто отождествляет трудность программирования с трудностью изощрённого использования громоздких и причудливых сооружений, известных под названием «языки программирования высокого уровня» или — ещё хуже! — «системы программирования». Если они сочтут себя обманутыми из-за того, что я вовсе не касаюсь всех этих погремушек и свистулек, могу ответить только одно: «А вполне ли вы уверены, что все эти погремушки и свистульки, все эти потрясающие возможности ваших, так сказать, “мощных” языков программирования имеют отношение к процессу решения, а не к самим задачам?»

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

1.3. О законах обучения

Наконец, третий и очень важный аспект статьи, который стоит обсудить — это проблема целевой аудитории и практической применимости читателями образцов работы автора.
Читая статью, я постоянно задавался вопросом: для кого она написана? С одной стороны, «Hello world» намекает на аудиторию начинающих, только приступающих к разработке embedded-приложений, не имеющих опыта. И здесь возникает вопрос, способны ли они научиться чему-нибудь из прочитанного ими текста? Они могут в порядке «шаманского ритуала» попытаться повторить все те действия, которые с великим тщанием описывает автор, поверив ему на слово, что это ведёт к результату. Но что с ними будет, если что-то пойдёт не так? Если неправильно установится какой-нибудь компонент, если будет допущена ошибка в конфигурации, ведущая к неописанным автором сбоям? Начинающий ничего не сможет с этим сделать, станет в тупик. Чтобы ему вполне понять как действия автора, так и свои собственные ошибки при попытке повторить эти действия, начинающий уже априори должен обладать опытом работы как в операционной среде, так и с каждым инструментальным средством в отдельности. Только при наличии этого багажа знаний читатель сможет усвоить практику автора, собрав целостную рабочую процедуру из отдельных приёмов работы — подобно разучиванию танца или сбору игрушки в конструкторе «Лего».
Тогда, быть может, статья написана для других профессионалов и являет собой средство обмена практическим опытом? Но, опять же, если другой профессионал уже владеет описываемыми в статье методиками и инструментами, то максимум даваемых автором знаний — это порядок разложения файлов по папкам, и то лишь ради невозможности запустить эмулятор... Ведь такой профессионал уже умеет разрабатывать архитектуру и писать unit-тесты. Если же он работает с иным набором инструментов, то ожидает увидеть пользу и выгоду, которую даёт метод автора по сравнению с его собственными практиками. Однако видит он лишь «Hello world», на котором в принципе невозможно показать какие-либо преимущества — это неподходящая демонстрационная задача для такой сложной рабочей среды. Она была бы уместна лишь при более высокой интеграции частей среды друг с другом — в рамках специализированной IDE, скрывающей от пользователя множество нюансов конфигурации и обеспечивающей простоту и лёгкость использования.
Избранный автором подход к изложению материала весьма эффективен для демонстрации конкретного решения конкретной проблемы, чему обычно и посвящены многочисленные статьи по программированию. Но очевидно, что автор не ставил себе целью написать «Hello world» — замах у него на нечто большее. Если главное — не в написании make-файлов, то для чего же столько подробностей, затеняющих суть дела?!
Упомянутые в заголовке законы обучения описывают переход ученика от какой-то имеющейся системы знаний к новой. Мне кажется, автор, хорошо понимая новую систему знаний, не задумался о том, каковы имеющиеся в головах читателей знания, какие вехи пути им нужны, и каков ожидаемый педагогический результат. Статья скорее информирует о наличии у автора красивого и сложного метода, нежели учит пользоваться этим методом. Единственный дидактический элемент, доступный любому читателю — это, к сожалению, морализация профессионализма. А вот лестницы из законченных учебных ступеней, по которой всякий может достичь профессиональных высот, увы, не видно.
Описанный выше недостаток структуры материала с учебной точки зрения проявляет себя и с неожиданной стороны. Представим себе компанию, разрабатывающую embedded-приложения. Допустим, у неё есть портфель заказов и линейка продуктов, которую необходимо поддерживать. Поскольку активность работ по заказам и продуктам никогда не бывает равномерной, во многом зависит от активности заказчиков, поставщиков и пользователей, такая компания не может себе позволить закреплять конкретных специалистов за конкретными продуктами. Скорее всего в ней есть некий отдел сопровождения, специалисты которого модифицируют старые проекты по заявкам пользователей. Обычно это скучноватая работа, на которую ставят молодых специалистов с небольшим опытом — тех, кого пока рискованно допускать к разработке новых продуктов. Обычно в таком отделе наблюдается заметная текучка кадров: набравшиеся опыта толковые специалисты переходят в отдел перспективных разработок, а бесперспективные сотрудники покидают компанию.
Представим себе, что в такой отдел, укомплектованный такими кадрами, попадает проект, написанный автором. И вот малоопытный специалист глядит на множество разнообразных папок и файлов, зная лишь о том, что вот это всё вместе заставляет мигать лампочку. Наверняка его первая реакция будет нецензурной. Непонятно, с чего ему начать, что здесь к чему, какие-такие mk- и rb-файлы. Он может прочитать про язык Ruby и спросить себя, какое это отношение имеет к embedded-приложениям?
Но, допустим, он, благодаря readme-файлам, уяснил себе, какие в проекте есть модули, и как они связаны друг с другом. Дальше он хочет внести изменение в проект. Станет ли он вдумчиво изучать код тестов и действовать строго по неизвестной ему (а если и известной, то на первый взгляд бессмысленной) методологии?! Скорее всего он внедрит изменение «квадратно-гнездовым методом» и на этом с облегчением вздохнёт, положив проект в архив.
Затем он будет надеяться, что больше никогда не придётся сталкиваться с этим проектом. И скорее всего его надежды оправдаются, учитывая текучку кадров. Спустя некоторое время, следующий малоопытных сотрудник извлечёт этот проект из архива — всё повторится.
Коротко говоря, передача предложенного автором проекта другому специалисту имеет высокий порог вхождения, требует многих знаний, навыков и большого опыта. Как я уже отмечал выше, найти такого достойного преемника — сложная кадровая проблема. Если компания и занимается обучением молодых сотрудников-стажёров, то как раз это происходит в отделах сопровождения или тестирования, где низкооплачиваемые стажёры могут смотреть и разбирать готовые образцы и одновременно приносить хоть какую-то пользу на подсобных работах.
Именно здесь педагогический аспект выходит на первый план. Вся структура решения, его внутренняя организация должны иметь форму, удобную для восприятия новым человеком. Это ступенчатая форма от простого и главного по мере возрастания сложности к деталям. Это значит, что решение должно быть слоистым как кочан капусты или луковица, чтобы, раскрыв верхние оболочки, можно было увидеть сердцевину. Сама сердцевина должна быть, с одной стороны, простой, а с другой — вполне законченной и самостоятельной, способной в общих чертах решать задачу без внешних украшений.
Лишь такая структуризация решения слоями, выраженная в том числе и в архитектуре, окажется удобной для введения в проект новых разработчиков со среднестатистической квалификацией. Автору же, судя по манере изложения материала, такие проблемы организации производства и работы команд чужды.

2. Альтернативный подход

После окончании критического анализа статьи «Hello world в embedded исполнении», теперь, как и было обещано выше, мне бы хотелось на примере той же самой задачи показать иной подход к её решению.
Поскольку это описание подхода является составной частью отзыва к статье, а не самостоятельной работой, я здесь для краткости пропущу всё то, что автор рассказывал про компилятор и средства сборки проекта. Таким образом, для начинающих, кто испытывает сложности с использованием этих средств, нижеизложенное можно читать по-диагонали, стараясь лишь ухватить общий способ мышления при анализе задачи. Более опытным специалистам могут оказаться интересными и детали.

2.1. Обзор

Подход весьма прост, поскольку в его основе лежит один единственный принцип. Поэтому я не буду приводить никаких списков литературы или ссылаться на каких-то авторитетов — прочтение сотен книг никак не обучит читателя. А вот освоить эту простоту может помочь лишь практика, тренировка умения анализировать задачи.
Отсутствие какого-либо заранее прописанного плана действий, тем не менее, не означает хаоса. Просто на каждом шаге, при решении каждой мелкой подзадачки нужно охватывать взглядом всё решение целиком. Подобно художнику, который сделает мазок, а затем отступит, чтобы увидеть всю картину, оценить удачность каждого мазка по отношению к целому полотну.
Каждым отдельным шагом у нас будет решение отдельной задачи по отношению к каким-то свойствам рабочего объекта. Решение задачи состоит из исследования объекта в контексте условий его использования, формулировки проблемы, выработки вариантов решения, анализа вариантов и выбора самого лучшего в данном контексте. При этом в простых и очевидных случаях некоторые элементы решения, разумеется, могут быть пропущены. Ключевыми элементами, которые нельзя пропускать ни при каких обстоятельствах, являются выявление проблемы и её решение. Вот эта пара — проблема-решение, или цель-средство — является элементарным узлом структуры любой задачи любой сложности. Чем сложнее задача, тем больше этих узлов, связанных между собой.
Отчасти то, что я описываю, напоминает процесс управления требованиями и трассировку требований внутри решения. Но я не считаю жизненно необходимыми в столь маленьких проектах те бюрократические процедуры, которые написаны в методических пособиях по управлению требованиями.
Упомянутый выше простой принцип заключается в том, чтобы систематически искать и формулировать проблемы для того, чтобы сделать решение задачи лучше. Это постоянное внимание к полученному решению в его целостности и настойчивый вопрос к самому себе: «А как можно сделать ещё лучше?» Однако ошибкой было бы думать, что это легко. «Лучше» всегда относится к какой-то цели, всегда должен быть известен ответ на вопрос: «Для чего (или кого) лучше?» Здесь совершенно недопустимы ответ вроде «Мне так кажется». У квалифицированного инженера ответ имеет объективное обоснование.
Структурные узлы задачи часто (но не обязательно) образуют иерархии. Недостатки определённого решения одной проблемы могут порождать дочерние проблемы, не имеющие смысла без родительской. Если какая-то проблема изымается из задачи, или меняется её решение, пересмотру (а часто и изъятию) подлежит всё поддерево дочерних проблем.

2.2. Пример применения к «Hello world в embedded исполнении»

Проблема 0 (корневая)

Итак, в начале у нас есть общая проблема — нужно создать аппаратно-программное решение мигающей лампочки. Лампочка должна мигать таким образом, чтобы 0,5 секунды светить и 1,5 секунды быть выключенной. Решение должно предполагать, что миганием лампочки управляет некий микроконтроллер. Нашей сферой ответственности является лишь программная часть. Проблема может иметь разнообразнейшие решения, а выбор того или иного подхода к решению зависит от обстоятельств.
В общем случае корневая проблема должна выявлять нечто самое важное в задаче, а решение корневой проблемы становится объектом для дальнейших улучшений и детализации. Для маленьких задач, вроде «Hello world», корневой проблемой является вся задача целиком, как она есть. Для более крупных задач корневой проблемой является их наиболее общий и наиболее грубый каркас или контур. Чтобы наметить этот контур, полезно задаться вопросом, ради чего вообще решается задача, что изменится во вселенной, когда решение будет готово? Важно, чтобы при решении всех дочерних проблем корневой каркас не терялся под грудой мелочей, а общее решение в своей целостности сохраняло бы смысл.

Решение 0

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

Код: (C)
#include <avr/io.h>
#include <util/delay_basic.h>

#define MS_LOOPS 4000

void main()
{
  DDRC |= _BV(PC0);
  for(;;)
  {
    PORTC |= _BV(PC0);
    _delay_loop_2(1500 * MS_LOOPS);
    PORTC &= ~_BV(PC0);
    _delay_loop_2(500 * MS_LOOPS);
  }
}

Если мы умудрились в этих нескольких строчках допустить какую-то опечатку — её нужно исправить и повторить сборку и запуск.
Когда есть сомнения в том, что написанное нами решение сразу заработает на «железе» должным образом, или испытания обходятся слишком дорого, или у нас нет железа, но точно известны его спецификации, можно прибегнуть к проверке работы на эмуляторе соответствующего микроконтроллера.
Но допустим, у нас нет ни железа, ни эмулятора, ни спецификаций аппаратной части. Тем не менее для столь короткой программы, которая пишется за несколько минут, нет никакого проку городить огород с какими-то предварительными заготовками и делать усложнения.
Задача решена, и у нас нет никаких объективно обоснованных критериев для улучшения полученного решения. Усложнение полученного решения приведёт к нежелательным последствиям: как посторонним людям, так и нам сами будет труднее разобраться с более сложным кодом и вносить изменения.

Проблема 1

Сразу оговорюсь, что эта проблема никак не проявляет себя в задаче «Hello world», она может возникнуть лишь в более крупных задачах, если их решение будет аналогичным решению 0. Здесь эта проблема приведена исключительно в качестве примера иерархичности структуры анализируемой задачи. Поэтому решение этой проблемы и всех её дочерних проблем можно смело выбрасывать из Hello world.
Эту проблему обозначил автор обсуждаемой статьи, и она, действительно, встречается в процессе разработки. Что делать, если объём программной части велик, спецификации на железо задерживаются, а сроки поджимают? Нельзя быстро написать большую и сложную программу. Можно ли сделать какую-нибудь заготовку? В каких случаях эта заготовка будет полезной?

Решение 1

Для решения такой проблемы следует задаться вопросом: «Насколько программное решение зависит от аппаратного? Можно ли выделить независимые части?»
Общий подход к решению таких проблем в математике вообще и в программировании в частности — абстрагирование. Применительно к разработке аппаратно-программных решений распространённой является практика выделение внутри решения двух уровней:
  • Hardware Abstraction Layer (HAL) — машинно-зависимого уровня абстракции от аппаратной реализации, инкапсулирующего (скрывающего) в себе особенности работы с конкретной аппаратной частью, и
  • Application Layer — машинно-независимого уровня, той части программы, которая будет одинакова для любой аппаратной реализации
Нас этот стандартный приём на первый взгляд вполне устроит, поскольку в детали мы ещё не вникали.
Чтобы получше понять этот приём, полезно обратиться к самым азам программирования. У нас есть, с одной стороны, исполнитель, который знает какие-то команды (операции) и умеет их выполнять, а с другой стороны, у нас есть алгоритм — инструкция для исполнителя, что в каком порядке ему делать. Ответственность за составление алгоритма, за то, что выполненная исполнителем по алгоритму работа даст именно тот результат (или эффект), который мы ожидаем, полностью лежит на нас — разработчиках. На исполнителе лежит лишь ответственность чётко и однозначно выполнять каждую операцию алгоритма и строго следовать их порядку в алгоритме. Когда мы составляем алгоритм, мы также обязательно должны учитывать возможности исполнителя. Если мы включим в алгоритм такие команды, которые не понимает исполнитель, виноваты в этом будем только мы сами.
В нашем случае HAL — это исполнитель каких-то операций (команд оборудованию), а AL — это алгоритм, представляющий собой некую комбинацию известных исполнителю команд. Всё вместе это похоже на программирование виртуального компьютера, который мы сами себе и придумаем. Если мы обладаем внутри программы разделением на такие уровни, мы получаем следующие возможности.
Во-первых, мы становимся способны писать произвольные алгоритмы на основе единого набора команд. Это значит, что сделав богатый возможностями HAL для конкретной аппаратной реализации, мы, в общем-то, можем даже забыть, как программировать конкретное аппаратное решение, какие у него есть особенности. С помощью HAL мы переходим в сферу прикладного программирования и можем сосредоточить своё внимание на прикладных аспектах решаемых задач.
Во-вторых, мы становимся способны выполнять один и тот же алгоритм на разных аппаратных платформах. Это значит, что написав достаточно сложное программное решение на базе гораздо меньшего по размерам и сложности HAL, при помощи замены одной реализации HAL на другую мы будем способны малыми усилиями портировать наше решение на новую аппаратную платформу. А если у нас есть целая библиотека разных реализаций HAL, мы можем обещать заказчикам или коллегам поддержку разных аппаратных платформ на их выбор.
В-третьих, имея доступ к отдельным операциям исполнителя, мы можем каждую из них подвергнуть проверке, чтобы убедиться, что исполнитель правильно их выполняет. Это открывает возможности для написания программных тестов аппаратной части.

Проблема 1.1

Сейчас у нас нет ни HAL, ни AL. У нас нет известных команд, чтобы мы из них составляли алгоритм. У нас нет алгоритма, чтобы вообще иметь представление о том, какие команды было бы полезно знать исполнителю. И это следующая, третья проблема, вытекающая из второй: как должен выглядеть алгоритм AL и набор команд HAL, чтобы наша задача была решена?

Решение 1.1

В начале определимся с критериями качества решения. Критериев этих два. С одной стороны, HAL должен предоставлять нам восможности использовать максимум аппаратных преимуществ конкретной платформы. С другой стороны, HAL должен быть мал и достаточен по отношению ко всем решаемым с его помощью задачам. Короче говоря, HAL должен давать нам доступ ровно к тем аппаратным возможностям, которые нам нужны для решения наших задач, и ни к каким другим. Но доступ к тем возможностям, которые открыты в HAL, должен быть полным. Нужно избегать разработки таких сложных HAL, в которых большая часть аппаратных возможностей заведомо никогда не будет использована при решении задач — трудозатраты на такую разработку окажутся бесполезными. Нужно избегать разработки таких примитивных HAL, которые создают трудности при решении задач и вынуждают переусложнять AL или вообще являются препятствием для разработки конкретных прикладных решений. Нужно избегать громоздких HAL, которые в своей работе не оставляют свободных вычислительных ресурсов для AL.
Если HAL разрабатывается в нескольких версиях для разных аппаратных платформ, все версии должны содержать одинаковый набор возможностей для AL — иметь единый межуровневый интерфейс или протокол. В этом случае за основу берётся самая примитивная аппаратная платформа. Можно взять и самую сложную аппаратную платформу, реализовав программными средствами те возможности, которые отсутствуют у более простых платформ. Однако второй вариант более рискован — не везде можно программно реализовать отсутствующие аппаратные возможности.
В примитивном HAL мы можем создать единственную инструкцию «решить задачу». Тогда алгоритм будет состоять из единственного действия «решить задачу» или серии таких действий, если задачу надо решать несколько раз. По здравому рассуждению мы придём к выводу, что с этой единственной операцией вперёд мы не продвинулись. При таком решении мы просто сосредоточим всю сложность проблемы в HAL и опустошим AL — ради такого результата нет смысла затевать само выделение уровней.
Традиционно к решению подобных проблем есть два подхода: снизу-вверх и сверху-вниз.
Подход снизу-вверх предполагает, что мы проанализируем возможности аппаратной части и включим в межуровневый интерфейс все выявленные элементарные операции. Это нас может привести к такому HAL, возможности которого для решения нашей задачи избыточны. Кроме того, интерфейс такого HAL наверняка будет содержать какие-то специфические для аппаратной платформы понятия и термины, а их интерпретация в понятия и термины предметной области будет осуществляться в AL.
Подход сверху-вниз предполагает разработку вначале неформального алгоритма решения задачи, затем выявление внутри алгоритма элементарных действий и включение этих действий в HAL. После этого неформальный алгоритм формализуется внутри AL на основе уже заданных операций HAL. Это нас приведёт к такому HAL, который специализирован для решения конкретной задачи, а интерфейс такого HAL наверняка будет содержать термины и понятия, специфические для предметной области. Это значит, что интерпретация этих понятий в понятиях аппаратной платформы будет осуществляться в HAL.
Для нашего решения мы изберём подход сверху-вниз по ряду причин. Во-первых, аппаратное решение нашей задачи не содержит структурной вариативности: есть соединённые каким-то образом микроконтроллер и лампочка, отсутствуют динамически подключаемые неизвестные устройства. Такую жёсткую структуру удобно полностью выразить средствами HAL, инкапсулировав внутри этого уровня детали соединения. Во-вторых, для реализации алгоритма не требуются все возможности микроконтроллера, поэтому HAL, построенный методом сверху-вниз, будет гораздо короче и проще. В-третьих, у нас не намечается никаких других задач для такого HAL, поэтому некоторая зависимость интерфейса HAL от предметной области допустима.
Обратимся к исходной проблеме: что мы хотим получить, и какие действия аппаратной части нам обеспечат желаемое. Подумав, мы обнаружим три действия: «включить лампочку», «выключить лампочку» и «подождать». Последнее действие также характеризуется дополнительным параметром — временем ожидания. Вот эти три действия суть команды для аппаратной части, и они образуют HAL. А комбинация этих действий таким образом, чтобы мы получили желаемый эффект, представляет собой алгоритм решения исходной проблемы. Опыт может нам подсказать, что после запуска аппаратная часть может требовать некой подготовки или настройки, прежде чем окажется готовой к работе. Поэтому включим в HAL ещё одну инструкцию «приготовиться к работе».
Алгоритм нашей задачи известен с самого начала, хотя точнее было бы сказать, что он очевиден. Поэтому, придумав деление решения на уровни AL и HAL, а также определив операции HAL, мы уже готовы внести изменения в исходный код:

Код: (C) hal.h
#ifndef HAL_H
#define HAL_H

/* Приготовиться к работе */
extern void hal_prepare();

/* Выключить лампочку */
extern void hal_led_off();

/* Включить лампочку */
extern void hal_led_on();

/* Подождать некоторое время
   milliseconds - количество миллисекунд ожидания */

extern void hal_wait(unsigned int milliseconds);

#endif

Код: (C) al.c
#include "hal.h"

int main() {

  /* Алгоритм */
  hal_prepare();
  for(;;) {
    hall_led_off();
    hal_wait(1500); // ms
    hal_led_on();
    hal_wait(500); // ms
  }

  return 0;
}

Код: (C) hal.c
#include <avr/io.h>
#include <util/delay_basic.h>
#include "hal.h"

#define MS_LOOPS 4000

void hal_prepare() {
  DDRC |= _BV(PC0);
}

void hal_led_off() {
  PORTC |= _BV(PC0);
}

void hal_led_on() {
  PORTC &= ~_BV(PC0);
}

void hal_wait(unsigned int milliseconds) {
  _delay_loop_2(milliseconds * MS_LOOPS);
}

Если у нас сохраняется неопределённость по поводу аппаратной реализации, мы можем написать специальную версию файла hal.c — hal_sym.c, обеспечивающую проверку работы алгоритма другими средствами. Тогда наш HAL будет работать как симулятор тех эффектов, которые мы ожидаем от аппаратной части. Воспользуемся на мой взгляд весьма удачным советом автора выводить на экран различные символы, соответствующие включению и выключению лампочки.

Код: (C) hal_sym.c
#include <stdio.h>
#include <time.h>
#include "hal.h"

void hal_prepare() {
}

void hal_led_on() {
  printf("#");
  fflush(stdout);
}

void hal_led_off() {
  printf("_");
  fflush(stdout);
}

void hal_wait(unsigned int milliseconds) {
  long long microseconds = milliseconds * 1000;
  usleep(microseconds);
}

Теперь, собрав всё решение и даже не имея представления об аппаратной части, мы уже можем убедиться, что алгоритм в AL работает.

$ cc al.c hal_sym.c
$ ./a.out
_#_#_#_#_

Проблема 1.1.1

Сделав что-то, теперь полезно взглянуть на всё полученное решение целиком. Такой взгляд может привести нас к следующей проблеме.
Поскольку у нас имеется два варианта реализации: hal.c и hal_sym.c, -полезно ли сохранять на будущее оба файла или файл с симуляцией операций HAL удалить после завершения разработки?

Решение 1.1.1

Поскольку файл симуляции HAL позволяет нам проверить логику работы AL независимо от работы аппаратной части, эту тестовую версию HAL полезно оставить.
Это решение является следствием самого факта выделения уровней AL и HAL внутри программы. Если мы удалим hal_sym.c, потеряет смысл всё разделение на AL и HAL — проблема 1 будет изъята из проекта. У нас не останется ни одного аргумента за такое усложнение по сравнению с более простой версией. Именно наличие двух версий реализации HAL удерживает нас от упрощения решения. Если есть две версии HAL, решение с общим AL и разными версиями HAL оказывается удобнее в сопровождении, нежели две независимые программы.
Версия hal_sym.c создаёт и ещё одно дополнительное преимущество. Поскольку две версии HAL реализованы принципиально разными средствами, из этого факта следует, что интерфейс HAL действительно не зависит от конкретной аппаратной платформы. Такое качество интерфейса может представлять архитектурную ценность в решении.
Если мы оставляем две версии HAL, как раз становится уместным написать make-файл, обеспечивающий сборку двух версий программного решения: тестовую и для прошивки микроконтроллера. (Подробные примеры, как создаются make-файлы, прекрасно описаны автором, поэтому я не буду утомлять читателей такими деталями.)

Проблема 1.1.2

Выделенные в HAL операции требуют некоторого анализа и размышлений.
Если у нас возникли серьёзные планы многократно использовать нашу версию HAL для конкретной аппаратной реализации в различных проектах, полезно превратить эту часть программы в библиотеку. Мы не будем создавать библиотеку, а рассмотрим лишь саму перспективу такого преобразования. Эта перспектива требует от нас критического анализа архитектуры HAL.
Естественно, такая перемена потребует пересмотра решения 1.1, поскольку там мы явно отметили, что не собираемся использовать разработанный HAL где-либо ещё. Однако допустим, что даже после пересмотра аргументации в решении 1.1 мы будем иметь тот же самый HAL.
Согласно заявленным планам с помощью операций HAL мы будем создавать в будущем сейчас нам неизвестные и в общем случае произвольные алгоритмы. Для HAL это значит, что его операции могут оказаться скомбинированными в произвольном порядке. Тогда возникает дополнительное требование к операциям HAL — они должны быть скалярными. Скалярность означает, что после выполнения любой операции HAL вся система (т.е. наш виртуальные компьютер) должна оказаться в таком состоянии, чтобы быть готовой выполнить любую другую операцию. Никакая операция не должна давать побочных эффектов, которые бы накапливались и влияли бы на работу последующих операций.
В нашей задаче скалярность на первый взгляд отсутствует. Если мы зажгли лампочку, мы не сможем её зажечь ещё раз, не погасив до этого. Операция подготовки к работе непременно должна предшествовать всем прочим операциям. Таким образом, предложенный нами HAL, с одной стороны, требует от разработчика алгоритма некоторой дисциплины — следования определённому протоколу работы, а с другой стороны, содержит риск сбоев при отсутствии этой самой дисциплины или просто в силу невнимательности.
Эта проблема является недостатком архитектуры. Что с этим можно сделать?

Решение 1.1.2

В части включения и выключения лампочки мы могли бы обеспечить скалярность, заменив пару операций «включить лампочку» и «выключить лампочку» на единую операцию «изменить состояние лампочки на противоположное», аналогичную обычной кнопке «вкл/выкл». Однако такая замена может привести нас к случаю, когда в достаточно сложном алгоритме из-за ошибки двойного подряд включения или выключения мы получим неожиданный и потому нежелательный эффект работы системы — лампочка может включаться, когда должна выключаться и наоборот. Мы могли бы добавить в HAL операцию определения текущего состояния лампочки и дать возможность разработчику выполнять проверку, но тем самым мы опять будем полагаться на дисциплинированность разработчика. Таким способом устойчивость в работе и безопасность всей аппаратно-программной системы мы не улучшим, поскольку с точки зрения конечного пользователя неработающая система быть может даже менее опасна, чем работающая противоположно ожиданиям.
Ко всему прочему решение частного вопроса с лапочкой не помогает решить нам общий вопрос, поскольку операция «приготовиться к работе» всё равно должна выполняться раньше всех остальных. Проще оставить разработчику возможность явным образом записать в алгоритме, хочет ли он включить или выключить лампочку и исключить случайности в этом вопросе.
Другой более универсальный подход к этой проблеме заключается в написании обёртки над HAL, обеспечивающей контроль за правильностью порядка выполнения операций. Операции, выполнение которых в тот или иной момент несвоевременно, должны игнорироваться. Такое поведение нам обеспечит автомат, приведённый на следующем рисунке.



В этом случае разработчик волен ошибаться и предписывать алгоритмом недопустимые действия.
Кроме того, реализуя независимые друг от друга функции внутри AL, разработчик сможет сознательно указывать такой порядок действий, который не учитывает порядок действий в других функциях. Например, достаточно естественным выглядит желание погасить лампочку в начале функции — в порядке инициализирующего действия. Успешность этой операции не должна зависеть от того, в каком состоянии осталась лампочка после работы предыдущей функции.
В рамках такого подхода виртуальный компьютер будет выполнять лишь то, что не может ему повредить и имеет смысл в отношении аппаратной части — наш HAL получит так называемую «защиту от дурака».
Чтобы реализовать этот подход, модифицируем имеющиеся файлы hal.h и hal.c/hal_sym.c, добавив им суффикс «usf» (hal_usf.h и hal_usf.c/hal_usf_sym.c), который у нас будет обозначать небезопасные операции, в то же время оставим исходную копию файла hal.h и напишем новый файл hal.c, реализующий необходимый автомат. Получим следующие файлы.

Код: (C) hal_usf.h
#ifndef HALU_H
#define HAL_USF_H

extern void hal_unsafe_prepare();
extern void hal_unsafe_led_off();
extern void hal_unsafe_led_on();
extern void hal_unsafe_wait(unsigned int milliseconds);

#endif

Код: (C) hal_usf_sym.c
#include <stdio.h>
#include <time.h>
#include "hal_usf.h"

void hal_unsafe_prepare() {
}

void hal_unsafe_led_on() {
        printf("#");
        fflush(stdout);
}

void hal_unsafe_led_off() {
        printf("_");
        fflush(stdout);
}

void hal_unsafe_wait(unsigned int milliseconds) {
        long long microseconds = milliseconds * 1000;
        usleep(microseconds);
}

Код: (C) hal_usf.c
#include <avr/io.h>
#include <util/delay_basic.h>
#include "hal_usf.h"

#define MS_LOOPS 4000

void hal_unsafe_prepare() {
  DDRC |= _BV(PC0);
}

void hal_unsafe_led_off() {
  PORTC |= _BV(PC0);
}

void hal_unsafe_led_on() {
  PORTC &= ~_BV(PC0);
}

void hal_unsafe_wait(unsigned int milliseconds) {
  _delay_loop_2(milliseconds * MS_LOOPS);
}

Код: (C) hal.c
#include "hal.h"
#include "hal_usf.h"

#define INITIAL_STATE 0
#define READY_STATE 1
#define LED_ON_STATE 2
#define LED_OFF_STATE 3

static int state = INITIAL_STATE;

void hal_prepare() {
  if(state == INITIAL_STATE) {
    hal_unsafe_prepare();
    state = READY_STATE;
  }
}

void hal_led_on() {
  if(state == READY_STATE || state == LED_OFF_STATE) {
    hal_unsafe_led_on();
    state = LED_ON_STATE;
  }
}

void hal_led_off() {
  if(state == LED_ON_STATE) {
    hal_unsafe_led_off();
    state = LED_OFF_STATE;
  }
}

void hal_wait(unsigned int milliseconds) {
  if(state != INITIAL_STATE) {
    hal_unsafe_wait(milliseconds);
  }
}

3. Синтез подходов

Как могли заметить читатели, при демонстрации альтернативного подхода я столь же часто прибегал к высасыванию проблемы из пальца, что и автор. Задача «Hello world», действительно, — неудачный объект для методологических упражнений.
Тем не менее, по множеству обсуждаемых проблем и аспектам решения у нас с автором вышло заметное несовпадение. К счастью, в этом несовпадении результатов видится повод скорее к взаимодополнению слабо пересекающихся аспектов, нежели к антагонизму.
В подходе автора почти отсутствует какая-либо мотивация выбора той структуры решения, которую он привёл. В частности, применение объектно-ориентированного подхода, выделение именно тех модулей, которые у него получились. Скорее всего автор действовал по сложившемуся шаблону, стремясь каждое понятие предметной области описать в виде объекта. Этот подход имеет не только сильные, но и слабые стороны — он способен дать избыточное описание, прямо сказывающееся на избыточной сложности программного кода. Происходит это от недостаточно чёткой формулировки цели анализа.
Так получается, что при систематическом следовании TDD автор в принципе не сможет создать нечто, не поддающееся тестированию. Каким бы это ни показалось странным автору, но труднее всего поддаются тестированию как раз очень простые, определённые и самоочевидные вещи. Поэтому TDD, хотя и обеспечивает высокое качество решений, оказывается главным препятствием для разработки простых, изящных и гибких архитектур.
В простой и гибкой архитектуре любой её элемент, любая связь и вообще любой штрих имеют обоснование. Это обоснование всегда ссылается на какое-то необходимое качество решения, требование к результату. Любой штрих в архитектуре существует только и только потому, что без него невозможно обеспечить некое совершенно необходимое качество (или группу качеств). Отдельные штрихи архитектуры могут быть неудачными в том смысле, что можно придумать ещё более изящное решение, но, к сожалению, разработчик не догадался. Однако существование даже неудачного штриха всё равно жёстко подчинено необходимости обеспечить некое качество решения.
Другим характерным признаком простоты и гибкости архитектуры является способность реализовать всё множество требований самыми минимальными средствами. Это означает безжалостное отбрасывание избыточных построений. В этом пункте программист и электронщик резко расходятся в своих привычках. Для разработчиков железа избыточность, дублирование каких-то элементов — это средство повышения живучести физического устройства. Но в мире математики присутствуют лишь идеальные объекты, для которых всевозможные «костыли» по принципу «на всякий случай» ведут лишь к усложнению и громоздкости без приобретения каких-либо полезных свойств. В программной инженерии в силу существующей практики разработки, ошибок спецификаций, нередкого отсутствия спецификаций, так называемых «дырявых абстракций» опять возникают полезные свойства избыточности. Но даже в программной инженерии избыточность в виде различных проверок и обходных ветвей обработки ошибок стремятся реализовать в оболочке вокруг простого ядра. Именно простое ядро является носителем смысла алгоритма, объекта, модуля, а то и всей программы, именно простое ядро отвечает за полезный прикладной результат работы программы.
Вокруг простого можно создавать защитные оболочки, обеспечивающие дополнительную устойчивость к ошибкам среды — так у меня была создана обёртка для HAL. Методы TDD могут подтолкнуть разработчика к подобному результату лишь в случае создания неких интегральных тестов на общую устойчивость. Однако идея о таких тестах посетит голову разработчика не ранее, чем он узнает о некоем интегральном аспекте или качестве решения (например, той же общей устойчивости). К этому знанию он может прийти либо аналитическим путём, самостоятельно сформулировав цели таких тестов, либо прочитать об этом в методическом руководстве, в котором тщательно перечислены все возможные аспекты тестирования системы.
Уже в этом месте возникает необходимость во взаимодополнении подходов. Ведь для попадания того или иного аспекта тестирования в методическое пособие, так или иначе кого-то должна посетить здравая мысль. И такая мысль вряд ли придет в голову человеку, который предпочитает думать «книжкой», а не самостоятельно. С другой стороны, малые проекты могут вовсе не иметь каких-то аспектов, описанных в пособии. Тогда при бездумных попытках применения метода разработчик создаст то, что не имеет смысла. (Хорошим примером таких безумств являются иные проектные документы, вроде SRS, заполняемые по строгой корпоративной или стандартной форме, в которых несчастные разработчики стараются придумать хоть какую-нибудь ерунду для каждого раздела, лишь бы не осталось пустых мест.)
Однако неправильно было бы считать, что аналитическим путём можно полностью избежать ошибок и снять необходимость в тестировании. При решении сложных задач результат может быть столь объёмен, что никак невозможно будет его во всех деталях охватить умом. В этих случаях неизбежны упущения и, как следствие, ошибки. Именно тщательное следование методу позволяет в таких ситуациях чувствовать себя увереннее за результат. Именно в этих случаях и нужны тесты, проверки.
В программировании проверки для алгоритмов называются предусловиями, постусловиями и инвариантами. Предусловие и постусловие позволяют контролировать границы контекста работы алгоритма; инварианты — устойчивость свойств контекста, нужных для работы алгоритма. Выполнения этих условий достаточно для обеспечения корректности работы программы и её частей. Именно эти условия по сути дела и являются теми тестами, которые стоит разрабатывать в рамках TDD. В этом случае, действительно, разработку алгоритма удобнее начинать со спецификаций того, что есть (предусловие) и что нужно получить (постусловие) — тогда предварительно написанный тест становится основанием для разработки алгоритма.
К сожалению, автор ни в этой статье, ни в предыдущей «Модульное тестирование ПО встроенных систем в среде Unity» вовсе не уделяет внимания тому, что стоит, а что не стоит тестировать, а также тому, какие у тестов бывают формы, и к каким формам следует прибегать в тех или иных случаях. В каких случаях следует внедрять проверки прямо в код, делая составной частью алгоритмов, а в каких — выносить в отдельные модули тестирования. В каких случаях предпочтительнее исключительные ситуации, в каких — assert-выражения, а в каких — обычные условия.
Без раскрытия всех этих вопросов умение пользоваться инструментом никак не поможет читателю сделать свою работу эффективной. Более того, бездумное написание тестов может породить ложное ощущение высокого качества результата и привести к пустым затратам времени.
Хотелось бы пожелать автору сосредотачиваться не только на технической стороне тестирования — как настроить ту или иную среду, какие действия в каком порядке выполнять, — но и озаботится демонстрацией полезных свойств демонстрируемых им методов, чтобы выгода от применения этих методов была бы очевидной, а само применение казалось совершенно необходимым. Хотелось бы услышать и более рациональные аргументы, нежели ссылки на «истинный профессионализм». И, конечно, большую вдумчивость при выборе тем демонстрационных проектов.
Версия для печати
Обсудить на форуме (8)