Как стать автором
Обновить

Автоматы против спагетти-кода

Время на прочтение20 мин
Количество просмотров7.8K
Автор оригинала: Mark Truluck

«Люблю спагетти-вестерны, ненавижу спагетти-код»

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

  • Обсуждаем, почему спагетти-код не так уж и вкусен.
  • Представляем новый взгляд на то, что же на самом делает код.
  • Обсуждаем Frame Machine Notation (FMN), помогающую разработчикам распутать клубок из пасты.

Все мы знаем, как тяжело бывает читать чужой код. Это может быть из-за того, что сложна сама задача или потому, что структура кода слишком… «креативна». Часто эти две проблемы идут рука об руку.

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

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


Чтобы проиллюстрировать ситуацию со спагетти-кодом, нам нужно для начала превратить это:


Хрустящая паста

В это:


Аль денте!

Давайте приступим к готовке.

Неявное состояние


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

Вот простой пример:

(temp < 32)

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

Если я действительно подтвержу, что она проверяет, является ли вода ТВЁРДОЙ [прим. пер.: по шкале Фаренгейта вода замерзает при +32 градусах], то что будет логически означать возвращаемое false?

if (temp < 32) {
	// SOLID water
} else { 
	// not SOLID water. is (LIQUID | GAS)
}

Хотя проверка разделила числа на две группы, на самом деле существует три логических состояния — твёрдое тело, жидкость и газ (SOLID, LIQUID, GAS)!

То есть эта числовая прямая:


разделена проверкой условия следующим образом:

if (temp < 32) { 


} else {


}

Заметьте, что произошло, потому что это очень важно для понимания природы спагетти-кода. Булева проверка разделила числовое пространство на две части, но НЕ категоризировала систему как настоящую логическую структуру из (SOLID, LIQUID, GAS). Вместо этого проверка разделила пространство на (SOLID, всё остальное).

Вот похожая проверка:

if (temp > 212) {
	// GAS water
} else { 
	// not GAS water. is (SOLID | LIQUID)
}

Визуально это будет выглядеть так:

if (temp > 212) {


} else {


}

Заметьте, что:

  1. полное множество возможных состояний нигде не объявляется
  2. нигде в условных конструкциях не объявляются проверяемые логические состояния или группы состояний
  3. некоторые состояния косвенно группируются структурой условной логики и ветвлением

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


Мне всё равно никогда не нравился твой код

Показанный выше код подразумевает существование трёх состояний вещества — SOLID, LIQUID, GAS. Однако согласно научным данным на самом деле есть четыре наблюдаемых состояния, в которые включается плазма (PLASMA) (на самом деле есть и множество других, но нам будет достаточно и этого). Хотя никто не готовит пасту из плазмы, если этот код будет опубликован на Github, а затем его форкнет какой-нибудь аспирант, изучающим физику высоких энергий, то нам придётся поддерживать и это состояние тоже.

Однако при добавлении плазмы показанный выше код наивным образом будет выполнять следующее:

if (temp < 32) {
	// SOLID water
} else { 
	// not SOLID water. is (LIQUID | GAS) + (PLASMA?)
	// how did PLASMA get in here??
}

if (temp > 212) {
	// GAS water + (PLASMA)
	// again with the PLASMA!!
} else { 
	// not GAS water. is (SOLID | LIQUID)
}

Вполне вероятно, что старый код при добавлении ко множеству состояний плазмы сломается в ветвлениях else. К сожалению, ничто в структуре кода не помогает сообщить о существовании нового состояния или повлиять на изменения. Кроме того, любые баги скорее всего окажутся малозаметными, то есть, найти их будет сложнее всего. Просто скажи «нет» насекомым в спагетти.

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

Хоть и не существует способов полностью избавиться от условных проверок данных, любая минимизирующая их техника будет снижать сложность кода.

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

Сначала доведите воду до кипения…


Наука дала названия всем возможных переходам, которые может совершать вещество при изменении температуры.


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

(Примечание: этот псевдокод написал я. Пользуйтесь им в работе только на свой страх и риск.)

class WaterSample {

	temp:int

	Water(temp:int) {
		this.temp = temp
	}

  	// gas -> solid
	func depose() {

		// If not in GAS state, throw an error
		if (temp < WATER_GAS_TEMP)
			throw new IllegalStateError()

		// do depose	
		while (temp > WATER_SOLID_TEMP)
			decreaseTemp(1)	
	}

	// gas -> liquid
	func condense() {

		// If not in GAS state, throw an error
		if (temp < WATER_GAS_TEMP)
			throw new IllegalStateError()

		// do condense	
		while (temp > WATER_GAS_TEMP)
			decreaseTemp(1)	
	}

	// liquid -> gas
	func vaporize() {

		// If not in LIQUID state, throw an error
		if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
			throw new IllegalStateError()

		// do vaporize	
		while (temp < WATER_GAS_TEMP)
			increaseTemp(1)	
	}

	// liquid -> solid
	func freeze() {

		// If not in LIQUID state, throw an error
		if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
			throw new IllegalStateError()

		// do freeze	
		while (temp > WATER_SOLID_TEMP)
			decreaseTemp(1)	
	}

	// solid -> liquid
	func melt() {

		// If not in SOLID state, throw an error
		if (temp > WATER_SOLID_TEMP)
			throw new IllegalStateError()

		// do melt	
		while (temp < WATER_SOLID_TEMP)
			increaseTemp(1)	
	}

	// solid -> gas
	func sublimate() {
		// If not in SOLID state, throw an error
		if (temp > WATER_SOLID_TEMP)
			throw new IllegalStateError()

		// do sublimate	
		while (temp < WATER_GAS_TEMP)
			increaseTemp(1)	
	}

	func getState():string {
		if (temp < WATER_SOLID_TEMP) return "SOLID"
		if (temp > WATER_GAS_TEMP) return "GAS"
		return "LIQUID"		
	}
}

По сравнению с первым примером этот код имеет определённые улучшения. Во-первых, жёстко заданные «магические» числа (32, 212) заменены на константы границ температур состояний (WATER_SOLID_TEMP, WATER_GAS_TEMP). Это изменение начинает делать состояния более явными, хотя и косвенным образом.

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

// liquid -> solid
if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP))
     throw new IllegalStateError()

Эта условная проверка делает следующее:

  1. Проверяет, меньше ли temp граничной температуры GAS
  2. Проверяет, больше ли temp граничной температуры SOLID
  3. Возвращает ошибку, если какая-то из этих проверок оказалась не true

Эта логика сбивает с толку. Во-первых, нахождение в жидком состоянии определяется тем, чем вещество не является — твёрдым телом или газом.

(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid?

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

!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously?

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

bool isLiquidWater = (temp < WATER_GAS_TEMP 
                           && temp > WATER_SOLID_TEMP)
if (!isLiquidWater) throw new IllegalStateError()

Этот код понять проще, потому что состояние isLiquidWater выражено явно.

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

Frame Machine Notation


Frame Machine Notation (FMN) — это предметно-ориентированный язык (Domain Specific Language, DSL), определяющий категоричный, методологический и простой подход к заданию и реализации различных типов автоматов. Для простоты я буду называть автоматы Frame просто «машинами», потому что эта нотация может определять теоретические критерии для любых отличающихся типов (машин состояний, магазинных автоматов и вершины эволюции автоматов — машин Тьюринга). Чтобы знать о различных видах автоматов и их применении, рекомендую изучить страницу в Википедии.

Хотя теория автоматов может быть интересной (ОЧЕНЬ сомнительное заявление), в этой статье мы сосредоточимся на практическом применении этих мощных концепций для создания систем и написания кода.

Для решения этой задачи Frame вводит стандартизированную нотацию, работающую на трёх интегрированных уровнях:

  1. Текстовый DSL для задания контроллеров Frame с помощью элегантного и лаконичного синтаксиса
  2. Набор эталонных паттернов кодинга для реализации объектно-ориентированных классов в виде машин, которые Frame называет «контроллерами»
  3. Визуальная нотация, в которой FMN используется для выражения сложных операций, которые трудно представить графически — Frame Visual Notation (FVN)

В этой статье я рассмотрю первые два пункта: FMN и эталонные паттерны, а обсуждение FVN оставлю для будущих статей.

Frame — это нотация, отличающаяся некоторыми важными аспектами:

  1. FMN имеет объекты первого уровня, связанные с концепцией автоматов, которые отсутствуют в объектно-ориентированных языках.
  2. Спецификация FMN определяет в псевдокоде стандартные паттерны реализации, демонстрирующие, как можно реализовывать нотацию FMN.
  3. FMN вскоре получит возможность компиляции (работа в процессе) на любом объектно-ориентированном языке

Примечание: эталонная реализация используется для демонстрации абсолютной эквивалентности нотации FMN и простого способа её реализации на любом объектно-ориентированном языке. Вы можете выбрать любой способ.

Сейчас я познакомлю вас с двумя самыми важными объектами первого уровня в Frame  — Frame Events и Frame Controllers.

Frame Events


FrameEvents являются неотъемлемой частью простоты нотации FMN. FrameEvent реализуется как структура или класс, которые минимум имеют следующие переменные-члены:

  • идентификатор сообщений
  • словарь или список параметров
  • возвращаемый объект

Вот псевдокод класса FrameEvent:

class FrameEvent {
   var _msg:String
   var _params:Object
   var _return:Object
   FrameEvent(msg:String, params:Object = null) {
      _msg = msg
      _params = params
   }
}

В нотации Frame используется символ @, идентифицирующий объект FrameEvent. Каждый из обязательных атрибутов FrameEvent имеет специальный токен для доступа к нему:

@|message| : вертикальные черты-скобки ссылаются на атрибут _msg
@[param1] : нотация [] позволяет разыменовывать список параметров переменных
@^ : знак возведения в степень используется здесь и в других контекстах для обозначения атрибута _return

Часто нам не обязательно указывать то, с чем работает FrameEvent. Так как большинство контекстов одновременно работают только с одним FrameEvent, нотацию однозначно можно упростить так, чтобы она использовала только селекторы атрибутов. Следовательно, мы можем упростить доступ:

|buttonClick|         // Select for a "buttonClick" event _msg
[firstName] = "Mark"  // Set firstName _params property to "Mark"
^ = "YES"             // Set the _return object to "YES"

Такая нотация поначалу может показаться странной, но вскоре мы увидим, как такой простой синтаксис для событий сильно упрощает понимание кода FMN.

Frame Controllers


Frame Controller — это объектно-ориентированный класс, упорядоченный чётко заданным образом для реализации машины Frame. Типы контроллеров идентифицируются префиксом #:

#MyController

это эквивалентно следующему объектно-ориентированному псевдокоду:

class MyController {}

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

Контроллеры структурируются таким образом, чтобы содержать блоки различного типа, которые идентифицируются по тире, окружающим имя типа блока:

#MyController<br>   -block 1-   -block 2-   -block 3-

В контроллере может быть не более одного экземпляра каждого блока, а типы блоков могут содержать только определённые типы подкомпонентов. В этой статье мы исследуем только блок -machine-, который может содержать только состояния. Состояния идентифицируются по токену префикса $.

Здесь мы видим FMN для контроллера, содержащего машину только с одним состоянием:

#MyController   // controller declaration
    -machine-   // machine block
        $S1     // state declaration

Вот реализация приведённого выше кода FMN:

class MyController {
    // -machine-
    
    var _state(e:FrameEvent) = S1   // initialize state variable
                                    // to $S1
    func S1(e:FrameEvent) {   // state $S1 does nothing
    }
}

Реализация блока machine состоит из следующих элементов:

  1. переменная _state, которая ссылается на функцию текущего состояния. Она инициализируется с функцией первого состояния в контроллере
  2. один или несколько методов состояний

Метод состояния Frame определяется как функция со следующей сигнатурой:

func MyState(e:FrameEvent);

После задания этих основ реализации блока machine, мы можем увидеть, как хорошо объект FrameEvent взаимодействует с машиной.

Блок интерфейса


Взаимодействие FrameEvents, управляющих работой машина — это сама суть простоты и мощи нотации Frame. Однако мы ещё не ответили на вопрос, откуда же берутся FrameEvents — как они попадают в контроллер, чтобы управлять им? Один из вариантов: внешние клиенты сами могут создавать и инициализировать FrameEvents, а затем напрямую вызывать метод, на который указывает переменная-член _state:

myController._state(new FrameEvent("buttonClick"))

Гораздо лучшей альтернативой будет создание общего интерфейса, оборачивающего прямой вызов переменной-члена _state:

myController.sendEvent(new FrameEvent("buttonClick"))

Однако наиболее беспроблемный способ, соответствующий обычному способу создания объектно-ориентированного ПО — это создание общих методов, отправляющих событие от лица клиента во внутреннюю машину:

class MyController {
    func buttonClick() {
        FrameEvent e = new FrameEvent("buttonClick")
        _state(e)
       return e._return
    }
}

Frame определяет синтаксис для блока интерфейса, который содержит методы, превращающие вызовы в общий интерфейс для FrameEvents.

#MyController
    -interface-
        buttonClick
...

У блока interface ещё много других особенностей, но этот пример даёт нам общее представление о том, как это работает. Дальнейшее объяснение я дам в следующих статьях серии.

Теперь давайте продолжим изучение работы автомата Frame.

Обработчики событий


Хоть мы и показали, как определять машину, у нас пока нет нотации с которой можно что-нибудь делать. Для обработки событий нам нужно 1) иметь возможность выбора события, которое нужно обрабатывать и 2) привязать его к выполняемому поведению.

Вот простой контроллер Frame, обеспечивающий инфраструктуру для обработки событий:

#MyController        // controller declaration
    -machine-        // machine block
        $S1          // state declaration
            |e1| ^   // e1 event handler and return

Как сказано выше, для доступа к атрибуту _msg события FrameEvent нотация FMN использует скобки из вертикальных линий:

|messageName|

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

class MyController {             // #MyController
    // -machine-
    var _state(e:FrameEvent) = S1
    func S1(e:FrameEvent) {      // $S1
        if (e._msg == "e1") {    // |e1|
            return               // ^
        }
    }
}

Здесь мы видим, насколько чётко нотация FMN соответствует паттерну реализации, который удобен в понимании и кодинге.

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

Однофокусные машины


Выше мы рассматривали контроллер без состояний, который был довольно бесполезным.

#MyController

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

Для начала давайте посмотрим, как будет реализован класс со всего одним (подразумеваемым) состоянием:

class Mono {
    String status() { return "OFF" }
}

Здесь не объявляется и даже не подразумевается никакого состояния, но давайте предположим, что если код что-то делает, то система находится в состоянии «Working».

Также мы введём важную идею: вызовы интерфейса будут рассматриваться аналогичными отправке события объекту. Поэтому представленный выше код можно рассматривать как способ передачи события |status| классу Mono, всегда находящемуся в состоянии $Working.

Визуализировать эту ситуацию можно с помощью таблицы привязок событий:


Теперь давайте рассмотрим FMN, которая демонстрирует тот же функционал и соответствует той же таблице привязок:

#Mono
    -machine-
        $Working
            |status|
                ^("OFF")

Вот как выглядит реализация:

class Mono {					// #Mono

	// -machine- 				

	var _state(e:FrameEvent) = Working  	// initialize start state

	func Working(e:FrameEvent) { 	        // $Working
        	if (e._msg == "status") {	// |status| 
            		e._return = "OFF"	
			return			// ^("OFF")
        	}          
    	}
}

Можно заметить, что мы также ввели новую нотацию для оператора return, которая обозначает вычисление выражения и возврат результата интерфейсу:

^(return_expr)

Этот оператор эквивалентен

@^ = return_expr

или просто

^ = return_expr

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

Включаем плиту


Пока мы видели контроллер с 0 состояний и контроллер с 1 состоянием. Они ещё не особо полезны, но мы уже на пороге чего-то интересного.

Чтобы сварить нашу пасту, нужно для начала включить плиту. Ниже показан простой класс Switch с единственной булевой переменной:

class Switch {

	boolean _isOn;

	func status() {
		if (_isOn) {
			return "ON";
		} else {
			return "OFF";
		}
	}
}

Хоть с первого взгляда это и не очевидно, но показанный выше код реализует следующую таблицу привязок событий:


Для сравнения, вот FMN для того же поведения:

#Switch1
	-machine-
		$Off
			|status| ^("OFF") 
		$On
			|status| ^("ON")

Теперь мы видим, насколько точно нотация Frame соответствует цели нашего кода — привязке события (вызова метода) к поведению на основании состояния, в котором находится контроллер. Кроме того, структура реализации также соответствует таблице привязок:

class Switch1 {                     		// #Switch1

	// -machine- 

	var _state(e:FrameEvent) = Off  

	func Off(e:FrameEvent) {		// $Off
		if (e._msg == "status") {	// |status|
			e._return = "OFF"		
			return			// ^("OFF")
    		} 
  	}

	func On(e:FrameEvent) {			// $On
		if (e._msg == "status") {	// |status|
			e._return = "ON"
			return			// ^("ON")
    		} 
  	}
}

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

Однако у нашего переключателя есть заметная проблема функционала. Он инициализируется в состоянии $Off, но никак не может переключиться в состояние $On! Для этого нам нужно ввести оператор изменения состояния.

Изменяем состояние


Оператор изменения состояния выглядит следующим образом:

->> $NewState

Теперь мы можем использовать этот оператор для переключения между $Off и $On:

#Switch2
	-machine-
		$Off
			|toggle| ->> $On ^
			|status| ^("OFF") 
		$On
			|toggle| ->> $Off ^
			|status| ^("ON")

А вот соответствующая таблица привязки событий:


Новое событие |toggle| теперь запускает изменение, которое просто циклически переключается между двумя состояниями. Как же можно реализовать операцию изменения состояния?

Проще некуда. Вот реализация Switch2:

class Switch2 {				// #Switch2

	// -machine- 

	var _state(e:FrameEvent) = Off  

	func Off(e:FrameEvent) {
		if (e._msg == "toggle") {	// |toggle|
      			_state = On		// ->> $On
      			return			// ^
    		} 
	  	if (e._msg == "status") {	// |status|
      			e._return = "OFF"
      			return			// ^("OFF")
    		} 
	}

	func On(e:FrameEvent) {
    		if (e._msg == "toggle") { 	// |toggle|
      			_state = Off		// ->> $Off
      			return			// ^("OFF")
    		} 
		if (e._msg == "status") {	// |status|
      			e._return = "ON"
      			return			// ^("ON")
    		} 
  	}
}

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

#Switch3
	-machine-
		$Off
			|turnOn| ->> $On ^
			|toggle| ->> $On ^
			|status| ^("OFF") 
		$On
			|turnOff| ->> $Off ^
			|toggle| ->> $Off ^
			|status| ^("ON")

В отличие от события |toggle|, если |turnOn| передаётся, когда Switch3 уже включен или |turnOff|, когда он уже выключен, то сообщение игнорируется и ничего не происходит. Это небольшое улучшение даёт клиенту возможность явно указывать состояние, в котором должен находиться переключатель:

class Switch3 { 					// #Switch3

	// -machine- 

	var _state(e:FrameEvent) = Off  

	/**********************************

		$Off
			|turnOn| ->> $On ^
			|toggle| ->> $On ^
			|status| ^("OFF") 

	***********************************/

	func Off(e:FrameEvent) {
		if (e._msg == "turnOn") {		// |turnOn|
			_state = On			// ->> $On
			return				// ^
		} 
		if (e._msg == "toggle") {		// |toggle|
			_state = On			// ->> $On
			return				// ^
		} 
		if (e._msg == "status") {		// |status|
			e._return = "OFF"	
			return				// ^("OFF")
		} 
	}

	/**********************************

		$On
			|turnOff| ->> $Off ^
			|toggle| ->> $Off ^
			|status| ^("ON")

	***********************************/

	func On(e:FrameEvent) {
		if (e._msg == "turnOff") {		// |turnOff|
			_state = Off			// ->> $Off
			return				// ^
		} 
		if (e._msg == "toggle") { 		// |toggle|
			_state = Off			// ->> $Off
			return				// ^
		} 
		if (e._msg == "status") {		// |status|
			e._return = "ON"
			return				// ^("ON")
		} 
	}
}

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

Создав автомат Switch, мы можем включить огонь и начать готовку!

Зондируем состояние


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

Однако это предположение не всегда верно. В некоторых ситуациях требуется проверка (или «зондирование») данных для определения текущего логического состояния:

  1. изначальное восстановленное состояние — когда машина восстановлена из постоянного состояния
  2. внешнее состояние — определяет «фактическую ситуацию», существующую в среде в момент создания, восстановления или работы машины
  3. изменчивое внутреннее состояние — когда часть внутренних данных, управляемых работающей машиной, могут изменяться вне пределов контроля машины

Во всех этих случаях данные, окружение или то и другое необходимо «прозондировать», чтобы определить ситуацию и соответствующим образом задать состояние машины. В идеале эту булеву логику можно реализовать в одной функции, определяющей верное логическое состояние. Для поддержки этого паттерна в нотации Frame есть особый тип функции, зондирующей вселенную и определяющей ситуацию в текущий момент времени. Такие функции обозначаются префиксом $ перед названием метода, возвращающего ссылку на состояние:

$probeForState()

В нашей ситуации такой метод можно реализовать следующим образом:

func probeForState():FrameState {
    if (temp < 32) return Solid
    if (temp < 212) return Liquid
    return Gas
}

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

->>  $probeForState()

Механизм реализации выглядит так:

_state = probeForState()

Метод зондирования состояния — это пример нотации Frame для управления состоянием заданным образом. Далее мы также изучим важную нотацию для управления FrameEvents.

Наследование поведений и диспетчер


Наследование поведений и диспетчер — это мощная парадигма программирования и последняя тема о нотации Frame в данной статье.

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

Для этого машины можно реализовывать с помощью техники под названием method chaining. Нотация FMN для передачи события из одного состояния в другое — это диспетчер =>:

$S1 => $S2

Этот оператор FMN можно реализовать следующим образом:

func S1(e:FrameEvent) {
    S2(e)  // $S1 => $S2
}

Теперь мы видим, насколько просто объединять в цепочки методы состояний. Давайте применим эту технику к достаточно сложной ситуации:

#Movement
	-machine-
		$Walking => $Moving
			|getSpeed| ^(3)
			|isStanding| ^(true)

		$Running => $Moving
			|getSpeed| ^(6)
			|isStanding| ^(true)

		$Crawling => $Moving
			|getSpeed| ^(.5)
			|isStanding| ^(false)

		$AtAttention => $Motionless
			|isStanding| ^(true)

		$LyingDown => $Motionless
			|isStanding| ^(false)

		$Moving
			|isMoving| ^(true)

		$Motionless
			|getSpeed| ^(0)
			|isMoving| ^(false)

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


Благодаря изученным нами техникам реализация будет очень простой:

class Movement {					// #Movement

	// -machine- 

	/**********************************

		$Walking => $Moving
			|getSpeed| ^(3)
			|isStanding| ^(true)

	***********************************/

	func Walking(e:FrameEvent) {
		if (e._msg == "getSpeed") {	
			e._return = 3		
			return			
		} 
		if (e._msg == "isStanding") {	
			e._return = true		
			return			
		}
		
		Moving(e) 			// $Walking => $Moving
	}

	/**********************************

		$Running => $Moving
			|getSpeed| ^(6)
			|isStanding| ^(true)

	***********************************/

	func Running(e:FrameEvent) {
		if (e._msg == "getSpeed") {	
			e._return = 6		
			return			
		} 
		if (e._msg == "isStanding") {	
			e._return = true		
			return			
		}
		
		Moving(e) 			// $Running => $Moving
	}

	/**********************************

		$Crawling => $Moving
			|getSpeed| ^(.5)
			|isStanding| ^(false)

	***********************************/

	func Crawling(e:FrameEvent) {
		if (e._msg == "getSpeed") {	
			e._return = .5		
			return			
		} 
		if (e._msg == "isStanding") {	
			e._return = false		
			return			
		}
		
		Moving(e) 			// $Crawling => $Moving
	}

	/**********************************

		$AtAttention => $Motionless
			|isStanding| ^(true)

	***********************************/

	func AtAttention(e:FrameEvent) {
 
		if (e._msg == "isStanding") {	
			e._return = true		
			return			
		}
		
		Motionless(e) 			// $AtAttention => $Motionless
	}

	/**********************************

		$LyingDown => $Motionless
			|isStanding| ^(false)

	***********************************/


	func LyingDown(e:FrameEvent) {

		if (e._msg == "isStanding") {	
			e._return = false		
			return			
		}
		
		Motionless(e) 			// $AtAttention => $Motionless
	}

	/**********************************

		$Moving
			|isMoving| ^(true)

	***********************************/


	func Moving(e:FrameEvent) {

		if (e._msg == "isMoving") {	
			e._return = true		
			return			
		}
	}

	/**********************************
		$Motionless
			|getSpeed| ^(0)
			|isMoving| ^(false)

	***********************************/

	func Motionless(e:FrameEvent) {

		if (e._msg == "getSpeed") {	
			e._return = 0		
			return			
		}		
		if (e._msg == "isMoving") {	
			e._return = false		
			return			
		}
	}
}

Машина для воды


Теперь мы обладаем основами знаний о FMN, позволяющими нам понять, как заново реализовать класс WaterSample с состояниями и в гораздо более интеллектуальном виде. Также мы сделаем полезным его для нашего физика-аспиранта и добавим в неё новое состояние $Plasma:


Вот как выглядит полная реализация FMN:

#WaterSample

	-machine-

	$Begin
		|create|	
			// set temp to the event param value
			setTemp(@[temp]) 	
			
			// probe for temp state and change to it
			->> $probeForState() ^	 
	
	$Solid => $Default
		|melt| 
			doMelt() ->> $Liquid ^
		|sublimate|
			doSublimate() ->> $Gas ^
		|getState|
			^("SOLID")

	$Liquid => $Default
		|freeze|
			doFreeze() ->> $Solid ^
		|vaporize|
			doVaporize() ->> $Gas ^
		|getState|
			^("LIQUID")

	$Gas => $Default
		|condense|
			doCondense() ->> $Liquid ^
		|depose|
			doDepose() ->> $Solid ^
		|ionize|
			doIonize() ->> $Plasma ^
		|getState|
			^("GAS")

	$Plasma => $Default
		|recombine|
			doRecombine() ->> $Gas ^
		|getState|
			^("PLASMA")

	$Default 
		|melt| throw new InvalidStateError() 
		|sublimate| throw new InvalidStateError() 
		|freeze| throw new InvalidStateError() 
		|vaporize| throw new InvalidStateError() 
		|condense| throw InvalidStateError() 
		|depose| throw InvalidStateError() 
		|ionize| throw InvalidStateError() 
		|recombine| throw InvalidStateError() 
		|getState| throw InvalidStateError() 

Как видите, у нас есть начальное состояние $Begin, которое отвечает на сообщение |create| и сохраняет значение temp. Функция зондирования сначала проверяет исходное значение temp для определения логического состояния, а затем выполняет переход машины в это состояние.

Все физические состояния ($Solid, $Liquid, $Gas, $Plasma) наследуют защитное поведение от состояния $Default. Все события, являющиеся недопустимыми для текущего состояния, передаются в состояние $Default, которое выдаёт ошибку InvalidStateError. Это показывает, как простое защитное программирование можно реализовать с помощью наследования поведений.

А теперь реализация:

class WaterSample {

	// -machine- 

	var _state(e:FrameEvent) = Begin  

	/**********************************

	$Begin
		|create|	
			// set temp to the event param value
			setTemp(@[temp]) 	
			
			// probe for temp state and change to it
			->> $probeForState() ^	

	***********************************/

	func Begin(e:FrameEvent) {
		if (e._msg == "create") {	
			setTemp(e["temp"])
			_state = probeForState()		
			return			
		} 
	}

	/**********************************

	$Solid => $Default
		|melt| 
			doMelt() ->> $Liquid ^
		|sublimate|
			doSublimate() ->> $Gas ^
		|sublimate|
			^("SOLID")

	***********************************/

	func Solid(e:FrameEvent) {
		if (e._msg == "melt") {	
			doMelt()
			_state = Liquid		
			return			
		} 
		if (e._msg == "sublimate") {	
			doSublimate()
			_state = Gas		
			return			
		} 
		if (e._msg == "getState") {	
			e._return = "SOLID"	
			return			
		} 

		Default(e)
	}

	/**********************************

	$Liquid => $Default
		|freeze|
			doFreeze() ->> $Solid ^
		|vaporize|
			doVaporize() ->> $Gas ^
		|getState|
			^("LIQUID")

	***********************************/

	func Liquid(e:FrameEvent) {
		if (e._msg == "freeze") {	
			doFreeze()
			_state = Solid		
			return			
		} 
		if (e._msg == "vaporize") {	
			doVaporize()
			_state = Gas		
			return			
		} 
		if (e._msg == "getState") {	
			e._return = "LIQUID"	
			return			
		} 

		Default(e)
	}

	/**********************************

	$Gas => $Default
		|condense|
			doCondense() ->> $Liquid ^
		|depose|
			doDepose() ->> $Solid ^
		|ionize|
			doIonize() ->> $Plasma ^
		|getState|
			^("GAS")

	***********************************/

	func Gas(e:FrameEvent) {
		if (e._msg == "condense") {	
			doCondense()
			_state = Liquid		
			return			
		} 
		if (e._msg == "depose") {	
			doDepose()
			_state = Solid		
			return			
		} 
		if (e._msg == "ionize") {	
			doIonize()
			_state = Plasma		
			return			
		} 
		if (e._msg == "getState") {	
			e._return = "GAS"	
			return			
		} 

		Default(e)
	}

	/**********************************

	$Plasma => $Default
		|recombine|
			doRecombine() ->> $Gas ^
		|getState|
			^("PLASMA")

	***********************************/

	func Plasma(e:FrameEvent) {
		if (e._msg == "recombine") {	
			doRecombine()
			_state = Gas		
			return			
		} 
		if (e._msg == "getState") {	
			e._return = "PLASMA"	
			return			
		} 

		Default(e)
	}	
	
	/**********************************

	$Default 
		|melt| throw new InvalidStateError() 
		|sublimate| throw new InvalidStateError() 
		|freeze| throw new InvalidStateError() 
		|vaporize| throw new InvalidStateError() 
		|condense| throw InvalidStateError() 
		|depose| throw InvalidStateError() 
		|ionize| throw InvalidStateError() 
		|recombine| throw InvalidStateError() 
		|getState| throw InvalidStateError() 

	***********************************/

	func Default(e:FrameEvent) {
		if (e._msg == "melt") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "sublimate") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "freeze") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "vaporize") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "condense") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "depose") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "ionize") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "recombine") {	
			throw new InvalidStateError()				
		} 
		if (e._msg == "getState") {	
			throw new InvalidStateError()				
		} 
	}
}

Заключение


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


Терминатор ест пасту (фотография Suzuki san)
В будущих статьях на основании изученных концепций мы создадим ещё большую мощь и выразительность нотации FMN. Со временем я расширю обсуждение до исследования визуального моделирования, которое включает в себя FMN и разрешает задачи неопределённого поведения в современных подходах к моделированию ПО.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+9
Комментарии16

Публикации

Изменить настройки темы

Истории

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн