[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"\u002Farticles\u002Fdesign-pattern-33":26,"\u002Farticles\u002Fdesign-pattern-33\u002Frelated":43,"\u002Farticles\u002Fdesign-pattern-33\u002Fadjacent":72,"\u002Fcategories":91,"render-design-pattern-33":117},{"code":4,"message":5,"data":6},0,"ok",{"author_city":7,"author_years":8,"site_tagline":9,"site_description":10,"skill_one_desc":11,"author_status":12,"skill_two_title":13,"about_hero_image":9,"skill_one_title":14,"icp":15,"author_role":16,"og_default_image":9,"closing_quote_date":17,"toolkit":18,"skill_two_desc":19,"author_bio":9,"site_footer":9,"site_url":20,"author_tagline":21,"weekly_thought":9,"tagline_line_one":22,"author_bio_two":9,"closing_quote":23,"site_name":24,"tagline_line_two":25,"home_hero_image":9},"西安","10","","花开空白 - 个人博客","熟悉Java、Python、Go等后端语言。","努力工作中","前端开发","后端开发","陕ICP备2021009682号-1","开发者","2022年·秋","Java,JavaScript,TypeScript,Python,Go","熟悉Js、Ts语言以及Vue、uniapp等框架。","https:\u002F\u002Firule.cn","很高兴遇见你。","把复杂的事，","把每次一发布，都当成一次和陌生人的握手。","花开空白","写成简单的故事。",{"code":4,"message":5,"data":27},{"id":28,"slug":29,"title":30,"summary":31,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":34,"likeCount":4,"wordCount":35,"readingMinutes":36,"publishedAt":37,"contentMd":38,"contentHtml":39,"tags":40,"metaTitle":9,"metaDescription":9,"keywords":9,"canonicalUrl":9,"robots":41,"ogImage":9,"faq":42,"aiSummary":42},"2113935321048551425","design-pattern-33","设计模式修炼之路 ㉝ 传输对象模式——一趟网络，带走整份数据","J2EE 模式第八站，也是 J2EE 八式的收官一站。","2113897628365230081",1,2516,8059,21,"2025-07-09T21:16:41","> J2EE 模式第八站，也是 J2EE 八式的收官一站。\n\n㉜ 结尾留了个问号：服务到手之后，数据怎么回去？顺着网络递对象，总不能把实体整个扔过去——它身上挂着行为、挂着一整窝依赖对象。㉘ 也早埋了话：\"本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\"\n\n答案本篇给：**客户端要什么，服务器拷一份打包——只有字段、没有行为、可序列化、拎起来就能走的数据快照**。传输对象模式，J2EE 八式的收官一站。\n\n## 一、传输对象模式是什么\n\nJ2EE 蓝皮书的定义大意：传输对象（Transfer Object，业界更常叫 DTO——Data Transfer Object）是一个可序列化的纯数据对象；业务对象按客户端的需要，把实体字段拷进快照，一次远程调用整份送达；客户端改动之后，同样打包一次送回——跨越网络传数据，以\"份\"为单位，不以\"字段\"为单位。\n\n三个角色：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>角色\u003C\u002Fth>\u003Cth>干什么\u003C\u002Fth>\u003Cth>本文对应\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Transfer Object（传输对象）\u003C\u002Ftd>\u003Ctd>纯数据快照：只有字段和读写方法，可序列化\u003C\u002Ftd>\u003Ctd>ClientProfileDto\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Business Object（业务对象）\u003C\u002Ftd>\u003Ctd>住在服务器：管实体、打包快照、接收写回\u003C\u002Ftd>\u003Ctd>ClientService（背后是 ClientEntity）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Client（客户端）\u003C\u002Ftd>\u003Ctd>拿快照直接渲染；要行为，另走业务接口\u003C\u002Ftd>\u003Ctd>ProfileScreen\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话理解分工：**业务对象说\"字段拷进快照，一次拿走\"，传输对象说\"我只带数据不带行为\"，客户端说\"渲染找 DTO，升 VIP 找业务接口\"。**\n\n它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉘ 组合实体是黄金搭档：㉘ 管服务器这头的生命周期，㉝ 管数据怎么过网络。第六节把这对搭档的账摊开。\n\n## 二、优点与缺点\n\n**优点：**\n\n1. **N 次往返收成 1 次**：细粒度接口一个字段一趟网络，渲染资料页 5 趟、1000ms；DTO 一趟全带到。网络延迟按次收费，字段越多省得越狠。\n2. **实体不出门**：升级要校验积分这条规则、points 这个中间态、实体的内部结构——全都留在服务器。漏出去的不只是字段，是可以被绕过的规则。\n3. **客户端拿到自包含快照**：断网也能渲染，本地就能编辑，改完一次送回——页面不必每点击一下就过一次网络。\n4. **接口对字段变化免疫**：DTO 加一个字段，方法签名不动，老客户端照跑；细粒度接口加一个字段就得加一个方法——接口数量跟着字段数量涨。\n\n**缺点：**\n\n1. **两套类，一份字段**：实体一份、DTO 一份，getProfile 里六个 set 一个不能漏，字段一改两边动——拷贝代码是这个模式的入场费（第五节看工业界怎么把这钱省下来）。\n2. **快照会过期**：拿到 DTO 那一刻起它就是过去时——别人改了实体，你手里还是旧数据；两个人同时改，后写的覆盖先写的。版本号、乐观锁，都是给这个缺点打的补丁。\n3. **写回边界靠约定**：DTO 上明明有 setVipLevel，updateProfile 却只写四个字段——哪些字段能动、哪些是业务专属，编译器不管，全靠服务器自觉。粗粒度是把双刃剑。\n\n## 三、什么时候用\n\n判断标准：**数据要过边界（网络\u002F进程\u002F层间）+ 一次要多个字段 + 调用按次收费**——三条都占，DTO 就值回票价。\n\n- **典型场景**：远程调用返回复合数据——EJB 早期把 Entity Bean 直接远程暴露，每个 getter 一趟网络，正是反面教材的历史原型\n- **日常场景**：前后端接口里的 Request\u002FResponse 对象、分层架构里 DO→DTO→VO 的转换——你天天在写传输对象，只是没叫它这个大名\n- **反面对照**：同进程内调用——对象引用直接传就行，硬套一层 DTO 是脱裤子放屁。\"每个实体必配一个 DTO\"是过度设计的重灾区：先问数据过不过边界，再问要不要快照\n\n## 四、实现方式：快照怎么打包\n\n场景：客户资料页。服务器上住着 ClientEntity——六个资料字段加一个 points，外加一条业务规则：\n\n```java\n\u002F** 服务器上的客户实体：字段 + 业务行为。业务规则不该顺着网络漏给客户端 *\u002F\npublic class ClientEntity {\n\n    private final String id;\n    private String name;\n    private String phone;\n    private String city;\n    private String street;\n    private int vipLevel;\n    private int points;\n\n    \u002F** 业务行为：升级要校验积分——这条规则只能住在服务器上 *\u002F\n    public void upgradeVip() {\n        points += 30;\n        if (points >= 130) {\n            vipLevel++;\n            System.out.println(\"   [实体] 积分 \" + points + \" 达标，VIP 升到 \" + vipLevel);\n        } else {\n            System.out.println(\"   [实体] 积分 \" + points + \" 未达标，VIP 保持 \" + vipLevel);\n        }\n    }\n    \u002F\u002F getter\u002Fsetter 略\n}\n```\n\n每次网络往返 200ms。先看反面教材。\n\n### 4.1 反面教材：细粒度远程接口\n\n```java\n\u002F** 反面教材：一个字段一个方法，每次调用一趟网络 *\u002F\npublic class RemoteClientService {\n\n    private final Map\u003CString, ClientEntity> store = new HashMap\u003C>();\n    private int calls = 0;\n\n    public String getName(String id) {\n        roundTrip(\"getName(\" + id + \")\");\n        return store.get(id).getName();\n    }\n\n    public String getPhone(String id) {\n        roundTrip(\"getPhone(\" + id + \")\");\n        return store.get(id).getPhone();\n    }\n\n    \u002F\u002F getCity、getStreet、getVipLevel、updatePhone 同款，一个字段一个方法……\n\n    private void roundTrip(String what) {\n        calls++;\n        System.out.println(\"   [网络] 第 \" + calls + \" 次往返：\" + what + \"……（耗时 200ms）\");\n    }\n}\n```\n\n客户端渲染一个页面，跟服务器要五次，UI 代码兼职拼数据：\n\n```java\npublic class BadProfileScreen {\n\n    public void render(String id) {\n        String name = remote.getName(id);\n        String phone = remote.getPhone(id);\n        String city = remote.getCity(id);\n        String street = remote.getStreet(id);\n        int vipLevel = remote.getVipLevel(id);\n        System.out.println(\"   [资料页] 姓名=\" + name + \" 电话=\" + phone + \" 城市=\" + city + \" 街道=\" + street + \" VIP=\" + vipLevel);\n    }\n\n    public void changePhone(String id, String newPhone) {\n        remote.updatePhone(id, newPhone);\n    }\n}\n```\n\n运行输出：\n\n```\n========== 反面教材：细粒度远程接口，一个字段一趟网络 ==========\n-- 渲染客户资料页（u001） --\n   [网络] 第 1 次往返：getName(u001)……（耗时 200ms）\n   [网络] 第 2 次往返：getPhone(u001)……（耗时 200ms）\n   [网络] 第 3 次往返：getCity(u001)……（耗时 200ms）\n   [网络] 第 4 次往返：getStreet(u001)……（耗时 200ms）\n   [网络] 第 5 次往返：getVipLevel(u001)……（耗时 200ms）\n   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\n   一个页面 5 次网络往返、1000ms——UI 代码还兼职拼数据\n\n-- 客户改电话（u001） --\n   [网络] 第 6 次往返：updatePhone(u001, 139-0000-0002)……（耗时 200ms）\n   改一个字段又是一趟网络——字段一多，网络跑成筛子\n```\n\n病灶：**网络被当成了本地内存总线**。getName() 在本地不过一次内存访问，一个字段一个方法顺理成章；同样的接口搬到网络上，一次调用就是 200ms。更糟的是 UI 兼职拼数据：实体每加一个字段，客户端就多一趟网络、多一行拼装。注意：功能一切正常，资料页照样渲染、电话照样改——反面教材从来不是功能错误，是结构错误。\n\n### 4.2 正解：传输对象\n\n**DTO——纯数据快照，可序列化，没有行为**：\n\n```java\npublic class ClientProfileDto implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private String id;\n    private String name;\n    private String phone;\n    private String city;\n    private String street;\n    private int vipLevel;\n\n    \u002F\u002F getter\u002Fsetter 全是普通读写，略\n\n    \u002F** 把字段拼成人话——㉘ describe() 的影子，在这里长成正式身份 *\u002F\n    public String display() {\n        return \"姓名=\" + name + \" 电话=\" + phone + \" 城市=\" + city + \" 街道=\" + street + \" VIP=\" + vipLevel;\n    }\n}\n```\n\n注意：实体七个字段，快照里只有六个——points 没上车，细节二展开。\n\n**业务对象——读打包、写解包、行为单独开门**：\n\n```java\n\u002F** 正解：粗粒度接口 + DTO。读一次打包全部字段；写一次打包送回；行为单独一条路 *\u002F\npublic class ClientService {\n\n    private final Map\u003CString, ClientEntity> store = new HashMap\u003C>();\n    private int calls = 0;\n\n    public ClientProfileDto getProfile(String id) {\n        roundTrip(\"getProfile(\" + id + \")\");\n        ClientEntity entity = store.get(id);\n        System.out.println(\"   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\");\n        ClientProfileDto dto = new ClientProfileDto();\n        dto.setId(entity.getId());\n        dto.setName(entity.getName());\n        dto.setPhone(entity.getPhone());\n        dto.setCity(entity.getCity());\n        dto.setStreet(entity.getStreet());\n        dto.setVipLevel(entity.getVipLevel());\n        return dto;\n    }\n\n    public void updateProfile(ClientProfileDto dto) {\n        roundTrip(\"updateProfile(dto)\");\n        ClientEntity entity = store.get(dto.getId());\n        entity.setName(dto.getName());\n        entity.setPhone(dto.getPhone());\n        entity.setCity(dto.getCity());\n        entity.setStreet(dto.getStreet());\n        System.out.println(\"   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\");\n    }\n\n    public void upgradeVip(String id) {\n        roundTrip(\"upgradeVip(\" + id + \")\");\n        store.get(id).upgradeVip();\n    }\n\n    private void roundTrip(String what) {\n        calls++;\n        System.out.println(\"   [网络] 第 \" + calls + \" 次往返：\" + what + \"……（耗时 200ms）\");\n    }\n}\n```\n\n**客户端——一次拿全，本地编辑，一次送回**：\n\n```java\n\u002F** 正解客户端：一次 getProfile 拿全，本地改 DTO，一次 updateProfile 送回 *\u002F\npublic class ProfileScreen {\n\n    private final ClientService service;\n    private ClientProfileDto current;\n\n    public void render(String id) {\n        current = service.getProfile(id);\n        System.out.println(\"   [资料页] \" + current.display());\n    }\n\n    public void changePhone(String newPhone) {\n        current.setPhone(newPhone);\n        System.out.println(\"   [资料页] 本地改 DTO：setPhone(\" + newPhone + \")\");\n        service.updateProfile(current);\n    }\n\n    public void upgradeVip(String id) {\n        System.out.println(\"   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\");\n        service.upgradeVip(id);\n    }\n}\n```\n\n运行输出：\n\n```\n========== 正解：传输对象，一来一回两趟网络 ==========\n-- 渲染客户资料页（u001） --\n   [网络] 第 1 次往返：getProfile(u001)……（耗时 200ms）\n   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\n   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\n   同一页 1 次往返——DTO 把整份快照一次送到\n\n-- 客户改电话（u001） --\n   [资料页] 本地改 DTO：setPhone(139-0000-0002)\n   [网络] 第 2 次往返：updateProfile(dto)……（耗时 200ms）\n   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\n   改动打包一次送回——一来一回，网络只跑两趟\n\n-- 客户要升 VIP（u001） --\n   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\n   [网络] 第 3 次往返：upgradeVip(u001)……（耗时 200ms）\n   [实体] 积分 130 达标，VIP 升到 4\n   数据走 DTO 的路，行为走业务接口的路——两条路不混\n```\n\n三处对照读出含金量：同一页资料，反面 5 趟 1000ms，正解 **1 趟 200ms**——往返次数与字段数脱钩，字段再涨网络不涨；改电话反面是\"UI 喊服务器改一个字段\"，正解是**本地改快照、一次打包送回**——服务器一共只被打扰两次；升 VIP 最有意思——**DTO 上根本没有 upgradeVip()**，客户端想触发业务只能走业务接口那条路，积分规则从头到尾没离开过服务器。\n\n### 4.3 三处细节\n\n**细节一：DTO 是快照，不是实体的引用。** getProfile 返回的 ClientProfileDto 是 new 出来的新对象，字段从实体逐个拷贝——此后实体怎么变，快照不知道；快照怎么改，实体也不知道，直到 updateProfile 写回那一刻。本地调用里\"两边各一份\"是浪费，跨网络是必须——引用过不了网络，能过的只有序列化的字节流。\n\n**细节二：字段要挑着带，不是照抄实体。** points 没进快照——它是业务中间态，客户端看到没意义；vipLevel 进了，但写回时不碰它——升级必须走 upgradeVip() 过积分校验。哪些字段出门、出门的字段哪些能写回，是设计决策而不是体力活。把实体字段一比一塞进 DTO，等于给客户端递了一份实体内部说明书。\n\n**细节三：贫血是刻意的，别治。** DTO 只有字段没有行为，教科书管这叫\"贫血对象\"还捎带批评——那说的是领域模型，不是 DTO。传输对象的职责就是运数据：行为要是跟着数据出了门，积分校验就管不住客户端了。**数据走一条路，行为走另一条路**，正是这个模式的灵魂。\n\n## 五、在框架里找它\n\n1. **Java Record**：`record ClientProfile(String id, String name, String phone) {}`——一行定义不可变快照，构造、读访问、equals、hashCode 全部免写。JDK 14 亲手给 DTO 造的官配语法，\"两套类一份字段\"的样板代码从此少一半。\n\n2. **MapStruct**：`ClientProfileDto toDto(ClientEntity entity)` 一个接口声明，字段拷贝在编译期生成实现——getProfile 里那六个 set，工业界早交给代码生成器了。同类的 Spring BeanUtils.copyProperties 走运行期反射，慢一点，但零声明。\n\n3. **前后端的 Request\u002FResponse**：每个 REST 接口的 body 都是一份序列化快照；分层架构的 DO→DTO→VO 流水线，是把快照按读者细分——数据库里的 DO、给别处服务的 DTO、给前端渲染的 VO，同一份数据三种打包。\n\n4. **GraphQL**：打包粒度的进化版——REST 是服务器定死一份快照，GraphQL 让客户端声明要哪几个字段，一次请求取回一棵自定义的树。和 ㉘ 里说的方向一致：从\"服务器说给你什么\"走向\"客户端说要什么\"。\n\n5. **名字的公案**：这个模式最初叫 Value Object——J2EE 蓝皮书早期版本和 Fowler 的 PoEAA 都用过这个名字；后来为了不和\"不可变值语义\"的 Value Object 混淆，蓝皮书改名 Transfer Object，Fowler 也专门撰文承认命名失当、改称 Data Transfer Object。今天满地的 VO\u002FDTO 之争，源头都在这段公案。\n\n## 六、对账——㉝ 传输对象 vs ㉘ 组合实体：黄金搭档摊账\n\n㉘ 结尾埋的话：\"㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里。\"搭档摊账：\n\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>\u003C\u002Fth>\u003Cth>传输对象（㉝）\u003C\u002Fth>\u003Cth>组合实体（㉘）\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>住在哪\u003C\u002Ftd>\u003Ctd>网络两头跑，序列化上路\u003C\u002Ftd>\u003Ctd>服务器上不动窝\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>管什么\u003C\u002Ftd>\u003Ctd>数据怎么过网络\u003C\u002Ftd>\u003Ctd>生命周期怎么成组（一次加载、一次提交）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>身上有什么\u003C\u002Ftd>\u003Ctd>只有字段——贫血是刻意\u003C\u002Ftd>\u003Ctd>字段 + 行为 + 一窝依赖对象\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>客户端碰什么\u003C\u002Ftd>\u003Ctd>快照（本地可改，打包送回）\u003C\u002Ftd>\u003Ctd>只碰组合实体的门（门后整窝进出）\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>典型同框\u003C\u002Ftd>\u003Ctd>EJB 标准答案：客户端 ←DTO→ Session Bean（业务对象）→ 组合实体 → 数据库\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\n\n一句话：**㉘ 管服务器这头的\"进整窝、出整窝\"，㉝ 管网络上的\"一去一回各一趟\"**——一个在源头捆生命周期，一个在路上捆数据。㉘ 那篇还留过一张三兄弟对账表（⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期），本篇补上最后一块拼图。再拉 ㉗ ㉜ 入伙算个总账：㉜ 管\"服务从哪儿来\"，㉗ 管\"远程怎么说得像本地\"，㉝ 管\"数据怎么打包回去\"——找服务的路、通话的路、运数据的路，三路齐全，J2EE 八式就此收官。\n\n## 写在最后\n\n- 传输对象 = 跨边界的数据快照：只有字段、可序列化，一次打包整份送达\n- 疗效：往返次数与字段数脱钩（5 趟变 1 趟）；实体不出门，业务规则不出门；客户端手里自包含\n- 代价：两套类一份字段、快照会过期、写回边界靠约定——版本号与乐观锁是标配补丁\n- 铁律：数据走 DTO，行为走业务接口——两条路不混，贫血是刻意\n- 黄金搭档：㉘ 在源头捆生命周期，㉝ 在路上捆数据，⑨ 捆结构——三兄弟的账，两篇合清\n- 当代转世：Record 一行定义、MapStruct 编译期拷贝、每个 JSON Response——你天天在写它，只是没叫它这个大名\n\nJ2EE 八式至此收官。回头望，从 ② 单例一路走到 ㉝，三十二个模式各治一种痛；可每次诊断病灶、开出药方，反复露面的都是几张熟面孔——单一职责、开闭、依赖倒置……它们从没单独出场，却藏在每一篇的病理与药理里。该算总账了——六大原则再回首，全系列最后一篇见。\n","\u003Cblockquote>\n \u003Cp>J2EE 模式第八站，也是 J2EE 八式的收官一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉜ 结尾留了个问号：服务到手之后，数据怎么回去？顺着网络递对象，总不能把实体整个扔过去——它身上挂着行为、挂着一整窝依赖对象。㉘ 也早埋了话：\"本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\"\u003C\u002Fp>\n\u003Cp>答案本篇给：\u003Cstrong>客户端要什么，服务器拷一份打包——只有字段、没有行为、可序列化、拎起来就能走的数据快照\u003C\u002Fstrong>。传输对象模式，J2EE 八式的收官一站。\u003C\u002Fp>\n\u003Ch2>一、传输对象模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：传输对象（Transfer Object，业界更常叫 DTO——Data Transfer Object）是一个可序列化的纯数据对象；业务对象按客户端的需要，把实体字段拷进快照，一次远程调用整份送达；客户端改动之后，同样打包一次送回——跨越网络传数据，以\"份\"为单位，不以\"字段\"为单位。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Transfer Object（传输对象）\u003C\u002Ftd>\n   \u003Ctd>纯数据快照：只有字段和读写方法，可序列化\u003C\u002Ftd>\n   \u003Ctd>ClientProfileDto\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Business Object（业务对象）\u003C\u002Ftd>\n   \u003Ctd>住在服务器：管实体、打包快照、接收写回\u003C\u002Ftd>\n   \u003Ctd>ClientService（背后是 ClientEntity）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>拿快照直接渲染；要行为，另走业务接口\u003C\u002Ftd>\n   \u003Ctd>ProfileScreen\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>业务对象说\"字段拷进快照，一次拿走\"，传输对象说\"我只带数据不带行为\"，客户端说\"渲染找 DTO，升 VIP 找业务接口\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉘ 组合实体是黄金搭档：㉘ 管服务器这头的生命周期，㉝ 管数据怎么过网络。第六节把这对搭档的账摊开。\u003C\u002Fp>\n\u003Ch2>二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>N 次往返收成 1 次\u003C\u002Fstrong>：细粒度接口一个字段一趟网络，渲染资料页 5 趟、1000ms；DTO 一趟全带到。网络延迟按次收费，字段越多省得越狠。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>实体不出门\u003C\u002Fstrong>：升级要校验积分这条规则、points 这个中间态、实体的内部结构——全都留在服务器。漏出去的不只是字段，是可以被绕过的规则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>客户端拿到自包含快照\u003C\u002Fstrong>：断网也能渲染，本地就能编辑，改完一次送回——页面不必每点击一下就过一次网络。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>接口对字段变化免疫\u003C\u002Fstrong>：DTO 加一个字段，方法签名不动，老客户端照跑；细粒度接口加一个字段就得加一个方法——接口数量跟着字段数量涨。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>两套类，一份字段\u003C\u002Fstrong>：实体一份、DTO 一份，getProfile 里六个 set 一个不能漏，字段一改两边动——拷贝代码是这个模式的入场费（第五节看工业界怎么把这钱省下来）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>快照会过期\u003C\u002Fstrong>：拿到 DTO 那一刻起它就是过去时——别人改了实体，你手里还是旧数据；两个人同时改，后写的覆盖先写的。版本号、乐观锁，都是给这个缺点打的补丁。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>写回边界靠约定\u003C\u002Fstrong>：DTO 上明明有 setVipLevel，updateProfile 却只写四个字段——哪些字段能动、哪些是业务专属，编译器不管，全靠服务器自觉。粗粒度是把双刃剑。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>数据要过边界（网络\u002F进程\u002F层间）+ 一次要多个字段 + 调用按次收费\u003C\u002Fstrong>——三条都占，DTO 就值回票价。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：远程调用返回复合数据——EJB 早期把 Entity Bean 直接远程暴露，每个 getter 一趟网络，正是反面教材的历史原型\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日常场景\u003C\u002Fstrong>：前后端接口里的 Request\u002FResponse 对象、分层架构里 DO→DTO→VO 的转换——你天天在写传输对象，只是没叫它这个大名\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：同进程内调用——对象引用直接传就行，硬套一层 DTO 是脱裤子放屁。\"每个实体必配一个 DTO\"是过度设计的重灾区：先问数据过不过边界，再问要不要快照\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>四、实现方式：快照怎么打包\u003C\u002Fh2>\n\u003Cp>场景：客户资料页。服务器上住着 ClientEntity——六个资料字段加一个 points，外加一条业务规则：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 服务器上的客户实体：字段 + 业务行为。业务规则不该顺着网络漏给客户端 *\u002F\npublic class ClientEntity {\n\n    private final String id;\n    private String name;\n    private String phone;\n    private String city;\n    private String street;\n    private int vipLevel;\n    private int points;\n\n    \u002F** 业务行为：升级要校验积分——这条规则只能住在服务器上 *\u002F\n    public void upgradeVip() {\n        points += 30;\n        if (points &gt;= 130) {\n            vipLevel++;\n            System.out.println(\"   [实体] 积分 \" + points + \" 达标，VIP 升到 \" + vipLevel);\n        } else {\n            System.out.println(\"   [实体] 积分 \" + points + \" 未达标，VIP 保持 \" + vipLevel);\n        }\n    }\n    \u002F\u002F getter\u002Fsetter 略\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>每次网络往返 200ms。先看反面教材。\u003C\u002Fp>\n\u003Ch3>4.1 反面教材：细粒度远程接口\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 反面教材：一个字段一个方法，每次调用一趟网络 *\u002F\npublic class RemoteClientService {\n\n    private final Map&lt;String, ClientEntity&gt; store = new HashMap&lt;&gt;();\n    private int calls = 0;\n\n    public String getName(String id) {\n        roundTrip(\"getName(\" + id + \")\");\n        return store.get(id).getName();\n    }\n\n    public String getPhone(String id) {\n        roundTrip(\"getPhone(\" + id + \")\");\n        return store.get(id).getPhone();\n    }\n\n    \u002F\u002F getCity、getStreet、getVipLevel、updatePhone 同款，一个字段一个方法……\n\n    private void roundTrip(String what) {\n        calls++;\n        System.out.println(\"   [网络] 第 \" + calls + \" 次往返：\" + what + \"……（耗时 200ms）\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>客户端渲染一个页面，跟服务器要五次，UI 代码兼职拼数据：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class BadProfileScreen {\n\n    public void render(String id) {\n        String name = remote.getName(id);\n        String phone = remote.getPhone(id);\n        String city = remote.getCity(id);\n        String street = remote.getStreet(id);\n        int vipLevel = remote.getVipLevel(id);\n        System.out.println(\"   [资料页] 姓名=\" + name + \" 电话=\" + phone + \" 城市=\" + city + \" 街道=\" + street + \" VIP=\" + vipLevel);\n    }\n\n    public void changePhone(String id, String newPhone) {\n        remote.updatePhone(id, newPhone);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 反面教材：细粒度远程接口，一个字段一趟网络 ==========\n-- 渲染客户资料页（u001） --\n   [网络] 第 1 次往返：getName(u001)……（耗时 200ms）\n   [网络] 第 2 次往返：getPhone(u001)……（耗时 200ms）\n   [网络] 第 3 次往返：getCity(u001)……（耗时 200ms）\n   [网络] 第 4 次往返：getStreet(u001)……（耗时 200ms）\n   [网络] 第 5 次往返：getVipLevel(u001)……（耗时 200ms）\n   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\n   一个页面 5 次网络往返、1000ms——UI 代码还兼职拼数据\n\n-- 客户改电话（u001） --\n   [网络] 第 6 次往返：updatePhone(u001, 139-0000-0002)……（耗时 200ms）\n   改一个字段又是一趟网络——字段一多，网络跑成筛子\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>病灶：\u003Cstrong>网络被当成了本地内存总线\u003C\u002Fstrong>。getName() 在本地不过一次内存访问，一个字段一个方法顺理成章；同样的接口搬到网络上，一次调用就是 200ms。更糟的是 UI 兼职拼数据：实体每加一个字段，客户端就多一趟网络、多一行拼装。注意：功能一切正常，资料页照样渲染、电话照样改——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Ch3>4.2 正解：传输对象\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>DTO——纯数据快照，可序列化，没有行为\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">public class ClientProfileDto implements Serializable {\n\n    private static final long serialVersionUID = 1L;\n\n    private String id;\n    private String name;\n    private String phone;\n    private String city;\n    private String street;\n    private int vipLevel;\n\n    \u002F\u002F getter\u002Fsetter 全是普通读写，略\n\n    \u002F** 把字段拼成人话——㉘ describe() 的影子，在这里长成正式身份 *\u002F\n    public String display() {\n        return \"姓名=\" + name + \" 电话=\" + phone + \" 城市=\" + city + \" 街道=\" + street + \" VIP=\" + vipLevel;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意：实体七个字段，快照里只有六个——points 没上车，细节二展开。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>业务对象——读打包、写解包、行为单独开门\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 正解：粗粒度接口 + DTO。读一次打包全部字段；写一次打包送回；行为单独一条路 *\u002F\npublic class ClientService {\n\n    private final Map&lt;String, ClientEntity&gt; store = new HashMap&lt;&gt;();\n    private int calls = 0;\n\n    public ClientProfileDto getProfile(String id) {\n        roundTrip(\"getProfile(\" + id + \")\");\n        ClientEntity entity = store.get(id);\n        System.out.println(\"   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\");\n        ClientProfileDto dto = new ClientProfileDto();\n        dto.setId(entity.getId());\n        dto.setName(entity.getName());\n        dto.setPhone(entity.getPhone());\n        dto.setCity(entity.getCity());\n        dto.setStreet(entity.getStreet());\n        dto.setVipLevel(entity.getVipLevel());\n        return dto;\n    }\n\n    public void updateProfile(ClientProfileDto dto) {\n        roundTrip(\"updateProfile(dto)\");\n        ClientEntity entity = store.get(dto.getId());\n        entity.setName(dto.getName());\n        entity.setPhone(dto.getPhone());\n        entity.setCity(dto.getCity());\n        entity.setStreet(dto.getStreet());\n        System.out.println(\"   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\");\n    }\n\n    public void upgradeVip(String id) {\n        roundTrip(\"upgradeVip(\" + id + \")\");\n        store.get(id).upgradeVip();\n    }\n\n    private void roundTrip(String what) {\n        calls++;\n        System.out.println(\"   [网络] 第 \" + calls + \" 次往返：\" + what + \"……（耗时 200ms）\");\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>客户端——一次拿全，本地编辑，一次送回\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F** 正解客户端：一次 getProfile 拿全，本地改 DTO，一次 updateProfile 送回 *\u002F\npublic class ProfileScreen {\n\n    private final ClientService service;\n    private ClientProfileDto current;\n\n    public void render(String id) {\n        current = service.getProfile(id);\n        System.out.println(\"   [资料页] \" + current.display());\n    }\n\n    public void changePhone(String newPhone) {\n        current.setPhone(newPhone);\n        System.out.println(\"   [资料页] 本地改 DTO：setPhone(\" + newPhone + \")\");\n        service.updateProfile(current);\n    }\n\n    public void upgradeVip(String id) {\n        System.out.println(\"   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\");\n        service.upgradeVip(id);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>========== 正解：传输对象，一来一回两趟网络 ==========\n-- 渲染客户资料页（u001） --\n   [网络] 第 1 次往返：getProfile(u001)……（耗时 200ms）\n   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\n   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\n   同一页 1 次往返——DTO 把整份快照一次送到\n\n-- 客户改电话（u001） --\n   [资料页] 本地改 DTO：setPhone(139-0000-0002)\n   [网络] 第 2 次往返：updateProfile(dto)……（耗时 200ms）\n   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\n   改动打包一次送回——一来一回，网络只跑两趟\n\n-- 客户要升 VIP（u001） --\n   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\n   [网络] 第 3 次往返：upgradeVip(u001)……（耗时 200ms）\n   [实体] 积分 130 达标，VIP 升到 4\n   数据走 DTO 的路，行为走业务接口的路——两条路不混\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：同一页资料，反面 5 趟 1000ms，正解 \u003Cstrong>1 趟 200ms\u003C\u002Fstrong>——往返次数与字段数脱钩，字段再涨网络不涨；改电话反面是\"UI 喊服务器改一个字段\"，正解是\u003Cstrong>本地改快照、一次打包送回\u003C\u002Fstrong>——服务器一共只被打扰两次；升 VIP 最有意思——\u003Cstrong>DTO 上根本没有 upgradeVip()\u003C\u002Fstrong>，客户端想触发业务只能走业务接口那条路，积分规则从头到尾没离开过服务器。\u003C\u002Fp>\n\u003Ch3>4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：DTO 是快照，不是实体的引用。\u003C\u002Fstrong> getProfile 返回的 ClientProfileDto 是 new 出来的新对象，字段从实体逐个拷贝——此后实体怎么变，快照不知道；快照怎么改，实体也不知道，直到 updateProfile 写回那一刻。本地调用里\"两边各一份\"是浪费，跨网络是必须——引用过不了网络，能过的只有序列化的字节流。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：字段要挑着带，不是照抄实体。\u003C\u002Fstrong> points 没进快照——它是业务中间态，客户端看到没意义；vipLevel 进了，但写回时不碰它——升级必须走 upgradeVip() 过积分校验。哪些字段出门、出门的字段哪些能写回，是设计决策而不是体力活。把实体字段一比一塞进 DTO，等于给客户端递了一份实体内部说明书。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：贫血是刻意的，别治。\u003C\u002Fstrong> DTO 只有字段没有行为，教科书管这叫\"贫血对象\"还捎带批评——那说的是领域模型，不是 DTO。传输对象的职责就是运数据：行为要是跟着数据出了门，积分校验就管不住客户端了。\u003Cstrong>数据走一条路，行为走另一条路\u003C\u002Fstrong>，正是这个模式的灵魂。\u003C\u002Fp>\n\u003Ch2>五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Java Record\u003C\u002Fstrong>：\u003Ccode>record ClientProfile(String id, String name, String phone) {}\u003C\u002Fcode>——一行定义不可变快照，构造、读访问、equals、hashCode 全部免写。JDK 14 亲手给 DTO 造的官配语法，\"两套类一份字段\"的样板代码从此少一半。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MapStruct\u003C\u002Fstrong>：\u003Ccode>ClientProfileDto toDto(ClientEntity entity)\u003C\u002Fcode> 一个接口声明，字段拷贝在编译期生成实现——getProfile 里那六个 set，工业界早交给代码生成器了。同类的 Spring BeanUtils.copyProperties 走运行期反射，慢一点，但零声明。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>前后端的 Request\u002FResponse\u003C\u002Fstrong>：每个 REST 接口的 body 都是一份序列化快照；分层架构的 DO→DTO→VO 流水线，是把快照按读者细分——数据库里的 DO、给别处服务的 DTO、给前端渲染的 VO，同一份数据三种打包。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>GraphQL\u003C\u002Fstrong>：打包粒度的进化版——REST 是服务器定死一份快照，GraphQL 让客户端声明要哪几个字段，一次请求取回一棵自定义的树。和 ㉘ 里说的方向一致：从\"服务器说给你什么\"走向\"客户端说要什么\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>名字的公案\u003C\u002Fstrong>：这个模式最初叫 Value Object——J2EE 蓝皮书早期版本和 Fowler 的 PoEAA 都用过这个名字；后来为了不和\"不可变值语义\"的 Value Object 混淆，蓝皮书改名 Transfer Object，Fowler 也专门撰文承认命名失当、改称 Data Transfer Object。今天满地的 VO\u002FDTO 之争，源头都在这段公案。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>六、对账——㉝ 传输对象 vs ㉘ 组合实体：黄金搭档摊账\u003C\u002Fh2>\n\u003Cp>㉘ 结尾埋的话：\"㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里。\"搭档摊账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>传输对象（㉝）\u003C\u002Fth>\n   \u003Cth>组合实体（㉘）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>住在哪\u003C\u002Ftd>\n   \u003Ctd>网络两头跑，序列化上路\u003C\u002Ftd>\n   \u003Ctd>服务器上不动窝\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>管什么\u003C\u002Ftd>\n   \u003Ctd>数据怎么过网络\u003C\u002Ftd>\n   \u003Ctd>生命周期怎么成组（一次加载、一次提交）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>身上有什么\u003C\u002Ftd>\n   \u003Ctd>只有字段——贫血是刻意\u003C\u002Ftd>\n   \u003Ctd>字段 + 行为 + 一窝依赖对象\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>客户端碰什么\u003C\u002Ftd>\n   \u003Ctd>快照（本地可改，打包送回）\u003C\u002Ftd>\n   \u003Ctd>只碰组合实体的门（门后整窝进出）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型同框\u003C\u002Ftd>\n   \u003Ctd>EJB 标准答案：客户端 ←DTO→ Session Bean（业务对象）→ 组合实体 → 数据库\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>㉘ 管服务器这头的\"进整窝、出整窝\"，㉝ 管网络上的\"一去一回各一趟\"\u003C\u002Fstrong>——一个在源头捆生命周期，一个在路上捆数据。㉘ 那篇还留过一张三兄弟对账表（⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期），本篇补上最后一块拼图。再拉 ㉗ ㉜ 入伙算个总账：㉜ 管\"服务从哪儿来\"，㉗ 管\"远程怎么说得像本地\"，㉝ 管\"数据怎么打包回去\"——找服务的路、通话的路、运数据的路，三路齐全，J2EE 八式就此收官。\u003C\u002Fp>\n\u003Ch2>写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>传输对象 = 跨边界的数据快照：只有字段、可序列化，一次打包整份送达\u003C\u002Fli>\n \u003Cli>疗效：往返次数与字段数脱钩（5 趟变 1 趟）；实体不出门，业务规则不出门；客户端手里自包含\u003C\u002Fli>\n \u003Cli>代价：两套类一份字段、快照会过期、写回边界靠约定——版本号与乐观锁是标配补丁\u003C\u002Fli>\n \u003Cli>铁律：数据走 DTO，行为走业务接口——两条路不混，贫血是刻意\u003C\u002Fli>\n \u003Cli>黄金搭档：㉘ 在源头捆生命周期，㉝ 在路上捆数据，⑨ 捆结构——三兄弟的账，两篇合清\u003C\u002Fli>\n \u003Cli>当代转世：Record 一行定义、MapStruct 编译期拷贝、每个 JSON Response——你天天在写它，只是没叫它这个大名\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>J2EE 八式至此收官。回头望，从 ② 单例一路走到 ㉝，三十二个模式各治一种痛；可每次诊断病灶、开出药方，反复露面的都是几张熟面孔——单一职责、开闭、依赖倒置……它们从没单独出场，却藏在每一篇的病理与药理里。该算总账了——六大原则再回首，全系列最后一篇见。\u003C\u002Fp>",[],"index,follow",null,{"code":4,"message":5,"data":44},[45,54,63],{"id":46,"slug":47,"title":48,"summary":49,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":50,"likeCount":4,"wordCount":51,"readingMinutes":52,"publishedAt":53},"2113935015761940481","design-pattern-28","设计模式修炼之路 ㉘ 组合实体模式——一窝小对象，一次网络往返","一窝小对象一次网络往返、读放大与 N+1、一次提交就是事务边界、依赖对象没有远程身份、JPA 聚合根转世",3270,7543,19,"2025-04-07T11:55:37",{"id":55,"slug":56,"title":57,"summary":58,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":59,"likeCount":4,"wordCount":60,"readingMinutes":61,"publishedAt":62},"2113935074440253441","design-pattern-29","设计模式修炼之路 ㉙ 数据访问对象模式——业务对象不该懂数据库","业务对象不该懂数据库、接口只说业务语言、换实现不换业务、单测不连库、Spring Data 工业化 DAO、DAO 与 Repository 对账",3913,6821,18,"2025-04-15T11:27:29",{"id":64,"slug":65,"title":66,"summary":67,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":68,"likeCount":4,"wordCount":69,"readingMinutes":70,"publishedAt":71},"2113934922031828994","design-pattern-27","设计模式修炼之路 ㉗ 业务代表模式——替表示层挡住远程的脏活","替表示层挡住远程脏活、查找+重试+异常翻译一肩挑、重试风暴、外观\u002F代理\u002F业务代表三兄弟对账、Feign 转世",1811,6147,16,"2025-03-25T21:44:13",{"code":4,"message":5,"data":73},{"prev":74,"next":82},{"id":75,"slug":76,"title":77,"summary":78,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":79,"likeCount":4,"wordCount":80,"readingMinutes":61,"publishedAt":81},"2113935261715927041","design-pattern-32","设计模式修炼之路 ㉜ 服务定位器模式——服务只找一次，之后全靠缓存","查找与缓存一处收口、注册中心只被打扰 N 次、缓存的是服务不是结果、种子从 ㉗ 长成 static 注册台、字符串名字没有编译器保护、与业务代表对账",1926,7172,"2025-06-20T10:10:19",{"id":83,"slug":84,"title":85,"summary":86,"coverUrl":9,"categoryId":32,"status":33,"topFlag":4,"viewCount":87,"likeCount":4,"wordCount":88,"readingMinutes":89,"publishedAt":90},"2113935380699942913","design-pattern-34","设计模式修炼之路 ㉞ 六大原则再回首——殊途同归的总账","全系列收官篇。没有新模式，没有代码——只有一本记了三十二个模式的账。",2800,3601,10,"2025-07-18T11:29:45",{"code":4,"message":5,"data":92},[93,99,104,110],{"id":94,"name":95,"slug":96,"description":97,"sortOrder":4,"articleCount":98},"1946000000000000001","antlr4","tech","编程与技术笔记","13",{"id":32,"name":100,"slug":101,"description":102,"sortOrder":4,"articleCount":103},"设计模式","design-pattern","设计模式系列","34",{"id":105,"name":106,"slug":107,"description":108,"sortOrder":33,"articleCount":109},"1946000000000000002","随笔","essay","生活与思考","0",{"id":111,"name":112,"slug":113,"description":114,"sortOrder":115,"articleCount":116},"2105200093764698113","工具","tools","好用的工具推荐",2,"1",{"html":118,"toc":119},"\u003Cblockquote>\n \u003Cp>J2EE 模式第八站，也是 J2EE 八式的收官一站。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>㉜ 结尾留了个问号：服务到手之后，数据怎么回去？顺着网络递对象，总不能把实体整个扔过去——它身上挂着行为、挂着一整窝依赖对象。㉘ 也早埋了话：\"本篇 describe() 已经隐约有它的影子，㉝ 正式展开。\"\u003C\u002Fp>\n\u003Cp>答案本篇给：\u003Cstrong>客户端要什么，服务器拷一份打包——只有字段、没有行为、可序列化、拎起来就能走的数据快照\u003C\u002Fstrong>。传输对象模式，J2EE 八式的收官一站。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、传输对象模式是什么\u003C\u002Fh2>\n\u003Cp>J2EE 蓝皮书的定义大意：传输对象（Transfer Object，业界更常叫 DTO——Data Transfer Object）是一个可序列化的纯数据对象；业务对象按客户端的需要，把实体字段拷进快照，一次远程调用整份送达；客户端改动之后，同样打包一次送回——跨越网络传数据，以\"份\"为单位，不以\"字段\"为单位。\u003C\u002Fp>\n\u003Cp>三个角色：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>角色\u003C\u002Fth>\n   \u003Cth>干什么\u003C\u002Fth>\n   \u003Cth>本文对应\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>Transfer Object（传输对象）\u003C\u002Ftd>\n   \u003Ctd>纯数据快照：只有字段和读写方法，可序列化\u003C\u002Ftd>\n   \u003Ctd>ClientProfileDto\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Business Object（业务对象）\u003C\u002Ftd>\n   \u003Ctd>住在服务器：管实体、打包快照、接收写回\u003C\u002Ftd>\n   \u003Ctd>ClientService（背后是 ClientEntity）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>Client（客户端）\u003C\u002Ftd>\n   \u003Ctd>拿快照直接渲染；要行为，另走业务接口\u003C\u002Ftd>\n   \u003Ctd>ProfileScreen\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话理解分工：\u003Cstrong>业务对象说\"字段拷进快照，一次拿走\"，传输对象说\"我只带数据不带行为\"，客户端说\"渲染找 DTO，升 VIP 找业务接口\"。\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>它不是 GoF 二十三式，是 J2EE 蓝皮书成员——和 ㉘ 组合实体是黄金搭档：㉘ 管服务器这头的生命周期，㉝ 管数据怎么过网络。第六节把这对搭档的账摊开。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、优点与缺点\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>优点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>N 次往返收成 1 次\u003C\u002Fstrong>：细粒度接口一个字段一趟网络，渲染资料页 5 趟、1000ms；DTO 一趟全带到。网络延迟按次收费，字段越多省得越狠。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>实体不出门\u003C\u002Fstrong>：升级要校验积分这条规则、points 这个中间态、实体的内部结构——全都留在服务器。漏出去的不只是字段，是可以被绕过的规则。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>客户端拿到自包含快照\u003C\u002Fstrong>：断网也能渲染，本地就能编辑，改完一次送回——页面不必每点击一下就过一次网络。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>接口对字段变化免疫\u003C\u002Fstrong>：DTO 加一个字段，方法签名不动，老客户端照跑；细粒度接口加一个字段就得加一个方法——接口数量跟着字段数量涨。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Cstrong>缺点：\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cstrong>两套类，一份字段\u003C\u002Fstrong>：实体一份、DTO 一份，getProfile 里六个 set 一个不能漏，字段一改两边动——拷贝代码是这个模式的入场费（第五节看工业界怎么把这钱省下来）。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>快照会过期\u003C\u002Fstrong>：拿到 DTO 那一刻起它就是过去时——别人改了实体，你手里还是旧数据；两个人同时改，后写的覆盖先写的。版本号、乐观锁，都是给这个缺点打的补丁。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>写回边界靠约定\u003C\u002Fstrong>：DTO 上明明有 setVipLevel，updateProfile 却只写四个字段——哪些字段能动、哪些是业务专属，编译器不管，全靠服务器自觉。粗粒度是把双刃剑。\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、什么时候用\u003C\u002Fh2>\n\u003Cp>判断标准：\u003Cstrong>数据要过边界（网络\u002F进程\u002F层间）+ 一次要多个字段 + 调用按次收费\u003C\u002Fstrong>——三条都占，DTO 就值回票价。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>典型场景\u003C\u002Fstrong>：远程调用返回复合数据——EJB 早期把 Entity Bean 直接远程暴露，每个 getter 一趟网络，正是反面教材的历史原型\u003C\u002Fli>\n \u003Cli>\u003Cstrong>日常场景\u003C\u002Fstrong>：前后端接口里的 Request\u002FResponse 对象、分层架构里 DO→DTO→VO 的转换——你天天在写传输对象，只是没叫它这个大名\u003C\u002Fli>\n \u003Cli>\u003Cstrong>反面对照\u003C\u002Fstrong>：同进程内调用——对象引用直接传就行，硬套一层 DTO 是脱裤子放屁。\"每个实体必配一个 DTO\"是过度设计的重灾区：先问数据过不过边界，再问要不要快照\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"toc-4\">四、实现方式：快照怎么打包\u003C\u002Fh2>\n\u003Cp>场景：客户资料页。服务器上住着 ClientEntity——六个资料字段加一个 points，外加一条业务规则：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 服务器上的客户实体：字段 + 业务行为。业务规则不该顺着网络漏给客户端 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ClientEntity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String id;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String phone;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String city;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String street;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> points;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 业务行为：升级要校验积分——这条规则只能住在服务器上 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> upgradeVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        points \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 30\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (points \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 130\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            vipLevel\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [实体] 积分 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> points \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 达标，VIP 升到 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">else\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [实体] 积分 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> points \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 未达标，VIP 保持 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F getter\u002Fsetter 略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>每次网络往返 200ms。先看反面教材。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">4.1 反面教材：细粒度远程接口\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 反面教材：一个字段一个方法，每次调用一趟网络 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RemoteClientService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">ClientEntity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> store \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> HashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> calls \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"getName(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"getPhone(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F getCity、getStreet、getVipLevel、updatePhone 同款，一个字段一个方法……\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">what\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        calls\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [网络] 第 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> calls \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 次往返：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> what \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"……（耗时 200ms）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>客户端渲染一个页面，跟服务器要五次，UI 代码兼职拼数据：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> BadProfileScreen\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String phone \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String city \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String street \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getStreet\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getVipLevel\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [资料页] 姓名=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 电话=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> phone \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 城市=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> city \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 街道=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> street \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" VIP=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> changePhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">newPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        remote.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">updatePhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id, newPhone);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 反面教材：细粒度远程接口，一个字段一趟网络 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 渲染客户资料页（u001） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 1 次往返：getName(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 2 次往返：getPhone(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 3 次往返：getCity(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 4 次往返：getStreet(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 5 次往返：getVipLevel(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   一个页面 5 次网络往返、1000ms——UI 代码还兼职拼数据\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 客户改电话（u001） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 6 次往返：updatePhone(u001, 139-0000-0002)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   改一个字段又是一趟网络——字段一多，网络跑成筛子\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>病灶：\u003Cstrong>网络被当成了本地内存总线\u003C\u002Fstrong>。getName() 在本地不过一次内存访问，一个字段一个方法顺理成章；同样的接口搬到网络上，一次调用就是 200ms。更糟的是 UI 兼职拼数据：实体每加一个字段，客户端就多一趟网络、多一行拼装。注意：功能一切正常，资料页照样渲染、电话照样改——反面教材从来不是功能错误，是结构错误。\u003C\u002Fp>\n\u003Ch3 id=\"toc-6\">4.2 正解：传输对象\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>DTO——纯数据快照，可序列化，没有行为\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ClientProfileDto\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> implements\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Serializable\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> long\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> serialVersionUID \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1L\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String id;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String name;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String phone;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String city;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String street;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F getter\u002Fsetter 全是普通读写，略\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F** 把字段拼成人话——㉘ describe() 的影子，在这里长成正式身份 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> String \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">display\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"姓名=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 电话=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> phone \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 城市=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> city \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 街道=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> street \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" VIP=\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vipLevel;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意：实体七个字段，快照里只有六个——points 没上车，细节二展开。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>业务对象——读打包、写解包、行为单独开门\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 正解：粗粒度接口 + DTO。读一次打包全部字段；写一次打包送回；行为单独一条路 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ClientService\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">ClientEntity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> store \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> HashMap&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> calls \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ClientProfileDto \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getProfile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"getProfile(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ClientEntity entity \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ClientProfileDto dto \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ClientProfileDto\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setCity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setStreet\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getStreet\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setVipLevel\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getVipLevel\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dto;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> updateProfile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ClientProfileDto \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">dto\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"updateProfile(dto)\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ClientEntity entity \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getId\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setCity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCity\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        entity.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setStreet\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dto.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getStreet\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> upgradeVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"upgradeVip(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        store.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">upgradeVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> roundTrip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">what\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        calls\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [网络] 第 \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> calls \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" 次往返：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> what \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"……（耗时 200ms）\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>客户端——一次拿全，本地编辑，一次送回\u003C\u002Fstrong>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F** 正解客户端：一次 getProfile 拿全，本地改 DTO，一次 updateProfile 送回 *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ProfileScreen\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ClientService service;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ClientProfileDto current;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> render\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        current \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> service.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getProfile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [资料页] \"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">display\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> changePhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">newPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        current.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">setPhone\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(newPhone);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [资料页] 本地改 DTO：setPhone(\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> newPhone \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        service.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">updateProfile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(current);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> upgradeVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(String \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">id\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        service.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">upgradeVip\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>运行输出：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>========== 正解：传输对象，一来一回两趟网络 ==========\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 渲染客户资料页（u001） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 1 次往返：getProfile(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务器] 从实体拷贝字段，打包成 ClientProfileDto\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [资料页] 姓名=张三 电话=138-0000-0001 城市=北京 街道=长安街 1 号 VIP=3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   同一页 1 次往返——DTO 把整份快照一次送到\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 客户改电话（u001） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [资料页] 本地改 DTO：setPhone(139-0000-0002)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 2 次往返：updateProfile(dto)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [服务器] 写回实体：name\u002Fphone\u002Fcity\u002Fstreet（VIP 是业务字段，只有业务方法能动）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   改动打包一次送回——一来一回，网络只跑两趟\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>-- 客户要升 VIP（u001） --\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [资料页] DTO 上没有 upgradeVip()——业务行为不跟着数据出门\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [网络] 第 3 次往返：upgradeVip(u001)……（耗时 200ms）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   [实体] 积分 130 达标，VIP 升到 4\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>   数据走 DTO 的路，行为走业务接口的路——两条路不混\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三处对照读出含金量：同一页资料，反面 5 趟 1000ms，正解 \u003Cstrong>1 趟 200ms\u003C\u002Fstrong>——往返次数与字段数脱钩，字段再涨网络不涨；改电话反面是\"UI 喊服务器改一个字段\"，正解是\u003Cstrong>本地改快照、一次打包送回\u003C\u002Fstrong>——服务器一共只被打扰两次；升 VIP 最有意思——\u003Cstrong>DTO 上根本没有 upgradeVip()\u003C\u002Fstrong>，客户端想触发业务只能走业务接口那条路，积分规则从头到尾没离开过服务器。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">4.3 三处细节\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>细节一：DTO 是快照，不是实体的引用。\u003C\u002Fstrong> getProfile 返回的 ClientProfileDto 是 new 出来的新对象，字段从实体逐个拷贝——此后实体怎么变，快照不知道；快照怎么改，实体也不知道，直到 updateProfile 写回那一刻。本地调用里\"两边各一份\"是浪费，跨网络是必须——引用过不了网络，能过的只有序列化的字节流。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节二：字段要挑着带，不是照抄实体。\u003C\u002Fstrong> points 没进快照——它是业务中间态，客户端看到没意义；vipLevel 进了，但写回时不碰它——升级必须走 upgradeVip() 过积分校验。哪些字段出门、出门的字段哪些能写回，是设计决策而不是体力活。把实体字段一比一塞进 DTO，等于给客户端递了一份实体内部说明书。\u003C\u002Fp>\n\u003Cp>\u003Cstrong>细节三：贫血是刻意的，别治。\u003C\u002Fstrong> DTO 只有字段没有行为，教科书管这叫\"贫血对象\"还捎带批评——那说的是领域模型，不是 DTO。传输对象的职责就是运数据：行为要是跟着数据出了门，积分校验就管不住客户端了。\u003Cstrong>数据走一条路，行为走另一条路\u003C\u002Fstrong>，正是这个模式的灵魂。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、在框架里找它\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Cstrong>Java Record\u003C\u002Fstrong>：\u003Ccode>record ClientProfile(String id, String name, String phone) {}\u003C\u002Fcode>——一行定义不可变快照，构造、读访问、equals、hashCode 全部免写。JDK 14 亲手给 DTO 造的官配语法，\"两套类一份字段\"的样板代码从此少一半。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>MapStruct\u003C\u002Fstrong>：\u003Ccode>ClientProfileDto toDto(ClientEntity entity)\u003C\u002Fcode> 一个接口声明，字段拷贝在编译期生成实现——getProfile 里那六个 set，工业界早交给代码生成器了。同类的 Spring BeanUtils.copyProperties 走运行期反射，慢一点，但零声明。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>前后端的 Request\u002FResponse\u003C\u002Fstrong>：每个 REST 接口的 body 都是一份序列化快照；分层架构的 DO→DTO→VO 流水线，是把快照按读者细分——数据库里的 DO、给别处服务的 DTO、给前端渲染的 VO，同一份数据三种打包。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>GraphQL\u003C\u002Fstrong>：打包粒度的进化版——REST 是服务器定死一份快照，GraphQL 让客户端声明要哪几个字段，一次请求取回一棵自定义的树。和 ㉘ 里说的方向一致：从\"服务器说给你什么\"走向\"客户端说要什么\"。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Cstrong>名字的公案\u003C\u002Fstrong>：这个模式最初叫 Value Object——J2EE 蓝皮书早期版本和 Fowler 的 PoEAA 都用过这个名字；后来为了不和\"不可变值语义\"的 Value Object 混淆，蓝皮书改名 Transfer Object，Fowler 也专门撰文承认命名失当、改称 Data Transfer Object。今天满地的 VO\u002FDTO 之争，源头都在这段公案。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-9\">六、对账——㉝ 传输对象 vs ㉘ 组合实体：黄金搭档摊账\u003C\u002Fh2>\n\u003Cp>㉘ 结尾埋的话：\"㉝ 和 ㉘ 则是黄金搭档：组合实体住在服务器上管生命周期，传输对象负责把数据快照递到客户端手里。\"搭档摊账：\u003C\u002Fp>\n\u003Ctable>\n \u003Cthead>\n  \u003Ctr>\n   \u003Cth>\u003C\u002Fth>\n   \u003Cth>传输对象（㉝）\u003C\u002Fth>\n   \u003Cth>组合实体（㉘）\u003C\u002Fth>\n  \u003C\u002Ftr>\n \u003C\u002Fthead>\n \u003Ctbody>\n  \u003Ctr>\n   \u003Ctd>住在哪\u003C\u002Ftd>\n   \u003Ctd>网络两头跑，序列化上路\u003C\u002Ftd>\n   \u003Ctd>服务器上不动窝\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>管什么\u003C\u002Ftd>\n   \u003Ctd>数据怎么过网络\u003C\u002Ftd>\n   \u003Ctd>生命周期怎么成组（一次加载、一次提交）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>身上有什么\u003C\u002Ftd>\n   \u003Ctd>只有字段——贫血是刻意\u003C\u002Ftd>\n   \u003Ctd>字段 + 行为 + 一窝依赖对象\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>客户端碰什么\u003C\u002Ftd>\n   \u003Ctd>快照（本地可改，打包送回）\u003C\u002Ftd>\n   \u003Ctd>只碰组合实体的门（门后整窝进出）\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n   \u003Ctd>典型同框\u003C\u002Ftd>\n   \u003Ctd>EJB 标准答案：客户端 ←DTO→ Session Bean（业务对象）→ 组合实体 → 数据库\u003C\u002Ftd>\n  \u003C\u002Ftr>\n \u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>一句话：\u003Cstrong>㉘ 管服务器这头的\"进整窝、出整窝\"，㉝ 管网络上的\"一去一回各一趟\"\u003C\u002Fstrong>——一个在源头捆生命周期，一个在路上捆数据。㉘ 那篇还留过一张三兄弟对账表（⑨ 捆结构、㉝ 捆数据、㉘ 捆生命周期），本篇补上最后一块拼图。再拉 ㉗ ㉜ 入伙算个总账：㉜ 管\"服务从哪儿来\"，㉗ 管\"远程怎么说得像本地\"，㉝ 管\"数据怎么打包回去\"——找服务的路、通话的路、运数据的路，三路齐全，J2EE 八式就此收官。\u003C\u002Fp>\n\u003Ch2 id=\"toc-10\">写在最后\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>传输对象 = 跨边界的数据快照：只有字段、可序列化，一次打包整份送达\u003C\u002Fli>\n \u003Cli>疗效：往返次数与字段数脱钩（5 趟变 1 趟）；实体不出门，业务规则不出门；客户端手里自包含\u003C\u002Fli>\n \u003Cli>代价：两套类一份字段、快照会过期、写回边界靠约定——版本号与乐观锁是标配补丁\u003C\u002Fli>\n \u003Cli>铁律：数据走 DTO，行为走业务接口——两条路不混，贫血是刻意\u003C\u002Fli>\n \u003Cli>黄金搭档：㉘ 在源头捆生命周期，㉝ 在路上捆数据，⑨ 捆结构——三兄弟的账，两篇合清\u003C\u002Fli>\n \u003Cli>当代转世：Record 一行定义、MapStruct 编译期拷贝、每个 JSON Response——你天天在写它，只是没叫它这个大名\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>J2EE 八式至此收官。回头望，从 ② 单例一路走到 ㉝，三十二个模式各治一种痛；可每次诊断病灶、开出药方，反复露面的都是几张熟面孔——单一职责、开闭、依赖倒置……它们从没单独出场，却藏在每一篇的病理与药理里。该算总账了——六大原则再回首，全系列最后一篇见。\u003C\u002Fp>",[120,123,126,129,132,136,139,142,145,148],{"id":121,"text":122,"level":115},"toc-1","一、传输对象模式是什么",{"id":124,"text":125,"level":115},"toc-2","二、优点与缺点",{"id":127,"text":128,"level":115},"toc-3","三、什么时候用",{"id":130,"text":131,"level":115},"toc-4","四、实现方式：快照怎么打包",{"id":133,"text":134,"level":135},"toc-5","4.1 反面教材：细粒度远程接口",3,{"id":137,"text":138,"level":135},"toc-6","4.2 正解：传输对象",{"id":140,"text":141,"level":135},"toc-7","4.3 三处细节",{"id":143,"text":144,"level":115},"toc-8","五、在框架里找它",{"id":146,"text":147,"level":115},"toc-9","六、对账——㉝ 传输对象 vs ㉘ 组合实体：黄金搭档摊账",{"id":149,"text":150,"level":115},"toc-10","写在最后"]