среда, 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)
... Успешно.

Что-то я наверное делаю не так? :)

Показать полностью...