Работающий программист

Мультипарадигматическая .NET. Процедурное программирование

Тэд Ньюард

Ted NewardВ прошлом месяце в центре дискуссии находились общность и вариативность при проектировании программного обеспечения (msdn.microsoft.com/magazine/gg232770). Мы остановились на идее, что такие языки, как C# и Visual Basic, предлагают разные парадигмы для представления этих концепций общности/вариативности и что суть мультипарадигматического проекта в сопоставлении требований предметной области с языковыми возможностями.

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

Продолжай свое дело, консерватор

Для тех, кто еще не появился на свет, когда структурное программирование было новым термином, поясню, что его основной принцип — размещение некоего определения (структуры) вокруг написанного блока кода; на практическом уровне это означало наличие единых точек входа в блоки кода и выхода из них, а сами блоки кода писались в то время на ассемблере. В ретроспективе цель была довольно простой: заключить повторяющие куски кода в некие высокоуровневые абстракции.

Но зачастую эти команды (процедуры) нуждались в некоторых вариациях и параметрах — ввод изменял выполнение процедуры.Параметры включались поначалу неформально («передайте символ, который вы хотите отобразить, в регистр AX»), а затем и формально (как параметры функций подобно C/C++/C#/Java/Visual Basic и др.). Процедуры часто вычисляли какое-то возвращаемое значение, иногда производное от переданного входного значения, а иногда просто указывающее на успех или неудачу (например, в случае записи данных в файл или базу данных); это также определялось и обрабатывалось компилятором.

Однако все это факультативно для большинства читателей. Мультипарадигматический подход требует от нас не перекраивать историю, а вновь окинуть ее взглядом и посмотреть на нее через призму анализа общности. Что конкретно обобщается в процедурном подходе и как вводится вариативность? А как только вариативность выявлена, как определяется ее тип — позитивная или негативная?

Одев «очки общности/вариативности», вы обнаружите в процедурной парадигме интересные секреты: общность собирается в процедуры, которые фактически являются именованными блоками кода, которые можно вызывать из любого заданного контекста. (Процедурные языки интенсивно используют концепцию области видимости для изоляции работы внутри процедуры от окружающего контекста.) Один из способов ввести вариативность в процедуру — использование параметров (указывающих, как обрабатывать остальные параметры, например), которые могут иметь либо позитивную, либо негативную вариативность в зависимости от того, как написана сама процедура. Если источник для этой процедуры недоступен или по какой-то причине не должен модифицироваться, вариативность все равно может быть за счет создания новой процедуры, которая либо вызывает старую, либо не делает этого в зависимости от того, какая именно вариативность вам нужна — позитивная или негативная.

Процедуры «Hello»

По сути, процедура предоставляет общее поведение, которое можно изменять на основе ввода. И, что весьма забавно, первый пример, который мы видим в процедурной парадигме, является первым же примером в Microsoft .NET Framework:

Sub Main()
    Console.WriteLine("{0}, {1}!", "Hello", "world!")
End Sub

В реализации WriteLine разработчики передают форматирующую строку; она описывает не только, что нужно вывести, но и как это сделать, и включает команды форматирования, содержащиеся внутри маркеров замены, например:

Sub Main()
    Console.WriteLine("Hello, world, it's {0:hh} o'clock!", Date.Now)
End Sub

Реализация WriteLine — интересный пример в том смысле, что эта реализация несколько отличается от своего античного предшественника: printf из стандартной библиотеки C. Вспомните, что printf принимал аналогичную форматирующую строку, используя другие маркеры форматирования, и писал прямо в консоль (поток STDOUT). Если программисту нужно было записать форматированный вывод в файл или строку, приходилось использовать другие виды printf: fprintf (вывод в файл) или sprintf (вывод в строку). Но само по себе форматирование вывода было общим, и зачастую библиотеки исполняющей среды C пользовались этим преимуществом, создавая единую обобщенную функцию, прежде чем посылать результаты в место назначения, — вот вам отличный пример общности. Однако это поведение считалось «закрытым» для среднего разработчика на C, и его нельзя было расширять. В .NET Framework сделан один шаг вперед:разработчикам предлагается шанс создавать новые маркеры форматирования, перекладывая ответственность за эту функциональность на объекты, которые передаются в WriteLine после форматирующей строки. Если объект реализует интерфейс IFormattable, ему выдается ответственность за распознавание маркера форматирования и возврат соответственно отформатированной строки для последующей обработки.

Вариативность тоже может прятаться в каких-либо местах при процедурном подходе. При сортировке значений процедуре qsort (реализация Quicksort) нужна была помощь, чтобы знать, как сравнивать два элемента (кто из них больше или меньше). Требовать от разработчиков писать собственные оболочки для qsort — традиционный механизм вариативности, когда оригинал должен оставаться неприкасаемым, — было бы слишком громоздко и трудно. К счастью, в процедурной парадигме предлагался другой подход — ранняя вариация того, что теперь известно под названием «инверсия управления» (Inversion of Control, IoC): разработчик на C передавал указатель на функцию, которую qsort вызывала в процессе своей работы. Это фактически разновидность подхода «вариативность на основе параметров»; при этом можно было использовать любую функцию (если она отвечала требованиям к параметрам и типу возвращаемого значения). Поначалу такая идиома этой парадигмы встречалась весьма редко, но со временем она стала широко распространенной и получила название «функция обратного вызова» (callback); к моменту выпуска Windows 3.0 функции обратного вызова уже были узаконены и были просто необходимы для написания Windows-программ.

Привет сервисам

Самое интересное, что область, в которой процедурная парадигма достигла наибольшего успеха (если не считать невероятного успеха и распространенности стандартной библиотеки C), — сервисы. (Здесь я подразумеваю под сервисами более широкий набор ПО, а не традиционные узкоспециализированные сервисы на основе только WS-* или SOAP/Web Services Description Language [WSDL]; под то же определение во многом подходят реализации на основе REST и Atom/RSS.)

Согласно старой литературе на msdn.com, например «Principles of Service-Oriented Design» (msdn.microsoft.com/library/bb972954), сервисы подчиняются четырем основным принципам:

  • их границы очерчены явным образом;
  • сервисы автономны;
  • сервисы совместно используют схему и контракт, но не класс;
  • совместимость сервисов основана на политике.

Эти принципы (возможно, и не предумышленно) укрепили природу сервисов как относящуюся к процедурной парадигме, а не объектно-ориентированной. «Их границы очерчены явным образом» вводит концепцию того, что сервисы являются отдельной сущностью и не связаны с вызывающей их системой; эта точка зрения еще больше укрепляется принципом «сервисы автономны», а значит, они вдобавок отделены и друг от друга — в идеале, даже на уровне инфраструктуры управления. «Сервисы совместно используют схему и контракт, но не класс» свидетельствует о том, что сервисы определяются в терминах передаваемых им параметров, выражаемых как конструкции XML (или JSON), а не конкретные типы исполняющей среды конкретного языка или платформы программирования. Наконец, «совместимость сервисов основана на политике» предполагает, что сервисы должны быть совместимы на основе объявлений политики, которые предоставляют более полный контекст вызова, а это то, что в процедурной парадигме традиционно логически распознавалось по окружающей среде, а значит, не требовало явного определения.

Разработчики могут весьма быстро указать на то, что в классических сервисах на основе WSDL труднее создать вариативность, так как сервис связан с определением схемы входного типа. Но это верно лишь для самых базовых (или генерирующих код) сервисов — входные и выходные типы можно использовать повторно (и зачастую так и делается) в определениях разных сервисов. По сути, если расширить концепцию сервиса и включить в нее системы на основе REST, то сервис может принимать любое количество разнообразных видов входных типов и вести себя по-разному в зависимости от них, что вновь прямо выдвигает вариативность внутри сервиса на первый план. Некоторая часть этого поведения будет, конечно, нуждаться в проверках, поскольку URL сервиса (его имя) не всегда подходит для любых данных, которые могут быть переданы ему.

Когда на сервисы смотришь сквозь призму системы обмена сообщениями, например BizTalk, ServiceBus или другую Enterprise Service Bus, процедурный аспект по-прежнему сохраняется, но теперь вся вариативность покоится на сообщениях, передаваемых туда-сюда, так как именно сообщения несут всю полноту контекста вызова — нет даже имени процедуры, подлежащей вызову. Это также означает, что механизма вариативности, с помощью которого мы обертываем одну процедуру в другую — либо расширяя, либо ограничивая вариативность, — больше нет, поскольку мы, как правило, не контролируем, как сообщения передаются по шине.

Преуспевая в развитии

Процедурная парадигма демонстрирует некоторые из самых ранних измерений общности/вариативности.

  • Имя и поведение Имена передают смысл. Мы можем использовать общность имени для группирования элементов (например, процедур/методов), имеющих одинаковый смысл. По сути, «современные» языки позволили нам фиксировать эту связь более формально, разрешая использование одинаковых имен для разных методов, если они варьируются в количестве и/или типах параметров:это перегрузка методов (method overloading). C++, C# и Visual Basic также могут использовать преимущества соответственно именованных методов, создавая методы, имена которых хорошо понятны из алгебры:это перегрузка операторов. F# идет еще дальше, разрешая создавать новые операторы.
  • Алоритм. Это не просто математические вычисления, а скорее повторяемые этапы выполнения. Если вся система (а не индивидуальные уровни) видна сверху вниз, проявляются интересные фрагменты процесса/кода — по сути, случаи применения, — которые образуют семейства. После выявления этих этапов (процедур) вокруг вариативности могут быть образованы семейства на основе того, как алгоритм/процедура работает с разными видами данных/параметрами. В C#, F# и Visual Basic эти алгоритмы могут варьироваться путем размещения их в базовых классах с последующим наследованием от базового класса и заменой нужного поведения:это переопределение методов. Алгоритмическое поведение также можно изменять, оставляя его часть неопределенной в расчете на передачу извне недостающей части:это использование делегатов как Inversion of Control или обратных вызовов.

Одно замечание перед тем, как мы покончим с этой частью. Процедурная парадигма может не совсем точно соответствовать миру, ориентированному на сервисы; по сути, многие идеологи и ярые сторонники архитектуры, ориентированной на сервисы, будут отрицать даже малейшие намеки на связь с процедурной парадигмой просто из страха, что такая связь каким-то образом разоблачит их давние интересы. Но, если отбросить политику, то классический сервис — будь то на основе RESTful или SOAP/WSDL — поразительно похож на классическую процедурную парадигму. В итоге применение того же анализа общности при проектировании сервиса помогает создать приемлемый уровень гранулярности, хотя проектировщики должны позаботиться о том, чтобы (предполагаемая) передача данных по сети, необходимых для выполнения сервиса в месте его размещения, не игнорировалась беспечно. В частности, наивные реализации сервисов, использующие процедурную парадигму, могут пытаться использовать подход с «передачей обратного вызова» для вариативности, и, хотя это не столь уж идиотская идея, такой подход мог бы привести к появлению узкого места и проблем с производительностью.

На сегодняшний день процедурная парадигма по-прежнему проявляется то тут, то там почти во всем, что мы делаем, но ее прячут поглубже внутрь, скрывая ее от разработчиков под надуманным названием. У нашего следующего субъекта, объектно-ориентированной парадигмы, нужды в такой скрытности нет — это бойкая младшая сестра своего процедурного родственника, которая так и кричит:«Эй, смотрите на меня!». В следующей статье мы начнем анализировать общность/вариативность объектов, и кое-что из того, что мы обнаружим, может здорово удивить вас.

Тем временем, в качестве упражнения приглядитесь к различным инструментам вокруг вас и определите, какие из них используют принципиально процедурную тактику. (Подсказка: два таких инструмента вы ежедневно используете при написании программ: компилятор и MSBuild — система сборки, скрытая за кнопкой Build в Visual Studio.)

И как всегда, удачи в кодировании!

Тэд Ньюард (Ted Neward) — глава независимой компании Neward and Associates, специализирующейся на гибких и надежных корпоративных системах с применением .NET и Java. Автор и соавтор многочисленных книг, в том числе «Professional F# 2,0» (Wrox, 2010), более сотни статей, лектор INETA, часто выступает на многих конференциях по всему миру; кроме того, имеет звание Microsoft MVP в области C#. С ним можно связаться по адресу ted@tedneward.com или через блог blogs.tedneward.com.

Выражаю благодарность за рецензировании статьи эксперту Энтони Грину (Anthony Green)