Стили программирования
Как известно, есть 3 стиля программирования: "инженерный", "программисткий" и "объектный".
Их описания можно посмотреть здесь - ни отнять, ни прибавить. Но интересно не это, а качество кода.
Код пишется не просто так: обычно создаваемые объекты надо потом как-то обрабатывать. Поэтому критериями качества сейчас примем:
- длина кода
- устойчивость работы
- возможность получения созданного объекта
- легкость и прозрачность внесения изменений в объект.
Для конкретизации примера будем создавать объект отрезка и однострочного текста
С одной стороны, эти объекты достаточно просты; с другой - позволяют на своем примере показать основные трудности при их создании. Характеристики создаваемых объектов:
- Отрезок: лежит на слое "СлойОтрезка"; цвет - 1; вес линии - 0,25 мм; тип линии - Непрерывный (Continuous); координаты начала - 0,0,0, координаты конца - 100,10,0.
- Текст: лежит на слое "СлойТекста"; цвет - 2; вес линии - 0,50 мм; тип линии - ПоБлоку; координаты точки вставки - 10,10,0; выравнивание - верх лево (чтобы особо не заморачиваться с вычислениями); содержимое - "Текст"; высота текста - 25 мм независимо от настроек стиля (стиль использовать текущий). Оба объекта лежат в мировой системе координат.
Слои объектов должны быть созданы (если они отсутствуют), настройки объектов жестко заданы и должны быть именно такими независимо ни от чего. После выполнения кода состояние AutoCAD должно вернуться к начальному (касается системных переменных). Для упрощения кода предполагаем работу только внутри пространства модели.
Инженерный подход достаточно легко даст возможность создания того же отрезка (казалось бы): меняется несколько системных переменных с предварительным запоминанием их начального состояния, рисуется отрезок и переменные возвращаются обратно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | (defun lispru-cmd (/ *error* sysvar_lst res) (defun *error* (msg) (if sysvar_lst (fun_restore-sysvar sysvar_lst) ) ;_ end of if (command "_.undo" "_end") (princ msg) (princ) ) ;_ end of defun (defun fun_save-sysvar (lst / res) (foreach item lst (if (getvar (car item)) (progn (setq res (cons (cons (car item) (getvar (car item))) res ) ;_ end of cons ) ;_ end of setq (if (cdr item) (setvar (car item) (cdr item)) ) ;_ end of if ) ;_ end of progn ) ;_ end of if ) ;_ end of foreach res ) ;_ end of defun (defun fun_restore-sysvar (lst) (foreach item lst (vl-catch-all-apply (function (lambda () (setvar (car item) (cdr item)) ) ;_ end of lambda ) ;_ end of function ) ;_ end of vl-catch-all-apply ) ;_ end of foreach ) ;_ end of defun ;; Создание отрезка ;; Сначала запоминаем текущий слой и создаем свой (setq sysvar_lst (fun_save-sysvar '(("clayer")))) (command "_.-layer" "_make" "СлойОтрезка" "") (setq sysvar_lst (append sysvar_lst (fun_save-sysvar '(("cecolor" . "1") ("celweight" . 25) ("celtype" . "Continuous") ) ) ;_ end of fun_save-sysvar ) ;_ end of append ) ;_ end of setq (command "_.line" "_none" '(0. 0. 0.) "_none" '(100. 10. 0.) "") ;_ end of command (setq res (list (entlast))) (fun_restore-sysvar sysvar_lst) (setq sysvar_lst nil) ;; Теперь создаем текст. (setq sysvar_lst (fun_save-sysvar '(("clayer")))) (command "_.-layer" "_make" "СлойТекста" "") (setq sysvar_lst (append sysvar_lst (fun_save-sysvar '(("cecolor" . "2") ("celweight" . 50) ("celtype" . "ByBlock") ) ) ;_ end of fun_save-sysvar ) ;_ end of append ) ;_ end of setq (command "_.text" "_none" '(10. 10. 0.) 25 0 "Текст" "") (setq res (cons (entlast) res)) (fun_restore-sysvar sysvar_lst) (setq sysvar_lst nil) (command "_.undo" "_end") res ) ;_ end of defun |
На самом деле в этом коде есть несколько весьма коварных подводных камней. Во-первых, в "чистом" файле такой код корректно сработает. Но, допустим, слои объектов уже есть. Правда, один из них заблокирован, а второй - заморожен. В результате функция выполнена не будет. Во-вторых, команда создания текста сделана под определенные настройки текстового стиля - с 0 высотой (то есть выполняется запрос высоты создаваемого текста). И, наконец, в-третьих (хотя это зачастую и не актуально, но предупредить надо): работа выполняется в текущей системе координат и в текущем пространстве. Что тоже придется отслеживать.
Если код пишется "под себя", то можно и "пофилонить", оставив все как есть. Если же есть хотя бы слабый шанс, что кодом будет пользоваться кто-то еще, придется вводить дополнительные проверки на состояние слоев, установленную высоту текстового стиля и т.п.
Теперь рассмотрим пример решения задачи в "программистком" стиле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | (defun lispru-entmake (/ res) (command "_.undo" "_begin") ;; Создаем отрезок (setq res (list (entmakex '((0 . "LINE") (10 0. 0. 0.) (11 100. 10. 0.) (8 . "СлойОтрезка") (62 . 1) (6 . "Continuous") (370 . 25) (210 0. 0. 1.) ) ) ;_ end of entmakex ;; Создаем текст (entmakex (list '(0 . "TEXT") '(100 . "AcDbEntity") '(100 . "AcDbText") '(10 10. 10. 0.) '(1 . "Текст") '(62 . 2) '(370 . 50) '(6 . "ByBlock") '(40 . 25) (cons 7 (getvar "textstyle")) '(210 0. 0. 1.) ) ;_ end of list ) ;_ end of entmakex ) ;_ end of list ) ;_ end of setq (command "_.undo" "_end") res ) ;_ end of defun |
Казалось бы, матерщина матерщиной. На самом деле это не так: просто создается список представления примитива и подставляется в качестве аргумента стандартной функции создания примитива entmakex. Необходимо просто проштудировать DXF Reference в составе справки (не локализовывается начиная как минимум с версии 2002). Код сделан так, что создание объектов выполняется в текущем пространстве, но зато гарантированно в мировой системе координат.
И, наконец "объектный" стиль.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | (defun lispru-vla (/ adoc *error* res) (vl-load-com) (vla-startundomark (setq adoc (vla-get-activedocument (vlax-get-acad-object))) ) ;_ end of vla-startundomark ;; Сначала создадим слои, чтобы не возвращаться ;; Создавать будем "по-лисповски", то есть достаточно извращенно ;; на неподготовленный взгляд (foreach item '("СлойОтрезка" "СлойТекста") (if (vl-catch-all-error-p (vl-catch-all-apply (function (lambda () (vla-item (vla-get-layers adoc) item) ) ;_ end of lambda ) ;_ end of function ) ;_ end of vl-catch-all-apply ) ;_ end of vl-catch-all-error-p (vla-add (vla-get-layers adoc) item) ) ;_ end of if ) ;_ end of foreach (setq res (list ((lambda (/ ent) (setq ent (vla-addline (vla-get-modelspace adoc) (vlax-3d-point '(0. 0. 0.)) (vlax-3d-point '(100. 10. 0.)) ) ;_ end of vla-addline ) ;_ end of setq (vla-put-color ent 1) (vla-put-lineweight ent aclnwt025) (vla-put-linetype ent "Continuous") (vla-put-layer ent "СлойОтрезка") ent ) ;_ end of lambda ) ((lambda (/ ent) (setq ent (vla-addtext (vla-get-modelspace adoc) "Текст" (vlax-3d-point '(10. 10. 0.)) 25 ) ;_ end of vla-addtext ) ;_ end of setq (vla-put-color ent 2) (vla-put-lineweight ent aclnwt050) (vla-put-linetype ent "ByBlock") (vla-put-normal ent (vlax-3d-point '(0. 0. 1.))) (vla-put-insertionpoint ent (vlax-3d-point '(10. 10. 0.))) (vla-put-layer ent "СлойТекста") ent ) ;_ end of lambda ) ) ;_ end of list ) ;_ end of setq (vla-endundomark adoc) res ) ;_ end of defun |
Тут уже участвуют вроде бы понятные слова (ну да, на английском), а не странные цифры. Но длина кода становится угрожающей. Да еще и дополнительные действия какие-то... И все равно - код не является устойчивым и "дураказащитным" (в частности, не отслеживается блокированность текущего слоя).
Сравним скорость работы по методике, описанной здесь (предупреждаю сразу - время проверки весьма существенно):
1 2 3 4 5 6 | _$ (BENCHMARK '((lispru-cmd) (lispru-entmake) (lispru-vla))) Benchmarking ..........Elapsed milliseconds / relative speed for 128 iteration(s): (LISPRU-VLA)..........1360 / 36.31 <fastest> (LISPRU-ENTMAKE).....23438 / 2.11 (LISPRU-CMD).........49375 / 1 <slowest> |
Результат достаточно ожидаем, т.к. в функции lispru-entmake используется командный метод установки точек отмены. А если его заменить на ActiveX?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | (defun lispru-entmake2 (/ res adoc) (vl-load-com) (vla-startundomark (setq adoc (vla-get-activedocument (vlax-get-acad-object))) ) ;_ end of vla-StartUndoMark ;; Создаем отрезок (setq res (list (entmakex '((0 . "LINE") (10 0. 0. 0.) (11 100. 10. 0.) (8 . "СлойОтрезка") (62 . 1) (6 . "Continuous") (370 . 25) (210 0. 0. 1.) ) ) ;_ end of entmakex ;; Создаем текст (entmakex (list '(0 . "TEXT") '(100 . "AcDbEntity") '(100 . "AcDbText") '(10 10. 10. 0.) '(1 . "Текст") '(62 . 2) '(370 . 50) '(6 . "ByBlock") '(40 . 25) (cons 7 (getvar "textstyle")) '(210 0. 0. 1.) ) ;_ end of list ) ;_ end of entmakex ) ;_ end of list ) ;_ end of setq (vla-endundomark adoc) res ) ;_ end of defun |
Повторно выполняем проверку:
1 2 3 4 5 6 7 | _$ (BENCHMARK '((lispru-cmd) (lispru-entmake) (lispru-entmake2) (lispru-vla))) Benchmarking ...........Elapsed milliseconds / relative speed for 256 iteration(s): (LISPRU-ENTMAKE2).......1875 / 54.75 <fastest> (LISPRU-VLA)............5593 / 18.35 (LISPRU-ENTMAKE).......53500 / 1.92 (LISPRU-CMD)..........102657 / 1 <slowest> |
Ситуация изменилась, причем достаточно серьезно. Все объяснимо: в entmake сразу создается примитив со всеми настройками. При использовании же ActiveX примитив сначала надо создать, потом последовательно ему задать все (ну или почти все) параметры, причем некоторые по два раза. Это все равно что сравнивать просьбу "поставь чайник" и "пойди на кухню, проверь заполненность чайника, в случае недостаточности открой водопроводный кран, проверь температуру воды, налей в чайник воды, поставь чайник на плиту, зажги огонь, вернись и доложи об исполнении".
Тем не менее: entmake и entmakex функции удобны при создании примитивов. entmod далеко не всегда удобен при изменении примитива (хотя я знаком с человеком, который с помощью этих функций творит чудеса). Но ent*-функции неприменимы при работе не в текущем документе. Они также не позволят, например, прямо из lisp-кода вызвать, к примеру, MS Excel и в новом документе Excel'a что-то заполнить.
Для областей, где применение ent*-функций становится неудобным либо невозможным, приходится применять vla*-функции.
Подводя некоторые итоги, можно сказать:
- инженерный подход прекрасно годится для начального изучения lisp. Но написать на нем устойчиво работающий код без привлечения понимания внутренностей dwg весьма проблематично. Кроме того, является самым медленным из возможных.
- программистский подход практически идеален для работы в текущем документе. Требует очень хорошего знания DXF.
- объектный метод позволяет работать не только с текущим документом, разрешает вызывать практически любые сторонние приложения (уточнение: приложения, реализующие СОМ-модель). Достаточно удобен в плане понимания кода. Запросто может проиграть в скорости выполнения программистскому методу.
К сожалению, сайт cad.kurganobl.ru закрылся Поэтому описание стилей программирования приведу здесь. Текст взят из книги "САПР на базе AutoCAD - как это делается" (С.Зуев, Н.Полещук, при участии П.Лоскутова):
"Инженерный" стиль.
Этот стиль программирование, прежде всего, отличается почти исключительным использованием функции command для создания и редактирования объектов. < ...> До версии AutoCAD R11 создавать объекты можно было только с помощью функции command, это было просто и наглядно. Некоторое неудобство былов том, что существовали версии AutoCAD на разных языках с локализованными командами, и приходилось писать разные версии программ. Кроме того, в ранних версиях AutoCAD наблюдалось некоторое несоответствие опций команд. После появления локализованных и оригинальных имен команд (добро пожаловать в поиск по сайту) ситуация стабилизировалась.
При использовании функции command разработчик должен точно знать имена команд, опции команд и возможные их варианты, т.к. он должен передать в функцию абсолютно точную последовательность ответов на возможные варианты запросов команды.
"Программисткий" стиль
Так стиль характеризуется тем, что при работе с функцией entmake разработчик собирает в ассоциативный список информацию о координатах, слое и цвете объекта, а затем передает все это в AutoCAD для построения. Изменение свойств объектв осуществляется функцией entmod с передачей ей аналогичного списка. < ...> Работа со списками полностью соответствует идеологии языка LISP, придает программам большую гибкость и выигрыш в скорости работы. Но разработчик должен знать уже гораздо больше о структуре данных, прежде всего DXF-коды различных объектов, и должен уметь свободно оперировать со списками.
"Объектный" стиль
После внедрения в AutoCAD технологии ActiveX начал формироваться "объектный" стиль программирования. Объектный стиль характеризуется тем, что разработчик работает не с именами и опциями команд и не с ассоциированными списками свойств различных объектов, а с объектной моделью AutoCAD через функции ActiveX. Создание объектов в VisualLISP с помощью функций ActiveX является самым современным (книга 2004 года) из имеющихся методов. Это действительно так, если под "современным" понимать новый или "молодой". Считается, что технология ActiveX обладает рядом преимуществ по сравнению с использованием функций command и entmake, потому что:
- функции ActiveX работают быстрее, чем функции command
- имена функций ActiveX обозначают действия, которые они выполняют, что обеспечивает более удобное чтение, обновление и исправление программ.