“我喜欢意大利面条的西部菜,我讨厌意大利面条的代码”“意大利面条式代码”是描述软件的理想表达,从认知和美学的角度来看,这都是一团混乱的蒸汽。 在本文中,我将讨论销毁意大利面条代码的三点计划:
- 我们讨论为什么意大利面条代码不那么美味。
- 介绍一下代码实际执行的功能。
- 我们正在讨论帧机器符号(FMN) ,它可以帮助开发人员解开一团糊状。
我们都知道阅读别人的代码有多困难。 这可能是由于任务本身很困难,或者由于代码的结构过于“创造性”。 通常这两个问题并存。
挑战是艰巨的任务,通常只有革命性的发现才能简化挑战。 但是,碰巧软件结构本身增加了不必要的复杂性,这个问题
值得解决。
意粉代码的丑陋在于其复杂的条件逻辑。 而且,如果没有许多棘手的if-then-else构造,虽然生活很难想象,但本文将为您提供更好的解决方案。
为了说明意大利细面条的代码情况,我们需要先将其打开:
脆皮意大利面在此:
Al dente!让我们开始做饭。
内隐状态
要制作面食,我们肯定需要水做饭。 但是,即使是涉及意大利面条代码的看似简单的元素也可能造成混乱。
这是一个简单的示例:
(temp < 32)
此检查实际上是做什么的? 显然,它将数字线分为两部分,但是这些部分
是什么
意思呢? 我认为您可以做出合理的假设,但问题是代码实际上并未
明确传达这
一点 。
如果我真的确认她检查水是否为固体
[大约。 车道:根据华氏度表,水冻结在+32度处] ,从逻辑上讲,返回假是什么意思?
if (temp < 32) {
尽管检查将数字分为两组,但实际上存在三种逻辑状态-固体,液体和气体(固体,液体和气体)!
也就是说,此数字行:
按条件拆分如下:
if (temp < 32) {
} else {
}
注意发生了什么,因为这对于理解意大利面条代码的性质非常重要。 布尔检查将数字空间分为两部分,但并未将系统归类为(SOLID,LIQUID,GAS)的真实逻辑结构。 取而代之的是,检查将空间分为(SOLID,其他所有内容)。
这是类似的检查:
if (temp > 212) {
从视觉上看,它将如下所示:
if (temp > 212) {
} else {
}
注意:
- 完整的可能状态集未在任何地方宣布
- 条件构造中无处声明可验证的逻辑状态或状态组
- 一些状态通过条件逻辑和分支的结构间接分组
这样的代码是易碎的,但是很常见,并且不大到导致其支持问题。 因此,让情况变得更糟。
反正我从来都不喜欢你的代码上面显示的代码暗含物质的三种状态-固体,液体,气体。 但是,根据科学数据,实际上存在
四个可观察到的状态,其中包含了等离子体(PLASMA)(实际上,还有许多其他状态,但这对我们来说已经足够了)。 尽管没有人准备用等离子体制备糊剂,但是如果此代码在Github上发布,然后由研究高能物理的一些研究生分叉,那么我们也必须保持这种状态。
但是,添加等离子后,上面显示的代码将天真地执行以下操作:
if (temp < 32) {
当旧代码添加到许多等离子体状态时,很可能会在else分支中中断。 不幸的是,代码结构中没有任何内容可以帮助报告新状态的存在或影响更改。 此外,任何错误都可能不那么明显,也就是说,找到它们将是最困难的。 对意大利面条中的昆虫说不。
简而言之,问题是这样的:布尔检查用于
间接确定状态。 逻辑状态通常没有声明,并且在代码中不可见。 如上所述,当系统添加新的逻辑状态时,现有代码可能会中断。 为避免这种情况,
开发人员应重新检查每个条件检查并分支,以确保代码路径对其
所有逻辑状态仍然有效! 这是大代码片段变得越来越复杂时降级的主要原因。
尽管没有方法可以完全摆脱条件数据检查,但是将其最小化的任何技术都会降低代码的复杂性。
现在,让我们看一下一个类的典型的面向对象的实现,该类创建一个
非常简单的水量模型。 该课程将管理水的状态变化。 在研究了该问题的经典解决方案的问题之后,我们将讨论一个称为
Frame的新符号,并说明它如何应对我们发现的困难。
首先把水烧开...
科学为物质在温度变化时可能发生的所有可能的转变命名。
我们的课程非常简单(并非特别有用)。 它回答了在状态之间进行转换并改变温度直到变得适合于所需目标状态的挑战:
(注意:我编写了此伪代码。仅在自担风险的情况下在您的工作中使用它。)
class WaterSample { temp:int Water(temp:int) { this.temp = temp }
与第一个示例相比,此代码具有某些改进。 首先,将硬编码的“魔术”数字(32、212)替换为状态温度边界的常量(WATER_SOLID_TEMP,WATER_GAS_TEMP)。 这种变化开始使状态更加明确,尽管是间接的。
此代码中还会出现“防御性编程”检查,如果该方法处于不合适的操作状态,则会限制该方法的调用。 例如,如果水不是液体,它就不会冻结-这违反了自然法。 但是添加看门狗条件会使对代码目的的理解变得复杂。 例如:
此条件检查执行以下操作:
- 检查
temp
低于GAS极限温度 - 检查
temp
超过SOLID极限温度 - 如果这些检查之一不正确,则返回错误
这种逻辑令人困惑。 首先,液态是由什么物质决定的-固体或气体。
(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)
其次,代码检查水是否为液态,以查明是否需要返回错误。
!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)
第一次了解状态的双重否定并不容易。 这是一个简化,可以稍微降低表达式的复杂度:
bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError()
此代码更易于理解,因为
isLiquidWater状态是
显式的 。
现在,我们正在探索将
显式状态作为解决问题的最佳方法的技术。 通过这种方法,系统的逻辑状态成为软件的物理结构,从而改善了代码并简化了对代码的理解。
相框机符号
帧机器符号(FMN)是一种领域特定语言(DSL),它定义了一种分类,方法论和简单的方法来定义和实现各种类型的
机器 。 为简单起见,我将框架自动机简称为“机器”,因为这种表示法可以定义任何不同类型(状态机,商店自动机和自动机的最高级发展—图灵机)的理论标准。 要了解不同类型的机器及其应用,我建议研究
Wikipedia上的页面。
尽管自动机理论可能很有趣(一个非常可疑的说法),但在本文中,我们将重点关注这些强大概念在构建系统和编写代码方面的实际应用。
为了解决此问题,Frame引入了一种在三个集成级别上起作用的标准化表示法:
- 用于通过优雅简洁的语法定义帧控制器的文本DSL
- 一组参考编码模式,用于以Frame称为“控制器”的机器形式实现面向对象的类
- 视觉表示法,其中FMN用于表示难以用图形表示的复杂操作- 帧直观表示法(FVN)
在本文中,我将考虑前两点:FMN和参考模式,在以后的文章中,我将不再讨论FVN。
框架是一种具有以下几个重要方面的符号:
- FMN具有与自动机概念相关的第一类对象,而这些对象在面向对象的语言中不可用。
- FMN规范在伪代码中定义了标准实现模式,这些模式演示了如何实现FMN表示法。
- FMN很快将能够以任何面向对象的语言进行编译(正在进行中)
注意:参考实现用于演示FMN表示法的绝对等价以及以任何面向对象的语言实现它的简单方法。 您可以选择任何方法。
现在,我将向您介绍Frame-
Frame Events和
Frame Controllers中两个最重要的第一级对象。
框架事件
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 } }
框架符号使用
@符号,它标识FrameEvent对象。 每个必需的FrameEvent属性都有一个特殊的令牌可以访问它:
@|message| : - _msg @[param1] : [] @^ : _return
通常,我们不必指定FrameEvent可以使用的功能。 由于大多数上下文一次只能使用一个FrameEvent,因此可以简化表示法,使其仅使用属性选择器。 因此,我们可以简化访问:
|buttonClick|
这种表示法一开始可能看起来很奇怪,但是很快我们将看到事件的这种简单语法如何极大地简化了对FMN代码的理解。
框架控制器
框架控制器是一个面向对象的类,以定义良好的方式排序以实现框架机器。 控制器类型由
#前缀标识:
#MyController
这等效于以下面向对象的伪代码:
class MyController {}
显然,该类不是特别有用。 为了使他可以做某事,控制器至少需要一种状态来响应事件。
控制器的结构使其包含各种类型的块,这些块由围绕块类型名称的短划线标识:
#MyController<br> -block 1- -block 2- -block 3-
一个控制器每个块最多只能有一个实例,并且块类型只能包含某些类型的子组件。 在本文中,我们仅检查
-machine-块,该块只能包含状态。 状态由
$前缀标记标识。
在这里,我们看到包含仅具有一种状态的机器的控制器的FMN:
#MyController
这是上面的FMN代码的实现:
class MyController {
机器块的实现包括以下元素:
- _state变量,指的是当前状态的函数。 通过控制器中的第一个状态功能对其进行初始化。
- 一种或多种状态方法
框架状态方法定义为具有以下签名的函数:
func MyState(e:FrameEvent);
在定义了机器模块实现的这些基础之后,我们可以看到FrameEvent对象与机器的交互程度。
接口单元
框架事件的交互控制机器的运行是框架表示法的简单性和强大功能的本质。 但是,我们尚未回答这个问题,FrameEvents来自哪里-它们如何进入控制器进行控制? 一种选择:外部客户端本身可以创建和初始化FrameEvent,然后直接调用_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)将其附加到正在执行的行为上。
这是一个简单的框架控制器,提供了处理事件的基础结构:
#MyController
如上所述,要访问
_msg
事件的
_msg
属性,FMN表示法使用垂直线中的括号:
|messageName|
FMN还使用表示返回语句的指数令牌。 上面显示的控制器将实现如下:
class MyController {
在这里,我们看到FMN标记与一种易于理解和编码的实现模式对应的清晰程度。
在设置了事件,控制器,机器,状态和事件处理程序的这些基本方面之后,我们可以在它们的帮助下着手解决实际问题。
单焦点机
在上面,我们看了一个非常无用的无状态控制器。
#MyController
在公用事业的食物链中,迈向更高一步的是具有单一状态的一类,尽管这不是没有用的,但它简直无聊。 但至少他至少在
做某事 。
首先,让我们看看如何实现只有一个(隐含)状态的类:
class Mono { String status() { return "OFF" } }
这里没有声明甚至没有隐含任何状态,但是我们假设如果代码执行了某些操作,则系统处于“工作”状态。
我们还将介绍一个重要的想法:接口调用将被视为类似于将事件发送给对象。 因此,以上代码可被视为传输|状态|的方法。 Mono类,始终处于$ Working状态。
可以使用事件绑定表将这种情况可视化:
现在让我们看一下FMN,它展示了相同的功能并匹配了相同的绑定表:
#Mono -machine- $Working |status| ^("OFF")
这是实现的样子:
class Mono {
您可能会注意到,我们还为
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")
现在,我们了解框架符号与代码目的的精确匹配-根据控制器所在的状态将事件(方法调用)附加到行为。 另外,实现结构还对应于绑定表:
class Switch1 {
该表使您可以快速了解控制器在各种状态下的用途。 帧标记结构和实现模式都具有相似的优点。
但是,我们的交换机存在明显的功能问题。 它被初始化为状态$ Off,但是不能切换到状态$ On! 为此,我们需要输入
状态更改运算符。
变更状态
状态更改语句如下:
->> $NewState
现在我们可以使用此运算符在$ Off和$ On之间切换:
#Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON")
这是对应的事件绑定表:
新事件|切换| 现在会触发一个更改,只需在两个状态之间循环即可。 如何执行状态更改操作?
没有地方比这容易。 这是Switch2的实现:
class Switch2 {
您还可以在Switch2中进行最后的改进,以便它不仅允许您在状态之间进行切换,而且可以显式设置状态:
#Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON")
与| toggle |事件不同,如果| turnOn | 当Switch3处于打开状态或| TurnOff |处于关闭状态时发送消息,该消息将被忽略并且没有任何反应。
这种小的改进使客户端能够明确指示开关应处于的状态: class Switch3 {
我们交换机发展的最后一步表明,了解FMN控制器的用途是多么容易。相关代码演示了使用框架机制实现起来很容易。创建了Switch机器之后,我们可以打开火炉并开始烹饪!声音状态
自动机的一个关键(尽管微妙)方面是机器的当前状态是某种情况(例如,开机)或某种数据或环境分析的结果。当机器已切换到所需状态时,即表示已隐含。没有汽车的知识,情况就不会改变。但是,这种假设并不总是正确的。在某些情况下,需要对数据进行验证(或“感应”)以确定当前的逻辑状态:- 初始还原状态 -当计算机从恒定状态还原时
- 外部状态 -定义在创建,恢复或操作机器时环境中存在的“实际情况”
- 不稳定的内部状态 -由运行中的机器管理的内部数据的一部分可以在机器的控制范围之外更改时
在所有这些情况下,都必须对“数据”,“环境”或“两者”进行“探测”,以便确定情况并相应地设置计算机的状态。理想情况下,可以在定义正确逻辑状态的单个函数中实现此布尔逻辑。为了支持此模式,帧符号具有一种特殊类型的功能,该功能可探测Universe并确定当前时间的情况。此类函数由返回状态的链接的方法名称前的$前缀表示: $probeForState()
在我们的情况下,可以按以下方式实现这种方法: func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas }
如我们所见,该方法仅返回对与正确逻辑状态相对应的状态函数的引用。然后可以使用此感应功能进入正确的状态: ->> $probeForState()
实现机制如下所示: _state = probeForState()
状态感测方法是用于以给定方式管理状态的帧符号的示例。接下来,我们还将学习管理FrameEvent的重要符号。行为继承和调度程序
行为继承和调度程序是一种强大的编程范例,也是本文中有关框架表示法的最后一个主题。框架使用行为的继承,而不是数据或其他属性的继承。对于此状态,如果初始状态不处理事件(或者,如我们将在下一篇文章中看到的那样,只想将其传递),则将FrameEvents发送到其他状态。这一系列事件可以达到任何期望的深度。为此,可以使用称为方法链接的技术来实现机器。用于将事件从一种状态发送到另一种状态的FMN标记是调度程序 =>: $S1 => $S2
此FMN语句可以按以下方式实现: func S1(e:FrameEvent) { S2(e)
现在我们看到链接状态方法是多么容易。让我们将此技术应用于一个相当困难的情况: #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 {
水机
现在,我们有了有关FMN的知识基础,使我们能够了解如何使用状态以及更智能的方式重新实现WaterSample类。我们还将使其对我们的研究生物理学家有用,并为其添加新的$等离子状态:完整的FMN实施如下所示: #WaterSample -machine- $Begin |create|
如您所见,我们的初始状态为$ Begin,它会响应消息| create |。并保留价值temp
。传感功能首先检查初始值temp
以确定逻辑状态,然后执行机器到该状态的转换。所有物理状态($固态,$液体,$气体,$等离子)都从$默认状态继承保护行为。对于当前状态无效的所有事件都将传递到$ Default状态,这将引发InvalidStateError错误。这显示了如何使用行为继承来实现简单的防御性编程。现在执行: class WaterSample {
结论
自动机是计算机科学的基本概念,仅在软件和硬件开发的专门领域中使用时间已久。 Frame的主要任务是创建一种描述自动机的符号,并设置简单的模式来编写代码或为实现它们而使用的“机制”。我希望Frame表示法将改变程序员看待计算机的方式,从而提供一种简单的方法将它们付诸实践,以执行日常编程任务,当然,也可以将它们从代码中省去。终结者吃面食(Suzuki san摄)在未来的文章中,基于我们所学的概念,我们将为FMN表示法创造更大的功能和表现力。随着时间的流逝,我将把讨论扩展到视觉建模的研究,其中包括FMN,并解决现代软件建模方法中行为不确定的问题。