Роль декомпозиции    функционалана отдельные классыпри следовании TDD     Бибичев Андрей      декабрь 2011
@bibigine                     Андрей Бибичев• E-mail:       bibigine@gmail.com• Twitter:      @bibigine• Profile:      htt...
Пример
ToBe                                                 Confirmed      Subscription                                      Пись...
Что делать, если ввели email,который уже есть?
public class AllSubscriptions : IAllSubscriptions{    ...    public Subscription CreateOrUpdate(        string userName, s...
И так попадет в    Это старая                                        систему рассылки      запись                         ...
Тестировать такой метод ой как неприятно и муторно
Лозунг TDD           тестируйРазделяй и властвуй
Specification ISpecification<Subscription>SubscriptionCanBeRenewedIsSatisfiedBy(obj): bool
public class SubscriptionCanBeRenewed : ISpecification<Subscription>{    public const int PARASITIC_RELOAD_INTERVAL_IN_MIN...
Тестировать такой класс –   одно удовольствие
// givenvar subscription = new Subscription    {         Status = SubscriptionStatus.Confirming,         MailedAt = new Da...
Возвращаемся к нашему «барану»
public Subscription CreateOrUpdate(    string userName, string email, string organization,    DateTime currentMoment){    ...
Но как теперь тестировать UPDATE?• Можно сделать фабрику для спецификации,  инжектить ее в репозиторий, на тестах mock-ать...
public interface ICommand<T> { void ExecuteFor(T obj); }internal class SubscriptionRenewCommand : ICommand<Subscription>{ ...
Можно еще сделать «наворот»
public static class CommandExtensions{    public static ICommand<T> If<T>(        this ICommand<T> command, ISpecification...
Тогда код будет выглядеть:new SubscriptionRenewCommand(dataContext)    .If(new SubscriptionCanBeRenewed(currentMoment))   ...
• Аналогично для выполнения команды в цикле• Для цепочки команд• Если что, можно команды конструировать с  объектом контек...
«interface»               ISpecification<T>             IsSatisfiedBy(obj):bool                     Composite             ...
public static class SpecificationExtensions{    public static ISpecification<T> And<T>(        this ISpecification<T> left...
«Философия»мелких классов
Sprout Method Sprout Class
someMethod(…)
При использовании TDD    логики в не-public методах       почти не содержится,да и самих таких методов, обычно,   очень ма...
Sprout Class
UsefulClass                     TinyUsefulClassHelperClass1          HelperClass2                 UtilClass
Математика• Теорема• Лемма
Автопром
Проблемыс мелкими классами
• тяжело придумывать имена классов  o Правило: чем уже scope использования класса, тем длиннее    может быть его имя  o Пл...
«Упаковка»мелких классов
«Old School» Packaging       MyMegaLogic         MegaClass         CoolClass        UsefulClass   HelperClass
«New School» Packaging                 Cool                CoolClass               InterfaceOne               InterfaceTwo...
Пространства имен• MyLogic o MegaCool    • здесь ваши классы:      CoolClass, MegaClass, OtherClass   • вложенные простран...
Увязывание классов   между собой
Задачка от Yandex
#include <stdio.h>class Feature {  public:    enum FeatureType {eUnknown, eCircle, eTriangle, eSquare};    Feature() : typ...
void draw() {       switch (type) {         case eCircle:           drawCircle(points[0], points[1], points[2]);          ...
int main(int argc, char* argv[]) {  Feature feature;  FILE* file = fopen("features.dat", "r");  feature.read(file);  if (!...
Шаг №1
«interface»                  Shape             read(file):bool             draw()             isValid():bool  Circle      ...
class Feature {  public:    Feature() : shape(new NullShape()) { }    ~Feature() { delete shape; }    bool isValid() { ret...
Этот switch ужасен и нарушает OCP
Шаг №2
typedef Shape* ShapeConstructor();class Feature {  public:    static int registerShape(        int uniqueCode, ShapeConstr...
Circle.hclass Circle : public Shape {     // …   private:     // …     static const int _readCodeNotUsed;};Circle.cppShape...
Если повторять этот подход на                Java/C#при помощи статических конструкторов,           то надо не забыть  в с...
Shape   Feature
Шаг №3
Circle      Shape       Feature         ShapeFactory
class Feature {  public:    //…     bool read(File* file) {        //...        shape = ShapeFactory().createBy(type);    ...
typedef Shape* ShapeConstructor();class ShapeFactory {  public:    static int registerShapeType(                 int typeC...
Circle.cppShape* circleConstructor() { return new Circle(); }int _readCodeNotUsed =         ShapeFactory::registerShapeTyp...
ОДНАКО
MyDomain        MyPersistance              WhereGeneratorSomeSpec              -visitAnd()              -visitOr()        ...
NotSpecification              NotVisitorItemOrSpecification               OrVisitorItemAndSpecification              AndVi...
Решается специализированным тестом   в рамках тестов на MyPersistance:     через reflection перебираются       все классы ...
Наличие 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 me...
Полуминус      Event Aggregator-аСкрытая часть API Но параметр EventAggregator eventAggregator в конструкторе какбэ намекает
Итого
А почему/зачем?
Спасибо за внимание!        Вопросы?              @bibigine        bibigine@gmail.com      http://tinyurl.com/bibigine  ht...
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Tdd and decomposition
Upcoming SlideShare
Loading in …5
×

Tdd and decomposition

1,928 views

Published on

Presentation for XPDays Ukraine

Published in: Technology
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,928
On SlideShare
0
From Embeds
0
Number of Embeds
474
Actions
Shares
0
Downloads
26
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Tdd and decomposition

  1. 1. Роль декомпозиции функционалана отдельные классыпри следовании TDD Бибичев Андрей декабрь 2011
  2. 2. @bibigine Андрей Бибичев• E-mail: bibigine@gmail.com• Twitter: @bibigine• Profile: http://tinyurl.com/bibigine• Slideshare: http://www.slideshare.net/bibigine
  3. 3. Пример
  4. 4. ToBe Confirmed Subscription Письмо отправлено Ошибка отправкиUserName : String письмаEmail : StringOrganization : String ConfirmingStatus «Жмакнута» ErrorConfirmationGuid ссылкаMailedAt : DateTime? Confirmed Перегружено в систему рассылки Subscribed
  5. 5. Что делать, если ввели email,который уже есть?
  6. 6. 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; }
  7. 7. И так попадет в Это старая систему рассылки запись И так должен получить письмо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
  8. 8. Тестировать такой метод ой как неприятно и муторно
  9. 9. Лозунг TDD тестируйРазделяй и властвуй
  10. 10. Specification ISpecification<Subscription>SubscriptionCanBeRenewedIsSatisfiedBy(obj): bool
  11. 11. 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;}
  12. 12. Тестировать такой класс – одно удовольствие
  13. 13. // givenvar 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);// whenvar isOk = sepcification.IsSatisfiedBy(subscription);// thenisOk.Should().BeTrue(); FluentAssertions
  14. 14. Возвращаемся к нашему «барану»
  15. 15. 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;}
  16. 16. Но как теперь тестировать UPDATE?• Можно сделать фабрику для спецификации, инжектить ее в репозиторий, на тестах mock-ать. Но это столько возни…• Лучше продолжим отрывать куски в отдельные классы
  17. 17. 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 }}
  18. 18. Можно еще сделать «наворот»
  19. 19. 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; }
  20. 20. Тогда код будет выглядеть:new SubscriptionRenewCommand(dataContext) .If(new SubscriptionCanBeRenewed(currentMoment)) .ExecuteFor(subscription); Почти jQuery Или даже монада? :)
  21. 21. • Аналогично для выполнения команды в цикле• Для цепочки команд• Если что, можно команды конструировать с объектом контекста (мутабельным)
  22. 22. «interface» ISpecification<T> IsSatisfiedBy(obj):bool Composite SpecificationNotSpecification AndSpecification OrSepcification
  23. 23. 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); }}
  24. 24. «Философия»мелких классов
  25. 25. Sprout Method Sprout Class
  26. 26. someMethod(…)
  27. 27. При использовании TDD логики в не-public методах почти не содержится,да и самих таких методов, обычно, очень мало (тупые хелперы)
  28. 28. Sprout Class
  29. 29. UsefulClass TinyUsefulClassHelperClass1 HelperClass2 UtilClass
  30. 30. Математика• Теорема• Лемма
  31. 31. Автопром
  32. 32. Проблемыс мелкими классами
  33. 33. • тяжело придумывать имена классов o Правило: чем уже scope использования класса, тем длиннее может быть его имя o Плюс помогают постфиксы из шаблонов проектирования• легко запутаться в таком количестве классов o Помогает (но не спасает) хорошая упаковка классов по пакетам/пространствам имен o см. ниже• большая косвенность кода o современные среды разработки слегка спасают• как организовать взаимодействие? o см. ниже• есть проблемы с областью видимости o Обычно на это плюют…
  34. 34. «Упаковка»мелких классов
  35. 35. «Old School» Packaging MyMegaLogic MegaClass CoolClass UsefulClass HelperClass
  36. 36. «New School» Packaging Cool CoolClass InterfaceOne InterfaceTwo TinyUsefulClassHelperClass1 UtilClassHelperClass2
  37. 37. Пространства имен• MyLogic o MegaCool • здесь ваши классы: CoolClass, MegaClass, OtherClass • вложенные пространства имен o Internals.CoolClass o Internals. MegaClass o SomethingOther
  38. 38. Увязывание классов между собой
  39. 39. Задачка от Yandex
  40. 40. #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); }
  41. 41. 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;};
  42. 42. int main(int argc, char* argv[]) { Feature feature; FILE* file = fopen("features.dat", "r"); feature.read(file); if (!feature.isValid()) return 1; return 0;}
  43. 43. Шаг №1
  44. 44. «interface» Shape read(file):bool draw() isValid():bool Circle Polygon NullShape-centerX -points[*]-centerY-radius Triangle Square
  45. 45. 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};};
  46. 46. Этот switch ужасен и нарушает OCP
  47. 47. Шаг №2
  48. 48. 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;};
  49. 49. Circle.hclass Circle : public Shape { // … private: // … static const int _readCodeNotUsed;};Circle.cppShape* circleConstructor() { return new Circle(); }int _readCodeNotUsed = Feature::registerShape(1, &circleConstructor);
  50. 50. Если повторять этот подход на Java/C#при помощи статических конструкторов, то надо не забыть в статическом конструкторе Featureпринудительно инициировать все классы, наследники от Shape
  51. 51. Shape Feature
  52. 52. Шаг №3
  53. 53. Circle Shape Feature ShapeFactory
  54. 54. class Feature { public: //… bool read(File* file) { //... shape = ShapeFactory().createBy(type); return shape->read(file); } //…};
  55. 55. 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;};
  56. 56. Circle.cppShape* circleConstructor() { return new Circle(); }int _readCodeNotUsed = ShapeFactory::registerShapeType(1, &circleConstructor);
  57. 57. ОДНАКО
  58. 58. MyDomain MyPersistance WhereGeneratorSomeSpec -visitAnd() -visitOr() -visitSomeSpec()
  59. 59. NotSpecification NotVisitorItemOrSpecification OrVisitorItemAndSpecification AndVisitorItem SomeSpec SomeSpecVisitorItem Словарь соответствия
  60. 60. Решается специализированным тестом в рамках тестов на MyPersistance: через reflection перебираются все классы из MyDomain, реализующие ISpecification<>,и проверяется, что для каждого из них есть элемент в словаре spec <-> visitorItem
  61. 61. Наличие mixin-ов и duck-typingв системе типов сильно бы помогло. См. например, google GO
  62. 62. «Общение»мелких классов между собой
  63. 63. Приёмы• ServiceLocator• Dependency Injection• Callbacks, Events• шаблон HasValue• EventAggregator
  64. 64. Event Aggregator• C# : Prism• Java : GWT (EventBus)
  65. 65. public interface IEventAggregator{ void Register<TMessage>(Action<TMessage> action); void Send<TMessage>(TMessage message); void Unregister<TMessage>(Action<TMessage> action);}
  66. 66. Полуминус Event Aggregator-аСкрытая часть API Но параметр EventAggregator eventAggregator в конструкторе какбэ намекает
  67. 67. Итого
  68. 68. А почему/зачем?
  69. 69. Спасибо за внимание! Вопросы? @bibigine bibigine@gmail.com http://tinyurl.com/bibigine http://www.slideshare.net/bibigine

×