суббота, 12 декабря 2020 г.

Intro

О нас
Привет! Мы увлеченные любители профессиональной разработки приложений, сгибанию agile-методологий и рефакторинга во имя добра. Мы работаем со стеком технологий Microsoft, имеем в общей сложности 20 лет опыта проектирования архитектуры, рефакторинга крупных (~ 1.000.000 строк) приложений, любим изучать новые фреймворки и инструменты, критически смотрим на ООП и внимательно следим за тенденциями в мире функционального программирования.
Мы всегда открыты для сотрудничества по важным и интересным проектам. Если у вас есть идея (а лучше - ТЗ), мы с большим удовольствием возьмёмся за ее реализацию. Если у вас уже есть проект с наработками, но есть какие-то сомнения относительно его оптимальности, мы с не меньшим удовольствием займёмся изучением и профилированием кода, изучением и оптимизацией бизнес-требований, после чего предоставим все необходимые данные для повышения качества вашего продукта. И сами его повысим, если вы этого захотите!
Внутри команды мы используем свежие инструементы для баг-трекинга, continuous integration, любим юнит-тесты, ведем итерационную разработку. У нас есть wpf-верстальщики, программисты WPF и asp.net MVC, специалист по автоматизации тестирования.
Для быстрой разработки приложений мы объединили свои наработки в фреймворк Rikrop.Core, который включает в себя проекты для работы с IoC unity, паттерном mvvm, собственный набор wpf-элементов, упрощенный механизм работы с wcf и сессиями пользователей, а так же огромный набор вспомогательных инструментов.
Чтобы этот текст не был таким скучным, вот вам наш фирменный кот.

воскресенье, 6 декабря 2015 г.

РИКРОП на третьем Ростовском .net meetup

NFX - это не просто фреймворк для разработке вашего очередного приложения. Это реализация стека разработки на языке C#, не связанная с .net framework (кроме BCL).
В NFX собраны все базовые компоненты, необходимые для создания и развертывания приложения и хранилища данных. Веб-сервер позволяющий хостить веб-приложения внутри NFX-контейнера, генераторы модели данных, драйвера к БД, application level firewall, модель безопасности.
Вместо подключения десятков nuget-пакетов можно использовать NFX. Это unified stack of software, предлагающий решения для бизнес-задач, встречающихся в приложении. Здесь есть встроенная поддержка очевидных сценариев работы приложений так, как это происходит в ОС. Работая с windows, нам не нужно скачивать nuget-пакет для работы с файловой системой, nuget-пакет для запуска исполняемых файлов, nuget-пакет для сетевых подключений, nuget-пакет для работы с мьютексами. Но именно так мы разарабатываем бинес-приложения. Выстраивая хрупкий код взаимодействия разнопарадигменных компонент, пытаясь заставить их работать как единое целое. NFX – это платформа, целостная и самодостаточная. Подробнее

понедельник, 23 ноября 2015 г.

РИКРОП на хакатоне 47hours #6

С 29 по 22 ноября в Ростове в помещении южного IT-парка проходил шестой хакатон #47hours.
Мы из всех сил старались не сесть за клавиатуру. Мы держались почти до самого конца.
Но слишком заманчив был соблазн попробовать что-то новенькое. Нет, нет, не китайскую лапшу, хотя и её тоже.
Ruby on Rails + Slim + MDL + PostgreSQL + Соба с говядиной и чесноком.

понедельник, 5 октября 2015 г.

Авторегистрируемые в Unity репозитории на .net для EF Code first

Привет. Приступим.


 Мотивация

  1. Есть проект с Entity framework (>= 5.0.0.0) code first.
  2. Вы любите IoC, но не любите бесконечные регистрации новых сущностей.
  3. В качестве контейнера используется Unity (или есть возможность потратить 10 минут на допиливание исходников под свой контейнер).
  4. Перспектива написания однотипного кода почему-то отпугивает вас.
Итак, что предлагает эта статья. Вы подключаете 2 nuget-пакета, реализуете для своих Entity простой интерфейс IRetrievableEntity<TId> (можно упростить задачу, отнаследовавшись от готового класса Entity<TId>), добавляете в код 2 строки регистрации и получаете на выходе полную независимость от DBContext и возможность резолвить репозитории для каждой IRetrievableEntity-сущности с возможностью построения объектно-ориентированных (типизированных) запросов к этим репозиториям. Только посмотрите:
var employeeRepository = container.Resolve<IRepository<Emloyee, int>>();
var employees = employeeRepository.Get(q =>
{
    q = q.Filter(e => e.EmploymentDate >= new DateTime(2014, 9, 1));
    if(excludeFired)
        q = q.Filter(e => !e.Fired);
    q = q.Include(e => e.Department, p => p.Department.Chief)
            .OrderBy(p => p.FirstName);
});


Как быстро начать использовать

Можно использовать репозитории без IoC, получив бонусы построения запросов и изоляции от контекста, но следующий пример и исходники дадут исчерпывающую информацию о наиболее продуктивном и простом применении.
1. Установить пакеты Rikrop.Core.Data и Rikrop.Core.Data.Unity. Первый - в проект с Entity-сущностями, второй - в проект с контекстом БД. Я для примера использовал один проект, получилось следующее:
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
  <package id="Rikrop.Core.Data" version="1.0.1.0" targetFramework="net45" />
  <package id="Rikrop.Core.Data.Unity" version="1.0.1.0" targetFramework="net45" />
  <package id="Unity" version="3.5.1404.0" targetFramework="net45" />
</packages>
2. Добавить к регистрациям в IoC примерно следующее:
container.RegisterRepositoryContext<MyDbContext>();
//container.RegisterRepositoryContext(s => new MyDbContext(s), "myConStr");
container.RegisterRepositories(typeof(Department).Assembly);
RepositoryContext это обёртка над классом DBContext, соответственно, регистрация принимает generic-параметр наследника от DBContext. Можно регистрировать контекст с именем строки подключения.
Метод-расширение RegisterRepositories принимает на вход Assembly, в которой расположены POCO-объекты, реализующие IRetrievableEntity<TId>.
3. Реализовать для своих POCO IRetrievableEntity. Например:
public class Department : Entity<Int32>, IRetrievableEntity<Department, Int32> {...}
public class Employee : DeactivatableEntity<Int32>, IRetrievableEntity<Employee, Int32> {...}
4. Готово. Можно пользоваться:
var departmentRepository = container.Resolve<IRepository<Department, int>>();
departmentRepository.Save(new Department { Name = "TestDepartment" });
var testDeps = departmentRepository.Get(q => q.Filter(dep => dep.Name.Contains("Test")));
Ошибиться невозможно, поскольку generic-параметры следят за тем, чтобы резолвились правильные репозитории:
// Разрешить IDeactivatableRepository для департамента нельзя (ошибка компиляции), 
// т.к. эта сущность не относледована от DeactivatableEntity.
//var departmentRepository2 = container.Resolvelt;IDeactivatableRepository<Department, int>>();
5. Если стандартной фунциональности, предлагаемой интерфейсами IRepository<TEntity, in TId> и IDeactivatableRepository<TEntity, in TId> для какой-либо сущности окажется недостаточно, всегда можно расширить существующую реализацию в пару простых шагов. Задаем интерфейс:
public interface IPersonRepository : IDeactivatableRepository<Person, int>
{
    void ExtensionMethod();
}
Добавляем реализацию и обязательно помечем атрибутом:
[Repository(typeof(IPersonRepository))]
public class PersonRepository : DeactivatableRepository<Person, int>, IPersonRepository
{
    public PersonRepository(IRepositoryContext repositoryContext) 
        : base(repositoryContext)
    {
    }

    public void ExtensionMethod()
    {
        // Здесь у вас будет доступ к DBContext
        Console.WriteLine("PersonRepository ExtensionMethod called");
    }
}
Просим Unity найти и зарегистрировать все расширенные репозитории в заданной сборке:
// Пример регистрации "расширенных" репозиториев без указания их типа.
container.RegisterCustomRepositories(typeof(Department).Assembly);
Пользуемся:
// Извлечение "расширенного" репозитория по интерфейсу.
var personRepository = container.Resolve<IPersonRepository>();
personRepository.ExtensionMethod();
При этом без необходимости в расширенных методах всегда можно воспользоваться стандартной реализацией:
// Для класса Person репозиторий зарегистрирован под обоими интерфейсами, поскольку сущность наследуется от DeactivatableEntity.
var personRepository2 = container.Resolve<IRepository<Person, int>>();
var personRepository3 = container.Resolve<IDeactivatableRepository<Person, int>>();


Как это работает

Есть базовая реализация репозитория, которая работает с контекстом через абстракцию IRepositoryContext. Обращение к набору данных из репозитория работает благодаря generic-методам DBContext:
public override DbSet<TEntity> Data { get { return Context.Set<TEntity>(); } }
Ключевым классом для работы с построением запросов к репозиторию служит класс RepositoryQuery. Класс реализует fluent interface и позволяет делать Include по Expression или по текстовому пути (последнее может быть актуально при загрузке свойств дочерних коллекций, когда путь невозможно указать через expression), фильтровать, сортировать, Skip и Take.
Магия регистрации основана на Reflection. При регистрации репозиториев в сборке находятся все классы, отнаследованные от IRetrievableEntity<,>, из них достаются generic-аргументы, строятся новые типы IRepository<,> и Repository<,> с нужными generic-аргументами, дальше всё это регистрируется по свежесозданным через рефлексию типам. Для расширенных репозиториев поиск происходит по атрибуту:
foreach (var repositoryType in assembly.GetTypes().Where(type => type.IsClass))
{
    var repositoryAttribute = repositoryType.GetCustomAttribute<RepositoryAttribute>();
    if (repositoryAttribute != null)
    {
          container.RegisterType(repositoryAttribute.RepositoryInterfaceType, 
                        repositoryType, new TransientLifetimeManager());
     }
}


Проблемы

  1. Только Entity framework и только Unity. Инструмент создавался для наших личных целей и потому довольно трудно найти мотивацию к реализации, например, регистраций для других контейнеров.
  2. По этой же причине классы разделены по сборкам неоптимально. Мы надеемся, что скоро найдём мотивацию для рефакторинга этой библиотеки в виде нового интересного проекта в котором будет возможно переиспользовать идеи из Rikrop.Core.Data.
  3. Сценарий подходит для использования с единственным DBContext - разные не сможет зарезолвить репозиторий. Это ограничение не распространяется на использование Rikrop.Core.Data без Rikrop.Core.Data.Unity.
  4. Только .net 4.0 и 4.5.

P.S. Rikrop.Core.Data мы не считаем на данный момент частью фреймворка Rikrop.Core. Однако, это инструмент, который выручал нас в разработке настольных приложений и веб-проектов. Сейчас существуют очень интересные решения проблемы написания boilerplate-кода для доступа к реляционным данным, но при прототипировании может быть важно воспользоваться самым быстрым в подключении инструментом.

среда, 8 октября 2014 г.

StarBan. Гибкая методология разработки по с элементами геймификации и еще много модных слов.

Преамбула

IT мир изобрёл немало подходов к построению процесса разработки ПО, заимствуя лучшие идеи у своих допостиндустриальных коллег. На определенном этапе своего профессионального развития мы столкнулись с проблемами мотивации, управления качеством и визуализации работ для команды и заказчика. Взяв все лучшее из всего лучшего, что взяли до нас, мы организовали работу над проектами среднего размера в виде старбана (STARban).

Историческая справка 

Долгое путешествие по вселенной коммерческой разработки программного обеспечения столкнуло нас с множеством подходов к ведению проекта. Мы видели и водопады, работали по RUP, пробовали применять базовые концепции MSF, но самые теплые воспоминания оставили гибкие методологии. В рамках одного проекта, который продолжался 2 года, мы перешли с scrum на Kanban, делили и объединяли команды, сгибали под свои нужды Голдратта и тойоту.
Из скрама мы взяли способы коммуникации команды и оценку задач в безразмерных (условных) единицах. Итеративность процесса так же важна, но может регулироваться. Так же, в скраме отлично детализирована визуализация текущей работы (тактическое планирование). Канбан хорош применением теории ограничений, гибкостью их настройки под изменения внутри команды и визуализацией текущей работы на стратегической карте. Сложно полностью разделить две этих методологии, поскольку в каноническом воплощении они встречаются редко. У многих программистов, например, есть сложности с оценкой задач в story point, поэтому зачастую создаются таблицы перевода человекочасов в стори поинты или же оценка сразу проставляется в часах. Вместо этого мы решили оценивать задачи по их текущей ценности для проекта и назвали эту величину звездами. Так появилось название STARban.

Когда нужен STARban

  1. Есть проект с не всегда готовым или понятным ТЗ, которое нужно представить в осязаемом и оцениваемом виде. 
  2. Размер проекта достаточен для того, чтобы команда не успевала следить за изменениями в проекте, вносимыми другими участниками и текущим статусом проекта. 
  3. Проблемы коллективной ответственности (Почему это должен делать я? Закомичу так, может тестировщик не найдет? Опять кто-то билд свалил, пойду пока пообедаю. Не баг это.) 
  4. Снижение мотивации с течением времени из-за перехода проекта в статус поддержки или из-за overqualified. (слишком хорош чтобы работать, слишком уродлив чтобы заниматься проституцией).
OK, у нас есть проект, в котором участвуют 5-15 человек, с необходимостью подтянуть стратегическое планирование и несколькими уставшими программистами, которые воспринимают любую задачу как рутину. Остальные воспринимают любую задачу как бремя и ставят своей целью формальное закрытие задач с минимизацией на то усилий. Нужное, как говорится, подчеркнуть. 
Прежде всего нужно отметить, что starban это не законченная методология разработки ПО, а набор расширений, позволяющий повысить продуктивность и не дать проекту погрузиться в кризис. Уставший разработчик может уйти с проекта и унести с собой уникальные не задокументированные знания. Менеджер может слиться на оффер получше и тогда понять текущий статус проекта может быть довольно проблематично. Отдел тестирования закроет задачу и построит приемочное тестирование таким образом, будто все пользователи продукта полностью прочитали инструкцию и поклялись могилой своего кота никогда ее не нарушать. Отдел разработки ругается с отделом с отделом тестирования, перекладывая ответственность на аналитиков и собирая масштабные митинги, на которых можно 2 часа слушать, 1 час составлять meeting minutes и по итогам назначить следующее собрание уже со стейкхолдером чтобы пересказать ему всё еще раз.
Кнут и пряник редко бывают сбалансированы, поскольку к каждому члену команды требуется индивидуальное сочетание этих компонент. Итак, рассмотрим по порядку взаимодополняющие правила, соблюдение которых принесло нам душевный покой, а проекту неизгладимую пользу.

Принцы разработки STARban 

Деление на команды 

Даже если в команде всего 4-5 разработчиков, есть резон разделить их на 2 команды, поскольку больше 3 человек вряд ли смогут параллельно заниматься разработкой одной фичи (пользовательской истории). Чаще всего фичей заняты 1-2 человека, третий может в это время заниматься подготовкой к разработке следующего функционала. 
Если усреднять, то наш опыт, почему-то, всегда показывал, что идеальные команды всегда состояли из трех человек. Возможно, это субъективное значение, но наши коллеги тоже часто приходили к такому же выводу. Быть универсальной автономной единицей сложно и плохо вследствие отсутствия внешнего контроля принимаемых решений, а коллектив большего состава может блокировать сам себя. К тому же, каждый член команды должен знать, чем заняты другие. 

Использование доски для визуализации прогресса проекта и команд 

Доска должна выполнять следующие функции: 
  1. Показывать наиболее полный RoadMap проекта. Команда будет видеть, к чему движется продукт и использовать это при разработке для применения наиболее оптимальных со стратегической точки зрения решений. Владелец продукта может на этой же схеме корректировать приоритеты и при этом понимать, что дополнительные срочные задачи повлияют на deadline других. 
  2. Иллюстрировать стек ближайших задач к разработке. Это дает стимул к завершению текущих задач. Задачи к разработке должны быть оценены в SP. 
  3. Отображать статус текущего исполнения задач командами. Это позволит членам команд скоординировать свои действия, информировать других членов команды о проблеме, увидеть моментальный статус. Менеджер на основе динамики этих данных может увидеть проблему и вмешаться в ее решение. 
  4. Готовые задачи могут разделяться на версии при выпуске очередного релиза, что позволит ориентироваться в функциональности того или иного выпуска продукта. 
Доска является важной частью в оптимизации процесса и должна быть максимально информативной. 
Самой левой колонкой является Roadmap проекта, отсортированный по приоритету задач. Здесь же должны содержаться глобальные технические задачи обслуживания и модернизации инфраструктуры разработки. Здесь же может содержаться срочный запрос на исправление функционала, но он должен соблюдать те же правила, что и остальные карточки. Для себя мы приняли два правила – карточки в этой колонке разделяются по цветам и для каждой карточки представлен ее числовой приоритет, который поможет при сортировке в системе ведения проекта. Эта колонка должна быть по возможности максимально заполнена чтобы все участники могли видеть ожидаемый результат разработки и учитывать будущие потребности при выполнении текущих задач. Менять приоритет выполнения задачи может единолично стейкхолдер (заказчик, его представитель, владелец разрабатываемого ПО). При изменении приоритета он сразу может оценить потери – выпуск каких фич сдвинется вследствие его изменений. 
Следующая колонка – product backlog – содержит формализованные в виде пользовательских историй задачи по изменению функционала, баги и технические задачи. Детализация здесь может быть разной, как и типы задач. Например, слияние веток в системе контроля версий может относиться к конкретной user story, но может быть и отдельной оцениваемой карточкой в зависимости от контекста. Приоритет в этой колонке перестает играть роль, а порядок извлечения задач должен настраиваться менеджером при помощи «звездной оценки». О звездах поговорим позже, пока только стоит знать, что командам выгоднее брать к себе в разработку более звездные карточки. На эту колонку можно ввести ограничение по количеству одновременно находящихся в ней карточек чтобы избежать спекуляций. Мы пробовали правило кошачьего лотка, когда число карточек должно на 1 превышать число мини-команд, занимающихся разработкой (количество лотков, если вы содержите дома несколько кошек, должно так же превышать их количество на единицу). 
При недостатке задач в этой колонке должно проводиться предпланироаание. В предпланровании участвуют представители каждой команды, менеджер и, при необходимости, представитель заказчика. Разработчики делятся оценками сложности выполнения задачи, заказчик или менеджер оценивают ее актуальность для проекта. При этом конечную оценку в звездах ставит менеджер на основе текущих приоритетов и ситуации на проекте. Задачи так же могут быть добавлены и оценены менеджером единолично, но так стоит делать только с критичными багами. Не стоит так же забывать о цветовом кодировании в этой колонке – это позволит при беглом взгляде на доску понять, ждать ли в ближайшее время выхода новых фич, стабилизации продукта или же команда возится с инфраструктурой разработки. OK, будем считать, что декомпозировать и оценивать задачи все умеют, а цвета выбираются подходящие для дальтоников.
Следующие четыре колонки разделены по горизонтали. Каждая команда владеет своим кусочком доски и ведет учёт на нём. Когда у команды возникает потребность взять в разработку очередную карточку из product backlog, она проводит планирование - декомпозирует задачу на таски, выясняет подробности реализации, оценивает сроки реализации. Задача берется "в работу" - и сюда же помещаются все карточки. Декомпозиция должна быть достаточно подробной для того, чтобы соблюдалось правило: один человек на одну задачу. Еще хорошо идёт иерархическая нумерация <номер фичи>.<номер таски> или цветовое деление по фичам.
Как удержать команду от захвата лишних задач, на которые нет свободных рук? Нужно ли это делать? Вводить ли ограничение на число карточек? Скорее это нужно регулировать звездно-рыночным методом, о котором еще будет рассказано, но при возникновении проблем можно без проблем вводить Голдраттовские ограничения. Для заданий из колонки "в работе" можно использовать значки - это дополнительные приметные издалека пометки для карточек. Стикеры с жирными символами - подходят. Примеры: "!" - задача находится в работе дольше, чем по изначальной оценке; "?" - по задаче есть вопрос, который требует проведения собрания с другими командами. Следует актуализировать эту часть доски как можно чаще и следить за бейджами на задачах.
Когда активности по задаче завершены, она перемещается в колонку "проверка". Команда тестирования видит, что все одноцветные фичевые задачи перешли на проверку и начинает тестирование. Баги на доске можно отображать стикерами к карточкам или просто добавить значок.
При завершении тестирования и отсутствии багов задачи переносятся в колонку "готово". Здесь их можно обратно сгруппировать в фичу, обычно просто склеивая карточки лесенкой. Некоторые значки, например, о просроченном сроке, могут здесь еще пригодиться. Периодически в рамках процедуры приемки команда устраивает демонстрацию задач, дошедших до этой колонки, для владельца продукта или ответственного аналитика. Принятые задачи в детализации колонки "product backlog" перемещаются в колонку "принято".
Колонка "принято". Если в системе много багов (а мы говорим о долгоиграющих проектах, поэтому багов в трекере будет прилично) и оценивать каждый нет смысла, в этой колонке записан текущий курс оценки багов в звездах, общий для всех команд. Например, четверть звезды за один баг или звезда за каждый баг. Желательно не менять это число в рамках одного релиза, поэтому постарайтесь заранее выбрать баланс между новым функционалом и стабилизацией продукта. Стоит упомянуть, что в этой колонке упоминаются только баги, которые пришли из беклога, но не те, которые были созданы в процессе тестирования реализуемых командой фич. Но везде есть свои исключения.
При релизе очередного выпуска продукта, все карточки из "принято" переносятся в общую для всех команд колонку "принято" и помечаются номером очередной версии. Проще всего визуализировать это сортировкой по номеру версии, цветовым кодированием по номеру версии или группировкой внутри колонки.

Экспоненциальная временная шкала доски 

Это так же можно переформулировать как "одна доска для всего". Бывает так, что доску с Roadmap продукта разделяют со скрам-доской, а уж распределение фич по релизам и вовсе не визуализируют. Это имеет свои преимущества, например, дорожную карту можно проиллюстрировать в виде настоящей карты, показав зависимость элементов вместо не всегда очевидных числовых приоритетов или плоского списка. Окей, никто не ограничивает вас в пространстве (если быть более точным - никто не ограничивает вас в пространстве-времени)!
Магнитно-маркерной доски с площадью 2-3 квадратных метра вполне хватит чтобы сделать двухмерную модель крупного проекта со всеми необходимыми колонками и еще останется место для художественного оформления. Если использовать электронную версию с масштабированием, то ничего не мешает детализировать задачи до любого необходимого предела. Доска с точки зрения временной шкалы будет выглядеть как "месяцы -> недели -> дни -> месяцы".

Оценка задач в звездах 

В чем оценивать задачи - в часах или в story point? Много существует попыток определения стори поинта и методики оценки в безразмерных величинах (слонах, попугаях). Большинство приводят к понятию сложности и в итоге к внутреннему (а иногда и официально утвержденному записанному) построению таблицы соответствия SP -> часы. Погуляв между проектами, можно встретить вначале оценку из расчета 1 SP == 8 часов команды, а потом, в соседнем кабинете, увидеть 20 SP == 8 часов работы 1 человека. Иногда зависимость нелинейная. И никогда нет понимания с первого раза, что же такое этот story point. Все, как говорят книги, в сравнении. Было бы с чем сравнивать.
"Star point" или просто "звезда" - безразмерная величина оценки задач в проекте на основе их текущей значимости для проекта. Можно провести аналогию с деньгами. Есть команда разработчиков, которые непрерывно работают по договорной ставке в звездах. Менеджер предлагает новую задачу - необходимо добавить в продукт версионирование и сделать присвоение версий полуавтоматическим при сборке на билд-сервере. Предлагает цену в 10 звезд. Программисты оценивают задачу и понимают, что проще поправить 10 багов, которые сейчас оцениваются в 1 звезду за каждый. Поскольку указание версии срочно требует служба поддержки для нужд фиксации ошибок, этот функционал является более приоритетным, чем стабилизация продукта, поэтому менеджер назначает цену в 20 звезд и первый же освободившийся разработчик торопливо забирает эту задачу для себя или своей команды, чтобы заработать сразу 20 звезд.
Через какое-то время менеджер проекта понимает, что разработчики набросились на крупные задачи, а в службу поддержки стало поступать много жалоб на мелкие ошибки при работе с приложением. Это могут быть опечатки, отсутствие сортировки в списочных формах и множество подобных незначительных, но неприятных недочетов. Так как торговаться за отдельные ошибки слишком накладно, руководитель принимает решение и объявляет команде - со вторника до конца недели любой закрытый баг оценивается в тройном коэффициенте. Прямых указаний при этом можно не давать, а команда разработчиков сама сменит вектор разработки чтобы не упустить шанса заработать лишние звезды.
Может показаться, что у программистов нет мотивации хватать важные или сложные задачи, но это не так.

Рынок 

Когда очередную карточку принимает руководитель, ее стоимость в звездах перечисляется на счет разработчика или команды, которые занимались реализацией. Прелесть звезд состоит в том, что их можно не только копить, но и тратить. Здесь есть некоторая проблема, называемая экономикой, из нее следует несколько других проблем - инфляция, рынок, спекуляции.
Это и хорошо и плохо одновременно. И, к сожалению или к счастью, самый крупный друг человека зарыт именно в этом пункте. Руководитель проекта должен запустить рыночные отношения среди сотрудников, занятых не проекте, и регулировать систему не через авторитарные указания, а невидимой рукой.
На что можно тратить звезды? Все зависит от проекта и компании, нематериальная мотивация бывает разной. Можно не брать в рассмотрение отгулы, xbox one, спортзал и прогулки на яхте за каждые полтысячи звезд - ведь это ваша компания предоставляет своим сотрудникам без всяких условий.  Внутри проекта есть много другой рутины, которая может являться товаром. Не все задачи интересные, а интересные могут быть не так актуальны и от этого в низком приоритете. Две команды могут присматриваться к одной задаче. Можно устроить торги за эту задачу и тот, кто даст больше звезд - имеет право взять ее на исполнение. У команды завал по фичевым багам, а хочется взять какую-то задачу, не влезающую в ограничения колонки? Пусть продаст баги по фиче другой команде! А уж о цене договорятся. Или пусть покупают дополнительный слот в колонке на неделю. Но если опоздают по срокам - оштрафовать в звездном эквиваленте так, чтобы ближайший месяц им доставались только самые неприятные задачи (другие команды откупятся от них). Кому-то не нравится мержить, а кому-то писать тесты. За отсутствие тестов на новый функционал положен штраф, но можно за половину стоимости штрафа найти кого-нибудь, кто эти тесты напишет.
Задача руководителя здесь удерживать баланс звезд, не обесценивая их и прозрачно указывая приоритеты, манипулируя рынком. Нужно задать определенные правила, такие, как курс обмена багов на звезды, но в то же время проявлять постоянную гибкость и поддерживать инициативы команды. Если кто-то решит продать свою очередь мыть кофемашину - почему бы и нет! Комиссия за операции со звездами? Можно. Но желательно сохранять желание к получению большего количества звезд, поскольку иначе ничего работать не будет.
Если этот пункт все-таки будет вызывать сложности, то настольная игра от Мосигры и Хабрахабра "Стартап" может дать хорошую почву для размышлений и лишний раз порадовать ваших коллег, уставших от xbox one.

Технические задачи важны 

Да. Нужно просто это помнить. Обновление версий используемых библиотек, ревизии, настройка CI и CD, настройка VCS, поддержка team-wiki, модификация трекера задач. Эти задачи должны быть учтены в общем объёме работы, они должны иметь свой приоритет и стоимость в звездах. Команда приобретет навыки DevOps, а проект немного сэкономит.

Постамбула 

"А мы пробовали скрам, но нам не понравилось"
"А у нас и так все работает"
"А тимлид не хочет ничего менять"
"Менеджер сказал, чтоб заканчивали в игрушки играть и писали код"
"У нас слишком мало программистов, зачем это вообще нужно?"
"А тут не написано, что делать, если ..."
"... программисты не захотят зарабатывать звезды"
"... у нас постоянно вклинивается срочный багфикс на продакшене"
"... мы неправильно оцениваем задачи, стоит ли на это вообще тратить время?"
"... все горит, а ограничения не дают ничего исправить"
"... у нас нет доски"
"... Мигалкин хомячит все звезды и не хочет их отдавать"
Ох, да. Разработка ПО это такая область, где скепсис является необходимым качеством для выживания как специалиста, так и проекта. Что ж, STARban предназначен скорее для тех, кто успел познать горечь разочарования в каноническом Agile, но и знает о существовании специального места в аду, где вы продолжаете работать программистом, один на проекте, с менеджером, у которого еще 4 таких же проекта. И у вас, возможно, еще один. Или на одном проекте у вас одна команда, а на другом совсем другая, частично укомплектованная заказчиком. Словом, лучше бы вам вести себя благопристойно в этой жизни.
Гибкие методологии разработки были созданы чтобы решить определенные задачи – защита разработчиков от заказчика, обеспечение конвейерной обработки задач, адаптивность к изменению требований. Есть полезные практики, которые выгодно использовать всегда независимо от конкретной методологии. STARban попытался собрать их воедино чтобы сделать разработку проекта комфортной и интересной для всех участников процесса. При этом он остаётся адаптивным к разным условиям. Бывает так, что на проекте всего пара разработчиков, но и им можно организовать звездную конкуренцию, заменив командный зачет личным. Доска может подстраиваться под реалии постановки задач и возможности формализации в проекте. Никто не мешает даже не привязывать starban к конкретному проекту - если можно представить задачи в рамках единого беклога (что часто бывает в IT-отделе производственных компаний) и уместить на одну доску, то проект является только условным делителем.
А в итоге каждый получит тот процесс разработки ПО, которого заслуживает ;)

пятница, 25 июля 2014 г.

Очень краткая история одного проекта



Привет!
Этой зимой мы не без удовольствия прикоснулись к миру веб-разработки для .net - попробовали свежую версию asp.net mvc, web api 2, познакомились с довольно спорным, но от этого не менее эффектным фреймворком angular js, поигрались в одностраничность и пощупали известный хостинг для совместной разработки GitHub. Всё новое всегда интересно, поэтому мы в буквальном смысле не могли оторваться от проекта:

вторник, 26 ноября 2013 г.

StarBan v.0.1 preview

StarBan - методология разработки, которая подходит для крупных команд, делающих один или несколько немасштабных проектов в рамках одного product backlog.
Старбан позволяет менеджеру гибко управлять приоритетами в бэклоге прозрачно для программистов. Эта методология, вобравшая в себя самый приятный опыт скрама и канбана, позволяет за счёт геймификации процесса разработки ПО упростить управление проектом и дать программистам нематериальный стимул к работе.
Старбан, рожденный в недрах РИКРОПа, показал свою эффективность для команд от 6 до 10 человек, но мы надеемся опробовать его в еще более крупномасштабной разработке. Вкратце можно объяснить эту практику следующим образом:

четверг, 24 октября 2013 г.

Фреймворк: Rikrop.Core.Wpf.dll. Часть 0, INotifyPropertyChanged

К оглавлению

 Rikrop.Core.Wpf.dll

Библиотека содержит огромное количество полезных инструментов для быстрого создания отзывчивых и умных wpf-приложений.
Мы любим создавать wpf-приложения. И мы делаем это так часто, что просто не смогли обойтись без полной целостной инфраструктуры для поддержания паттерна mvvm. Мы пробовали использовать разные подходы: view-first, viewmodel-first. И очень важной целью было создание такого набора классов, который позволял бы с минимальными затратами решать типичные для этих подходов задачи.
Мы глубоко интегрировали работу с деревьями выражений для упрощения работы с INotifyPropertyChanged. Для навигации и управления представлением используется абстракция рабочих областей, которая является центральной при построении отображения в wpf.

namespace Rikrop.Core.Wpf

Для удобной, типобезопасной и прозрачной работы с классами, реализующими интерфейс INotifyPropertyChanged, мы создали базовую реализацию ChangeNotifier, интерфейс которого выглядит следующим образом:

[DataContract(IsReference = true)]
[Serializable]
public abstract class ChangeNotifier : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    protected void NotifyPropertyChanged(Expression<Func<object, object>> property)
    protected void NotifyPropertyChanged(Expression<Func<object>> property)
    protected virtual void OnPropertyChanged(string propertyName)

    protected ILinkedPropertyChanged AfterNotify(Expression<Func<object> property)
    protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property)
    protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
        where T : INotifyPropertyChanged
    protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
        where T : ChangeNotifier

    protected ILinkedObjectChanged Notify(Expression<Func<object>> property)
}

Здесь же сразу стоит указать интерфейсы ILinkedPropertyChanged и ILinkedObjectChanged:
public interface ILinkedPropertyChanged
{
    ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty);
    ILinkedPropertyChanged Execute(Action action);
}

public interface ILinkedObjectChanged
{
    ILinkedObjectChanged AfterNotify(Expression<Func<object>> sourceProperty);
    ILinkedObjectChanged AfterNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty)
            where T : INotifyPropertyChanged;

    ILinkedObjectChanged BeforeNotify(Expression<Func<object>> sourceProperty);
    ILinkedObjectChanged BeforeNotify<T>(T sourceChangeNotifier, Expression<Func<T, object>> sourceProperty)
            where T : ChangeNotifier;
}

Как и зачастую в программировании - реализация проще, чем может показаться на первый взгляд, а использование еще проще, чем реализация. Если попытаться описать назначение методов ChangeNotifier "на пальцах", то этот класс позволяет информировать об изменениях свойств с помощью деревьев выражений, а так же указывать взаимосвязи между свойствами таким же образом. Для повышения производительности мы кэшируем деревья выражений и при повторном использовании применяем уже разобранные деревья.*
Но что может быть понятнее простого практического примера?! Следующий код иллюстрирует устройство с несколькими датчиками, где каждый датчик имеет значение и известное ему отклонение от среднего значения, а устройство управляет обновлением данных:
/// <summary>
/// Датчик.
/// </summary>
public class Sensor : ChangeNotifier
{
    /// <summary>
    /// Значение измерения.
    /// </summary>
    public int Value
    {
        get { return _value; }
        set { SetProperty(ref _value, value); }
    }
    private int _value;

    /// <summary>
    /// Отклонения значения измерения от среднего.
    /// </summary>
    public double Delta
    {
        get { return _delta; }
        set { SetProperty(ref _delta, value); }
    }
    private double _delta;

    public Sensor()
    {
        IValueProvider valueProvider = new RandomValueProvider();
        Value = valueProvider.GetValue(this);
    }
}

/// <summary>
/// Прибор с датчиками, проводящими измерения.
/// </summary>
public class Device : ChangeNotifier
{
    /// <summary>
    /// Число датчиков.
    /// </summary>
    private const int SensorsCount = 3;

    /// <summary>
    /// Множество датчиков в устройстве.
    /// </summary>
    public IReadOnlyCollection<Sensor> Sensors
    {
        get { return _sensors; }
    }
    private IReadOnlyCollection<Sensor> _sensors;
        
    /// <summary>
    /// Среднее значение с датчиков.
    /// </summary>
    public double AvgValue
    {
        get { return (Sensors.Sum(s => s.Value)) / (double) Sensors.Count; }
    }

    public Device()
    {
        InitSensors();

        AfterNotify(() => AvgValue).Execute(UpdateDelta);

        NotifyPropertyChanged(() => AvgValue);
    }

    private void InitSensors()
    {
        var sensors = new List<Sensor>();
        for (int i = 0; i < SensorsCount; i++)
        {
            var sensor = new Sensor();
            BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);
            sensors.Add(sensor);
        }

        _sensors = sensors;
    }

    private void UpdateDelta()
    {
        foreach (var sensor in Sensors)
            sensor.Delta = GetDelta(sensor);
    }

    private double GetDelta(Sensor sensor)
    {
        return Math.Abs(sensor.Value - AvgValue);
    }
}

Интересующие нас строки кода:
SetProperty(ref _delta, value);
NotifyPropertyChanged(() => AvgValue);
AfterNotify(() => AvgValue).Execute(UpdateDelta);
BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);

Отдельно разберем каждую конструкцию и посмотрим на реализацию приведенных методов.

SetProperty(ref _delta, value);

Этот код присваивает полю, переданному в первом параметре метода, значение из второго параметра, а так же уведомляет подписчиков об изменении свойства, имя которого передаётся третьим параметров. Если третий параметр не задан, используется имя вызывающего свойства.
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
    if (Equals(field, value))
    {
        return;
    }

    field = value;
    NotifyPropertyChangedInternal(propertyName);
}

NotifyPropertyChanged(() => AvgValue);

Все методы нотификации об изменении объектов, принимают ли они дерево выражений или строковое значение имени свойства, в конечном итоге вызывают следующий метод:
private void NotifyPropertyChanged(PropertyChangedEventHandler handler, string propertyName)
{
    NotifyLinkedPropertyListeners(propertyName, BeforeChangeLinkedChangeNotifierProperties);

    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(propertyName));
    }
    OnPropertyChanged(propertyName);

    NotifyLinkedPropertyListeners(propertyName, AfterChangeLinkedChangeNotifierProperties);
}

private void NotifyLinkedPropertyListeners(string propertyName,
                                            Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedChangeNotifiers)
{
    LinkedPropertyChangeNotifierListeners changeNotifierListeners;
    if (linkedChangeNotifiers.TryGetValue(propertyName, out changeNotifierListeners))
    {
        changeNotifierListeners.NotifyAll();
    }
}

Каждый объект-наследник ChangeNotifier хранит коллекции связок "имя свойства" - "набор слушателей уведомлений об изменении свойств":
private Dictionary<string, LinkedPropertyChangeNotifierListeners> AfterChangeLinkedChangeNotifierProperties { get { ... } }
private Dictionary<string, LinkedPropertyChangeNotifierListeners> BeforeChangeLinkedChangeNotifierProperties { get { ... } }

Отдельно необходимо рассмотреть класс LinkedPropertyChangeNotifierListeners:
private class LinkedPropertyChangeNotifierListeners
{
    /// <summary>
    /// Коллекция пар "связанный объект" - "набор действий над объектом"
    /// </summary>
    private readonly Dictionary<ChangeNotifier, OnNotifyExecuties> _linkedObjects =
        new Dictionary<ChangeNotifier, OnNotifyExecuties>();

    /// <summary>
    /// Регистрация нового связанного объекта.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="targetPropertyName">Имя свойства связанного объекта для уведомления.</param>
    public void Register(ChangeNotifier linkedObject, string targetPropertyName)
    {
        var executies = GetOrCreateExecuties(linkedObject);

        if (!executies.ProprtiesToNotify.Contains(targetPropertyName))
        {
            executies.ProprtiesToNotify.Add(targetPropertyName);
        }
    }

    /// <summary>
    /// Регистрация нового связанного объекта.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="action">Действие для вызова.</param>
    public void Register(ChangeNotifier linkedObject, Action action)
    {
        var executies = GetOrCreateExecuties(linkedObject);

        if (!executies.ActionsToExecute.Contains(action))
        {
            executies.ActionsToExecute.Add(action);
        }
    }

    /// <summary>
    /// Получение имеющегося или создание нового набора действий над связанным объектом.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <returns>Обёртка над набором действий со связанным объектом.</returns>
    private OnNotifyExecuties GetOrCreateExecuties(ChangeNotifier linkedObject)
    {
        OnNotifyExecuties executies;
        if (!_linkedObjects.TryGetValue(linkedObject, out executies))
        {
            executies = new OnNotifyExecuties();
            _linkedObjects.Add(linkedObject, executies);
        }
        return executies;
    }

    /// <summary>
    /// Вызов уведомлений и действий для всех связанных объектоы.
    /// </summary>
    public void NotifyAll()
    {
        foreach (var linkedObject in _linkedObjects)
        {
            NotifyProperties(linkedObject.Key, linkedObject.Value.ProprtiesToNotify);
            ExecuteActions(linkedObject.Value.ActionsToExecute);
        }
    }

    /// <summary>
    /// Вызов уведомлений об изменении свойств над связанным объектом.
    /// </summary>
    /// <param name="linkedObject">Связанный объект.</param>
    /// <param name="properties">Имена свойств связанного объекта для уведомления.</param>
    private void NotifyProperties(ChangeNotifier linkedObject, IEnumerable<string> properties)
    {
        foreach (var targetProperty in properties)
        {
            linkedObject.NotifyPropertyChangedInternal(targetProperty);
        }
    }

    /// <summary>
    /// Вызов действий.
    /// </summary>
    /// <param name="actions">Действия</param>
    private void ExecuteActions(IEnumerable<Action> actions)
    {
        foreach (var action in actions)
        {
            action();
        }
    }

    private class OnNotifyExecuties
    {
        private List<string> _proprtiesToNotify;
        private List<Action> _actionsToExecute;

        public List<string> ProprtiesToNotify
        {
            get { return _proprtiesToNotify ?? (_proprtiesToNotify = new List<string>()); }
        }

        public List<Action> ActionsToExecute
        {
            get { return _actionsToExecute ?? (_actionsToExecute = new List<Action>()); }
        }
    }
}

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

AfterNotify(() => AvgValue).Execute(UpdateDelta);

BeforeNotify(sensor, s => s.Value).Notify(() => AvgValue);

Для добавления нового связанного объекта и действий над ним служат связка методов AfterNotify/BeforeNotify класса ChangeNotifier и методов Notify/Execute классов-наследников ILinkedPropertyChanged. В качестве последних выступают вложенные по отношению к ChangeNotifier классы AfterLinkedPropertyChanged и BeforeLinkedPropertyChanged.
/// <summary>
/// Связыватель для событий перед нотификаций об изменении свойства объекта.
/// </summary>
private class BeforeLinkedPropertyChanged : ILinkedPropertyChanged
{
    /// <summary>
    /// Исходный объект.
    /// </summary>
    private readonly ChangeNotifier _sourceChangeNotifier;

    /// <summary>
    /// Имя свойство исходного объекта.
    /// </summary>
    private readonly string _sourceProperty;

    /// <summary>
    /// Связываемый объект.
    /// </summary>
    private readonly ChangeNotifier _targetChangeNotifier;

    public BeforeLinkedPropertyChanged(ChangeNotifier sourceChangeNotifier,
                                        string sourceProperty,
                                        ChangeNotifier targetChangeNotifier)
    {
        _sourceChangeNotifier = sourceChangeNotifier;
        _sourceProperty = sourceProperty;
        _targetChangeNotifier = targetChangeNotifier;
    }

    /// <summary>
    /// Связывание объекта и нотификации свойства с исходным объектом.
    /// </summary>
    /// <param name="targetProperty">Свойство целевого объекта.</param>
    /// <returns>Связыватель.</returns>
    public ILinkedPropertyChanged Notify(Expression<Func<object>> targetProperty)
    {
        _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener(_sourceProperty, _targetChangeNotifier,
                                                                    (string) targetProperty.GetName());
        return this;
    }

    /// <summary>
    /// Связывание объекта и действия с исходным объектом.
    /// </summary>
    /// <param name="action">Действие.</param>
    /// <returns>Связыватель.</returns>
    public ILinkedPropertyChanged Execute(Action action)
    {
        _sourceChangeNotifier.RegisterBeforeLinkedPropertyListener(_sourceProperty, _targetChangeNotifier,
                                                                    action);
        return this;
    }
}

Для связывания используются методы RegisterBeforeLinkedPropertyListener/RegisterAfterLinkedPropertyListener класса ChangeNotifier:
public abstract class ChangeNotifier : INotifyPropertyChanged
{
    ...
    private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName,
                                                        ChangeNotifier targetObject,
                                                        string targetPropertyName)
    {
        RegisterLinkedPropertyListener(linkedPropertyName, targetObject, targetPropertyName,
                                        BeforeChangeLinkedChangeNotifierProperties);
    }

    private void RegisterBeforeLinkedPropertyListener(string linkedPropertyName,
                                                        ChangeNotifier targetObject,
                                                        Action action)
    {
        RegisterLinkedPropertyListener(linkedPropertyName, targetObject, action,
                                        BeforeChangeLinkedChangeNotifierProperties);
    }

    private static void RegisterLinkedPropertyListener(string linkedPropertyName,
                                            ChangeNotifier targetObject,
                                            string targetPropertyName,
                                            Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, targetPropertyName);
    }

    private static void RegisterLinkedPropertyListener(string linkedPropertyName,
                                             ChangeNotifier targetObject,
                                             Action action,
                                             Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        GetOrCreatePropertyListeners(linkedPropertyName, linkedProperties).Register(targetObject, action);
    }

    private static LinkedPropertyChangeNotifierListeners GetOrCreatePropertyListeners(string linkedPropertyName,
                                                    Dictionary<string, LinkedPropertyChangeNotifierListeners> linkedProperties)
    {
        LinkedPropertyChangeNotifierListeners changeNotifierListeners;
        if (!linkedProperties.TryGetValue(linkedPropertyName, out changeNotifierListeners))
        {
            changeNotifierListeners = new LinkedPropertyChangeNotifierListeners();
            linkedProperties.Add(linkedPropertyName, changeNotifierListeners);
        }
        return changeNotifierListeners;
    }
    ...
}

Методы AfterNotify/BeforeNotify создают новые экземпляры связывателей для предоставления простого интерфейса связывания:
protected ILinkedPropertyChanged AfterNotify(Expression<Func<object>> property)
{
    var propertyCall = PropertyCallHelper.GetPropertyCall(property);
    return new AfterLinkedPropertyChanged((INotifyPropertyChanged) propertyCall.TargetObject,
                                            propertyCall.TargetPropertyName,
                                            this);
}

protected ILinkedPropertyChanged BeforeNotify(Expression<Func<object>> property)
{
    var propertyCall = PropertyCallHelper.GetPropertyCall(property);
    return new BeforeLinkedPropertyChanged((ChangeNotifier) propertyCall.TargetObject,
                                            propertyCall.TargetPropertyName,
                                            this);
}

protected ILinkedPropertyChanged AfterNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
    where T : INotifyPropertyChanged
{
    return new AfterLinkedPropertyChanged(changeNotifier, property.GetName(), this);
}

protected ILinkedPropertyChanged BeforeNotify<T>(T changeNotifier, Expression<Func<T, object>> property)
    where T : ChangeNotifier
{
    return new BeforeLinkedPropertyChanged(changeNotifier, property.GetName(), this);
}

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

 Еще раз на пальцах.

Объект ChangeNotifier содержит несколько коллекций, в которых хранятся данные о связанных с нотификацией свойства объектах, нотифицируемых свойствах этих объектов, а так же о действиях, которые должны быть вызваны до или после нотификации. Для предоставления простого интерфейса связывания объектов методы AfterNotify/BeforeNotify возвращают наследников ILinkedPropertyChanged, которые позволяют легко добавлять нужную информацию в коллекции. Методы ILinkedPropertyChanged возвращают исходный объект ILinkedPropertyChanged, что позволяет использовать цепочку вызовов для регистрации.
При нотификации об изменении свойства объект обращается к коллекциям связанных объектов и вызывает все необходимые зарегистрированные заранее действия.
ChangeNotifier предоставляет удобный интерфейс для изменения свойств объектов и нотификации об изменениях свойствах, который минимизирует затраты на разбор деревьев выражений.

Еще более простое объяснение.

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



*Стоит упомянуть, что реализация извещения об изменении свойств при помощи деревьев выражений есть и в других фреймворках, например у класса NotificationObject в Microsoft prism 4, однако там разбор дерева ограничивается простейшими и базовыми случаями, что позволяет использовать нотификацию об изменении свойств только в самых типичных сценариях.

пятница, 6 сентября 2013 г.

Социальный РИКРОП

Сегодня отличный день для отличных новостей. Мы появились в LinkedIn. Следите за обновлениями, будет интересно!