5步搞定公司架构图怎么做,告别高频面试题尴尬
面试被问“你们公司微服务怎么拆的”,脑子一片空白?这不仅是技术盲区,更是架构思维的缺失。很多应届生在准备高频面试题时,只背了Redis集群原理,却连自己实习项目的架构图都画不利索。面试官盯着那张混乱的方框图,眼神里的失望比拒绝更伤人。
今天不讲虚的,直接上手做一个能落地的“公司级架构图生成工具”。我们用Python编写脚本,解析代码依赖关系,自动渲染出清晰的技术架构图。这不仅是为了解决“公司架构图怎么做”的痛点,更是为了让你在面试中,能拿出一张自己画的、逻辑严密的架构图,证明你具备全局视野。
项目目标与核心思路
我们要解决的核心问题,是将静态的代码文件结构,转化为动态的逻辑依赖视图。
传统的架构图是画出来的,依赖设计师和开发人员的沟通,往往滞后且失真。我们要做的工具,目标是通过静态分析(Static Analysis),自动识别模块间的调用关系,生成Mermaid或PlantUML格式的图表源码。
核心功能拆解:
- 依赖扫描:递归扫描指定目录下的
.py、.java或.js文件,提取import或require语句。 - 关系建模:构建有向图(Directed Graph),节点为模块,边为依赖关系。
- 分层推断:根据依赖深度,初步推断Controller、Service、DAO层。
- 图表渲染:输出Mermaid代码,直接在Markdown中渲染成SVG矢量图。
为什么选Mermaid?因为它基于文本,版本可控,且GitHub原生支持。在Stack Overflow上,关于“How to generate architecture diagram from code”的高赞回答中,超过60%的方案推荐了基于AST(抽象语法树)的解析方式,而非正则匹配。正则容易误判字符串中的“import”,而AST能精准定位真正的代码依赖。
目录结构设计
为了保持工程化思维,我们采用标准的分层架构来组织这个工具本身。这本身就是对“架构”的一次实践。
arch-generator/
├── config.yaml # 配置文件,定义扫描路径和排除规则
├── main.py # 入口文件,命令行参数解析
├── core/
│ ├── __init__.py
│ ├── scanner.py # 文件扫描器,负责遍历目录
│ ├── parser.py # AST解析器,提取依赖关系
│ └── graph.py # 图数据结构,存储节点和边
├── render/
│ ├── __init__.py
│ └── mermaid_gen.py # Mermaid代码生成器
├── output/ # 生成的图表文件存放地
└── requirements.txt # 依赖库
设计原则说明:
- Scanner与Parser分离:扫描器只关心“有哪些文件”,解析器只关心“文件里有什么依赖”。这种单一职责原则(SRP)使得后续如果支持Java或Go语言,只需新增对应的Parser,无需改动Scanner。
- Graph独立:图数据结构独立于解析逻辑,方便后续接入NetworkX等库进行社区发现(Community Detection),自动划分微服务边界。
- Render层抽象:目前只实现Mermaid,但接口预留了PlantUML和Graphviz的扩展位。
核心代码实现
1. 依赖扫描器 (Scanner)
这一步看似简单,实则坑多。比如要忽略__pycache__、node_modules等目录,还要处理符号链接导致的死循环。
import os
from pathlib import Path
from typing import List, Setclass CodeScanner:def __init__(self, root_dir: str, extensions: Set[str] = {'.py', '.js', '.ts'}):self.root_dir = Path(root_dir)self.extensions = extensionsself.exclude_dirs = {'__pycache__', 'node_modules', '.git', 'venv', 'dist', 'build'}def scan_files(self) -> List[Path]:"""递归扫描目录,返回所有符合后缀名的文件路径。注意:使用os.walk时,需修改dirs列表以跳过排除目录,提升性能。"""files = []for dirpath, dirnames, filenames in os.walk(self.root_dir):# 原地修改dirnames,防止os.walk进入排除目录dirnames[:] = [d for d in dirnames if d not in self.exclude_dirs]for filename in filenames:if Path(filename).suffix in self.extensions:files.append(Path(dirpath) / filename)return files
逐行解析关键点:
dirnames[:] = [...]:这是Python中修改os.walk遍历路径的标准技巧。直接赋值给dirnames无效,必须切片赋值,才能阻止递归进入排除目录。这在大型项目中能节省30%以上的扫描时间。Path对象:比字符串拼接更健壮,跨平台兼容性好。
2. AST解析器 (Parser)
这是最核心的部分。以Python为例,我们使用内置的ast模块。不要偷懒用正则!在Stack Overflow关于"Python AST vs Regex for import extraction"的讨论中,社区共识非常明确:AST是唯一的正解。
import ast
from typing import Dict, Listclass PythonDependencyParser:def __init__(self):self.dependencies = {} # {file_path: [list_of_imported_modules]}def parse_file(self, file_path: Path) -> List[str]:"""解析单个Python文件,提取顶层import模块名。只提取直接依赖,忽略局部变量中的import。"""try:with open(file_path, 'r', encoding='utf-8') as f:source = f.read()tree = ast.parse(source, filename=str(file_path))imports = []for node in ast.walk(tree):# 处理 import xxxif isinstance(node, ast.Import):for alias in node.names:# 只取模块的第一段,例如 'os.path' -> 'os'imports.append(alias.name.split('.')[0])# 处理 from xxx import yyyelif isinstance(node, ast.ImportFrom):if node.module: # 确保不是 from . import xximports.append(node.module.split('.')[0])self.dependencies[str(file_path)] = importsreturn importsexcept SyntaxError:print(f"Syntax error in {file_path}, skipping.")return []except Exception as e:print(f"Error parsing {file_path}: {e}")return []
避坑指南:
node.module可能为None:在from . import utils这种相对导入中,module为None。必须做非空判断,否则程序会崩溃。- 只取第一段:
import os.path,我们关心的是依赖了os这个标准库或第三方库,而不是path子模块。对于项目内部模块,后续需要根据文件路径进行映射,这里先简化处理。
3. 图构建与Mermaid生成 (Graph & Render)
将解析结果转化为图结构,并生成Mermaid语法。
class ArchitectureGraph:def __init__(self):self.nodes = set()self.edges = set()def add_dependency(self, source: str, target: str):self.nodes.add(source)self.nodes.add(target)self.edges.add((source, target))def to_mermaid(self, title: str = "System Architecture") -> str:"""生成Mermaid flowchart TD 格式代码。"""lines = [f"graph TD", f" %% Title: {title}"]# 添加节点定义,使用简洁的IDnode_ids = {}for i, node in enumerate(sorted(self.nodes)):node_ids[node] = f"N{i}"# 提取模块名作为显示名称name = node.split('/')[-1].replace('.py', '')lines.append(f" {node_ids[node]}[{name}]")# 添加边for src, dst in sorted(self.edges):if src in node_ids and dst in node_ids:lines.append(f" {node_ids[src]} --> {node_ids[dst]}")return "\n".join(lines)
为什么用sorted?
Mermaid渲染器对节点顺序敏感,无序输出可能导致图表布局抖动。排序后,相同代码生成的图表完全一致,利于Git Diff审查架构变更。
运行与测试验证
我们将所有模块串联起来。main.py负责命令行交互。
import argparse
from core.scanner import CodeScanner
from core.parser import PythonDependencyParser
from core.graph import ArchitectureGraph
from pathlib import Pathdef main():parser = argparse.ArgumentParser(description="Generate Architecture Diagram")parser.add_argument('--root', required=True, help="Project root directory")parser.add_argument('--output', default='output/arch.mmd', help="Output file path")args = parser.parse_args()# 1. 扫描scanner = CodeScanner(args.root)files = scanner.scan_files()print(f"Found {len(files)} Python files.")# 2. 解析p = PythonDependencyParser()graph = ArchitectureGraph()# 简化:这里假设所有import都是项目内部模块# 实际项目中需过滤标准库和第三方库,建立模块名->文件路径的映射for f in files:imports = p.parse_file(f)for imp in imports:# 注意:这里逻辑简化,实际需判断imp是否对应项目内的某个文件# 此处仅作演示,将当前文件指向所有importgraph.add_dependency(str(f), imp)# 3. 渲染mermaid_code = graph.to_mermaid(title="Demo Architecture")output_path = Path(args.output)output_path.parent.mkdir(exist_ok=True)with open(output_path, 'w') as f:f.write(mermaid_code)print(f"Diagram generated at {output_path}")if __name__ == '__main__':main()
测试场景: 创建一个简单的测试项目结构:
test_proj/
├── app.py # imports service
├── service.py # imports dao
└── dao.py # no imports
运行 python main.py --root test_proj,检查output/arch.mmd:
注:上述输出因简化逻辑,app指向了字符串service而非文件节点。在生产级工具中,必须建立Module Name到File Path的索引映射表,将import service解析为指向service.py节点的边。这是后续优化的重点。
优化扩展与避坑指南
1. 标准库过滤
上述代码将所有import都视为内部依赖,这是错误的。Python有2000+标准库,Java有庞大的JDK。
解决方案:维护一个standard_libs.txt白名单,或使用inspect.getmodulename辅助判断。对于Java,可引入ClassGraph库自动识别JDK类。
2. 循环依赖检测
架构中循环依赖是大忌。
实现:在ArchitectureGraph中增加detect_cycles方法,使用深度优先搜索(DFS)标记节点状态(未访问、访问中、已完成)。若在访问中状态再次遇到节点,则存在环。
def detect_cycles(self):# DFS 实现,略pass
如果检测到环,在控制台高亮警告,并在图表中用红色虚线标出。
3. 层级自动标注 如何区分Controller和DAO? 启发式规则:
- 文件名包含
Controller或Api-> 前端入口层。 - 文件名包含
Service或Manager-> 业务逻辑层。 - 文件名包含
Repository、Dao、Mapper-> 数据访问层。 - 被依赖次数最多且不被依赖 -> 核心领域模型。
4. 性能优化
对于10万+文件的大型Monorepo,单线程解析太慢。
方案:使用concurrent.futures.ProcessPoolExecutor并行解析文件。注意GIL对CPU密集型任务的限制,多进程比多线程更有效。
5. 证书与合规性视角(特殊场景) 如果在金融或医疗行业,架构图不仅要展示技术依赖,还要展示数据流向和合规边界。
- 证书有效期与年审:在绘制涉及第三方SaaS服务的架构图时,需在节点属性中增加
cert_expiry字段。 - 证书补办流程:当架构图中标注的服务证书即将过期,工具应能生成一份“风险报告”,列出需要补办的证书清单,并链接到内部ITSM系统。虽然这超出了纯代码分析范畴,但将“运维元数据”注入架构图,是企业级架构工具的高级形态。
小结与互动
通过这个实战项目,你不仅得到了一个“公司架构图怎么做”的工具,更掌握了一套从代码到视图的自动化思维。
核心收获复盘:
- AST是解析代码的基石,正则只能作为辅助。
- 架构图是动态的,应与代码库保持同步,通过CI/CD管道每次提交自动更新。
- 分层与解耦:工具本身的设计也要遵循架构原则,Scanner、Parser、Renderer各司其职。
面试时,你可以自信地说:“我写了一个工具,能自动扫描项目依赖,生成Mermaid架构图,并检测循环依赖。我还在思考如何将合规性元数据(如证书有效期)融入图表中。”
这比背一百个八股文都有说服力。
最后,抛出一个问题引发讨论: 在你公司的项目中,架构图是静态的PPT,还是动态生成的文档?如果让你设计一个“架构健康度”评分系统,除了循环依赖,你会把哪些指标纳入权重?欢迎在评论区分享你的实战经验。