【设计模式精讲】25.状态模式(State)

【设计模式精讲】25.状态模式(State)

【摘要】:订单能支付、能发货、能关闭------每种动作在每种状态下都有不同答案,直觉的实现是给每个方法配一份全量 switch,于是「Created/Paid/Shipped」的分支在五个方法里各复制一遍,加一个状态全体加 case,合法的迁移图散落得没人能一眼看清。本文从订单状态机的 switch 蔓延讲起,给出状态模式的 GoF 意图:状态一变、行为整个换一套,对象「看起来像换了一个类」;状态对象无状态、全程共享一份(与享元、单例同宗),迁移由当前状态自己决定;现代版给出迁移表与 C++17 的 variant 值语义状态------状态第一次能自带数据。本文重点辨析状态与策略:结构完全相同,切换驱动方相反。文末对照 AOSP 系统状态机与 Boost.Statechart。

【关键词】:状态模式、状态机、迁移、switch 消除、variant、状态共享

【代码基准】:C++17

1. 每个方法里都长着一模一样的 switch

电商订单有四个状态:已创建、已支付、已发货、已关闭。每个动作在每种状态下答案不同------直觉的实现是这样的:

cpp 复制代码
// 说明性片段
// ❌ switch 在每个方法里复制粘贴
void Order::pay() {
  switch (state_) {
    case Created:
      state_ = Paid;
      break;
    case Paid:
      throw "已支付,请勿重复";
    case Shipped:
      throw "已发货,不能改支付";
    case Closed:
      throw "已关闭";
  }
}

void Order::ship() {
  switch (state_) {      // 又是一模一样的一棵
    case Created:
      throw "未支付,不能发货";
    case Paid:
      state_ = Shipped;
      break;
    ...
  }
}

三笔账很快就找上门:其一,同一批 case 在每个方法里复制 ------pay/ship/close/refund 四个方法四棵 switch,改一处忘三处;其二,合法迁移图无人能答 ------「从 Paid 能到哪」这个问题,要读完四棵 switch 才敢回答,第 22 篇中介者埋的「交互规则状态机化」伏笔在这里兑现;其三,加状态 = 全体方法加 case------想把「关闭」拆成「用户取消」与「超时关闭」,四个方法全部返工。

细看病灶会发现它和第 26 篇预告的「策略 switch」形似而神异:这里的分支不是「同一问题的平行解法」,而是对象自身的生涯阶段------行为随状态整体切换,状态之间还有合法的迁移关系。把「每种状态下的全部行为」收进一个类,switch 就没了住处。

2. 模式意图与定义

  • 一句话定义 :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
    解决的问题:行为随状态大变、状态间有迁移规则的领域里,switch 的复制蔓延与迁移图的失散。
  • GoF 原文意图Allow an object to alter its behavior when its internal state changes. The object will appear to change its class. (允许对象在内部状态改变时改变它的行为。)后半句是全部魔法所在------调用方拿着的还是同一个 Order&,但 pay() 的答案已经完全换了人,「好像换了个类」
  • Refactoring Guru 的表述:状态模式让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身的类;RG 用媒体播放器与订单做例------同一个「播放/暂停」按钮,在不同状态下做完全相反的事。

三条定性:

  1. 「状态 × 行为」矩阵按列拆开。switch 的写法是按行为切(每个方法横扫所有状态),状态模式按状态切------每种状态一个类,该状态下的全部行为住在一起;
  2. 状态对象无状态、可共享 。订单号、金额、物流单号这些数据全归上下文 ,状态类只有行为没有字段------于是四种状态全程各共享一份实例,与第 8 篇单例的 instance()、第 15 篇享元「种类数代替实例数」一脉相承;
  3. 迁移由当前状态决定 。「谁能到哪」写在各状态的操作里------CreatedState::pay 自己知道下一步是 Paid。这个「自己迁自己」的性质是与策略模式辨析的分水岭,第 6 节正面展开。

3. UML 图 + 结构说明

#mermaid-svg-WShZgKu7Wu3INz1n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WShZgKu7Wu3INz1n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WShZgKu7Wu3INz1n .error-icon{fill:#552222;}#mermaid-svg-WShZgKu7Wu3INz1n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WShZgKu7Wu3INz1n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WShZgKu7Wu3INz1n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WShZgKu7Wu3INz1n .marker.cross{stroke:#333333;}#mermaid-svg-WShZgKu7Wu3INz1n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WShZgKu7Wu3INz1n p{margin:0;}#mermaid-svg-WShZgKu7Wu3INz1n g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-WShZgKu7Wu3INz1n g.classGroup text .title{font-weight:bolder;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster-label text{fill:#333;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster-label span{color:#333;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster-label span p{background-color:transparent;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster text{fill:#333;}#mermaid-svg-WShZgKu7Wu3INz1n .cluster span{color:#333;}#mermaid-svg-WShZgKu7Wu3INz1n .nodeLabel,#mermaid-svg-WShZgKu7Wu3INz1n .edgeLabel{color:#131300;}#mermaid-svg-WShZgKu7Wu3INz1n .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-WShZgKu7Wu3INz1n .label text{fill:#131300;}#mermaid-svg-WShZgKu7Wu3INz1n .labelBkg{background:#ECECFF;}#mermaid-svg-WShZgKu7Wu3INz1n .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-WShZgKu7Wu3INz1n .classTitle{font-weight:bolder;}#mermaid-svg-WShZgKu7Wu3INz1n .node rect,#mermaid-svg-WShZgKu7Wu3INz1n .node circle,#mermaid-svg-WShZgKu7Wu3INz1n .node ellipse,#mermaid-svg-WShZgKu7Wu3INz1n .node polygon,#mermaid-svg-WShZgKu7Wu3INz1n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WShZgKu7Wu3INz1n .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n g.clickable{cursor:pointer;}#mermaid-svg-WShZgKu7Wu3INz1n g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-WShZgKu7Wu3INz1n g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-WShZgKu7Wu3INz1n .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-WShZgKu7Wu3INz1n .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-WShZgKu7Wu3INz1n .dashed-line{stroke-dasharray:3;}#mermaid-svg-WShZgKu7Wu3INz1n .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-WShZgKu7Wu3INz1n #compositionStart,#mermaid-svg-WShZgKu7Wu3INz1n .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #compositionEnd,#mermaid-svg-WShZgKu7Wu3INz1n .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #dependencyStart,#mermaid-svg-WShZgKu7Wu3INz1n .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #dependencyStart,#mermaid-svg-WShZgKu7Wu3INz1n .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #extensionStart,#mermaid-svg-WShZgKu7Wu3INz1n .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #extensionEnd,#mermaid-svg-WShZgKu7Wu3INz1n .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #aggregationStart,#mermaid-svg-WShZgKu7Wu3INz1n .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #aggregationEnd,#mermaid-svg-WShZgKu7Wu3INz1n .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #lollipopStart,#mermaid-svg-WShZgKu7Wu3INz1n .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n #lollipopEnd,#mermaid-svg-WShZgKu7Wu3INz1n .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-WShZgKu7Wu3INz1n .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-WShZgKu7Wu3INz1n .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WShZgKu7Wu3INz1n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WShZgKu7Wu3INz1n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WShZgKu7Wu3INz1n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 当前状态(共享实例)
pay() 迁移
ship() 迁移
<<interface>>
OrderState
+pay(o) : void
+ship(o) : void
+name() : string
CreatedState
+pay(o) : void
+ship(o) : void
PaidState
+pay(o) : void
+ship(o) : void
ShippedState
+pay(o) : void
+ship(o) : void
Order
-state_ OrderState
+pay() : void
+ship() : void
+changeTo(s) : void

三个参与者:

  • 状态(State)接口:声明该领域全部动作------每个具体状态都要对每个动作给出自己的答案;
  • 具体状态(Concrete State)CreatedState 等------只写「我这个状态下每个动作干什么、迁去哪」;
  • 上下文(Context)Order------持当前状态指针、保管全部数据字段,把每个动作原样委托给当前状态。

值得盯着图认的两处:CreatedState ..> PaidState 那两条虚线是状态互相认识 的证据(迁移目标是自己的同伴)------记住它,第 6 节与策略的辨析要用;而 Order o-- OrderState 的组合边指向共享实例,上下文从不创建/销毁状态对象。拓扑与第 26 篇策略「Context o-- Strategy」完全同形------差异全在语义箭头上,这正是行为型模式比结构型更依赖意图判别的原因

4. 传统 C++ 写法(C++11 之前)

C++98 完整形态:状态接口声明全部动作,具体状态用 instance() 静态实例共享,上下文只管数据与委托:

cpp 复制代码
// C++98/03 写法
#include <stdio.h>

class Order;

// ---- 状态接口 ----
class OrderState {
public:
  virtual ~OrderState() {}

  virtual void pay(Order& o) = 0;
  virtual void ship(Order& o) = 0;
  virtual const char* name()
      const = 0;

protected:
  OrderState() {}

private:
  OrderState(const OrderState&);
  OrderState& operator=(
      const OrderState&);
};

// ---- 上下文:数据归它,行为全委托 ----
class Order {
public:
  Order();

  void pay() {
    state_->pay(*this);
  }
  void ship() {
    state_->ship(*this);
  }

  void changeTo(const OrderState* s) {
    printf("  %s -> %s\n",
           state_->name(), s->name());
    state_ = s;
  }

  void dispatch() {   // 业务动作归上下文
    printf("  商品已发出\n");
  }

private:
  const OrderState* state_;
};

// ---- 具体状态:声明与定义分离,
//      因为迁移目标互相引用 ----
class CreatedState : public OrderState {
public:
  static const OrderState* instance();
  const char* name() const;
  void pay(Order& o);
  void ship(Order& o);
};

class PaidState : public OrderState {
public:
  static const OrderState* instance();
  const char* name() const;
  void pay(Order& o);
  void ship(Order& o);
};

class ShippedState : public OrderState {
public:
  static const OrderState* instance();
  const char* name() const;
  void pay(Order& o);
  void ship(Order& o);
};

const OrderState*
CreatedState::instance() {
  static CreatedState s;   // 无状态,共享
  return &s;
}
const char*
CreatedState::name() const {
  return "Created";
}
void CreatedState::pay(Order& o) {
  printf("支付成功\n");
  o.changeTo(PaidState::instance());
}
void CreatedState::ship(Order&) {
  printf("未支付,拒绝发货\n");
}

const OrderState* PaidState::instance() {
  static PaidState s;
  return &s;
}
const char* PaidState::name() const {
  return "Paid";
}
void PaidState::pay(Order&) {
  printf("重复支付,忽略\n");
}
void PaidState::ship(Order& o) {
  o.dispatch();
  o.changeTo(
      ShippedState::instance());
}

const OrderState*
ShippedState::instance() {
  static ShippedState s;
  return &s;
}
const char*
ShippedState::name() const {
  return "Shipped";
}
void ShippedState::pay(Order&) {
  printf("已发货,不能改支付\n");
}
void ShippedState::ship(Order&) {
  printf("已发货,忽略\n");
}

Order::Order()
    : state_(CreatedState::instance()) {}

int main() {
  Order order;
  order.pay();    // 支付成功 Created->Paid
  order.ship();   // 发出 Paid->Shipped
  order.pay();    // 已发货,不能改支付
  return 0;
}

对照第 1 节:四棵 switch 整体消失;加「ClosedState」= 新写一个类,已有状态里只有出边指向它的地方各加一行;「从 Paid 能到哪」------读 PaidState 两个方法即可,迁移的出边总是写在状态自己家里

三条传统写法的铁律:状态类只有行为没有数据 ------字段全在 Order,状态对象才能安全共享一份(谁往状态类里塞字段,谁就为并发埋雷);迁移写在状态操作里 ------changeTo 只由状态调用,上下文永不「查表式」地指挥迁移(要查表就整体换表驱动,见第 5 节);声明与定义分离------状态之间互为迁移目标,C++98 里先声明三个类再依次定义,绕开「先有鸡还是先有蛋」。

5. 现代 C++ 进阶写法

升级零:override 补齐 + 无锁 instance()virtual 实现处补 override 拼写即查;instance() 里的局部静态初始化自 C++11 起由语言保证线程安全(第 8 篇讲过的守卫字节),多线程下订单各持状态指针、共享同一批实例,读路径无锁。

改进一:迁移表------当规则规整成数据。若状态机的动作很规整(「状态 × 事件 → 迁移 + 动作」),类层次可以让位给一张表与一个通用引擎:

cpp 复制代码
// 节选:表驱动的规整状态机
#include <cstdio>
#include <functional>
#include <string>
#include <unordered_map>

struct Rule {
  std::string to;                 // 迁移目标
  std::function<void()> action;   // 出发动作
};

// key = "当前状态:事件"
std::unordered_map<std::string, Rule>
    rules = {
  {"Created:pay", {"Paid", [] {
     std::printf("支付成功\n"); }}},
  {"Paid:ship", {"Shipped", [] {
     std::printf("商品已发出\n"); }}},
};

void fire(std::string& state,
          const std::string& event) {
  auto it = rules.find(
      state + ":" + event);
  if (it == rules.end()) {
    std::printf("非法迁移:%s + %s\n",
                state.c_str(),
                event.c_str());
    return;               // 兜底必须明确
  }
  it->second.action();
  state = it->second.to;
}

表与类两条路线的取舍:行为复杂、状态间逻辑差异大 用类层次(多态的表达力);规则规整、要配置化或可视化用表(一张表就是一张可审计的迁移图)。第 22 篇中介者「规则表化」的护栏正是这条路线。

改进二:variant 值语义状态------状态第一次能带数据 。「已支付时间」「物流单号」这类天然属于某个状态的数据,类层次里只能委屈地住在上下文;C++17 的 variant 让状态本身携带数据:

cpp 复制代码
// 节选:值语义状态(C++17)
#include <ctime>
#include <exception>
#include <string>
#include <variant>

struct Created {};
struct Paid {
  std::time_t at;         // 状态自带数据
};
struct Shipped {
  std::string tracking;   // 物流单号
};
struct Closed {};

using State = std::variant<Created, Paid,
                           Shipped, Closed>;

class Order {
public:
  void pay() {
    std::visit(
        [this](auto& s) {
          using T = std::decay_t<
              decltype(s)>;
          if constexpr (std::is_same_v<
                            T, Created>) {
            state_ =
                Paid{std::time(nullptr)};
          } else {
            throw std::runtime_error(
                "当前状态不能支付");
          }
        },
        state_);
  }

  State state_;   // 状态是值,不是指针
};

状态变成值对象后:非法迁移天然无法表达(Paid 里根本没有「支付时间」之外的字段),状态数据随赋值整体更换,内存连续、无堆分配;代价是「状态集」编译期定死------运行期动态发现新状态(插件式状态机)还得回到类层次。

展望:协程让「异步状态机」(await 一个迁移条件)有了新写法;轻量级状态机库(如 boost-ext/SML)用表达式把迁移图写成代码,一行一条边。

6. 优缺点与适用场景

  • ✅ 优点(GoF 后果清单):行为按状态局部化 ------每个状态的答案集中在一个类,switch 蔓延终结;迁移规则有了住址------每个状态的出边写在自家方法里,加状态不碰旧状态的行为;状态对象无状态可共享,多上下文并发复用同一批实例;「看起来换了类」------上下文接口稳定,调用方零感知。
  • ❌ 缺点:类数量随状态膨胀 ------四状态四类,简单两态状态机用 enum + switch 反而更直白;迁移分散后全景图要拼装------「整张迁移图长什么样」没有一个地方能直接回答(表驱动补此短板);状态与上下文双向认识(状态要改上下文、读上下文),耦合比多数模式紧。
  • 🎯 适用场景:对象行为随状态显著变化且状态较多------订单/工单/审批流、游戏 AI 与角色、网络协议会话、播放器与设备控制;迁移规则复杂到值得单独建模;状态可作为术语与业务方对表(状态机图即沟通工具)。

〔辨析〕状态 vs 策略(第 26 篇)------结构全同、驱动方相反 。两张 UML 几乎重叠:上下文持一个接口指针、逐操作委托。三处分野:谁驱动切换 ------策略由客户端或外部条件注入一次(换不换是使用者的事),状态由对象自己在操作里迁移 (自己换自己);参与者互相认识吗 ------策略们是平行解法、彼此陌生,状态们互为迁移目标、彼此认识(图上那两条 ..> 虚线);建模对象 ------策略建模「同一问题的多种算法」,状态建模「一个对象的生涯阶段」。一句话:策略是算法插头,状态是对象的生涯 。另两条:状态 vs 观察者(第 24 篇)------观察者是对象向外的广播,状态是对象向内的重构;状态 vs 中介者(第 22 篇)------中介者用状态机管理一组对象 的交互阶段,本篇管单个对象自身的阶段,两者可以嵌套。

7. 开源项目中的身影

AOSP:StateMachine,系统服务的中枢神经 。Android 框架内置 com.android.internal.util.StateMachine:状态组织成层次树,消息(Message)到达时交给当前状态processMessage 处理并按需迁移,Wi-Fi、蓝牙等系统服务用它管理连接生涯(Disconnected → Connecting → Connected),未处理的消息自动上浮给父状态------第 18 篇责任链的「上浮找认领」与第 12 篇的组合树在这里与本篇合流。

点评:系统服务的状态机选择类层次而非表,正是因为各状态的行为差异巨大且要挂定时器、日志等富逻辑------第 5 节的取舍判据在工业代码里的直接印证。

Boost.Statechart:重量级状态机的全功能形态。Boost 的官方状态机库支持状态层次、历史伪状态、延迟事件、异步运行------UML 状态图能表达的它基本都能:

cpp 复制代码
// 说明性片段(需包含 Boost/statechart,
// 节选其教程骨架)
class Active;
class StopWatch
    : public sc::state_machine<
          StopWatch, Active> {};

class Stopped;
class Active
    : public sc::simple_state<
          Active, StopWatch, Stopped> {
 public:
  typedef sc::transition<
      EvReset, Active> reactions;
};

// 迁移即类型:reaction 列表声明出边

点评:功能与复杂度等重------学习曲线陡、编译慢,新项目多转向 SML 这类表达式式轻量库(transition 写成一行代码)。它像状态机世界的「全功能相机」:专业场合无可替代,日常记录用手机(表驱动/variant)就好。

轻量路线:variant + 表------现代 C++ 的无名状态机 。第 5 节的两条路线本身就是「无名却无处不在」的基础设施:协议解析的 enum 分支、variant 里的会话状态、unordered_map 的迁移规则------大量生产状态机从未引入任何模式名,却在做完全相同的事:把「状态 × 行为」按列拆开、给迁移一个明确住址。模式的完成态,是被吸收进日常语法。

本篇小结

行为随状态整体切换的领域,别让每个方法各扛一棵 switch:把「每种状态下的全部行为」收进一个类,上下文持共享实例逐操作委托------switch 消失、迁移出边写回状态自己家、对象「看起来换了类」。状态类零数据、全共享,是安全并发的底线;规则规整时退到迁移表,需要状态携带数据时换 variant 值语义------工具三档,按复杂度取。与策略的辨析刻进肌肉:算法插头 vs 对象生涯,外部注入 vs 自己迁移 。下一篇策略模式,把主导权从对象手里交还给外部------并且要回第 1 篇的老朋友 calcPrice,兑现那一篇埋下的承诺。

本文模式定义与角色划分参考了 Refactoring Guru《设计模式》中文版「状态」一章,意图译文与「谁拥有迁移」的讨论参考了 GoF《Design Patterns》第 5 章 State 一节。

相关推荐
C++ 老炮儿的技术栈1 小时前
MFC CPtrArray的用法
开发语言·数据结构·c++·算法·mfc·c
佳児素花痴╮1 小时前
C++基础速通
开发语言·c++
程序喵大人2 小时前
【C++入门】值类别与表达式 - 03 引用绑定:为什么有些参数能接住临时对象
开发语言·c++·引用绑定
handler013 小时前
【Linux】信号:内核的“敲门声”
linux·运维·服务器·c++·c·信号·signal
rolt3 小时前
用UML表示的行业标准02汽车-AUTOSAR
软件工程·ddd·uml·领域驱动设计·ontology·本体
欧特克_Glodon3 小时前
OpenCV计算机视觉开发入门与实践<三十五>:开发视频播放器
c++·opencv·计算机视觉·音视频
geovindu4 小时前
java:Observer Pattern
java·开发语言·后端·观察者模式·设计模式·行为模式
hansang_IR4 小时前
【题解】P9753 [CSP-S 2023] 消消乐
数据结构·c++·算法
竞赛考级题库4 小时前
202606 青少年等级考试C/C++真题一级 建议答题时长:60min
java·c语言·c++