О магическом подходе к программированию

Позволю себе цитату из [info]vit_r:

The great wizard Yasha lives in the Magical World of IT. He discovers Rotten Code by smell and applies the mysterious Refactoring Spell. The software becomes better. At least this is what other great wizards and their magical books say.

И ее же перевод на человеческий язык:

Yasha reads some old source code, he finds dependencies, connections and logical errors which where unknown or overlooked in the time when this code was written, than Yasha simply updates the source code to the level of his current understanding.

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

На днях в комментариях был озвучен другой пример «волшебства». Для начала: что не так в этом коде (PHP, если кто не догадался)?

$name = isset($_GET['name']) ? $_GET['name'] : 1;
$link = new dbLink();
$list = new listWidget($link, "SELECT name, age, class 
                               FROM students 
                               WHERE name='%s';",
                              $name);
$list->show();

Ответ: пока мы не увидим конструктора listWidget и кода, строящего SQL-запрос, мы не можем говорить о корректности этого кода. Правда, комментаторы отчего-то решили, что конструктор listWidget — это что-то типа sprintf, и пришедшее извне $name вставляется в SQL-запрос безо всякой обработки. Более того, в ответ на комментарий, что все тут пучком, kettle безапелляционно заявил:

Данные надо проверять на входе, а не в середине.

Как только появляется такая вот безапелляционность — то надо задуматься, а не столкнулись ли мы с очередным шаманским рецептом? Сейчас я попытаюсь популярно объяснить, что как и любое шаманство, он проистекает из непонимания ситуации и некритического следования готовым решениям.

Для начала — обрисуем проблему. Если $_GET[‘name’] — это что-то нормальное, типа John или Jack — то при подстановке его вместо %s в шаблон запроса мы получим что-то типа

SELECT name, age, class 
FROM students 
WHERE name='John';

exploits_of_a_mom

Что будет, если зловредные родители назовут своего ребенка каким-то экзотическим именем? Little Bobby Tables, говорят, ходит по нашей земле, и при подстановке простого американского имени Robert’; DROP TABLE students;— мы получим вот такую штуку:

SELECT name, age, class 
FROM students 
WHERE name='Robert'; DROP TABLE students;--';

Вместо одного запроса мы получаем два, второй из которых может содержать все, что угодно — например, как тут, удалять одну из таблиц в БД. Проблема эта довольно «популярная», обросшая большим количеством «доморощенных» решений, и очень неплохо освещенная, например, в презентации SQL Injection Myths and Fallacies.

Таким «решением» в кавычках можно назвать и предложение kettle. Дело в том, что в простой ситуации «принять параметры, сделать запрос к БД, вывести результат» мы имеем не один тип данных — текстовую строку, а три разных — «пользовательский» ввод, запрос к БД и HTML-код выводимой страницы. PHP до неприличия упрощает ситуацию, объединяя эти совершенно разнородные вещи.

SQL Injection не имеет ничего общего с «проверкой входных данных». И «John», и «O’Brien», и «<script type=text/javascript>alert(‘Pwned!’);</script>», и «Robert’; DROP TABLE students;—» — это прекрасные нерусские имена, которые могут и храниться в базе данных, и выводиться на страничку, и фигурировать в запросах пользователей. Задача программиста — сделать так, чтобы и в виде запроса от пользователя, и в БД, и на страничке, выводимой скриптом, они отображались одинаково. Как говорил В. И. Ленин, «величайшей ошибкой было бы думать», что примеры из книжки «PHP для начинающих» имеют какое-либо отношение к реальности. Использование одних и тех же данных безо всяких преобразований и в роли «ввода», и в запросах к БД, и для вывода — это чудовищный «антипаттерн», причем культивируемый во всех учебниках.

Представим себе несколько извращенную и надуманную ситуацию. Предположим, что ввод от пользователя к нам приходит в кодировке КОИ-8, БД работает только с CP866, а для вывода мы должны использовать CP1251. Я думаю, любой русскоговорящий программист поймет, что здесь минимум в двух местах нужно осуществить соответствующие преобразования. Но ведь и в «обычном» случае мы имеем дело с такой ситуацией! «Алфавит» и «семантика» текста в $_GET, SQL-запросах и HTML-коде — совершенно разные, и похожи они лишь отдаленно. Собственно, игнорирование необходимости преобразования между ними — это причина самых распространенных ошибок в веб-программировании.

Кстати, предлагаю посмотреть, как устроены SQL-запросы с параметрами в «более других» драйверах для работы с БД в PHP:

http://bobby-tables.com/php.html

Как легко видеть, я просто «изобрел велосипед», практически заново переизобретя уже готовые решения. Не вижу в этом ничего дурного :)

О магическом подходе к программированию: 18 комментариев

  1. Я не очень вкуриваю в php (то есть вообще не вкуриваю, если честно), так что сейчас, возможно, напишу глупость. Но если бы я работал со строками, требующими проверки, то я бы просто сделал подтип «проверенной строки» для стандартного типа строк, и объявлял бы функции, требующих проверенных строк так, чтобы они принимали только этот тип. Ну и к стандартному типу строк написал бы расширение типа .GetSafe().

    1. Да тут на самом деле пох, PHP тут или не PHP. В чем-то «более типизированном», типа C++ или Java можно сделать и так — унаследовать от стандартного строкового типа разные новые, типа SQLParamString, HTMLOutputString и т. п., и определить для них соответствующие конструкторы (.GetSafe() — ну это же отстой, надо делать конструктор Safe(Unsafe s)) — но тут мы всецело полагаемся на механизмы языка. Кроме того, применительно к PHP — ну да, проблема с формированием SQL решается хотя бы частично — но как быть с выводом HTML-кода? Иногда мы хотим вывести конструкции HTML, иногда — просто строки со значками «больше» и «меньше».

      «На самом деле» любые подобные проблемы устраняются (само)дисциплиной программиста. Всякого рода фреймворки и прочая муть — это всего лишь способ добиться этой самодисциплины в «добровольно-принудительном» порядке. Вот в моей предыдущей записи — типичный пример таких «самоограничений». Если в C отказаться от многих его возможностей, и писать функции только типа list* f(list* args, list* env) — то мы получим убогое подобие Lisp, и даже получим кое-какие преимущества — но понимание того, как эти преимущества достигаются, позволяет обойтись и без таких радикальных мер.

      1. > Да тут на самом деле пох, PHP тут или не PHP.

        Ну я буквально даже синтаксис не могу прочитать, поэтому на всякий случай от PHP открестился.

        > В чем-то “более типизированном”, типа C++ или Java можно сделать и так – унаследовать от стандартного строкового типа разные новые, типа SQLParamString, HTMLOutputString и т. п., и определить для них соответствующие конструкторы

        Ну, собственно, именно это я и имел в виду.

        > (.GetSafe() – ну это же отстой, надо делать конструктор Safe(Unsafe s)) – но тут мы всецело полагаемся на механизмы языка.

        Ну, тут зависит от языка, да. Например, в C# эта конструкция вполне нормальна. В Objective C, например, присутствуют на равных правах две конструкции, типа [[someType alloc] initAsShit] или [someType newShit], различаются только работой с retain/release.

        > как быть с выводом HTML-кода?

        А вот про этом я забыл совершенно. Но вообще, мне (как человеку, который никогда не писал ничего, связанного с WWW, так что это априори мнение идиота снаружи) кажется, что код контроллера (MVC же, все дела) априори не должен ничего знать про тэги разметки, а код viewer’а не должен заморачивать себе голову про санитизирование ввода и так далее. Ну то есть, по мне, на сервере должен сочиняться XML/json, а на клиенте он уже должен раскладываться в красивые формы, вообще желательно с помощью другого языка — того же JS, например. Но, повторюсь, у меня просто афигенно приблизительное представление об этой предметной области.

        > “На самом деле” любые подобные проблемы устраняются (само)дисциплиной программиста.

        Я тут с тобой соглашусь скорее в том, что вообще эти дебаты — вопрос подхода к дисциплине. Ребята из условного C-лагеря (к которым я отношу и тебя) обычно считают, что у программиста должна максимальная свобода, но он не должен быть идиотом, и не видят ничего страшного в возможности отстрелить себе условную ногу. Я, наоборот, очень люблю самоограничения, так как очень критически отношусь к уровню своих знаний, и по определению считаю себя и любого другого программиста идиотом — и пытаюсь использовать инструменты, максимально секьюрные и с минимальными возможностями для выполнения задачи. Того же C и C++ я вообще боюсь как ада: пока я не буду писать операционные системы либо что-то настолько же сложное и одновременно требовательное к времени выполнения, я к ним и близко не подойду.

        1. > Например, в C# эта конструкция вполне нормальна.

          Она везде вполне нормальна с точки зрения языка. Просто она представляет собой способ «переизобрести» встроенные в язык механизмы перегрузки конструкторов — а следовательно, ненормальна в смысле логики.

          > Ну то есть, по мне, на сервере должен сочиняться XML/json, а на клиенте он уже должен раскладываться в красивые формы, вообще желательно с помощью другого языка — того же JS, например.

          Тоже вариант, гуглим про REST-протокол. Вообще, тут много подходов — и тот, которые предлагает PHP по умолчанию, вырос из Perl и прочего CGI образца середины 90-х, и совершенно отвратителен. Достаточно интересно выглядит подход из ASP, когда на стороне сервера создаются какие-то «объекты», напоминающие те, которые фигурируют в «настольных» GUI — типа MFC, WinForms и Qt. На ASP вообще можно не писать HTML-кода и работать с кнопочками на веб-странице у клиента почти так же, как и с кнопочками в «настольном» приложении.

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

          Отстрелить ногу можно в любом языке. В «безопасном» PHP понаставлено не меньше ловушек. Более того, где в C, например, ошибка очевидна, в PHP о ее существовании можно даже не догадаться.

          1. на входе преобразовывать к тому, что нам надо, на выходе преобразовывать к тому, что им надо. Внутри — никаких преобразований.
            Это та категоричность, которая тебе не нравится.
            А мне нравится, потому что снижает вероятность что-нибудь провафлить, когда функция А надеется что ей передадут правильные параметры, а функция Б считает что фунцкция А сама разберется что ей подсунули.

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

            1. > на входе преобразовывать к тому, что нам надо, на выходе преобразовывать к тому, что им надо

              На входе и выходе чего?

              > на мой взгляд вообще за гранью добра

              На мой — тоже.

              1. Насчёт второго — вы про что конкретно? Звучит очень похоже на printf. Но printf — это просто замечательно, когда нужно локализировать эту строку на 20 языков.

                1. Тут у меня натуральное нарушение принципа «модель-представление». Модель — это данные из таблицы, получаемые по какому-то запросу с параметрами. Представление — это виджет. Правильнее сделать что-то типа такого:

                  $data = new dataFromSQL(«SELECT * from table WHERE param=’%s'», $param);
                  $widget — new tableWidget($data);

                  1. Вполне. Поэтому на вход сей модуль у меня получает сформированный запрос, а не подобие sprintf’а.
                    но snum в моих принципах туда доедет в виде нуля и не будет нуждаться ни в апострофах, ни в экранировании.
                    Т.е. внутри того, что я контролирую, не должно быть никаких дополнительных прообразований, ибо это позволит, повторю, не провафлить тот момент, когда эти преобразования надо бы сделать.

          2. Насчёт конструкторов — а что, если для .Safe(Unsafe s) надо получать доступ к приватным полям Unsafe? Тогда .GetSafe() выглядит гораздо правильнее.

            А php, он не «безопасный», он просто идиотский, по моему скромному мнению. Безопасный — это не про язык, а про sandbox, в котором он работает, скорее.

              1. Почему? Если что, я уже не имею в виду safe и unsafe, просто изготовление экземпляров класса. Например, если у нас есть тип Money, то неужели нельзя сделать

                class Account {

                private int credit;
                private int debit;

                Money Account.GetBalance() {

                return new Money (credit — debit);

                }

                }

                потому что я что-то в таком виде имел в виду, естественно.

                  1. Тогда можешь объяснить подробнее, почему

                    .GetSafe() – ну это же отстой, надо делать конструктор Safe(Unsafe s)) – но тут мы всецело полагаемся на механизмы языка.

                    реально не понял.

                    1. Safe для разных СУБД разный. Если мы запихиваем превращение небезопасной строки в безопасную в класс, не имеющий никакого отношения к СУБД, то мы сразу же создаем еще одну неочевидную зависимость и «размазываем» зависящий от выбранной СУБД код по разным местам.

          3. Насчёт конструкторов — а что, если для .Safe(Unsafe s) надо получать доступ к приватным полям Unsafe? Тогда .GetSafe() выглядит гораздо правильнее.

            В остальном мы с тобой как-то во всём согласны, получается.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *