尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Getting Real:告别过度设计,用MVP实现业务系统快速迭代

Getting Real:告别过度设计,用MVP实现业务系统快速迭代 在做业务系统迭代的时候我们经常陷入一种“先做大而全的设计再开始写代码”的惯性里。需求文档越写越长架构图越画越复杂数据库表设计恨不得提前规划三年后的业务形态。结果往往是项目迟迟无法上线或者上线后才发现用户真正需要的根本不是这套东西。《Getting Real》的核心观点恰恰相反不要试图预测未来不要为想象中的复杂场景提前买单而是回到真实的需求、真实的用户、真实的反馈里用最简的方案快速做出一个能用的产品然后根据真实数据持续修正。它不是鼓励你“不设计”而是鼓励你把设计建立在真实信息之上而不是建立在假设之上。这篇文章会把“Getting Real”这套理念拆成可落地的工程实践围绕最小可行产品MVP、简化技术栈、快速反馈循环三个方向展开。我会用一个真实的业务场景“团队周报提醒服务”完整演示第一版只做单文件脚本第二版引入数据库第三版提供 API 接口每一步都解释“为什么现在只需要做这么多”还会整理一份常见过度设计清单和排查思路。适合正在做业务系统、想提升交付速度、又担心代码质量失控的后端开发同学。1. 背景与核心概念“Getting Real”最早来自 37signals也就是后来的 Basecamp提出的一套产品与开发理念。简单说它的核心主张是规模越小、流程越短、离用户越近做出的东西就越贴合真实需要。翻译成技术语言可以从几个角度理解真实需求优先于想象需求。不要用“未来肯定会用到”作为设计理由需求只有发生的时候才是真实的。真实数据优先于模拟数据。用真实用户的行为数据、真实日志、真实报错来驱动功能调整。真实反馈优先于完美计划。与其花三个月做一次完美发布不如花两周上线一个最简版本然后根据反馈快速迭代。很多团队把“Getting Real”误解为“不好好设计、直接写代码”这其实是两种不同的东西。对照一下传统重量级流程和务实流程的区别对比维度重量级流程Getting Real 式流程需求来源靠前期调研和文档推演靠真实使用和反馈数据第一个版本完整功能集只解决一个核心痛点架构设计提前规划微服务、消息队列、分布式事务先单体够用为止上线时间半年到一年几周甚至几天技术栈追求最新最全追求熟悉、稳定、够用失败代价投入巨大难以回退可以快速推翻重来从工程实践角度看Getting Real 不是某一门编程语言、某一个框架而是一套决策方法。它的价值在项目初期和后期的维护阶段都很大前期帮你控制范围后期帮你降低认知负担。2. 环境准备与版本说明为了让下面的示例能直接跑起来也为了让文章里的代码有一个具体的运行基础先统一一下环境。本文的示例代码以Python 3.10为例采用FastAPI作为 Web 框架SQLite作为存储。选择这套组合的原因很简单生态成熟、上手成本低、单机部署足够支撑中小业务非常契合 Getting Real 的“够用就好”原则。版本不能写死因为 Python 和相关库的迭代速度较快你需要根据自己机器的实际环境调整。建议使用虚拟环境隔离依赖避免污染系统 Pythonmkdir weekly-report-reminder cd weekly-report-reminder python3 -m venv venv source venv/bin/activate pip install fastapi uvicorn如果你的网络环境比较特殊或者 pip 安装较慢可以换成国内镜像源pip install fastapi uvicorn -i https://pypi.tuna.tsinghua.edu.cn/simpleSQLite 是 Python 标准库自带的不需要额外安装。你只需要确认当前环境能正常执行 Python 即可python3 --version最终的项目结构建议如下weekly-report-reminder/ ├── venv/ ├── app.py # 第三版API 服务入口 ├── reminder.py # 核心业务逻辑 ├── storage.py # SQLite 持久化层 ├── schedule.py # 定时触发脚本第一版 ├── requirements.txt └── data/ └── reminder.db # 运行时自动生成不要求结构一次性到位后续的实战案例会按照版本演进逐步补充文件。这正是 Getting Real 的实践方式先有一个能跑的东西再根据真实需要长出一个合理的结构。3. 核心原则拆解从“过度设计”到“真实需求”在开始写代码之前先拆解 Getting Real 在工程实践中最重要的三条原则。理解这三条原则后面的代码示例才会有依据而不是“为了简单而简单”。3.1 最小可行版本只做当前必须做的事最小可行版本Minimum Viable ProductMVP的意思是用最少的功能集合验证一个核心假设。举个例子需求是“用户注册后发送欢迎邮件”。很多团队的第一反应是引入消息队列用户注册后发送事件消费者异步消费失败重试甚至考虑分布式事务。但实际上在你还没有每天上千封邮件的时候一个同步发送函数加错误日志完全够用。再举个更具体的例子需求是“团队周报提醒”。第一版其实只需要做到一件事每周五下午给指定成员发送提醒文本。不需要用户管理、不需要权限系统、不需要报表统计。MVP 的边界判断可以借助一个简单问题“如果砍掉这个功能用户还能不能完成核心任务”如果答案是可以那就先不做。3.2 简单技术栈每增加一个组件都要有理由技术选型是过度设计的高发区。很多系统在初期就引入了注册中心、配置中心、网关、消息队列理由是“到时候一定会用到”。但真实情况是大部分系统在很长一段时间内用一台服务器加一个数据库就能跑得很好。每引入一个中间件都会带来额外的运维成本、学习成本和故障排查成本组件版本是否兼容是否需要单独部署和监控团队是否熟悉出了问题定位链路是否变长因此技术栈应该遵循“需要时才引入”的原则。例如单机进程内定时任务能满足需求就不需要分布式调度平台。SQLite 或者单表 MySQL 能满足需求就不需要分库分表。REST API 能满足需求就不需要上复杂的 RPC 框架。团队 10 个人以内、代码量不大就不需要强行拆微服务。这里要给一个“代码对比”帮助理解务实和过度设计的区别。假设需求是“向用户发送欢迎邮件”# over_engineered.py —— 过度设计示意不推荐 # 为了发送一封邮件引入事件、消息队列、多个消费者 class UserCreatedEvent: def __init__(self, user_id: int): self.user_id user_id class EventBus: def publish(self, event): # 实际上线时需要连接 Kafka/RabbitMQ pass def send_welcome_email(user_id: int): event UserCreatedEvent(user_id) EventBus().publish(event) # 消费者是另一个服务需要单独部署 def handle_user_created(): pass# simple_send_mail.py —— 务实版本推荐先用这个 import smtplib import logging logger logging.getLogger(__name__) def send_welcome_email(user_email: str) - None: try: # 这里调用邮件 SDK 发送即可出错就记录日志 print(fsending welcome email to {user_email}) except Exception: logger.exception(send welcome email failed, user%s, user_email)对比很明显过度设计的版本把“发邮件”这个简单动作拆成了事件、队列、消费服务三个环节每个环节都要部署和运维。而务实版本就是一行调用加错误日志。等真的到了每天几千封邮件的量级再把发送动作放到队列里也不迟而且这个迁移非常局部。3.3 真实反馈循环让使用数据指导下一步Getting Real 强调“学习和用户一起”。这意味着你需要在第一个版本上线后就埋好基本的日志和数据采集关注真实使用情况用户有没有使用这个功能使用频率如何在哪个步骤流失了有没有产生报错很多团队花了很长时间做了一堆“看起来有用”的功能结果上线后没人用。原因就是前期的验证不够所有决策都建立在会议室里的假设之上。反馈循环的具体做法是上线一个最简版本 → 观察真实数据 → 和用户交流 → 调整方向 → 再发布下一个版本。节奏可以是每周一次而不是每季度一次。4. 完整实战案例一个团队周报提醒服务的演进下面用一个非常常见的场景来演示 Getting Real 的落地过程。背景是团队每周五需要提交周报但总有成员忘记填写希望有一个服务能自动提醒。这个案例会分成三个版本版本一命令行脚本输出需要提醒的成员名单。版本二增加 SQLite 存储记录提醒历史和成员订阅方式。版本三提供 HTTP API便于前端页面或其他系统接入。每个版本只解决当前最痛的问题不提前引入不必要的复杂度。4.1 场景与需求梳理先整理真实需求不要急着写代码当前痛点周五经常有人忘记写周报。核心用户团队负责人或者每个成员自己。最简可行功能周五定时产生一条提醒信息。暂时不做周报内容填写、自动汇总、评分统计、移动端 App。基于这些需求第一版甚至都不需要数据库一个纯脚本就够了。4.2 版本一单文件脚本验证提醒逻辑创建schedule.py逻辑很简单读取一个成员名单打印提醒文本。# 文件路径weekly-report-reminder/schedule.py from datetime import datetime # 第一版先用硬编码成员名单后续再替换为数据库或配置 MEMBERS [ zhangsanexample.com, lisiexample.com, wangwuexample.com, ] def build_reminder(member: str) - str: current_week datetime.now().isocalendar()[1] return f【周报提醒】今天是第 {current_week} 周的周五请记得在 18:00 前提交本周周报。 def run() - None: for member in MEMBERS: message build_reminder(member) # 第一版先用 print 模拟发送后面替换为真实邮件/钉钉/企业微信通知 print(fsend to {member}: {message}) if __name__ __main__: run()运行验证python schedule.py预期输出类似于send to zhangsanexample.com: 【周报提醒】今天是第 12 周的周五请记得在 18:00 前提交本周周报。 send to lisiexample.com: 【周报提醒】今天是第 12 周的周五请记得在 18:00 前提交本周周报。 send to wangwuexample.com: 【周报提醒】今天是第 12 周的周五请记得在 18:00 前提交本周周报。这个版本虽然简单但它验证了一个关键假设团队成员名单和提醒文案的组合是否能满足提醒需求。如果这个时候发现提醒的方式不是邮件而是企业微信更合适改起来成本极低。4.3 版本二引入 SQLite记录提醒历史版本一上线跑了一周之后团队负责人提出一个新需求希望能看到“历史提醒记录”确认每个人是否都被提醒到了。这时候才需要引入数据库。也就是说不是一开始就设计数据库表而是当“记录历史”这个真实需求出现时再自然引入。创建storage.py# 文件路径weekly-report-reminder/storage.py import sqlite3 from datetime import datetime from pathlib import Path BASE_DIR Path(__file__).resolve().parent DB_PATH BASE_DIR / data / reminder.db def get_connection(): Path(DB_PATH).parent.mkdir(parentsTrue, exist_okTrue) conn sqlite3.connect(DB_PATH) return conn def init_db() - None: with get_connection() as conn: conn.execute( CREATE TABLE IF NOT EXISTS reminder_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_email TEXT NOT NULL, message TEXT NOT NULL, created_at TEXT NOT NULL ) ) def insert_reminder_log(member_email: str, message: str) - None: with get_connection() as conn: conn.execute( INSERT INTO reminder_log (member_email, message, created_at) VALUES (?, ?, ?), (member_email, message, datetime.now().isoformat(timespecseconds)), ) def list_reminder_logs(limit: int 20): with get_connection() as conn: rows conn.execute( SELECT id, member_email, message, created_at FROM reminder_log ORDER BY id DESC LIMIT ?, (limit,), ).fetchall() return rows注意这里使用了with get_connection() as conn的上下文管理器Python 的sqlite3连接在with语句块结束时如果块内没有异常会提交事务如果出现异常会回滚。这样能避免数据写入一半的情况。修改schedule.py把发送日志写入数据库# 文件路径weekly-report-reminder/schedule.py from datetime import datetime from storage import init_db, insert_reminder_log MEMBERS [ zhangsanexample.com, lisiexample.com, wangwuexample.com, ] def build_reminder(member: str) - str: current_week datetime.now().isocalendar()[1] return f【周报提醒】今天是第 {current_week} 周的周五请记得在 18:00 前提交本周周报。 def run() - None: init_db() for member in MEMBERS: message build_reminder(member) # 第一版用 print 模拟发送 print(fsend to {member}: {message}) # 第二版新增记录提醒日志 insert_reminder_log(member, message) if __name__ __main__: run()运行验证python schedule.py python -c from storage import list_reminder_logs; print(list_reminder_logs())第二版新增的能力是可以回答“我们到底有没有给成员发过提醒”这个问题。这个能力来自真实需求而不是设计阶段凭空想象的。4.4 版本三提供 HTTP API接入内部系统又过了一段时间团队希望把提醒记录做成一个内部页面成员可以自己查看自己有没有被提醒。这时候需要一个 HTTP 接口供前端页面或者内部系统调用。创建app.py这是一个最小的 FastAPI 服务# 文件路径weekly-report-reminder/app.py from datetime import datetime from fastapi import FastAPI, HTTPException from pydantic import BaseModel, EmailStr from storage import get_connection app FastAPI(titleWeekly Report Reminder API, version0.1.0) class ReminderLogOut(BaseModel): id: int member_email: str message: str created_at: str app.get(/health) def health_check(): return {status: ok, time: datetime.now().isoformat(timespecseconds)} app.get(/reminders, response_modellist[ReminderLogOut]) def list_reminders(limit: int 20): if limit 1 or limit 100: raise HTTPException(status_code400, detaillimit must be between 1 and 100) with get_connection() as conn: rows conn.execute( SELECT id, member_email, message, created_at FROM reminder_log ORDER BY id DESC LIMIT ?, (limit,), ).fetchall() return [ ReminderLogOut(idrow[0], member_emailrow[1], messagerow[2], created_atrow[3]) for row in rows ] app.get(/reminders/{email}) def list_reminders_by_email(email: str, limit: int 20): with get_connection() as conn: rows conn.execute( SELECT id, member_email, message, created_at FROM reminder_log WHERE member_email ? ORDER BY id DESC LIMIT ?, (email, limit), ).fetchall() if not rows: raise HTTPException(status_code404, detailno reminders found for this email) return [ ReminderLogOut(idrow[0], member_emailrow[1], messagerow[2], created_atrow[3]) for row in rows ]这里使用了EmailStr需要安装email-validatorpip install email-validator但其实上面这个例子中没有实际用到EmailStr为了保持一致直接去掉这个导入依赖更稳妥。让我修正一下避免引入不必要的验证# 文件路径weekly-report-reminder/app.py from datetime import datetime from fastapi import FastAPI, HTTPException from pydantic import BaseModel from storage import get_connection app FastAPI(titleWeekly Report Reminder API, version0.1.0) class ReminderLogOut(BaseModel): id: int member_email: str message: str created_at: str app.get(/health) def health_check(): return {status: ok, time: datetime.now().isoformat(timespecseconds)} app.get(/reminders, response_modellist[ReminderLogOut]) def list_reminders(limit: int 20): if limit 1 or limit 100: raise HTTPException(status_code400, detaillimit must be between 1 and 100) with get_connection() as conn: rows conn.execute( SELECT id, member_email, message, created_at FROM reminder_log ORDER BY id DESC LIMIT ?, (limit,), ).fetchall() return [ ReminderLogOut(idrow[0], member_emailrow[1], messagerow[2], created_atrow[3]) for row in rows ] app.get(/reminders/{email}) def list_reminders_by_email(email: str, limit: int 20): with get_connection() as conn: rows conn.execute( SELECT id, member_email, message, created_at FROM reminder_log WHERE member_email ? ORDER BY id DESC LIMIT ?, (email, limit), ).fetchall() if not rows: raise HTTPException(status_code404, detailno reminders found for this email) return [ ReminderLogOut(idrow[0], member_emailrow[1], messagerow[2], created_atrow[3]) for row in rows ]启动服务uvicorn app:app --reload --host 0.0.0.0 --port 8000验证接口curl http://127.0.0.1:8000/health curl http://127.0.0.1:8000/reminders curl http://127.0.0.1:8000/reminders/zhangsanexample.com这三个版本串起来核心演进逻辑是只有真实需求出现时系统边界才扩展每个阶段都保留“推倒重来”的可能性。如果第二版发现 SQLite 不够用换 MySQL 的成本集中在storage.py一个文件里如果第三版发现内部系统只需要导出 CSV根本不需要 API 服务那也完全可以不写app.py。这就是 Getting Real 在工程落地中的价值每一个版本都是完整可用的而不是一个永远在路上的中间产物。5. 常见问题与排查思路在实践 Getting Real 的过程中团队和个人容易遇到下面几类问题。下面表格整理了问题现象、常见原因和解决思路。问题现象常见原因解决思路需求越做越多版本迟迟无法发布没有定义 MVP 边界任何人都能往里加需求冻结需求清单所有新增需求排到下个版本技术栈越来越重部署越来越复杂前期为了“未来可能用到”引入中间件清理无用组件遵循“需要时才引入”原则功能上线后没人用功能建立在假设之上没有提前验证做用户访谈、埋点跟踪小步快跑获取反馈代码结构混乱改一处坏多处为了快速交付完全放弃设计MVP 也要有基本分层核心逻辑和外部依赖分开错误日志缺失问题无法定位只做了正常流程没有处理异常在关键链路补充日志和异常捕获测试环境没问题生产环境报错开发、测试、生产配置不一致使用同一份部署脚本环境变量统一管理再重点说两个高频问题。第一个需求蔓延。这是 Getting Real 最大的敌人。解决方式不是不让别人提需求而是把所有需求放进一个“待验证池”只有满足以下条件才进入当前迭代一是和核心痛点强相关二是能快速验证一个假设三是没有它当前版本也能正常使用。每次都问这个问题需求就会收敛很多。第二个前期过度设计。很多开发者的心理是“现在不设计好以后重构成本更高”。这个担忧有一定道理但要注意重构成本和业务失败成本哪个更高。多数业务系统的重构成本远低于一个错误方向上的大版本成本。而且只要代码分层清晰、核心逻辑独立后续重构通常是局部的不会伤筋动骨。6. 最佳实践与工程建议Getting Real 并不代表不要规范。恰恰相反正因为系统规模小、交付节奏快更需要用一些基础规范来保证代码可维护性。这里整理几点实际项目中最值得关注的工程建议。6.1 代码分层核心逻辑与外部依赖解耦即便在 MVP 阶段也要保持“业务逻辑”和“外部依赖”分离。比如上面的案例中storage.py只负责数据库操作schedule.py只负责提醒业务编排app.py只负责 HTTP 接口。这样将来换数据库、换通知渠道、加前端都是局部改造。6.2 配置管理环境变量加默认值系统早期不建议引入配置中心。配置文件可以先用环境变量加默认值的方式管理。比如提醒发送时间、收件人名单变化频繁的字段都可以用环境变量覆盖export REMINDER_WEEKDAY4 export REMINDER_HOUR17 export REMINDER_MINUTE0代码里读取配置时保证有兜底默认值避免环境变量缺失导致启动失败import os REMINDER_WEEKDAY int(os.getenv(REMINDER_WEEKDAY, 4)) REMINDER_HOUR int(os.getenv(REMINDER_HOUR, 17)) REMINDER_MINUTE int(os.getenv(REMINDER_MINUTE, 0))当配置项超过十个、且需要可视化修改和灰度发布时再考虑 Apollo 或 Nacos 这类配置中心而不是一开始就上。6.3 异常处理捕获边界保留上下文编写工具脚本或接口时一定要区分“可恢复错误”和“不可恢复错误”。可恢复错误网络超时、临时连接失败可以记录日志并重试。不可恢复错误参数错误、数据格式错误应快速失败并返回明确错误信息。在 Python 中避免捕获所有异常后直接pass这会让问题完全不可见。至少要记录logger.exception保留堆栈import logging logger logging.getLogger(__name__) try: # 发送周报提醒 send_reminder(member) except Exception: logger.exception(send reminder failed, member%s, member)6.4 部署与验证保持简单可回滚MVP 阶段的部署越简单越好。如果只是一台 Linux 服务器可以用 systemd 管理常驻进程也可以用 Docker 加一条启动命令。关键是部署过程要能重复执行避免“只在某个人电脑上能跑起来”。下面是一个简单的 Dockerfile 示例FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, app:app, --host, 0.0.0.0, --port, 8000]生产环境部署前建议至少准备一条健康检查命令curl -f http://127.0.0.1:8000/health6.5 日志与监控上线第一天就埋点不要等功能出问题才补日志。第一版就该在核心路径上打日志至少包括任务触发的开始和结束时间。每个成员的发送结果。失败时的异常堆栈。请求接口的耗时和状态码。日志可以用 JSON 格式输出便于后续采集到 ELK 或 Loki 中。这里也强调一下所有涉及用户数据的日志要注意脱敏不要把完整的邮箱地址、手机号、密码等信息打印到日志里。6.6 性能优化延迟到真实瓶颈出现时Getting Real 对性能的态度是不为想象中的高并发买单。判断标准是真实数据。如果日志显示接口平均耗时已经超过 500ms或者数据库连接池被打满才需要优化。在早期加一层缓存、加一个索引往往只是让代码更复杂并不能带来真实的业务收益。7. 总结与学习路线这篇文章围绕“Getting Real”展开了从理念到落地的完整内容重点掌握三件事MVP 是边界控制工具用最小功能集验证核心假设让需求蔓延没有可乘之机。技术栈是成本决策每新增一个中间件、一个框架、一个服务都在增加运维成本和认知负担必须等真实需求出现再引入。反馈循环是方向保障用真实数据和用户交流驱动迭代而不是用会议室的猜测驱动开发。对于想继续深入的同学建议按照这个顺序学习先练习“把一个业务需求拆成可以两三天上线的 MVP”。再学习“演进式架构”了解如何在系统成长过程中保持结构清晰。然后学习持续集成和持续交付把小步快跑变成团队协作习惯。最后可以了解领域驱动设计但要注意它是解决复杂业务建模的工具不应该成为所有项目的默认起点。在实际项目中优先级最高的风险并不是“代码不够优雅”而是“方向错了”。与其在错误的方向上把架构做得无比工整不如先用一个简单版本去获取真实反馈。下一周做迭代时试着把一个功能砍得更小或者把一个预想中“迟早会用到”的技术栈去掉你会明显感受到交付节奏的变化。如果这篇文章对你有帮助可以收藏备用。你在实践 Getting Real 时踩过哪些坑或者有哪些“砍掉复杂设计反而更快上线”的经历欢迎在评论区交流。
返回列表