项目结构:

rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Component_Code.rs
//! 物料编码值对象
//!
//! 职责单一:规范化承载物料/组件编码,并提供编码相关的查询语义。
/// 物料编码(构造时自动规范化:去除首尾空白并转为大写)
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ComponentCode {
/// 规范化之后的编码文本
value: String,
}
impl ComponentCode {
/// 构造一个物料编码
///
/// # 参数
/// - `raw`:原始编码文本(大小写与空白均会被规范化)
pub fn new(raw: &str) -> Self {
Self {
// 去除首尾空白并统一转为大写,保证编码比较的稳定性
value: raw.trim().to_uppercase(),
}
}
/// 以字符串切片形式返回编码内容
pub fn as_str(&self) -> &str {
&self.value
}
/// 判断是否与给定编码完全相同(忽略大小写与首尾空白)
///
/// # 参数
/// - `other`:待比较的编码文本
pub fn equals(&self, other: &str) -> bool {
self.value.eq_ignore_ascii_case(other.trim())
}
/// 判断编码中是否包含给定关键字(忽略大小写)
///
/// # 参数
/// - `keyword`:搜索关键字
pub fn contains(&self, keyword: &str) -> bool {
// 双方统一小写后做子串匹配
self.value
.to_lowercase()
.contains(&keyword.trim().to_lowercase())
}
/// 返回编码的分类前缀
///
/// 约定形如 `"MAT-GOLD-001"` 的编码以第一个 `-` 之前的片段作为分类,
/// 若不含分隔符则整段即分类。
pub fn prefix(&self) -> &str {
match self.value.split_once('-') {
// 取第一个分隔符之前的部分
Some((head, _)) => head,
// 无分隔符时整段即为分类
None => &self.value,
}
}
/// 判断编码是否为空字符串
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
}
/// 支持 `to_string()` 与 `format!("{}", ...)` 输出编码原文
impl std::fmt::Display for ComponentCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.value)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Money.rs
//! 货币金额值对象
//!
//! 职责单一:以「分」为最小单位精确表示金额。
//!
//! 【为什么不用 f64】
//! 浮点数无法精确表示 0.1 元这类十进制小数,多层级递归求和后会累积明显误差。
//! 组合模式的核心操作恰恰是"沿树逐层累加",因此必须使用整数分作为存储单位。
use std::iter::Sum;
use std::ops::Add;
/// 货币金额(内部以「分」为单位存储)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Money {
/// 金额数值,单位为「分」
cents: i64,
}
impl Money {
/// 金额零值常量
pub const ZERO: Money = Money { cents: 0 };
/// 以「分」为单位构造金额
///
/// # 参数
/// - `cents`:以分为单位的整数金额
pub fn from_cents(cents: i64) -> Self {
// 直接以整数分初始化
Self { cents }
}
/// 以「元」为单位构造金额(四舍五入到分)
///
/// # 参数
/// - `yuan`:以元为单位的浮点金额
pub fn from_yuan(yuan: f64) -> Self {
// 先放大 100 倍,再四舍五入,最后转为整数分
Self {
cents: (yuan * 100.0).round() as i64,
}
}
/// 返回以「分」表示的金额
pub fn cents(&self) -> i64 {
self.cents
}
/// 返回以「元」表示的金额(浮点近似值,仅用于展示与统计)
pub fn yuan(&self) -> f64 {
self.cents as f64 / 100.0
}
/// 判断是否为零金额
pub fn is_zero(&self) -> bool {
self.cents == 0
}
/// 按整数倍数放大金额
///
/// # 参数
/// - `factor`:放大倍数(例如件数、次数)
pub fn multiply(&self, factor: i64) -> Self {
// 整数乘法,结果精确无误差
Self {
cents: self.cents * factor,
}
}
/// 计算当前金额占总额的比例(0.0 ~ 1.0)
///
/// # 参数
/// - `total`:用于比较的总额
///
/// # 返回
/// 占比;当总额为 0 时返回 0.0,避免除零
pub fn ratio_to(&self, total: Money) -> f64 {
if total.cents == 0 {
// 总额为零的兜底处理,避免产生 NaN
return 0.0;
}
self.cents as f64 / total.cents as f64
}
}
/// 支持 `+` 运算,便于沿树累加
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money {
cents: self.cents + other.cents,
}
}
}
/// 支持迭代器 `sum()`,使组合体的聚合表达为一行
impl Sum for Money {
fn sum<I: Iterator<Item = Money>>(iter: I) -> Money {
// 以零值为初始值逐项累加
iter.fold(Money::ZERO, |accumulated, item| accumulated + item)
}
}
/// 支持 `to_string()` 与 `format!("{}", ...)` 输出人民币格式
impl std::fmt::Display for Money {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// 整数部分与小数部分分别取绝对值输出,正确处理负数金额
write!(
formatter,
"¥{}.{:02}",
self.cents / 100,
(self.cents % 100).abs()
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Weight.rs
//! 重量值对象
//!
//! 职责单一:以「毫克」为最小单位精确表示贵金属/宝石重量。
//!
//! 与金额同理,重量也会沿产品结构树逐层累加,
//! 因此同样采用整数存储以避免浮点误差。
use crate::domain::Money;
use std::iter::Sum;
use std::ops::Add;
/// 重量(内部以「毫克」为单位存储)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Weight {
/// 重量数值,单位为「毫克」
milligrams: i64,
}
impl Weight {
/// 重量零值常量
pub const ZERO: Weight = Weight { milligrams: 0 };
/// 以「毫克」为单位构造重量
///
/// # 参数
/// - `milligrams`:以毫克为单位的整数重量
pub fn from_milligrams(milligrams: i64) -> Self {
// 直接以整数毫克初始化
Self { milligrams }
}
/// 以「克」为单位构造重量(四舍五入到毫克)
///
/// # 参数
/// - `grams`:以克为单位的浮点重量
pub fn from_grams(grams: f64) -> Self {
// 先放大 1000 倍,再四舍五入,最后转为整数毫克
Self {
milligrams: (grams * 1000.0).round() as i64,
}
}
/// 返回以「毫克」表示的重量
pub fn milligrams(&self) -> i64 {
self.milligrams
}
/// 返回以「克」表示的重量(浮点近似值,仅用于展示)
pub fn grams(&self) -> f64 {
self.milligrams as f64 / 1000.0
}
/// 判断是否为零重量
///
/// 服务类组件(如电镀、镶嵌工时)重量恒为零,可借此识别。
pub fn is_zero(&self) -> bool {
self.milligrams == 0
}
/// 按整数倍数放大重量
///
/// # 参数
/// - `factor`:放大倍数(例如件数)
pub fn multiply(&self, factor: i64) -> Self {
// 整数乘法,结果精确无误差
Self {
milligrams: self.milligrams * factor,
}
}
/// 按「每克单价」计算对应金额
///
/// # 参数
/// - `price_per_gram`:每克单价
///
/// # 返回
/// 该重量对应的金额(四舍五入到分)
pub fn cost_at(&self, price_per_gram: Money) -> Money {
// 中间结果使用 i128,避免"毫克 × 分"在高价材料场景下溢出 i64
let numerator: i128 = self.milligrams as i128 * price_per_gram.cents() as i128;
// 加 500 后再整除 1000,实现四舍五入到分
let rounded_cents: i128 = (numerator + 500) / 1000;
Money::from_cents(rounded_cents as i64)
}
}
/// 支持 `+` 运算,便于沿树累加
impl Add for Weight {
type Output = Weight;
fn add(self, other: Weight) -> Weight {
Weight {
milligrams: self.milligrams + other.milligrams,
}
}
}
/// 支持迭代器 `sum()`,使组合体的聚合表达为一行
impl Sum for Weight {
fn sum<I: Iterator<Item = Weight>>(iter: I) -> Weight {
// 以零值为初始值逐项累加
iter.fold(Weight::ZERO, |accumulated, item| accumulated + item)
}
}
/// 支持 `to_string()` 与 `format!("{}", ...)` 输出克重格式
impl std::fmt::Display for Weight {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// 固定保留三位小数,符合贵金属称重行业习惯
write!(formatter, "{:.3} g", self.grams())
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Component_Spec.rs
//! 组件规格描述
//!
//! 职责单一:以纯数据描述一棵"待构建"的组件树。
//!
//! 【与组件树的区别】
//! `ComponentSpec` 是**数据**(可序列化、可来自配置文件或数据库),
//! 组件树是**对象**(可执行递归聚合)。
//! 由 `ProductBuilder` 负责把前者转换为后者。
//!
//! 新增一种组件类型时,只需在枚举中追加一个变体,
//! 并在 `ProductBuilder` 中补一个分支即可。
/// 组件规格
#[derive(Debug, Clone)]
pub enum ComponentSpec {
/// 材料规格
Material {
/// 物料编码
code: String,
/// 物料名称
name: String,
/// 单件净重(克)
unit_weight_grams: f64,
/// 每克单价(元)
price_per_gram_yuan: f64,
/// 件数
quantity: u32,
},
/// 工艺服务规格
Service {
/// 服务编码
code: String,
/// 服务名称
name: String,
/// 单次服务费(元)
unit_price_yuan: f64,
/// 服务次数
times: u32,
},
/// 装配体规格(可递归嵌套子规格)
Assembly {
/// 装配体编码
code: String,
/// 装配体名称
name: String,
/// 子规格集合
children: Vec<ComponentSpec>,
},
}
impl ComponentSpec {
/// 构造一个材料规格
///
/// # 参数
/// - `code`:物料编码
/// - `name`:物料名称
/// - `unit_weight_grams`:单件净重(克)
/// - `price_per_gram_yuan`:每克单价(元)
/// - `quantity`:件数
pub fn material(
code: &str,
name: &str,
unit_weight_grams: f64,
price_per_gram_yuan: f64,
quantity: u32,
) -> Self {
ComponentSpec::Material {
code: code.to_string(),
name: name.to_string(),
unit_weight_grams,
price_per_gram_yuan,
quantity,
}
}
/// 构造一个工艺服务规格
///
/// # 参数
/// - `code`:服务编码
/// - `name`:服务名称
/// - `unit_price_yuan`:单次服务费(元)
/// - `times`:服务次数
pub fn service(code: &str, name: &str, unit_price_yuan: f64, times: u32) -> Self {
ComponentSpec::Service {
code: code.to_string(),
name: name.to_string(),
unit_price_yuan,
times,
}
}
/// 构造一个不含子规格的装配体规格
///
/// # 参数
/// - `code`:装配体编码
/// - `name`:装配体名称
pub fn assembly(code: &str, name: &str) -> Self {
ComponentSpec::Assembly {
code: code.to_string(),
name: name.to_string(),
// 初始为空,随后用 with_children 装配
children: Vec::new(),
}
}
/// 以链式风格装配子规格
///
/// # 参数
/// - `children`:子规格集合
///
/// # 说明
/// 仅对 [`ComponentSpec::Assembly`] 生效;
/// 对材料或服务规格调用时子规格会被忽略(详见 [`Self::push_child`] 的返回值判断)。
pub fn with_children(mut self, children: Vec<ComponentSpec>) -> Self {
self.set_children(children);
self
}
/// 追加一个子规格
///
/// # 参数
/// - `child`:子规格
///
/// # 返回
/// 成功追加返回 `true`;当前规格不是装配体时返回 `false` 且不做任何修改
/// (显式返回结果,避免"静默丢弃"造成的隐蔽错误)
pub fn push_child(&mut self, child: ComponentSpec) -> bool {
match self {
ComponentSpec::Assembly { children, .. } => {
children.push(child);
true
}
// 非装配体无法容纳子规格
_ => false,
}
}
/// 覆盖式设置子规格集合
///
/// # 参数
/// - `children`:新的子规格集合
///
/// # 返回
/// 当前规格不是装配体时静默忽略
pub fn set_children(&mut self, children: Vec<ComponentSpec>) {
if let ComponentSpec::Assembly {
children: slot, ..
} = self
{
*slot = children;
}
}
/// 返回本规格的编码
pub fn code(&self) -> &str {
match self {
ComponentSpec::Material { code, .. } => code,
ComponentSpec::Service { code, .. } => code,
ComponentSpec::Assembly { code, .. } => code,
}
}
/// 返回本规格的显示名称
pub fn display_name(&self) -> &str {
match self {
ComponentSpec::Material { name, .. } => name,
ComponentSpec::Service { name, .. } => name,
ComponentSpec::Assembly { name, .. } => name,
}
}
/// 返回本规格的种类名称
pub fn kind_name(&self) -> &'static str {
match self {
ComponentSpec::Material { .. } => "Material",
ComponentSpec::Service { .. } => "Service",
ComponentSpec::Assembly { .. } => "Assembly",
}
}
/// 返回子规格切片(非装配体返回空切片)
pub fn child_specs(&self) -> &[ComponentSpec] {
match self {
ComponentSpec::Assembly { children, .. } => children,
// 材料与服务规格没有子规格
_ => &[],
}
}
/// 返回直接子规格数量
pub fn child_count(&self) -> usize {
self.child_specs().len()
}
/// 判断本规格是否为装配体
pub fn is_assembly(&self) -> bool {
matches!(self, ComponentSpec::Assembly { .. })
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : product_builder.rs
//! 组件构建器
//!
//! 职责单一:把 [`ComponentSpec`] 规格树递归转换为真实的组件树。
//!
//! 【本层是唯一的"类型登记点"】
//! 新增一种组件类型时,需要修改的地方只有两处:
//! 1. `ComponentSpec` 追加一个变体;
//! 2. 本文件 `build` 方法补一个 `match` 分支。
//!
//! 组件层、叶子层、组合层与分析层**均无需改动**,
//! 这正是把"构建知识"集中到单一工厂的价值。
use crate::component::{ComponentContainer, ProductComponent};
use crate::composite::ProductAssembly;
use crate::factory::ComponentSpec;
use crate::leaf::{MaterialItem, ServiceItem};
use std::collections::HashSet;
/// 构建期错误
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildError {
/// 编码为空
EmptyCode {
/// 出错的规格名称,便于定位
spec_name: String,
},
/// 编码重复
DuplicateCode {
/// 重复出现的编码
code: String,
},
}
/// 支持 `to_string()` 输出可读错误信息
impl std::fmt::Display for BuildError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::EmptyCode { spec_name } => {
write!(formatter, "规格「{}」的编码为空,无法构建组件", spec_name)
}
BuildError::DuplicateCode { code } => {
write!(formatter, "编码「{}」在规格树中重复出现", code)
}
}
}
}
/// 使错误类型可用于 `?` 与错误链
impl std::error::Error for BuildError {}
/// 组件构建器
pub struct ProductBuilder;
impl ProductBuilder {
/// 依据规格**递归**构建组件树(不做校验)
///
/// # 参数
/// - `spec`:待构建的规格节点
///
/// # 返回
/// 装箱后的组件树根节点
pub fn build(spec: &ComponentSpec) -> Box<dyn ProductComponent> {
match spec {
// 材料规格 -> 材料单件叶子
ComponentSpec::Material {
code,
name,
unit_weight_grams,
price_per_gram_yuan,
quantity,
} => Box::new(MaterialItem::new(
code,
name,
*unit_weight_grams,
*price_per_gram_yuan,
*quantity,
)),
// 服务规格 -> 服务项叶子
ComponentSpec::Service {
code,
name,
unit_price_yuan,
times,
} => Box::new(ServiceItem::new(code, name, *unit_price_yuan, *times)),
// 装配体规格 -> 组合体,并递归装配每个子规格
ComponentSpec::Assembly {
code,
name,
children,
} => {
// 先创建空装配体
let mut assembly: ProductAssembly = ProductAssembly::new(code, name);
for child_spec in children {
// 递归构建子组件并挂载
assembly.add_child(Self::build(child_spec));
}
Box::new(assembly)
}
}
}
/// 依据规格递归构建组件树,并校验整棵规格树的编码合法
///
/// # 校验规则
/// 1. 任何节点的编码不得为空;
/// 2. 全树编码必须唯一(BOM 的基本业务约束)。
///
/// # 参数
/// - `spec`:待构建的规格节点
///
/// # 返回
/// 构建成功的组件树;校验失败时返回 [`BuildError`]
pub fn build_checked(spec: &ComponentSpec) -> Result<Box<dyn ProductComponent>, BuildError> {
// 已出现过的规范化编码集合
let mut seen_codes: HashSet<String> = HashSet::new();
// 先递归校验,再真正构建
Self::validate(spec, &mut seen_codes)?;
Ok(Self::build(spec))
}
/// 递归校验规格树
///
/// # 参数
/// - `spec`:当前待校验的规格节点
/// - `seen_codes`:跨层级共享的已见编码集合
fn validate(
spec: &ComponentSpec,
seen_codes: &mut HashSet<String>,
) -> Result<(), BuildError> {
// 规范化编码:去空白 + 转大写,与 ComponentCode 的规则保持一致
let normalized_code: String = spec.code().trim().to_uppercase();
// 规则一:编码非空
if normalized_code.is_empty() {
return Err(BuildError::EmptyCode {
spec_name: spec.display_name().to_string(),
});
}
// 规则二:编码全局唯一;insert 返回 false 说明此前已存在
if !seen_codes.insert(normalized_code.clone()) {
return Err(BuildError::DuplicateCode {
code: normalized_code,
});
}
// 递归校验全部子规格
for child_spec in spec.child_specs() {
Self::validate(child_spec, seen_codes)?;
}
Ok(())
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Material_Item.rs
//! 叶子实现 A:材料单件(MaterialItem)
//!
//! 职责单一:描述一件按「重量 × 每克单价」计价的实物料,
//! 例如 18K 金料、钻石、宝石、配链。
//!
//! 本类型**没有子节点**,因此所有聚合量都由自身字段直接算出。
use crate::component::{ProductComponent, NO_CHILDREN};
use crate::domain::{ComponentCode, Money, Weight};
/// 材料单件(Leaf)
#[derive(Debug)]
pub struct MaterialItem {
/// 物料编码
code: ComponentCode,
/// 物料名称
name: String,
/// 单件净重(克)
unit_weight: Weight,
/// 每克单价
price_per_gram: Money,
/// 件数
quantity: u32,
}
impl MaterialItem {
/// 构造一件材料单件
///
/// # 参数
/// - `code`:物料编码原文
/// - `name`:物料名称
/// - `unit_weight_grams`:单件净重(克)
/// - `price_per_gram_yuan`:每克单价(元)
/// - `quantity`:件数
pub fn new(
code: &str,
name: &str,
unit_weight_grams: f64,
price_per_gram_yuan: f64,
quantity: u32,
) -> Self {
Self {
// 编码由值对象负责规范化
code: ComponentCode::new(code),
// 名称转为拥有所有权的字符串
name: name.to_string(),
// 克重转换为精确的毫克存储
unit_weight: Weight::from_grams(unit_weight_grams),
// 单价转换为精确的整数分存储
price_per_gram: Money::from_yuan(price_per_gram_yuan),
// 件数原样保存
quantity,
}
}
/// 返回单件净重
pub fn unit_weight(&self) -> Weight {
self.unit_weight
}
/// 返回每克单价
pub fn price_per_gram(&self) -> Money {
self.price_per_gram
}
/// 返回件数
pub fn quantity(&self) -> u32 {
self.quantity
}
/// 计算单件成本(单件净重 × 每克单价,四舍五入到分)
pub fn unit_cost(&self) -> Money {
// 先按克重算出单件金额,再乘以件数由调用方决定
self.unit_weight.cost_at(self.price_per_gram)
}
}
impl ProductComponent for MaterialItem {
/// 返回物料编码
fn code(&self) -> &ComponentCode {
&self.code
}
/// 返回物料名称
fn name(&self) -> &str {
&self.name
}
/// 返回组件种类名称
fn kind_name(&self) -> &'static str {
"Material"
}
/// 叶子没有子节点,直接复用静态空切片
fn children(&self) -> &[Box<dyn ProductComponent>] {
NO_CHILDREN
}
/// 材料单件不是组合体
fn is_composite(&self) -> bool {
false
}
/// 累计重量 = 单件净重 × 件数
fn total_weight(&self) -> Weight {
self.unit_weight.multiply(self.quantity as i64)
}
/// 累计成本 = 单件成本 × 件数
///
/// 先算单件金额再乘件数,保证"单价 × 件数"的行业计价习惯。
fn total_cost(&self) -> Money {
self.unit_cost().multiply(self.quantity as i64)
}
/// 计件数 = 件数
fn piece_count(&self) -> u32 {
self.quantity
}
/// 一行属性描述
fn describe(&self) -> String {
format!(
"材料单件 | 编码 {} | 名称 {} | 单件净重 {} | 每克单价 {} | 件数 {} | 合计净重 {} | 合计成本 {}",
self.code,
self.name,
self.unit_weight,
self.price_per_gram,
self.quantity,
self.total_weight(),
self.total_cost()
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Service_Item.rs
//! 叶子实现 B:服务项(ServiceItem)
//!
//! 职责单一:描述一项按「次数 × 单次服务费」计价的工艺服务,
//! 例如电镀、镶嵌工时、抛光、激光刻字。
//!
//! 【设计意图】
//! 本类型刻意与 `MaterialItem` 在语义上不同:
//! **它没有重量**(`total_weight()` 恒为零),只有服务费。
//! 组合体却可以用完全相同的方式对待它们 ------
//! 这正是组合模式"统一对待叶子与组合"的价值体现。
use crate::component::{ProductComponent, NO_CHILDREN};
use crate::domain::{ComponentCode, Money, Weight};
/// 服务项(Leaf)
#[derive(Debug)]
pub struct ServiceItem {
/// 服务编码
code: ComponentCode,
/// 服务名称
name: String,
/// 单次服务费
unit_price: Money,
/// 服务次数
times: u32,
}
impl ServiceItem {
/// 构造一个服务项
///
/// # 参数
/// - `code`:服务编码原文
/// - `name`:服务名称
/// - `unit_price_yuan`:单次服务费(元)
/// - `times`:服务次数
pub fn new(code: &str, name: &str, unit_price_yuan: f64, times: u32) -> Self {
Self {
// 编码由值对象负责规范化
code: ComponentCode::new(code),
// 名称转为拥有所有权的字符串
name: name.to_string(),
// 单价转换为精确的整数分存储
unit_price: Money::from_yuan(unit_price_yuan),
// 次数原样保存
times,
}
}
/// 返回单次服务费
pub fn unit_price(&self) -> Money {
self.unit_price
}
/// 返回服务次数
pub fn times(&self) -> u32 {
self.times
}
}
impl ProductComponent for ServiceItem {
/// 返回服务编码
fn code(&self) -> &ComponentCode {
&self.code
}
/// 返回服务名称
fn name(&self) -> &str {
&self.name
}
/// 返回组件种类名称
fn kind_name(&self) -> &'static str {
"Service"
}
/// 叶子没有子节点,直接复用静态空切片
fn children(&self) -> &[Box<dyn ProductComponent>] {
NO_CHILDREN
}
/// 服务项不是组合体
fn is_composite(&self) -> bool {
false
}
/// 服务项不计重量,恒为零
fn total_weight(&self) -> Weight {
Weight::ZERO
}
/// 累计成本 = 单次服务费 × 服务次数
fn total_cost(&self) -> Money {
self.unit_price.multiply(self.times as i64)
}
/// 计件数 = 服务次数
fn piece_count(&self) -> u32 {
self.times
}
/// 一行属性描述
fn describe(&self) -> String {
format!(
"工艺服务 | 编码 {} | 名称 {} | 单次服务费 {} | 次数 {} | 合计成本 {} | 不计重量",
self.code, self.name, self.unit_price, self.times, self.total_cost()
)
}
}
rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : aggregate_analysis.rs
//! 聚合分析
//!
//! 职责单一:对组件树做分组统计与结构度量。
//!
//! 本模块完全建立在组件抽象之上,不依赖任何具体叶子或组合类型,
//! 因此**未来新增任何叶子类型都会被自动纳入统计**,无需修改本文件。
use crate::component::{collect_composites, collect_leaves, ProductComponent};
use crate::domain::{Money, Weight};
use std::collections::BTreeMap;
/// 成本构成条目(按编码前缀分类)
#[derive(Debug, Clone)]
pub struct CostCompositionEntry {
/// 分类键(物料编码的第一个 `-` 之前片段,如 "MAT")
pub category: String,
/// 分类的中文显示名
pub category_label: String,
/// 该分类的累计成本
pub cost: Money,
/// 该分类的累计净重
pub weight: Weight,
/// 该分类下的叶子节点数量
pub leaf_count: usize,
/// 该分类的累计件数
pub pieces: u32,
/// 该分类成本占总额的百分比(0.0 ~ 100.0)
pub share_percent: f64,
}
/// 结构统计结果
#[derive(Debug, Clone)]
pub struct StructureStatistics {
/// 节点总数(含组合体)
pub node_count: usize,
/// 叶子节点数量
pub leaf_count: usize,
/// 组合体节点数量
pub composite_count: usize,
/// 树的最大深度(根为 0)
pub max_depth: usize,
/// 合计净重
pub total_weight: Weight,
/// 合计成本
pub total_cost: Money,
/// 合计件数
pub total_pieces: u32,
}
/// 聚合分析器
pub struct AggregateAnalysis;
impl AggregateAnalysis {
/// 计算整棵树的结构统计指标
///
/// # 参数
/// - `root`:树的根节点
pub fn structure_statistics(root: &dyn ProductComponent) -> StructureStatistics {
StructureStatistics {
// 节点总数由组件层默认实现递归得出
node_count: root.node_count(),
// 叶子数量由组件层默认实现递归得出
leaf_count: root.leaf_count(),
// 组合体数量借助遍历工具统计
composite_count: collect_composites(root).len(),
// 最大深度由组件层默认实现递归得出
max_depth: root.max_depth(),
// 聚合量直接取自根节点的递归求和结果
total_weight: root.total_weight(),
total_cost: root.total_cost(),
total_pieces: root.piece_count(),
}
}
/// 按"叶子节点编码前缀"分组,统计成本构成
///
/// # 参数
/// - `root`:树的根节点
///
/// # 返回
/// 按成本从高到低排序的分类条目
pub fn cost_composition(root: &dyn ProductComponent) -> Vec<CostCompositionEntry> {
// 取出全部叶子节点(仅叶子承载实际计价值)
let leaves = collect_leaves(root);
// 总成本用于计算占比
let total_cost: Money = root.total_cost();
// 使用 BTreeMap 保证分类顺序稳定可复现(按分类键排序)
let mut buckets: BTreeMap<String, CostCompositionEntry> = BTreeMap::new();
for visit in &leaves {
// 分类键取自编码前缀
let category: String = visit.node.code().prefix().to_string();
// 取出或初始化该分类的累计条目
let entry: &mut CostCompositionEntry =
buckets
.entry(category.clone())
.or_insert_with(|| CostCompositionEntry {
category: category.clone(),
category_label: category_label(&category),
cost: Money::ZERO,
weight: Weight::ZERO,
leaf_count: 0,
pieces: 0,
share_percent: 0.0,
});
// 逐项累加成本、重量、叶子数与件数
entry.cost = entry.cost + visit.node.total_cost();
entry.weight = entry.weight + visit.node.total_weight();
entry.leaf_count += 1;
entry.pieces += visit.node.piece_count();
}
// 转为向量并回填占比
let mut entries: Vec<CostCompositionEntry> = buckets.into_values().collect();
for entry in entries.iter_mut() {
// 占比以百分比表示
entry.share_percent = entry.cost.ratio_to(total_cost) * 100.0;
}
// 按成本降序排列,便于直接展示主要成本来源
entries.sort_by(|left, right| right.cost.cmp(&left.cost));
entries
}
/// 计算平均单件成本
///
/// # 参数
/// - `root`:树的根节点
///
/// # 返回
/// 平均单件成本;当总件数为 0 时返回零金额
pub fn average_piece_cost(root: &dyn ProductComponent) -> Money {
// 总件数
let pieces: u32 = root.piece_count();
if pieces == 0 {
// 除零保护
return Money::ZERO;
}
// 整数分除以件数后截断,保持金额精度可控
Money::from_cents(root.total_cost().cents() / pieces as i64)
}
/// 计算材料成本与工艺服务成本的比值
///
/// # 参数
/// - `root`:树的根节点
///
/// # 返回
/// `(材料成本, 工艺服务成本)`
pub fn material_versus_service_cost(root: &dyn ProductComponent) -> (Money, Money) {
let mut material_cost: Money = Money::ZERO;
let mut service_cost: Money = Money::ZERO;
// 遍历全部叶子,按种类归类
for visit in collect_leaves(root) {
match visit.node.kind_name() {
// 材料类
"Material" => material_cost = material_cost + visit.node.total_cost(),
// 服务类
"Service" => service_cost = service_cost + visit.node.total_cost(),
// 其它自定义叶子类型统一归入材料侧,保证兼容未来扩展
_ => material_cost = material_cost + visit.node.total_cost(),
}
}
(material_cost, service_cost)
}
}
/// 把编码前缀翻译为中文分类名
///
/// # 参数
/// - `category`:编码前缀
///
/// # 说明
/// 未登记的前缀统一显示为 `其它(前缀)`,
/// 因此新增前缀分类**不需要修改本函数的调用方**。
fn category_label(category: &str) -> String {
match category {
// 材料类
"MAT" => String::from("材料"),
// 工艺服务类
"SVC" => String::from("工艺服务"),
// 装配体类
"ASM" => String::from("装配体"),
// 包装类
"PKG" => String::from("包装"),
// 未登记前缀的兜底展示
other => format!("其它({})", other),
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : search_analysis.rs
//! 查找分析
//!
//! 职责单一:在组件树中按编码或名称定位节点。
//!
//! 【为什么查找不放在 Component 接口里】
//! 若把 `find_by_code` 写成 trait 默认方法,实现体内需要 `Some(self)`,
//! 即把 `&Self` 转换为 `&dyn ProductComponent`。
//! 但 trait 默认方法中的 `Self` 是 `?Sized` 的,无法完成这一强制转换
//! (只有 `Self: Sized` 才允许 unsize coercion)。
//! 因此查找逻辑本就应当外置 ------ 借助通用遍历器实现,既绕开了语言限制,
//! 又让"新增一种查找策略"不再触碰核心接口。
use crate::component::{depth_first, ProductComponent};
/// 查找命中项
#[derive(Debug, Clone)]
pub struct SearchHit<'a> {
/// 命中的节点引用
pub node: &'a dyn ProductComponent,
/// 该节点在树中的深度(根为 0)
pub depth: usize,
/// 从根到该节点的名称路径(含首尾两端)
pub path: Vec<String>,
}
impl<'a> SearchHit<'a> {
/// 把路径渲染为 `A / B / C` 形式
pub fn path_text(&self) -> String {
self.path.join(" / ")
}
/// 返回命中节点的编码
pub fn code_text(&self) -> String {
self.node.code().to_string()
}
}
/// 查找分析器
pub struct SearchAnalysis;
impl SearchAnalysis {
/// 按编码精确查找,返回**前序遍历顺序**下的首个命中
///
/// # 参数
/// - `root`:树的根节点
/// - `code`:目标编码(忽略大小写与首尾空白)
pub fn find_by_code<'a>(root: &'a dyn ProductComponent, code: &str) -> Option<SearchHit<'a>> {
depth_first(root)
.find(|visit| visit.node.code().equals(code))
.map(|visit| SearchHit {
node: visit.node,
depth: visit.depth,
path: visit.path,
})
}
/// 按编码查找并仅返回名称路径
///
/// # 参数
/// - `root`:树的根节点
/// - `code`:目标编码
pub fn find_path<'a>(root: &'a dyn ProductComponent, code: &str) -> Option<Vec<String>> {
// 复用精确查找,取路径字段
Self::find_by_code(root, code).map(|hit| hit.path)
}
/// 按关键字模糊查找全部命中(同时匹配名称与编码,忽略大小写)
///
/// # 参数
/// - `root`:树的根节点
/// - `keyword`:搜索关键字
pub fn find_all_by_keyword<'a>(
root: &'a dyn ProductComponent,
keyword: &str,
) -> Vec<SearchHit<'a>> {
// 统一去除首尾空白,避免空白关键字匹配到全部节点
let trimmed_keyword: &str = keyword.trim();
if trimmed_keyword.is_empty() {
// 空关键字直接返回空结果
return Vec::new();
}
depth_first(root)
.filter(|visit| {
// 名称命中或编码命中均视为匹配
visit.node.name().contains(trimmed_keyword)
|| visit.node.code().contains(trimmed_keyword)
})
.map(|visit| SearchHit {
node: visit.node,
depth: visit.depth,
path: visit.path,
})
.collect()
}
/// 按组件种类名称查找全部命中
///
/// # 参数
/// - `root`:树的根节点
/// - `kind_name`:组件种类名称(如 "Material" / "Service" / "Assembly")
pub fn find_all_by_kind<'a>(
root: &'a dyn ProductComponent,
kind_name: &str,
) -> Vec<SearchHit<'a>> {
depth_first(root)
.filter(|visit| visit.node.kind_name() == kind_name)
.map(|visit| SearchHit {
node: visit.node,
depth: visit.depth,
path: visit.path,
})
.collect()
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : tree_analysis.rs
//! 树形渲染分析
//!
//! 职责单一:把组件树渲染为带连接线的可读文本。
//!
//! 本分析需要"当前节点是否为其父的最后一个子节点"这一信息,
//! 而通用遍历器只提供深度与路径,因此这里采用自有的递归渲染算法。
use crate::component::ProductComponent;
/// 树形渲染分析器
pub struct TreeAnalysis;
impl TreeAnalysis {
/// 渲染纯结构树
///
/// 每个节点仅展示 `名称 [种类] 编码`,适合快速查看层级。
///
/// # 参数
/// - `root`:树的根节点
pub fn render_structure(root: &dyn ProductComponent) -> String {
// 复用内部渲染函数,关闭指标展示
let mut buffer: String = String::new();
render_node(root, "", true, true, &mut buffer, false);
buffer
}
/// 渲染带聚合指标的树
///
/// 每个节点展示编码、名称、种类、累计重量、累计成本与累计件数。
///
/// # 参数
/// - `root`:树的根节点
pub fn render_with_metrics(root: &dyn ProductComponent) -> String {
// 复用内部渲染函数,开启指标展示
let mut buffer: String = String::new();
render_node(root, "", true, true, &mut buffer, true);
buffer
}
/// 渲染指定深度的子树(裁剪过深的层级)
///
/// # 参数
/// - `root`:树的根节点
/// - `max_depth`:允许展示的最大深度(根为 0)
pub fn render_truncated(root: &dyn ProductComponent, max_depth: usize) -> String {
let mut buffer: String = String::new();
render_node_truncated(root, "", true, true, &mut buffer, max_depth);
buffer
}
}
/// 内部递归渲染函数
///
/// # 参数
/// - `node`:当前节点
/// - `prefix`:当前节点左侧已累积的前缀(由祖先的"是否为末子"决定)
/// - `is_last`:当前节点是否为其父的最后一个子节点
/// - `is_root`:当前节点是否为整棵树的根
/// - `buffer`:输出缓冲区
/// - `with_metrics`:是否展示聚合指标
fn render_node(
node: &dyn ProductComponent,
prefix: &str,
is_last: bool,
is_root: bool,
buffer: &mut String,
with_metrics: bool,
) {
// 节点自身的展示文本
let label: String = if with_metrics {
node.summary()
} else {
format!("{} [{}] {}", node.name(), node.kind_name(), node.code())
};
if is_root {
// 根节点不加连接线
buffer.push_str(&format!("{}\n", label));
} else {
// 末子使用 └──,非末子使用 ├──
let connector: &str = if is_last { "└── " } else { "├── " };
buffer.push_str(&format!("{}{}{}\n", prefix, connector, label));
}
// 取出直接子节点
let children = node.children();
let child_total: usize = children.len();
for (index, child) in children.iter().enumerate() {
// 该子节点是否为其父的末子
let child_is_last: bool = index + 1 == child_total;
// 子节点的前缀 = 当前前缀 + (当前为末子 ? 四个空格 : 竖线加空格)
let child_prefix: String = if is_root {
// 根节点的子节点前缀为空
String::new()
} else {
format!("{}{}", prefix, if is_last { " " } else { "│ " })
};
render_node(
&**child,
&child_prefix,
child_is_last,
false,
buffer,
with_metrics,
);
}
}
/// 内部递归渲染函数(带深度裁剪)
///
/// # 参数
/// - `node`:当前节点
/// - `prefix`:当前节点左侧前缀
/// - `is_last`:当前节点是否为其父的末子
/// - `is_root`:当前节点是否为整棵树的根
/// - `buffer`:输出缓冲区
/// - `remaining_depth`:剩余可展示的深度
fn render_node_truncated(
node: &dyn ProductComponent,
prefix: &str,
is_last: bool,
is_root: bool,
buffer: &mut String,
remaining_depth: usize,
) {
let label: String = format!("{} [{}] {}", node.name(), node.kind_name(), node.code());
if is_root {
buffer.push_str(&format!("{}\n", label));
} else {
let connector: &str = if is_last { "└── " } else { "├── " };
buffer.push_str(&format!("{}{}{}\n", prefix, connector, label));
}
// 达到深度上限时,仅提示剩余子节点数量
if remaining_depth == 0 {
let hidden: usize = node.children().len();
if hidden > 0 {
let child_prefix: String = if is_root {
String::new()
} else {
format!("{}{}", prefix, if is_last { " " } else { "│ " })
};
buffer.push_str(&format!("{}└── ...... 省略 {} 个子节点\n", child_prefix, hidden));
}
return;
}
let children = node.children();
let child_total: usize = children.len();
for (index, child) in children.iter().enumerate() {
let child_is_last: bool = index + 1 == child_total;
let child_prefix: String = if is_root {
String::new()
} else {
format!("{}{}", prefix, if is_last { " " } else { "│ " })
};
render_node_truncated(
&**child,
&child_prefix,
child_is_last,
false,
buffer,
remaining_depth - 1,
);
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : bom_report.rs
//! BOM 报表生成
//!
//! 职责单一:把聚合分析、树形渲染与查找分析的结果编排为可读报表。
//!
//! 本模块只消费 `&dyn ProductComponent`,
//! 因此无论传入的是叶子、组合体还是自定义组件,都能生成同样的报表。
use crate::analysis::{AggregateAnalysis, SearchAnalysis, StructureStatistics, TreeAnalysis};
use crate::component::ProductComponent;
use crate::domain::Money;
/// 报表中使用的横向分隔线
const SEPARATOR: &str = "--------------------------------------------------------------------------";
/// BOM 报表生成器
pub struct BomReport;
impl BomReport {
/// 生成完整的 BOM 报表文档
///
/// # 参数
/// - `root`:产品结构树的根节点
/// - `search_keywords`:需要附带定位的关键字列表
pub fn generate(root: &dyn ProductComponent, search_keywords: &[&str]) -> String {
let mut sections: Vec<String> = Vec::new();
// 标题区
sections.push(Self::document_header(root));
// 一、结构总览
sections.push(Self::structure_overview(root));
// 二、产品结构树
sections.push(Self::structure_tree(root));
// 三、成本构成分析
sections.push(Self::cost_composition_table(root));
// 四、材料与服务成本对比
sections.push(Self::material_versus_service(root));
// 五、关键节点定位
sections.push(Self::search_footprint(root, search_keywords));
// 各区之间以分隔线连接
sections.join(&format!("\n{}\n\n", SEPARATOR))
}
/// 生成文档头
///
/// # 参数
/// - `root`:产品结构树的根节点
pub fn document_header(root: &dyn ProductComponent) -> String {
format!(
"========================= 产品结构 BOM 报表 =========================\n根节点:{}({})\n编码:{}",
root.name(),
root.kind_name(),
root.code()
)
}
/// 生成「一、结构总览」区
///
/// # 参数
/// - `root`:产品结构树的根节点
pub fn structure_overview(root: &dyn ProductComponent) -> String {
// 取得全部结构指标
let statistics: StructureStatistics = AggregateAnalysis::structure_statistics(root);
// 平均单件成本
let average_cost: Money = AggregateAnalysis::average_piece_cost(root);
let mut lines: Vec<String> = Vec::new();
lines.push(String::from("【一、结构总览】"));
lines.push(format!(" 节点总数(含装配体):{}", statistics.node_count));
lines.push(format!(" 叶子节点数量:{}", statistics.leaf_count));
lines.push(format!(" 装配体数量:{}", statistics.composite_count));
lines.push(format!(" 最大层级深度:{}", statistics.max_depth));
lines.push(format!(" 合计净重:{}", statistics.total_weight));
lines.push(format!(" 合计成本:{}", statistics.total_cost));
lines.push(format!(" 合计件数:{}", statistics.total_pieces));
lines.push(format!(" 平均单件成本:{}", average_cost));
lines.join("\n")
}
/// 生成「二、产品结构树」区
///
/// # 参数
/// - `root`:产品结构树的根节点
pub fn structure_tree(root: &dyn ProductComponent) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push(String::from("【二、产品结构树】"));
// 逐行缩进展示随指标的结构树
for tree_line in TreeAnalysis::render_with_metrics(root).lines() {
lines.push(format!(" {}", tree_line));
}
lines.join("\n")
}
/// 生成「三、成本构成分析」区
///
/// # 参数
/// - `root`:产品结构树的根节点
pub fn cost_composition_table(root: &dyn ProductComponent) -> String {
let entries = AggregateAnalysis::cost_composition(root);
let mut lines: Vec<String> = Vec::new();
lines.push(String::from("【三、成本构成分析】"));
for entry in &entries {
lines.push(format!(
" ▸ {}(分类键 {})",
entry.category_label, entry.category
));
lines.push(format!(
" 成本 {} | 占总成本 {:.1}%",
entry.cost, entry.share_percent
));
lines.push(format!(" 净重 {} | 叶子数 {} | 件数 {}", entry.weight, entry.leaf_count, entry.pieces));
}
// 合计行
lines.push(String::from(" ── 合计"));
lines.push(format!(
" 成本 {} | 净重 {} | 合计件数 {}",
root.total_cost(),
root.total_weight(),
root.piece_count()
));
lines.join("\n")
}
/// 生成「四、材料与服务成本对比」区
///
/// # 参数
/// - `root`:产品结构树的根节点
pub fn material_versus_service(root: &dyn ProductComponent) -> String {
// 取得材料与服务两侧的成本
let (material_cost, service_cost) = AggregateAnalysis::material_versus_service_cost(root);
// 总成本用于计算占比
let total_cost: Money = root.total_cost();
let mut lines: Vec<String> = Vec::new();
lines.push(String::from("【四、材料与服务成本对比】"));
lines.push(format!(
" 材料成本合计:{}({:.1}%)",
material_cost,
material_cost.ratio_to(total_cost) * 100.0
));
lines.push(format!(
" 工艺服务成本合计:{}({:.1}%)",
service_cost,
service_cost.ratio_to(total_cost) * 100.0
));
lines.join("\n")
}
/// 生成「五、关键节点定位」区
///
/// # 参数
/// - `root`:产品结构树的根节点
/// - `keywords`:需要定位的关键字列表
pub fn search_footprint(root: &dyn ProductComponent, keywords: &[&str]) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push(String::from("【五、关键节点定位】"));
if keywords.is_empty() {
// 无关键字时给出明确提示,而非空段落
lines.push(String::from(" 未提供待查找关键字。"));
return lines.join("\n");
}
for keyword in keywords {
// 逐个关键字做模糊查找
let hits = SearchAnalysis::find_all_by_keyword(root, keyword);
lines.push(format!(" 关键字「{}」命中 {} 个节点", keyword, hits.len()));
for (index, hit) in hits.iter().enumerate() {
lines.push(format!(
" {}. [{}] {} | 深度 {} | 路径:{}",
index + 1,
hit.node.kind_name(),
hit.node.name(),
hit.depth,
hit.path_text()
));
}
}
lines.join("\n")
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : component_container.rs
//! 容器角色接口(Container Role)
//!
//! 职责单一:把"管理子节点"这一能力从 Component 主体接口中**隔离**出来。
//!
//! 【接口隔离原则】
//! 叶子类型(单件物料、服务项)根本不需要增删子节点的能力。
//! 若把 `add_child` 写在 `ProductComponent` 上,
//! 叶子就只能实现为空操作(或直接抛错),既污染接口又埋下运行期隐患。
//!
//! 因此本工程采用两个 trait:
//! - [`ProductComponent`]:所有节点都必须具备的"读"能力(统一对待);
//! - [`ComponentContainer`]:仅组合体具备的"写"能力(构建树时使用)。
//!
//! 泛型代码若需要装配子树,只需在约束中加上 `ComponentContainer`,
//! 而需要遍历与统计时只需 `ProductComponent`。
use crate::component::ProductComponent;
/// 子节点容器能力接口 ------ 仅由「组合体(Composite)」实现
pub trait ComponentContainer {
/// 以只读切片形式访问子节点
fn child_slice(&self) -> &[Box<dyn ProductComponent>];
/// 以可变引用形式访问子节点向量
///
/// 供构建器批量装配子树使用。
fn child_slice_mut(&mut self) -> &mut Vec<Box<dyn ProductComponent>>;
/// 追加一个子组件
///
/// # 参数
/// - `child`:子组件实例(装箱后的 trait 对象)
fn add_child(&mut self, child: Box<dyn ProductComponent>) {
// 默认实现直接复用可变切片访问器
self.child_slice_mut().push(child);
}
/// 批量追加多个子组件
///
/// # 参数
/// - `children`:子组件迭代器
fn extend_children<I>(&mut self, children: I)
where
I: IntoIterator<Item = Box<dyn ProductComponent>>,
{
// 一次性扩展,避免逐条 push 的重复容量检查
self.child_slice_mut().extend(children);
}
/// 按编码移除并返回子组件
///
/// 只移除**直接子节点**,不递归到更深层级。
///
/// # 参数
/// - `code`:目标子组件的物料编码
///
/// # 返回
/// 被移除的子组件;若未找到则返回 `None`
fn remove_child_by_code(&mut self, code: &str) -> Option<Box<dyn ProductComponent>> {
// 先定位目标下标,避免在查找过程中同时可变借用
let target_index: Option<usize> = self
.child_slice()
.iter()
.position(|child| child.code().equals(code));
match target_index {
// 找到则移除并返回
Some(index) => Some(self.child_slice_mut().remove(index)),
// 未找到则返回 None
None => None,
}
}
/// 返回直接子节点数量
fn child_count(&self) -> usize {
self.child_slice().len()
}
/// 判断当前容器是否为空
fn is_empty_container(&self) -> bool {
self.child_slice().is_empty()
}
/// 清空全部直接子节点
fn clear_children(&mut self) {
self.child_slice_mut().clear();
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : product_component.rs
//! 组件抽象接口(Component)
//!
//! ============================================================================
//! 组合模式的灵魂:统一的 Component 接口
//! ============================================================================
//!
//! 叶子(单件物料)与组合体(装配套件)实现**同一个** trait,
//! 客户端因此可以把它们当作同一种东西处理,
//! 无需写 `if 是目录 then ... else ...` 这类分支判断。
//!
//! # 本设计的三个关键取舍
//!
//! 1. **`children()` 是唯一必须由各类型实现的结构方法。**
//! 只要它能正确返回子节点,所有递归操作都可由默认实现自动完成,
//! 新增叶子/组合类型时几乎零负担。
//!
//! 2. **递归查询用默认实现,但"返回自身引用"的查询不能写在 trait 里。**
//! Rust 的 trait 默认方法中 `Self` 是 `?Sized` 的,
//! 无法把 `&Self` 强制转换为 `&dyn ProductComponent`。
//! 因此 `find_by_code` / `render_tree` 等需要返回自身引用的操作,
//! 被下沉到 `analysis` 层以自由函数实现(见 `analysis/search_analysis.rs`)。
//!
//! 3. **展示与结构分离。**
//! 本 trait 只保留"身份 + 结构 + 聚合"三类能力,
//! 树形渲染、占比分析等展示型操作全部放在 `analysis` 层,
//! 从而满足"职责单一",也让新增报表无需触碰核心接口。
use crate::domain::{ComponentCode, Money, Weight};
/// 叶子节点共用的静态空子节点切片
///
/// 所有叶子实现都直接返回它,避免每次调用都构造新对象。
pub const NO_CHILDREN: &[Box<dyn ProductComponent>] = &[];
/// 产品组件抽象接口 ------ 组合模式中的「Component」
///
/// # 实现须知
/// - **叶子**:`children()` 返回 [`NO_CHILDREN`],聚合方法返回自身数值;
/// - **组合体**:`children()` 返回子节点集合,聚合方法对子节点求和。
///
/// 本 trait 保持对象安全(所有方法无泛型参数、不返回 `Self`),
/// 因此可以自由装箱为 `Box<dyn ProductComponent>` 并按树形嵌套。
pub trait ProductComponent: std::fmt::Debug {
// ==================== 一、身份(Identity) ====================
/// 返回物料编码
fn code(&self) -> &ComponentCode;
/// 返回组件名称
fn name(&self) -> &str;
/// 返回组件种类名称(如 "Material" / "Service" / "Assembly")
///
/// 用于报表分类与诊断输出。
fn kind_name(&self) -> &'static str;
// ==================== 二、结构(Structure) ====================
/// 返回子组件切片
///
/// 这是本 trait 中**唯一**必须由各类型自行实现的结构方法,
/// 也是全部递归默认实现的基石。
fn children(&self) -> &[Box<dyn ProductComponent>];
/// 判断本节点是否为组合体
///
/// 采用显式方法而非"子节点非空"推断,
/// 因为一个尚未装配任何子件的空套件仍是组合体。
fn is_composite(&self) -> bool;
// ==================== 三、聚合(Aggregate) ====================
/// 返回本节点及其所有后代的累计重量
fn total_weight(&self) -> Weight;
/// 返回本节点及其所有后代的累计成本
fn total_cost(&self) -> Money;
/// 返回本节点及其所有后代的累计计件数
fn piece_count(&self) -> u32;
// ==================== 四、衍生查询(默认实现,基于结构与聚合递归) ====================
/// 返回子树中的节点总数(含自身)
fn node_count(&self) -> usize {
// 自身 1 个,加上所有子树节点数之和
1 + self
.children()
.iter()
.map(|child| child.node_count())
.sum::<usize>()
}
/// 返回子树中的叶子节点数量
fn leaf_count(&self) -> usize {
if self.children().is_empty() {
// 无子节点者即为叶子
return 1;
}
// 组合体的叶子数等于其子节点叶子数之和
self.children()
.iter()
.map(|child| child.leaf_count())
.sum::<usize>()
}
/// 返回子树的最大深度(叶子为 0,根到最远叶子的边数)
fn max_depth(&self) -> usize {
// 无子节点时深度为 0;否则为子树最大深度加一
self.children()
.iter()
.map(|child| child.max_depth() + 1)
.max()
.unwrap_or(0)
}
/// 判断子树中是否存在指定编码的组件
///
/// # 参数
/// - `code`:待查找的物料编码
fn contains_code(&self, code: &str) -> bool {
// 命中自身
if self.code().equals(code) {
return true;
}
// 递归检查所有子树
self.children()
.iter()
.any(|child| child.contains_code(code))
}
/// 判断子树中是否存在指定种类的组件
///
/// # 参数
/// - `kind_name`:组件种类名称
fn contains_kind(&self, kind_name: &str) -> bool {
if self.kind_name() == kind_name {
return true;
}
self.children()
.iter()
.any(|child| child.contains_kind(kind_name))
}
// ==================== 五、展示(Presentation) ====================
/// 返回本节点自身的一行属性描述(不包含子节点、不做递归)
///
/// 由各类型自行实现,以体现叶子与组合体在字段上的差异。
fn describe(&self) -> String;
/// 返回带编码、种类与聚合指标的精简摘要
///
/// 提供默认实现,供报表统一排版使用。
fn summary(&self) -> String {
format!(
"[{}] {} ({}) | 重 {} | 成本 {} | 件数 {}",
self.code(),
self.name(),
self.kind_name(),
self.total_weight(),
self.total_cost(),
self.piece_count()
)
}
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : traversal.rs
//! 通用树遍历工具
//!
//! 职责单一:提供与具体业务无关的树遍历能力。
//!
//! 【为什么单独成层】
//! `ProductComponent` 上的默认递归方法虽然能完成统计,
//! 但它们无法在递归过程中附带"路径"这类上下文信息
//! (因为"返回自身引用"在 trait 默认方法中无法转换为 `&dyn`)。
//! 本模块用显式栈把遍历过程外置,从而:
//! - 产出节点时同时给出**深度**与**从根到该节点的路径**;
//! - 让 `analysis` 层可以直接"站在遍历结果之上"写新报表,
//! **新增一种分析无需修改组件层任何一个文件**。
use crate::component::ProductComponent;
/// 一次节点访问记录
#[derive(Debug, Clone)]
pub struct NodeVisit<'a> {
/// 被访问的节点引用
pub node: &'a dyn ProductComponent,
/// 该节点所在深度(根节点为 0)
pub depth: usize,
/// 从根节点到该节点的名称路径(含首尾两端)
pub path: Vec<String>,
}
impl<'a> NodeVisit<'a> {
/// 把路径渲染为 `A / B / C` 形式
pub fn path_text(&self) -> String {
self.path.join(" / ")
}
}
/// 深度优先遍历器
///
/// 使用显式栈而非递归,避免深层产品结构可能造成的调用栈溢出。
/// 遍历顺序为**从左到右的前序遍历**(父节点先于其全部子节点)。
pub struct DepthFirstWalker<'a> {
/// 待处理栈,元素为(节点引用, 深度, 从根到该节点父级的路径)
stack: Vec<(&'a dyn ProductComponent, usize, Vec<String>)>,
}
impl<'a> DepthFirstWalker<'a> {
/// 以指定根节点创建遍历器
///
/// # 参数
/// - `root`:树的根节点
pub fn new(root: &'a dyn ProductComponent) -> Self {
Self {
// 根节点深度为 0,其父级路径为空
stack: vec![(root, 0, Vec::new())],
}
}
}
impl<'a> Iterator for DepthFirstWalker<'a> {
type Item = NodeVisit<'a>;
fn next(&mut self) -> Option<NodeVisit<'a>> {
// 取出栈顶元素;栈空则遍历结束
let (node, depth, parent_path) = self.stack.pop()?;
// 当前节点路径 = 父级路径 + 自身名称
let mut current_path: Vec<String> = parent_path;
current_path.push(node.name().to_string());
// 子节点**逆序**压栈,以保证弹出顺序为从左到右
let children = node.children();
for child in children.iter().rev() {
self.stack
.push((&**child, depth + 1, current_path.clone()));
}
Some(NodeVisit {
node,
depth,
path: current_path,
})
}
}
/// 便捷入口:以深度优先方式遍历整棵树
///
/// # 参数
/// - `root`:树的根节点
pub fn depth_first(root: &dyn ProductComponent) -> DepthFirstWalker<'_> {
DepthFirstWalker::new(root)
}
/// 收集子树中所有满足条件的节点
///
/// # 参数
/// - `root`:树的根节点
/// - `predicate`:判定函数,接收节点引用与其深度
pub fn collect_matching<'a>(
root: &'a dyn ProductComponent,
predicate: impl Fn(&dyn ProductComponent, usize) -> bool,
) -> Vec<NodeVisit<'a>> {
depth_first(root)
.filter(|visit| predicate(visit.node, visit.depth))
.collect()
}
/// 收集子树中的全部叶子节点
///
/// # 参数
/// - `root`:树的根节点
pub fn collect_leaves<'a>(root: &'a dyn ProductComponent) -> Vec<NodeVisit<'a>> {
// 叶子即"没有子节点"的节点
collect_matching(root, |node, _depth| node.children().is_empty())
}
/// 收集子树中的全部组合体节点
///
/// # 参数
/// - `root`:树的根节点
pub fn collect_composites<'a>(root: &'a dyn ProductComponent) -> Vec<NodeVisit<'a>> {
collect_matching(root, |node, _depth| node.is_composite())
}
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : Product_Assembly.rs
//! 组合体实现:产品装配体(ProductAssembly)
//!
//! 职责单一:承载子组件集合并把聚合操作**递归委托**给子组件。
//!
//! 【组合模式的核心机制】
//! 组合体与叶子实现同一个 `ProductComponent` 接口,
//! 但组合体的 `total_weight()` / `total_cost()` 等实现是
//! "对每个子节点再调一次同名方法,然后求和"------
//! 递归就此自然产生,客户端完全无需关心当前节点是叶子还是组合体。
//!
//! 同时,组合体额外实现 `ComponentContainer`,
//! 提供叶子不具备的 `add_child` / `remove_child_by_code` 等能力。
use crate::component::{ComponentContainer, ProductComponent};
use crate::domain::{ComponentCode, Money, Weight};
/// 产品装配体 / 套件(Composite)
///
/// 例如「龙凤呈祥三件套」,其子节点可以是单件物料、工艺服务,
/// 也可以是更小的装配体(如「项链主体」),从而形成任意深度的树。
#[derive(Debug)]
pub struct ProductAssembly {
/// 装配体编码
code: ComponentCode,
/// 装配体名称
name: String,
/// 子组件集合(递归嵌套的关键)
children: Vec<Box<dyn ProductComponent>>,
}
impl ProductAssembly {
/// 构造一个不含子组件的空装配体
///
/// # 参数
/// - `code`:装配体编码原文
/// - `name`:装配体名称
pub fn new(code: &str, name: &str) -> Self {
Self {
// 编码由值对象负责规范化
code: ComponentCode::new(code),
// 名称转为拥有所有权的字符串
name: name.to_string(),
// 初始为空集合,随后通过 add_child 装配
children: Vec::new(),
}
}
/// 构造一个已装配好子组件的装配体
///
/// # 参数
/// - `code`:装配体编码原文
/// - `name`:装配体名称
/// - `children`:子组件集合
pub fn with_children(
code: &str,
name: &str,
children: Vec<Box<dyn ProductComponent>>,
) -> Self {
Self {
code: ComponentCode::new(code),
name: name.to_string(),
children,
}
}
/// 返回直接子节点数量
pub fn direct_child_count(&self) -> usize {
self.children.len()
}
}
impl ProductComponent for ProductAssembly {
/// 返回装配体编码
fn code(&self) -> &ComponentCode {
&self.code
}
/// 返回装配体名称
fn name(&self) -> &str {
&self.name
}
/// 返回组件种类名称
fn kind_name(&self) -> &'static str {
"Assembly"
}
/// 返回子组件切片 ------ 递归的入口
fn children(&self) -> &[Box<dyn ProductComponent>] {
&self.children
}
/// 装配体恒为组合体(即使当前尚未装配任何子件)
fn is_composite(&self) -> bool {
true
}
/// 累计重量 = 全部子组件累计重量之和
fn total_weight(&self) -> Weight {
self.children
.iter()
.map(|child| child.total_weight())
.sum()
}
/// 累计成本 = 全部子组件累计成本之和
fn total_cost(&self) -> Money {
self.children
.iter()
.map(|child| child.total_cost())
.sum()
}
/// 累计件数 = 全部子组件累计件数之和
fn piece_count(&self) -> u32 {
self.children
.iter()
.map(|child| child.piece_count())
.sum()
}
/// 一行属性描述
fn describe(&self) -> String {
format!(
"产品装配体 | 编码 {} | 名称 {} | 直接子件 {} 个 | 节点总数 {} | 叶子数 {} | 最大深度 {} | 合计净重 {} | 合计成本 {} | 合计件数 {}",
self.code,
self.name,
self.direct_child_count(),
self.node_count(),
self.leaf_count(),
self.max_depth(),
self.total_weight(),
self.total_cost(),
self.piece_count()
)
}
}
impl ComponentContainer for ProductAssembly {
/// 以只读切片形式访问子节点
fn child_slice(&self) -> &[Box<dyn ProductComponent>] {
&self.children
}
/// 以可变引用形式访问子节点集合
fn child_slice_mut(&mut self) -> &mut Vec<Box<dyn ProductComponent>> {
&mut self.children
}
}
调用:
rust
//!# encoding: utf-8
//!# 版权所有 2026 ©涂聚文有限公司™ ®
//!# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
//!# 描述:组合模式Composite Pattern
//!# Author : geovindu,Geovin Du 涂聚文.
//!# IDE : RustRover 2025.1.1 Rust rustc 1.98.1
//!# os : windows 10
//!# database : mysql 9.0 sql server 2025, postgreSQL 18.0 Oracle 21c Neo4j
//!# Datetime : 2026/10/1 19:24
//!# User : geovindu
//!# Product : RustRover
//!# Project : compositepattern
//!# File : main.rs
// 本示例属于"可扩展框架式"代码:各层刻意暴露了完整的公开 API 面
// (构造器、访问器、分析入口等),以便读者按需扩展组合。
// 这些接口不会被当前演示入口全部调用,故在此关闭未使用代码告警。
#![allow(dead_code)]
//! 组合模式(Composite Pattern)演示入口
//!
//! # 模式意图
//! 将对象组合成**树形结构**来表示"部分-整体"的层次关系。
//! 组合模式使得客户端可以**一致地**对待单个对象与对象组合。
//!
//! # 本工程的统一对待机制
//! [`component::ProductComponent`] 同时被两类角色实现:
//! - **叶子(Leaf)**:材料单件、工艺服务 ------ 没有子节点,直接给出自身数值;
//! - **组合体(Composite)**:产品装配体 ------ 持有子节点,把聚合递归委托给它们。
//!
//! 客户端(`app` 层与分析层)只依赖 `&dyn ProductComponent`,
//! 因此完全不需要判断"当前节点是叶子还是组合体"。
//!
//! # 严格分层结构
//! ```text
//! main.rs 入口,仅做流程编排
//! └── app 应用层:报表编排(只取数 + 排版)
//! └── analysis 分析层:树渲染 / 查找 / 聚合(可无限新增)
//! └── component 组件层:Component 抽象 + 容器角色 + 通用遍历
//! └── factory 工厂层:规格数据 → 组件树
//! └── composite 组合层:ProductAssembly(递归委托)
//! └── leaf 叶子层:MaterialItem / ServiceItem
//! └── domain 领域层:Money / Weight / ComponentCode
//! ```
// 声明各分层模块(编译期强制分层可见性)
mod analysis;
mod app;
mod component;
mod composite;
mod domain;
mod factory;
mod leaf;
// ---------- 引入组件层 ----------
use component::{
collect_matching, depth_first, ComponentContainer, DepthFirstWalker, NodeVisit,
ProductComponent, NO_CHILDREN,
};
// ---------- 引入组合层 ----------
use composite::ProductAssembly;
// ---------- 引入领域层 ----------
use domain::{ComponentCode, Money, Weight};
// ---------- 引入叶子层 ----------
use leaf::{MaterialItem, ServiceItem};
// ---------- 引入工厂层 ----------
use factory::{BuildError, ComponentSpec, ProductBuilder};
// ---------- 引入分析层 ----------
use analysis::{CostCompositionEntry, SearchAnalysis, SearchHit, TreeAnalysis};
// ---------- 引入应用层 ----------
use app::BomReport;
// ============================================================================
// 第四幕所需的「自定义叶子类型」
//
// 本类型定义在 main.rs 中,**不属于工程的任何一个分层**。
// 它只实现 `ProductComponent` 一个 trait,即可:
// - 被挂载进任何 `ProductAssembly`;
// - 被组件层的默认递归方法自动统计;
// - 被分析层自动纳入成本构成分类;
// - 被应用层自动渲染进报表。
//
// 全程未修改任何既有分层文件 ------ 这就是"完全可扩展"的实证。
// ============================================================================
/// 礼品包装项(自定义叶子类型)
#[derive(Debug)]
struct PackagingItem {
/// 包装物料编码
code: ComponentCode,
/// 包装名称
name: String,
/// 单套包装费
unit_price: Money,
/// 套数
sets: u32,
}
impl PackagingItem {
/// 构造一个礼品包装项
///
/// # 参数
/// - `code`:包装编码原文
/// - `name`:包装名称
/// - `unit_price_yuan`:单套包装费(元)
/// - `sets`:套数
fn new(code: &str, name: &str, unit_price_yuan: f64, sets: u32) -> Self {
Self {
code: ComponentCode::new(code),
name: name.to_string(),
unit_price: Money::from_yuan(unit_price_yuan),
sets,
}
}
}
/// 实现组合模式的 Component 接口
impl ProductComponent for PackagingItem {
/// 返回包装编码
fn code(&self) -> &ComponentCode {
&self.code
}
/// 返回包装名称
fn name(&self) -> &str {
&self.name
}
/// 返回组件种类名称
fn kind_name(&self) -> &'static str {
"Packaging"
}
/// 叶子没有子节点
fn children(&self) -> &[Box<dyn ProductComponent>] {
NO_CHILDREN
}
/// 不是组合体
fn is_composite(&self) -> bool {
false
}
/// 包装不计贵金属重量
fn total_weight(&self) -> Weight {
Weight::ZERO
}
/// 累计成本 = 单套包装费 × 套数
fn total_cost(&self) -> Money {
self.unit_price.multiply(self.sets as i64)
}
/// 计件数 = 套数
fn piece_count(&self) -> u32 {
self.sets
}
/// 一行属性描述
fn describe(&self) -> String {
format!(
"礼品包装 | 编码 {} | 名称 {} | 单套包装费 {} | 套数 {} | 合计成本 {}",
self.code,
self.name,
self.unit_price,
self.sets,
self.total_cost()
)
}
}
/// 程序入口
fn main() {
// 全局分隔线,用于终端输出美化
let global_separator: &str =
"================================================================================";
// ========================================================================
// 第一幕:以「规格数据」驱动,由工厂递归构建组件树
// ========================================================================
println!("{}", global_separator);
println!("第一幕:以规格数据驱动,由工厂递归构建产品结构树");
println!("{}", global_separator);
// 构造规格树:嵌套深度即产品结构的层级
let product_specification: ComponentSpec =
ComponentSpec::assembly("ASM-SET-001", "龙凤呈祥三件套").with_children(vec![
// ---------- 项链主体 ----------
ComponentSpec::assembly("ASM-NECK-001", "项链主体").with_children(vec![
// 金链身
ComponentSpec::material("MAT-GOLD-001", "18K金链身", 3.25, 520.0, 1),
// 吊坠主体(再嵌一层装配体)
ComponentSpec::assembly("ASM-PEND-001", "吊坠主体").with_children(vec![
ComponentSpec::material("MAT-PLAT-001", "铂金吊坠托", 2.80, 380.0, 1),
ComponentSpec::material("MAT-DIA-001", "碎钻群镶", 0.60, 6800.0, 1),
ComponentSpec::service("SVC-SET-001", "钻石镶嵌工时", 180.0, 1),
]),
// 抛光打磨
ComponentSpec::service("SVC-POLISH-001", "抛光打磨", 45.0, 3),
]),
// ---------- 戒指主体 ----------
ComponentSpec::assembly("ASM-RING-001", "戒指主体").with_children(vec![
ComponentSpec::material("MAT-GOLD-002", "18K金戒圈", 4.10, 520.0, 1),
ComponentSpec::service("SVC-PLATE-001", "电镀铑", 35.0, 1),
]),
]);
// 统计规格树的规模,验证递归遍历在数据侧同样成立
println!(
"规格树构建完毕:根节点 {},直接子规格 {} 个",
product_specification.display_name(),
product_specification.child_count()
);
// 带校验地构建(校验编码非空且全树唯一)
let build_result: Result<Box<dyn ProductComponent>, BuildError> =
ProductBuilder::build_checked(&product_specification);
let product_tree: Box<dyn ProductComponent> = match build_result {
// 校验通过,成功构建
Ok(tree) => {
println!("编码校验通过:全树编码唯一且非空。");
tree
}
// 校验失败(本演示数据不会触发,保留分支以展示错误处理路径)
Err(build_error) => {
println!("构建失败:{}", build_error);
return;
}
};
// 用精确查找验证某一关键物料确实位于预期位置
let diamond_hit: Option<SearchHit<'_>> =
SearchAnalysis::find_by_code(product_tree.as_ref(), "MAT-DIA-001");
if let Some(hit) = diamond_hit {
println!(
"关键物料定位:{} 位于深度 {},路径为 {}",
hit.code_text(),
hit.depth,
hit.path_text()
);
}
// ========================================================================
// 第二幕:生成完整 BOM 报表
// ========================================================================
println!();
println!("{}", global_separator);
println!("第二幕:生成完整 BOM 报表");
println!("{}", global_separator);
// 传入根节点的 trait 对象引用;分析层与报表层完全无需知道具体类型
let bom_document: String = BomReport::generate(
product_tree.as_ref(),
&["金", "SVC-SET-001", "吊坠"],
);
println!("{}", bom_document);
// ========================================================================
// 第三幕:运行时动态重构 ------ 子树的挂载、摘除与迁移
// ========================================================================
println!();
println!("{}", global_separator);
println!("第三幕:运行时动态重构 ------ 子树的挂载、摘除与迁移");
println!("{}", global_separator);
// 说明:要修改"嵌套子节点",必须持有具体类型的可变句柄。
// 这是因为 `ProductComponent` 只提供只读访问(读取用统一接口),
// 而增删能力被隔离在 `ComponentContainer` 中(构建用具体类型)。
// 因此这里用「对象驱动」方式直接装配,与第一幕的「数据驱动」互为补充。
// 独立构建"项链主体"
let mut necklace_assembly: ProductAssembly = ProductAssembly::new("ASM-NECK-900", "项链主体");
necklace_assembly.add_child(Box::new(MaterialItem::new(
"MAT-GOLD-900",
"18K金链身",
3.25,
520.0,
1,
)));
necklace_assembly.add_child(Box::new(ServiceItem::new("SVC-POLISH-900", "抛光打磨", 45.0, 1)));
// 独立构建"吊坠主体"(一个完整的子树)
let mut pendant_assembly: ProductAssembly = ProductAssembly::new("ASM-PEND-900", "吊坠主体");
pendant_assembly.add_child(Box::new(MaterialItem::new(
"MAT-PLAT-900",
"铂金吊坠托",
2.80,
380.0,
1,
)));
pendant_assembly.add_child(Box::new(MaterialItem::new(
"MAT-DIA-900",
"碎钻群镶",
0.60,
6800.0,
1,
)));
pendant_assembly.add_child(Box::new(ServiceItem::new(
"SVC-SET-900",
"钻石镶嵌工时",
180.0,
1,
)));
// 迁移前的状态
println!("① 迁移前 ------ {}", necklace_assembly.summary());
// 把整棵"吊坠主体"子树挂到项链上(组合体可容纳组合体,形成任意深度)
necklace_assembly.add_child(Box::new(pendant_assembly));
println!("② 挂载吊坠子树后 ------ {}", necklace_assembly.summary());
// 从项链上摘除吊坠子树:返回的是一株**结构完整**的子树
let detached_pendant: Option<Box<dyn ProductComponent>> =
necklace_assembly.remove_child_by_code("ASM-PEND-900");
println!("③ 摘下吊坠后 ------ {}", necklace_assembly.summary());
// 把摘下的子树原样迁移到新的装配体中
let mut single_item_box: ProductAssembly =
ProductAssembly::new("ASM-SINGLE-900", "单品礼盒");
if let Some(pendant_component) = detached_pendant {
// 子树连同其全部后代一并迁移,聚合值保持不变
single_item_box.add_child(pendant_component);
}
println!("④ 迁移后 ------ {}", single_item_box.summary());
println!();
println!("迁移后的单品礼盒完整结构:");
for tree_line in TreeAnalysis::render_with_metrics(&single_item_box).lines() {
println!(" {}", tree_line);
}
// ========================================================================
// 第四幕:扩展性验证 ------ 挂载一个定义在本文件中的自定义叶子类型
// ========================================================================
println!();
println!("{}", global_separator);
println!("第四幕:扩展性验证 ------ 挂载工程之外定义的自定义叶子类型");
println!("{}", global_separator);
// 新建"臻品礼盒套装"装配体
let mut luxury_gift_set: ProductAssembly =
ProductAssembly::new("ASM-LUX-900", "臻品礼盒套装");
// 子件一:由工厂构建的既有子树(复用第二幕的规格)
luxury_gift_set.add_child(ProductBuilder::build(&product_specification));
// 子件二:自定义叶子类型(来自本文件,不属于任何分层)
luxury_gift_set.add_child(Box::new(PackagingItem::new(
"PKG-GIFT-900",
"臻品绒面礼盒",
158.0,
1,
)));
// 子件三:再挂一个自定义叶子
luxury_gift_set.add_child(Box::new(PackagingItem::new(
"PKG-CARD-900",
"品牌贺卡",
28.0,
2,
)));
// 自定义类型被组件层默认递归方法自动纳入统计
println!("自定义叶子已挂载,以下指标由组件层默认递归方法自动得出:");
println!(" 节点总数:{}", luxury_gift_set.node_count());
println!(" 叶子数量:{}", luxury_gift_set.leaf_count());
println!(
" 装配体数:{}",
analysis::AggregateAnalysis::structure_statistics(&luxury_gift_set).composite_count
);
println!(" 最大深度:{}", luxury_gift_set.max_depth());
println!(" 合计净重:{}", luxury_gift_set.total_weight());
println!(" 合计成本:{}", luxury_gift_set.total_cost());
println!(" 合计件数:{}", luxury_gift_set.piece_count());
println!();
println!("自定义类型「Packaging」是否已被分析层识别:{}", luxury_gift_set.contains_kind("Packaging"));
println!();
println!("臻品礼盒套装的成本构成(自定义叶子被自动归类到「包装」):");
let composition_entries: Vec<CostCompositionEntry> =
analysis::AggregateAnalysis::cost_composition(&luxury_gift_set);
for entry in &composition_entries {
println!(
" ▸ {} ------ 成本 {}({:.1}%)| 净重 {} | 叶子 {} 个 | 件数 {}",
entry.category_label,
entry.cost,
entry.share_percent,
entry.weight,
entry.leaf_count,
entry.pieces
);
}
// ========================================================================
// 第五幕:仅用公开遍历 API 现场撰写全新分析,核心层零改动
// ========================================================================
println!();
println!("{}", global_separator);
println!("第五幕:仅用公开遍历 API 现场撰写新分析,核心层零改动");
println!("{}", global_separator);
// 需求一:列出"深度不小于 2 的全部叶子节点"及其完整路径
// 实现只需一次 collect_matching 调用,无需新增任何分层文件
let deep_leaf_visits: Vec<NodeVisit<'_>> = collect_matching(
product_tree.as_ref(),
|node, depth| node.children().is_empty() && depth >= 2,
);
println!("深度不小于 2 的叶子节点共 {} 个:", deep_leaf_visits.len());
for visit in &deep_leaf_visits {
println!(
" · {} [{}] | 深度 {} | 路径:{}",
visit.node.name(),
visit.node.kind_name(),
visit.depth,
visit.path_text()
);
}
// 需求二:统计"每一层各有多少个节点"
let mut depth_histogram: Vec<usize> = Vec::new();
for visit in depth_first(product_tree.as_ref()) {
// 首次触达更深的层级时先扩容
if depth_histogram.len() <= visit.depth {
depth_histogram.resize(visit.depth + 1, 0);
}
depth_histogram[visit.depth] += 1;
}
println!();
println!("各层节点数分布(第 0 层为根节点):");
for (level, node_count) in depth_histogram.iter().enumerate() {
println!(" 第 {} 层:{} 个节点", level, node_count);
}
// 需求三:直接持有遍历器类型,验证其满足标准迭代器协议
let walker: DepthFirstWalker<'_> = depth_first(product_tree.as_ref());
let visited_total: usize = walker.count();
println!();
println!(
"遍历器直接计数:完整前序遍历共访问 {} 个节点(应等于节点总数)",
visited_total
);
// ========================================================================
// 收尾:组合规模总结
// ========================================================================
println!();
println!("{}", global_separator);
println!("收尾:组合模式的可组合规模");
println!("{}", global_separator);
println!(" 抽象角色(Component):1 个统一接口");
println!(" 叶子角色(Leaf):3 种(MaterialItem / ServiceItem / 自定义 PackagingItem)");
println!(" 组合角色(Composite):1 种(ProductAssembly,可自我嵌套)");
println!(
" 可组合层级:不受限制(本演示最大深度 {},装配体可再套装配体)",
product_tree.max_depth()
);
println!(" 分析操作:树形渲染 / 模糊查找 / 成本构成 / 结构统计(新增操作零侵入核心层)");
println!("{}", global_separator);
println!("演示结束。");
}
输出:
