AI Library · 知识库
按精选导读跳读

# 行为抽象:将报告写入不同目标

学习目标:让报告函数只依赖写入能力,并在不修改它的前提下从内存目标切换到真实文件。

前置知识与范围:先理解函数、参数和返回值,可从字符串转端口号继续阅读。本篇只把借用、泛型、Result、interface 和抽象类作为表达能力契约的机制来认识,不要求先掌握它们的完整语法体系,也不展开异步流。

# 问题与边界

一个命令行工具需要输出报告。在生产环境写入文件,在测试中写入内存。报告生成逻辑只需要“写入文本”的能力,不应负责创建文件。

先采用标准库已经提供的写入协议。这样可以观察行为抽象,不必先设计一个 ReportWriterFactory 或新的继承体系。

本篇选择 Rust、Go、Java、Python 展示四种自然写法。Kotlin/JVM 可以使用 Java 的 Writer;JS/TS 的流接口涉及异步和背压,C++ 的流又有独立的错误状态约定,它们留在资源与异步专题中展开,不伪装成与本例完全相同的调用模型。

报告内容固定为 items: 3\n。先用内存目标验证内容,再用 Python 展示真实文件目标;报告函数始终不创建、刷新或关闭传入的资源。

# 关键差异

语言 本例依赖 何时检查主要调用约束
Rust Write trait 泛型约束 编译时检查 trait 约束
Go io.Writer 接口 编译时检查接口赋值与方法集
Java Writer 抽象类 编译时检查类型关系
Python 对象提供兼容的 write 行为 执行调用时;可另加静态检查

Rust 的 std::io::Write 和 Go 的 io.Writer 写入字节;Java 的 Writer 和 Python 的文本流接收文本。本例输出只有 ASCII 字符,因此结果容易比较;换成一般文本时,必须在文本转字节的边界明确编码。Rust 格式化字符串和 Go 字符串输出在本例中不涉及额外的文件编码配置,Java/Python 的文件文本流则需要选择编码。

这只是本例选用的机制,不是各语言全部多态能力的列表。Rust 也可以用动态分派,Python 也可以用显式抽象基类;这里优先复用标准库已有能力。

# 各语言实现

以下四个内存示例分别是完整程序,彼此独立,不要求输出到控制台。Python 后另有可追加组装的真实文件调用片段。

# Rust

标准库 trait 与泛型参数。

Rustuse std::io::{self, Write};

fn write_report(out: &mut impl Write, count: usize) -> io::Result<()> {
    writeln!(out, "items: {}", count)
}

fn main() -> io::Result<()> {
    let mut buffer = Vec::new();
    write_report(&mut buffer, 3)?;
    assert_eq!(buffer, b"items: 3\n");
    Ok(())
}

Write 表达所需能力,&mut 允许借用期间修改输出对象。这里的 impl Write 是泛型参数写法,不是运行时 trait 对象。换成可写文件时,报告函数无需改变。参见 std::io::Write。

# Go

隐式满足接口。基线为 Go 1.22。

Gopackage main

import (
    "bytes"
    "fmt"
    "io"
)

func writeReport(out io.Writer, count int) error {
    _, err := fmt.Fprintf(out, "items: %d\n", count)
    return err
}

func main() {
    var buffer bytes.Buffer
    if err := writeReport(&buffer, 3); err != nil {
        panic(err)
    }
    if buffer.String() != "items: 3\n" {
        panic("unexpected report")
    }
}

调用方传入 *bytes.Buffer,不需要声明“实现 io.Writer”。是否满足接口由方法集决定。io.Writer 文档。

这里用 panic 让演示程序在校验失败时退出;库函数仍通过 error 报告失败。

# Java

复用已有抽象类。使用 JDK 21。

Javaimport java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

public class Main {
    static void writeReport(Writer out, int count) throws IOException {
        out.write("items: " + count + "\n");
    }

    public static void main(String[] args) throws IOException {
        var buffer = new StringWriter();
        writeReport(buffer, 3);
        if (!buffer.toString().equals("items: 3\n")) {
            throw new AssertionError("unexpected report");
        }
    }
}

这里的 Writer 是标准库抽象类,StringWriter 是具体实现。没有必要再给同一操作套一层自定义接口。Java Writer 文档。

# Python

调用所需行为。使用 Python 3.11。

Pythonfrom io import StringIO

def write_report(out, count: int) -> None:
    out.write(f"items: {count}\n")

buffer = StringIO()
write_report(buffer, 3)
assert buffer.getvalue() == "items: 3\n"

函数直接调用 write。对象必须在运行时提供兼容行为;仅有同名方法不意味着语义一定正确。当前 out 没有类型注解,不会仅凭这段声明获得静态写入能力检查。需要静态检查的项目可用 typing.Protocol(Python 3.11)描述所需方法,再由静态检查工具检查;本篇不另写一套协议实现。文本流行为参见 Python 3.11 io 文档。

# 真实文件目标

下面是追加调用片段,不是独立程序。将它原样追加到上面的完整 main.py 末尾,即得到同时验证内存和文件的完整程序;保留原有导入、write_report 函数和内存断言,不修改函数。执行命令仍为 python main.py。

Pythonfrom pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    report_path = Path(directory) / "report.txt"
    with report_path.open("x", encoding="utf-8", newline="") as out:
        write_report(out, 3)
    assert report_path.read_bytes() == b"items: 3\n"

这段调用确实创建并写入磁盘上的临时文件,随后断言真实文件字节,不是再次检查内存缓冲区。临时目录由标准库分配,退出外层 with 时清理;"x" 表示排他创建,若文件已存在便失败,不覆盖用户文件。参见 TemporaryDirectory与 Path.open。

encoding="utf-8" 明确文本到字节的编码,newline="" 禁止写出时把 \n 转换为平台行尾,因此 Windows 上也应写出同样的字节。参见 open 的编码与换行约定。

资源职责仍在调用方:外层创建临时目录,内层创建并管理文件;退出内层 with 时关闭文本流,关闭过程刷新缓冲区,所以字节断言放在关闭之后。write_report 只执行写入,不负责刷新或关闭。打开、写入、刷新、关闭和清理都可能失败;本演示不捕获这些异常,让失败传播并终止程序,而不是伪装成成功。实际应用应在拥有资源的调用边界决定如何报告或恢复。关闭刷新并不等于保证断电后的持久化。参见 IOBase.close。

# 迁移误区与适用限制

面向对象章节不必从“先创建一个父类”开始。先确定使用方依赖的能力,再选择语言和标准库中合适的机制。如果整个程序只有一个简单输出点,直接调用文件写入函数也可能更清楚;本例引入抽象,是因为已经有“文件输出”和“内存验证”两个真实需求。

# 运行与验证

按各语言段落保存完整程序,再按需为 Python 追加文件调用片段。预期内存目标内容为 items: 3\n;Python 文件调用还应在关闭后通过字节断言,并在退出时删除临时目录。成功时程序不要求打印任何内容。

各段落给出了运行命令和代码组装方式;实际工具链、已执行用例与未覆盖部分见验证记录。这里的断言与预期行为不代表已经在当前环境执行通过。

按精选导读跳读