Статьи

Структурное непредвиденное обстоятельство (часть первая)

Порядок в математике имеет значение.

Возьмите две функции, f и g , и составьте их, применяя их к аргументу как f (g (x)) или g (f (x)) . В общем случае нельзя предполагать, что f (g (x)) = g (f (x)) . Если f (x) = 2x и g (x) = x 2 , например, тогда f (g (3)) = 18, но g (f (3)) = 36 . Порядок, в котором применяются функции, имеет значение. Математика, часто предоставляющая несколько способов выражения идеи, эта f (g (x)) также может быть написана с использованием композиционного оператора o, означающего «Применено после», такого, что f (g (x)) = fog (x) .

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

1
2
3
4
public void recoverFromException() {
    View view = registry.get(View.class)
    view.endWaiting();
    }

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

1
recoverFromException = endWaiting o get

Такая композиция проистекает из необходимой взаимосвязи задействованных вызовов get (), предоставляющих аргумент endWaiting () . Явность синтаксического упорядочения требует, чтобы get () вызывался перед endWaiting () , иначе программа не будет компилироваться. Но как быть с внешне не связанными вызовами методов? Учтите следующее:

1
2
3
4
public void clearViewCache(View view) {
    view.clearPositionCache();
    view.clearImageCache();
    }

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

1
2
3
4
public void clearViewCache(View view) {
    view.clearImageCache();
    view.clearPositionCache();
    }

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

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

Некоторые обозначения могут помочь. Так же, как оператор o означает композицию, допустим, что оператор \ означает случайность. Таким образом, второй фрагмент может быть записан как:

1
clearViewCache = clearPositionCache \ clearImageCache

И это, по определению, равно:

1
clearViewCache = clearImageCache \ clearPositionCache

Как программист может использовать эту тривиальность, чтобы прояснить намерение? Один из способов – минимизировать количество условных методов и максимизировать количество составных методов. Здесь, увы, вселенная смеется над программистом из-за некоторых неудачных комбинаторных правил; свободно:

Композиция + композиция = композиция.
Случайность + случайность = случайность.
Состав + случайность = случайность.

То есть, fogoh представляет собой составной метод, но fo (g \ h) является условным методом, даже если он содержит оператор композиции. Однажды загубленная неопределенностью порядка, ни один метод не может избежать непредвиденных обстоятельств. Тем не менее, кажется, стыдно скрывать даже частичное намерение под замешательством. Поэтому, скажем, далее, метод, сформированный исключительно вокруг оператора композиции или непредвиденной ситуации, например, последовательности вызова k \ m \ p \ s или последовательности komopos, мы будем называть «простым по порядку» методом, в то время как один из них может похвастаться как составными, так и непредвиденными операторами, мы назовем его «Составление заказа» Таким образом, максимизация намерения превращает составные методы порядка в простые шарды порядка.

Рассматривать:

1
a = f \(g o h)

Если мы введем составной метод j такой, что j = goh, то вышеупомянутое порядковое соединение сводится к двум простым по порядку методам:

1
a = f \ j
1
j = g o h

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

Рисунок 1: Порядок составного сокращения непредвиденных расходов до (слева) и после (справа).

Рисунок 1: Порядок составного сокращения непредвиденных расходов до (слева) и после (справа).

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

свойства

Некоторые свойства непредвиденных обстоятельств:

  1. 1
    f o (g \ h) = f(g, h)
  2. коммутативности:
    1
    f \ g = g \ f
  3. Ассоциативность:
    1
    (f \ g) \ h = f \ (g \ h)
  4. Композиция распределяется по случайности:
    1
    (f \ g) o h = (f o h) \ (g o h)

Другие существуют, чтобы быть исследованным в более позднем посте.

Сложенный порядок-простой

Давайте рассмотрим некоторый реальный производственный код. FitNesse предоставляет все следующие фрагменты, первый из класса PageDriver .

1
2
3
4
5
6
7
private NodeList getMatchingTags(NodeFilter filter) throws Exception {
    String html = examiner.html();
    Parser parser = new Parser(new Lexer(new Page(html)));
    NodeList list = parser.parse(null);
    NodeList matches = list.extractAllNodesThatMatch(filter, true);
    return matches;
  }

Моделирование этого как его основного вызова метода приводит к уравнению:

1
getMatchingTags = extractAllNodesThatMatch o parse o new Parser o new Lexer o new Page o html

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

Условный приказ – простой

1
2
3
4
5
6
7
public void close() throws IOException {
    super.close();
    removeStopTestLink();
    publishAndAddLog();
    maybeMakeErrorNavigatorVisible();
    finishWritingOutput();
  }

Этот метод в классе TestHtmlFormatter представляет случайное простое условие , уравнение которого:

1
close = close /  removeStopTestLink / publishAndAddLog / maybeMakeErrorNavigatorVisible / finishWritingOutput

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

Порядок-соединение

Конструктор MultiUserAuthenticator представляет минималистский пример порядка компоновки :

1
2
3
4
5
public MultiUserAuthenticator(String passwdFile) throws IOException {
    PasswordFile passwords = new PasswordFile(passwdFile);
    users = passwords.getPasswordMap();
    cipher = passwords.getCipher();
  }

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

1
MultiUserAuthenticator = (getPasswordMap \ getCipher) o new PasswordFile

Выполнение сокращения составного порядка включает в себя рефакторинг метода, чтобы минимизировать размер составных порядков, из которых он построен. Наиболее очевидное сокращение для выполнения вышеизложенного извлекает новый метод f = getPasswordMap \ getCipher , таким образом производя два уравнения:

1
2
MultiUserAuthenticator = f o new PasswordFile
f = getPasswordMap \ getCipher

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

1
2
3
4
5
6
7
8
9
public MultiUserAuthenticator(String passwdFile) throws IOException {
    PasswordFile passwords = new PasswordFile(passwdFile);
    f(passwords);
  }
 
  private f(PasswordFile passwords) throws IOException {
    users = passwords.getPasswordMap();
    cipher = passwords.getCipher();
  }

(Возможно, существует лучшее имя для метода f () .)

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

1
MultiUserAuthenticator = (getPasswordMap o new PasswordFile) \ (getCipher o new PasswordFile)

Это сводится к трем простым по порядку уравнениям:

1
2
3
MultiUserAuthenticator = f \ g
f = getPasswordMap o new PasswordFile
g = getCipher o new PasswordFile

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

Проблема нормализации

Метод processTestFile () PageHistory освещает некоторые интересные трудности:

1
2
3
4
5
6
7
8
void processTestFile(TestResultRecord record) throws ParseException {
    Date date = record.getDate();
    addTestResult(record, date);
    countResult(record);
    setMinMaxDate(date);
    setMaxAssertions(record);
    pageFiles.put(date, record.getFile());
  }

Предыдущий пример представил дублирование вызова метода как средство для изучения другого порядка размещения. Однако уравнение порядка для processTestFile () не может быть выражено без дублирования. В минимальном случае кажется, что getDate () появляется дважды:

1
processTestFile = put o (getDate \ getFile) \ setMaxAssertions \ (setMinMaxDate o getDate) \ countResult \ (addTestResult o getDate)
1
= put o (getDate \ getFile) \ setMaxAssertions \ countResult \ ((setMinMaxDate \ addTestResult) o getDate)

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

Ради полноты, если мы введем a = put o (getDate \ getFile) и b = (setMinMaxDate \ addTestResult) , то получим:

1
processTestFile = a \ setMaxAssertions \ countResult \ (b o getDate)

Наконец, мы вводим c = bo getDate, мы получаем:

1
processTestFile = a \ setMaxAssertions \ countResult \ c

Теперь это простой по порядку условный метод, а оставшаяся порядковая сложность секвестируется в метод a () , хотя getDate () вызывается из a () и c () .

Резюме

Программисты смотрят на абсолюты с подозрением.

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

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

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

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