Когда я впервые услышал о генераторах исходного кода Roslyn в анонсе, я сначала захотел написать в блоге статью, в которой сравнивал бы их с аспектно-ориентированным программированием. Пост в блоге постепенно превратился в философское эссе. Что такое программирование? Что такое языки программирования? Что такое дизайн? Эти вопросы задают редко, а философские эссе - непопулярный жанр в нашей индустрии. Тем не менее, я считаю, что они чрезвычайно актуальны. В индустрии, где взлом так популярен, я хотел бы сегодня предложить противоположное: обзор деятельности по созданию программного обеспечения с гуманистической и когнитивной точки зрения.

Что такое генераторы источников Roslyn?

Если вы не читали о генераторах исходного кода, то, вкратце, это новая точка расширения компилятора Roslyn, которая позволяет вам (и любому пакету NuGet) динамически генерировать новый код C # и добавлять его в выполняемую компиляцию. Генераторы исходного кода получают объектную модель, представляющую компилируемый код, и на основе этого могут генерировать новый код. Однако они не могут изменять существующий код.

Строго говоря, генераторы исходных текстов Roslyn не допускают никаких сценариев, которые раньше были невозможны. Новизна генераторов исходного кода Roslyn заключается в том, что они встроены в компилятор Roslyn, что дает два преимущества. Во-первых, сборка будет быстрее, потому что первый этап компиляции нужно будет выполнить только один раз, а не дважды. Во-вторых, поскольку компилятор C # размещен в среде IDE, артефакты кода программной части будут видны во время разработки в среде IDE с помощью таких инструментов, как IntelliSense.

Еще одно решающее преимущество генераторов исходного кода Roslyn заключается в том, что они изобретены Microsoft и поэтому выиграют от немедленного, широкого, но также некритического принятия. Мы можем ожидать, что генераторы исходного кода Roslyn будут широко использоваться и злоупотреблять, как только они станут стабильной частью .NET.

Когда использовать исходные генераторы?

Microsoft дает несколько вариантов использования генераторов исходного кода. Большинство из них отличные. Спойлер: не все.

Среди обоснованных вариантов использования исходных генераторов можно перечислить:

  • Создание файлов кода программной части для XAML, ASP.NET, настроек,… что теперь выполняется как отдельные задачи MSBuild.
  • Создание «каталога приложений», например, для индексации контроллеров ASP.NET, компонентов MEF… что теперь обычно выполняется во время выполнения во время запуска приложения с использованием System.Reflection. Его также можно было реализовать с помощью инструмента посткомпиляции, такого как PostSharp, но не было серьезных попыток сделать это.
  • Генерация кода инфраструктуры, который требует реализации таких методов, как ToString или Equals, GetHashCode, операторов сравнения… который сейчас обычно реализуется с использованием таких инструментов, как PostSharp.

Я считаю, что в этих трех классах вариантов использования генераторы исходного кода превосходят технологию, которую они заменяют.

Когда не использовать генераторы исходников?

Генераторы исходного кода созданы только аддитивными, т.е. вы можете добавлять в компиляцию новые файлы (возможно, новые частичные классы), но не можете изменять существующие. Если вы столкнулись с проблемой, которую вы, естественно, решите, отредактировав существующий код, и которую нельзя выделить в частичный класс, то генератор исходного кода не подходит для этой задачи.

Чтобы проиллюстрировать это правило, я возьму пример непосредственно из материалов Microsoft и покажу, почему это можно рассматривать как злоупотребление этой технологией.

Давайте посмотрим на код, воспроизведенный по образцам Microsoft:

public partial class UserClass 
{ 
    [AutoNotify] 
    private bool _boolProp;
    [AutoNotify(PropertyName = "Count")] 
    private int _intProp; 
}

Исходный генератор сгенерирует следующий частичный класс:

public partial class UserClass : INotifyPropertyChanged
{
    public bool BoolProp
    {
        get => _boolProp;
        set
        {
            _boolProp = value;
            PropertyChanged?.Invoke(
              this, new PropertyChangedEventArgs("UserBool"));
        }
    }
    public int Count
    {
        get => _intProp;
        set
        {
            _intProp = value;
            PropertyChanged?.Invoke(
              this, new PropertyChangedEventArgs("Count"));
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

Что случилось с этим?

На первый взгляд кажется, что этот инструмент отлично справляется с написанием шаблонов. С этой точки зрения генераторы-источники выглядят хорошо.

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

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

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

Проблема использования генератора исходного кода для решения проблемы INotifyPropertyChanged десятилетней давности заключается в том, что это решение не является идиоматическим.

Если у вас уже есть класс со свойствами и вам нужно добавить INotifyPropertyChanged, вам придется преобразовать свои свойства в поля. Ваш код будет выглядеть неестественно (нарушая «принцип наименьшего удивления»), а сам рефакторинг непродуктивен. Вам нужно будет добавить правильные параметры в свой [AutoNotify], чтобы он генерировал средства доступа с необходимой вам видимостью, в то время как эта проблема уже элегантно решена в языке C #. Все это запахи дизайна, которые показывают, что решение не является идиоматическим.

Реализация INotifyPropertyChanged - задача из учебника аспектно-ориентированного программирования (АОП). На самом деле, мы можем сказать, что [AutoNotify] - это аспект, но реализованный без излишеств с генератором исходного кода.

В отличие от генераторов исходного кода, фреймворки АОП специально разработаны для добавления проблем инфраструктуры в рукописный код без необходимости его изменения. Хорошо написанные аспекты работают с вашим кодом, а не против него. То есть хорошие аспектно-ориентированные фреймворки идиоматичны для своего основного языка.

Генераторы исходного кода - полезный инструмент, но они не заменяют аспектно-ориентированное программирование.

Отношения любви и ненависти Microsoft к АОП

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

С одной стороны, многие команды добавили функции АОП в свой собственный продукт:

  • Microsoft Transaction Server, позже переименованный в COM +
  • Пользовательское поведение в WCF
  • Перехватчики в Unity
  • Фильтры действий в ASP.NET MVC и ASP.NET MVC Core

Эти команды добавляли эти функции не просто потому, что они были забавными, а потому, что они были полезны. Во всех этих технологических стеках аспекты могут применяться только на границах компонента, потому что технология является посредником при вызове между клиентом и компонентом. Здесь не участвовали ни компилятор, ни движок времени выполнения, только хост службы.

Другой пример технологии, в которой жизненно необходимо аспектно-ориентированное программирование, - WPF. Количество кода, необходимого для реализации INotifyPropertyChanged, свойств зависимостей или команды, колоссально. Вот почему INotifyPropertyChanged упоминается как вариант использования генераторов исходного кода. Потому что это так полезно.

Наконец, некоторые команды Microsoft использовали PostSharp для повышения своей производительности.

Это неудивительно: аспектно-ориентированное программирование невероятно полезно. Наши данные телеметрии показывают, что АОП может легко уменьшить размер и сложность исходного кода на 15%. Это означает, что некоторые пользователи нашего продукта получают выгоду от 1% и, вероятно, должны прекратить его использовать, но другие пользователи уменьшают размер своего кода на 50%. Один из наших клиентов признался, что сэкономил миллионы долларов благодаря PostSharp. Вот насколько велик АОП. Многие пользователи PostSharp говорят, что хотят, чтобы он стал стандартной частью .NET. В мире несколько миллионов разработчиков C #, работающих полный рабочий день. На каждый миллион разработчиков сейчас 150 000 человек пишут код, который можно было бы сгенерировать с помощью алгоритма более высокого качества. Подумайте об этой огромной трате времени.

Проблема в том, что, с другой стороны, команда C # на самом деле никогда не принимала аспектно-ориентированное программирование. Этого нет даже на их карте характеристик.

С эгоистической точки зрения для нас хорошо, что языковая команда не заботится об АОП, потому что мы можем заработать (вряд ли выигранные) деньги, восполнив этот пробел. Но для сообщества это жаль.

А пока вы можете положиться на PostSharp, если хотите воспользоваться преимуществами АОП. Откровенно говоря, многие компании не хотят рисковать и полагаться на такую ​​небольшую компанию, как наша, в критически важной части их процесса строительства - даже если мы работаем в этом направлении почти 15 лет. Я благодарен тысячам клиентов, которые решили нам доверять, но я также понимаю, что для многих из них риск поставщика является более серьезным препятствием, чем цена лицензии.

Поэтому я считаю, что отношения Microsoft с АОП с любовью и ненавистью пагубно сказываются на продуктивности экосистемы .NET, особенно для крупных проектов.

Да, аспектно-ориентированное программирование - это старая концепция, и годы ее раскрутки остались позади. Я не поклонник подхода AspectJ и никогда не пытался скопировать его в .NET. Но факт в том, что АОП устраняет пробел в абстракции C #, и этот пробел нельзя игнорировать. AOP, AspectJ и даже PostSharp - всего лишь решения проблемы. Тот факт, что вам не нравится решение, не устраняет проблему волшебным образом.

Итак, как мы можем подойти к этой проблеме? Какие проблемы все равно решают языки программирования?

Что такое язык программирования?

Язык программирования - это язык. Язык, используемый людьми и только людьми. Компиляторы и интерпретаторы - это инструменты, которые преобразуют элементы человеческой речи в инструкции, которые могут выполняться компьютером. Но сам язык предназначен для потребления человеком. Человечность - самая фундаментальная характеристика языка программирования. Это кажется парадоксальным, потому что языки программирования традиционно разрабатывались людьми с технологическим образованием, а не, скажем, в философии или когнитивной психологии. Возможно, потому, что языки разрабатывались той же группой людей, которая реализовывала компиляторы, что, безусловно, является технологической деятельностью.

Чем больше абстракция между языком и оборудованием, тем сложнее должен быть компилятор или интерпретатор. В течение первых десятилетий развития вычислений сложность компиляторов и интерпретаторов сильно ограничивалась аппаратными ограничениями, особенно ЦП и ОЗУ. В 1970-х годах было разработано специализированное оборудование, получившее название LISP-машины. В конце 90-х были даже сопроцессоры Java. Язык C с его системой файлов заголовков был разработан для оптимизации использования памяти компилятором. В эти новаторские времена языки программирования должны были оставаться близкими к аппаратному обеспечению, то есть гомоморфными машине.

С каждым поколением языки становились ближе к человеческому мышлению. Объектно-ориентированное программирование было изобретено в 1960-х годах для создания языка, который позволил бы выражать модели физического мира. Это стало SIMULA. Найдите время, чтобы подумать о концепциях класса и наследования. Эти концепции не имеют отношения к аппаратному обеспечению. Однако они имеют первостепенное значение для человеческого мышления и были впервые теоретизированы в западной культуре Аристотелем в 4 веке до нашей эры. (Пожалуйста, не заставляйте меня говорить, что Аристотель изобрел ООП!) Можно сказать, что языки программирования становятся все более и более логоморфными, если мы принимаем логотипы в значении рассуждения (логики), а не речи. Успешные языки программирования подражают нашему мышлению.

Однако в современном мире оперативная память и время компиляции больше не являются ограничением. Компилятор потребляет лишь часть общего времени сборки. Что же действительно мешает языкам программирования стать еще более логоморфными? Две вещи: недостаток инвестиций в компиляторы (я считаю, что каждый миллион долларов, вложенный в язык C #, может привести к миллиарду повышения производительности для сообщества) и система ментальных привычек, которая усваивает ограничения, которые теперь устарели.

Какую проблему решают языки программирования и фреймворки?

Проблема, которую решают разработчики, обычно состоит в том, чтобы реализовать решение бизнес-проблемы - например, создать веб-сайт. Реализация может быть простой или сложной, может потребовать немного или больше дизайна. Назовем проблему реализации первой проблемой. Разработчики обычно сосредотачивают свое внимание на первой проблеме. Разработчики - интеллектуалы; их работа заключается в основном в производстве, хотя они могут также выполнять многие другие роли в своей работе. Сфера разработчиков состоит из исходного кода, инструментов разработки, API, облачных сервисов и других программных или аппаратных артефактов.

У менеджеров по развитию, однако, другая проблема. Вместо того, чтобы сосредотачиваться на первой проблеме, менеджерам необходимо сосредоточиться на деятельности разработки, то есть на команде разработчиков, на том, как они сотрудничают, общаются или конфликтуют, на найме и увольнении, на финансировании. команды и ее взаимоотношений с внешними заинтересованными сторонами и так далее. Сфера менеджеров состоит из людей, организаций и процессов. Назовем это второй проблемой, направленной на социальную активность разработки программного обеспечения.

Другими словами, вторая проблема - это то, что обнаруживается, когда вы пытаетесь повысить свою осведомленность от технической проблемы к действиям по решению технической проблемы - деятельности, которая носит человеческий и социальный характер. Это похоже на осознанность, но применимо к разработке программного обеспечения. Более высокий уровень осведомленности.

Теперь, если вы посмотрите на разработку программного обеспечения с этой более высокой точки зрения, задайте себе вопрос: с какими основными ограничениями сталкивается отрасль? Каковы его основные проблемы? Это недостаток вычислительной мощности, ОЗУ, хранилища, подключения к сети, энергии или емкости аккумулятора? Для большинства из нас нет. Я признаю, что все эти проблемы реальны, но они не являются реальным ограничивающим фактором. Это не дефицитный экономический фактор, с которым приходится справляться нашей отрасли.

Настоящая проблема нашей отрасли - нехватка когнитивных ресурсов. Что действительно ограничивает команды разработчиков и компании, так это объем интеллекта, который они могут привлечь в свои проекты за счет своего бюджета. Когнитивные способности каждого конечны. Наши индивидуальные познавательные способности определяют максимальную сложность проблемы, которую мы способны «загрузить» в наш мозг и, следовательно, рассуждать. Чтобы справиться с этой ограниченной емкостью, мы можем попытаться разложить большие проблемы на несколько меньших проблем, которые можно рассуждать отдельно (позже я увижу, как это влияет на дизайн языков программирования), но индивидуальные способности накладывают ограничение на размер этих проблем. атомные проблемы. Когда проблема, которую мы рассматриваем, находится в пределах наших возможностей, мы быстро и быстро справляемся с задачей. Когда мы приближаемся к нашим пределам, наша производительность резко снижается. Когда сложность превышает наши возможности, мы начинаем упрощать проблему, а когда наши упрощения становятся некорректными, мы начинаем разрабатывать неправильные решения. Самая сложная атомарная задача, которую может решить команда, ограничена возможностями ее самого умного члена. Первое измерение нехватки когнитивных ресурсов - это наша индивидуальная ограниченность.

Второе измерение очевидно: как компания, количество людей, которых вы можете нанять, ограничено вашим бюджетом. Чем больше мозгов, тем больше элементарных подзадач вы можете решить параллельно - предположим, вы можете разделить свои проблемы на достаточно независимых подзадач, которые достаточно просты, чтобы их могли решать среднестатистические члены команды. Эта способность разделять проблемы и обеспечивать эффективное социальное сотрудничество сама по себе является серьезной проблемой, которую наша отрасль решает с помощью инструментов и практик в широких областях: версионный контроль версий, управление проектами, специализация команд в компании и компаниях в отрасли, анализ и моделирование. , и так далее. Разработка программного обеспечения - это человеческая деятельность; Помимо когнитивных аспектов (работа с индивидуальным мозгом), он также имеет социальные / политические аспекты (организация эффективного сотрудничества многих людей).

Вернемся к языкам программирования и инструментам разработки.

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

Лучший язык программирования - это не обязательно тот, который позволяет построить заданную функциональность с наименьшим количеством символов. Это тот, который требует самых низких когнитивных ресурсов, или тот, который позволяет наилучшим образом распределить работу между командой различной квалификации.

Шаблоны и языковые расширения

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

Все мы знаем знаменитую книгу «Банды четырех» Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения, но лишь немногие разработчики вернулись к источникам и изучили работы духовного отца шаблона проектирования. теория: Кристофер Александр, 1936 г.р., влиятельный архитектор и теоретик дизайна. Он рассмотрел теоретические основы деятельности по проектированию артефактов, таких как сараи или котлы (дизайн как деятельность по проектированию, а не результат этой деятельности). Вклад «Банды четырех» заключался в применении этих принципов в разработке программного обеспечения - с огромным успехом, который мы знаем. Прискорбно, что большинство разработчиков программного обеспечения теперь рассматривают эту работу как поваренную книгу, которая теперь в значительной степени устарела, и пропускают теоретические части. Теоретические основы шаблонов проектирования по-прежнему имеют огромное значение для разработки языков программирования и фреймворков.

Александр обращает наше внимание на то, что в каждой профессии есть повторяющиеся элементы дизайна, которые он называет паттернами. Частью обучения каждого студента каждой профессии является изучение этих закономерностей. Например, Плотницкие работы для мальчиков, опубликованные в 1914 году, перечислены более сотни столярных изделий, которые должны изучить все ученики. Они должны не только научиться их выполнять, но и понимать, в каком контексте их следует использовать. Шаблоны - это многократно используемые элементы решения повторяющихся, но неидентичных проблем.

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

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

Поэтому развитие торговли всегда сопровождается обогащением ее диалекта.

Как это применимо к языкам программирования? Как мы можем расширить наш общий язык, скажем C #, чтобы он отражал наш опыт в предметной области?

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

Как вы представляете шаблон на C #? Ну, нет. Язык программирования C # не позволяет вам выразить исполняемым способом такую ​​простую вещь, как утверждение «класс Foo является аффинным по отношению к потоку» или «класс Bar является наблюдаемым», даже если сродство потока и наблюдаемость являются двумя хорошими - выявили закономерности разработки пользовательского интерфейса.

Невозможно расширить язык C # концепциями сходства потоков, наблюдаемости или других шаблонов, которые могут иметь отношение к конкретной нише, отрасли или проекту. Либо шаблон реализован на языке (например, событие, блокировка, использование, асинхронность, перечислители), либо нет. Вы не можете самостоятельно расширить язык.

Я считаю это ограничение важным недостатком C #. Важно, чтобы язык программирования соответствовал уровню абстракции, на котором люди рассуждают.

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

Именно этим мы занимаемся с PostSharp с 2004 года - хотя я признаю, что мне потребовалось 10 лет, чтобы понять, в чем на самом деле была моя работа. Таким же жестом мы запечатываем кирпич, строим стену, строим собор и совершенствуем искусство строительства соборов. Важно быть экспертами в герметизации кирпичей, но мы не должны упускать из виду здание, которое мы строим, и совершенство нашей профессии.

Мы не должны забывать, что программирование сегодня - это лишь вспомогательное средство для выполнения чего-то, что машина может выполнить, и, главным образом, для преодоления ограничений ваших собственных умственных способностей.

При разработке языков программирования и фреймворков мы не должны забывать о когнитивном аспекте нашей работы.

Ортогональная декомпозиция задачи

Наш мозг может одновременно обрабатывать только ограниченное количество элементов. Когда мы должны рассуждать о проблеме сотен движущихся частей, мы не можем рассуждать обо всех частях одновременно. Один из подходов называется «разделяй и властвуй». Мы рекурсивно разделяем проблему на подзадачи и реализуем их как отдельные артефакты, организованные в красивое дерево, напоминающее ботаническую таксономию: проекты, пространства имен, классы и, наконец, методы. Помимо этого дерева исходного кода, у нас есть вторая иерархия: наследование, то есть абстракция общих поведений. Эти две иерархии представляют собой то, как объектно-ориентированное программирование реализует принцип «разделяй и властвуй».

Есть еще одна техника, которую люди используют при проектировании сложных систем: разложение на несколько аспектов, которые являются как можно более независимыми (ортогональными). Например, когда архитектор проектирует дом, ему не нужно одновременно проектировать тепловые и электрические решения. Эти проблемы можно решить в разное время, нарисовав на разных слоях в редакторе САПР. Позже в проекте будет проверяться взаимодействие между этими слоями. Между тем, благодаря этой декомпозиции ее мозг мог сосредоточиться на различных подгруппах проблемы.

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

Как вы уже догадались, аспектно-ориентированное программирование было разработано для устранения этого недостатка.

Теоретически аспектно-ориентированное программирование не рассматривало проблему шаблонов проектирования, вероятно, потому, что и АОП, и шаблоны теоретизировались одновременно и независимо. Гораздо эффективнее теоретизировать и реализовать АОП как инструмент для создания реализации шаблонов.

Возможно, вам не понравится АОП в том виде, в каком он был разработан почти 20 лет назад, с его системой точек соединения и pointcut. Возможно, вам не понравится AspectJ, его каноническая реализация. Честно говоря, они мне тоже не нравятся. Вам может не понравиться PostSharp, особенно его зависимость от перезаписи MSIL. Но опять же, недостатки текущих реализаций не устраняют волшебным образом проблему, которую они пытаются решить. Разрыв между человеческим мышлением и языками программирования остается.

Разработка механизма расширения языка в 2020 году

Я бы хотел избежать двух догм.

Во-первых, мы обязательно должны следовать всем принципам аспектно-ориентированного программирования, которые вы найдете в Википедии. Мы не должны. Насколько мы не можем игнорировать проблему, решаемую аспектно-ориентированным программированием, мы не обязаны следовать их канонической стратегии реализации. На самом деле PostSharp никогда не был ортодоксальным аспектно-ориентированным фреймворком. Он никогда не полагался на концепции точек соединения и pointcut. В отличие от аспектов AspectJ, аспекты PostSharp всегда имели доступ к модели кода. Этот подход был также подходом анализа кода (FxCop), а теперь и анализаторами и генераторами исходного кода Roslyn. Следовательно, нам не нужно принимать или отклонять АОП в целом. Мы можем выбрать то, что нам нравится, и отказаться от того, что нам не нравится.

Вторая и противоположная догма, которую мы должны избегать, заключается в том, что любой механизм, позволяющий аспектам изменять код, по своей сути плох. Эта догма, кажется, прочно укоренилась в некоторых командах Microsoft. Я цитирую вводную запись в блоге о генераторах исходного кода Roslyn:

Генераторы исходного кода - это форма метапрограммирования, поэтому их естественно сравнивать с аналогичными функциями на других языках, такими как макросы [или плагины компилятора]. Ключевое отличие состоит в том, что генераторы исходного кода не позволяют вам писать пользовательский код _rewrite_. Мы рассматриваем это ограничение как существенное преимущество, поскольку оно позволяет предсказуемому пользовательскому коду относительно того, что он на самом деле делает во время выполнения.

Это заблуждение против переписывания кода непродуктивно. Невозможно преодолеть пробел в абстракции без предоставления аспектов (или плагина компилятора) некоторого способа переписать код. Я согласен с тем, что давать аспектам неограниченную и неконтролируемую возможность переписывать код слишком опасно. Но, как показывает мой опыт работы с PostSharp, можно дать аспекту возможность применять некоторые четко определенные преобразования к некоторым четко определенным возможным целевым элементам кода. Эти четко определенные преобразования:

  • перехват любого члена (метода, свойства, события, поля) - самое главное;
  • оборачивание метода в try / catch / finally;
  • добавление нового члена в класс;
  • манипулирование настраиваемыми атрибутами;
  • манипулирование управляемыми ресурсами;
  • добавление новых реализаций интерфейса.

Ключевой момент - сделать эти преобразования безопасными компонуемыми. Если несколько перехватчиков добавляются к одному и тому же методу, результирующее поведение должно быть предсказуемым, детерминированным и правильным.

По иронии судьбы, даже то, что генераторы исходных текстов Roslyn только надстройки, не делает их безопасными. Два несвязанных генератора могут сгенерировать два частичных класса с одним и тем же именем, которые будут конфликтовать друг с другом - например, оба попытаются реализовать IDisposable и обязательно будут конфликтовать, чтобы ввести метод Dispose. Этот сценарий поддерживается в PostSharp с версии 2.0 в 2009 году, когда он был полностью переработан для поддержки сильной компоновки. Генераторы исходного кода - это быстрая и грязная функция компилятора. Они полезны, но не поддерживают долгосрочное инженерное видение.

Если вы хотите реализовать что-то вроде АОП (или любое изменяемое метапрограммирование) и действительно решить проблему абстракции, вам понадобятся две вещи: доступ только для чтения к исходной модели и механизм для добавления предопределенных и безопасно составляемых (и, следовательно, сортируемых) преобразований. к существующему проекту. Эти функции могут быть реализованы компилятором, не затрагивая язык C #. PostSharp реализован как переписчик MSIL после компилятора, потому что он был разработан в то время, когда компилятор C # был с закрытым исходным кодом, но не было непреодолимых технических препятствий для реализации того же подхода прямо в компиляторе.

Сделать исходный код более предсказуемым при наличии перехватчиков - решенная задача. Скрытие деталей реализации из исходного бизнес-кода является преднамеренным, поэтому отображение созданных компилятором инструкций в исходном коде на самом деле нежелательно. Разработчикам обязательно нужно знать, что метод кэшируется, но им не нужно знать, как он кэшируется - до тех пор, пока они действительно не захотят сосредоточиться на этом аспекте. Вы можете визуализировать присутствие аспекта в методе, используя усовершенствования редактора кода, такие как CodeLens, значки, подчеркивание и всплывающие подсказки.

Адаптация отладки к наличию аспектов также важна, но, опять же, эта проблема уже частично решена. В PostSharp у вас есть два режима, когда вы входите в вызов метода: переходите через аспекты и переходите непосредственно к бизнес-коду или переходите к аспекту. Этот опыт можно улучшить, визуализировав во время отладки список перехватчиков, присутствующих в методе, и предложив перейти к одному из них. Здесь нет реальной концептуальной проблемы, но реализация нашей надстройки отладчика была чрезвычайно сложной.

Нет непреодолимых препятствий для преодоления разрыва в абстракции в C # при сохранении строго предсказуемой модели выполнения.

Резюме

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

Хакерство - это развлечение, и его можно быстро решить, но взлом не масштабируется. Уменьшение количества символов, необходимых для реализации функции, - благородная цель, но не в том случае, если это делает поддержание исходного кода когнитивно более сложным.

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

В этой борьбе за простоту языки программирования должны сыграть решающую роль. Как языки, они определяют, но также ограничивают то, что мы можем выразить с их помощью, и даже то, как мы рассуждаем. К сожалению, есть вещи, которые мы можем думать, но не можем выразить на C #: например, реализация шаблонов или декомпозиция ортогональных проблем. Я называю это пробелом в абстракции языка.

Аспектно-ориентированное программирование было попыткой 20 лет назад устранить этот пробел в абстракции. Вам может не понравиться его каноническая реализация в AspectJ или переписывание MSIL, которое делает PostSharp, но простое отклонение решений не решает проблему волшебным образом.

Опасения языковых команд Microsoft относительно переписывания кода в значительной степени необоснованны. Да, разработка хорошо спроектированной структуры для переписывания кода - сложный и дорогостоящий проект, но выполнимый.

За последнее десятилетие был накоплен большой опыт в области разработки аспектно-ориентированных фреймворков, а также в области визуализации и отладки кода, дополненного аспектами. Это стало зрелым искусством. Я горжусь тем, что PostSharp внес в это свой вклад.