Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Реализация fluent builder в C#

48 комментариев

builderСейчас многие библиотеки реализуют так называемые fluent builder’ы, то есть дают возможность пользователю создавать объекты через более предсказуемый и документированный интерфейс чем просто массивно перегруженные конструкторы. Такой способ позволяет лучше контролировать процесс создания объектов, предупреджать ошибки, проще документировать шаги инициализации, производить валидацию и в целом лучше “выказывать намерения” (reveal intentions) в плане используемости API.

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

Простой пример

Паттерн Builder в основном полезен для сложных объектов, но здесь и далее примеры будут упрощены, и начнем мы с самого тривиального. Допустим что у вас есть объект типа Person:

class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

Чтобы добавить сборщик (PersonBuilder) для подобного объекта, сначала надо определить, куда его помещать. Тут есть несколько вариантов – мне на ум приходят как минимум четыре:

  • Не делать отдельного сборщика, а заставить сам Person собирать себя, благо это можно реализовать. К сожалению, такой подход загрязняет API.
  • Сделать отдельный сборщик как вложенный private класс.
  • Сделать сборщик обычным отдельным internal классом (а можно даже public – особых рисков это не несет), а все подвластные ему структуры действительно сделать вложенными.
  • Сделать сборщик классом-контейнером методов расширений (т.е. static), и сделать метод(ы) расширения для начала работы с конфигурацией.

Давайте возьмем 3й вариант. Представим на секунду, что объект Person имеет статический метод Create(), который возвращает некий PersonBuilder:

public class Person
{
  public string Name {get;set;}
  public int Age {get;set;}
  public static PersonBuilder Create() 
  {
    return new PersonBuilder(new Person());
  }
}

Итак, мы видим первый трюк fluent builder’ов: создаваемый объект пробрасывается через все вызовы. Это полезно. Только вот наблюдательный читатель задаст вопрос: какого-такого Create() возвращает PersonBuilder а не Person? Спокойствие, мой падаван, ответ уже близок.

Класс PersonBuilder – это обычный класс который держит ссылку на Person. Помимо этого, он дает нам возможность управлять создаваемым объектом через методы (или свойства – об этом скоро) с помощью fluent-вызовов, т.е. методов которые кончаются на return this. Но самой замечательной особенностью является то, что наш builder имеет оператор неявного приведения к Person. Вот как все это выглядит:

public class PersonBuilder
{
  private Person person;
  public PersonBuilder(Person person)
  {
    this.person = person;
  }
  public PersonBuilder Called(string name)
  {
    person.Name = name;
    return this;
  }
  public PersonBuilder Age(int age)
  {
    person.Age = age;
    return this;
  }
  public static implicit operator Person(PersonBuilder pb)
  {
    return pb.person;
  }
}

Использование этого сборщика тривиально:

Person me = Person.Create().Called("Dmitri").Age(25);

Итак, мы познакомились еще с одним правилом: любой конечный сборщик должен быть приводим к типу, который собирает. Под “конечным сборщиком” я подразумеваю не только PersonBuilder, но также любой специализированный сборщик, который PersonBuilder мог подставить. В следующей секции мы как раз об этом и поговорим.

Промежуточные объекты

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

Давайте сделаем еще один пример: у нас есть все тот же Person, и у него есть два набора свойств (или два аггрегированных объекта – без разницы) – адрес и информация о месте работы. Перепишем Person:

public class Person
{
  public string Name {get;set;}
  public int Age {get;set;}
  
  public string StreetAddress {get;set;}
  public string PostCode {get;set;}
  public string Country {get;set;}
  
  public string CompanyName {get;set;}
  public string Position {get;set;}
  public int AnnualIncome {get;set;}
  
  public static PersonBuilder Create() 
  {
    return new PersonBuilder(new Person());
  }
}

Для конфигурации адреса и работы отдельно создаются промежуточные сборщики (т.н. “подсборщики”), в нашем случае PersonAddressBuilder и PersonJobBuilder. Эти классы – обязательно вложенные. Возьмем первый класс – вот его базовое определение:

public class PersonAddressBuilder
{
  private Person person;
  public PersonAddressBuilder(Person person)
  {
    this.person = person;
  }
  public PersonAddressBuilder At(string streetAddress)
  {
    person.StreetAddress = streetAddress;
    return this;
  }
  public PersonAddressBuilder WithPostCode(string postCode)
  {
    person.PostCode = postCode;
    return this;
  }
  public PersonAddressBuilder In(string country)
  {
    person.Country = country;
    return this;
  }
}

Думаю намек понят – согласно правилу про которое я уже писал, Person пробрасывается и сюда. Пока что мы видим обычный fluent interface, но это только пока. Тем временем, PersonBuilder выбрасывает этот сборщик через свойство что, собственно, не запрещено:

public PersonAddressBuilder Lives
{
  get
  {
    return new PersonAddressBuilder(person);
  }
}

Вернемся к PersonAddressBuilder – как я уже писал, вы обязаны выбрасывать отсюда Person по желанию. Но помимо этого, вы должны дуплицировать все fluent-подвызовы корнегого builder’а в каждом подсборщике. От такого словоизвержения может начать глючить, поэтому вот сразу код:

public PersonJobBuilder Works
{
  get
  {
    return new PersonJobBuilder(person);
  }
}
public static implicit operator Person(PersonAddressBuilder builder)
{
  return builder.person;
}

Ну вот собственно и всё. А второй сборщик реализован симметрично:

public class PersonJobBuilder
{
  private Person person;
  public PersonJobBuilder(Person person)
  {
    this.person = person;
  }
  public PersonJobBuilder At(string companyName)
  {
    person.CompanyName = companyName;
    return this;
  }
  public PersonJobBuilder AsA(string position)
  {
    person.Position = position;
    return this;
  }
  public PersonJobBuilder Earning(int dollarsPerMonth)
  {
    person.AnnualIncome = dollarsPerMonth;
    return this;
  }
  public PersonAddressBuilder Lives
  {
    get
    {
      return new PersonAddressBuilder(person);
    }
  }
  public static implicit operator Person(PersonJobBuilder builder)
  {
    return builder.person;
  }
}

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

Тем временем, у нас всё. Хотите посмотреть как это счастье использовать? Да вот так:

Person me = Person.Create()
  .Lives.At("123 London Road").WithPostCode("SO17 1BJ").In("Southampton")
  .Works.At("CRSI").AsA("VisitingResearcher").Earning(12345);

Сложные сценарии

Сушествует несколько более сложных сценариев для использования fluent-конфигураторов (сборщиков, builder’ов, называйте как хотите).

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

Во-вторых, существуют ситуации когда хочется создавать свои собственный объекты исключительно для конфигурации. Например, что если я хочу указать возраст не только как 25 но как 25.years + 7.months? Это возможно, но для этого нужно много методов расширения, и в результате у нас получится загрязнение API, много промежуточных объектов, много переопределенных операторов. Слава богу что большинство их этого можно вынести в отдельные классы, которые не мешаются с основным “контентом”.

Ну и наконец, осталась нераскрытой тема использования анонимных классов и Expression<T> – но это относится скорее к типам параметров к типам параметров чем к инфраструктуре. А про инфраструктуру пока все. Comments welcome. ■

Реклама

Written by Dmitri

25 августа 2010 в 1:32

Опубликовано в .NET, C#

Tagged with

комментариев 48

Subscribe to comments with RSS.

  1. Если у свойств Person публичные сеттеры, почему бы не использовать инициализацию при создании нового Person?
    new Person() {Name = «Iam», Age = 23}
    И конструкторы писать не надо и классов никаких дополнительно.

    muradovm

    25 августа 2010 at 6:41

    • Никто не запрещает это делать. Просто паттерн Builder сам по себе подразумевает дополнительные действия, которые происходят при инициализации, а также всякую intention-revealing API-магию, которую я в посте не показал.

      Dmitri

      25 августа 2010 at 9:43

      • Жаль, что не получилось в записи указать преимущества билдеров. Жду вашего следующего поста ^-)

        muradovm

        25 августа 2010 at 9:46

        • На самом деле уже начинаю думать что надо было начинать с описания паттерна Builder и его приемуществ.

          Dmitri

          25 августа 2010 at 9:49

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

    Roman

    25 августа 2010 at 7:04

    • Тут дело не только в полях. В конструкторах может быть особая conditional logic, которая в случае с Builder-ами делается более явной.

      Dmitri

      25 августа 2010 at 9:38

      • conditional logic, конечно, выложена в отдельный класс. Но тогда хотелось бы как минимум видеть конструктор у класса Person c доступом protected internal.

        muradovm

        25 августа 2010 at 9:49

        • Это спорный вопрос. Ну то есть если хочется заставить всех использовать builder то так и надо сделать. Все зависит от того, подходит объект под «вольное» конфигурирование или нет.

          Dmitri

          25 августа 2010 at 9:51

      • >>Это спорный вопрос. Ну то есть если хочется заставить >>всех использовать builder то так и надо сделать. Все >>зависит от того, подходит объект под «вольное» >>конфигурирование или нет.

        Согласен. Если нужно иметь возможность «вольно» конфигурировать объект, а также конфигурировать его с использованием какой-то сложной логики, то билдеры уместны. Однако, в вашей статье это не освещено. Применительно к статье у Person не показана эта сложная логика, а, значит, билдеры излишни.

        muradovm

        25 августа 2010 at 10:40

  3. Согласен с Романом. Корректное состояние и связанные с ним процессы внутри объекта важнее, чем всякие билдеры.

    muradovm

    25 августа 2010 at 7:23

  4. читая сразу вспомнил про ninject =)

    vittore

    25 августа 2010 at 8:57

  5. Тоже настороженно отношусь к благам fluent-интерфейсов. Выглядит, конечно, «человечно», но в силу инертности мышления, такого вида запись меня пугает.
    Хотелось бы узнать о преимуществах.

    sh

    25 августа 2010 at 9:01

    • преимущества так сказать «налицо»
      читабельность

      vittore

      25 августа 2010 at 9:05

      • Ну, читабельность понятие весьма субъективное.
        Ещё ладно бы было написано на F#, тогда действительно сойдёт за текст на человеческом языке.
        А так это нечто страшное. В особенности если учитывать что код будут читать программисты, то мне кажется проще использовать стандартные конструкции.

        Steck

        25 августа 2010 at 12:55

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

      Dmitri

      25 августа 2010 at 9:39

  6. вот только я не понял почему не наследовать билдеры от базового билдера…

    vittore

    25 августа 2010 at 9:04

    • Интересно, что бы вы указали в базовом билдере.

      muradovm

      25 августа 2010 at 9:06

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

      Dmitri

      25 августа 2010 at 9:41

  7. А в чём прелесть свойства с get { return new SomeReferenceType(); }? С тем, что каждое обращение к свойству создаёт новый экземпляр? Почему бы его не переделать на метод или не создать один единственный экземпляр и возвращать из свойства его?

    Viacheslav Ivanov

    25 августа 2010 at 9:34

    • Ну свойство просто лучше выглядит, а вообще можно и метод сделать — особой разницы нет. А «единственный экземпляр» тут скорее всего не подойдет — ведь каждый сборщик строит свой конкретный объект.

      Dmitri

      25 августа 2010 at 9:40

  8. Имхо, лавры LINQ с его цепочками вроде list.Where(…).Skip(…).Take(..).Reverse()… не дают покоя. Только в его случае это практически декларативный механизм, а здесь мы видими императивный оверхед, который преимуществ перед стандартными механизмами инициализации не дает никаких. Даже синтаксическим сахаром назвать сложно :)

    Guderian

    25 августа 2010 at 9:43

    • Думаю вы не будете спорить что паттерн Builder дает весьма серьезные приемущества? Я же просто представил его fluent-версию.

      Dmitri

      25 августа 2010 at 9:45

      • Возможно, fluent версия билдеров и будет полезна где-то. Но не в ваших вышеприведенных примерах.

        muradovm

        25 августа 2010 at 10:43

      • Преимущества дает возможность выделить стратегию инициализации. В этом нет сомнений. С этой задачей фабрики объектов отлично справлялись уже давно. А вот преимущества fluent-версии, лично для меня, не очевидны. Т.е. либо не хватает примеров, которые бы показали их во всей красе, либо это просто fluent-sugar ;)

        Guderian

        25 августа 2010 at 11:55

    • Fluent интерфейс, кстати, придуман задолго до LINQ. Как и паттерн Builder, конечно.

      Чак Норрис

      25 августа 2010 at 18:05

  9. Использование билдеров — это, на мой взгляд, усложнение по сравнению с использованием конструкторов с параметрами.

    NVS

    25 августа 2010 at 9:46

    • Ну… а вы попробуйте представить себе StringBuilder который инициализируется полностью в конструкторе :) Ладно, я шучу, понимаю что здесь Builder используется несколько по-другому.

      Dmitri

      25 августа 2010 at 9:49

    • > Использование билдеров – это, на мой взгляд, усложнение по сравнению с использованием конструкторов с параметрами.

      А использование паттерна Abstract Factory Вам не кажется усложнением по сравнению с конкатенацией строк? :-) Это я к тому, что не корректно сравнивать коров с апельсинами. Билдер — это ПАТТЕРН и у него свои предпосылки и границы применения. В статье же просто простейший вариант описан для понимания сути вопроса. О каком конструкторе, например, может идти речь, если строиться целый граф объектов?

      Чак Норрис

      25 августа 2010 at 18:26

  10. Конструкторы развернули невиданную по масштабам дискуссию. Много знатоков в этом вопросе:)

    yiith

    25 августа 2010 at 12:27

  11. Дмитрий, спасибо, исключительно интересная статья и изящное решение. Возьму на вооружение. Вообще, всегда с удовольствием читаю Ваши статьи — много очень нетривиальных и красивых решений.

    eugene5010

    25 августа 2010 at 13:19

  12. Очень похоже на монадический синтаксис из функционального программирования. =)

    Артуп

    25 августа 2010 at 15:58

  13. Обычно необходимость множества билдеров решается единым классом с несколькими интерфейсами. В данном случае, если мы хотим быть уверены, что адрес введен полностью, метод Lives возвращает INeedAddress, метод At — INeedPostCode и так далее (все это один и тот же экземпляр Builder-а).

    ulu

    25 августа 2010 at 16:34

    • Хмм… ты прав (выделил жирным чтобы мысль не убежала). Действительно, интерфейсы я как-то не подумал сюда приплести. На самом деле единственное что меня пожалуй беспокоит в интерфейсах так это то что количество типов на верхнем уровне будет больше одного.

      Dmitri

      25 августа 2010 at 16:40

  14. Builder + Fluent Иногда дают очень существенные преимущества. Например, когда нужно построить древовидные структуры (Criteria API в NHibernate) или при конфигурации (те же ioc-контейнеры) где за сценой создаётся нетривиальный граф. Кстати, совершенно случайно (совпадение!) сегодня написал пост в песочницу хабра — про использование fluent для контроллеров в ASP.NET MVC. Там расписаны некоторые получаемые в этом случае преимущества:

    http://habrahabr.ru/sandbox/17699/

    Чак Норрис

    25 августа 2010 at 18:17

  15. В принципе спасибо за статью.
    Теперь понимаю для чего это всё. Таким способом можно создавать софты, которые после создателя (родителя) никем не будут понятны. Ведь надо-же так «заморочить» то, что простыми словами выразить можно.
    Так и хочется сказать «короче, Склифосовский». Я думаю, надо быть проще. И конструкторы вполне справляются с выше описаными исходными ситуациями.
    Иначе такие описания тоже возможны, но хотим-ли мы этого (WithPostCode повторяется несколько раз):

    Person me = Person.Create().Lives.At("123 London Road").WithPostCode("SO17 1BJ").In("Southampton").WithPostCode("BLA-BLA").Works.At("CRSI").AsA("VisitingResearcher").Earning(12345).Works.At("BLA-BLA").WithPostCode("BRED");
    

    Граматически всё конечно-же правильно у Вас в статье. Но если так будут библиотеки классов писаться, то мне как-то не по себе.

    И в одном из проектов, в котором был задействован — как раз-таки такую тенденцию и видел. Софт, который имел 15 окон и обращался на 20 БД таблиц, состоял из более 700 классов. И вроде всё работало. А понять почему — никому не было дано.

    Кеша

    26 августа 2010 at 0:19

    • Вы вообще в курсе, что такое ПАТТЕРН? Если нет, то наверное не стоит гордиться своим невежеством.

      > И конструкторы вполне справляются с выше описаными исходными ситуациями.

      С теми, которые преведены в статье, возможно, и справляются. Но есть много других случаев, где билдер незаменим. В статье же даётся только ЭЛЕМЕНТАРНЫЙ ПРИМЕР использования builder+fluent.

      > Но если так будут библиотеки классов писаться, то мне как-то не по себе.

      Так УЖЕ пишутся многие библиотеки. Так что уже можно идти плакать))

      > Софт, который имел 15 окон и обращался на 20 БД таблиц, состоял из более 700 классов.

      Как это вообще экстраполируется на Builder/Fluent?)) Кстати, сложность возникает обычно когда разработчики игнорируют лючшие практики, а не когда следуют им. А паттерны (в том числе и билдер) — это как раз best practices.

      Чак Норрис

      26 августа 2010 at 7:53

    • Лучше уж 700 классов по 3 строчки, чем один класс с 700 if-ами!

      ulu

      26 августа 2010 at 9:39

      • Лучше ни то ни другое;)

        eugene5010

        26 августа 2010 at 10:03

      • Во-первых одно другое не исключает. Это может быть дискуссия не о чём.

        А вот тенденция на то, что простые вещи подменяются более сложными — это наблюдаю постоянно. И подходы меняются. Программа создаётся по ходу процесса, не так — сесть, проанализировать, обдумать и в голове (на бумаге) концепт её создать. И только потом перевести это всё в строчки кода. Нет на сегодняшний день с первой секунды любого проекта уже стучат по клавишам, уже вылетает код из под рук. И потом принимает такие «размахи», что переделками и подклеиванием то что в итоге остаётся и системной архитектурой и не на зовёшь — не позволяет воспитание. И прав автор — это не панацея для всего, но сам факт наличия этого иструмента и теоритической обоснования, понимается многими как стандарт.
        И усложнение вроде простых по ожиданию программ — тому пример.

        А большое кол-во классов часто связано с тем, что многие считают своим долгом — велосипеды по-новому изобретать.

        Ладно забудем об этом. Суть была в показе возможности языка. Получилось неплохо. А вот что из этого будет делаться. Ну это дело вкуса.

        Кеша

        26 августа 2010 at 11:01

      • Безусловно, с этим никто не спорит, просто приведенное утверждение про 700 классов точно такая же крайность как и 700 if-ов, и ничем не лучше. И понятно, что в простых случаях builder использовать никто не будет. Паттерны вообще весьма опасная штука, особенно для начинающего разработчика. Не зря ведь у Кириевски книжка называется «Refactoring to Patterns».

        eugene5010

        26 августа 2010 at 12:19

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

      Граматически всё конечно-же правильно у Вас в статье. Но если так будут библиотеки классов писаться, то мне как-то не по себе.

      Я вас удивлю, но многие библиотеки именно так и пишутся. И именно поэтому их проще понимать и использовать.

      Dmitri

      26 августа 2010 at 12:58

      • Здравствуйте, у меня возникло впечатление, что ваш «конструкт» несколько опционален, не имеет ли смыла попробовать решить задачу очеловечивания кода с помощью extension методов? (Тогда в случае, если велосипед себя не оправдает, можно будет просто нажать кнопку Del)

        Андрей

        2 сентября 2010 at 4:15

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

    Это называется Agile, и в умелых руках приводит, как раз, к более простому коду — мы вначале пишем, «чтобы работало», а потом уже, при необходимости, рефакторим все это. В данном случае мы с первой секунды пишем, действительно, конструктор, а потом уже решаем, целесообразно ли тут билдер использовать.

    ulu

    26 августа 2010 at 13:20

    • Да, а еще бывают ситуации когда заранее знаешь что такая форма записи приведет к значительным бенефитам в будущем. Тогда можно сразу внедрять и использовать.

      Dmitri

      26 августа 2010 at 13:26

  17. Ребята, ну не о банальностях-же идёт речь.
    Конечно-же многим понятно, что иногда проснёшься и весь будущий софт у тебя уже в голове.
    Но право — если-бы так всегда было, почему тогда существуют отладчики, и слово баг тоже не с потолка свалилось. И софты глючат. Все, и у моих ребят и у вас и у ваших teems.

    И уже три IFа подряд иметь — уже и «не модно» вроде.

    Небольшое отступление:

    Если вы только программированием занимаетесь, то в ваших глазах это так всё красиво и звучит.
    Когда управляешь проектами и время на рефакторинг не поддаётся определению сроков исполнения, а клиент ожидает рабочую версию и вы не в состоянии ничего, кроме как «мы налаживаем» — сказать, когда пытаешься расширить team, а с «выдуманными велосипедами» кроме автора никто не сможет справиться и прочее и прочее, тогда понимаешь, чем такой «Агилевский» подход закончится может.

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

    PS Извените, я наверное не туда попал, здесь друг-другу за простые прописные истины оценки ставят. Немного смешно даже стало. Воспринимайте мой взгляд — как со сотороны — ведь так и есть.

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

    Кеша

    27 августа 2010 at 11:25

    • Наверное, мы действительно в разных ситуациях. Когда у Вас горят сроки, а в каждом методе по три IFа, лучше, наверное, так и оставить. А за поддержку взять побольше денег, потому как через год разобраться с вложенными ифами в несколько раз труднее, чем с fluent декларациями. Совершенно другая ситуация, когда у Вас, наоборот, over-engineered проект без внятной документации и грамотного покрытия тестами (которые вполне могут заменить документацию, если грамотные). Тогда надо по науке работы с legacy systems. Т.е., в первом случае — недобор, а во втором — перебор.

      И совершенно третья ситуация у меня, как пользователя написанного кем-то fluent API — я пишу код типа
      mapping.HasMany(item => item.Values).AsEntityMap(«`index`»).Cascade.AllDeleteOrphan().Access.CamelCaseField(Prefix.Underscore) — и через год буду с первого взгляда понимать, что тут творится. Так что спасибо огромное разработчикам, которые потрудились написать для меня такой API (но я надеюсь, что мне не придется разбираться в том, как оно работает).

      ulu

      27 августа 2010 at 14:31


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: