关注点分离(Separation of Concerns,简称 SoC)是一种思考方式,主张不要同时处理问题的多个方面,而是集中精力深入研究某一特定方面,有意识地将其他方面暂时搁置。1
在程序设计领域,关注点分离是一种被广泛应用的设计原则。从不同的关注点出发,将系统划分为多个相对独立的模块,可以显著降低系统的复杂性,同时提高系统的可维护性和可扩展性。2
关注点分离的实践
网页内容和样式的关注点分离
以网页开发为例,我们通常使用 HTML 组织文档内容,用 CSS 控制样式风格,它们一般位于不同文件中。这种分离使得内容和样式相互独立,便于单独修改和维护。
.highlight {
color: red;
font-size: 18px;
}
.reference {
color: grey;
font-size: 16px;
}
<p class="highlight">Neque porro quisquam</p>
<ul>
<li class="highlight">qui dolorem ipsum quia</li>
<li class="reference">consectetur, adipisci</li>
</ul>
如果不遵循关注点分离原则,代码可能会变成这样:
<p style="color: red; font-size: 18px;">Neque porro quisquam</p>
<ul>
<li style="color: red; font-size: 18px;">qui dolorem ipsum quia</li>
<li style="color: grey; font-size: 16px;">consectetur, adipisci</li>
</ul>
在这种情况下,样式代码直接嵌入 HTML 中,导致可读性和可维护性变差。样式和结构紧密耦合也导致难以复用样式,需要修改样式时,必须在 HTML 内容中逐个查找并修改 style
属性。
命令执行和结果输出的关注点分离
这是一个真实的 Rust CLI 应用的例子,程序需要处理不同的子命令(如 new
、list
、remove
),每个子命令都支持 json
和 text
两种输出格式。
在最初的实现中,子命令的执行逻辑和结果输出逻辑混合在一起,导致代码难以阅读和维护。
fn main() {
let sub_command = parse_args();
let format = parse_format_option();
match sub_command {
SubCommand::New => {
if let Ok(data) = execute_new_command() {
match format {
"json" => println!(r#"{{"status": "success", "data": "{}"}}"#, data),
_ => println!("Success: {}", data),
}
} else {
match format {
"json" => eprintln!(r#"{{"status": "error", "message": "{}"}}"#, "Failed to execute new command"),
_ => eprintln!("Error: Failed to execute new command"),
}
}
}
SubCommand::List => {
if let Ok(data) = execute_list_command() {
match format {
"json" => println!(r#"{{"status": "success", "data": {}}}"#, data),
_ => println!("List: {}", data),
}
} else {
match format {
"json" => eprintln!(r#"{{"status": "error", "message": "{}"}}"#, "Failed to execute list command"),
_ => eprintln!("Error: Failed to execute list command"),
}
}
}
SubCommand::Remove => {
if let Ok(data) = execute_remove_command() {
match format {
"json" => println!(r#"{{"status": "success", "data": "{}"}}"#, data),
_ => println!("Success: {}", data),
}
} else {
match format {
"json" => eprintln!(r#"{{"status": "error", "message": "{}"}}"#, "Failed to execute remove command"),
_ => eprintln!("Error: Failed to execute remove command"),
}
}
}
}
}
我们通过以下重构来实现关注点分离:
- 创建
ExecutionResult
枚举来表示命令执行的结果。 - 将子命令的执行逻辑提取到
execute_command
函数中,返回ExecutionResult
。 - 将结果输出逻辑分别提取到
print_json
和print_text
函数中,各自根据ExecutionResult
的类型进行不同的处理。 - 在
main
函数中调用execute_command
执行子命令,并根据用户选择的格式调用相应的输出函数。
fn main() {
let sub_command = parse_args();
let format = parse_format_option();
let execution_result = execute_command(sub_command);
match format {
"json" => print_json(execution_result),
_ => print_text(execution_result),
}
}
fn execute_command(sub_command: SubCommand) -> ExecutionResult {
match sub_command {
SubCommand::New => execute_new_command(),
SubCommand::List => execute_list_command(),
SubCommand::Remove => execute_remove_command(),
}
}
enum ExecutionResult {
Success(String),
List(Vec<String>),
Error(String),
}
fn print_json(result: ExecutionResult) {
match result {
ExecutionResult::Success(data) => {
println!(r#"{{"status": "success", "data": {}}}"#, data);
}
ExecutionResult::List(data) => {
println!(r#"{{"status": "success", "data": {}}}"#, data);
}
ExecutionResult::Error(err) => {
eprintln!(r#"{{"status": "error", "message": "{}"}}"#, err);
}
}
}
fn print_text(result: ExecutionResult) {
match result {
ExecutionResult::Success(data) => {
println!("Success: {}", data);
}
ExecutionResult::List(data) => {
println!("List: {}", data);
}
ExecutionResult::Error(err) => {
eprintln!("Error: {}", err);
}
}
}
经过重构,命令执行和结果格式化输出这两个关注点被明确分离开来,代码可读性显著提升,更易于维护和扩展。
业务逻辑和控制逻辑的关注点分离
陈皓经常在他的文章中提到的“业务逻辑和控制逻辑分离”其实就是一种关注点分离的实践。3 4 5
编程时如何实现关注点分离
我时常会遇到这样的困境:面对一段杂乱无章的代码,虽然能够识别出不同的关注点,却绞尽脑汁也想不出合适的分离方式。
如果换个角度分析,从“如何把这些关注点分离开”转为思考“假设这些关注点已经分离,我该如何把分离的部分连接起来”,就更容易想出解决方案。因为编程时大部分工作就是把不同的组件组合起来,我们已经充分练习过“连接分离的部分”这件事,做起来更加得心应手。
这其实是对“倒推法”的应用,从预期的结果出发,假设各个关注点已经被分离开,把“分离”的任务变成我们更加熟悉的“组合”任务,问题的难度就会大大降低。