четверг, 12 июня 2014 г.

[prog.c++] Синхронность в SObjectizer: зачем это вообще и что же в итоге получилось?

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

[prog] Суворов Александр Васильевич о маленьких командах :)

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

[prog] Кстати, о трудоемкости программирования. На личном примере.

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

Сложно сказать, насколько эти случаи распространены. К сожалению, я и сам попался на эту удочку. Хотя честно пытался бороться с этим эффектом и старался себя сдерживать, но не всегда это удавалось :(

Тем интереснее, после руководящей работы, вновь почувствовать себя в шкуре программиста и ощутить на себе трудоемкость этого трудно прогнозируемого занятия. На личном, так сказать, примере.

В течении последней недели я занимался реализацией синхронного взаимодействия агентов в SObjectizer. На рождение идеи ушло где-то два дня. И еще дня полтора на получение первой реализации. Казалось, что осталось всего-ничего: пару тестов, один-два примера и всех делов.

Однако сейчас, когда все это уже закончено, можно оглянуться назад и сказать, что в действительности эти "пара тестов и один-два примера" заняли намного больше, чем я ожидал. Как по времени, так и по усилиям.

Проиллюстрировать это можно в цифрах. Хотя LOC-и (т.е. количество строк кода) -- это параметр не очень адекватный, но в данном случае подойдет и он.

Итак, создание первой рабочей версии привело к увеличению количества строк в проекте на ~500 штук (с 16271 на ревизии 570 до 16744 на ревизии 573). Тут считаются только новые строки, еще какое-то их количество было переработано, а что-то и вовсе выброшено. Ну да не суть. Важно то, что в эти 500 строк уложились основные изменения, которые затем правились еще, но уже не кардинальным образом. Т.е., грубо говоря, ядро изменений, на придумывание которого ушло два дня, и еще день на реализацию -- это эти самые 500 строк кода.

А вот когда новый код был покрыт тестами и проиллюстрирован несколькими примерами, размер проекта увеличился еще на ~2000 строк (18977 на ревизии 597). И опять таки, для простоты считаются только новые строки, а те, которые перерабатывались и удалялись от ревизии к ревизии, не учитываются. Т.е. фактической работы было еще больше.

Промашка по срокам на создание всей этой тестовой и демонстрационной обвязки составила два раза: 4 дня вместо предполагавшихся мной 2.

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

И еще одно крайне немаловажное уточнение: над SObjectizer-ом я сейчас работаю просто в идеальных условиях: трачу на него столько времени, сколько нужно. Мои усилия ограничиваются только моими физическими возможностями. Это прекрасно известная мне предметная область и хорошо знакомый мне код. Я пользуюсь инструментами, которыми владею достаточно уверенно и про которые легко найти необходимую информацию в Интернете. Надо мной не давлеют сроки. Мне не нужно общаться с заказчиком и/или руководством. Меня не отвлекают работающие по соседству программисты или тестировщики, или внедренцы, или суппорт. Мне не нужно тратить время на дорогу в офис и обратно...

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

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

К сожалению, чем дальше ты уходишь от разработки в начальствование, тем меньше ты об этом помнишь. Отсюда и неправильное мнение о том, с какой скорость должна идти работа. Хотя, казалось бы, всего год-полтора назад ты об этом прекрасно помнил! :)

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

В качестве демонстрации приведу пример из той же реализации синхронных сервисов в SObjectizer. Дабы не забывать что еще нужно сделать, я завел в Wiki простой TODO-лист, который начался всего с нескольких строк, имеющих отношение к синхронным сервисам (вот этот вариант). Но затем он трансформировался во все более и более подробный перечень разных вкусностей и полезностей (в том числе и крайне спорных). Вот, к примеру, один из промежуточных вариантов. И это при том, что я далеко не сразу фиксирую там то, что приходит в голову. Какие-то вещи требуют "вызревания" для формализации. Какие-то требуют борьбы с собственной ленью :) А ведь далеко не все разработчики имеют привычку вот такой фиксации своих идей. Особенно, если речь идет не о собственном проекте, а о том, чем ты занимаешься на работе "для дяди"...

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

среда, 11 июня 2014 г.

[prog.c++] Анонс: Очередная встреча C++ User Group в Санкт-Петербурге 21 июня

На правах дружеской рекламы :)
21 июня в Санкт-Петербурге пройдет очередная встреча C++ User Group. Это мероприятие — возможность общения для разработчиков, пишущих на C++, где можно поговорить именно о C++ и связанных вопросах. На встрече выступят докладчики из Москвы, Санкт-Петербурга.
Программа
11.30 Регистрация
12.00 Максим Хижинский, “Flat Combining — легкий путь в сложный мир конкурентных контейнеров”
13.00 Григорий Демченко, “Fine-grained locks”
14.00 Перерыв
14.30 Антон Бикинеев, “Generic Numeric Programming with Boost.Math and Boost.Multiprecision”
15.30 Дмитрий Нестерук, “Метод Монте-Карло на C++“

Место проведения
Гостиница «Октябрьская», зал «Синий» (Санкт-Петербург, Лиговский проспект, дом 10).
Участие бесплатное, но требуется предварительная регистрация (по приведенной выше ссылке).

PS. Репост приветствуется.

вторник, 10 июня 2014 г.

[prog.c++] Да уж, засада с этим ACE 6.2.6 (а так же MSVC, MinGW-w64 и пр.)...

Текущая версия ACE (6.2.6 на данный момент) поддерживает только MSVC 11.0, и не имеет готовых sln-файлов для MSVC 12.0.

При этом в MSVC 11.0 нет поддержки variadic templates и есть баги в реализации std::future (например, мне очень мешает вот этот). Вроде как MSVC 12.0 все это уже умеет, но ACE пока его не поддерживает официально... :(

MinGW-w64 предоставляет два варианта GCC: один win32, второй posix. В первом нет поддержки для C++11 threading (т.е. нет нитей, promise, future и т.д.). А во втором, который posix, все это есть (хоть и не быстрое). Но ACE не может скомпилироваться в posix-варианте GCC от MinGW-w64 из-за того, что в этом варианте есть куча макросов вроде localtime_r. Для ACE это проблема, т.к. ACE пытается определять localtime_r самостоятельно, да еще в собственном пространстве имен ACE_OS, чему подобные макросы сильно препятствуют... Про эту проблему знают еще с версии 6.2.1, даже предлагали патчи и оформляли тикет в bugzilla, да только воз и ныне там... :(

Пичалька. Пока спасает наличие нормального GCC 4.8.3 под Cygwin-ом. Ну и остается надеяться на то, что ACE-овцы таки сделают поддержку MSVC 12.0 "искаропки"... На поддержку MinGW-w64-posix я и не надеюсь.

[prog.c++] Ну вот нравятся мне возможности современного C++!

Программирование на современном C++ сейчас -- это совсем другие ощущения, чем программирование на современном C++ десятилетней давности. И хотя тогда какой-нибудь Visual C++ 2003 с более-менее нормальной поддержкой STL был большим прорывом по сравнению с Visual C++ 6.0, но все равно, количество необходимых приседаний было слишком большим. Да и общая атмосфера была такая, что казалось, что C++ будет неотвратимо превращаться в устаревший, мало кем и где используемый маргинальный язык.

Тем не менее, время шло, долгострой под названием C++0x успешно завершился и его результаты (например, в Visual C++ 2012/2013 или GCC 4.8/4.9) не могут не радовать. Например, пару дней назад перерабатывал старый класс, в нескольких методах которого нужно было простым поиском найти элемент в векторе простых структур. Поскольку операция эта 1-в-1 повторялась во всех методах, то возникла мысль вынести обращение к std::find_if в один вспомогательный метод, а дальше дергать только его. Но...

Но есть нюанс :) В C++ не получится ограничится вот такой простой реализацией:

typedef std::vector< state_and_handler_t > handler_container_t;

handler_container_t::iterator
try_find_handler( handler_container_t & where,
      const state_t & search_key )
   {
      return std::find_if( where.begin(), where.end(), ... );
   }

Поскольку, если экземпляр handler_container_t является атрибутом какого-то объекта, то иногда ссылка на него будет константной:

class handler_caller_t
   {
      handler_container_t m_handlers;
      ...
   public :
      void call_handler( const state_t & current_state ) const
         {
            // Это не скомпилируется!!!
            handler_container_t::iterator it = try_find_handler(
                  m_handlers, current_state );
            ...
         }

      void remove_handler( const state_t & target_state )
         {
            m_handlers.erase(
                  try_find_handler( m_handlers, target_state ) );
         }
      ...
   };

Проблема здесь в том, что в handler_caller_t::call() ссылка на m_handlers константна, т.к. сам метод call() константен. Поэтому ее нельзя передать в try_find_handler.

При работе в C++03 на ум сразу приходит два решения: либо тупо продублировать try_find_handler для константного и неконстантного аргументов, либо же попробовать сделать try_find_handler шаблоном, тип аргумента которого выводится. Второй вариант кажется более привлекательным, т.к. он позволяет избежать дублирования кода, но он не сработает:

templateclass C >
typename C::iterator
try_find_handler( C & where,
      const state_t & search_key )
   {
      return std::find_if( where.begin(), where.end(), ... );
   }

Проблема в возвращаемом значении. Его тип будет iterator только для неконстантного контейнера. Если же в try_find_handler передается константная ссылка, то методы begin()/end() будут возвращать const_iterator, а не iterator. Следовательно, const_iterator будет возвращаться из std::find_if. А так как const_iterator -- это отличный от типа iterator тип, то значение данного типа нельзя вернуть из такого варианта try_find_handler. Что приведет к невозможности скомпилировать шаблонный вариант try_find_handler для константного контейнера.

Честно скажу, что сходу я не смог придумать обходной маневр для решения этой проблемы в рамках C++03, хотя если задействовать шаблонную магии, он, может быть, и отыщется. Будь у меня необходимость работать жестко в рамках C++03, то ухищрениями с шаблонами я бы не занимался, а тупо бы продублировал бы код try_find_handler.

К счастью, сейчас я могу себе позволить ограничится только C++11. А в нем, за счет некоторых новых фич, появляется возможность написать шаблонный код try_find_handler всего один раз. Вот как это в итоге стало выглядеть у меня:

templateclass C >
auto /* (1) */
try_find_handler(
   C & container,
   const state_t & state ) -> decltype( std::begin(container) ) /* (2) */
   {
      typedef decltype(*std::begin(container)) value_t; /* (3) */

      return std::find_if(
            std::begin( container ),
            std::end( container ),
            [&state]( value_t & o ) { return o.m_state == &state; } );
   }

Ключевые моменты помеченны в комментариях циферьками.

Пункт первый: в С++11 можно не писать сразу тип возвращаемого функцией/методом значения, а вместо этого поставить ключевое слово auto. Это означает, что тип возвращаемого значения будет уточнен после декларации аргументов функции. Что позволяет задействовать типы аргументов в процессе вывода типа возвращаемого значения.

Именно это показано в пункте (2). Там за счет еще одной новой фишки C++11 -- decltype -- определяется, какой тип возвращает std::begin() для данного типа контейнера. Не суть важно, будет ли это const_iterator, iterator или что-то еще. Важно, что компилятор определит это сам, и будет считать, что значение именно этого типа будет возвращаться самой функцией try_find_handler().

Пункт три, хоть и показывает еще раз возможности C++11, на самом деле нужен для того, чтобы обойти ограничение, которое еще есть в C++11, но которого не будет в C++14. Дело в том, что в std::find_if передается лямбда-функция. А в ее декларации нужно указать, какой тип будет у ее аргумента. В C++11 это нужно сделать явно, а вот в C++14 можно будет указать вместо типа auto и тип аргумента выведет компилятор.

В данном случае, поскольку я имею дело с собственным контейнером, можно было бы тупо указать тип его элемента, т.к. я все равно его знаю. Но хотелось задействовать C++11 на полную катушку и пойти максимально далеко :) Да, такой overuse для полезной feature, но получилось интересно :)

Так вот, в пункте (3) за счет decltype определяется, какой тип будет, если разыменовать возвращенный std::begin() итератор. Т.е. тип значения, на которое ссылается итератор. Этому типу через typedef дается псевдоним value_t, а затем value_t используется в декларации лямбда-функции.

Еще один маленький, а может и не маленький, бонус мы получаем в связи с использованием std::begin/std::end вместо std::vector::begin/end. Это позволяет нам использовать в качестве контейнера обычные C-шные массивы, у которых нет методов begin/end. Но зато с ними прекрасно работают свободные функции std::begin/end.

Полный текст автономного примера, демонстрирующего try_find_handler для разных типов контейнеров, приведен под катом.

Еще раз отмечу, что C++11, на мой взгляд, настолько далеко продвинул C++ вперед, что сейчас он воспринимается, если не как совсем новый язык, то уж как очень сильно усовершенствованный язык точно. Конечно, утечки памяти, обращения по невалидным указателям и прочие прелести native-разработки никуда не делись, хотя за счет усовершенствования стандартной библиотеки количество граблей несколько уменьшилось. Но общее удовольствие от работы на C++ сейчас гораздо выше. В чем-то даже похоже на удовольствие, которое я когда-то получал от программирования на Ruby. Только при этом язык позволяет писать очень большие и намного более эффективно работающие программы :)

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

Раньше C++ные возможности пытались эксплуатировать для того, чтобы получить что-то, чего в языке не было. Навороченные шаблоны имени Александреску, Вандервуда или Джосаттиса тому пример (особенно мне нравится в качестве примера приводить старую Boost.Lambda). Поэтому и прикладной код получался не слишком понятным, а уж во вспомогательные библиотеки и вовсе страшно было заглядывать.

Теперь же C++ настолько продвинулся, что сложность его восприятия возникает из-за его выскоуровневости (которая, при этом, в умелых руках не противоречит эффективности). Так, рассматривая некоторые примеры кода на современном C++ возникает чувство, что читаешь какой-то Haskell-ный код, который чуть-чуть больше разбавлен анотациями и в котором используются нормальные, а не однобуквенные с апострофами, идентификаторы :) Вот, например, коротенький пример из отличной статьи Бьёрна Страуструпа "Software Development for Infrastructure":

template <typename Iter, typename Predicate>
pair<Iter, Iter>
gather(Iter first, Iter last, Iter p, Predicate pred)
   // move e for which pred(e) to the insertion point p
   {
      return make_pair(
            // from before insertion point:
            stable_partition(first, p, !bind(pred, _1)),
            // from after insertion point:
            stable_partition(p, last, bind(pred, _1))
         );
   }

Статья, кстати говоря, классная. Рекомендую, даже не смотря на то, что она объемная и на английском. Интересные вопросы Страуструп там затрагивает, многие из которых касаются не только C++. И, если Страуструп прав, а мне кажется, что во многом он прав, то будущее С++ представляется уже не таким мрачным, как 10 лет назад. Особенно, если C++14 и C++17 не станут такими же долгостроями, как C++0x.

Ну а под катом полный текст автономного примера, с которым можно поиграться.

PS. Еще пара заметок на тему современного C++ и функционального стиля: #1, #2.

воскресенье, 8 июня 2014 г.

[prog.c++] Синхронность в SObjectizer: всего-то спустя 12 лет... :)

То, о чем мне так долго говорили большевики все вокруг, нашло таки свое воплощение в жизни :) В SObjectizer появилось синхронное взаимодействие между агентами. Реализовано оно на основе C++11 классов std::promise и std::future. Что дает возможность делать не просто синхронное взаимодействие, но и более хитрые варианты.

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

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

Ключевые фрагменты агента, который предоставляет синхронный сервис:

virtual void
so_define_agent()
   {
      so_subscribe( m_self_mbox )
            .service( &a_convert_service_t::svc_convert );
   }

std::string
svc_convert( const so_5::rt::event_data_t< msg_convert > & evt )
   {
      std::cout << "svc_convert called: value=" << evt->m_value
            << std::endl;

      std::ostringstream s;
      s << evt->m_value;

      return s.str();
   }

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

virtual void
so_evt_start()
   {
      auto hello =
         //NOTE: it could be a method of agent_t.
         so_5::rt::service< std::string >( m_svc_mbox )
               .request< msg_hello_svc >();

      auto convert =
         so_5::rt::service< std::string >( m_svc_mbox )
               .request( new msg_convert( 42 ) );

      std::cout << "hello_svc: " << hello.get() << std::endl;
      std::cout << "convert_svc: " << convert.get() << std::endl;

      std::cout << "sync_convert_svc: "
            << so_5::rt::service< std::string >( m_svc_mbox )
                  .sync_request( new msg_convert( 1020 ) )
            << std::endl;

      // More complex case with conversion.
      auto svc_proxy = so_5::rt::service< std::string >( m_svc_mbox );

      // These requests should be processed before next 'sync_request'...
      auto c1 = svc_proxy.request( new msg_convert( 1 ) );
      auto c2 = svc_proxy.request( new msg_convert( 2 ) );

      // Two previous request should be processed before that call.
      std::cout << "sync_convert_svc: "
            << svc_proxy.sync_request( new msg_convert( 3 ) )
            << std::endl;

      // But their value will be accessed only now.
      std::cout << "convert_svc: c2=" << c2.get() << std::endl;
      std::cout << "convert_svc: c1=" << c1.get() << std::endl;

      so_environment().stop();
   }

Пока выглядит многословно. Но, думаю, если тщательно разобраться с С++11 variadic templates и std::forward, объем писанины можно будет посократить.

[prog.thoughts] О том, насколько же быстро в нашем ремесле все меняется...

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

Например, готовя предыдущую заметку, я с удивлением обнаружил, что написанной мной когда-то реализации хамелеонов на C++ и ACE уже семь, повторю, СЕМЬ(!) лет. Срок очень не маленький. Тем не менее, написанный тогда код без каких-либо изменений был успешно скомпилирован на совсем-совсем другой версии библиотеки ACE и совсем-совсем, я бы даже сказал, совершенно совсем-совсем другим C++ компилятором.

Для нашего ремесла семь лет -- это очень немаленький срок. Вот, например, через пару недель исполнится семь лет "эре iPhone-ов" (первый iPhone был представлен публике 29 июня 2007 года, Wikipedia). Т.е. очень мощная, очень большая и активно развивающаяся ниша смартфонов и приложений для них, уже насчитывает семь лет своей истории. И это если не брать в расчет еще десяток лет до того, когда была эпоха Palm-ов и WinCE-устройств.

Но семь лет -- это какая-то не круглая цифра. Лучше взять, например, 10 лет. Внезапно оказывается, что у языка программирования Scala в этом году первый юбилей -- его публичный релиз состоялся в 2004-м, Wikipedia. Как-то внезапно выяснилось, что язык, который многим (в том числе и мне) казался 100% заменой и убийцей Java, уже отнюдь не молод :) Не удалось ему убить Java в свои лучшие годы, явно не получится и в дальнейшем :)))

Ну да то чужие увлечения, которые не так сильно затронули меня самого. Забавнее другое -- если все будет нормально, то через пару-тройку месяцев я буду писать для блога заметку под названием "ViM, Ruby и Mxx_ru - десять лет в пути". И это будет продолжением серии (#1, #2), начатой пять лет назад.

Десять, мать его..., лет! Не удивлюсь, если выясниться, что ViM-ом я пользуюсь дольше, чем многие из моих читателей программируют вообще. И что, за это время что-то настолько сильно изменилось, что ViM стал неактуальным?

Да чего там мелочится! :) Как выясняется, за 23 года существования ViM-a (1991-й год, Wikipedia) индустрия не смогла уйти так далеко вперед, чтобы ViM перестал быть востребованным. А если взять GNU Emacs, который еще постарше будет (это где-то 1985-й, почти тридцать лет назад)... Ведь для многих актуальных сейчас языков программирования ничего лучше-то и нет.

Кстати, об этих самых языках программирования. Вот в последние лет десять активно и много говорят о Haskell-е. А ведь Haskell появился в 1990-м (Wikipedia). Почти двадцать пять лет назад.

Или возьмем Erlang, тоже довольно популярный на околопрограммерских форумах язык (причем у меня складывается впечатление, что на практике он используется даже больше, чем Haskell). Его разработка в лабораториях Ericsson-а началась в начале 1980-х, а датой публичного релиза можно считать 1991-й год (www.erlang.org).

Ну да что там всякая маргинальная экзотика! :) Обратимся к мейнстримам из мейнстримов. Языку Java в будущем году будет 20 лет (публичный релиз в 1995-м). Столько же будет и JavaScript-у, который впервые явился миру в 1995-м под именем LiveScript, но в его название было запихнуто слово Java в качестве, к сожалению, удачного маркетингового хода. Питону через пару лет стукнет 25 (публичный релиз в 1991-м). Даже Perl, которому, по-хорошему, не нужно было давать рождаться, и которого, к большому сожалению, не может убить даже долгострой под названием Perl 6, используется уже больше 26 лет :))) /любителей Perl-а прошу обратить внимание на количество смайликов/

Ну а старому, доброму C++, который кормил меня на протяжении практически всей моей профессиональной карьеры, через два года стукнет тридцать(!). И это при том, что последние версии "древнего" C++ выглядят посовременнее Java :)

В общем, в нашем ремесле действительно мода меняется очень быстро. И вне зависимости от моды нужно постоянно учится. Но многие вещи, которые мы используем в повседневной работе, насчитывают уже не один десяток лет. И, что немаловажно, события развиваются так, что мы будем пользоваться ими еще не один десяток :) Правда, лет через 10-15-20 подобные заметки в своих блогах будут писать уже более совершенные существа :)