четверг, 20 января 2011 г.

Хороший программист

Не раз читал статьи в стиле "n способов стать хорошим программистом", например:

10 способов стать хорошим программистом (с) habrahabr.ru

или, знаменитые 12 шагов к лучшему коду? Joel'a

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

Вы пишите код, разрабатываете системы, которые используют другие люди? Вы хорошо зарабатываете и считаете себя хорошим специалистом? Тогда мы идем к вам :) Ниже представлен список из 10 вещей , которые хорошо было бы изучить . NET разработчику , чтобы его считали профессионалом. Оригинал статьи находится здесь

1. Модульное тестирование (Unit testing)

Весь ваш код, в идеале, дослжен быть покрыт модульными тестами, каждая сборка должна проверяться на наборе тестов, перед тем, как она попадет в руки заказчиков или клиентов. Для модульного тестирования можно использовать встроенные в Visual Studio средства, NUnit, Pex, Mole и другие инструменты. также вы должны быть знакомы с понятием TDD и как можно чаще применять этот подход на практике.

(UPD: Пример подхода TDD показан тут)

2. Системы управления версиями
Хороший специалист должен уметь работать с различными системами управления версиями: Team Foundation Server, Git, SVN, Mercurial и т.д. Это позволит следить за кодом и версиями сборок.

3. IoC (Inversion of control) контейнеры
Инверсия управлений - важный принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах и входящий в пятерку важнейших принципов SOLID (вот еще одна ссылка, про описание SOLID принципов)
Сейчас активно развиваются ряд инcтрументов:

(UPD: Вообще, помоему здесь нет ничего сложного, чтобы понять принцип инверсии управления - достаточно посмотреть видео Управление зависимостями в коде)

4. Участие в open source проектах
Здесь, думаю, все понятно.

5. Continuous Integration
Continuous Integration - это практика разработки программного обеспечения, которая заключается в выполнении частых автоматизированных сборок проекта для скорейшего выявления и решения интеграционных проблем. В обычном проекте, где над разными частями системы разработчики трудятся независимо, стадия интеграции является заключительной. Она может непредсказуемо задержать окончание работ. Переход к непрерывной интеграции позволяет снизить трудоёмкость интеграции и сделать её более предсказуемой за счет наиболее раннего обнаружения и устранения ошибок и противоречий.

Можно использовать такие инструменты:
TeamCity,
Hudson,
CruiseControl.NET,
msbuild.

Можно почитать книгу "Continuous Integration in .NET" издательства Manning по этому вопросу.

6. Базы данных, работа з данными
В этом пункте нужно упомянуть NHibernate Object Relational Mapper, а также NoSQL базы данных - CouchDB, MongoDB или RavenDB.

7. Облачные технологии
Все будем в шоКоЛАУДе :) Изучите принципы облачных технологий, попробуйте в действии Windows Azure Platform.

8. Mono на Linux
Попробуйте запустить ваши приложения с помощью Mono на Linux, Mac OS X и других альтернативных платформах. Обратите внимание на MonoTouch, SharpDevelop.

9. Messaging architecture
Ознакомтесь с такими инструментами как NServiceBus, MassTransit или RhinoQueues, а затем AMQP и RabbitMQ

10 . Domain-Driven Design
Почитайте о Domain-Driven Design и начните использовать в своих приложениях.

11. Архитектура CQRS (кто-нибудь, объясните мне, что это такое ? :) )
Ознакомтесь с подходом к разработке корпоративных бизнес приложений - CQRS


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

среда, 19 января 2011 г.

Отображение иерархических данных с использованием asp.net mvc, часть 2

Как и обещал продолжение поста про отображение иерархических данных с использованием asp.net mvc

В прошлый раз я остановился на том, как выглядит представление:

<%
 var product = (Product) ViewData["Product"];
 var childs = (IEnumerable<Product>)ViewData["Childs"];
%>

<p><%: product.Name %></p>
<ul>
<% foreach(var child in childs) { %>
<li><%: child.Name %></li>
<% } %>
</ul>

* This source code was highlighted with Source Code Highlighter.

Для того, чтобы это представление можно было реально использовать - просматривать с его помощью иерархическую структуру, нужно генерировать ссылки на дочерние объекты. Для этого нужно иметь возможность получать всю цепочку иерархии объектов, вплоть до корня. В интерфейсе IProductService (и его реализации) есть функция IProductService.GetAllParents(Product) которая рекурсивно получает родителя текущего объекта, затем пытается получить родителя - родителя .. и т.д. (об устройстве IProductService'а и вообще о архитектуре я расскажу в другой раз).

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

var allParents = _productService.GetAllParents(product);

// ...

ViewData["AllParents"] = allParents;


* This source code was highlighted with Source Code Highlighter.

Таким образом у нас в представлении есть все нужные объекты, чтобы сгенерировать ссылки на дочерние объекты, посмотрим как это делается:

  <%
    var product = (Product) ViewData["Product"];
    var childs = (IEnumerable<Product>)ViewData["Childs"];
    var allParents = (((IEnumerable<Product>) ViewData["AllParents"])).Reverse();
  %>

  <h1><%: product.Name %></h1>
  <ul>
    <% foreach(var child in childs) { %>
      <li><%= Html.ActionLink(child.Name, "Index", new
{ path = Html.GenerateHierarchyLink(allParents.Concat(new[] { product }).Concat(new[] { child })) })%></li>
    <% } %>
  </ul>


* This source code was highlighted with Source Code Highlighter.

Как видно из кода, у нас используется расширение для HtmlHelper'а , оно очень простое:

  public static class GenerateHierarchyLinkHelper
  {
    public static string GenerateHierarchyLink(
this HtmlHelper htmlHelper,
IEnumerable<Product> products)
    {
      return string.Join("/", products.Skip(1).Select(x => x.Name));
    }
  }


* This source code was highlighted with Source Code Highlighter.

Здесь с помощью Linq выбираются имена всех объектов за исключением первого - первый - корневой объект это catalog, наш контроллер называется также - Catalog, поэтому ссылка на элемент каталога выглядела бы не очень хорошо:
/catalog/catalog/some/product

Что же покажет представление пользователю, который зашел по ссылке?
/catalog/some/product

при этом иерархия объектов в бд такая:
catalog
|
some
|
product
|
--- child_1
|
--- child_2
|
--- child_3

сначала пользователь увидит заголовок h1 с именем текущего продута, затем маркированный список со ссылками на дочерние продукты
- [a="/catalog/some/product/child_1"]child_1[/a]
- [a="/catalog/some/product/child_3"]child_2[/a]
- [a="/catalog/some/product/child_2"]child_3[/a]

перейдя по которым можно увидеть соответствующие вложенные объекты в иерархии, при этом строка адреса в браузере будет корректно отображать seo-url (а не чтото вроде /catalog/1/2/3)

итак , с первой проблемой - отображение ссылок на дочерние объекты иерархии - разобрались, теперь проблема номер 2 - кастомизация отображения текущей страницы (/Catalog/Index) для некоторых продуктов

Возьмем предыщую иерархию:

catalog
|
some
|
product
|
--- child_1
|
--- child_2
|
--- child_3

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

<% if(product.Name == "product123") %>
<%= "Дополнительный конент" %>
<% if(product.Name == "product456") %>
<%= "Другой дополнительный конент" %>
<% else %>
<%= "Представление по умолчанию" %>


* This source code was highlighted with Source Code Highlighter.

Имхо, не очень хорошее решение, т.к. файл представления превратится в одну большую страницу, в которой трудно будет чтото найти и исправить

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

catalog
|
some
|
product

для продукта "catalog->some" физически отображать файл:
/Views/Catalog/some.aspx
а для продукта "catalog->some->product" физически отображать файл:
/Views/Catalog/some/product.aspx
ну и т.д.
(при необходимости можно добавлять такие представления даже в райнтайме)

Как это сделать ?

В конце действия Index контроллера Catalog возвращается объект типа View()

public ActionResult Index(string path)
{
// ..
 return View();
}


* This source code was highlighted with Source Code Highlighter.

Объект View() это экземпляр текущего движка представления (по умолчанию ASP.NET WebForms), asp.net mvc ищет файл с именем Index.aspx (или .ascx) в папках:
/Views/Catalog/
или
/Views/Shared
нам же нужно, чтобы framework искал этот файл внутри папки /Views/Catalog (в том числе и во вложенных в него папках). Для того, чтобы указать, где искать, можно например создать собственный ViewResult (наследовав его от базового ViewResult) и переопределить метод:
ViewEngineResult FindView(ControllerContext context)

* This source code was highlighted with Source Code Highlighter.

чем мы щас и займемся:

  public class CatalogViewResult : ViewResult
  {
    public CatalogViewResult(ViewDataDictionary viewData)
    {
      ViewData = viewData;
    }

    protected override ViewEngineResult FindView(ControllerContext context)
    {
      var product = (Product)ViewData["Product"];
      var allParents = (((IEnumerable<Product>)ViewData["AllParents"])).Reverse();

      var
pathToView = GenerateHierarchyLinkHelper.GenerateHierarchyLink(null,
allParents.Concat(new[] { product }));
      if (!string.IsNullOrWhiteSpace(pathToView))
      {
        var result = ViewEngines.Engines.FindView(context, pathToView, null);
        if (result.View != null)
          return result;
      }

      return base.FindView(context);
    }
  }


* This source code was highlighted with Source Code Highlighter.

Как видно, код достаточно простой, в переопределенной функции FindView() с помощью, уже знакомого нам расширения для HtmlHelper'а определяется иерархический путь:

var pathToView = GenerateHierarchyLinkHelper.GenerateHierarchyLink(null,
allParents.Concat(new[] { product }));


* This source code was highlighted with Source Code Highlighter.

а затем, с помощью стандартного движка представлений ищется представление поэтому пути:
        var result = ViewEngines.Engines.FindView(context, pathToView, null);
        if (result.View != null)
          return result;


* This source code was highlighted with Source Code Highlighter.

В случае его нахождения - т.е. в случае когда файл с кастомизированным представлением существует - оно возвращается в контроллер и тот использует его, чтобы отобразить данные, в случае, если кастомное представление не найдено - возвращается стандартный вариант /Views/Catalog/Index.aspx

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

Ура, я написал свой первый пост, который, как мне кажется будет полезен кому-нибудь. Следующие сообщения также планирую посвятить ASP.NET MVC, т.к. в данный момент активно занимаюсь разработкой именно на этой платформе.

Отображение иерархических данных с использованием asp.net mvc

Это первый технический пост, ради которого я решился завести свой блог.

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

public class Product
{
  public int Id { get; set; }

  public string Name { get; set; }

  public ProductType Type { get; set; }

  public decimal Price { get; set; }

  public Product Parent { get; set; }
}

В бд это хранится в таком же виде. Задача - отображать на сайте иерархическую структуру продуктов с url'ами вида
/catalog/some/product/in/other/product/

т.е. юзер, зайдя по ссылке /catalog/some получит продукт с именем "some" самого верхнего уровня (у этого продукта нет родителя), зайдя по ссыке /catalog/some/product получит продукт с именем "product" и родителем у которого будет является "some" , ну и т.д...
Вообщем ничего сложного, в коде это реализуется примерно так:

Маршрутизация:
// маршрут /Catalog/*
routes.MapRoute(
  null,
  "Catalog/{*path}",
  new { controller = "Catalog", action = "Index", path = string.Empty}
);

объявляется маршрут с путевой частью "catalog/" и параметром path , который принимает любую строку, а по в случае отсутствия этой строки - является пустой строкой

Код тестов проверяющих заданное поведение будет выглядеть так (я использую NUnit и NMoq):
[Test]
// проверяем поведение при пустом значении path
public void CatalogIndexWithEmptyPathTest()
{
  // arrange
  var p = new Product(){Id = 1, Parent = null };
  var p1 = new Product(){Id = 2, Parent = p };
  var p2 = new Product(){Id = 3, Parent = p };
  var p3 = new Product(){Id = 4, Parent = p };
  var pChilds = new[]{p1,p2,p3};


  var mockProductService = new Mock<IProductService>();
  mockProductService.Setup(x => x.GetByParent(null)).Returns(new[]{p});
  mockProductService.Setup(x => x.GetByParent(p)).Returns(pChilds);

  var catalogController = new CatalogController(mockProductService.Object, null);


  // act
 var result = (ViewResult)catalogController.Index(string.Empty);

 // assert
 Assert.IsNotNull(result);

  var exceptedProduct = (Product)result.ViewData["Product"];
  Assert.AreEqual(p, exceptedProduct);

  var exceptedChilds = (IEnumerable<Product>) result.ViewData["Childs"];
  Assert.AreEqual(pChilds, exceptedChilds);
}

[Test]
// проверяем поведение при не правильном (не существующем) значении path
public void CatalogIndexWithNotExistsPathTest()
{
  // arrange
  var mockProductService = new Mock<IProductService>();
  mockProductService.Setup(x => x.GetByName(It.IsAny<string>())).Returns((Product) null);

  var catalogController = new CatalogController(mockProductService.Object, null);

  // act
  var result = (RedirectToRouteResult)catalogController.Index("not/exists/product");

  // assert
  Assert.IsNotNull(result);
  Assert.AreEqual("Index", result.RouteValues["action"]);
}

и наконец самый большой тест проверяющий поведение при правильных данных

[Test]
public void CatalogIndexWithSomePathTest()
{
  // arrange

  var p = new Product(){Id = 1, Parent = null, Name = "item" };
  var p1 = new Product(){Id = 2, Parent = p, Name = "in" };
  var p2 =  new Product(){Id = 3, Parent = p1, Name = "catalog" };
  var p21 = new Product(){Id = 4, Parent = p2 };
  var p22 = new Product(){Id = 5, Parent = p2 };
  var p23 = new Product(){Id = 6, Parent = p2 };
  var p2Childs = new[]{p21,p22,p23};

  var mockProductService = new Mock<IProductService>();
  mockProductService.Setup(x => x.GetByName(p.Name)).Returns(p);
  mockProductService.Setup(x => x.GetByName(p1.Name)).Returns(p1);
  mockProductService.Setup(x => x.GetByName(p2.Name)).Returns(p2);
  mockProductService.Setup(x => x.GetByParent(p2)).Returns(p2Childs);

  // act
  var result = (ViewResult)catalogController.Index("item/in/catalog");

  // assert
  Assert.IsNotNull(result);

  var exceptedProduct = (Product) result.ViewData["Product"];  
  Assert.AreEqual(p2, exceptedProduct);


  var exceptedChilds = (IEnumerable<Product>) result.ViewData["Childs"];
  Assert.AreEqual(p2Childs, exceptedChilds);
}

Итак, три теста покрывают три основных сценария работы действия Index контроллера Catalog

1) Пустое значение path
2) Не существующее значение path
3) Существующее значение path

В идеале нужно тестировать еще и входящие и исходящие маршруты .. но об этом в другой раз


Сам код метода Index контроллера Catalog выглядит примерно так:
public class CatalogController : Controller
{
  private readonly IProductService _productService;
  private readonly IProductTypeService _productTypeService; 

  public CatalogController(IProductService productService, IProductTypeService productTypeService)
  {
    _productService = productService;
    _productTypeService = productTypeService;
  }


  public ActionResult Index(string path)
  {
    Product product = null;
    if(!string.IsNullOrWhiteSpace(path))
    {
      var pathParts = path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);      
      foreach (var pathPart in pathParts)
      {
        var parent = product;
        product = _productService.GetByName(pathPart);

        // если продукт с именем pathPart не найден или для найденного продукта parent не является родительским
        if ((product == null) || (parent != null && parent != product.Parent))
          return RedirectToAction("Index", new { path = string.Empty }); // делаем редирект на корневую точку каталога
      }
    }

    if (product == null)
      product = _productService.GetByParent(null).FirstOrDefault();

    // получаем список продуктов для который product является родительским
    var childs = _productService.GetByParent(product);

    ViewData["Product"] = product;
    ViewData["Childs"] = childs;
  }
}

Код представления /Views/Catalog/Index.aspx будет выглядеть примерно так:
<%
  var product = (Product) ViewData["Product"];
  var childs = (IEnumerable<Product>)ViewData["Childs"];
%>

<p><%: product.Name %></p>
<ul>
<% foreach(var child in childs) { %>
<li><%: child.Name %></li>
<% } %>
</ul>

// на самом  деле на месте child.Name нужно, с помощью метода Html.ActionLink, генерировать ссылку на дочерний объект
Об этом я напишу в следующем посте.

В целом у нас получилось одно действие Index контроллера Catalog. Это хорошо, потому что это просто и понятно как работает. В моем проекте понадобилось для некоторых продуктов, расположенных близко к корню (с уровнем вложенности 1-2 относительно корня) кастомизировать представление - т.е. отображать не просто иерархию со ссылками, а выводить некоторый дополнительный html-контент в представлении Index. При этом не хотелось в коде представления писать условия вида:
<% if(product.Name == "product123") %>
<%= "Дополнительный конент" %>
<% if(product.Name == "product456") %>
<%= "Другой дополнительный конент" %>
<% else %>
<%= "Представление по умолчанию" %>

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

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

вторник, 18 января 2011 г.

Первый пост

Приветствую всех !

Меня зовут Дмитрий, я занимаюсь программированием на платформе Microsoft .NET Framework. Живу в Екатеринбурге. Активно программирую на C# около года (до этого писал на C++, java, php). С этого же времени начал читать форумы, блоги, статьи посвященные платформе .NET вообще и языку C# в частности, также читаю книги посвященные .NET технологиям - в данный момент это ASP.NET MVC Framework с примерами на C# для профессионалов. Хорошая и подробная книга по технологии ASP.NET MVC , а также по практике использования unit-тестирования и подхода tdd. Для решения проблем, возникающих в процессе работы активно использую гугл и, как правило, нахожу ответы на форумах и в блогах. Поэтому и решил начать писать свой блог - вдруг комуто будут интересны те знания, которые я захочу опубликовать. Возможно все это уже написано в 10 других местах, может быть, что все мои посты будут бесполезны.. - в этом случае можно считать, что я пишу блог для самого себя. Кроме тем непосредственно относящихся к теме .net'а, возможно, буду писать тут какие-нибудь общественно-политические события, которые интересны и важны мне, посмотрим.