65.9K
CodeProject 正在变化。 阅读更多。
Home

一种用于保龄球的商业智能方法

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018年3月8日

MIT

27分钟阅读

viewsIcon

9671

downloadIcon

91

关于JavaScript中的数据、上下文、类架构和十瓶保龄球。

引言

为十瓶保龄球比赛计分是学生和求职者都常遇到的编程挑战。无论使用何种语言或平台,编程问题通常都通过“当前帧”的概念来解决。当前帧(从1到10)在一个变量中维护,滚球值(即滚球击倒的球瓶数量)应用于其以进行计分。通常会有变量来跟踪是否存在“击倒”或“补中”,这意味着滚球值也应用于之前的帧。本质上,这种方法将当前帧作为相关的操作单元进行管理,并相对于它进行计分。

然而,具有商业智能背景的专业人士可能会感知到更细粒度的操作单元:即“滚球”。从这个角度来看,“帧”的概念并非基本概念,而是从一系列滚球中派生出来的。滚球成为组织我们工作的操作单元,而帧则是派生的信息单元。

在本文中,“保龄球”一词特指十瓶保龄球。对于不熟悉这项运动或其计分规则的人,维基百科上的相关条目很有启发性[1]。在简要介绍商业智能以及“操作”和“信息”上下文的含义之后,本文将介绍一个用JavaScript编写的计分应用程序,它可能是由数据仓库或商业智能专业人员设计的。本文将关注JavaScript中的架构问题,包括类继承设计和命名空间策略的应用。

商业智能视角

OLAP.com提供了“商业智能”的有用定义

引用

商业智能 (BI) 一词是指用于收集、整合、分析和呈现商业信息的技术、应用程序和实践。商业智能的目的是支持更好的商业决策。

摘自 OLAP.com,《商业智能》[2]

尽管该描述的第一句话很有价值,但第二句话才反映了商业智能的核心。商业智能从业者致力于提供及时、可靠的信息,这些信息来源于组织通过其日常运营和记录保存所产生的数据,以支持明智的决策。

值得注意的是,“商业智能”一词作为这种功能的标签,在私营企业中于20世纪80年代和90年代开始兴起[3]。在公共领域,高等教育领域的专业人士甚至更早,在20世纪60年代初,就正式组织了“机构研究”领域[4]。尽管机构研究根植于社会科学,而商业智能则植根于信息技术,但这两个领域是真正的兄弟,经常寻求实现相同的目标:通过利用组织数据来帮助决策者做出明智的决策。

赋予数据意义

商业智能有各种备受推崇的方法论,但无论从业者选择何种方法,一个简单的事实都呈现出一个不可避免的症结:数据只有在特定上下文中才有意义。为了说明这一点,请考虑以下数据:

42

 

这意味着什么?它好吗?我们应该采取行动吗?如果没有额外的上下文,这个数字就没有意义。它只是42。然而,有了上下文,我们就可以开始理解其意义:

  • “42……我孩子高中英语班上的学生。”这好吗?加上上下文“师生比为42比1”,也许我们会觉得它应该更好。
     
  • “42……百万美元的销售收入。”这也许不错?看起来不错。再添加更多上下文:“去年我们收入4700万;此外,我们今年开销超过5000万。”也许这毕竟不好。
     
  • “42%……我的大学学生在六年内毕业。”这……很糟糕吗?如果你加上“我的大学的竞争对手毕业率为56%”的上下文,那么这看起来很糟糕。但如果再加上这个上下文:“去年这个数字是40%,前年是39%”,那么今年的42%突然被理解为显著的进步。

任何数据只有在特定上下文中才能被理解并获得意义。无论选择哪种BI方法论,人们都可以欣赏两种提供数据意义的通用上下文类别:操作性上下文和信息性上下文。

操作性上下文

我们将“操作性数据”视为在给定业务流程中发生的事件的记录。值得记录的事件因组织所从事工作的性质而异。例如,学校会发现诸如“学生已注册课程”之类的操作性事件很有意义,特别是对于其管理招生、收入和教学的领域。参与零售销售的组织会发现诸如“客户已使用信用卡通过我们的在线商店购买产品”之类的事件对其多项操作很重要,包括库存跟踪、客户关系和财务会计。

操作数据在其操作上下文中是有意义的。尽管这可能看起来是显而易见的陈述,但商业智能专业人员发现自己经常需要解释这一点。在操作事件记录中记录的值仅在该单一事件的上下文中才有意义,因为操作事件是赋予记录值意义的定义上下文。

一种证明这一点的方法是理解通过查看操作记录可以回答的问题的性质,以及不能回答的问题的性质。对于学校来说,“学生x何时注册课程y?”这个问题可以通过审查相关的注册记录并注意其“注册日期”字段中的值来回答。这个问题不仅暗示了操作上下文;它需要操作上下文才能给出有意义的答案。

现在考虑这个问题:“我们今年的班级是比去年更满还是更不满?”在单一注册事件中没有记录“年度满员情况”的概念。检查“注册日期”字段或注册记录中的任何其他字段都无法以有意义的方式回答这个问题。在操作上下文中记录的数据在其描述操作事件方面具有意义。对于超出该操作上下文的问题,它们缺乏意义。

信息性上下文

除了操作性查询之外,决策者还会提出一些问题,这些问题如果得到有意义的回答,将有助于组织实现更具战略性的成果。例如,我们可能会问“我们今年的班级是否比去年更满”,因为我们可能正在决定是否应该雇用更多的教师。这类问题包含的概念在操作性上下文中没有意义,但对明智的决策却非常有意义。

这样的信息性问题通常包含多层附加概念。例如,定义“满员”意味着什么可能是一个关于容量或成本效益的问题……或者两者兼有。将今年与去年关联起来的决定意味着在比较中可以获取有意义的信息,而比较本身就是一个需要自己定义的概念。两年比较的想法自然而然地引出了更长期趋势的概念,从而引出更高层次的战略问题,例如:“我们未来 xxx 年预计会有多少学生?我们应该如何预期这会影响我们在此期间的收入?我们应该如何相应地规划人员配置?”

将这些问题视为存在于“信息”上下文中是很有用的。尽管它们无法通过检查单一操作数据记录来回答,但聚合的完整操作记录集仍然是派生信息以帮助回答信息性问题的有用来源。

从操作数据中获取信息

从操作数据中提取有用信息的关键在于“定义”和“解释”。我们接受一个信息性问题或一组问题,并将其分解为构成定义。我们可以将这些构成部分称为“度量”、“维度”或“信息元素”。无论使用何种术语(该术语通常是所选方法论的函数),这些都指有助于解决战略问题的有意义的细粒度信息单元。

让我们回到我们学校示例的问题:“我们今年的班级是比去年更满还是更不满?”以下是细粒度信息元素及其描述的列表,它们可能开始支持有意义的答案:

课程

可在特定时间提供的课程模板。

课程目录

课程的完整集合。

术语

学生可以注册、参与并完成一个或多个课程实例的特定时间段。

在特定学期开设的课程实例。

班级目录

特定学期开设的所有班级实例的完整集合。

班级招生人数

在给定学期开设的单个班级中注册的学生的不同计数。

学期学生人数

在给定学期注册任何班级的学生的不同计数。注册多个班级的学生只计数一次。

总班级招生人数

在给定学期开设的所有班级中注册的学生总数。注册多个班级的学生,每注册一个班级计数一次。

本年度 - 秋季招生

本年度秋季学期的“总班级招生人数”值。

上年度 - 秋季招生

上年度秋季学期的“总班级招生人数”值。

 

此时,人们可能会认为“本年度 - 秋季招生”与“上年度 - 秋季招生”的比较将满足信息需求,特别是如果我们的战略兴趣在于增加整体招生人数。对该问题的不同解释可能会导致以下附加信息元素:

班级容量

给定班级实例可招收的最大学生人数。

班级入学率

给定班级实例的班级入学人数除以班级容量的值。

满班阈值

百分比值 0.95。我们已决定班级入学率达到 95% 意味着“满班”。

班级数量

一个学期中提供的不同班级实例的总数。

满班数量

达到满班阈值(即班级入学率达到容量的95%)的不同班级实例的数量。

学期班级满员度

给定学期的“满班数量”除以“班级数量”的值。

 

有了这些元素,我们就可以通过比较每年“学期班级满员度”的值来解决这个问题。如果我们的战略兴趣更多地与管理班级规模以提高成本效益有关,这可能是一个很好的问题解释。

值得注意的是,在将我们的信息需求(我们的战略问题)详细列为细粒度信息元素时,每个元素都以其信息上下文表达。所有标题和描述都不需要涉及操作数据或底层业务操作的任何讨论。然而,最终,我们希望利用我们的操作数据并确定它们是否支持我们的信息元素。

定义与解释

操作数据在一定程度上支持信息元素,即人们可以可靠地解释数据以导出该元素。在信息元素不受操作数据支持的情况下——即无法进行可靠推导的情况下——我们会说操作数据不支持信息需求。这是一个很好的信号,可以促使组织调整操作数据的跟踪方式,以便它们能够支持所需信息,或者决定与其他优先级相比,这些信息并不那么重要。将战略需求记录为细粒度元素有助于揭示这些信息差距。

正如我们之前所见,元素定义始于在其信息上下文中理解的有意义的标题和描述。我们添加操作解释来描述如何从操作数据中导出元素。为了说明,我们将为之前的一些示例添加操作解释:

元素标题

描述

操作解释

班级数量

一个学期中开设的不同班级实例的总数。

要导出此元素,请对“班级”表中的记录执行 COUNT DISTINCT 操作,其中字段 STATUS 等于“A”(表示“活动”),字段 TERM 等于所需的学期代码。

班级招生人数

一个学期中单个班级注册学生的唯一计数。

要推导此元素,请对“Enrollments”表中的记录执行 COUNT 操作,其中字段 CLASS_ID 是所需班级的标识符,字段 TERM 是所需的学期代码,并且字段 ENRL_STATUS 的值为“E”(表示“成功注册”)。

班级容量

给定班级可以注册的学生总数。

班级容量记录在模板课程记录中,而不是实例班级记录中。对于所需的班级实例,从其在“Class”表中的记录中识别其 COURSE_ID 值。然后选择“Courses”表中与匹配的 COURSE_ID 对应的 ENROLLMENT_CAPACITY 字段的值。

 

我们为信息元素创建“定义”,其中包括如何“解释”操作数据以获取我们战略利益所需信息的说明。基于这些定义,商业智能从业者力求使解释操作数据的过程尽可能准确、高效和自动化,所有这些都是为了使信息的交付对其受众可靠和便捷。

我们来打保龄球吧

保龄球可以理解为具有一个“操作”上下文,其中操作数据在整个比赛过程中进行跟踪,然后从中派生出用于“信息”上下文的计分和显示数据。

保龄球的“操作”上下文

在保龄球比赛中,我们只追踪一个操作事件的数据。这个事件很简单:“球已滚出”,追踪的数据是滚球击倒的球瓶数量。这个操作——“滚球”——在传统的十帧比赛中重复至少十一次,最多二十一次。这不是大量数据——最多二十一条记录,每条记录只有一个数字字段——但它们仍然是操作数据,因为它们代表了比赛期间发生的重要事件的日志。

滚球的顺序也是此数据的重要组成部分。由于保龄球的规则,相同的滚球值以不同的顺序可能会产生不同的分数。因此,保龄球比赛的操作数据可以理解为滚球值的有序列表。

记住操作数据只有在操作上下文中才有意义,当我们看到这个有序的滚球值列表时

5, 4, 10, 6, 3, 7

我们可以回答“我的第二次滚球值是多少?(四)”,或者“我目前滚了多少次球?(六)”这些都是很好的操作问题。我们无法回答的问题,至少在没有进一步解释的情况下无法回答的问题是“我的实时得分是多少?”

保龄球的“信息”上下文

问题“我的实时得分是多少”可以在两种信息上下文中的第一种中得到回答。在这些信息阶段,“帧”的概念也变得有意义,最初用于计分,然后用于显示。

“计分帧”。滚球值的有序列表可以被解释以派生出我们称之为“计分帧”的新对象列表。单个计分帧是一个数据结构,最多可容纳三个滚球值。保龄球的计分规则在此派生中应用,以将每个滚球与一个或多个计分帧关联起来。

例如,以相同的滚球列表作为我们的操作数据

5, 4, 10, 6, 3, 7

根据保龄球的计分规则,前两次滚球形成一个完整的计分帧:5加4得到实时得分9。接下来的三次滚球形成第二个计分帧:第一个10是“全中”——第一次滚球击倒所有球瓶——一个有全中的帧可以将其接下来的两次滚球(6和3)计入得分。这相同的两次滚球(6和3)也被分配到第三个完整的计分帧,留下7形成一个不完整的第四个计分帧。因此,从原始的有序操作数据列表中,我们派生出以下四个计分帧。通过以这种方式将滚球值与计分帧槽关联起来,我们将原始的操作数据赋予了信息性(计分)上下文中的意义。

“显示帧”。我们应用第二层派生,进一步将计分帧解释为一组显示指令,我们称之为“显示帧”。计分逻辑已经解决。现在我们正在处理围绕如何在传统记分卡上反映计分的约定。

例如,如果一帧开始时所有球瓶都被击倒,那么滚球值10通常“不”在记分卡上标记。相反,在帧的第二个“框”槽中标记一个“X”。这仅仅是传统惯例。在另一个平行宇宙中,保龄球记分卡标记可能演变为只在帧的第一个显示槽中输入值10,但在我们的世界中,它是一个“X”在框中。

以上述示例中的四个计分帧为例,我们将推导出以下十个显示帧,以反映完整计分卡当前的显示状态。

尽管计分帧已正确计算,但第四个显示帧的实时得分留空,以遵循不为未完成计分的帧标记实时得分的约定。此外,根据显示约定,第三个用于标记滚球值的槽位仅适用于比赛的最后一帧。

认为我们在保龄球中传统应用的计分卡标记很大程度上是习俗问题,这可能有些奇怪。然而,从商业智能的角度来看,很容易将其视为从第一个派生出的第二个“信息”上下文。这些信息上下文中的每一个都提供了微妙的不同之处。“计分帧”层让我们随时了解比赛的正确总分,这本身就足以实现准确计分。“显示帧”层则为我们提供了如何在传统计分卡上独特呈现该计分的说明。

编写计分应用程序

随附的代码实现了一种受商业智能视角启发的保龄球计分应用程序方法,其中有意义的计分和显示信息从操作数据中派生。代码用JavaScript编写,并使用类架构组织在以下文件中:

MishaBowling.Core.js:这些是核心类,用于管理保龄球比赛中的操作数据(即滚球),并派生计分和显示信息。

游戏

Game类以一系列顺序滚球的形式维护保龄球比赛的操作数据。它公开了开始新游戏、添加滚球以及处理滚球列表以派生ScoringFrameList和DisplayFrameList对象的方法。

计分帧列表

ScoringFrameList类在构造时接收一个滚球列表,并生成一个计分帧列表。该类负责解释滚球列表,确定哪些滚球应该作为哪些特定帧的计分组件进行分配。本质上,这个类实现了保龄球的计分逻辑。

计分帧

ScoringFrame 类是一个数据结构,它将最多三个滚球值与单个顺序帧关联起来。ScoringFrame 对象由 ScoringFrameList 实例化。

显示帧列表

DisplayFrameList 类在构建时接受一个 ScoringFrameList,并从计分帧生成相应的显示帧列表。

DisplayFrame, DisplayFinalFrame

DisplayFrame和DisplayFinalFrame类负责解释单个ScoringFrame,生成数据以支持其显示。这些类包含的逻辑,例如,将正常计分帧中第一次滚球的10解释为第二个(带框)槽中的“X”和第一个(不带框)槽中的空白。DisplayFinalFrame继承自DisplayFrame,并覆盖了比赛最后一帧的解释。

 

MishaBowling.Web.js:这些是用于通过基于Web的客户端管理游戏的类,支持将显示指令渲染为HTML代码。

游戏用户界面

GameUI 类维护一个内部 Game 对象以进行保龄球比赛,并公开用于用户交互和在 HTML Web 上下文中显示分数的可见方法。

HtmlDisplayFrameList

HtmlDisplayFrameList 在构建时接受一个 DisplayFrameList,并生成一个 HTML 字符串,用于在网页上下文中输出。

HtmlDisplayFrame, HtmlDisplayFinalFrame

HtmlDisplayFrame 作为基类。它表示单个“正常”帧的 HTML 表示,通过使用 getHTML() 方法检索 HTML。HtmlDisplayFinalFrame 类继承自 HtmlDisplayFrame,并重写了那些使显示指令转换为 HTML 对于最后一帧有所不同的元素。

 

MishaBowling.Util.js:提供实用工具类 RandomRoller,用于生成可调整能力水平的随机滚球值。

原型世界中的“类”架构

JavaScript 本质上是基于“原型”的,而不是基于“类”的对象方法。像 Java 和 C# 等其他面向对象语言中作为原生构造的“类”概念在 JavaScript 中不存在。JavaScript 对象不过是命名值的集合。即便如此,我们仍然可以创建一个模板来实例化对象,并使用特殊的“原型”属性来模拟类继承。

JavaScript 中的“类”始于一个被视为对象构造函数的函数。按照惯例,这个函数的名称通常大写,并且使用“new”关键字从它实例化对象。在这个构造函数中,经常使用关键字“this”。“this”始终是一个上下文关键字;它根据使用的上下文代表不同的对象。在构造函数的情况下,“this”表示“正在使用此函数实例化的对象”。

为了说明,这是一个简单的“Teacher”类示例。此构造函数接受三个参数,这些参数作为值分配给实例化对象中的命名属性。

function Teacher(title, lastName, grade) {
   this.title    = title;
   this.lastName = lastName;
   this.grade    = grade;
}

var myFavorite = new Teacher("Ms.", "Morrison", 3);
alert(myFavorite.grade);        // displays “3”

一个对象不一定拥有一个命名值才能让代码引用它。下面添加到我们示例中的行是完全可接受的 JavaScript 代码:

var s = myFavorite.schoolName;
alert(s);   // displays “undefined"

没有语法错误,也没有抛出“属性‘schoolName’不存在”异常。变量“s”返回的值只是“undefined”。

当 JavaScript 遇到对属性(命名值)的引用时,它首先查找对象中包含的命名值。如果找不到所需的名称,它会接着查找对象的“原型”。JavaScript 对象的特殊“原型”属性本身就是一个对象。为“Teacher”原型的属性赋值,使得该值对所有“Teacher”对象都可用。

Teacher.prototype.schoolName = "Rockbrook Elementary";
alert(myFavorite.schoolName);  // displays “Rockbrook Elementary”

“方法”(对象可以执行的操作)的概念是通过将函数作为类的原型中的命名值来赋值实现的。在我们简单的示例中,这里定义了我们“Teacher”类的“getName()”方法:

Teacher.prototype.getName = function() {
    return this.title + " " + this.lastName;
}

alert(myFavorite.getName());  // displays “Ms. Morrison”

方法也可以像其他命名值一样在构造函数中赋值,使用诸如“this.getName = function() {…}”的语法。但是,如果以这种方式定义,每次实例化“Teacher”对象时都会创建一个新的函数副本。更高效的做法是在“Teacher.prototype”对象上定义一次函数;由于基于原型的继承,该方法对所有实例化的“Teacher”对象都可用。

类式继承和子类

支持基于类的继承的语言提供了建立“子类”的能力——一个类从另一个类继承功能,通常是为了在特定情况下扩展或覆盖其功能。例如,在保龄球中,最后一帧的显示与早期帧的显示略有不同。“MishaBowling.Core.js”文件中的“DisplayFrame”类建立“正常”帧的显示指令。“DisplayFinalFrame”类建立比赛最后一帧的显示指令。两者定义的方式使得“DisplayFinalFrame”表现得像“DisplayFrame”的子类。

尽管 JavaScript 不原生支持基于类的继承,但基于原型的继承仍然是该语言的强大方面。对象可以从“原型”对象继承功能,而“原型”对象本身可能拥有提供继承值的原型对象,如此沿着“原型链”向上。Mozilla 的教程《继承和原型链》提供了很好的解释[5]。通过注意一些细节,我们可以利用“原型”对象来建立运行良好的子类。

回想一下,我们的 DisplayFrame 是通过解释 ScoringFrame 的数据而派生的结构化信息,而 ScoringFrame 本身又是从一系列有序的滚球(操作数据)派生的。DisplayFrame 构造函数中包含使用 this 的代码,用于初始化实例化对象的 scoringFrame、score 和 isCurrent 属性的值。(注意:为简单起见,这里的示例与随附代码中 DisplayFrame 类的编写方式略有不同,随附代码中使用了“命名空间”策略。我们将在本文后面讨论命名空间)。

function DisplayFrame (scoringFrame, valueForStrike, strikeDisplay, spareDisplay
                        , zeroDisplay)
{   
    this.scoringFrame = scoringFrame;   
   
    // the running score is displayed in the frame, depending on whether the scoring
    // for the frame is complete.  If not complete, the running score is blank
    this.score = !scoringFrame.isComplete ? "" : scoringFrame.runningScore.toString();

    // and upon construction, populate display slot properties from the rolls
    // in the scoring frame
    this.populateSlots(valueForStrike, strikeDisplay, spareDisplay, zeroDisplay); 

    // initialize the isCurrent property; the parent DisplayFrameList will mark
    // the appropriate frame "current" as it finishes its processing
    this.isCurrent = false;
}

构造函数中引用的“populateSlots()”方法是在该类的原型对象上定义的。此方法包含填充帧中两个显示“槽位”的逻辑,每个槽位根据其关联的计分帧填充适当的数字或字母符号,用于记分卡标记。

DisplayFrame.prototype.populateSlots = function(valueForStrike, strikeDisplay
                                          , spareDisplay, zeroDisplay)
{
    var sf = this.scoringFrame;
   
    // display slot1:  blank if not rolled yet, blank on a strike,
    //    the “zero” character on a 0 value, or otherwise the roll number
    this.slot1 = sf.roll1 === null           ? ""
               : sf.roll1 === valueForStrike ? ""
               : sf.roll1 === 0              ? zeroDisplay
               : sf.roll1.toString();
              
    // slot2:  strike mark if first roll strike; blank if first or
    //         second roll not yet rolled; spare mark if first and
    //         second rolls form a spare; the “zero” character
    //         on a 0 value for the second roll; otherwise the roll number
    this.slot2 = sf.roll1 === valueForStrike            ? strikeDisplay        
               : sf.roll1 === null || sf.roll2 === null ? ""
               : sf.roll1 + sf.roll2 === valueForStrike ? spareDisplay
               : sf.roll2 === 0                         ? zeroDisplay
               : sf.roll2.toString();
              
    // slot3:  in a normal frame, there is no “slot 3”. 
    this.slot3 = "";     

}

子类“DisplayFinalFrame”的构造函数很简单。子类使用“call()”方法引用基类的构造函数。

function DisplayFinalFrame(scoringFrame, valueForStrike, strikeDisplay, spareDisplay
                            , zeroDisplay)
{
    // call the constructor for the base class
    DisplayFrame.call(this, scoringFrame, valueForStrike, strikeDisplay, spareDisplay
                       , zeroDisplay);   
}

在 JavaScript 中,在这种上下文中使用“call”方法与在 C# 中使用“base”关键字或在 Java 中使用“super”关键字非常相似。它执行我们基类“DisplayFrame”的构造函数方法,确保其构造函数中设置的属性也应用于我们的子类。为了建立一个适当的子类,我们还需要额外一步来确保应用于“DisplayFrame”类的“prototype”对象的所有内容,无论是方法还是属性,也都应用于子类“DisplayFinalFrame”的原型,使用以下代码行:

DisplayFinalFrame.prototype = Object.create(ns.DisplayFrame.prototype);

本质上,我们正在用一个作为基类原型副本创建的新实例来覆盖 JavaScript 会给我们的“DisplayFinalFrame”类提供的默认原型。虽然这确实让我们享受到可能分配给基类原型的所有方法(和属性),这是我们真正子类所需的东西,但它也做了一件不幸的事情。它替换了我们的子类构造函数。

JavaScript 将类构造函数本身分配给原型对象的“constructor”属性。在替换子类的原型对象时,我们复制了原始类原型的所有内容。这包括原始类的构造函数。此时,如果我们创建一个 DisplayFinalFrame 对象,它会认为自己是一个 DisplayFrame 对象。因此,我们再用一行代码重新建立子类的构造函数。

DisplayFinalFrame.prototype.constructor = DisplayFinalFrame;

有了这三个要素——子类构造函数调用基类的 call() 方法;将基类的 prototype 对象复制到子类;并将子类的 constructor 属性重置为其自身的构造函数——我们有效地模拟了类/子类继承关系。从那时起,我们可以向子类的 prototype 添加额外的方法,或者覆盖基类中任何所需的方法。在 DisplayFinalFrame 的情况下,我们覆盖了基类的 populateSlots() 方法,以实现将最后一帧与之前帧区分开来的特殊标记逻辑。我们还填充了第三个显示槽,这在其他帧中是不相关的。

DisplayFinalFrame.prototype.populateSlots = function(valueForStrike, strikeDisplay
                          , spareDisplay, zeroDisplay)
{
    var sf = this.scoringFrame;   

    // display slot 1
    this.slot1  = sf.roll1 === null           ? ""
                : sf.roll1 === valueForStrike ? strikeDisplay
                : sf.roll1 === 0              ? zeroDisplay
                : sf.roll1.toString();               

    // display slot 2
    this.slot2 = sf.roll1 === null || sf.roll2 === null ? ""
               : sf.roll1 === valueForStrike && sf.roll2 === valueForStrike
                     ? strikeDisplay
               : sf.roll1 !== valueForStrike && sf.roll1 + sf.roll2 === valueForStrike
                     ? spareDisplay
               : sf.roll2 === 0 ? zeroDisplay
               : sf.roll2.toString();
              
    // display slot 3: special for the final frame
    this.slot3 = sf.roll1 === null || sf.roll2 === null || sf.roll3 === null
                      ? ""
               : sf.roll1 === valueForStrike && sf.roll2 !== valueForStrike
                  && sf.roll2 + sf.roll3 === valueForStrike
                  ? spareDisplay
               : sf.roll3 === valueForStrike ? strikeDisplay
               : sf.roll3 === 0 ? zeroDisplay
               : sf.roll3.toString();
}

 

命名空间方法:MishaBowling

JavaScript 具有极强的灵活性。虽然这使得该语言非常强大,但它也可能助长不良的组织习惯。开发人员可以完全不考虑架构地编写 JavaScript 代码,而 JavaScript 会说,“好的,尽管去做!”JavaScript 程序员需要具备架构纪律感,才能使生成的代码有组织且易于管理。一个有用的策略是“命名空间”。

“命名空间”的概念是 C# 和 Java 等语言的原生特性。通过命名空间,类总是定义在特定的命名上下文之下。这可以防止不同类型但名称相同的对象之间发生冲突。例如,您的保龄球代码和橄榄球代码可能都有一个“Game”类。命名空间有助于区分它们:“Bowling.Game”与“Football.Game”。

在 JavaScript 中应用命名空间策略也是组织代码的有效方法。JavaScript 命名空间的方法有很多,其中一些在 Angus Croll 的文章《JavaScript 中的命名空间》[6] 中总结得很好。我这里的方法与该文章中的 #5 类似,利用关键字“this”在一个匿名函数中表示命名空间对象。让我们看看 MishaBowling.Core.js,该文件定义了支持计分所需的核心类。代码顶部有以下几行:

// Establish the namespace "MishaBowling" if it doesn't already exist.  The
// namespace is essentially a globally scoped object named MishaBowling, within which
// our classes, methods, and other nested namespaces will be defined
if (typeof MishaBowling === 'undefined' )
    var MishaBowling = {};

// ... and establish the MishaBowling.Core namespace if it doesn't already exist
if (typeof MishaBowling.Core === 'undefined')   
    MishaBowling.Core = {};

首先定义了一个名为“MishaBowling”的全局变量。这是整套代码产生的唯一全局变量,“MishaBowling”作为所选的命名空间。第二行建立了一个名为“Core”的嵌套对象。我们所有相关的计分代码都组织在这个“MishaBowling.Core”命名空间中。

该模式延续了一个匿名函数,作为我们所有“MishaBowling.Core”代码的容器

(function() {

    ...
    ... all the rest of the MishaBowling.Core code ...
    ...

}).apply(MishaBowling.Core);

当 JavaScript 引擎处理时,匿名函数上的 .apply() 方法确保其参数对象(我们的 MishaBowling.Core 命名空间对象)填充了其内容。然后我们将使用完整命名空间从此 MishaBowling.Core 对象中定义的类实例化对象,如本例所示:

var myGame = new MishaBowling.Core.Game(...);

作为匿名命名空间函数中的第一步,我允许自己拥有一个方便的属性。为了让开发人员更容易区分关键字“this”表示不同对象的不同上下文,我向命名空间对象添加了一个属性“ns”,并将其赋值为“this”,根据其上下文,它代表命名空间对象本身。

(function() {

    var ns = this;

    ...

}).apply(MishaBowling.Core);

为了清晰起见,我随后在匿名函数中使用“ns”变量作为代理,表示“此命名空间对象”。例如,定义“ScoringFrame”类的代码位于“ns”变量声明之后,并且其本身也以“ns”作为前缀。

(function() {

    var ns = this;

    ...

    ns.ScoringFrame = function(...)
    {
      ... class constructor code ...
    }

    ...

}).apply(MishaBowling.Core);

通过这种方式,ScoringFrame 和其他类成为 MishaBowling.Core 对象的属性,因此被语境化为命名空间的一部分。

摘要

鉴于数据只有在特定上下文中理解时才有意义,商业智能从业者从操作上下文中解释数据,以在信息上下文中为战略问题提供有意义的答案。当这种方法应用于保龄球计分应用程序时,作为有序滚球列表的操作数据可以被解释以派生计分帧,而计分帧本身又可以被解释以派生传统计分卡上出现的显示标记。尽管 JavaScript 在对象和继承方面是基于原型的,但它足够灵活,可以支持以更传统的基于类的方式组织代码的设计。开发人员可以通过采用命名空间策略进一步组织 JavaScript 代码,该策略是许多面向对象语言的常见设计特性。正如 BI 专业人员可以为从操作数据中获取信息的过程带来结构一样,开发人员也可以为 JavaScript 代码带来深思熟虑的架构策略。

参考文献

[1] 维基百科,《十瓶保龄球》:https://en.wikipedia.org/wiki/Ten-pin_bowling

[3] 霍华德·德雷斯纳(Howard Dresner)通常被认为是 1989 年创造“商业智能”现代用法的人。https://en.wikipedia.org/wiki/Business_intelligence

[4] 机构研究协会(Association for Institutional Research)于1966年成立,旨在支持高等教育专业人士从事从机构数据中获取有意义信息的工作,并将其战略性地用于有效的决策。 http://www.airweb.org/AboutUs/Pages/AboutAIR.aspx

[5] Mozilla开发者网络文档,《继承与原型链》:https://mdn.org.cn/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

[6] Angus Croll,《JavaScript 中的命名空间》:https://javascriptweblog.wordpress.com/2010/12/07/namespacing-in-javascript/

© . All rights reserved.