404频道

学习笔记

1 MVC 结构

分为了 Model(模型层)、View(视图层)、Controller(控制器层)。

1.1 Model(模型层)

职责:表示应用程序的数据结构和业务逻辑。
包含内容

  • 数据实体类(如 User, Product
  • 数据访问逻辑(如数据库 CRUD)
  • 业务逻辑方法(如计算、状态切换)

1.2 View(视图层)

职责:负责数据的展示和用户界面。
包含内容

  • 页面模板(HTML / XML / JSX 等)
  • UI 渲染逻辑
  • 与用户交互(输入、按钮等)

1.3 Controller(控制器层)

职责:接收用户请求,调用模型处理数据,并选择视图进行展示。
包含内容

  • 路由逻辑
  • 请求分发
  • 控制流程协调(调用 Model → 返回给 View)

2 MVC 的交互流程(经典模式)

一个典型的用户请求处理流程如下:

  1. 用户交互: 用户在 View 上进行操作(例如,点击提交按钮)。
  2. 请求路由: View 将用户的输入/事件(例如,表单数据)传递给对应的 Controller(通常由框架的路由机制完成)。
  3. Controller 处理:
    • Controller 解析用户输入。
    • Controller 调用一个或多个 Model 对象的方法:
      • 可能更新 Model 的状态(例如,将表单数据保存到数据库)。
      • 可能从 Model 获取数据
  4. Model 更新与通知:
    • Model 执行业务逻辑,更新自身状态(可能涉及数据库操作等)。
    • Model 通知所有注册的观察者(通常是依赖它的 View)状态已改变(这一步是观察者模式,在被动视图变体中可能由 Controller 显式通知)。
  5. View 更新:
    • View 接收到 Model 状态改变的通知(或由 Controller 明确告知)。
    • View 从 Model 查询最新的数据。
    • View 使用新数据重新渲染用户界面。
  6. 用户看到更新: 用户看到更新后的 View。

3 MVC 的变体

3.1 更精细划分

为了更好地解耦和扩展,很多项目在经典 MVC 基础上再进一步划分层次,例如:

1
2
3
4
5
6
7
8
9
┌────────────────────┐
│ Controller │ ← 接收请求,协调流程
├────────────────────┤
│ Service Layer │ ← 业务逻辑(可选扩展层)
├────────────────────┤
│ Model Layer │ ← 实体数据结构 / ORM / 数据访问
├────────────────────┤
│ Repository DAO │ ← 数据库交互(封装 SQL / ORM)
└────────────────────┘

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种在复杂业务系统中常用的架构模式,其核心思想是将“读操作”(Query)和“写操作”(Command)进行职责分离,从而提升系统的可维护性、扩展性和性能。

  • 命令(Command):负责处理数据的更新操作(如创建、修改、删除),通常涉及业务逻辑和事务管理。
  • 查询(Query):仅用于读取数据,专注于高效的数据检索和展示,不包含业务逻辑。

1 具体示例

命令模型:

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
// 1. 定义命令类
public class CreateOrderCommand {
private String orderId;
private String productId;
private int quantity;

// 构造函数、Getter/Setter
}

// 2. 命令处理服务
@Service
public class OrderCommandService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryRepository inventoryRepository;
@Autowired
private EventPublisher eventPublisher;

public void handleCreateOrder(CreateOrderCommand command) {
// 1. 创建订单
Order order = new Order(command.getOrderId(), command.getProductId(), command.getQuantity());
order.setStatus("Pending");
orderRepository.save(order);

// 2. 扣减库存
Inventory inventory = inventoryRepository.findByProductId(command.getProductId());
inventory.deduct(command.getQuantity());
inventoryRepository.save(inventory);

// 3. 发布事件
eventPublisher.publish(new OrderCreatedEvent(command.getOrderId(), command.getProductId(), command.getQuantity()));
}
}

事件模型(Event Model)

1
2
3
4
5
6
7
8
// 领域事件:订单创建事件
public class OrderCreatedEvent {
private String orderId;
private String productId;
private int quantity;

// 构造函数、Getter/Setter
}

查询模型(Query Model)

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
// 1. 查询服务接口
public interface OrderQueryService {
OrderView getOrderStatus(String orderId);
}

// 2. 查询视图类(订单状态缓存)
public class OrderView {
private String orderId;
private String status;
private LocalDateTime createdAt;

// Getter/Setter
}

// 3. 查询服务实现
@Service
public class OrderQueryServiceImpl implements OrderQueryService {
@Autowired
private RedisTemplate<String, OrderView> redisTemplate;

@Override
public OrderView getOrderStatus(String orderId) {
return redisTemplate.opsForValue().get("order:" + orderId);
}

// 订阅事件并更新缓存
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
OrderView orderView = new OrderView();
orderView.setOrderId(event.getOrderId());
orderView.setStatus("Pending");
orderView.setCreatedAt(LocalDateTime.now());
redisTemplate.opsForValue().set("order:" + event.getOrderId(), orderView);
}
}

事件总线(Event Bus)

1
2
3
4
5
6
7
8
9
// 简化的事件发布接口
public interface EventPublisher {
void publish(Event event);
}

// 简化的事件监听接口
public interface EventListener {
void onEvent(Event event);
}

1.1.1 工作流程

  1. 用户下单
    • 客户端调用命令服务 OrderCommandService.handleCreateOrder()
    • 命令服务创建订单并扣减库存,发布 OrderCreatedEvent
  2. 更新查询视图
    • 查询服务 OrderQueryServiceImpl 监听到 OrderCreatedEvent
    • 将订单状态缓存到 Redis,生成 OrderView
  3. 查询订单状态
    • 客户端调用查询服务 OrderQueryService.getOrderStatus()
    • 查询服务从 Redis 快速返回订单状态。

1

2 SOLID 原则

2.1 开闭原则 OCP

定义:软件实体(类、模块、函数)应当对扩展开放,对修改关闭。

简单理解:不要修改已有代码,而是增加代码来扩展行为。

2.2 单一职责原则 SRP

定义:一个类只有一个引起他变化的原因。

简单理解:一个类或者模块应该仅做一件事,并且将这件事情做好。

典型代码实例如下:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 职责1: 只负责生成报告核心内容
public class ReportGenerator {
private String data;

public ReportGenerator(String data) {
this.data = data;
}

public String generate() {
// ... 核心生成逻辑 ...
return reportContent;
}
}

// 职责2: 只负责报告格式转换
public interface ReportFormatter {
String format(String content);
}

public class HtmlFormatter implements ReportFormatter {
@Override
public String format(String content) {
// ... HTML格式化逻辑 ...
return formattedContent;
}
}

public class PdfFormatter implements ReportFormatter {
@Override
public String format(String content) {
// ... PDF格式化逻辑 (可能调用外部库) ...
return formattedContent;
}
}

// 职责3: 只负责报告持久化
public interface ReportRepository {
void save(String content, String filename);
}

public class FileSystemRepository implements ReportRepository {
@Override
public void save(String content, String filename) {
// ... 文件I/O操作 ...
}
}

// 职责4: 只负责报告发送
public interface ReportSender {
void send(String content, String recipient);
}

public class EmailReportSender implements ReportSender {
@Override
public void send(String content, String recipient) {
// ... 邮件发送逻辑 ...
}
}

// 高层模块/客户端代码 (组合各个职责)
public class ReportService {
private ReportGenerator generator;
private ReportFormatter formatter;
private ReportRepository repository;
private ReportSender sender;

// 通过构造器或Setter注入依赖
public ReportService(ReportGenerator generator, ReportFormatter formatter,
ReportRepository repository, ReportSender sender) {
this.generator = generator;
this.formatter = formatter;
this.repository = repository;
this.sender = sender;
}

public void createAndSendReport(String filename, String recipient, String formatType) {
// 1. 生成内容
String content = generator.generate();

// 2. 格式化 (根据formatType选择Formatter, 这里简化)
String formattedContent = formatter.format(content);

// 3. 保存
repository.save(formattedContent, filename);

// 4. 发送
sender.send(formattedContent, recipient);
}
}

2.3 依赖倒置原则 DIP

定义:

  1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。
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
// 步骤1: 定义抽象接口 (高层和低层都将依赖它)
public interface DataRepository {
void save(String data);
}

// 步骤2: 低层模块 - 具体实现1: MySQL
public class MySQLDatabase implements DataRepository {
@Override
public void save(String data) {
// ... 具体的 MySQL 保存逻辑 ...
System.out.println("Saving data to MySQL: " + data);
}
}

// 步骤2: 低层模块 - 具体实现2: PostgreSQL (新增实现很容易)
public class PostgreSQLDatabase implements DataRepository {
@Override
public void save(String data) {
// ... 具体的 PostgreSQL 保存逻辑 ...
System.out.println("Saving data to PostgreSQL: " + data);
}
}

// 步骤2: 低层模块 - 具体实现3: 文件系统 (新增实现很容易)
public class FileSystemRepository implements DataRepository {
@Override
public void save(String data) {
// ... 具体的文件保存逻辑 ...
System.out.println("Saving data to file: " + data);
}
}

// 步骤3: 修改高层模块 - 只依赖抽象接口 DataRepository
public class ReportService {
private DataRepository repository; // 依赖抽象!

// 步骤5: 依赖注入 (通过构造函数)
public ReportService(DataRepository repository) {
this.repository = repository; // 接收注入的具体实现
}

public void generateReport(String reportData) {
// ... 生成报告的复杂业务逻辑 (核心策略) 不变 ...
repository.save(reportData); // 通过接口调用
}
}

// 客户端使用 (负责组装对象,决定具体实现)
public class Client {
public static void main(String[] args) {
// 选择并创建具体的低层实现
DataRepository mySqlRepo = new MySQLDatabase();
// DataRepository pgRepo = new PostgreSQLDatabase();
// DataRepository fsRepo = new FileSystemRepository();

// 将依赖注入高层模块
ReportService reportService = new ReportService(mySqlRepo); // 注入 MySQL
// ReportService reportService = new ReportService(pgRepo); // 注入 PostgreSQL
// ReportService reportService = new ReportService(fsRepo); // 注入文件系统

reportService.generateReport("Important Report Data");
// 输出取决于注入的具体实现:
// 注入 MySQL: Saving data to MySQL: Important Report Data
// 注入 PostgreSQL: Saving data to PostgreSQL: Important Report Data
// 注入 FileSystem: Saving data to file: Important Report Data
}
}

2.4 里氏替换原则

定义:子类型必须能够替换掉他们的父类型,程序的行为仍保持正确。

通俗理解:子类不能违背父类的语义和契约。

2.5 接口隔离原则

定义:客户端不应该被迫依赖它不使用的方法。

通俗理解:一个接口只包含调用者真正需要的方法。

1 核心概念

1.1 战略设计

从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
战略设计主要流程包括:建立统一语言、领域分解、领域建模。

1.2 战术设计

从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地。

1.2.1 核心概念

image.png

1.2.2 实体

具有唯一标识符 ID 和生命周期的对象。状态会随时间变化,但标识符不会变化。实体通常会封装跟自身状态相关的核心业务逻辑和不变规则。

1.2.3 值对象

描述事物某些特征或者属性的对象。
具有如下特点:

  1. 无标识。
  2. 不可变。一旦创建后属性值不能再改变。
  3. 通常用来描述实体的属性,比如 Customer 实体有一个 Address 值对象。
  4. 可组合。值对象可以包含其他的值对象。Address 可能包含 StreetCityZipCode 等更小的值对象。

例子: Money (金额 + 货币), Address (街道、城市、邮编), Color (RGB值), DateRange (开始日期 + 结束日期), ProductInfo (产品ID、名称、单价 - 用于订单项中)。

1.2.4 聚合 & 聚合根

聚合将一组强关联的实体和值对象组合在一起,形成一个一致性边界和一个事务边界。每个聚合有一个聚合根

聚合根:是聚合的唯一入口点,通常是一个实体。

例子:
Order (聚合根) 包含:

  • 自身属性 (订单号、下单时间、状态、总金额)
  • OrderItem (实体) 列表 - 每个订单项有自己的 ProductIdProductNameUnitPriceQuantityLineTotal (值对象?)
  • ShippingAddress (值对象)
  • BillingAddress (值对象)
    要修改某个 OrderItem 的数量,必须调用 Order 聚合根的 ChangeItemQuantity(OrderItemId, NewQuantity) 方法。Order 会检查新数量是否有效,重新计算该订单项金额和订单总金额。

1.2.5 领域服务

当某个操作或业务逻辑不适合放在实体或值对象内部时,将其封装在领域服务中。它代表了一个无状态的操作或过程。

场景:

  • 操作涉及多个聚合/实体的协调。(例如:TransferService 处理银行转账,需要操作 源账户 和 目标账户 两个聚合根)。
  • 操作本身是一个无状态的计算或转换。(例如:复杂的 RiskAssessmentService 计算贷款风险)。
    • 需要调用外部系统或基础设施(但核心逻辑仍在领域层)。(例如:NotificationService 封装发送通知的规则,实际发送动作可能由基础设施层实现)。

关键特征:

  • 无状态: 服务本身不持有业务状态。
  • 领域概念: 服务执行的操作本身是领域专家关心的核心业务概念(如“转账”、“风险评估”)。
  • 接口定义在领域层: 具体实现在领域层或基础设施层(如果需要访问外部资源)。

与“应用服务”的区别:

  • 应用服务: 位于应用层,负责协调领域对象、领域服务、仓储、事务、权限、外部调用等,完成一个用户用例(Use Case)。它更偏重流程协调和技术层面。
  • 领域服务: 位于领域层,封装了核心的、无法放入实体/值对象的领域逻辑。它只关心业务规则。

1.2.6 Repository (仓储或资源库)

只用于聚合根! 提供类似集合(Collection)的接口,负责聚合的持久化(保存)和检索(查询)

关键特征:

  • 聚合根入口: 只负责聚合根的持久化和加载。加载时,会重建整个聚合(包含内部实体和值对象)。保存时,保存整个聚合的变更。
  • 领域层接口: 仓储的接口定义在领域层,因为它表达的是领域模型需要什么样的持久化能力(如 IOrderRepository 定义 Add(Order order)GetById(OrderId id)Save(Order order) 等方法)。
  • 基础设施层实现: 仓储的具体实现在基础设施层(如 SqlOrderRepositoryMongoOrderRepository)。它知道如何操作数据库、缓存、文件系统等。
  • 解耦: 领域层只依赖于仓储接口,完全不知道底层存储细节(SQL, NoSQL, File)。这符合依赖倒置原则(DIP)
  • 查询分离: 仓储通常只提供基于聚合根ID的简单查询。复杂的查询(跨聚合、报表)建议使用单独的查询层(如CQRS模式中的Query Side),避免污染领域模型和仓储。

1.2.7 工厂

负责封装复杂对象(尤其是聚合)创建逻辑的对象或方法。

适用场景:

  • 对象的创建过程很复杂,涉及多个步骤、规则校验、依赖组合,不适合放在构造函数中(构造函数应尽量简单)。
  • 需要解耦创建逻辑和使用逻辑。
  • 需要根据条件创建不同的实现(结合抽象工厂模式)。

形式:

  • 独立工厂类: OrderFactory.createOrder(customer, items, ...)
  • 聚合根上的工厂方法: Order.createDraft(customer, ...) (静态方法)

作用: 将复杂的构造逻辑集中管理,保持客户端代码和领域对象(尤其是聚合根)的简洁。

1.2.8 领域事件

表示在领域中发生的、对业务有重要意义的事件。

关键特征:

  • 过去时: 事件名通常是过去时态 (如 OrderPlacedPaymentReceivedInventoryLow)。
  • 包含信息: 包含事件发生时相关的数据 (如 OrderPlaced 事件包含 OrderIdCustomerIdOrderItemsTotalAmountTimestamp)。
  • 由聚合根发布: 通常由聚合根在其状态发生重要变更后发布。发布事件是聚合根业务操作的一部分。
  • 轻量级通知: 事件本身不包含“如何处理”的逻辑,它只是一个通知。

作用:

  • 解耦限界上下文: 最重要的作用! 一个上下文内的聚合根发布事件,其他上下文可以订阅这些事件并触发本地操作,实现松耦合的集成。这是实现最终一致性的基础。(例如:订单上下文 发布 OrderConfirmed 事件,库存上下文 订阅该事件并扣减库存,物流上下文 订阅该事件并安排发货)。
  • 驱动内部流程: 同一个聚合或限界上下文内,事件可以触发后续步骤。(例如:Order 支付成功后发布 PaymentReceived 事件,触发自身状态变更为 已支付 并发布 OrderPaid 事件)。
  • 审计追踪: 记录系统中发生的重要业务事实。
  • CQRS 读模型更新: 更新用于查询的读模型。

实现: 通常需要基础设施支持(事件总线、消息队列)来实现可靠的事件发布和订阅。

2 分层架构

严格分层架构:某层只能与直接位于的下层发生耦合。
松散分层架构:允许上层与任意下层发生耦合。

在领域驱动设计(DDD)中采用的是松散分层架构,层间关系不那么严格。每层都可能使用它下面所有层的服务,而不仅仅是下一层的服务。

image.png

2.1 应用层

负责编排、转发、校验等,该层应该尽可能的做薄,它知道“做什么”(流程步骤),但不知道“怎么做”(具体业务规则怎么做在领域层)。

职责:

  • 协调者: 编排领域对象(实体、值对象、聚合根)、领域服务、资源库等,完成一个具体的用户用例 (Use Case) 或系统任务。它代表一个业务场景的完整流程。
  • 事务管理: 通常负责定义和管理事务边界(确保一个用例内的操作要么全成功,要么全失败)。
  • 安全认证: 执行权限检查(用户是否有权执行此操作?)。
  • 基础验证: 执行简单的、不依赖领域上下文的验证(如 ID 是否存在)。
  • 发布领域事件: 接收领域层产生的事件,并负责将其发布到事件总线/消息队列(通常委托给基础设施层)。
  • 返回结果: 将执行结果(通常是 DTO)返回给用户界面层,或处理异步事件。

包含的内容:

  • 应用服务: 这是这一层的核心组件。每个应用服务方法通常对应一个用户用例或一个原子性系统任务。方法名通常是动词,描述操作(如 PlaceOrderService.execute(PlaceOrderCommand))。
  • 命令处理器 / 查询处理器 (CQRS): 如果采用 CQRS 模式,应用层会包含处理 Command 和 Query 的处理器。
  • DTO (输入/输出): CommandQueryResponse 等对象,用于层间数据传输。

2.2 领域层

领域层是绝对的核心,包含了实体、值对象、聚合与聚合根、领域服务、领域事件、仓储接口、工厂接口/实现。

2.3 基础设施层

职责: 为上层提供具体的技术实现细节与外部世界的交互能力。是系统的“工具箱”和“适配器”。

包含的内容:

  • 仓储实现: 提供 IOrderRepository 等的具体实现(如 SqlOrderRepositoryMongoOrderRepository),负责与数据库(SQL/NoSQL)、文件系统、缓存等持久化机制交互。
  • 外部服务客户端实现: 调用第三方 API、支付网关、短信服务、邮件服务的具体实现。
  • 消息通信: 实现消息队列(RabbitMQ, Kafka)的生产者/消费者,事件总线 (IEventBus 的实现)。
  • 文件 I/O: 读写文件、操作存储(S3, Azure Blob)的具体代码。
  • 网络通信: HTTP 客户端、RPC 客户端的封装。
  • 配置管理: 读取配置文件、环境变量。
  • 日志记录实现: ILogger 接口的具体实现(如 Log4Net, Serilog)。
  • 身份认证/授权实现: 与 Auth0、OAuth2 服务器等集成的具体代码。
  • 框架集成: ORM (Entity Framework, Hibernate), Web 框架 (ASP.NET Core, Spring Boot) 的特定配置和粘合代码。

3 项目实战

下面是一个订单管理系统的代码示例。

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
order-ddd/
├── application/ # 应用层
│ └── OrderApplicationService.java
├── domain/ # 领域层
│ ├── model/ # 实体、值对象、聚合根
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── Customer.java
│ ├── repository/ # 仓储接口
│ │ └── OrderRepository.java
│ └── service/ # 领域服务
│ └── OrderDomainService.java
├── infrastructure/ # 基础设施层
│ ├── persistence/ # 数据库持久化实现
│ │ └── OrderRepositoryImpl.java
│ └── event/ # 事件发布等
│ └── OrderEventPublisher.java
└── interfaces/ # 用户接口层(如 REST API)
└── OrderController.java

3.1 领域层

3.1.1 领域模型层

Order.java 聚合根

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
package com.example.order.domain.model;

import java.util.List;
import java.util.UUID;

public class Order {
private String orderId;
private String customerId;
private List<OrderItem> items;
private OrderStatus status;

public Order(String customerId, List<OrderItem> items) {
this.orderId = UUID.randomUUID().toString();
this.customerId = customerId;
this.items = items;
this.status = OrderStatus.CREATED;
}

public void confirm() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Only created orders can be confirmed.");
}
this.status = OrderStatus.CONFIRMED;
}

// Getters
}

OrderItem.java - 值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.order.domain.model;

public class OrderItem {
private String productId;
private int quantity;
private double price;

public OrderItem(String productId, int quantity, double price) {
this.productId = productId;
this.quantity = quantity;
this.price = price;
}

public double getTotalPrice() {
return quantity * price;
}

// Getters
}

Customer.java - 实体

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.order.domain.model;

public class Customer {
private String id;
private String name;

public Customer(String id, String name) {
this.id = id;
this.name = name;
}

// Getters
}

OrderStatus.java - 枚举

1
2
3
4
5
6
7
package com.example.order.domain.model;

public enum OrderStatus {
CREATED,
CONFIRMED,
CANCELLED
}

3.1.2 领域仓储层

OrderRepository.java - 领域层仓储接口

1
2
3
4
5
6
7
8
9
package com.example.order.domain.repository;

import com.example.order.domain.model.Order;

public interface OrderRepository {
void save(Order order);
Order findById(String orderId);
Order update(Order order);
}

3.1.3 领域服务层

OrderDomainService.java

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
package com.example.order.domain.service;

import com.example.order.domain.model.Order;
import com.example.order.domain.model.OrderItem;
import com.example.order.domain.model.OrderStatus;
import com.example.order.domain.repository.OrderRepository;

import java.util.List;

public class OrderDomainService {

public boolean validateOrderItems(List<OrderItem> items) {
for (OrderItem item : items) {
if (item.getQuantity() <= 0 || item.getPrice() <= 0) {
return false;
}
}
return true;
}

public void validateOrderStatusForConfirm(Order order) {
if (order.getStatus() != OrderStatus.CREATED) {
throw new IllegalStateException("Order can only be confirmed if it is in CREATED status.");
}
}

public boolean isOrderValidForCreate(String customerId, List<OrderItem> items) {
return validateOrderItems(items);
}

public void confirmOrder(Order order) {
validateOrderStatusForConfirm(order);
order.confirm();
}
}

3.2 应用层

OrderApplicationService.java

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
package com.example.order.application;

import com.example.order.domain.model.Customer;
import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;
import com.example.order.infrastructure.event.OrderEventPublisher;

import java.util.List;

public class OrderApplicationService {

private final OrderRepository orderRepository;
private final OrderEventPublisher eventPublisher;

public OrderApplicationService(OrderRepository orderRepository, OrderEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}

public String createOrder(String customerId, List<OrderItem> items) {
Order order = new Order(customerId, items);
orderRepository.save(order);
eventPublisher.publishOrderCreatedEvent(order.getOrderId());
return order.getOrderId();
}

public void confirmOrder(String orderId) {
Order order = orderRepository.findById(orderId);
order.confirm();
orderRepository.update(order);
eventPublisher.publishOrderConfirmedEvent(orderId);
}
}

3.3 基础设施层(infrastructure)

OrderRepositoryImpl.java - 模拟数据库操作

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
package com.example.order.infrastructure.persistence;

import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;

import java.util.HashMap;
import java.util.Map;

public class OrderRepositoryImpl implements OrderRepository {
private Map<String, Order> db = new HashMap<>();

@Override
public void save(Order order) {
db.put(order.getOrderId(), order);
}

@Override
public Order update(Order order) {
return db.put(order.getOrderId(), order);
}

@Override
public Order findById(String orderId) {
return db.get(orderId);
}
}

OrderEventPublisher.java - 发布事件

1
2
3
4
5
6
7
8
9
10
11
package com.example.order.infrastructure.event;

public class OrderEventPublisher {
public void publishOrderCreatedEvent(String orderId) {
System.out.println("Order Created Event Published: " + orderId);
}

public void publishOrderConfirmedEvent(String orderId) {
System.out.println("Order Confirmed Event Published: " + orderId);
}
}

3.4 接口层

OrderController.java - 简单的控制层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.order.interfaces;

import com.example.order.application.OrderApplicationService;
import com.example.order.domain.model.OrderItem;

import java.util.List;

public class OrderController {

private final OrderApplicationService orderService;

public OrderController(OrderApplicationService orderService) {
this.orderService = orderService;
}

public String createOrder(String customerId, List<OrderItem> items) {
return orderService.createOrder(customerId, items);
}

public void confirmOrder(String orderId) {
orderService.confirmOrder(orderId);
}
}

4 参考资料

通过 pyenv 命令可以管理本地的 python 版本和 python 环境(各类安装包)。

安装 python 版本

mac 下目前已经无法通过 brew 命令安装 python2,可以使用 pyenv 工具来安装 python 2。

1
2
brew install pyenv
pyenv install 2.7.18

下载下来的 python 版本位于 ~/.pyenv/versions 目录下。

创建 virtualenv 环境

需要先安装插件 pyenv-virtualenv:

1
brew install pyenv-virtualenv

并在 ~/.zshrc 文件中新增加如下内容,并新打开终端窗口,确保插件被加载:

1
2
3
4
5
# 添加 pyenv 和 pyenv-virtualenv 初始化
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

创建新的 python 环境 python2,并激活该环境:

1
2
pyenv virtualenv 2.7.18 python2
pyenv activate python2

创建的环境会存在于 ~/.pyenv/versions 目录下。

  • NLP:自然语言处理 Natural Language Processing
  • 词向量:Word Vector,一种寻找词和词之间相似性的技术,词汇在各个维度上的特征用数值向量进行表示,利用这些维度上的特征相似程度,从而判断出词和词之间的相似程度。通常又叫做 ”词嵌入“(Word Embedding)。
  • 词嵌入:Word Embedding。将词映射到向量空间的过程。
  • 张量:概念源自物理学和数学中的张量分析,可以理解为多维数组的通用说法。可以是一维、或者 N 维的数组。
  • 泛化:Generalization,模型在未见过数据上的表现能力。
  • 过拟合:Overfitting,模型在训练数据上表现良好,但在新数据上表现不佳。
  • 欠拟合:Underfitting,模型在训练数据上和新数据上都表现不佳。
  • One-Hot 编码:一种用于处理分类数据的编码方法,基本思想是将每个类别的特征表示为一个二进制向量,向量的长度等于类别的数量。对于每个样本,该样本属于的类别的位置上的值为1,其余位置的值为0。这种编码方式确保了每个样本的表示是稀疏的,即除了一个位置为1外,其他位置都是0。

本文为对《GPT 图解 - 大模型是怎样构建的》一书的学习笔记,所有的例子和代码均来源于本书。

基本介绍

Bag-of-Words 模型为早期的语言模型,诞生于 1954 年。

核心思想:将文本中的词看作一个个独立的个体,不考虑在句子中的顺序,只关心词出现频次。

应用场景:文本分析、情感分析

代码实践

准备数据集

1
2
3
4
5
corpus = ["我特别特别喜欢看电影",  
"这部电影真的是很好看的电影",
"今天天气真好是难得的好天气",
"我今天去看了一部电影",
"电影院的电影都很好看"]

对数据集进行分词

1
2
3
4
import jieba
corpus_tokenized = [list(jieba.cut(sentence)) for sentence in corpus]

print(corpus_tokenized)

分词完成的结果如下:

1
[['我', '特别', '特别', '喜欢', '看', '电影'], ['这部', '电影', '真的', '是', '很', '好看', '的', '电影'], ['今天天气', '真好', '是', '难得', '的', '好', '天气'], ['我', '今天', '去', '看', '了', '一部', '电影'], ['电影院', '的', '电影', '都', '很', '好看']]

创建词汇表

1
2
3
4
5
6
7
8
word_dict = {} # 初始化词汇表  
# 遍历分词后的语料库
for sentence in corpus_tokenized:
for word in sentence:
# 如果词汇表中没有该词,则将其添加到词汇表中
if word not in word_dict:
word_dict[word] = len(word_dict) # 分配当前词汇表索引
print(" 词汇表:", word_dict) # 打印词汇表

获取到如下结果,其中 key 为对应的词,value 为词出现的频率。

1
词汇表: {'我': 0, '特别': 1, '喜欢': 2, '看': 3, '电影': 4, '这部': 5, '真的': 6, '是': 7, '很': 8, '好看': 9, '的': 10, '今天天气': 11, '真好': 12, '难得': 13, '好': 14, '天气': 15, '今天': 16, '去': 17, '了': 18, '一部': 19, '电影院': 20, '都': 21}

生成词袋表示

1
2
3
4
5
6
7
8
9
10
11
12
# 根据词汇表将句子转换为词袋表示  
bow_vectors = [] # 初始化词袋表示
# 遍历分词后的语料库
for sentence in corpus_tokenized:
# 初始化一个全 0 向量,其长度等于词汇表大小
sentence_vector = [0] * len(word_dict)
for word in sentence:
# 将对应词的索引位置加 1,表示该词在当前句子中出现了一次
sentence_vector[word_dict[word]] += 1
# 将当前句子的词袋向量添加到向量列表中
bow_vectors.append(sentence_vector)
print(" 词袋表示:", bow_vectors) # 打印词袋表示

输出如下结果:

1
2
3
4
5
6
7
[
[1, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
]

输出结果需要稍微理解一下

  • 数组中的每一行表示语料中的一句话
  • 每个元素表示该词在当前句子中出现的次数。例如第一行的第二个元素 2 为“特别”,说明“特别”在第一句话“我特别特别喜欢看电影”总一共出现了两次。
  • 每一行一共有 22 个元素,说明所有的语料一共有 22 个词。

可以看到整个的输出结果为一个稀疏矩阵,尤其是当词汇量变大后,矩阵会更加稀疏。

计算余弦相似度

该步骤需要有一点数学基础。

余弦相似度:用来衡量两个向量的相似程度。值在 -1 到 1 之间,值越接近 1,两个向量越相似。越接近 -1,表示两个向量越不相似。值为 0 时,表示没有明显的相似性。

公式如下:

1
(A * B) / (||A|| * ||B||)

(A * B) 为向量的点积,||A|| 和 ||B|| 表示向量的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 导入 numpy 库,用于计算余弦相似度  
import numpy as np
# 定义余弦相似度函数
def cosine_similarity(vec1, vec2):
dot_product = np.dot(vec1, vec2) # 计算向量 vec1 和 vec2 的点积
norm_a = np.linalg.norm(vec1) # 计算向量 vec1 的范数
norm_b = np.linalg.norm(vec2) # 计算向量 vec2 的范数
return dot_product / (norm_a * norm_b) # 返回余弦相似度
# 初始化一个全 0 矩阵,用于存储余弦相似度
similarity_matrix = np.zeros((len(corpus), len(corpus)))
# 计算每两个句子之间的余弦相似度
for i in range(len(corpus)):
for j in range(len(corpus)):
similarity_matrix[i][j] = cosine_similarity(bow_vectors[i],
bow_vectors[j])

可视化余弦相似度

1
2
3
4
5
6
7
8
9
10
11
12
13
# 导入 matplotlib 库,用于可视化余弦相似度矩阵  
import matplotlib.pyplot as plt
plt.rcParams["font.family"]=['SimHei'] # 用来设定字体样式
plt.rcParams['font.sans-serif']=['SimHei'] # 用来设定无衬线字体样式
plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号
fig, ax = plt.subplots() # 创建一个绘图对象
# 使用 matshow 函数绘制余弦相似度矩阵,颜色使用蓝色调
cax = ax.matshow(similarity_matrix, cmap=plt.cm.Blues)
fig.colorbar(cax) # 条形图颜色映射
ax.set_xticks(range(len(corpus))) # x 轴刻度
ax.set_yticks(range(len(corpus))) # y 轴刻度
ax.set_xticklabels(corpus, rotation=45, ha='left') # 刻度标签 ax.set_yticklabels(corpus) # 刻度标签为原始句子
plt.show() # 显示图形

最终获取到如下的结果:
image.png
每个单元格表示两个句子之间的相似度。

总结

缺点:

  1. 采用了稀疏矩阵,每个单词是一个维度,计算效率较低。
  2. 忽略了文本的上下文信息。

在 2022 年的上半年我有了属于自己的北京新能源指标,终于可以抛弃掉倍受限制的外地牌照,实现出门自由的目标了,有种翻身当家做主人般的无奈喜悦。

摆在我面前的第一个问题就是该换哪款新能源汽车,车对我而言仅仅是个代步工具而已,能满足出行需求即可,因此买车的原则比较简单:

  1. 买最大众化的销量最高的车型。当时销量最高的品牌当属比亚迪和特斯拉,单车型销量最高的当属特斯拉 Model Y,甚至 Model Y 火热到需要等至少两个月才交付,而且还在涨价。
  2. 后排座椅可以放倒,可以在车里睡觉,作为一个行走的床使用,Model Y 也正好可以满足需求。

我在苦苦观望了半年之久,等过了特斯拉的两拨降价后,在 2023 年元旦前夕选择了 Model Y 标准续航版。我担心过了 2023 年元旦后就没有补贴了,会额外增加一部分购车成本。事实证明我的判断是完全错误的,补贴不仅没有取消,而且一个月后居然降价两万多,肠子都要悔青了。

到目前为止,已经开了两年的时间,一共跑了 2 万多公里。虽然我对车并不感兴趣,但油车也开过,多少还是有些话语权的,作为一个单纯的用户,我来聊聊我的用车感受。

IMG_0976 2.jpg

整体感受

驾驶

开车后的第一感受就是悬挂偏硬。虽然采用了双叉臂 + 五连杆悬挂,但不得不说 Model Y 还是非常颠的。在换车之后,家人们都觉得车变贵了,肯定会变舒服了,殊不知颠簸感要强于之前的大众朗逸,但颠簸相比朗逸会有一种高级感。

其次是加速度。我对于加速基本无追求,长期在舒适模式下驾驶,运动模式对我而言提速过于激烈了一些。即使是在舒适模式下,也经常会让后排乘客晕车,加速度仍然是很快。但要是自己独自开车,驾驶感受还是挺不错的。尤其是绿灯起步时,往往能一骑绝尘,超越同一起跑线的其他车辆。

单踏板真香。特斯拉的单踏板一直在被外界诟病,但我一直是单踏板的模式运行,日常的通行中,很少踩刹车,基本上一个踏板就够了,大大减轻了右腿的压力。抛开第一性原理不谈,产品的设计本来就是服务人类的,以简化操作作为设计目标。

车子质量

特斯拉是一家对于利润极度压榨的公司,在这种原则下造车,往往车关键部位的质量非常好,但次要的零件由于要追求性价比,不会用最好的供应商,质量就会有些堪忧,反正坏了给你换,只要不坏就是赚到了,整体而言,特斯拉还是赚的。

在用车的短短一年内,我遇到了两次故障:

  1. 空调坏掉一次。在一次超充充电完成后,空调完全不能制冷,去售后维修认定是空调压缩机坏掉了。
  2. 离开车后不能自动锁车。经过检查是主驾驶座椅下传感器坏掉了,检测不到驾驶员已经离开车。

内饰

​相比于国内品牌的冰箱彩电大沙发的豪华配置,特斯拉无遗是毛坯房。我个人对内饰也没有过多的要求,也喜欢极简风,倒是可以接受。

座椅加热功能在冬天还是非常的必要,但夏天就比较难受了,居然没有座椅通风。开一路车后,后背非常容易湿透。不过网上倒是有可以通过点烟器供电的外置通风座椅垫,我暂时没有体验过。

车顶的大玻璃对我而言影响不大,第一年的时候我买了一个手动遮阳帘挂上去。但第二年我就干脆没有挂上去,也不影响用车。在夏天的太阳下,虽然大玻璃会比较热, 但如果不用手去触摸,坐在车内头部是基本感受不到上面热量的。

空间

作为大号鼠标,​Model Y 的空间还是非常给力的。我车上长期放着一整套的露营装备,除了露营车外,其他的装备全部可以放到前备箱和后备箱的下方储物格中,这些空间完全是比油车多出的储物空间。

值得一提的是,我曾经搬过一次家,全程没有找搬家公司,家里所有的物品全部是我蚂蚁搬家一点点通过 Model Y 搬完的,虽然断断续续得搬了近两个月。

后排放倒后,在车里​睡觉完全不是问题。​略微不爽的就是不能完全放平,稍微有个角度,但在车里睡觉倒是没啥影响。配合着露营模式,我个人也曾在车里睡过很多次的午觉。

相比之下地盘离地高度就是个大大的劣势了,当然这是电车的通用设计理念。我曾经拿着手机测量过,最矮处跟手机的高度差不多。原先的油车偶尔还会应急停在马路牙子上,Model Y 我是一次也没敢往马路牙子上停。走在马路上过个坑都生怕磕到电池,怕跟保险公司扯上不解的缘分。(此处求 Model 3 车主的心理阴影面积)

续航

相比油车而言,电池无遗是电车的最大短板了。

刚买来时,车机显示满电续航为 435km,目前仅能显示 415km,掉了 20 km,属于在可接受的范围内,如果不追求数字,实际用车也感受不明显。要是跑高速,满打满算 350km 问题不大。

冬天是磷酸铁锂电池的噩梦,充电速度变慢,续航衰减明显,再加上还需要方向盘加热、座椅加热、空调加热这些功能,电耗会进一步提升。电池的回收动能变差,单踏板的减速效果会大大折扣。在零下十度的天气里,刚开启动的时候,动能回收甚至是完全失效的,开一段时间热起来后动能回收才能逐渐恢复一些。

充电方面,虽然有购车和推荐送的充电里程,但是一直没有舍得用完,大部分的时间都在外面的充电桩充电。在北京城市里面,充电还是非常方便的,方圆两公里内就有非常多的充电桩。时间久了后,我基本上会选择在出租车师傅长出没的充电桩充电了,而且大部分会选择在晚上十一点后,充电的价格要便宜挺多,我正好睡的晚,也不影响作息。

最怕的就是节假日期间的高速上了,很多情况下都是要排队充电的。再就是去偏远的地方,甚至北京周边河北境内的很多地方都很难找到充电桩,在张家口市沽源县城,地图上只能找到一家充电桩,而且晚上还不营业,我迫不得已要跑到高速的服务区去充电。

额外吐槽一下,不得不说,特斯拉是家特别会营销的公司。前两年的时候,4680 电池都快要让媒体吹上了天,能量密度提升了多少多少,啥时候量产,最近一年,几乎已经听不到 4680 电池的新闻了。(当然也有可能是我没关注到,顺便 Q 下比亚迪的刀片电池)

软件

特斯拉的 OTA 升级还是比较频繁的,差不多每两个月一次升级。但两年回顾下来,真正有用的软件功能升级并不多。比如下面这次更新,看着就像是个把刀架在程序员脖子上写的更新简介。
IMG_4820.jpg

特斯拉做产品非常的轴,跟苹果有的一拼。有的功能明明非常难用,却愣是不更新。另外就是因为国内的缘故,很多在国外可用的功能,在国内却无法使用,导致使用体验又大打折扣。

相比之下,国内的新能源厂商的车机体验却一路猛追,应该说早已超越了特斯拉的自研车机。当然这么比并不公平,毕竟特斯拉的主战场不是中国,而中国却是新能源厂商的主战场。中国的新能源厂商出海后,车机的体验也一定会大打折扣,有大量的本地化、安全合规等需要适配。

导航

导航功能刚开始用的时候弱爆了,但凡走陌生的路我都要打开手机上的高德导航。上班路上有一段特别拥堵,为了躲避拥堵,我都是刻意的多走一段路,但当我已经偏离了导航很长一段距离后,导航仍然傻到不断提醒让我前方掉头。

经过了两年的迭代,导航已经多次改版,已经好用了很多,车道级导航、红绿灯倒计时这些最近两年才在导航软件上有的新功能都已经陆陆续续引入了。

但相比手机上的导航软件还是有一定的差距,如:在计算拥堵方面还不够精确,显示的拥堵程度总是令我半信半疑,特斯拉可获取的数据量也不足以计算出拥堵的精度。

语音

国内的新势力厂商的语音交互早已经非常流畅,跟车机交流起来毫无障碍。而特斯拉的语音功能只能说是智障,以至于我跟车机的交互都会选择手动点击屏幕。使用语音的唯一场景就是导航时输入目的地了,剩下的下发指令类的场景完全没有使用过,没有通过点击屏幕来的便捷。

摄像头

特斯拉通过摄像头将物理环境通过视觉算法转换为内部的模型,并以此为基础来做更上层的数据输入来源。

但在目前国内的版本上,在一些极端的场景下,摄像头的识别能力还是不行的。比如在下面的场景下,明明前面有一个非常粗的杆子,在车机上却视而不见。

IMG_4553.jpg

车上有那么多的摄像头,却没有 360 全景功能,而是给用户展示出经过处理的模型,这明明是你内部的实现细节,暴露给用户有啥用。在摄像头的识别率还不能到百分百的情况下,其实直接将全景交给人是个更好的选择。

在刚交付时,车身前后的雷达还是可以使用的,在停车时可以在车机上显示出距离障碍物的距离,对停车还是非常有帮助的。但后来特斯拉一直在宣称要基于纯视觉,雷达和纯视觉变为了二选一。

辅助驾驶

我没有花钱购买额外的服务,用的最多的功能就是车道居中和自适应巡航了,不过这个功能在普通的油车上都快成为标配了。另外,车道居中功能在走山路 S 弯的时候根本没法使用,弱爆了!国外的 FSD V12 和 V13 快吹上天了,而国内的辅助驾驶还处于智障水平。

相比于普通的油车,稍微高级点的辅助驾驶功能就只有两个:

  1. 当车速太快,且没有踩刹车的情况下,车机会判断出是否会追尾,并发出滴滴的提示音。这一点还是挺有用的,可以大大降低追尾的概率。
  2. 车道偏离后的被车机紧急控制。我刚买车的时候,曾经有一次向右侧变道,右侧的车在后视镜的盲区内,变道过程中就被车机紧急接管方向盘,避免了一次剐蹭的事故。

当然辅助驾驶还是比较弱的,在很多的极端情况下,还是无能为力的。分享我曾经遇到过两个案例。

案例一:

在右转刚开始,外卖小哥还在马路对面,在车内是很难观察的,即使看到了也预判不了外卖小哥的风骚走位。当车头调转 90 度后,正准备加速时,外卖小哥突然冲到了车身前面,没有任何一个摄像头看到了外面小哥的存在,车机也没有发出告警,位置正好卡在了摄像头的盲区。

好在这次事故后,外卖小哥没啥事,扶起电动车看看外卖无碍后头也不回的就走了,佩服小哥的敬业精神!而我的小 Y 就没那么幸运了,车牌凹进去了一大块,车牌上的漆被碰掉了很多。

这次事故后,我每次右转都要提心吊胆,尤其是在转方向时不做过快的加速,否则可能会超出对方的预判,尤其是视时间为金钱的外卖骑手们的预判。

案例二:

在等红绿灯的时候,左侧车道为摩托车。绿灯起步时,摩托车要右转到辅路。摩托车以为其他车道的车肯定没他加速快,忽略了有一种车叫 Model Y。由于带着头盔,头也没法向后扭,就直接右转到辅路。

摩托车右转到我车前时,还突然减了一下速,车机都完全没反应过来,没有任何告警。幸好我当时反应快,踩了一脚刹车,要不然后果不堪设想。估计事后摩托车也不知道自己离事故这么近。

上面两个危险的经历告诉了我们两个道理:

  1. 远离电动车和摩托车,能离多远离多远。
  2. 特斯拉的辅助驾驶是完全靠不住的。

周边产品

因为特斯拉有 OpenAPI 可以供第三方调用,因此有一些第三方的软件我也在使用。据说后续 OpenAPI 要收费了,不知道后续围绕 OpenAPI 的一些应用是否会有调整。

TeslaMate

可以在电脑上运行的软件,会持续调用 OpenAPI,并将车的运行数据都记录在本地的数据库中,并通过 grafana 来展出出来。通过该软件,可以方便的查看历史的行车轨迹。

11415AB8-DC9C-4F62-857B-5467F499B35D-14202-0000032B37B0A7DD.PNG
比如今年夏天的时候去达达线、热阿线的路线图,可以精准的绘制出来。

小特钥匙

小特钥匙是 Apple Watch 上的应用,可以实现通过 Apple Watch 来打开车门,而且走的是蓝牙协议。主要的场景就是当手机没电时,可以通过手表了开锁,多了一条开门的途径。

因为是收费软件,而且功能非常单一,很多人都觉得该应用很鸡肋,我也是在一次手机没电开不了车门后下定决心购买的。

另外,特斯拉的官方 Apple Watch 版本的应用也快要上线了。

总结

开了两年的车,整体而言还是比较满意的。本来也不可能有百分百满足自己的产品,只有适合自己的产品。

一件东西也就在刚开始买的时候会比较纠结,各种比较权衡,一旦拥有了,即使有缺点也是可以忍受的,除非忍无可忍。人类真是个奇怪的动物。

交换机堆叠:是指将一台以上的交换机组合起来工作,从逻辑上虚拟成一台交换机,作为一个整体参与数据转发。

image.png
交换机的堆叠可以通过DAC高速线缆,光模块或者专门用于堆叠的线缆来实现。

交换机堆叠包括堆叠主交换机和堆叠备交换机,一主多备工作模式。堆叠主交换机存储整个交换机堆栈的运行配置文件,并通过堆叠主交换机对所有的堆叠交换机进行管理。如果主交换机发生故障,堆叠系统会从备交换机中选择新的堆叠主交换机,且不会影响整个网络的性能。

堆叠的协议都是私有的,因此不同品牌的交换机无法实现堆叠。

资料

阿里巴巴的财年为每年4月1日至次年3月31日,而4月1日至9月30日为财年的上半年。大部分的团队每半个财年要完成一次述职,而眼下的 9 月 30 日前又到了述职的日子。

在我过往的工作经历中,并没有述职的习惯,绩效的考核也相对随意一些,绩效的好坏跟主管的主管判断关系比较大,但通常绩效的好坏跟个人的成绩也是成正比的,不会出现较大的偏差。

在阿里绩效的绩效考核会非常被重视,希望能够通过公平的方法来分配高低绩效,降低主管的主观判断,当然主观的判断仍然是无法避免的,而且也会仍然占较大的比重。

述职可以作为一种常见的职场总结方式,也是绩效考核的一个重要的参考依据。

记得刚来阿里的时候,团队处于业务的爆发期,述职并不是太被重视,尤其是形式方面。某一年的述职中,我仅准备了可怜的三页 PPT,仅完成了做的事情的简单罗列,缺少一些自己的思考总结,当时就被喷的比较惨。当然述职中没有体现,并不代表平常就没有思考,即使平时没有思考也不代表就会影响实际的工作。

一到述职,自己也曾经非常头疼,经过了这几年的洗礼,自己对职场的述职也有了一点点自己的总结,希望能帮助到大家。

为什么要做述职?

存在即合理,我认为的述职有这么几个好处:

  1. 对自己过去做的事情的非常好的总结,帮助自己重新认识自己。这也是最关键的一点。大家肯定有这样的体会,如果让你回想过去几个月甚至一年做的事情,恐怕很难一下子想全。在现代职场中,大家都是匆匆忙忙的赶路人,任务一个接一个的完成,很难像在学校读书一样有温故而知新的机会。而述职恰恰是一个非常好的机会,回顾过去一段时间内自己做的事情,找找自己的闪光点,反思一下如果一件事情再重新做,会不会做的更好更高效。人总是在不断的总结经验中成长,述职是个很不错的职场成长机会。
  2. 让自己了解别人做的事情。别人做的最好的事情、吃的亏、踩过的坑都已经帮你总结好了,这是多好的一个学习机会。也许在吸取别人经验的过程中,会迸发出一些新的灵感。
  3. 让别人了解自己做的事情。自己做的申请同样需要传播给其他的同学,或许别人才能发现自己的闪光点,是个提升自己影响力的好时机(当然前提是自己要有货)。自己身上的不足也能够让别人发现,进而别人来帮助自己成长。
  4. 作为主管绩效考核的依据。这一点我倒认为并没有那么关键,自己做的事情自己的能力在日常工作中已经足够体现,不需要在述职的时候才做出判断,最多也就是作为判断的依据而已。
  5. 提升自己的演讲能力。演讲是一种能力,需要刻意练习。述职中要面对不同的人群,在有效的时间内将自己的事情给别人讲明白属实不是一件容易的事情。
  6. 通过述职这种形式化的方式来间接的促进员工在日常中工作的积极性。对于一部分员工,如果缺少了述职这种形式,在日常工作中就少了一份压力,工作中可能就会稍显懈怠。

述职的常见误区有哪些?

述职也并非百害而无一利,同样是一把双刃剑,如果能利用的好,会对自己有较大的帮助。如果利用不好,就容易反噬。

  1. 不要搞的花里胡哨太抽象。也是述职中最常见的问题,也是最让人诟病的。我见到过不少搞得花里胡哨的述职材料,一件很简单的事情非要说的很复杂,让人感觉很高大上,殊不知包装后,能听懂的人群更加少了。尽量少用 PPT,直接使用文档形式就非常不错,省去了大量排版的工作。图片也尽可能的简介,不要为了美观而消耗太多的时间。简简单单的真实就是最好的述职。
  2. 不要报喜不报忧。这也是个非常常见的问题。这种虚假的汇报,或许可以玩转一次述职,下次述职就很难遮盖过去了。或许一部分人可以糊弄过去,另外一部分人就未必。一个谎言的背后就得需要无数个谎言来弥补,实事求是在述职中非常的关键,严禁弄虚作假。
  3. 不要为了述职而述职。很多人都比较头疼述职,为了述职不得不做准备。其实这种心态就不太对,为了做而做,那么一定做不好。
  4. 不要消耗过多的时间来准备。如果一次要准备上好几天的时间就有点不值了,毕竟一年真正有效的工作时间才能有多少天呢,述职本身并不会产生业务价值,不要本末倒置。
  5. 不要陷在自己的主观世界中。要考虑到参加述职的人群,给别人讲明白自己做的事情。

述职的形式是什么样子的?

职场中的很多事情都有固定的套路,尤其是在一家公司内部。最好的方式就是参考老员工的述职报告,找到固定的套路,或者按照主管要求的套路来。最好不要试图找到一条新的方法,前辈们总结的路基本都不会错。

述职的内容基本要遵循 STAR 法则(即背景 Situation、任务 Task、行为 Action、结果 Result),主要包含几部分内容:

  1. Situation:事情的背景。
  2. Task:面临的挑战和遇到的困难。项目干系人,自己在其中承担的角色。
  3. Action:自己的解决方案。
  4. Result:获得的业务结果。能量化的一些业务结果最好要量化,这样会非常直观。

其中也可以讲讲自己的总结思考和个人成长等内容。

另外在讲述的时候要符合金字塔原理,核心的原则:

  1. 结论先行。
  2. 每层结论下面的论据不要超过7个。
  3. 每一个论点要言之有物,有明确的思想。

述职的材料该如何准备?

这里有几个建议:

  1. 如果有写周报的习惯,看看周报的内容。
  2. 看看各种群的聊天记录的内容。
  3. 看看自己的代码提交记录。
  4. 看看自己的文档。

当然,上面的一些前提都是平常要有一些积累,如果平常积累的多,那么述职的材料准备起来就比较简单快速。

最后,述职是一种管理员工的手段,作为员工在无法取消述职这种形式的时候,还是要尽可能将述职作为一种促进自己提升的手段,不要为了述职而述职。

0%