SlideShare ist ein Scribd-Unternehmen logo
1 von 78
Downloaden Sie, um offline zu lesen
Роль декомпозиции
    функционала
на отдельные классы
при следовании TDD

     Бибичев Андрей
      декабрь 2011
@bibigine
                     Андрей Бибичев

• E-mail:       bibigine@gmail.com
• Twitter:      @bibigine
• Profile:      http://tinyurl.com/bibigine
• Slideshare:   http://www.slideshare.net/bibigine
Пример
ToBe
                                                 Confirmed
      Subscription
                                      Письмо
                                    отправлено                Ошибка
                                                              отправки
UserName     : String                                          письма
Email        : String
Organization : String                   Confirming

Status                           «Жмакнута»                  Error
ConfirmationGuid                   ссылка
MailedAt     : DateTime?
                                        Confirmed


                              Перегружено
                           в систему рассылки


                                        Subscribed
Что делать,
 если ввели email,
который уже есть?
public class AllSubscriptions : IAllSubscriptions
{
    ...
    public Subscription CreateOrUpdate(
        string userName, string email, string organization,
        DateTime currentMoment)
    {
        var guid = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);

        // INSERT INTO subscriptions(....)
        // VALUES (...)
        // ON DUPLICATE KEY UPDATE subscription_id = subscription_id

        var subscription = FindByEmail(email);
        if (subscription.ConfirmationGuid != guid
            && !new[] { SubscriptionStatus.Confirmed,
                        SubscriptionStatus.ToBeConfirmed
               }.Contains(subscription.Status)
            && (subscription.Status != SubscriptionStatus.Confirming
                || subscription.MailedAt == null
                || subscription.MailedAt.Value < currentMoment.AddMinutes(-10)))
        {
            // UPDATE subscriptions SET status = 'ToBeConfirmed', ...
            // WHERE subscription_id = subscription.id
        }

        return subscription;
    }
И так попадет в
    Это старая                                        систему рассылки
      запись

                                                              И так должен
                                                             получить письмо
if (subscription.ConfirmationGuid != guid                      со ссылкой
    && !new[] { SubscriptionStatus.Confirmed,
                SubscriptionStatus.ToBeConfirmed
              }.Contains(subscription.Status)
    && (subscription.Status != SubscriptionStatus.Confirming
        || subscription.MailedAt == null
        || subscription.MailedAt.Value <
                                 currentMoment.AddMinutes(-10)))


     С момента отправки письма со ссылкой
       прошло больше 10 минут (возможно,
     пользователь не получил его и повторно
                подписывается)                   А если прошло меньше
                                              времени, то это больше похоже
                                                 на «паразитный» Reload
Тестировать такой метод ой
 как неприятно и муторно
Лозунг TDD



           тестируй
Разделяй и властвуй
Specification

 ISpecification<Subscription>




SubscriptionCanBeRenewed

IsSatisfiedBy(obj): bool
public class SubscriptionCanBeRenewed : ISpecification<Subscription>
{
    public const int PARASITIC_RELOAD_INTERVAL_IN_MINUTES = 10;

    public SubscriptionCanBeRenewedSpecification(
                                              DateTime currentMoment)
    {
        this.currentMoment = currentMoment;
    }

    public bool IsSatisfiedBy(Subscription subscription)
    {
        var mailedBefore = currentMoment.AddMinutes(
                             -PARASITIC_RELOAD_INTERVAL_IN_MINUTES);

        return !new[] { SubscriptionStatus.Confirmed,
                        SubscriptionStatus.ToBeConfirmed
                      }.Contains(subscription.Status)
            && (subscription.Status != SubscriptionStatus.Confirming
                || subscription.MailedAt == null
                || subscription.MailedAt.Value < mailedBefore);
    }

    private readonly DateTime currentMoment;
}
Тестировать такой класс –
   одно удовольствие
// given
var subscription = new Subscription
    {
         Status = SubscriptionStatus.Confirming,
         MailedAt = new DateTime(2011, 12, 17, 18, 10, 0),
    };

var now = DateTime = new DateTime(2011, 12, 17, 18, 22, 0);
var sepcification = new SubscriptionCanBeRenewed(now);

// when
var isOk = sepcification.IsSatisfiedBy(subscription);

// then
isOk.Should().BeTrue();


                                          FluentAssertions
Возвращаемся к нашему «барану»
public Subscription CreateOrUpdate(
    string userName, string email, string organization,
    DateTime currentMoment)
{
    var guid = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture);

    // INSERT INTO subscriptions(....)
    // VALUES (...)
    // ON DUPLICATE KEY UPDATE subscription_id = subscription_id

    var subscription = FindByEmail(email);
    var isOldRecord = subscription.ConfirmationGuid != guid;
    if (isOldRecord)
    {
        var spec = new SubscriptionCanBeRenewed(currentMoment);
        if (spec.IsSatisfiedBy(subscription))
        {
            // UPDATE subscriptions SET status = 'ToBeConfirmed', ...
            // WHERE subscription_id = subscription.id
        }
    }

    return subscription;
}
Но как теперь тестировать UPDATE?

• Можно сделать фабрику для спецификации,
  инжектить ее в репозиторий, на тестах mock-ать.
  Но это столько возни…

• Лучше продолжим отрывать куски в отдельные
  классы
public interface ICommand<T> { void ExecuteFor(T obj); }




internal class SubscriptionRenewCommand : ICommand<Subscription>
{
    public SubscriptionRenewCommand(IDataContext dataContext)
    { … }


    public void ExecuteFor(Subscription obj)
    {
        // UPDATE subscriptions SET status = 'ToBeConfirmed', ...
        // WHERE subscription_id = obj.id
    }
}
Можно еще сделать «наворот»
public static class CommandExtensions
{
    public static ICommand<T> If<T>(
        this ICommand<T> command, ISpecification<T> condition)
    {
        return new IfCommand<T>(command, condition);
    }

   private class IfCommand<T> : ICommand<T>
   {
       public IfCommand(
           ICommand<T> command, ISpecification<T> condition)
       {
           this.command = command;
           this.condition = condition;
       }
        public void ExecuteFor(T obj)
        {
            if (condition.IsSatisfiedBy(obj))
                command.ExecuteFor(obj);
        }
        private readonly ICommand<T> command;
        private readonly ISpecification<T> condition;
   }
Тогда код будет выглядеть:


new SubscriptionRenewCommand(dataContext)
    .If(new SubscriptionCanBeRenewed(currentMoment))
    .ExecuteFor(subscription);




                                     Почти jQuery
                                     Или даже монада? :)
• Аналогично для выполнения команды в цикле

• Для цепочки команд

• Если что, можно команды конструировать с
  объектом контекста (мутабельным)
«interface»
               ISpecification<T>

             IsSatisfiedBy(obj):bool




                     Composite
                   Specification




NotSpecification    AndSpecification   OrSepcification
public static class SpecificationExtensions
{
    public static ISpecification<T> And<T>(
        this ISpecification<T> left, ISpecification<T> right)
    {
        return new AndSpecification(left, right);
    }

    public static ISpecification<T> Or<T>(
        this ISpecification<T> left, ISpecification<T> right)
    {
        return new OrSpecification(left, right);
    }

    public static ISpecification<T> Not<T>(
        this ISpecification<T> spec)
    {
        return new NotSpecification(spec);
    }
}
«Философия»
мелких классов
Sprout Method

 Sprout Class
someMethod(…)
При использовании TDD
    логики в не-public методах
       почти не содержится,
да и самих таких методов, обычно,
   очень мало (тупые хелперы)
Sprout Class
UsefulClass




                     TinyUsefulClass
HelperClass1

          HelperClass2                 UtilClass
Математика
• Теорема
• Лемма
Автопром
Проблемы
с мелкими классами
• тяжело придумывать имена классов
  o Правило: чем уже scope использования класса, тем длиннее
    может быть его имя
  o Плюс помогают постфиксы из шаблонов проектирования

• легко запутаться в таком количестве
  классов
  o Помогает (но не спасает) хорошая упаковка классов по
    пакетам/пространствам имен
  o см. ниже

• большая косвенность кода
  o современные среды разработки слегка спасают

• как организовать взаимодействие?
  o см. ниже

• есть проблемы с областью видимости
  o Обычно на это плюют…
«Упаковка»
мелких классов
«Old School» Packaging


       MyMegaLogic

         MegaClass

         CoolClass

        UsefulClass



   HelperClass
«New School» Packaging

                 Cool


                CoolClass

               InterfaceOne

               InterfaceTwo


          TinyUsefulClass
HelperClass1
             UtilClass
HelperClass2
Пространства имен
• MyLogic
 o MegaCool
    • здесь ваши классы:
      CoolClass, MegaClass, OtherClass

   • вложенные пространства имен
      o Internals.CoolClass
      o Internals. MegaClass

 o SomethingOther
Увязывание классов
   между собой
Задачка от Yandex
#include <stdio.h>
class Feature {
  public:
    enum FeatureType {eUnknown, eCircle, eTriangle, eSquare};
    Feature() : type(eUnknown), points(0) { }
    ~Feature() { if (points) delete points; }
    bool isValid() { return type != eUnknown; }

   bool read(FILE* file) {
     if (fread(&type, sizeof(FeatureType), 1, file)
         != sizeof(FeatureType)
       return false;
     short n = 0;
     switch (type) {
       case eCircle: n = 3; break;
       case eTriangle: n = 6; break;
       case eSquare: n = 8; break;
       default: type = eUnknown;
       return false;
     }
     points = new double[n];
     if (!points) return false;
     return fread(&points, sizeof(double), n, file)
            == n*sizeof(double);
   }
void draw() {
       switch (type) {
         case eCircle:
           drawCircle(points[0], points[1], points[2]);
           break;
         case eTriangle:
           drawPoligon(points, 6);
           break;
         case eSquare:
           drawPoligon(points, 8);
           break;
       }
     }

 protected:
   void drawCircle(double centerX, double centerY,
                   double radius);
   void drawPoligon(double* points, int size);

     double* points;
     FeatureType type;
};
int main(int argc, char* argv[]) {
  Feature feature;
  FILE* file = fopen("features.dat", "r");
  feature.read(file);
  if (!feature.isValid())
    return 1;
  return 0;
}
Шаг №1
«interface»
                  Shape
             read(file):bool
             draw()
             isValid():bool




  Circle         Polygon                NullShape
-centerX       -points[*]
-centerY
-radius



           Triangle            Square
class Feature {
  public:
    Feature() : shape(new NullShape()) { }
    ~Feature() { delete shape; }
    bool isValid() { return shape->isValid; }
   bool read(FILE* file) {
     FeatureType type;
     if (fread(&type, sizeof(FeatureType), 1, file) != sizeof(FeatureType))
       return false;
     delete shape;
     switch (type) {
       case eCircle: shape = new Circle(); break;
       case eTriangle: shape = new Triangle(); break;
       case eSquare: shape = new Square(); break;
       default: shape = new NullShape();
     }
     return shape.read(file);
   }
   void draw() { shape->draw(); }
  private:
    Shape* shape;
    enum FeatureType {eUnknown, eCircle, eTriangle, eSquare};
};
Этот switch ужасен и нарушает OCP
Шаг №2
typedef Shape* ShapeConstructor();

class Feature {
  public:
    static int registerShape(
        int uniqueCode, ShapeConstructor* constructor)
    {
      shapeMap.insert(std::pair<int, ShapeConstructor*>(
                                         uniqueCode, constructor));
      return uniqueCode;
    }
    //…

   bool read(File* file)
   {
     //...
     auto it = shapeMap.find(type);
     shape = it == shapeMap.end() ? new NullShape() : it->second();
     return shape->read(file);
   }
    //…
  private:
    //…
    static std::map<int, ShapeConstructor*> shapeMap;
};
Circle.h

class Circle : public Shape {
     // …
   private:
     // …
     static const int _readCodeNotUsed;
};


Circle.cpp

Shape* circleConstructor() { return new Circle(); }

int _readCodeNotUsed =
        Feature::registerShape(1, &circleConstructor);
Если повторять этот подход на
                Java/C#
при помощи статических конструкторов,
           то надо не забыть
  в статическом конструкторе Feature
принудительно инициировать все классы,
          наследники от Shape
Shape   Feature
Шаг №3
Circle      Shape       Feature




         ShapeFactory
class Feature {
  public:
    //…

     bool read(File* file) {
        //...
        shape = ShapeFactory().createBy(type);
        return shape->read(file);
      }

     //…
};
typedef Shape* ShapeConstructor();

class ShapeFactory {
  public:
    static int registerShapeType(
                 int typeCode, ShapeConstructor* constructor) {
      shapeMap.insert(std::pair<int, ShapeConstructor*>(
                                      typeCode, constructor));
      return uniqueCode;
    }

   Shape* createBy(int typeCode) {
     auto it = shapeMap.find(typeCode);
     return it == shapeMap.end()
                         ? new NullShape() : it->second();
   }

  private:
    static std::map<int, ShapeConstructor*> shapeMap;
};
Circle.cpp

Shape* circleConstructor() { return new Circle(); }

int _readCodeNotUsed =
         ShapeFactory::registerShapeType(1, &circleConstructor);
ОДНАКО
MyDomain        MyPersistance


              WhereGenerator
SomeSpec
              -visitAnd()
              -visitOr()
              -visitSomeSpec()
NotSpecification              NotVisitorItem


OrSpecification               OrVisitorItem


AndSpecification              AndVisitorItem


    SomeSpec              SomeSpecVisitorItem



                 Словарь
               соответствия
Решается специализированным тестом
   в рамках тестов на MyPersistance:
     через reflection перебираются
       все классы из MyDomain,
     реализующие ISpecification<>,
и проверяется, что для каждого из них
        есть элемент в словаре
           spec <-> visitorItem
Наличие mixin-ов и duck-typing
в системе типов сильно бы помогло.
     См. например, google GO
«Общение»
мелких классов
 между собой
Приёмы
•   ServiceLocator
•   Dependency Injection
•   Callbacks, Events
•   шаблон HasValue
•   EventAggregator
Event Aggregator
• C# : Prism
• Java : GWT (EventBus)
public interface IEventAggregator
{
    void Register<TMessage>(Action<TMessage> action);
    void Send<TMessage>(TMessage message);
    void Unregister<TMessage>(Action<TMessage> action);
}
Полуминус
      Event Aggregator-а

Скрытая часть API

 Но параметр
 EventAggregator eventAggregator
 в конструкторе какбэ намекает
Итого
А почему/зачем?
Спасибо за внимание!

        Вопросы?

              @bibigine
        bibigine@gmail.com
      http://tinyurl.com/bibigine
  http://www.slideshare.net/bibigine

Weitere ähnliche Inhalte

Ähnlich wie Tdd and decomposition

Jboss drools expert (ru)
Jboss drools expert (ru)Jboss drools expert (ru)
Jboss drools expert (ru)
Victor_Cr
 
Язык программирования Scala / Владимир Успенский (TCS Bank)
Язык программирования Scala / Владимир Успенский (TCS Bank)Язык программирования Scala / Владимир Успенский (TCS Bank)
Язык программирования Scala / Владимир Успенский (TCS Bank)
Ontico
 
Управление зависимостями в программном коде
Управление зависимостями в программном кодеУправление зависимостями в программном коде
Управление зависимостями в программном коде
Dmitrey Naumenko
 
Разработка расширяемых приложений на Django
Разработка расширяемых приложений на DjangoРазработка расширяемых приложений на Django
Разработка расширяемых приложений на Django
MoscowDjango
 

Ähnlich wie Tdd and decomposition (16)

Jboss drools expert (ru)
Jboss drools expert (ru)Jboss drools expert (ru)
Jboss drools expert (ru)
 
Язык программирования Scala / Владимир Успенский (TCS Bank)
Язык программирования Scala / Владимир Успенский (TCS Bank)Язык программирования Scala / Владимир Успенский (TCS Bank)
Язык программирования Scala / Владимир Успенский (TCS Bank)
 
Coding like a sex
Coding like a sexCoding like a sex
Coding like a sex
 
Бодрящий микс из Selenium и TestNG- регрессионное тестирование руками разрабо...
Бодрящий микс из Selenium и TestNG- регрессионное тестирование руками разрабо...Бодрящий микс из Selenium и TestNG- регрессионное тестирование руками разрабо...
Бодрящий микс из Selenium и TestNG- регрессионное тестирование руками разрабо...
 
MVVM в WinForms – DevExpress Way (теория и практика)
MVVM в WinForms – DevExpress Way (теория и практика)MVVM в WinForms – DevExpress Way (теория и практика)
MVVM в WinForms – DevExpress Way (теория и практика)
 
Transactions and Denormalization in MongoDB - Artem Slobolinsky - Dnipropetro...
Transactions and Denormalization in MongoDB - Artem Slobolinsky - Dnipropetro...Transactions and Denormalization in MongoDB - Artem Slobolinsky - Dnipropetro...
Transactions and Denormalization in MongoDB - Artem Slobolinsky - Dnipropetro...
 
Управление зависимостями в программном коде
Управление зависимостями в программном кодеУправление зависимостями в программном коде
Управление зависимостями в программном коде
 
Разработка расширяемых приложений на Django
Разработка расширяемых приложений на DjangoРазработка расширяемых приложений на Django
Разработка расширяемых приложений на Django
 
Triggers для Mysql
Triggers для MysqlTriggers для Mysql
Triggers для Mysql
 
Android service
Android serviceAndroid service
Android service
 
Мультитенанстность в Exchange
Мультитенанстность в ExchangeМультитенанстность в Exchange
Мультитенанстность в Exchange
 
MyBatis на практике
MyBatis на практикеMyBatis на практике
MyBatis на практике
 
SECON'2016. Бочкарев Игорь, Реактивные мобильные приложения
SECON'2016. Бочкарев Игорь, Реактивные мобильные приложенияSECON'2016. Бочкарев Игорь, Реактивные мобильные приложения
SECON'2016. Бочкарев Игорь, Реактивные мобильные приложения
 
Быстрое введение в TDD от А до Я
Быстрое введение в TDD от А до ЯБыстрое введение в TDD от А до Я
Быстрое введение в TDD от А до Я
 
My batis
My batisMy batis
My batis
 
Java Persistence API (JPA) Basics
Java Persistence API (JPA) BasicsJava Persistence API (JPA) Basics
Java Persistence API (JPA) Basics
 

Mehr von Andrey Bibichev

Usability-for-programmers
Usability-for-programmersUsability-for-programmers
Usability-for-programmers
Andrey Bibichev
 

Mehr von Andrey Bibichev (20)

О usability водопроводных кранов
О usability водопроводных крановО usability водопроводных кранов
О usability водопроводных кранов
 
Geeks vs Managers (part 2)
Geeks vs Managers (part 2)Geeks vs Managers (part 2)
Geeks vs Managers (part 2)
 
Фрактальная природа IT-проектов (блиц)
Фрактальная природа IT-проектов (блиц)Фрактальная природа IT-проектов (блиц)
Фрактальная природа IT-проектов (блиц)
 
Usability-for-programmers
Usability-for-programmersUsability-for-programmers
Usability-for-programmers
 
Geeks vs Managers
Geeks vs ManagersGeeks vs Managers
Geeks vs Managers
 
Mockist vs Classicist
Mockist vs ClassicistMockist vs Classicist
Mockist vs Classicist
 
Natural User Interface (WUDRU-2011)
Natural User Interface (WUDRU-2011)Natural User Interface (WUDRU-2011)
Natural User Interface (WUDRU-2011)
 
Puasson burning
Puasson burningPuasson burning
Puasson burning
 
Архитектура в Agile: слабая связность
Архитектура в Agile: слабая связностьАрхитектура в Agile: слабая связность
Архитектура в Agile: слабая связность
 
Пользовательский автоматизм
Пользовательский автоматизмПользовательский автоматизм
Пользовательский автоматизм
 
Augmented Reality
Augmented RealityAugmented Reality
Augmented Reality
 
Agile: Think different
Agile: Think differentAgile: Think different
Agile: Think different
 
BDD
BDDBDD
BDD
 
DDD Workshop
DDD WorkshopDDD Workshop
DDD Workshop
 
Обзор Feature-Driven Development и Domain-Driven Design
Обзор Feature-Driven Development и Domain-Driven DesignОбзор Feature-Driven Development и Domain-Driven Design
Обзор Feature-Driven Development и Domain-Driven Design
 
О текстовом вводе замолвите слово
О текстовом вводе замолвите словоО текстовом вводе замолвите слово
О текстовом вводе замолвите слово
 
Проектирование больших ИС в Agile (статья)
Проектирование больших ИС в Agile (статья)Проектирование больших ИС в Agile (статья)
Проектирование больших ИС в Agile (статья)
 
Проектирование больших ИС в Agile
Проектирование больших ИС в AgileПроектирование больших ИС в Agile
Проектирование больших ИС в Agile
 
Enterprise Level Agile The Art Of Start
Enterprise Level Agile   The Art Of StartEnterprise Level Agile   The Art Of Start
Enterprise Level Agile The Art Of Start
 
Humane Interface (Гуманный интерфейс)
Humane Interface (Гуманный интерфейс)Humane Interface (Гуманный интерфейс)
Humane Interface (Гуманный интерфейс)
 

Tdd and decomposition

  • 1. Роль декомпозиции функционала на отдельные классы при следовании TDD Бибичев Андрей декабрь 2011
  • 2. @bibigine Андрей Бибичев • E-mail: bibigine@gmail.com • Twitter: @bibigine • Profile: http://tinyurl.com/bibigine • Slideshare: http://www.slideshare.net/bibigine
  • 4.
  • 5. ToBe Confirmed Subscription Письмо отправлено Ошибка отправки UserName : String письма Email : String Organization : String Confirming Status «Жмакнута» Error ConfirmationGuid ссылка MailedAt : DateTime? Confirmed Перегружено в систему рассылки Subscribed
  • 6. Что делать, если ввели email, который уже есть?
  • 7. public class AllSubscriptions : IAllSubscriptions { ... public Subscription CreateOrUpdate( string userName, string email, string organization, DateTime currentMoment) { var guid = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); // INSERT INTO subscriptions(....) // VALUES (...) // ON DUPLICATE KEY UPDATE subscription_id = subscription_id var subscription = FindByEmail(email); if (subscription.ConfirmationGuid != guid && !new[] { SubscriptionStatus.Confirmed, SubscriptionStatus.ToBeConfirmed }.Contains(subscription.Status) && (subscription.Status != SubscriptionStatus.Confirming || subscription.MailedAt == null || subscription.MailedAt.Value < currentMoment.AddMinutes(-10))) { // UPDATE subscriptions SET status = 'ToBeConfirmed', ... // WHERE subscription_id = subscription.id } return subscription; }
  • 8. И так попадет в Это старая систему рассылки запись И так должен получить письмо if (subscription.ConfirmationGuid != guid со ссылкой && !new[] { SubscriptionStatus.Confirmed, SubscriptionStatus.ToBeConfirmed }.Contains(subscription.Status) && (subscription.Status != SubscriptionStatus.Confirming || subscription.MailedAt == null || subscription.MailedAt.Value < currentMoment.AddMinutes(-10))) С момента отправки письма со ссылкой прошло больше 10 минут (возможно, пользователь не получил его и повторно подписывается) А если прошло меньше времени, то это больше похоже на «паразитный» Reload
  • 9. Тестировать такой метод ой как неприятно и муторно
  • 10. Лозунг TDD тестируй Разделяй и властвуй
  • 12. public class SubscriptionCanBeRenewed : ISpecification<Subscription> { public const int PARASITIC_RELOAD_INTERVAL_IN_MINUTES = 10; public SubscriptionCanBeRenewedSpecification( DateTime currentMoment) { this.currentMoment = currentMoment; } public bool IsSatisfiedBy(Subscription subscription) { var mailedBefore = currentMoment.AddMinutes( -PARASITIC_RELOAD_INTERVAL_IN_MINUTES); return !new[] { SubscriptionStatus.Confirmed, SubscriptionStatus.ToBeConfirmed }.Contains(subscription.Status) && (subscription.Status != SubscriptionStatus.Confirming || subscription.MailedAt == null || subscription.MailedAt.Value < mailedBefore); } private readonly DateTime currentMoment; }
  • 13. Тестировать такой класс – одно удовольствие
  • 14. // given var subscription = new Subscription { Status = SubscriptionStatus.Confirming, MailedAt = new DateTime(2011, 12, 17, 18, 10, 0), }; var now = DateTime = new DateTime(2011, 12, 17, 18, 22, 0); var sepcification = new SubscriptionCanBeRenewed(now); // when var isOk = sepcification.IsSatisfiedBy(subscription); // then isOk.Should().BeTrue(); FluentAssertions
  • 16. public Subscription CreateOrUpdate( string userName, string email, string organization, DateTime currentMoment) { var guid = Guid.NewGuid().ToString("D", CultureInfo.InvariantCulture); // INSERT INTO subscriptions(....) // VALUES (...) // ON DUPLICATE KEY UPDATE subscription_id = subscription_id var subscription = FindByEmail(email); var isOldRecord = subscription.ConfirmationGuid != guid; if (isOldRecord) { var spec = new SubscriptionCanBeRenewed(currentMoment); if (spec.IsSatisfiedBy(subscription)) { // UPDATE subscriptions SET status = 'ToBeConfirmed', ... // WHERE subscription_id = subscription.id } } return subscription; }
  • 17. Но как теперь тестировать UPDATE? • Можно сделать фабрику для спецификации, инжектить ее в репозиторий, на тестах mock-ать. Но это столько возни… • Лучше продолжим отрывать куски в отдельные классы
  • 18. public interface ICommand<T> { void ExecuteFor(T obj); } internal class SubscriptionRenewCommand : ICommand<Subscription> { public SubscriptionRenewCommand(IDataContext dataContext) { … } public void ExecuteFor(Subscription obj) { // UPDATE subscriptions SET status = 'ToBeConfirmed', ... // WHERE subscription_id = obj.id } }
  • 19. Можно еще сделать «наворот»
  • 20. public static class CommandExtensions { public static ICommand<T> If<T>( this ICommand<T> command, ISpecification<T> condition) { return new IfCommand<T>(command, condition); } private class IfCommand<T> : ICommand<T> { public IfCommand( ICommand<T> command, ISpecification<T> condition) { this.command = command; this.condition = condition; } public void ExecuteFor(T obj) { if (condition.IsSatisfiedBy(obj)) command.ExecuteFor(obj); } private readonly ICommand<T> command; private readonly ISpecification<T> condition; }
  • 21. Тогда код будет выглядеть: new SubscriptionRenewCommand(dataContext) .If(new SubscriptionCanBeRenewed(currentMoment)) .ExecuteFor(subscription); Почти jQuery Или даже монада? :)
  • 22. • Аналогично для выполнения команды в цикле • Для цепочки команд • Если что, можно команды конструировать с объектом контекста (мутабельным)
  • 23. «interface» ISpecification<T> IsSatisfiedBy(obj):bool Composite Specification NotSpecification AndSpecification OrSepcification
  • 24. public static class SpecificationExtensions { public static ISpecification<T> And<T>( this ISpecification<T> left, ISpecification<T> right) { return new AndSpecification(left, right); } public static ISpecification<T> Or<T>( this ISpecification<T> left, ISpecification<T> right) { return new OrSpecification(left, right); } public static ISpecification<T> Not<T>( this ISpecification<T> spec) { return new NotSpecification(spec); } }
  • 26.
  • 29.
  • 30.
  • 31.
  • 32. При использовании TDD логики в не-public методах почти не содержится, да и самих таких методов, обычно, очень мало (тупые хелперы)
  • 34.
  • 35. UsefulClass TinyUsefulClass HelperClass1 HelperClass2 UtilClass
  • 38.
  • 40. • тяжело придумывать имена классов o Правило: чем уже scope использования класса, тем длиннее может быть его имя o Плюс помогают постфиксы из шаблонов проектирования • легко запутаться в таком количестве классов o Помогает (но не спасает) хорошая упаковка классов по пакетам/пространствам имен o см. ниже • большая косвенность кода o современные среды разработки слегка спасают • как организовать взаимодействие? o см. ниже • есть проблемы с областью видимости o Обычно на это плюют…
  • 42. «Old School» Packaging MyMegaLogic MegaClass CoolClass UsefulClass HelperClass
  • 43. «New School» Packaging Cool CoolClass InterfaceOne InterfaceTwo TinyUsefulClass HelperClass1 UtilClass HelperClass2
  • 44. Пространства имен • MyLogic o MegaCool • здесь ваши классы: CoolClass, MegaClass, OtherClass • вложенные пространства имен o Internals.CoolClass o Internals. MegaClass o SomethingOther
  • 45. Увязывание классов между собой
  • 47. #include <stdio.h> class Feature { public: enum FeatureType {eUnknown, eCircle, eTriangle, eSquare}; Feature() : type(eUnknown), points(0) { } ~Feature() { if (points) delete points; } bool isValid() { return type != eUnknown; } bool read(FILE* file) { if (fread(&type, sizeof(FeatureType), 1, file) != sizeof(FeatureType) return false; short n = 0; switch (type) { case eCircle: n = 3; break; case eTriangle: n = 6; break; case eSquare: n = 8; break; default: type = eUnknown; return false; } points = new double[n]; if (!points) return false; return fread(&points, sizeof(double), n, file) == n*sizeof(double); }
  • 48. void draw() { switch (type) { case eCircle: drawCircle(points[0], points[1], points[2]); break; case eTriangle: drawPoligon(points, 6); break; case eSquare: drawPoligon(points, 8); break; } } protected: void drawCircle(double centerX, double centerY, double radius); void drawPoligon(double* points, int size); double* points; FeatureType type; };
  • 49. int main(int argc, char* argv[]) { Feature feature; FILE* file = fopen("features.dat", "r"); feature.read(file); if (!feature.isValid()) return 1; return 0; }
  • 51. «interface» Shape read(file):bool draw() isValid():bool Circle Polygon NullShape -centerX -points[*] -centerY -radius Triangle Square
  • 52. class Feature { public: Feature() : shape(new NullShape()) { } ~Feature() { delete shape; } bool isValid() { return shape->isValid; } bool read(FILE* file) { FeatureType type; if (fread(&type, sizeof(FeatureType), 1, file) != sizeof(FeatureType)) return false; delete shape; switch (type) { case eCircle: shape = new Circle(); break; case eTriangle: shape = new Triangle(); break; case eSquare: shape = new Square(); break; default: shape = new NullShape(); } return shape.read(file); } void draw() { shape->draw(); } private: Shape* shape; enum FeatureType {eUnknown, eCircle, eTriangle, eSquare}; };
  • 53. Этот switch ужасен и нарушает OCP
  • 55. typedef Shape* ShapeConstructor(); class Feature { public: static int registerShape( int uniqueCode, ShapeConstructor* constructor) { shapeMap.insert(std::pair<int, ShapeConstructor*>( uniqueCode, constructor)); return uniqueCode; } //… bool read(File* file) { //... auto it = shapeMap.find(type); shape = it == shapeMap.end() ? new NullShape() : it->second(); return shape->read(file); } //… private: //… static std::map<int, ShapeConstructor*> shapeMap; };
  • 56. Circle.h class Circle : public Shape { // … private: // … static const int _readCodeNotUsed; }; Circle.cpp Shape* circleConstructor() { return new Circle(); } int _readCodeNotUsed = Feature::registerShape(1, &circleConstructor);
  • 57. Если повторять этот подход на Java/C# при помощи статических конструкторов, то надо не забыть в статическом конструкторе Feature принудительно инициировать все классы, наследники от Shape
  • 58. Shape Feature
  • 60. Circle Shape Feature ShapeFactory
  • 61. class Feature { public: //… bool read(File* file) { //... shape = ShapeFactory().createBy(type); return shape->read(file); } //… };
  • 62. typedef Shape* ShapeConstructor(); class ShapeFactory { public: static int registerShapeType( int typeCode, ShapeConstructor* constructor) { shapeMap.insert(std::pair<int, ShapeConstructor*>( typeCode, constructor)); return uniqueCode; } Shape* createBy(int typeCode) { auto it = shapeMap.find(typeCode); return it == shapeMap.end() ? new NullShape() : it->second(); } private: static std::map<int, ShapeConstructor*> shapeMap; };
  • 63. Circle.cpp Shape* circleConstructor() { return new Circle(); } int _readCodeNotUsed = ShapeFactory::registerShapeType(1, &circleConstructor);
  • 65. MyDomain MyPersistance WhereGenerator SomeSpec -visitAnd() -visitOr() -visitSomeSpec()
  • 66. NotSpecification NotVisitorItem OrSpecification OrVisitorItem AndSpecification AndVisitorItem SomeSpec SomeSpecVisitorItem Словарь соответствия
  • 67. Решается специализированным тестом в рамках тестов на MyPersistance: через reflection перебираются все классы из MyDomain, реализующие ISpecification<>, и проверяется, что для каждого из них есть элемент в словаре spec <-> visitorItem
  • 68.
  • 69. Наличие mixin-ов и duck-typing в системе типов сильно бы помогло. См. например, google GO
  • 71. Приёмы • ServiceLocator • Dependency Injection • Callbacks, Events • шаблон HasValue • EventAggregator
  • 72. Event Aggregator • C# : Prism • Java : GWT (EventBus)
  • 73. public interface IEventAggregator { void Register<TMessage>(Action<TMessage> action); void Send<TMessage>(TMessage message); void Unregister<TMessage>(Action<TMessage> action); }
  • 74. Полуминус Event Aggregator-а Скрытая часть API Но параметр EventAggregator eventAggregator в конструкторе какбэ намекает
  • 76.
  • 78. Спасибо за внимание! Вопросы? @bibigine bibigine@gmail.com http://tinyurl.com/bibigine http://www.slideshare.net/bibigine