尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

同一个需求为何有多种写法?多一种范式,多一种解决问题的视角

同一个需求为何有多种写法?多一种范式,多一种解决问题的视角 很多开发者写代码都有一个习惯一种写法一旦用熟了就再也不换。需求来了第一个想到的不是“这个需求适合什么写法”而是“我上次是怎么写的”。这种习惯在 CRUD 项目里问题不大但一旦遇到数据量上涨、需求频繁变化、团队协作规模变大问题就会集中爆发出来。这篇文章想讨论一个很朴素但经常被忽略的问题同一个需求为什么会有多种写法多掌握一种写法到底有没有价值我的判断是多一种写法不是多一个可以炫技的姿势而是多一种理解问题的方式。在真实项目里很多“代码很难维护”“框架用不明白”“SQL 写不出来”的困局根源往往都是只会一种写法不知道原来还可以换一个角度表达同一段逻辑。为了不让讨论停留在抽象层面这篇文章会从一个贯穿全文的单词统计需求出发分别从多语言、多范式、多框架、多配置格式、多 SQL 风格几个维度来拆解“写法”这件事。你会发现同一个功能不同语言有各自的惯用法同一种语言不同风格背后是不同思维模型同一个框架注解和编程式写法解决的是不同问题。1. 为什么“会写代码”和“会很多种写法”是两回事先说一个很容易被误解的结论不要为了“显得厉害”去学很多种写法但一定要为了“看清问题”去理解不同写法。一段代码的写法本质上是一种思维模型的映射。你用什么方式组织数据流用什么方式处理状态用什么方式表达约束最后都会反映在代码结构上。只会一种写法意味着你只有一个思维模型。问题一旦超出这个模型的表达边界你就会觉得“框架不好用”“语言不行”“需求不合理”但实际上可能只是缺少另一种表达方式。常见的基础范式至少有这几种范式关注点典型特征代表技术命令式怎么做一步步操作改变状态循环、条件判断、赋值面向对象数据和行为的归属对象封装状态对象之间协作Java、C、Python 类函数式数据流和变换不可变数据函数作为参数和返回值Java Stream、Python map/filter、JS reduce声明式要什么只描述目标不关心执行步骤SQL、正则表达式、配置声明这四种范式并不互斥。一个工程里往往命令式写业务控制流面向对象组织领域模型函数式处理集合数据SQL 做数据查询。如果你只会其中一种遇到其他代码时就会产生“读不懂”“看不懂为什么这么写”的挫败感。写法的选择还会直接影响工程指标。可读性决定了别人接手代码的效率可复用性决定了逻辑变更时改一个地方还是改十个地方可测试性决定了单元测试能不能把逻辑隔离出来性能虽然重要但很多时候写法的差异并不会带来数量级变化反而是业务场景决定了必须用哪种写法。把这些问题放在一起看多掌握一种写法实际上是给自己增加了一个判断维度。2. 同一个需求不同语言的写法对比为了把问题说清楚我们用同一个非常简单的需求作为例子给定一组英文单词统计每个单词出现的次数并输出次数最多的前 3 个单词及次数。这个需求不大但恰好能体现不同语言对“循环、聚合、排序”这三个环节的不同组织方式。2.1 Python 写法Python 程序员最习惯的写法是“字典 循环”words [apple, banana, apple, orange, banana, apple, grape] counter {} for word in words: if word not in counter: counter[word] 0 counter[word] 1 top3 sorted(counter.items(), keylambda item: item[1], reverseTrue)[:3] print(top3)这段代码的逻辑非常直白遍历单词累加次数按次数从高到低排序取前三个。初学 Python 时几乎所有人都会这么写。但 Python 的标准库为这个场景提供了更简洁的写法from collections import Counter words [apple, banana, apple, orange, banana, apple, grape] top3 Counter(words).most_common(3) print(top3)用Counter之后聚合统计和排序取 Top N 都变成了一个方法调用。两种写法结果完全一样但第二段代码明显更聚焦“你要统计什么”而不是“你如何一步步统计”。这里想说明的是第一段代码不是错的第二段代码也不是银弹。但在团队里如果每个人都只熟悉自己的那一种写法代码风格会非常割裂。有人用循环有人用Counter有人用defaultdict最终维护成本会落在后来接手的人身上。2.2 Java 写法Java 8 之前的传统写法是HashMap 循环import java.util.*; public class WordCount { public static void main(String[] args) { ListString words Arrays.asList(apple, banana, apple, orange, banana, apple, grape); MapString, Integer counter new HashMap(); for (String word : words) { counter.put(word, counter.getOrDefault(word, 0) 1); } ListMap.EntryString, Integer sorted new ArrayList(counter.entrySet()); sorted.sort((a, b) - b.getValue().compareTo(a.getValue())); for (int i 0; i Math.min(3, sorted.size()); i) { System.out.println(sorted.get(i)); } } }Java 8 引入 Stream 之后写法的思维模型发生了变化import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; public class WordCountStream { public static void main(String[] args) { ListString words Arrays.asList(apple, banana, apple, orange, banana, apple, grape); words.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() .stream() .sorted(Map.Entry.String, LongcomparingByValue().reversed()) .limit(3) .forEach(System.out::println); } }Stream 版本把“分组、计数、排序、截断、输出”串联成一条数据流。你看代码时重点会放在“数据经历了几次变换”上而不是“变量什么时候赋值、循环到第几次”。这是命令式思维和函数式思维最明显的差异。但 Stream 也不是万能的。如果聚合逻辑复杂、需要抛出受检异常、或者需要中途调试传统循环反而更容易插入日志和断点。写法的选择本质是在不同约束之间做取舍。2.3 JavaScript 写法JavaScript 很典型的一个风格是用reduce把一个数组归约成Mapconst words [apple, banana, apple, orange, banana, apple, grape]; const counter words.reduce((acc, word) { acc.set(word, (acc.get(word) || 0) 1); return acc; }, new Map()); const top3 [...counter.entries()] .sort((a, b) b[1] - a[1]) .slice(0, 3); console.log(top3);reduce和前两个语言的循环思路不太一样它把“累积状态”作为函数的第一参数显式传递每一步的返回值会成为下一步的输入。这种写法在函数式编程里叫“折叠”。第一次接触时可能会觉得不直观但一旦习惯你会发现很多“分组、汇总、累加”逻辑都能用同一个模式表达。2.4 SQL 写法如果把单词放在数据库表里比如words表只有一列word那么这个需求就是一句 SQLSELECT word, COUNT(*) AS cnt FROM words GROUP BY word ORDER BY cnt DESC LIMIT 3;SQL 和前几种写法的差异最大。你完全不需要关心数据库是怎么循环的只需要描述“我要按单词分组、统计数量、排序、取前三”。这就是声明式表达。对于复杂查询SQL 往往比等价的程序循环短得多数据库引擎还能自动选择执行计划。不过 SQL 也有自己的边界一旦查询变得非常复杂可读性会断崖式下降而且很难在代码里写完整的单元测试。更常见的情况是简单统计用 SQL复杂业务逻辑在应用层用代码写。每种写法都有它最舒适的作用域。2.5 这一节的结论同一个需求在 Python、Java、JavaScript、SQL 里至少有四种不同的组织方式。它们都能正确运行但背后思维模型完全不同。如果你只熟悉其中一门语言很容易认为“程序本来就该这么写”。看得多了才会意识到所谓优雅往往只是你熟悉的语言恰好擅长表达这种逻辑。3. 同为 Python一种语言里也有多种风格跨语言对比能看出大方向上的差异但更常见的问题发生在同一种语言内部。仍以 Python 为例一个简单的“筛选长度大于 5 的单词并转大写”需求至少有四种写法。第一种最基础的for循环words [apple, banana, orange, strawberry, grape] result [] for word in words: if len(word) 5: result.append(word.upper()) print(result)第二种列表推导式words [apple, banana, orange, strawberry, grape] result [word.upper() for word in words if len(word) 5] print(result)第三种mapfilterwords [apple, banana, orange, strawberry, grape] result list(map(str.upper, filter(lambda w: len(w) 5, words))) print(result)第四种生成器表达式适合数据量很大、不想一次性生成完整列表的场景words [apple, banana, orange, strawberry, grape] result (word.upper() for word in words if len(word) 5) for item in result: print(item)这四种写法前两种在工程里最常见第三种在函数式风格明显的代码里会出现第四种用于流式处理。这里就引出了一个真实的团队矛盾如果项目主要用for循环某个人突然提交了一段复杂的mapfilter即使运行结果正确别人读起来也会很费力。反过来一个以函数式风格为主的项目里到处是for循环和临时变量同样会显得杂音很多。所以在真实的工程里“会不会多种写法”只是第一层问题第二层问题是“在哪个范围内统一使用哪种写法”。很多人误以为写法越高级越好但实际上团队可读性优先级高于个人偏好。高级写法应该出现在它能清晰表达意图的地方而不是出现在所有能使用它的地方。再看一个容易踩坑的例子。用字典推导式做词频统计可以写成words [apple, banana, apple, orange, banana, apple, grape] result {word: words.count(word) for word in set(words)} print(result)这段代码能跑但count每次都会遍历一次words列表时间复杂度是 O(n²)。数据量小的时候毫无感知数据量一大就非常慢。这说明多会一种写法不等于每种写法都适合当前场景。写法越简洁越要确认它背后的计算方式。4. 框架层面的写法差异声明式 vs 编程式如果说语言层面讨论的是“语法风格”那框架层面讨论的就是“谁控制流程”。这一点在 Java 后端框架里体现得特别明显。4.1 Spring 事务注解与 TransactionTemplate很多 Java 开发者第一次接触 Spring 事务都是从Transactional注解开始的Service public class OrderService { private final OrderRepository orderRepository; private final AccountRepository accountRepository; public OrderService(OrderRepository orderRepository, AccountRepository accountRepository) { this.orderRepository orderRepository; this.accountRepository accountRepository; } Transactional public void createOrder(Order order, Long accountId) { orderRepository.save(order); Account account accountRepository.findById(accountId) .orElseThrow(() - new RuntimeException(account not found)); account.setBalance(account.getBalance().subtract(order.getAmount())); accountRepository.save(account); } }注解方式的优点是很明显的代码看起来干净事务边界由框架在方法调用前开启、方法结束后提交或回滚。你只需要声明“这个方法需要事务”不用关心事务怎么创建。但注解方式也有一个经典陷阱当同一个类里的方法通过this调用另一个带有Transactional的方法时事务不生效。因为 Spring 事务是基于 AOP 代理实现的内部自调用不会经过代理对象。很多人遇到“明明加了事务数据半成功半失败”的问题根源就在这里。同样的事务逻辑用编程式事务可以这样写Service public class OrderService { private final TransactionTemplate transactionTemplate; private final OrderRepository orderRepository; private final AccountRepository accountRepository; public OrderService(TransactionTemplate transactionTemplate, OrderRepository orderRepository, AccountRepository accountRepository) { this.transactionTemplate transactionTemplate; this.orderRepository orderRepository; this.accountRepository accountRepository; } public void createOrder(Order order, Long accountId) { transactionTemplate.execute(status - { orderRepository.save(order); Account account accountRepository.findById(accountId) .orElseThrow(() - new RuntimeException(account not found)); account.setBalance(account.getBalance().subtract(order.getAmount())); accountRepository.save(account); return null; }); } }TransactionTemplate的优点是你清楚地看到事务在哪里开始、在哪里结束也可以更灵活地处理异常和回滚条件。缺点是业务代码被包在 lambda 里如果方法很长嵌套层级会变深。这里想强调的不是哪一个更好而是注解和编程式是两种控制流模型的写法。注解把“控制权”交给框架编程式把“控制权”留在代码里。理解这一点后遇到“注解不生效”“事务回滚不按预期”等问题时你至少会有一个排查方向而不是只会搜“Spring 事务失效的 10 个原因”。4.2 MyBatisXML、注解、WrapperMyBatis 的 SQL 写法也经历了类似的过程。最传统的是 XML 方式!-- mapper/OrderMapper.xml -- select idcountByStatus resultTypeint SELECT COUNT(*) FROM orders WHERE status #{status} /select对应 Mapper 接口public interface OrderMapper { int countByStatus(Param(status) String status); }XML 方式的优点是 SQL 和 Java 代码分离复杂 SQL 容易格式化动态 SQL 用if、where也足够灵活。缺点是每加一个查询都要在 XML 里维护一段文件多了以后项目里会出现一堆零散 XML。简单查询可以换成注解public interface OrderMapper { Select(SELECT COUNT(*) FROM orders WHERE status #{status}) int countByStatus(Param(status) String status); }注解方式把 SQL 和接口放在一起看代码时不需要跳文件。但动态 SQL 一旦复杂注解里的 Java 字符串拼接会非常难读。如果项目再用上 MyBatis-Plus 这类增强工具还会看到基于 Lambda 的 Wrapper 写法long count orderMapper.selectCount( new LambdaQueryWrapperOrder().eq(Order::getStatus, status) );Wrapper 的写法连 SQL 字符串都省了属于“类 SQL 的 Java DSL”。它适合简单查询但遇到窗口函数、复杂嵌套子查询、多表关联时还是得回到 XML 或注解。看出规律了吗框架层面的多种写法实际上是在不同控制粒度之间移动XML 把 SQL 完全独立出来注解把 SQL 和接口绑定Wrapper 把 SQL 变成 Java API。你选择哪一种取决于你的项目里哪一部分最容易变化、最需要被检查、最需要被复用。这里真正值得思考的是框架为什么要提供多种写法因为不同使用者、不同业务复杂度、不同维护阶段对“控制权放在哪里”的需求不一样。新项目可以先从注解和 Wrapper 快速迭代SQL 复杂到注解影响可读性时再抽取到 XML。这不是风格问题而是工程决策。5. 同一份配置多种书写格式写法的差异不只体现在代码上配置文件的格式选择同样是一个反复出现的争论点。假设我们要配置一个数据源常见格式就有好几种。propertiesspring.datasource.urljdbc:mysql://localhost:3306/demo spring.datasource.usernameroot spring.datasource.passwordsecret spring.datasource.driver-class-namecom.mysql.cj.jdbc.DriverYAMLspring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: secret driver-class-name: com.mysql.cj.jdbc.DriverJSON{ spring: { datasource: { url: jdbc:mysql://localhost:3306/demo, username: root, password: secret, driver-class-name: com.mysql.cj.jdbc.Driver } } }注意Spring Boot 原生默认并不直接用 JSON 作为主配置格式但在配置中心、云平台和部分工具链中JSON 是常见的数据交换格式。这里把它作为“同一种结构化数据不同表达载体”的例子来看。这三种格式描述的是同一份信息但差异很实际properties结构扁平写起来简单但一旦配置层级多前缀会重复很长而且天然不支持复杂嵌套。YAML通过缩进表达层级可读性最好适合人类维护。但缩进错误非常隐蔽有些工具对 Tab 和空格的处理不一致。JSON被大多数编程语言和 API 天然支持程序解析最稳定但手写配置时更容易出现逗号、括号错误。很多团队在配置格式上产生分歧其实不是因为某种格式“不行”而是因为配置的使用场景不同。如果配置需要被自动化工具生成JSON 更友好如果配置主要靠人读、人改YAML 更合适如果配置非常简单、希望使用方零学习成本properties也是一种稳妥选择。这里还牵出一个安全提醒无论使用哪种格式不要把数据库密码、密钥、Token 直接写在配置文件里并提交到代码仓库。配置里的密码应使用环境变量或专用配置中心管理。这个原则比“你选 YAML 还是 JSON”重要得多。6. 数据库查询从“能查出来”到“查得好”SQL 的写法差异可能是所有技术里最容易被低估的。同一个查询不同写法可能结果相同但可读性、性能、扩展性差异巨大。先看一个常见需求查询 2024 年 1 月 1 日之后每个分类下订单数量大于 10 的分类。最简单的写法是GROUP BYSELECT category, COUNT(*) AS order_cnt FROM orders WHERE created_at 2024-01-01 GROUP BY category HAVING COUNT(*) 10 ORDER BY order_cnt DESC;这段 SQL 直白、好读也是绝大多数开发者最熟悉的写法。但如果需求变成“查询每个分类中下单金额最大的那笔订单”GROUP BY就不太好写了。一个经典的处理方式是用子查询SELECT category, order_id, amount FROM ( SELECT category, order_id, amount, ROW_NUMBER() OVER (PARTITION BY category ORDER BY amount DESC) AS rn FROM orders ) t WHERE rn 1;这里使用了窗口函数ROW_NUMBER()在分类内部按金额排序并编号然后只保留每个分类的第一行。这个写法解决了一个原先很难表达的问题既要分组、又要保留组内明细。换成 CTE 后可读性更清晰WITH ranked_orders AS ( SELECT category, order_id, amount, ROW_NUMBER() OVER (PARTITION BY category ORDER BY amount DESC) AS rn FROM orders ) SELECT category, order_id, amount FROM ranked_orders WHERE rn 1;CTE 和子查询执行逻辑通常等价但 CTE 把“先排序编号”这一步提取成命名片段读起来更接近业务流程。如果你再用 Python ORM 来表达同一个需求风格又不一样from sqlalchemy import func, select from sqlalchemy.orm import Session # 查询每个分类的订单数量这里示意分组统计写法 stmt ( select(Order.category, func.count().label(order_cnt)) .where(Order.created_at 2024-01-01) .group_by(Order.category) .having(func.count() 10) .order_by(func.count().desc()) ) with Session(engine) as session: for row in session.execute(stmt): print(row)对比之后会看到手写 SQL 更贴近数据库执行逻辑ORM 写法更贴近语言类型系统和代码协作习惯。没有高低之分关键是你在这个项目里更重视哪一点。如果查询复杂、需要多种数据库兼容手写 SQL 更可控如果希望查询条件在 Java/Python 里动态拼接、避开字符串拼接的坑ORM 的 Query 对象往往更合适。这里必须提醒一个生产环境问题窗口函数、CTE 等写法的可用性取决于数据库版本。MySQL 8.0 才支持窗口函数MySQL 5.7 会直接报语法错误。再好的写法也要先确认线上数据库版本支持而不是在代码评审时凭感觉说“这个写法更高级”。7. 常见误区与排查思路关于“不要局限于一种写法”最常见的不是“不会写”而是“写偏了”。我整理了几个高频误区和排查方向。问题现象可能原因排查方式解决方案代码风格五花八门每个人写的看起来都不像同一个项目团队缺少统一约定写法人各一套抽查历史提交统计同一逻辑出现的写法数量制定编码规范Code Review 时对齐写法用新写法后性能反而变差新写法只是语法简洁底层复杂度不同用基准测试对比旧写法与新写法保留性能测试数据必要时回退到传统写法照搬框架新特性后启动失败依赖版本不支持该写法查看框架版本和官方文档支持矩阵升级依赖版本或改用兼容版本的等价写法Transactional没有生效同一类内this自调用绕过代理检查被调用处是否通过注入对象调用拆分类或改用TransactionTemplate窗口函数在线上报语法错误数据库版本过低不支持新语法查看数据库版本与官方语法文档改写为 JOIN 或子查询配置格式反复切换导致冲突团队对配置格式没有明确约定查看配置文件和提交记录统一配置格式敏感配置走配置中心除了表格里的技术问题还有三个更隐蔽的思维误区。第一个误区是认为新写法一定比旧写法好。Java 8 Stream 确实让集合处理更简洁但在循环次数极高的场景下Stream 的装箱和中间操作会带来额外开销。写法没有银弹只有更适合当前场景的选择。第二个误区是只学写法不学写法背后的限制条件。看到一个优雅的写法要先问三个问题它依赖什么版本它是否改变了控制流它在什么场景下会失效比如Transactional之所以失效正是因为代理机制的存在。不了解限制只抄写代码迟早会在某个奇怪的问题上卡住。第三个误区是把“多种写法”理解成“随时随便换写法”。在个人练习项目里今天写循环、明天写推导式、后天用reduce没问题。但在生产项目里写法一致性价值高于个人偏好。引入新写法前要考虑团队里其他人是否熟悉、是否能在 Code Review 时发现问题。8. 最佳实践与工程建议既然多种写法是客观存在且无法回避的真正的问题就不是“要不要学”而是“如何让多种写法成为团队资产而不是维护负担”。第一先绑定一个基准写法。每个语言、每个框架、每个项目都应该确定一套默认写法。例如“Java 后端默认使用 Service 层 Mapper 接口 XML SQL”“Python 数据脚本默认使用 Python 3 类型注解 列表推导式”。有了基准大家写出来的代码至少有一个共同底色。在此基础上确实有更合适的写法时通过 Code Review 和讨论去调整而不是每个人随手用自己偏好的风格。第二Code Review 时多问一个“为什么不用另一种写法”。这句话不是为了否定别人的实现而是为了暴露思维盲区。看到 Stream 代码时可以问“这里为什么不直接用循环”看到for循环时可以问“这里能不能用推导式更清晰地表达意图”。技术评审最怕的不是意见不一致而是大家都默认只有一种写法没人思考过其他选择。第三新写法引入要走小范围验证流程。如果想在项目里引入一种新的框架写法不要直接在核心模块全部重写。先选一个非核心、逻辑简单、依赖少的模块试点。写完后让团队里不熟悉这种写法的人尝试阅读和修改观察是否真的降低了维护成本。如果只是“写的时候很爽维护的人很痛苦”就不应该推广。第四保持配置和 SQL 的“最小惊讶原则”。配置文件格式一旦选定不要因为“换个格式更流行”就频繁切换。SQL 同样如此。某个查询已经有稳定的写法时除非性能问题确实存在否则不要为了展示窗口函数而重写一个能正常工作的查询。工程代码是团队协作的产物不是个人技巧展示台。第五把写法训练放进日常学习。不需要专门开一门课最简单的做法是每月选一个你最近写过的函数用另一种范式重写一遍。不一定要提交到生产项目单独写一个练习文件记下两种写法在可读性、行数、调试难度上的差异。坚持一段时间后你对“哪种写法更适合什么场景”会有直觉。还有一个容易被忽视的点文档和注释应该解释“为什么选择这种写法”。有经验的工程师都知道代码里最难沟通的不是“做了什么”而是“为什么不改成另一种做法”。当你在代码里选择了一种看起来不那么主流的写法或者刻意没有用某种写法时简短在注释里写一句原因能省去后续很多讨论。9. 你可以这样开始练习与其收藏很多“XX 写法大全”的文章不如亲手做一个对比练习。这里提供三个适合不同阶段的练习思路。第一个练习是改写。找一个你近期写过的、逻辑完整的函数分别用命令式、函数式、声明式三种思维重写。如果是一个 Python 函数可以分别写循环版、推导式版、map/filter版。如果是一个 Java 方法可以写普通循环版和 Stream 版。重点是观察同一个逻辑在不同写法下的代码行数、临时变量数量、调试难易程度。第二个练习是记录取舍。每次改写后用几句话记录你感受到的差异。比如“循环版调试方便因为临时变量可见Stream 版表达更紧凑但断点里看不到中间状态。如果条件是极端性能场景我会选循环。”把这些记录沉淀成自己的判断而不是跟着网上争论随风倒。第三个练习是在 Code Review 里主动提问。下次评审别人代码时如果对方和你用了不同写法先不要急着说“这个写法不好”。先问一句“你选这个写法主要考虑的是什么”很多时候你会发现对方的判断来自你没想到的场景。这种对话本身就是写法治愈团队分歧的方式。我做技术分享和团队协作时经常会引用一句话不要让你的技能栈窄化你的思考方式。编程语言和框架提供的每一种写法都是一扇窗口让你看到同样的问题还可以如何被建模。多掌握一种写法不是为了让代码更炫而是让你在面对复杂业务时手里多一个解决问题的角度。下次拿到需求时可以先停下来想一想这个需求除了我最熟悉的写法还有没有第二种、第三种表达方式如果你能清晰回答“为什么最终用这种写法”那“多写法”对你来说就不再是焦虑而是实实在在的工程能力。
返回列表