乐正

Actions speak louder than words.

企业应用架构概述

《企业应用架构模式》读书笔记 (一)

企业应用的具体定义是什么?

我无法给出一个具体的定义,但是可以罗列一些自己的理解:

  • 企业应用一般都涉及到持久化数据。这些数据一般会被持久化若干年,数据本身的结 构也可能改变,使得它在不影响原本数据的基础上还能表示更多的信息。
  • 企业应用一般还涉及到大量数据。目前,我们主要使用数据库系统存储数据,大多数 是关系型数据库。
  • 企业应用一般都涉及到很多人同时访问数据。尤其是对于一些基于Web的系统,人数更 是呈指数增长。而我们要确保这些人能够正确的访问和操作这些数据。
  • 企业应用还设计到大量操作数据的用户界面。有几百个用户界面也是不足为奇的,用 户的使用频率也相差很大。
  • 企业应用通常很少独立存在,通常需要与其他企业应用集成。这些系统是在不同的时 期,采用不同的技术创建的,甚至连协作机制都不相同。
  • 企业应用还会遇到业务过程中的差异,以及数据中的概念不一致性
  • 企业应用还会在业务逻辑上遇到困难。很多的特殊情况导致了复杂业务的无逻辑。而 且,可以确定的是,这些逻辑会随着时间不断变化。

企业应用的分类

考虑这样的三种情况。

一个B2C的网上商城系统。这样的系统必须能够应对大量的客户访问,因此,其解决方案不 但要考虑资源利用的有效性,还需要考虑到系统的可伸缩性,以便在用户量加大的时候,能 够通过增加硬件的方式解决。我们希望任何人都能访问这个系统,所以图形界面可以选择通 用的Web表现方式,以支持不同的浏览器。

再考虑一个租赁合同的自动处理程序。这个系统比前面系统的访问人数要少,但是业务逻辑 却比较复杂。租赁的标准规则会因交易发生很多不同的变化,正式因为规则的随意性很大, 才使得这样的一个复杂的业务领域具有挑战性。

第三个例子是一个公司的开支跟踪系统。它功能少,逻辑简单,用户也少,能够很容易的实 现。然后,这样的系统也不是没有挑战:一方面是你需要很快速的开发出来,另一方面你又 需要为它以后的发展考虑。

企业应用的性能

很多架构的设计和决策和性能有关。在做性能优化之后一定要和优化前进行测量对比,以确 定真的得到了优化。配置上的重大变化会使得某些优化失效,所以在升级硬件、数据库或者 其他东西到新版本之后,必须重新确认性能优化工作的有效性。

列举下和性能有关的术语:

  • 响应时间:系统完成一次外部请求所需要的时间。
  • 响应性:不同于响应时间,它是指系统响应请求的速度有多快。如果在请求期间,系 统一直处于等待状态,则响应性等于响应时间。举例:在文件拷贝过程中的进度条就是提 高响应性但并不会提高响应时间。
  • 等待时间:是获得系统任何形式响应的最小时间。
  • 吞吐量:是给定时间内,能够处理请求量。吞吐量以每秒事务数来表示(单位:tps)。
  • 负载:关于当前系统负荷的表述。
  • 负载敏感度:是指响应时间随着负载变化的程度。
  • 系统的容量:是指最大有效负载或者吞吐量的指标。它可以是一个绝对最大值,或者 性能衰减至一个可接受的阀值的临界点。
  • 可伸缩性:是向系统中添加资源(通常是硬件),对系统性能的影响。一个可伸缩的 系统允许在添加硬件后性能得到合理提升。垂直可伸缩性「或称垂直延展」是提高单 台服务器的性能,水平可伸缩性「或称水平延展」是提高服务器的数量。

当构建企业应用时,关注硬件的可伸缩性比关注容量或者效率更重要。

企业应用的模式

模式的核心就是特定的解决方案,它有效而且有足够的通用性,可以解决重复出现的问题。 一旦决定使用模式,就必须知道如何将它应用在当前问题上。使用模式的关键之一就是不能 盲目使用。

模式的结构

  • 模式的名称。
  • 意图和概要。意图用一两句话总结模式;而概要是模式的一种可视化表示,通常(但不总 是)用一个UML图表示。
  • 运行机制和使用时机。运行机制描述了解决方案;使用时机描述了模式何时应该被使用。

模式的局限性

所有模式都是不完备的。

分层

在分层组织方式下,上层既对下层定义了各种服务;又对下层隐藏了其细节。将系统按层次 分解又很多重要好处:

  • 在无需了解过多其他层次的基础上,可以将某一层作为一个整体来理解。
  • 可以替换某层的全部实现,只要其提供同样的服务就可以。
  • 分层可以降低层次间的依赖性。
  • 分层有利于标准化工作。
  • 构建好某一层次后,可以让它为很多上次服务提供支持。

分层是一种重要的技术,但是也有缺陷

  • 层次并不能封装所有的东西。有时它会为我们带来级联修改。比如在一个分层设计的企业 应用中,要增加视图上的数据域,必须要在数据库中添加响应的字段,还需要修改数据库 和视图之间的其他层。
  • 过多的层次会影响性能。

分层架构中,最困难的地方在于确定层次结构以及它们的职责

三个层次

三个基本的层次为:

层次 职责
表现层 提供服务、显示信息、用户请求、HTTP请求和命令行调用。
领域层 逻辑处理,系统中真正的核心。
数据层 与数据库、消息系统、事物管理器和其他软件包通讯。

伴随着分层,还有一条关于依赖性的普遍原则:数据层和领域层绝对不要依赖表现层。

为各层选择运行环境

对于大多数信息系统,主要的决定就是在那里运行处理工作。通常,最简单的情况就是所有 的东西都运行在服务器上。这样做最大的好处是所有东西运行在有限的环境内,很容易修改 和维护。

在客户机上运行应用程序的好处是系统的响应性好,或者是在脱离网络的环境下也能工作。

领域逻辑既可以全部运行在服务器端,也可以全部运行在客户端或者一分为二也可以。全部 运行在服务器端有助于系统维护,像客户端转移是为了响应时间和离线操作的需求。如果你 在客户端运行领域逻辑,可以考虑将全部的该类逻辑都在客户端运行,可以保证一致性。

组织领域逻辑

领域逻辑的组织可以分为三种主要的模式:事务脚本、领域模型以及表模型。

事务脚本是这样的一个过程:从表示层中获取输入、进行校验和计算处理,将数据存储到数 据库中,以及调用其他系统操作等。然后,该过程将更多的数据返回给表现层。

事务脚本具有以下优点:

  • 它是一个大多数程序员都能理解的简单过程模型。
  • 它能狗与一个使用行数据入口或者表数据入口的数据层很好的协作。
  • 事务脚本的边界很明显:起源于脚本打开,终止于脚本关闭。

领域模型中,不再是每个过程控制用户的动作逻辑,而是由对象承担动作逻辑的一部分。领 域模型的好处在于可以使用较多的技术来组织日趋复杂的领域逻辑。

第三种组织领域逻辑的模式为表模型,表模型看起来与领域模型很相似,关键区别在于:领 域模型对于数据库中的每一条数据都有一个相对应类的实例;而表模型只有一个公共类的实 例。

选择

这三种模式并不互相排斥,但是更倾向于主要使用领域模型,同时使用事务脚本和表模型处 理剩余逻辑。

服务层

处理领域逻辑的常见方法是将领域层再细分为两层。服务层置于领域模型或者表模型之上。

发声器和光敏电阻

发声器

在 Arduino 中,Arduino IDE 提供了函数用以控制扬声器,使得在 Arduino 项目上增加声 音功能非常容易。

控制发声器与控制 LED 在代码层面没有很大的区别,电路也相差不多。

1
2
3
4
5
6
7
8
9
10
11
12
pinMode(pinNum, OUTOUT);

// 使发声器发声, 第二个参数是频率
tone(pinNum, frequency);

// 第三个参数是可选参数,以毫秒作为单位,表示声音长度。
// 如果没有指定,则声音将一直持续输出知道一个不同的声音
// 或者使用 noTone 函数结束声音。
tone(pinNum, frequency, duration);

// 使发声器不发声
noTone(pinNum);

光敏元件

光敏电阻是一个依赖与光的电子元件。在黑暗环境中,光敏电阻是一个具有非常高阻值的电 阻。当光子撞击到光检测器时,电阻值降低。光线越强,电阻值越低。

在接下来这个项目中,使用 LDR 检测光,用压电扬声器给出光亮度的声音反馈。

需要的元件

  • 压电扬声器
  • 光敏电阻
  • 10kΩ电阻

LDR 可以以任何方式插入电路中,它没有极性。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte piezoPin = 8;
byte ldrPin = 0;
int ldrValue = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  ldrValue = analogRead(ldrPin);
  Serial.print(ldrValue);
  delay(25);
  noTone(piezoPin);
  delay(ldrValue);
}

在代码层面,还是很容易理解的。

硬件回顾

项目中,接触到了一个新的概念——分压器(也叫电位分配器)。分压器是由电阻构成的。使 用两个电阻串联,从其中一个取出电压,这样可以减小进入电路的电压。

'分压器'

输入电压(Vin)连接在两个电阻上。当测量通过一个电阻的电压(Vout) 时,电压将小俞输入电压(分压)。计算 R2 两端的输出电压公式如下:

Vout = R2 / (R2 + R1) * Vin

使用串口与 Arduino 通信

在此之前

在串口通信实验之前还有几个其他的实验里面有一些需要记录的知识。

constrain() 函数:需要三个参数 x, a, b。这里 x 是一个被约束的数,它的最小值是 a 最大值是 b。

random() 函数生成一个给定范围内的随机数,可以使用 randomSeed() 函数预先设置 随机数范围。

需要的硬件

这个实验会需要一个5V的 RGB 彩灯,也可以使用三个分别是红、绿、蓝色的 LED 灯代替。

代码回顾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
char buffer[18];
int red, green, blue;

byte redPin   = 11;
byte bluePin  = 9;
byte greenPin = 10;

void setup() {
  Serial.begin(9600);
  Serial.flush();
  pinMode(redPin, OUTPUT);
  pinMode(bluePin, OUTPUT);
  pinMode(greenPin, OUTPUT);
}

void loop() {
  if (Serial.available() > 0) {
    int index = 0;
    delay(100);
    int numChar = Serial.available();
    if (numChar > 15) {
      numChar = 15;
    }
    while (numChar--) {
      buffer[index++] = Serial.read();
    }
    splitString(buffer);
  }
}

void splitString(char * data) {
  Serial.print("Data entered: ");
  Serial.println(data);
  char * parameter = strtok(data, " ,");
  while (parameter != NULL) {
    setLED(parameter);
    parameter = strtok(NULL, " ,");
  }

  for (int i = 0; i < 16; i++) {
    buffer[i] = '\0';
  }
  Serial.flush();
}

void setLED(char * data) {
  if ((data[0] == 'r' || data[0] == 'R')) {
    int Ans = strtol(data + 1, NULL, 10);
    Ans = constrain(Ans, 0, 255);
    analogWrite(redPin, Ans);
    Serial.print("Red is set to: ");
    Serial.println(Ans);
  }
  if ((data[0] == 'g' || data[0] == 'G')) {
    int Ans = strtol(data + 1, NULL, 10);
    Ans = constrain(Ans, 0, 255);
    analogWrite(greenPin, Ans);
    Serial.print("Green is set to: ");
    Serial.println(Ans);
  }
  if ((data[0] == 'b' || data[0] == 'B')) {
    int Ans = strtol(data + 1, NULL, 10);
    Ans = constrain(Ans, 0, 255);
    analogWrite(bluePin, Ans);
    Serial.print("Blue is set to: ");
    Serial.println(Ans);
  }
}

在 setup 函数中,使用了新的函数 Serial.begin(9600)。它到诉 Arduino 开始串口通 信,函数的参数是波特率(每秒内信号或者脉冲数)。串口以这个波特率开始通信。

Serial.flush() 命令清空串口中的残存的字符,为输入/输出做准备。

串口是 Arduino 与外界通信的一个简单方法。

在 loop 函数一开始,使用了 Serial.available() 函数检查是否有字符串发送到串口。 因为程序的执行快于串口数据的发送速度,所以在读取数据之前,需要执行 delay(100) 等待串口缓冲区数据到满。 后面,使用 Serial.read() 函数读串口数据,每次读取一个 字节。

读取完数据之后,需要对读取的字符串进程处理。在 Arduino 中,可以使用 Serial.printSerial.println 函数打印字符串。之后,使用 strtok(str, delimiter) 函数分隔 字符串。如果delimiter的值是 “ ,”,那么将按照空格或者逗号分割。

1
2
3
4
5
char * parameter = strtok(data, " ,");
while (parameter != NULL) {
  setLED(parameter);
  parameter = strtok(NULL, " ,");
}

看上面代码,在while循环中通过传递给 strtok 一个NULL值来实现对原来切割的字符串 的再次分割。

最后,需要清除串口缓冲区和 buffer 数组内的数据,为下一次数据的输入做准备。

之后,将处理后的字符串传递给 setLED 函数,让它控制 LED 的发光情况。

setLED 函数中,使用了 strtol 方法将分割后的字符串转换成数字.

strtol reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * @description 从一个字符串中解析integer类型数字
 * 这个方法会忽视 str 中前所有的空白字符,直到遇到第一个非空白字符,
 * 然后它将分析 str 可能的进制,将他转换为指定进制的数字。一个合法
 * 的数字由以下几个方面组成:
 * 1. + / - 代表正负的符号
 * 2. 前缀是0代表它大部分情况下是8进制
 * 3. 0x 或者 0X 前缀被认为是16进制
 * 4. 接下来的数字
 * 
 * @param str 需要解析的字符串
 * @param str_end 结尾字符,如果是NULL,这个参数会被忽略
 * @param base 被解析的字符串的进制
 **/
long strtol(const char * str, char ** str_end, int base);

将解析得到的亮度通过 analogWrite(pinNum, value) 写入则完成了对 LED 的控制。

总结

  • 脉宽调制『PWM』以及如何利用 analogWrite 使用它们
  • 串口通信概念
  • 如何使用相同的电路、不同的代码产生各种灯光效果
  • 使用 Serial.begin() 设置波特率
  • 使用串口监视器发送命令
  • 使用 Serial.flush() 清空串口缓冲区
  • 使用 Serial.available() 检查数据是否发送到串口
  • 使用 Serial.read() 从串口中读数据
  • 使用 Serial.print(), Serial.println() 打印数据到串口监视器
  • 使用 strtok() 分隔字符串
  • 使用 strtol() 将字符串转为长整形数
  • 使用 constrain() 函数限制一个变量值