среда, 2 декабря 2009 г.

Игры со временем

Многие начинающие agile команды наверняка испытывают проблемы с планированием. И наша команда тоже через это прошла. Мы долго и упорно пытались планировать время, строить графики сгорания, пока не поняли что смысла не будет. И теперь начинаю понимать - почему...

Не нужно планировать время.

Очень много времени в SCRUM тратится на планирование. Мало того, что мы должны разделить все истории на задачи, мы еще должны, как дураки, оценивать время играя в карты.

Программистам не нужно время. Дайте программисту четко поставленную задачу и не отвлекайте, пока не будет готово.

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

Чтобы программисты могли точно оценивать время - нужен большой опыт. Причем опыт именно в предсказании времени :). Можно прекрасно представлять себе объем работ, но постоянно ошибаться со сроками.

Кроме того, можно достаточно легко представить сколько времени займет очередное изменение, но конкретно плавать в оценке сроков задач, которые предстоит сделать под конец итерации, потому что объем предварительных измнений - только в головах...

И это тоже проблема. Поэтому не надо планировать все задачи сразу.

Если вы планируете внедрять SCRUM - послушайте моего совета. На бумажках нельзя писать абстрактные и неконкретные вещи. Каждая бумажка должна представлять из себя конкретную задачу для одного человека. Не получается чтобы по одной бумажке и программист программировал и тестер тестировал. Не пойдет скрам. Бумажки будут мотаться по доске без видимого результата. Еще раз: конкретная задача для одного человека. Взял, сделал, done, без вариантов!

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

Это чем-то перекликается с GTD Дэвида Аллена. Тем, что все запланировать практически нереально, но первое конкретное действие легко определяется почти всегда (Или фичу нужно сразу вернуть оунеру).

Вероятно на этом принципе держится Канбан. MMF - достаточно крупное понятие, и программист должен видеть конкретные действия, которые способны продвинуть задачу вперед. На доске место для них не предусмотрено. И если конкретности вИдения не достает - то канбан не пойдет, встанет.

В SCRUM проще. Мы разбиваем задачу на работы и прикидываем время выполнения этих работ. Только проблема в том, что если работа неконкретная, то и время выполнения получается неконкретное, и следовательно вряд ли оно будет соблюдено.

И получается, что время не играет роли. Если задача и действия по ней конкретные - она будет двигаться вперед, в противном случае мы будем топтаться на месте. А время - чисто статистическая характеристика, нужная, как я уже сказал, только PO.

Но это вовсе не означает что время совершенно не нужно для программистов.

Программирование - процесс творческий. И зачастую связанный с познанием неизведанного. А познание неизведанного - процесс крайне трудно поддающийся планированию. Когда речь идет о неизведанном - разговоры о времени неуместны? Еще как уместны!

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

То есть просто временной лимит. Который, следует заметить, для конкретной работы не просто не нужен - а даже вреден, поскольку вносит лишний нервирующий фактор. Риск неуложиться во время, демотивация, и тд и тп...

Вот где-то такие мысли у меня по поводу agile. Не SCRUM, не Kanban...

PS: Опять давно ничего не писал, мыслей вроде много, а писать - руки не доходят. Как с этим бороться?

среда, 14 октября 2009 г.

KDE3 is dead?

На прошлой неделе в Gentoo размаскировали KDE-4.3 для платформы amd64. При этом, после окончательной стабилизации KDE4, KDE3 будет удалена из репозитория навсегда.

Двоякое ощущение.

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

Но правда открытого софта такова, что старая версия уже никому не нужна. И никто не будет возиться с багами, которых там еще много. Ну будем надеяться, что в kde4 помимо красивых плазмоидов у разработчиков дойдут руки и до полезных и нужных вещей, как то работа с сертификатами, получение адресов из LDAP, почему-то все мои проблемы крутятся в основном вокруг kmail? Нет, в kopete очень слабая поддержка jabber. :)

Короче, переехал на kde-4.3.1. Прошарил свою систему, удалил оттуда все, что зависело от qt3, и теперь у меня чистая qt4 система.

Кроме того, я наконец то нашел решение проблемы ntlmaps, терзавшей меня долгое время. Проблема заключается в том, что в один прекрасный момент он перестал запускаться из init скрипта. С консоли запускается, а из init-скрипта ни в какую. Кроме того я давным давно не могу пустить через него firefox - ntlmaps не проксирует запросы почему-то. А иногда еще и проц жрет почем зря - под 100%.

Обнаружилась вполне достойная альтернатива - cntlm , которая пока правда не входит в репозиторий gentoo, но ebuild можно скачать отсюда, чуть чуть поплясать с бубном и все, настало полнейшее счастье, и причем значительно более шустрое.

Значит жизнь продолжается! :) Что касается сертификатов - Thawte наконец таки перестал раздавать публичные сертификаты. И значит информацию о зарплате отныне будут доводить на наших, внутренних.
Пойду получать новый сертификат Информзащиты.

понедельник, 14 сентября 2009 г.

Куда едут мыши?

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

... Я уже собирался было ее выкинуть, но тут меня осенило.

Клавиатура у меня безпроводная, BTC-9116URF. Она совмещает в себе клавиатуру и мышь. Черная штука в правом-верхнем углу - это джойстик.

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

А тем временем Google включил на GoogleCode Mercurial. Я сразу же, как узнал, одно из своих извращений перевел - побаловаться для начала. wiki они отделили, теперь wiki хранится в отдельном репозитории, версии посмотреть можно, а вот как склонировать - могу только догадываться. Возможно, что не предполагает клонирования, только веб-редактирование. Ну и ладно, а то раньше веб-редактирование сильно засоряло репозиторий, я предпочитал редактировать локально.

Кроме того балуюсь с Google Wave, много всяких интересных идей, но о них попозже.

четверг, 10 сентября 2009 г.

The big class unittest

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

Вот и у меня такая же ситуация. Все имена и методы по возможности изменены, все совпадения случайны.

Есть большой класс... Назовем его CBigApp... 173 публичных метода, еще 40 частных. Видимо для того, чтобы с ним было проще работать реализация разнесена приблизительно на 20 файлов... Ну чтож, это облегчает нашу задачу.

А задача заключается в том, что приложение почему-то перестало импортировать конфигурацию от предыдущей версии. Хороший момент для написания первого юниттеста! :)

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

Можно создать ему конструктор, который ничего не создает, но это именно то, чего мы пока хотим избежать - модификация кода приложения. Да и одним конструктором дело точно не ограничится.

Для проверки импорта конфигурации нам вполне неплохо подходит одна функция - назовем ее import_config, хоят по легенде это сделано давным давно, в 19-м веке. :) Эта функция практически не пользуется другими методами класса, кроме функции convert_config, которая в свою очередь дергает несколько мелких функций по конвертации. Все эти функции не зависят от остального содержимого класса... одна беда - они private.

Как говорят великие, такой набор функций - хороший кандидат на извлечение класса по виду ответственности.

Призовем на помощ препроцессор. Поскольку оригинальное описание класса нас совершенно не устраивает - мы его прячем:
namespace hidden {
#include "CBigApp.h"
}
Если мы этого не сделаем, то оно заинклюдится из сишного файла, который мы хотим заинклюдить к себе. Но для начала мы опишем свою версию класса, и делать это мы будем в cьюте, чтобы изолировать его от взаимозависимостей, ведь у нас как бы появятся свои версии функций. Неизбежны проблемы при линковке тестового приложения. Сьют позволяет изолировать эти манипуляции.
BOOST_AUTO_TEST_SUITE(suiteCBiGApp_ImportConfig)

struct CBigApp {
void import_config(const archive &aconfig, const std::string &config_file);
void convert_config(const std::string &config_file, u_int version);
void fix_10_value(const std::string &value &);
void fix_21_value(const std::string &value &);
void fix_22_value(const std::string &value &);
};
Это собственно наше тестовое видение класса. Оно еще не окончательное и нам еще придется с ним повозиться. Прототипы мы без искажений скопировали из CBigApp.h. Ну а теперь подтягиваем реализацию.
#include "Import.cpp"
Может так сложиться, что помимо вышеописанных функций в этом файле реализуются и другие. Тогда и их прототипы необходимо вписать в тестовый класс.

Может быть эта реализация ссылается на другие функции класса, описанные в другом файле. Их тоже можно описать в структуре с необходимой реализацией.
void create_minimal_config(const std::string &)
{ throw logic_error("Не используется"); }
Помимо того наверняка будет так, что в этом файле включаются и другие инклюды - все эти включения, в том числе и стандартные необходимо перенести в начало нашего юниттест-файла, чтобы они первый раз включились в глобальном пространстве имен, второй раз они уже не будут включаться.

После этого у нас появилась возможность вызвать тест...
BOOST_AUTO_TEST_CASE(testImport10)
{
CBigApp app;
BOOST_REQUIRE_NO_THROW(app.import_config(cfg10, "config_file-1.0.cfg"));
::unlink("config_file-1.0.cfg");
}
Да, я знаю, что использовать файловую систему в юниттестах - плохо... но лучше иметь юниттесты, которые используют файловую систему (базы данных, подключения по сети), чем не иметь никаких. Для успокоения совсети их можно назвать функциональными. :)

В тестировании можно пойти дальше, объявив некоторые из методов как virtual, и заменить их в процессе тестирования на что-то удобное для себя:
BOOST_AUTO_TEST_CASE(testImport21)
{
struct inCBigApp : public CBigApp {
virual fix_10_value(const std::string &value &)
{ throw logic_error("Не должен вызываться"); }
} app;
BOOST_REQUIRE_NO_THROW(app.import_config(cfg21, "config_file-2.1.cfg"));
::unlink("config_file-2.1.cfg");
}

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

Вроде пока все.

вторник, 1 сентября 2009 г.

null ostream


Последнее время старательно осваиваю TDD. На работе это пока не используется, но для себя все пишу через тесты. Привычку вырабатываю. :) Купил вот недавно самую дорогую книгу в своей жизни: Шаблоны тестирования xUnit. Рефакторинг кода тестов. Очень познавательная книга. Юнит тестинг без фанатизма. Там в частности написано, что использовать базы данных в тестах можно! Но есть способы лучше. Вообще главная мораль этой книги - что наличие тестов, каких бы то ни было значительно лучше, чем их отсутствие.

Но собственно написать я хотел о другом. Изобретаю консольное приложение. Консольное приложение должно что-то выводить на консоль. Для этого традиционно используется cout. И собственно приложению ничего больше не нужно.

Но когда речь заходит о тестах - cout становится серьезной помехой.

Я выбрал следующий путь решения. Методы, которые что-то выводят, получают в качестве параметра ostream &. Это позволяет в тестах подсунуть вместо cout что-то другое, например ostringstream. И проверить что же собственно вывелось на консоль после вызова метода.

Но иногда тестам и этого не нужно. Бывает нужно передать что нибудь, только чтобы метод им удовлетворился и проглотил. И встает вопрос dummy ostream - потока-заглушки.

Вариант #1 с заглушиванием cout:
std::cout.exceptions(std::ios::goodbit); 
std::cout.setstate(std::ios::failbit);
std::cout << 1; // ignore
... мне совершенно не нравится. Да и для тестов он не очень удобен. Каждый раз глушить и разглушать обратно? Нет, нам нужен объект, наследник ostream, который просто не делает ничего. Сделать так можно. Решение приводить не буду, сошлюсь. Но давайте посмотрим что есть на эту тему в boost. Решение #3, найденное в интернете:
struct null_sink : boost::io::sink {
void write(const char*, std::streamsize) { }
};
typedef boost::io::stream_facade<null_sink> null_ostream;
... сейчас не работает. Не скажу точно когда работало, не важно. Решение #4:
typedef boost::iostreams::stream<boost::iostreams::null_sink> null_ostream;
... работает, но для того, чтобы все было гладко, перед использованием надо вызвать out.open(boost::iostreams::null_sink());, что не совсем удобно. И последнее... onullstream описан в файле boost/test/utils/nullstream.hpp. Одного только не понимаю, почему я не могу создавать его непосредственно при вызове?
int foo(ostream &out);
BOOST_CHECK_EQUAL(foo(onullstream()), 1)
... возвращает ошибку, о том, что нет метода foo, в который можно было бы передать onullstream... но если создать onullstream заранее, то никаких возражений у компилятора не возникает.
onullstream out;
BOOST_CHECK_EQUAL(foo(oot), 1)
... Успешно. Что-то я наверное делаю не так? :)