ФЭНДОМ


(Оригинал. Автор - Alex Baran.)

У меня были тесты, в которых надо было создавать графы, потом проводить над этими графами некоторые манипуляции, а потом сверять результат. Сначала для создания ребра я писал нечто вроде v1 refTo: v2 weight: 100. Но даже при самых небольших графах писать приходилось много. Да и понять, какие ребра откуда выходят/куда входят ... по такой записи было тяжело.

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

etalonGraph
       | mb |
       mb := MatrixBuilder new.

       mb  |  v1 |  v2 |  v3  |  v4 |  v5  !
       v1  |  0  |  1  |  -3  |  2  |  -4  !
       v2  |  3  |  0  |  -4  |  1  |  -1  !
       v3  |  7  |  4  |  0   |  5  |  3   !
       v4  |  2  | -1  |  -5  |  0  |  -2  !
       v5  |  8  |  5  |  1   |  6  |  0   .

       ^mb matrix graph

Создавать такую матрицу просто - для выравнивания я использую табуляцию.

v1, v2, ... это вершины графа, т.е. объекты класса GraphNode. Суть в том что стобцом, строкой, и значением в матрице может быть любой объект. v1, v2, ... - также instance являются instance переменными теста

| , ! - это сообщения, посылаемые matrix builder-у. Например matrixBuilder | v1, аналогично вызову matrixBuilder addColumn: v1. Эти сообщения возвращают matrix builder, поэтому все последующие вызовы этих сообщений опять же идут к matrix builder-у. | - добавляет новый столбец или вбивает значение в матрицу. ! - обозначает конец строки, а также добавляет строку к матрице.

В конце теста граф, как правило, проверялся относительно другой, эталонной, матрицы.

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

testSmoke
       tb := TableBuilder newFor: applicationModel.

       tb |  #firstName  |  #lastName   |  #company    !
       1  |  'Иванов'    |  'Сергей'    |  'Company1'  !
       2  |  'Петров'    |  'Александр' |  'Company1'  .

       tb play.

tb play проигрывает наш тест. tb play знает applicationModel(некий аналог формы) и может автоматом заносить данные прямо в адаптеры, например

firstName value: 'Иванов'
lastName value: 'Сергей'

На самом деле внутри это будет выглядить так: (applicationModel perform: columnName) value: cellValue. Интересная особенность в том, что при просмотре ссылок на метод firstName мы увидим метод testSmoke. Также при переименовании метода firstName RefactoringBrowser? будет переименовывать #firstName внутри метода testSmoke. Но оказалось, что такой тест достаточно бесполезен. Дело в том, что это smoke тест. В нем нет никаких проверок. Тогда я подумал, что проверки надо делать именно в матрице - в еще одном столбце. Теперь я думаю использовать примерно такую матрицу:

testSmoke
       tb := TableBuilder newFor: applicationModel testCase: self.
       tb | #firstName | #lastName | #company   **  'error'               !
       1  | 'Иванов'   | 'Сергей'  | 'Company1' |   nil                   !
       2  | 'Иванов'   | 'Сергей'  | 'Company1' |   PossibleDoublingError .
       tb play.
    • - это новый метод (может звучать очень странно для тех, кто не знаком с Smalltalk-ом). Этот метод говорит нашему tableBuilder-у, что столбец 'error' содержит класс ошибки, которую необходимо ожидать при забивании строки. При tb play будет происходит нечто вроде:
self 
   should: [(self applicationModel perform: columnName) value: cellValue] 
   raise: errorClass

Опять к графам. На самом деле представлять графы только лишь в виде матрицы мне тоже было не очень удобно. В дебагере я постоянно запускал рисовалку графа и постоянно перетягивать вершины, чтобы получить человечесикй layout. Было бы неплохо если бы в тесте граф хранился как в виде матрицы, так и графически. Тогда я понял, что для этого надо не так много. Матрица есть. На ее основании можно построить граф. Не хватает только координат вершин.

graphAndPoints
       | gb |
       gb := GraphBuilder new.
       gb |  v1  |  v2  |  v3  |  v4   !
       v1 |  nil |  nil |  nil |  nil  !
       v2 |  nil |  nil |  10  |  10   !
       v3 |  nil |  10  |  nil |  nil  !
       v4 |  10  |  nil |  10  |  nil  .
       ^ gb graph -> (#() + (359@191) + (165@62) + (49@194) + (244@185))

359@191 - это координата вершина v1, 165@62 - вершины v2 и т.д. Я не указываю координаты вручную. Просто я добавил в рисовалку графов возможность сохранить эти координаты в методе. Я также могу попросить рисовалку выдать мне только строчку с координатами и вставить ее в ручную, например если я не хочу потерять изменения в методе. Я также могу нарисовать граф в рисовалке с нуля и попросить рисовалку создать метод. Опять же, я могу править этот метод. Фактически это два представления одной и той же модели. Так же, как спецификации GUI и сам GUI. Только это код, а не текст.

Идея матриц продолжает жить. Недавно я опять обратился к матрицам. На этот раз мне нужна была матрица с вычисляемым столбцом. В этой матрице я могу указывать вычислимое поле, в котором у меня просто Smalltalk-выражение. Например простую операцию \\ (остаток от деления) можно представить матрицой:

mb |  a  |  b |  (a \\ b) !
1  |  10 |  2 |  nil      !
2  |  10 |  3 |  nil      !
3  |  10 |  4 |  nil      .

matrix compute вернет уже вычисленную матрицу

mb |  a  |  b |  (a \\ b) !
1  |  10 |  2 |  0      !
2  |  10 |  3 |  1      !
3  |  10 |  4 |  2      .

Вместо a\\b может быть записанно любое выражение на Smalltalk-е. В этом выражении можно использовать любые переменные, которые видны из метода, где определенна матрица. Такая простая матрица большого интереса не представляет. К тому же она имеет недостаток. Она ничего не проверяет. Опять же ее можно использовать только в smoke тестах. Полезной могла бы оказалатся матрица, которая бы являлась одновременно тестом. Для этого надо сразу проставить вычисляемое поле. Матрица будет производить вычисления и сверять результат с значением в поле. Если результат расходится матрица сообщает об ошибке (либо Exception либо, если матрица имеет ссылку на testCase, она говорит об ошибке прямо testCase-у).

Отступление. В принципе, можно засовывать в матрицу любые объекты. Например можно засовывать блоки или вызовы сообщений. Можно например представить GUI действия как блоки, а в матрице комбинировать эти действия. Писать новые Builder-ы, которые будут с этим работать. Будет это удобно или нет - я пока не представляю. Здесь весь вопрос в удобстве написания, читаемости и понятности кода. Может что-то из этого и получится :). Вся суть даже не в матрицах как таковых, а в нахождении более простой формы. В Smalltalk-е легко изменить то, что в других языках является синтаксисом. В Self-е выполнять подобные манипуляции должно быть еще проще. В Self-е одно зарезервированное слово и отстутсвие классов :). Мечта - есть объекты и нет классов :-). Также интересно узнать другие, альтернативные способы записывания тестов.

Alex Baran