Показаны сообщения с ярлыком Разработка. Показать все сообщения
Показаны сообщения с ярлыком Разработка. Показать все сообщения

вторник, 17 марта 2015 г.

Как стать хорошим программистом

Зададимся вопросом: что нужно для того, чтобы стать хорошим программистом?

Как и везде, нужно развивать качества и навыки.

Для разработчика есть несколько навыков, которые, фактически, обязательны.

  1.     Уметь читать и писать по-английски
  2.     Уметь "вслепую" печатать на клавиатуре
  3.     Уметь грамотно и связно выражаться

Со стороны качеств - нужно всегда осознавать, что каждый день придётся узнавать что-то новое, процесс обучения не останавливается никогда. Нужно также в целом любить читать.

Со своего опыта могу сказать, что только с такими качествами можно нормально ориентироваться в разработке ПО.

Есть несколько книг, которые реально полезны для начинающего разработчика, и все они переведены на русский.

И да, математика в общем не нужна.

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

  1. Веб-приложения, такие, как, например, этот блог.
  2. Приложения реального времени, такие, как, например, серьёзные симуляции виртуальной реальности ("компьютерные игры"), или Google Earth.
  3. Программное обеспечение контроллеров, встроенное в аппаратуру ("встроенное ПО"), например, ПО калькулятора, стиральной машины, бионического протеза, инсулиновой помпы.
  4. "Коробочное" ПО, традиционное, которое условно "устанавливается" на компьютер и позволяет решать какие-то задачи.

Это разделение лично моё, в разных источниках можно прочитать другие варианты. Суть в том, что разные программы выглядят, составляются, и обслуживаются по-разному. Можно ещё условно выделить мобильные приложения, которые суть нечто среднее между веб-приложениями и "коробочными".

Я обещал книги. Вот они:

  1. "Чистый код", Роберт Мартин
  2. "Совершенный код", Стив Макконнелл
  3. "Программист-прагматик. Путь от подмастерья к мастеру", Эндрю Хант et al.
  4. "Идеальный программист", Роберт Мартин
  5. "Алгоритмы. Руководство по разработке", Стивен Скиена
  6. "Человеческий фактор. Успешные проекты и команды", Том ДеМарко et al.

Здесь перечислены книги, которые образуют фундамент знания разработчика любого ПО вообще. Они великолепны. Я вот "алгоритмы" ещё сам не дочитал, но это потому что она просто систематизирует то, что я уже на собственном опыте узнал из разных источников. После этих книг имеет смысл почитать хардкор:

  1. "Структура и Интерпретация Компьютерных Программ", Харольд Абельсон et al.
  2. На русском нет: "Concepts, Techniques, and Models of Computer Programming", Peter Van Roy

Есть ещё несколько книг такого же характера - углубляющих знания и описывающих хитрые неочевидные приёмы, но я их ещё не читал, поэтому рекомендовать не буду.

Ещё есть пять книг, описывающих приемы не разработки, а сопровождения программ.

  1. "Рефакторинг", Мартин Фаулер et al.
  2. "Приемы объектно-ориентированного проектирования. Паттерны проектирования", Эрих Гамма et al.
  3. "Шаблоны корпоративных приложений", Мартин Фаулер et al.
  4. На русском нет: "Specification by Example", Gojko Adzic
  5. "Непрерывное развертывание ПО", Джез Хамбл
Весь этот свод из 13 книг - это Веды разработки ПО. Никто не скажет, как стать "хорошим программистом" лучше, чем они.

Есть, конечно же, второй способ, более лёгкий, но который нельзя пройти в одиночку. ;) Можно найти уже установившегося "хорошего программиста" и работать с ним в паре. Есть техника "парного программирования", но я не об этом, там нужно, чтобы квалификация была одного уровня. Я о том, чтобы делать за него какую-нибудь вспомогательную работу, и параллельно учиться на его примере.

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

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

Например, вы решили стать разработчиком веб-приложений. Освойте платформу Ruby on Rails, пройдя последовательно вот это руководство: http://railstutorial.ru/chapters/4_0/beginning или платформу Node.js, начав отсюда: https://nodejs.org/documentation/tutorials/ (я предупреждал про английский язык), или платформу PHP, начав отсюда: http://php.net/manual/ru/ (я предупреждал про любовь к чтению). Это в целом равномощные платформы, разница только в синтаксисе и некоторых специфических возможностях, трудно реализуемых на смежных платформах.

Или вы решили писать встроенное ПО. Тогда стоит начать с проекта Arduino: http://www.arduino.cc/, правда придётся вложиться в "железо".

Или вы решили писать "коробочное" ПО, тут у вас огромный выбор языков программирования, например, Haskell: https://www.haskell.org/ или Erlang: http://learnyousomeerlang.com/ или C#: https://msdn.microsoft.com/ru-ru/library/67ef8sbd.aspx или Common Lisp: http://www.gigamonkeys.com/book/

Кстати, для оценки "хорошести" программиста можно воспользоваться вот этой "матрицей компетентности программиста": http://dev.by/pages/programming_matrix

четверг, 6 июня 2013 г.

Как написать веб-утилиту небольшого размера на Common Lisp

Когда-то написал небольшую программку для сообщества игры Prime World. Она парсит таблицу рейтингов и составляет по ней линейные графики, плюс вычислят средний рейтинг (в рейтинге указываются первые 25 мест) по каждому персонажу. Персонажей 62, соответственно, линий на графике тоже 62. :)

Здесь напишу, как и из чего сделал и как оно работает.

Полный исходный код выложил к себе на GitHub.

Как работает

План был такой: приложение представляет собой одну HTML страницу, с одним файлом CSS и одним Javascript.

При загрузке страницы яваскрипт сразу делает AJAX-запрос на бэкэнд за данными.

Бэкэнд грабит исходную HTML-страницу, при помощи XPath выбирает значения рейтингов, имена игроков, места и имена персонажей. Затем кодирует это всё в JSON и отправляет скрипту обратно.

Скрипт отрисовывает полученные от бэкэнда рейтинги в виде графика при помощи библиотеки Highcharts. Дополнительно бэкэнд возвращает вычисленные средние значения рейтинга; они отображаются в виде таблицы, которая обрабатывается javascript библиотекой DataTables, чтобы добавить сортировку.

Между таблицей и графиком переключаемся кнопками.

Вот само приложение.

Как я уже сказал, приложение достаточно маленькое.

В качестве недостатка можно упомянуть то, что каждый раз, когда приложение открывается, делается запрос на исходный сайт Нивала. Делать так, конечно, дурной тон, и стоит прикрутить какое-нибудь кэширование на стороне сервера. Хотя у них всё равно топ-25 не так часто обновляется, да и посещаемость никакая, так что для начала сойдёт.

Как собрано

Серверная часть основана на Hunchentoot, то есть, мы сами себе веб-сервер.

Вот все необходимые URL:

  1. /, по которому возвращается HTML страница приложения.
  2. /data, который AJAX endpoint, возвращающий JSON объект с данными
  3. /assets/scripts.js, клиентский скрипт, который раскладывает данные из JSON в график и таблицу
  4. /assets/styles.css, бумажка со стилями (с украшениями я не парился, в стилях только сокрытие некоторых элементов для инициализации интерфейса).
  5. /assets/loading.gif, показываем эту картинку пока /data грузится.
  6. /favicon.ico, иконка до кучи, честно стырил из пакета Html5 Boilerplate.

Хостимся на Heroku.

Извлечение исходных данных

Сам парсинг исходной веб-страницы полностью заключён в одном файле под названием dno.lisp, и единственная функция, которая оттуда нужна публично, это send-data-from-origin-as-json, которая генерирует строку в JSON формате, содержащую данные, нужные в scripts.js для того, чтобы построить графики и таблицу. Сам файл dno.lisp можно посмотреть в репозитарии на гитхабе, я здесь его приводить не буду, слишком большой.

В итоге мы можем делать (load "dno.lisp") и получать с этого функцию, которая даст нам JSON, который мы можем возвращать как результат AJAX запроса, то есть, то, что нужно для пункта 2.

Отдача статических файлов

В идеологии hunchentoot'а (если не углубляться в детали), инициализация приложения выглядит так:

  1. Настраиваем все пути в приложении, вызывая hunchentoot:create-folder-dispatcher-and-handler, hunchentoot-create-static-file-dispatcher-and-handler, hunchentoot:define-easy-handler и т. п. Это как раз было упрощено при помощи хелперов в предыдущем пункте.

  2. Создаём экземпляр приложения заклинанием:

    (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor 
                                      :port 4242)))
    

Для начала делаем три хелпера в отдельном файле, для того, чтобы добавление путей в Hunchentoot не было пыткой:

helpers.lisp

(defun publish-directory (uri dirname)
  (push (hunchentoot:create-folder-dispatcher-and-handler uri dirname)
        hunchentoot:*dispatch-table*))

(defun publish-file (uri filename)
  (push (hunchentoot:create-static-file-dispatcher-and-handler uri filename)
        hunchentoot:*dispatch-table*))

(defmacro publish-ajax-endpoint (uri name params &body body)
  `(hunchentoot:define-easy-handler (,name :uri ,uri) ,params 
     (setf (hunchentoot:content-type*) "application/json")
     ,@body))

Имея эти три хелпера, файл инициализации, фактически, будет иметь следующий вид:

init.lisp

;; Webroot
(publish-file "/" "webroot/index.html")

;; Favicon, just because
(publish-file "/favicon.ico" "webroot/favicon.ico")

;; Images, CSS and JS files referenced from index.html
(publish-directory "/assets/" "assets/")

;; AJAX endpoint to grab data to display in chart
(publish-ajax-endpoint "/data" data () (send-data-from-origin-as-json))

(defun run ()
  "Launch Hunchentoot web server instance on default port"
  (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)))

Само содержимое файлов index.html, scripts.js и styles.css можно посмотреть на гитхабе, здесь я описываю только то, как приложение собрано, а не как оно работает.

Мы не можем сделать (publish-directory "/" "webroot/"), потому что мы хотим дефолтный хэндлер для пути /.

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

Теперь мы можем делать так:

(load "dno.lisp")
(load "helpers.lisp")
(load "init.lisp")
(run)

После чего открываем браузер по адресу http://localhost:4242/ и наблюдаем готовое приложение, при условии, что в том же каталоге, что и эти два скрипта, находятся папки webroot и assets с соответствующими файлами в них.

Структурирование

Теперь запакуем всё в ASD. Вот моё определение приложения:

dno.asd

(asdf:defsystem #:dno
    :serial t
    :description "Presents ratings of players of Prime World as charts."
    :author "Mark Safronov <hijarian@gmail.com>"
    :license "Public Domain"
    :depends-on (#:drakma
                 #:cl-ppcre
                 #:cl-libxml2
                 #:iterate
                 #:cl-json
                 #:hunchentoot)
    :components ((:file "package")
                 (:module :src
                          :serial t
                          :components ((:file "helpers")
                                       (:file "dno")
                                       (:file "init")))))

package.lisp

(defpackage #:dno
  (:export :run)
  (:use #:cl
        #:cl-user 
        #:hunchentoot))

Как видно, все три исходника на Common Lisp, описанные выше, были переложены в отдельный подкаталог src. Понятное дело, во всех файлах теперь первой строчкой идёт вызов (in-package :dno).

Заметьте, что в ASD прописаны библиотеки, от которых зависит приложение (cl-ppcre, cl-libxml2, iterate и cl-json используются только в dno.lisp, на самом деле), однако сам пакет (package.lisp) не подключает эти библиотеки. Это просто дело вкуса: я предпочитаю использовать символы из чужих пакетов по их полному имени, не сокращая. Так сразу понятно, в какую документацию лезть.

Теперь у нас такая структура файлов:

assets/
    scripts.js
    styles.css
webroot/
    index.html
    favicon.ico
src/
    dno.lisp
    init.lisp
    helpers.lisp
dno.asd
package.lisp

Запуск на локальной машине

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

Если в рабочем каталоге Quicklisp в подкаталоге local-projects сделать символическую ссылку на каталог приложения, то можно будет подключать пакет приложения простым вызовом (ql:quickload :dno).

Таким образом, кладём в корневой каталог приложения следующий скрипт запуска:

runner.lisp

(in-package :cl-user)
(ql:quickload :dno)
(dno:run)

Теперь видно, зачем нужна отдельная функция run, определённая в init.lisp: для того, чтобы разделить загрузку самого приложения в рантайм и запуск приложения как веб-сервера.

Всё, готовое веб-приложение можно запустить из консоли, например, так:

$ cd path/to/dno/app
$ sbcl
* (load "runner.lisp")

Понятное дело, вместо SBCL может быть любой Лисп на ваш выбор.

Консоль становится неюзабельной после этого. Когда понадобится загасить приложение — нажатие Ctrl+D убивает приложение вместе с рантаймом лиспа.

Деплой на Heroku

Благодаря специальному buildpack'у для CL появилась возможность хостить SBCL + Hunchentoot приложения на Heroku.

Для этого нужно подготовить приложение следующим образом:

  1. Настраиваем у себя подключение к Heroku.
  2. heroku create -s cedar --buildpack http://github.com/jsmpereira/heroku-buildpack-cl.git. Запоминаем название проги, которое Heroku нам сгенерировало.
  3. heroku labs:enable user-env-compile -a myapp, вместо myapp пишем название проги из п.2.
  4. heroku config:add CL_IMPL=sbcl
  5. heroku config:add CL_WEBSERVER=hunchentoot
  6. heroku config:add LANG=en_US.UTF-8

Теперь в корневом каталоге приложения нужно добавить следующий скрипт, который ожидает buildpack:

heroku-setup.lisp

(in-package :cl-user)
(load (merge-pathnames *build-dir* "dno.asd"))
(ql:quickload :dno)

Крайне важно то, что этот скрипт не выполняет (dno:run), как это делает runner.lisp, потому что buildpack сам запустит Hunchentoot, так что от init.lisp требуется только настроить пути.

После того, как этот скрипт добавлен в репозитарий (имя heroku-setup.lisp имеет значение), можно пушить: git push heroku master, при условии, что п. 1 выполнен полностью.

Поздравляю

Всё, теперь прога готова, есть как возможность запустить на локальной машине, так и деплой сразу в Сеть (доменное имя тоже вполне приличное получается).

По тому же принципу можно строить любые другие простые веб-приложения, добавляя в init.lisp определения для путей в приложении.

Конечно же, если прога сложная, то придётся делать какой-то кустарный роутер, а также скорее всего, не отдавать статичные HTML файлы, а генерировать страницы при помощи какого-нибудь шаблонизатора типа CL-WHO, CL-EMB или чего-то подобного. Яваскрипт и CSS тоже можно генерировать прямо из Common Lisp, для этого есть проекты Parenscript и css-lite соответственно.

Мне всё это не понадобилось, приложение достаточно простое чтобы сразу отдавать статичные ассеты и HTML.

вторник, 28 августа 2012 г.

Context-dependent Behat tests steps

Preamble is this: we have the PHP-based website, and we are testing it with the Behat+Mink+MinkExtension combo.

Suppose we want to write the following test scenario:

When I am in the Friends section

… (something there) …

Then I should see “My Friend” in search results

Let's define this steps in our FeatureContext. First step we can define with the following regexp: /^I am in the Friends section$/ because we really don’t need the method of FeatureContext class containing long switch enumerating every possible section of the site.


/**
 * @Given /^I am in the "Friends" section$/
 */
public function iAmInTheFriendsSection() {
 return new Given('I am on "/friends"');
}

Second step we can define with the following regexp: /^I(?: should)? see "([^"]*)" in the search results$/.


/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {

 // separate helper function to search the "search results" HTML element
 $search_results = $this->getSearchResultsElement();

 // separate helper function to search the $search_term text in $search_results element
 $this->trySearchTextOnDomElement($search_term, $search_results);

}

It should be obvious why we use the custom test step instead of using the predefined test steps and writing something like 'I should see "My Friend" in ".search-wrapper form input[role="search"]" element'.

Then, someday, sure thing, we will want to write the following scenario:

When I am in the Shop section

… (something there) …

Then I should see “Interesting product” in search results

And in here, we have another “search results”, which should be found by completely different selector and which is located on different page.

So, this is the context-dependent statement: term “search results” depends on what “section” we mentioned previously. This is right from the linguistics. To be able to use this natural-language feature we need to implement it somehow.

I'll use the abbrev CDTS instead of longer "context-dependent test step".

Fortunately, Behat has a feature with exactly the same purpose: subcontexts. Unfortunately, it's not working in the way we need to use the CDTS properly.

In an ideal world, we can do this:


/**
* @Given /^I am in the Friends section$/
*/
public function iAmInTheFriendsSection() {
 $this->useContext('friends_section', new FriendsSectionContext())
 return new Given('I am on "/friends"');
}

and this would load the FriendsSectionContext and all CDTS definitions in it, like the following:


// in FriendsSectionContext class
/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {
 // This selector is valid only in the context of "Friends" section.
 $search_results_selector = '#PeopleFinder #pf_all .results';

 // Logic to check if the given $search_term is the present in anything called "search results" in the context of "Friends" section.
 $search_results = $this->getSession()->getPage()->find('css', $search_results_selector);
 $search_term_present = strpos($search_results->getHtml(), $search_term);
 if ($search_term_present === false) {
  throw new Exception(...);
 }
}

We useContext different context class, we get different definition for the /^I should see "([^"]*)" in the search results$/ test step.

Unfortunately, Behat cannot load the test step definitions from subcontexts at runtime. Apparently, it’s because it should parse the regexps in docblocks corresponding to definitions or something like that. So, are forced to load all our subcontexts right in our constructor.

Apart from being horribly ineffective, this prevents us from defining the test steps having same regexp across several different separate subcontexts.

Workaround for this problem is this:

  1. add the property to the FeatureContext which will hold the reference to current subcontext, name it like "location_context" or so,
  2. make the context-setting ('I am in the "..." section') test step set the "location_context" to the subcontext needed (you can get the subcontext with the call to getSubcontext('alias')),
  3. move the context-dependent logic to “normal” subcontext methods, which should have the same name across all subcontexts,
  4. register all subcontexts with useContext under meaningful aliases like "friends_section", "shop_section", etc,
  5. define the context-dependent test step like 'I should see "..." in search results' in main FeatureContext class,
  6. in the definition of this step, get the context-dependent logic needed by calling the relevant method on the subcontext the "location_context" property currently points at.

So, we need our context-setting test steps to be like this:


/**
 * @Given /^I am in the "Friends" section$/
 */
public function iAmInTheFriendsSection() {
 $this->location_context = $this->getSubcontext('friends_section');
 return new Given('I am on "/people"');
}

Assuming 'friends_section' is an alias of the FriendsSectionContext, and it was set in the constructor, after this test step, our "location_context" will be FriendsSectionContext, and, say, it's getSearchResultsElement() will do exactly what we need in the "Friends" section.

Then, the context-depentent test step will be like this, getting the location-dependent logic from the "location_context" set previously:


/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {
 // getSearchResultsElement() is defined in subcontext which was set before in $this->location_context
 $search_results = $this->location_context->getSearchResultsElement();
 $search_term_present = strpos($search_results->getHtml(), $search_term);
 if ($search_term_present === false) {
  throw new Exception(...);
 }
}

Main point is this: we want to check if something should appear in the "search results" entity in some different page → we can use the same test step in our .feature files, just explicitly name the section needed beforehand somewhere above in the text. This will make the .feature files a lot more human-readable.

This concludes the explanation about how to use this linguistic technique in Behat tests.

понедельник, 2 июля 2012 г.

Использование Qt для реализации GUI в Common Lisp

У меня нет никаких предрассудков насчёт Qt и на десктопе у меня KDE, так что для первых шагов в GUI я воспользовался проектом под названием CommonQt. Это CFFI-биндинги к libqt и libsmoke.

Здесь я запишу, как настроить окружение для того, чтобы писать Qt-приложения. Ниже скриншот конечного результата. ;)

четверг, 14 июня 2012 г.

Простое и сложное в Common Lisp

Сегодня развлекался с моим любимым Common Lisp и обнаружил интересную деталь.

Допустим, мы хотим написать простую функцию для определения среднего арифметического набора заданных чисел. Чтобы она работала вот так:

(average 1 2 3 4)
5/2

...ну хорошо, хорошо:

(coerce (average 1 2 3 4) 'float)
2.5

Заметьте то, что я не хочу передавать в average список, я хочу кортеж чисел. Интересная деталь вот в чём: самая простая (в смысле, лаконичная) реализация, которая пришла мне в голову, это такая:

(defun average (&rest args)
  (/ (reduce #'+ args) (list-length args)))

Дело в том, что определение среднего арифметического — это задачка для средней школы, и мы вполне можем её дать в качестве задания по программированию на второй паре (на первой мы изучим весь синтаксис лиспа вместе с макросами defun и defvar). Но на самом деле мы не можем её дать, потому что у нас есть вот эта форма в решении: (reduce #'+ args), и, чтобы студенты смогли ей бегло пользоваться, нужно, чтобы они знали как минимум:

  • о разделении пространства имён переменных и пространства имён функций;
  • как передавать в функцию имя другой функции;
  • о назначении и работе reduce, что для не знакомого с функциональными методами, вообще говоря, стопроцентная магия (особенно если их уже обработали алголоподобными языками);
  • о том, что параметры, переданные через &rest, оборачиваются в список.

Не то, чтобы я здесь докапывался до Лиспа, но всё же факт интересный: организация этого языка такова, что для решения подобной простой задачи нам требуется знать пол-языка и ещё минимум одну функциональную концепцию.

Конечно, мы можем соорудить нечто работающее из let и dotimes, но тогда зачем нам Common Lisp вообще? :)

четверг, 7 июня 2012 г.

Паттерн «Веб-функция»

Допустим, вам нужно написать endpoint для аякс-запроса. Или обычный обработчик обычного POST-запроса от веб-формы. Тогда основные действия, которые обязательно должны быть в процессе обработки, будут такие:

  • Фильтрация входных данных (это НЕ валидация бизнес-правилами, только лишь такие действия, как укорачивание, очистка от окружающих пробелов, исключение непечатных символов, приведение к нижнему регистру и т. д.). Или мы можем даже сыграть роль адаптера и переименовать некоторые параметры запроса в вид, ожидаемый обработчиком.
  • Обработка запроса. Здесь мы собственно делаем то, зачем существуем. Мы ожидаем уже более-менее очищенный от мусора массив входных параметров, возможно, среди которых есть ошибочные (например, числовое значение вне допустимого диапазона, или передан массив вместо скаляра).
  • Форматирование результата для отклика клиенту. Например, мы можем возвращать JSON, или генерировать HTML страницу по шаблону. Или генерировать изображения. Или отдавать файлы с диска.

Если записать в функциональном стиле на PHP, то можно получить следующий шаблон хэндлера:


<?php

echo format(process(clean($_REQUEST)));

/**
 * Здесь очистка суперглобального массива $_REQUEST и генерация массива $request
 * 
 * @param array $request Данные запроса из массива $_POST или $_GET (или $_REQUEST).
 * @return array Очищенные данные из запроса, которые ожидает функция process.
 */
function clean($request)
{
  // TODO
  return $request;
}

/**
 * Здесь выполнение действий согласно запросу $request
 * 
 * @param array $request Параметры запроса, очищенные функцией clean.
 * @return array $result Результат работы со всей информацией, необходимой для форматирования ответа клиенту.
 */
function process($request)
{
  // TODO
  return $request;
}

/**
 * Здесь форматирование результата работы для выдачи клиенту.
 * 
 * @param array $result Результат работы, как он сгенерирован процессором. Нам не дозволяется использовать какие-либо другие данные.
 * @return string Форматированный ответ, готовый к отправке клиенту через echo.
 */
function format($result)
{
  // TODO
  return json_encode($result);
}
?>

Если process сталкивается с ошибкой, он пишет об этом в результат своей работы (например, в поле 'error') и сразу возвращает результат в format.

Если format должен генерировать большой HTML документ, то ничего страшного, он может и делать echo внутри себя, вместо того, чтобы возвращать строку. Тогда вызов всей цепочки будет без echo в начале, конечно же.

Как писать юнит-тесты к программе на Free Pascal при помощи FPTest

В свете работы над моими старыми программами из КГУ понадобилось покрывать код юнит-тестами. Как выяснилось после гуглопоиска, для Free Pascal, которым я компилирую свою переработку, существует проект под названием FPTest, представляющий собой каркас для написания юнит-тестов на этом замечательном языке.

Документации по этому проекту довольно немного, и официальная вики, как и README, сильно Lazarus-ориентированы. Поэтому расскажу здесь, как подключить FPTest к существующему консольному проекту и собрать тесты, написанные с его помощью.

воскресенье, 13 мая 2012 г.

Рефакторинг заданий курса КГУ по программированию 2004-2011 гг.

У нас в набережночелнинском филиале КГУ на факультете прикладной математики и информационных технологий преподавали несколько программистских курсов. На них студентам задавали задания в духе "перемножьте каждый четвертый член массива чисел полученной от пользователя длины", и их нужно было запрограммировать. В первом семестре - на Delphi. Потом - на C++. После третьего курса можно было писать на чём хочешь.

Пожалуй, это были единственные учебные курсы из всех пяти лет обучения, которые хоть что-то дали лично мне как разработчику. Во всяком случае, я узнал об ООП в стиле C++ и о такой вещи, как указатели.

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

Изменения, которые я хочу внести, следующие:

  1. Структура каждой программы должна быть как можно проще. Всё должно компилироваться из командной строки. Всю IDE машинерию нужно убрать.
  2. Для сборки будет использоваться GNU Make, для программ на любом языке.
  3. Покрыть код юнит-тестами.  Чистый TDD использовать уже поздно, всё-таки код уже написан.
  4. Код единообразно переформатировать, очистить от "попахивающих" мест и снабдить формальными комментариями.
  5. Текст заданий изъять изо всяких PDF и DJVU файлов, в которых он сейчас находится, и перенести в текстовые README.md.
Кроме того, для разработки на первых курсах активно использовался homemade хак для того, чтобы выводить кириллический текст в консоль. Отдельный хак для программ на Паскале, отдельный для программ на Си. Общаться с пользователем по-русски не так важно, так что для простоты все сообщения от программ будут переведены на английский.

В первом семестре использовался Delphi, с помощью которого компилировались консольные приложения. Вместо Delphi будет использоваться Free Pascal в объектном режиме. Всю машинерию, связанную с Delphi, нужно убрать.

Для тестов программ на Паскале будем использовать FPTest.

Начиная со второго семестра и дальше использовался C++. Для компиляции точно буду использовать g++ вместо Visual Studio, а вот какой каркас для тестов использовать, пока не знаю (просто пока не пробовал xUnit-каркас для C++).

Исходники уже выложил на ГитХабе. Пока более-менее нормально обработано только первое задание.

Думаю, когда закончу задания первого семестра, напишу письмо в КГУ. :)

понедельник, 23 апреля 2012 г.

Мысли о третьем шаге в TDD

Сегодня второй раз в жизни пишу код согласно чистому TDD, прямо как заповедали такие мастера, как Роберт Мартин, Эндрю Хант и Майкл Физерс. Один случай меня заставил серьёзно задуматься о разнице в подходах к разработке.

Третий шаг в TDD гласит: «напиши ровно столько кода, сколько нужно для того, чтобы проваливающийся ранее тест начал завершаться успешно». (Первые два это «напиши проваливающийся тест» и «заставь этот тест компилироваться»). Несмотря на то, что он кажется довольно простым, пункт насчёт «ровно столько, сколько нужно» невероятно важен в рамках этой методологии.

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

Только после раздумья длиной что-то около полудня я понял, почему так важно соблюдать это правило, хотя ответ очень прост. Если ты напишешь код, который не предусматривают твои тесты, пусть даже он будет выполнять какую-то идейно полезную работу (а он, конечно же, будет выполнять идейно полезную работу), он не будет покрыт тестами. А код, который не покрыт тестами — это legacy код, которому положено гореть в аду. Более того, если ты оставишь весь этот код среди другого, чистого и покрытого тестами, он будет, как ржавая арматура, мешать добавлять новую функциональность. Так как он не покрыт тестами, а ты в целом стараешься следовать TDD, значит, все эти буквы становятся просто шумом, про полезность и надёжность которого ты не знаешь ничего.

Наверное, теперь я на самом деле понял, насколько иной философией разработки пользуюсь. Сегодня я удалил целый класс, почти 100 строк кода, который написал, пытаясь закрыть один проваливающийся тест, и ни разу не пожалел об этом. Скорее наоборот: вспоминая ту ужасную массу кода, я уверен, что моя программа стала гораздо лучше, лишившись её.