diff --git a/404.html b/404.html index d5d285fc..5c42538b 100644 --- a/404.html +++ b/404.html @@ -33,11 +33,11 @@ 《软件设计的哲学》 - - + + -

404

That's a Four-Oh-Four.
Take me home
- +

404

How did we get here?
Take me home
+ diff --git a/assets/404.html-B-0HHih8.js b/assets/404.html-Dvmo5moK.js similarity index 83% rename from assets/404.html-B-0HHih8.js rename to assets/404.html-Dvmo5moK.js index aeba8df1..4f68a489 100644 --- a/assets/404.html-B-0HHih8.js +++ b/assets/404.html-Dvmo5moK.js @@ -1 +1 @@ -import{_ as e,c as o,a,o as l}from"./app-CvqtBB8Z.js";const n={};function r(s,t){return l(),o("div",null,t[0]||(t[0]=[a("p",null,"404 Not Found",-1)]))}const _=e(n,[["render",r],["__file","404.html.vue"]]),i=JSON.parse('{"path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound"},"headers":[],"git":{},"filePathRelative":null}');export{_ as comp,i as data}; +import{_ as e,c as o,a,o as l}from"./app-BQdhfat9.js";const n={};function r(s,t){return l(),o("div",null,t[0]||(t[0]=[a("p",null,"404 Not Found",-1)]))}const _=e(n,[["render",r],["__file","404.html.vue"]]),i=JSON.parse('{"path":"/404.html","title":"","lang":"zh-CN","frontmatter":{"layout":"NotFound"},"headers":[],"git":{},"filePathRelative":null}');export{_ as comp,i as data}; diff --git a/assets/app-CvqtBB8Z.js b/assets/app-BQdhfat9.js similarity index 99% rename from assets/app-CvqtBB8Z.js rename to assets/app-BQdhfat9.js index 643b3451..0deed128 100644 --- a/assets/app-CvqtBB8Z.js +++ b/assets/app-BQdhfat9.js @@ -30,7 +30,7 @@ Server rendered element contains fewer child nodes than client vdom.`),C=!0),cn( * @vue/runtime-dom v3.5.13 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let xs;const ca=typeof window<"u"&&window.trustedTypes;if(ca)try{xs=ca.createPolicy("vue",{createHTML:e=>e})}catch{}const bc=xs?e=>xs.createHTML(e):e=>e,Sh="http://www.w3.org/2000/svg",Ah="http://www.w3.org/1998/Math/MathML",Mt=typeof document<"u"?document:null,fa=Mt&&Mt.createElement("template"),Ch={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t==="svg"?Mt.createElementNS(Sh,e):t==="mathml"?Mt.createElementNS(Ah,e):n?Mt.createElement(e,{is:n}):Mt.createElement(e);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Mt.createTextNode(e),createComment:e=>Mt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Mt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{fa.innerHTML=bc(r==="svg"?`${e}`:r==="mathml"?`${e}`:e);const a=fa.content;if(r==="svg"||r==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},qt="transition",ir="animation",Ir=Symbol("_vtc"),Ec={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Th=Ne({},Hu,Ec),wh=e=>(e.displayName="Transition",e.props=Th,e),fi=wh((e,{slots:t})=>_e(hp,Oh(e),t)),dn=(e,t=[])=>{re(e)?e.forEach(n=>n(...t)):e&&e(...t)},da=e=>e?re(e)?e.some(t=>t.length>1):e.length>1:!1;function Oh(e){const t={};for(const C in e)C in Ec||(t[C]=e[C]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:a=`${n}-enter-to`,appearFromClass:l=s,appearActiveClass:u=i,appearToClass:c=a,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:p=`${n}-leave-to`}=e,_=xh(o),v=_&&_[0],b=_&&_[1],{onBeforeEnter:E,onEnter:A,onEnterCancelled:m,onLeave:y,onLeaveCancelled:L,onBeforeAppear:W=E,onAppear:M=A,onAppearCancelled:T=m}=t,U=(C,D,Z,ee)=>{C._enterCancelled=ee,pn(C,D?c:a),pn(C,D?u:i),Z&&Z()},O=(C,D)=>{C._isLeaving=!1,pn(C,f),pn(C,p),pn(C,d),D&&D()},B=C=>(D,Z)=>{const ee=C?M:A,I=()=>U(D,C,Z);dn(ee,[D,I]),pa(()=>{pn(D,C?l:s),Lt(D,C?c:a),da(ee)||ha(D,r,v,I)})};return Ne(t,{onBeforeEnter(C){dn(E,[C]),Lt(C,s),Lt(C,i)},onBeforeAppear(C){dn(W,[C]),Lt(C,l),Lt(C,u)},onEnter:B(!1),onAppear:B(!0),onLeave(C,D){C._isLeaving=!0;const Z=()=>O(C,D);Lt(C,f),C._enterCancelled?(Lt(C,d),_a()):(_a(),Lt(C,d)),pa(()=>{C._isLeaving&&(pn(C,f),Lt(C,p),da(y)||ha(C,r,b,Z))}),dn(y,[C,Z])},onEnterCancelled(C){U(C,!1,void 0,!0),dn(m,[C])},onAppearCancelled(C){U(C,!0,void 0,!0),dn(T,[C])},onLeaveCancelled(C){O(C),dn(L,[C])}})}function xh(e){if(e==null)return null;if(Oe(e))return[os(e.enter),os(e.leave)];{const t=os(e);return[t,t]}}function os(e){return ad(e)}function Lt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Ir]||(e[Ir]=new Set)).add(t)}function pn(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[Ir];n&&(n.delete(t),n.size||(e[Ir]=void 0))}function pa(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let kh=0;function ha(e,t,n,r){const o=e._endId=++kh,s=()=>{o===e._endId&&r()};if(n!=null)return setTimeout(s,n);const{type:i,timeout:a,propCount:l}=Ph(e,t);if(!i)return r();const u=i+"end";let c=0;const f=()=>{e.removeEventListener(u,d),s()},d=p=>{p.target===e&&++c>=l&&f()};setTimeout(()=>{c(n[_]||"").split(", "),o=r(`${qt}Delay`),s=r(`${qt}Duration`),i=ma(o,s),a=r(`${ir}Delay`),l=r(`${ir}Duration`),u=ma(a,l);let c=null,f=0,d=0;t===qt?i>0&&(c=qt,f=i,d=s.length):t===ir?u>0&&(c=ir,f=u,d=l.length):(f=Math.max(i,u),c=f>0?i>u?qt:ir:null,d=c?c===qt?s.length:l.length:0);const p=c===qt&&/\b(transform|all)(,|$)/.test(r(`${qt}Property`).toString());return{type:c,timeout:f,propCount:d,hasTransform:p}}function ma(e,t){for(;e.lengthga(n)+ga(e[r])))}function ga(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function _a(){return document.body.offsetHeight}function Ih(e,t,n){const r=e[Ir];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const So=Symbol("_vod"),Sc=Symbol("_vsh"),Ao={beforeMount(e,{value:t},{transition:n}){e[So]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):ar(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),ar(e,!0),r.enter(e)):r.leave(e,()=>{ar(e,!1)}):ar(e,t))},beforeUnmount(e,{value:t}){ar(e,t)}};function ar(e,t){e.style.display=t?e[So]:"none",e[Sc]=!t}const Rh=Symbol(""),Lh=/(^|;)\s*display\s*:/;function Dh(e,t,n){const r=e.style,o=xe(n);let s=!1;if(n&&!o){if(t)if(xe(t))for(const i of t.split(";")){const a=i.slice(0,i.indexOf(":")).trim();n[a]==null&&lo(r,a,"")}else for(const i in t)n[i]==null&&lo(r,i,"");for(const i in n)i==="display"&&(s=!0),lo(r,i,n[i])}else if(o){if(t!==n){const i=r[Rh];i&&(n+=";"+i),r.cssText=n,s=Lh.test(n)}}else t&&e.removeAttribute("style");So in e&&(e[So]=s?r.display:"",e[Sc]&&(r.display="none"))}const va=/\s*!important$/;function lo(e,t,n){if(re(n))n.forEach(r=>lo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Vh(e,t);va.test(n)?e.setProperty(Ut(r),n.replace(va,""),"important"):e[r]=n}}const ya=["Webkit","Moz","ms"],ss={};function Vh(e,t){const n=ss[t];if(n)return n;let r=ot(t);if(r!=="filter"&&r in e)return ss[t]=r;r=Br(r);for(let o=0;ois||(Bh.then(()=>is=0),is=Date.now());function Uh(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;St(jh(r,n.value),t,5,[r])};return n.value=e,n.attached=$h(),n}function jh(e,t){if(re(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Ta=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,zh=(e,t,n,r,o,s)=>{const i=o==="svg";t==="class"?Ih(e,r,i):t==="style"?Dh(e,n,r):Hr(t)?Ws(t)||Fh(e,t,n,r,s):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Kh(e,t,r,i))?(Sa(e,t,r),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Ea(e,t,r,i,s,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!xe(r))?Sa(e,ot(t),r,s,t):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Ea(e,t,r,i))};function Kh(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ta(t)&&oe(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return Ta(t)&&xe(n)?!1:t in e}const Wh={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},qh=(e,t)=>{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=o=>{if(!("key"in o))return;const s=Ut(o.key);if(t.some(i=>i===s||Wh[i]===s))return e(o)})},Gh=Ne({patchProp:zh},Ch);let as,wa=!1;function Yh(){return as=wa?as:Xp(Gh),wa=!0,as}const Xh=(...e)=>{const t=Yh().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=Jh(r);if(o)return n(o,!0,Zh(o))},t};function Zh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Jh(e){return xe(e)?document.querySelector(e):e}var Kr=e=>/^[a-z][a-z0-9+.-]*:/.test(e)||e.startsWith("//"),Qh=/.md((\?|#).*)?$/,em=(e,t="/")=>Kr(e)||e.startsWith("/")&&!e.startsWith(t)&&!Qh.test(e),No=e=>/^(https?:)?\/\//.test(e),Oa=e=>{if(!e||e.endsWith("/"))return e;let t=e.replace(/(^|\/)README.md$/i,"$1index.html");return t.endsWith(".md")?t=`${t.substring(0,t.length-3)}.html`:t.endsWith(".html")||(t=`${t}.html`),t.endsWith("/index.html")&&(t=t.substring(0,t.length-10)),t},tm="http://.",nm=(e,t)=>{if(!e.startsWith("/")&&t){const n=t.slice(0,t.lastIndexOf("/"));return Oa(new URL(`${n}/${e}`,tm).pathname)}return Oa(e)},rm=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"},om=/(#|\?)/,Ac=e=>{const[t,...n]=e.split(om);return{pathname:t,hashAndQueries:n.join("")}},sm=["link","meta","script","style","noscript","template"],im=["title","base"],am=([e,t,n])=>im.includes(e)?e:sm.includes(e)?e==="meta"&&t.name?`${e}.${t.name}`:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,Object.entries(t).map(([r,o])=>typeof o=="boolean"?o?[r,""]:null:[r,o]).filter(r=>r!=null).sort(([r],[o])=>r.localeCompare(o)),n]):null,lm=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=am(r);o&&!t.has(o)&&(t.add(o),n.push(r))}),n},um=e=>e.endsWith("/")||e.endsWith(".html")?e:`${e}/`,Cc=e=>e.endsWith("/")?e.slice(0,-1):e,Tc=e=>e.startsWith("/")?e.slice(1):e,di=e=>Object.prototype.toString.call(e)==="[object Object]",bt=e=>typeof e=="string";const cm=JSON.parse("{}"),fm=Object.fromEntries([["/",{loader:()=>Ce(()=>import("./index.html-C2UbQzrl.js"),[]),meta:{title:"目录"}}],["/ch01.html",{loader:()=>Ce(()=>import("./ch01.html-D6ExKKlk.js"),[]),meta:{title:"第 1 章 介绍"}}],["/ch02.html",{loader:()=>Ce(()=>import("./ch02.html-Dk8nl67z.js"),[]),meta:{title:"第 2 章 复杂性的本质"}}],["/ch03.html",{loader:()=>Ce(()=>import("./ch03.html-umPzp8To.js"),[]),meta:{title:"第 3 章 工作代码是不够的"}}],["/ch04.html",{loader:()=>Ce(()=>import("./ch04.html-DkQGekeV.js"),[]),meta:{title:"第 4 章 模块应该是深的"}}],["/ch05.html",{loader:()=>Ce(()=>import("./ch05.html-CdTpMeLr.js"),[]),meta:{title:"第 5 章 信息隐藏(和泄漏)"}}],["/ch06.html",{loader:()=>Ce(()=>import("./ch06.html-DA1mEo-t.js"),[]),meta:{title:"第 6 章 通用模块更深入"}}],["/ch07.html",{loader:()=>Ce(()=>import("./ch07.html-DK38cxa_.js"),[]),meta:{title:"第 7 章 不同的层,不同的抽象"}}],["/ch08.html",{loader:()=>Ce(()=>import("./ch08.html-BUqMm1Bk.js"),[]),meta:{title:"第 8 章 降低复杂性"}}],["/ch09.html",{loader:()=>Ce(()=>import("./ch09.html-CdBSB_a2.js"),[]),meta:{title:"第 9 章 在一起更好还是分开更好?"}}],["/ch10.html",{loader:()=>Ce(()=>import("./ch10.html-B5eK6nuj.js"),[]),meta:{title:"第 10 章 通过定义规避错误"}}],["/ch11.html",{loader:()=>Ce(()=>import("./ch11.html-CMvLx2cz.js"),[]),meta:{title:"第 11 章 设计两次"}}],["/ch12.html",{loader:()=>Ce(()=>import("./ch12.html-DHfEHiJg.js"),[]),meta:{title:"第 12 章 为什么要写注释?有四个理由"}}],["/ch13.html",{loader:()=>Ce(()=>import("./ch13.html-Ca7OIfBM.js"),[]),meta:{title:"第 13 章 注释应该描述代码中不明显的内容"}}],["/ch14.html",{loader:()=>Ce(()=>import("./ch14.html-BOfqUa3t.js"),[]),meta:{title:"第 14 章 选择的名字"}}],["/ch15.html",{loader:()=>Ce(()=>import("./ch15.html-4sanLber.js"),[]),meta:{title:"第 15 章 先写注释"}}],["/ch16.html",{loader:()=>Ce(()=>import("./ch16.html-CYy_oqbT.js"),[]),meta:{title:"第 16 章 修改现有的代码"}}],["/ch17.html",{loader:()=>Ce(()=>import("./ch17.html-CJ9pGCip.js"),[]),meta:{title:"第 17 章 一致性"}}],["/ch18.html",{loader:()=>Ce(()=>import("./ch18.html-Cq8r6WwA.js"),[]),meta:{title:"第 18 章 代码应该是显而易见的"}}],["/ch19.html",{loader:()=>Ce(()=>import("./ch19.html-_fJpQetx.js"),[]),meta:{title:"第 19 章 软件发展趋势"}}],["/ch20.html",{loader:()=>Ce(()=>import("./ch20.html-DDvZmVuW.js"),[]),meta:{title:"第 20 章 设计性能"}}],["/ch21.html",{loader:()=>Ce(()=>import("./ch21.html-Q8Jm88-g.js"),[]),meta:{title:"第 21 章 结论"}}],["/preface.html",{loader:()=>Ce(()=>import("./preface.html-CFq2nus8.js"),[]),meta:{title:"前言"}}],["/summary.html",{loader:()=>Ce(()=>import("./summary.html-eSWz3qhW.js"),[]),meta:{title:"总结"}}],["/404.html",{loader:()=>Ce(()=>import("./404.html-B-0HHih8.js"),[]),meta:{title:""}}]]);function dm(){return wc().__VUE_DEVTOOLS_GLOBAL_HOOK__}function wc(){return typeof navigator<"u"&&typeof window<"u"?window:typeof globalThis<"u"?globalThis:{}}const pm=typeof Proxy=="function",hm="devtools-plugin:setup",mm="plugin:settings:set";let xn,ks;function gm(){var e;return xn!==void 0||(typeof window<"u"&&window.performance?(xn=!0,ks=window.performance):typeof globalThis<"u"&&(!((e=globalThis.perf_hooks)===null||e===void 0)&&e.performance)?(xn=!0,ks=globalThis.perf_hooks.performance):xn=!1),xn}function _m(){return gm()?ks.now():Date.now()}class vm{constructor(t,n){this.target=null,this.targetQueue=[],this.onQueue=[],this.plugin=t,this.hook=n;const r={};if(t.settings)for(const i in t.settings){const a=t.settings[i];r[i]=a.defaultValue}const o=`__vue-devtools-plugin-settings__${t.id}`;let s=Object.assign({},r);try{const i=localStorage.getItem(o),a=JSON.parse(i);Object.assign(s,a)}catch{}this.fallbacks={getSettings(){return s},setSettings(i){try{localStorage.setItem(o,JSON.stringify(i))}catch{}s=i},now(){return _m()}},n&&n.on(mm,(i,a)=>{i===this.plugin.id&&this.fallbacks.setSettings(a)}),this.proxiedOn=new Proxy({},{get:(i,a)=>this.target?this.target.on[a]:(...l)=>{this.onQueue.push({method:a,args:l})}}),this.proxiedTarget=new Proxy({},{get:(i,a)=>this.target?this.target[a]:a==="on"?this.proxiedOn:Object.keys(this.fallbacks).includes(a)?(...l)=>(this.targetQueue.push({method:a,args:l,resolve:()=>{}}),this.fallbacks[a](...l)):(...l)=>new Promise(u=>{this.targetQueue.push({method:a,args:l,resolve:u})})})}async setRealTarget(t){this.target=t;for(const n of this.onQueue)this.target.on[n.method](...n.args);for(const n of this.targetQueue)n.resolve(await this.target[n.method](...n.args))}}function ym(e,t){const n=e,r=wc(),o=dm(),s=pm&&n.enableEarlyProxy;if(o&&(r.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__||!s))o.emit(hm,e,t);else{const i=s?new vm(n,o):null;(r.__VUE_DEVTOOLS_PLUGINS__=r.__VUE_DEVTOOLS_PLUGINS__||[]).push({pluginDescriptor:n,setupFn:t,proxy:i}),i&&t(i.proxiedTarget)}}/*! +**/let xs;const ca=typeof window<"u"&&window.trustedTypes;if(ca)try{xs=ca.createPolicy("vue",{createHTML:e=>e})}catch{}const bc=xs?e=>xs.createHTML(e):e=>e,Sh="http://www.w3.org/2000/svg",Ah="http://www.w3.org/1998/Math/MathML",Mt=typeof document<"u"?document:null,fa=Mt&&Mt.createElement("template"),Ch={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t==="svg"?Mt.createElementNS(Sh,e):t==="mathml"?Mt.createElementNS(Ah,e):n?Mt.createElement(e,{is:n}):Mt.createElement(e);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Mt.createTextNode(e),createComment:e=>Mt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Mt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const i=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{fa.innerHTML=bc(r==="svg"?`${e}`:r==="mathml"?`${e}`:e);const a=fa.content;if(r==="svg"||r==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},qt="transition",ir="animation",Ir=Symbol("_vtc"),Ec={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Th=Ne({},Hu,Ec),wh=e=>(e.displayName="Transition",e.props=Th,e),fi=wh((e,{slots:t})=>_e(hp,Oh(e),t)),dn=(e,t=[])=>{re(e)?e.forEach(n=>n(...t)):e&&e(...t)},da=e=>e?re(e)?e.some(t=>t.length>1):e.length>1:!1;function Oh(e){const t={};for(const C in e)C in Ec||(t[C]=e[C]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:a=`${n}-enter-to`,appearFromClass:l=s,appearActiveClass:u=i,appearToClass:c=a,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:p=`${n}-leave-to`}=e,_=xh(o),v=_&&_[0],b=_&&_[1],{onBeforeEnter:E,onEnter:A,onEnterCancelled:m,onLeave:y,onLeaveCancelled:L,onBeforeAppear:W=E,onAppear:M=A,onAppearCancelled:T=m}=t,U=(C,D,Z,ee)=>{C._enterCancelled=ee,pn(C,D?c:a),pn(C,D?u:i),Z&&Z()},O=(C,D)=>{C._isLeaving=!1,pn(C,f),pn(C,p),pn(C,d),D&&D()},B=C=>(D,Z)=>{const ee=C?M:A,I=()=>U(D,C,Z);dn(ee,[D,I]),pa(()=>{pn(D,C?l:s),Lt(D,C?c:a),da(ee)||ha(D,r,v,I)})};return Ne(t,{onBeforeEnter(C){dn(E,[C]),Lt(C,s),Lt(C,i)},onBeforeAppear(C){dn(W,[C]),Lt(C,l),Lt(C,u)},onEnter:B(!1),onAppear:B(!0),onLeave(C,D){C._isLeaving=!0;const Z=()=>O(C,D);Lt(C,f),C._enterCancelled?(Lt(C,d),_a()):(_a(),Lt(C,d)),pa(()=>{C._isLeaving&&(pn(C,f),Lt(C,p),da(y)||ha(C,r,b,Z))}),dn(y,[C,Z])},onEnterCancelled(C){U(C,!1,void 0,!0),dn(m,[C])},onAppearCancelled(C){U(C,!0,void 0,!0),dn(T,[C])},onLeaveCancelled(C){O(C),dn(L,[C])}})}function xh(e){if(e==null)return null;if(Oe(e))return[os(e.enter),os(e.leave)];{const t=os(e);return[t,t]}}function os(e){return ad(e)}function Lt(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[Ir]||(e[Ir]=new Set)).add(t)}function pn(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[Ir];n&&(n.delete(t),n.size||(e[Ir]=void 0))}function pa(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let kh=0;function ha(e,t,n,r){const o=e._endId=++kh,s=()=>{o===e._endId&&r()};if(n!=null)return setTimeout(s,n);const{type:i,timeout:a,propCount:l}=Ph(e,t);if(!i)return r();const u=i+"end";let c=0;const f=()=>{e.removeEventListener(u,d),s()},d=p=>{p.target===e&&++c>=l&&f()};setTimeout(()=>{c(n[_]||"").split(", "),o=r(`${qt}Delay`),s=r(`${qt}Duration`),i=ma(o,s),a=r(`${ir}Delay`),l=r(`${ir}Duration`),u=ma(a,l);let c=null,f=0,d=0;t===qt?i>0&&(c=qt,f=i,d=s.length):t===ir?u>0&&(c=ir,f=u,d=l.length):(f=Math.max(i,u),c=f>0?i>u?qt:ir:null,d=c?c===qt?s.length:l.length:0);const p=c===qt&&/\b(transform|all)(,|$)/.test(r(`${qt}Property`).toString());return{type:c,timeout:f,propCount:d,hasTransform:p}}function ma(e,t){for(;e.lengthga(n)+ga(e[r])))}function ga(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function _a(){return document.body.offsetHeight}function Ih(e,t,n){const r=e[Ir];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const So=Symbol("_vod"),Sc=Symbol("_vsh"),Ao={beforeMount(e,{value:t},{transition:n}){e[So]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):ar(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),ar(e,!0),r.enter(e)):r.leave(e,()=>{ar(e,!1)}):ar(e,t))},beforeUnmount(e,{value:t}){ar(e,t)}};function ar(e,t){e.style.display=t?e[So]:"none",e[Sc]=!t}const Rh=Symbol(""),Lh=/(^|;)\s*display\s*:/;function Dh(e,t,n){const r=e.style,o=xe(n);let s=!1;if(n&&!o){if(t)if(xe(t))for(const i of t.split(";")){const a=i.slice(0,i.indexOf(":")).trim();n[a]==null&&lo(r,a,"")}else for(const i in t)n[i]==null&&lo(r,i,"");for(const i in n)i==="display"&&(s=!0),lo(r,i,n[i])}else if(o){if(t!==n){const i=r[Rh];i&&(n+=";"+i),r.cssText=n,s=Lh.test(n)}}else t&&e.removeAttribute("style");So in e&&(e[So]=s?r.display:"",e[Sc]&&(r.display="none"))}const va=/\s*!important$/;function lo(e,t,n){if(re(n))n.forEach(r=>lo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=Vh(e,t);va.test(n)?e.setProperty(Ut(r),n.replace(va,""),"important"):e[r]=n}}const ya=["Webkit","Moz","ms"],ss={};function Vh(e,t){const n=ss[t];if(n)return n;let r=ot(t);if(r!=="filter"&&r in e)return ss[t]=r;r=Br(r);for(let o=0;ois||(Bh.then(()=>is=0),is=Date.now());function Uh(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;St(jh(r,n.value),t,5,[r])};return n.value=e,n.attached=$h(),n}function jh(e,t){if(re(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Ta=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,zh=(e,t,n,r,o,s)=>{const i=o==="svg";t==="class"?Ih(e,r,i):t==="style"?Dh(e,n,r):Hr(t)?Ws(t)||Fh(e,t,n,r,s):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Kh(e,t,r,i))?(Sa(e,t,r),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Ea(e,t,r,i,s,t!=="value")):e._isVueCE&&(/[A-Z]/.test(t)||!xe(r))?Sa(e,ot(t),r,s,t):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),Ea(e,t,r,i))};function Kh(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&Ta(t)&&oe(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const o=e.tagName;if(o==="IMG"||o==="VIDEO"||o==="CANVAS"||o==="SOURCE")return!1}return Ta(t)&&xe(n)?!1:t in e}const Wh={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},qh=(e,t)=>{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=o=>{if(!("key"in o))return;const s=Ut(o.key);if(t.some(i=>i===s||Wh[i]===s))return e(o)})},Gh=Ne({patchProp:zh},Ch);let as,wa=!1;function Yh(){return as=wa?as:Xp(Gh),wa=!0,as}const Xh=(...e)=>{const t=Yh().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=Jh(r);if(o)return n(o,!0,Zh(o))},t};function Zh(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Jh(e){return xe(e)?document.querySelector(e):e}var Kr=e=>/^[a-z][a-z0-9+.-]*:/.test(e)||e.startsWith("//"),Qh=/.md((\?|#).*)?$/,em=(e,t="/")=>Kr(e)||e.startsWith("/")&&!e.startsWith(t)&&!Qh.test(e),No=e=>/^(https?:)?\/\//.test(e),Oa=e=>{if(!e||e.endsWith("/"))return e;let t=e.replace(/(^|\/)README.md$/i,"$1index.html");return t.endsWith(".md")?t=`${t.substring(0,t.length-3)}.html`:t.endsWith(".html")||(t=`${t}.html`),t.endsWith("/index.html")&&(t=t.substring(0,t.length-10)),t},tm="http://.",nm=(e,t)=>{if(!e.startsWith("/")&&t){const n=t.slice(0,t.lastIndexOf("/"));return Oa(new URL(`${n}/${e}`,tm).pathname)}return Oa(e)},rm=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"},om=/(#|\?)/,Ac=e=>{const[t,...n]=e.split(om);return{pathname:t,hashAndQueries:n.join("")}},sm=["link","meta","script","style","noscript","template"],im=["title","base"],am=([e,t,n])=>im.includes(e)?e:sm.includes(e)?e==="meta"&&t.name?`${e}.${t.name}`:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,Object.entries(t).map(([r,o])=>typeof o=="boolean"?o?[r,""]:null:[r,o]).filter(r=>r!=null).sort(([r],[o])=>r.localeCompare(o)),n]):null,lm=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=am(r);o&&!t.has(o)&&(t.add(o),n.push(r))}),n},um=e=>e.endsWith("/")||e.endsWith(".html")?e:`${e}/`,Cc=e=>e.endsWith("/")?e.slice(0,-1):e,Tc=e=>e.startsWith("/")?e.slice(1):e,di=e=>Object.prototype.toString.call(e)==="[object Object]",bt=e=>typeof e=="string";const cm=JSON.parse("{}"),fm=Object.fromEntries([["/",{loader:()=>Ce(()=>import("./index.html-_5zP8eA1.js"),[]),meta:{title:"目录"}}],["/ch01.html",{loader:()=>Ce(()=>import("./ch01.html-BM9hxgpH.js"),[]),meta:{title:"第 1 章 介绍"}}],["/ch02.html",{loader:()=>Ce(()=>import("./ch02.html-CPlVQyLd.js"),[]),meta:{title:"第 2 章 复杂性的本质"}}],["/ch03.html",{loader:()=>Ce(()=>import("./ch03.html-V7G6GftO.js"),[]),meta:{title:"第 3 章 工作代码是不够的"}}],["/ch04.html",{loader:()=>Ce(()=>import("./ch04.html-C8UCSGA_.js"),[]),meta:{title:"第 4 章 模块应该是深的"}}],["/ch05.html",{loader:()=>Ce(()=>import("./ch05.html-DkKNi3Y4.js"),[]),meta:{title:"第 5 章 信息隐藏(和泄漏)"}}],["/ch06.html",{loader:()=>Ce(()=>import("./ch06.html-BJln56Ye.js"),[]),meta:{title:"第 6 章 通用模块更深入"}}],["/ch07.html",{loader:()=>Ce(()=>import("./ch07.html-BAAHFTXC.js"),[]),meta:{title:"第 7 章 不同的层,不同的抽象"}}],["/ch08.html",{loader:()=>Ce(()=>import("./ch08.html-CemH-4b2.js"),[]),meta:{title:"第 8 章 降低复杂性"}}],["/ch09.html",{loader:()=>Ce(()=>import("./ch09.html-DbK-CbmQ.js"),[]),meta:{title:"第 9 章 在一起更好还是分开更好?"}}],["/ch10.html",{loader:()=>Ce(()=>import("./ch10.html-Cs5324Hi.js"),[]),meta:{title:"第 10 章 通过定义规避错误"}}],["/ch11.html",{loader:()=>Ce(()=>import("./ch11.html-bgw0RYTe.js"),[]),meta:{title:"第 11 章 设计两次"}}],["/ch12.html",{loader:()=>Ce(()=>import("./ch12.html-DoWOdQUQ.js"),[]),meta:{title:"第 12 章 为什么要写注释?有四个理由"}}],["/ch13.html",{loader:()=>Ce(()=>import("./ch13.html-BAyTk7fJ.js"),[]),meta:{title:"第 13 章 注释应该描述代码中不明显的内容"}}],["/ch14.html",{loader:()=>Ce(()=>import("./ch14.html-D75FiwUn.js"),[]),meta:{title:"第 14 章 选择的名字"}}],["/ch15.html",{loader:()=>Ce(()=>import("./ch15.html-BK0-CmCt.js"),[]),meta:{title:"第 15 章 先写注释"}}],["/ch16.html",{loader:()=>Ce(()=>import("./ch16.html-DwaSHeqm.js"),[]),meta:{title:"第 16 章 修改现有的代码"}}],["/ch17.html",{loader:()=>Ce(()=>import("./ch17.html-Bs7DbX5Q.js"),[]),meta:{title:"第 17 章 一致性"}}],["/ch18.html",{loader:()=>Ce(()=>import("./ch18.html-N4aSI0ya.js"),[]),meta:{title:"第 18 章 代码应该是显而易见的"}}],["/ch19.html",{loader:()=>Ce(()=>import("./ch19.html-5VdqwktA.js"),[]),meta:{title:"第 19 章 软件发展趋势"}}],["/ch20.html",{loader:()=>Ce(()=>import("./ch20.html-Do36buxh.js"),[]),meta:{title:"第 20 章 设计性能"}}],["/ch21.html",{loader:()=>Ce(()=>import("./ch21.html-Cc6N4a4h.js"),[]),meta:{title:"第 21 章 结论"}}],["/preface.html",{loader:()=>Ce(()=>import("./preface.html-DOreJfjA.js"),[]),meta:{title:"前言"}}],["/summary.html",{loader:()=>Ce(()=>import("./summary.html-UdUuxpqv.js"),[]),meta:{title:"总结"}}],["/404.html",{loader:()=>Ce(()=>import("./404.html-Dvmo5moK.js"),[]),meta:{title:""}}]]);function dm(){return wc().__VUE_DEVTOOLS_GLOBAL_HOOK__}function wc(){return typeof navigator<"u"&&typeof window<"u"?window:typeof globalThis<"u"?globalThis:{}}const pm=typeof Proxy=="function",hm="devtools-plugin:setup",mm="plugin:settings:set";let xn,ks;function gm(){var e;return xn!==void 0||(typeof window<"u"&&window.performance?(xn=!0,ks=window.performance):typeof globalThis<"u"&&(!((e=globalThis.perf_hooks)===null||e===void 0)&&e.performance)?(xn=!0,ks=globalThis.perf_hooks.performance):xn=!1),xn}function _m(){return gm()?ks.now():Date.now()}class vm{constructor(t,n){this.target=null,this.targetQueue=[],this.onQueue=[],this.plugin=t,this.hook=n;const r={};if(t.settings)for(const i in t.settings){const a=t.settings[i];r[i]=a.defaultValue}const o=`__vue-devtools-plugin-settings__${t.id}`;let s=Object.assign({},r);try{const i=localStorage.getItem(o),a=JSON.parse(i);Object.assign(s,a)}catch{}this.fallbacks={getSettings(){return s},setSettings(i){try{localStorage.setItem(o,JSON.stringify(i))}catch{}s=i},now(){return _m()}},n&&n.on(mm,(i,a)=>{i===this.plugin.id&&this.fallbacks.setSettings(a)}),this.proxiedOn=new Proxy({},{get:(i,a)=>this.target?this.target.on[a]:(...l)=>{this.onQueue.push({method:a,args:l})}}),this.proxiedTarget=new Proxy({},{get:(i,a)=>this.target?this.target[a]:a==="on"?this.proxiedOn:Object.keys(this.fallbacks).includes(a)?(...l)=>(this.targetQueue.push({method:a,args:l,resolve:()=>{}}),this.fallbacks[a](...l)):(...l)=>new Promise(u=>{this.targetQueue.push({method:a,args:l,resolve:u})})})}async setRealTarget(t){this.target=t;for(const n of this.onQueue)this.target.on[n.method](...n.args);for(const n of this.targetQueue)n.resolve(await this.target[n.method](...n.args))}}function ym(e,t){const n=e,r=wc(),o=dm(),s=pm&&n.enableEarlyProxy;if(o&&(r.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__||!s))o.emit(hm,e,t);else{const i=s?new vm(n,o):null;(r.__VUE_DEVTOOLS_PLUGINS__=r.__VUE_DEVTOOLS_PLUGINS__||[]).push({pluginDescriptor:n,setupFn:t,proxy:i}),i&&t(i.proxiedTarget)}}/*! * vue-router v4.5.0 * (c) 2024 Eduardo San Martin Morote * @license MIT @@ -39,4 +39,4 @@ Expects a CSS selector, a Node element, a NodeList or an array. See: https://github.com/francoischalifour/medium-zoom`)}},F_=function(t){var n=document.createElement("div");return n.classList.add("medium-zoom-overlay"),n.style.background=t,n},H_=function(t){var n=t.getBoundingClientRect(),r=n.top,o=n.left,s=n.width,i=n.height,a=t.cloneNode(),l=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,u=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;return a.removeAttribute("id"),a.style.position="absolute",a.style.top=r+l+"px",a.style.left=o+u+"px",a.style.width=s+"px",a.style.height=i+"px",a.style.transform="",a},kn=function(t,n){var r=gn({bubbles:!1,cancelable:!1,detail:void 0},n);if(typeof window.CustomEvent=="function")return new CustomEvent(t,r);var o=document.createEvent("CustomEvent");return o.initCustomEvent(t,r.bubbles,r.cancelable,r.detail),o},B_=function e(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(C){function D(){}C(D,D)},o=function(C){var D=C.target;if(D===U){_();return}m.indexOf(D)!==-1&&v({target:D})},s=function(){if(!(L||!T.original)){var C=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(W-C)>M.scrollOffset&&setTimeout(_,150)}},i=function(C){var D=C.key||C.keyCode;(D==="Escape"||D==="Esc"||D===27)&&_()},a=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},D=C;if(C.background&&(U.style.background=C.background),C.container&&C.container instanceof Object&&(D.container=gn({},M.container,C.container)),C.template){var Z=uo(C.template)?C.template:document.querySelector(C.template);D.template=Z}return M=gn({},M,D),m.forEach(function(ee){ee.dispatchEvent(kn("medium-zoom:update",{detail:{zoom:O}}))}),O},l=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(gn({},M,C))},u=function(){for(var C=arguments.length,D=Array(C),Z=0;Z0?D.reduce(function(I,V){return[].concat(I,Ya(V))},[]):m;return ee.forEach(function(I){I.classList.remove("medium-zoom-image"),I.dispatchEvent(kn("medium-zoom:detach",{detail:{zoom:O}}))}),m=m.filter(function(I){return ee.indexOf(I)===-1}),O},f=function(C,D){var Z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return m.forEach(function(ee){ee.addEventListener("medium-zoom:"+C,D,Z)}),y.push({type:"medium-zoom:"+C,listener:D,options:Z}),O},d=function(C,D){var Z=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return m.forEach(function(ee){ee.removeEventListener("medium-zoom:"+C,D,Z)}),y=y.filter(function(ee){return!(ee.type==="medium-zoom:"+C&&ee.listener.toString()===D.toString())}),O},p=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},D=C.target,Z=function(){var I={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},V=void 0,z=void 0;if(M.container)if(M.container instanceof Object)I=gn({},I,M.container),V=I.width-I.left-I.right-M.margin*2,z=I.height-I.top-I.bottom-M.margin*2;else{var le=uo(M.container)?M.container:document.querySelector(M.container),ye=le.getBoundingClientRect(),ke=ye.width,be=ye.height,et=ye.left,_t=ye.top;I=gn({},I,{width:ke,height:be,left:et,top:_t})}V=V||I.width-M.margin*2,z=z||I.height-M.margin*2;var tt=T.zoomedHd||T.original,nt=Ga(tt)?V:tt.naturalWidth||V,x=Ga(tt)?z:tt.naturalHeight||z,q=tt.getBoundingClientRect(),j=q.top,J=q.left,ce=q.width,Ee=q.height,h=Math.min(Math.max(ce,nt),V)/ce,g=Math.min(Math.max(Ee,x),z)/Ee,S=Math.min(h,g),k=(-J+(V-ce)/2+M.margin+I.left)/S,w=(-j+(z-Ee)/2+M.margin+I.top)/S,R="scale("+S+") translate3d("+k+"px, "+w+"px, 0)";T.zoomed.style.transform=R,T.zoomedHd&&(T.zoomedHd.style.transform=R)};return new r(function(ee){if(D&&m.indexOf(D)===-1){ee(O);return}var I=function ke(){L=!1,T.zoomed.removeEventListener("transitionend",ke),T.original.dispatchEvent(kn("medium-zoom:opened",{detail:{zoom:O}})),ee(O)};if(T.zoomed){ee(O);return}if(D)T.original=D;else if(m.length>0){var V=m;T.original=V[0]}else{ee(O);return}if(T.original.dispatchEvent(kn("medium-zoom:open",{detail:{zoom:O}})),W=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,L=!0,T.zoomed=H_(T.original),document.body.appendChild(U),M.template){var z=uo(M.template)?M.template:document.querySelector(M.template);T.template=document.createElement("div"),T.template.appendChild(z.content.cloneNode(!0)),document.body.appendChild(T.template)}if(T.original.parentElement&&T.original.parentElement.tagName==="PICTURE"&&T.original.currentSrc&&(T.zoomed.src=T.original.currentSrc),document.body.appendChild(T.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),T.original.classList.add("medium-zoom-image--hidden"),T.zoomed.classList.add("medium-zoom-image--opened"),T.zoomed.addEventListener("click",_),T.zoomed.addEventListener("transitionend",I),T.original.getAttribute("data-zoom-src")){T.zoomedHd=T.zoomed.cloneNode(),T.zoomedHd.removeAttribute("srcset"),T.zoomedHd.removeAttribute("sizes"),T.zoomedHd.removeAttribute("loading"),T.zoomedHd.src=T.zoomed.getAttribute("data-zoom-src"),T.zoomedHd.onerror=function(){clearInterval(le),console.warn("Unable to reach the zoom image target "+T.zoomedHd.src),T.zoomedHd=null,Z()};var le=setInterval(function(){T.zoomedHd.complete&&(clearInterval(le),T.zoomedHd.classList.add("medium-zoom-image--opened"),T.zoomedHd.addEventListener("click",_),document.body.appendChild(T.zoomedHd),Z())},10)}else if(T.original.hasAttribute("srcset")){T.zoomedHd=T.zoomed.cloneNode(),T.zoomedHd.removeAttribute("sizes"),T.zoomedHd.removeAttribute("loading");var ye=T.zoomedHd.addEventListener("load",function(){T.zoomedHd.removeEventListener("load",ye),T.zoomedHd.classList.add("medium-zoom-image--opened"),T.zoomedHd.addEventListener("click",_),document.body.appendChild(T.zoomedHd),Z()})}else Z()})},_=function(){return new r(function(C){if(L||!T.original){C(O);return}var D=function Z(){T.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(T.zoomed),T.zoomedHd&&document.body.removeChild(T.zoomedHd),document.body.removeChild(U),T.zoomed.classList.remove("medium-zoom-image--opened"),T.template&&document.body.removeChild(T.template),L=!1,T.zoomed.removeEventListener("transitionend",Z),T.original.dispatchEvent(kn("medium-zoom:closed",{detail:{zoom:O}})),T.original=null,T.zoomed=null,T.zoomedHd=null,T.template=null,C(O)};L=!0,document.body.classList.remove("medium-zoom--opened"),T.zoomed.style.transform="",T.zoomedHd&&(T.zoomedHd.style.transform=""),T.template&&(T.template.style.transition="opacity 150ms",T.template.style.opacity=0),T.original.dispatchEvent(kn("medium-zoom:close",{detail:{zoom:O}})),T.zoomed.addEventListener("transitionend",D)})},v=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},D=C.target;return T.original?_():p({target:D})},b=function(){return M},E=function(){return m},A=function(){return T.original},m=[],y=[],L=!1,W=0,M=n,T={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?M=t:(t||typeof t=="string")&&u(t),M=gn({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},M);var U=F_(M.background);document.addEventListener("click",o),document.addEventListener("keyup",i),document.addEventListener("scroll",s),window.addEventListener("resize",_);var O={open:p,close:_,toggle:v,update:a,clone:l,attach:u,detach:c,on:f,off:d,getOptions:b,getImages:E,getZoomedImage:A};return O};function $_(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css",n==="top"&&r.firstChild?r.insertBefore(o,r.firstChild):r.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}var U_=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";$_(U_);const j_=Symbol("mediumZoom");var z_={};const K_="[vp-content] > img, [vp-content] :not(a) > img",W_=z_,q_=300,G_=un({enhance({app:e,router:t}){const n=B_(W_);n.refresh=(r=K_)=>{n.detach(),n.attach(r)},e.provide(j_,n),t.afterEach(()=>{ef(q_).then(()=>{n.refresh()})})}}),Y_=Object.freeze(Object.defineProperty({__proto__:null,default:G_},Symbol.toStringTag,{value:"Module"}));/** * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress * @license MIT - */const Xa=(e,t)=>{e.classList.add(t)},Za=(e,t)=>{e.classList.remove(t)},X_=e=>{var t;(t=e==null?void 0:e.parentNode)==null||t.removeChild(e)},fs=(e,t,n)=>en?n:e,Ja=e=>(-1+e)*100,Z_=(()=>{const e=[],t=()=>{const n=e.shift();n&&n(t)};return n=>{e.push(n),e.length===1&&t()}})(),J_=e=>e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(t,n)=>n.toUpperCase()),ro=(()=>{const e=["Webkit","O","Moz","ms"],t={},n=s=>{const{style:i}=document.body;if(s in i)return s;const a=s.charAt(0).toUpperCase()+s.slice(1);let l=e.length;for(;l--;){const u=`${e[l]}${a}`;if(u in i)return u}return s},r=s=>{const i=J_(s);return t[i]??(t[i]=n(i))},o=(s,i,a)=>{s.style[r(i)]=a};return(s,i)=>{for(const a in i){const l=i[a];Object.hasOwn(i,a)&&S_(l)&&o(s,a,l)}}})(),Vt={minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},Ie={percent:null,isRendered:()=>!!document.getElementById("nprogress"),set:e=>{const{speed:t,easing:n}=Vt,r=Ie.isStarted(),o=fs(e,Vt.minimum,1);Ie.percent=o===1?null:o;const s=Ie.render(!r),i=s.querySelector(Vt.barSelector);return s.offsetWidth,Z_(a=>{ro(i,{transform:`translate3d(${Ja(o)}%,0,0)`,transition:`all ${t}ms ${n}`}),o===1?(ro(s,{transition:"none",opacity:"1"}),s.offsetWidth,setTimeout(()=>{ro(s,{transition:`all ${t}ms linear`,opacity:"0"}),setTimeout(()=>{Ie.remove(),a()},t)},t)):setTimeout(()=>{a()},t)}),Ie},isStarted:()=>typeof Ie.percent=="number",start:()=>{Ie.percent||Ie.set(0);const e=()=>{setTimeout(()=>{Ie.percent&&(Ie.trickle(),e())},Vt.trickleSpeed)};return e(),Ie},done:e=>!e&&!Ie.percent?Ie:Ie.increase(.3+.5*Math.random()).set(1),increase:e=>{let{percent:t}=Ie;return t?(t=fs(t+(typeof e=="number"?e:(1-t)*fs(Math.random()*t,.1,.95)),0,.994),Ie.set(t)):Ie.start()},trickle:()=>Ie.increase(Math.random()*Vt.trickleRate),render:e=>{if(Ie.isRendered())return document.getElementById("nprogress");Xa(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=Vt.template;const n=t.querySelector(Vt.barSelector),r=document.querySelector(Vt.parent),o=e?"-100":Ja(Ie.percent??0);return ro(n,{transition:"all 0 linear",transform:`translate3d(${o}%,0,0)`}),r&&(r!==document.body&&Xa(r,"nprogress-custom-parent"),r.appendChild(t)),t},remove:()=>{Za(document.documentElement,"nprogress-busy"),Za(document.querySelector(Vt.parent),"nprogress-custom-parent"),X_(document.getElementById("nprogress"))}},Q_=()=>{Qe(()=>{const e=wn(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||Ie.start()}),e.afterEach(n=>{t.add(n.path),Ie.done()})})},ev=un({setup(){Q_()}}),tv=Object.freeze(Object.defineProperty({__proto__:null,default:ev},Symbol.toStringTag,{value:"Module"})),nv=({selector:e='div[class*="language-"].has-collapsed-lines > .collapsed-lines'}={})=>{dt("click",t=>{const n=t.target;if(n.matches(e)){const r=n.parentElement;r!=null&&r.classList.toggle("collapsed")&&r.scrollIntoView({block:"center",behavior:"instant"})}})},rv={setup(){nv()}},ov=Object.freeze(Object.defineProperty({__proto__:null,default:rv},Symbol.toStringTag,{value:"Module"})),sv="VUEPRESS_CODE_TAB_STORE",oo=Ai(sv,{}),iv=he({name:"CodeTabs",props:{active:{type:Number,default:0},data:{type:Array,required:!0},id:{type:String,required:!0},tabId:{type:String,default:""}},slots:Object,setup(e,{slots:t}){const n=ve(e.active),r=Et([]),o=()=>{e.tabId&&(oo.value[e.tabId]=e.data[n.value].id)},s=(u=n.value)=>{n.value=u{n.value=u>0?u-1:r.value.length-1,r.value[n.value].focus()},a=(u,c)=>{u.key===" "||u.key==="Enter"?(u.preventDefault(),n.value=c):u.key==="ArrowRight"?(u.preventDefault(),s()):u.key==="ArrowLeft"&&(u.preventDefault(),i()),e.tabId&&(oo.value[e.tabId]=e.data[n.value].id)},l=()=>{if(e.tabId){const u=e.data.findIndex(({id:c})=>oo.value[e.tabId]===c);if(u!==-1)return u}return e.active};return Qe(()=>{n.value=l(),je(()=>oo.value[e.tabId],(u,c)=>{if(e.tabId&&u!==c){const f=e.data.findIndex(({id:d})=>d===u);f!==-1&&(n.value=f)}})}),()=>e.data.length?_e("div",{class:"vp-code-tabs"},[_e("div",{class:"vp-code-tabs-nav",role:"tablist"},e.data.map(({id:u},c)=>{const f=c===n.value;return _e("button",{type:"button",ref:d=>{d&&(r.value[c]=d)},class:["vp-code-tab-nav",{active:f}],role:"tab","aria-controls":`codetab-${e.id}-${c}`,"aria-selected":f,onClick:()=>{n.value=c,o()},onKeydown:d=>{a(d,c)}},t[`title${c}`]({value:u,isActive:f}))})),e.data.map(({id:u},c)=>{const f=c===n.value;return _e("div",{class:["vp-code-tab",{active:f}],id:`codetab-${e.id}-${c}`,role:"tabpanel","aria-expanded":f},[_e("div",{class:"vp-code-tab-title"},t[`title${c}`]({value:u,isActive:f})),t[`tab${c}`]({value:u,isActive:f})])})]):null}}),av="VUEPRESS_TAB_STORE",ds=Ai(av,{}),lv=he({name:"Tabs",props:{active:{type:Number,default:0},data:{type:Array,required:!0},id:{type:String,required:!0},tabId:{type:String,default:""}},slots:Object,setup(e,{slots:t}){const n=ve(e.active),r=Et([]),o=()=>{e.tabId&&(ds.value[e.tabId]=e.data[n.value].id)},s=(u=n.value)=>{n.value=u{n.value=u>0?u-1:r.value.length-1,r.value[n.value].focus()},a=(u,c)=>{u.key===" "||u.key==="Enter"?(u.preventDefault(),n.value=c):u.key==="ArrowRight"?(u.preventDefault(),s()):u.key==="ArrowLeft"&&(u.preventDefault(),i()),o()},l=()=>{if(e.tabId){const u=e.data.findIndex(({id:c})=>ds.value[e.tabId]===c);if(u!==-1)return u}return e.active};return Qe(()=>{n.value=l(),je(()=>ds.value[e.tabId],(u,c)=>{if(e.tabId&&u!==c){const f=e.data.findIndex(({id:d})=>d===u);f!==-1&&(n.value=f)}})}),()=>e.data.length?_e("div",{class:"vp-tabs"},[_e("div",{class:"vp-tabs-nav",role:"tablist"},e.data.map(({id:u},c)=>{const f=c===n.value;return _e("button",{type:"button",ref:d=>{d&&(r.value[c]=d)},class:["vp-tab-nav",{active:f}],role:"tab","aria-controls":`tab-${e.id}-${c}`,"aria-selected":f,onClick:()=>{n.value=c,o()},onKeydown:d=>{a(d,c)}},t[`title${c}`]({value:u,isActive:f}))})),e.data.map(({id:u},c)=>{const f=c===n.value;return _e("div",{class:["vp-tab",{active:f}],id:`tab-${e.id}-${c}`,role:"tabpanel","aria-expanded":f},[_e("div",{class:"vp-tab-title"},t[`title${c}`]({value:u,isActive:f})),t[`tab${c}`]({value:u,isActive:f})])})]):null}}),uv={enhance:({app:e})=>{e.component("CodeTabs",iv),e.component("Tabs",lv)}},cv=Object.freeze(Object.defineProperty({__proto__:null,default:uv},Symbol.toStringTag,{value:"Module"}));var fv=Object.create,of=Object.defineProperty,dv=Object.getOwnPropertyDescriptor,Ci=Object.getOwnPropertyNames,pv=Object.getPrototypeOf,hv=Object.prototype.hasOwnProperty,mv=(e,t)=>function(){return e&&(t=(0,e[Ci(e)[0]])(e=0)),t},gv=(e,t)=>function(){return t||(0,e[Ci(e)[0]])((t={exports:{}}).exports,t),t.exports},_v=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ci(t))!hv.call(e,o)&&o!==n&&of(e,o,{get:()=>t[o],enumerable:!(r=dv(t,o))||r.enumerable});return e},vv=(e,t,n)=>(n=e!=null?fv(pv(e)):{},_v(of(n,"default",{value:e,enumerable:!0}),e)),qr=mv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),yv=gv({"../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(e,t){qr(),t.exports=r;function n(s){return s instanceof Buffer?Buffer.from(s):new s.constructor(s.buffer.slice(),s.byteOffset,s.length)}function r(s){if(s=s||{},s.circles)return o(s);const i=new Map;if(i.set(Date,f=>new Date(f)),i.set(Map,(f,d)=>new Map(l(Array.from(f),d))),i.set(Set,(f,d)=>new Set(l(Array.from(f),d))),s.constructorHandlers)for(const f of s.constructorHandlers)i.set(f[0],f[1]);let a=null;return s.proto?c:u;function l(f,d){const p=Object.keys(f),_=new Array(p.length);for(let v=0;vnew Date(p)),l.set(Map,(p,_)=>new Map(c(Array.from(p),_))),l.set(Set,(p,_)=>new Set(c(Array.from(p),_))),s.constructorHandlers)for(const p of s.constructorHandlers)l.set(p[0],p[1]);let u=null;return s.proto?d:f;function c(p,_){const v=Object.keys(p),b=new Array(v.length);for(let E=0;E(i=wv(e,u,c),i.finally(()=>{if(i=null,n.trailing&&a&&!o){const f=l(u,a);return a=null,f}}),i);return function(...u){return i?(n.trailing&&(a=u),i):new Promise(c=>{const f=!o&&n.leading;clearTimeout(o),o=setTimeout(()=>{o=null;const d=n.leading?r:l(this,u);for(const p of s)p(d);s=[]},t),f?(r=l(this,u),c(r)):s.push(c)})}}async function wv(e,t,n){return await e.apply(t,n)}function Ns(e,t={},n){for(const r in e){const o=e[r],s=n?`${n}:${r}`:r;typeof o=="object"&&o!==null?Ns(o,t,s):typeof o=="function"&&(t[s]=o)}return t}const Ov={run:e=>e()},xv=()=>Ov,af=typeof console.createTask<"u"?console.createTask:xv;function kv(e,t){const n=t.shift(),r=af(n);return e.reduce((o,s)=>o.then(()=>r.run(()=>s(...t))),Promise.resolve())}function Pv(e,t){const n=t.shift(),r=af(n);return Promise.all(e.map(o=>r.run(()=>o(...t))))}function ps(e,t){for(const n of[...e])n(t)}class Iv{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(t,n,r={}){if(!t||typeof n!="function")return()=>{};const o=t;let s;for(;this._deprecatedHooks[t];)s=this._deprecatedHooks[t],t=s.to;if(s&&!r.allowDeprecated){let i=s.message;i||(i=`${o} hook has been deprecated`+(s.to?`, please use ${s.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(i)||(console.warn(i),this._deprecatedMessages.add(i))}if(!n.name)try{Object.defineProperty(n,"name",{get:()=>"_"+t.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[t]=this._hooks[t]||[],this._hooks[t].push(n),()=>{n&&(this.removeHook(t,n),n=void 0)}}hookOnce(t,n){let r,o=(...s)=>(typeof r=="function"&&r(),r=void 0,o=void 0,n(...s));return r=this.hook(t,o),r}removeHook(t,n){if(this._hooks[t]){const r=this._hooks[t].indexOf(n);r!==-1&&this._hooks[t].splice(r,1),this._hooks[t].length===0&&delete this._hooks[t]}}deprecateHook(t,n){this._deprecatedHooks[t]=typeof n=="string"?{to:n}:n;const r=this._hooks[t]||[];delete this._hooks[t];for(const o of r)this.hook(t,o)}deprecateHooks(t){Object.assign(this._deprecatedHooks,t);for(const n in t)this.deprecateHook(n,t[n])}addHooks(t){const n=Ns(t),r=Object.keys(n).map(o=>this.hook(o,n[o]));return()=>{for(const o of r.splice(0,r.length))o()}}removeHooks(t){const n=Ns(t);for(const r in n)this.removeHook(r,n[r])}removeAllHooks(){for(const t in this._hooks)delete this._hooks[t]}callHook(t,...n){return n.unshift(t),this.callHookWith(kv,t,...n)}callHookParallel(t,...n){return n.unshift(t),this.callHookWith(Pv,t,...n)}callHookWith(t,n,...r){const o=this._before||this._after?{name:n,args:r,context:{}}:void 0;this._before&&ps(this._before,o);const s=t(n in this._hooks?[...this._hooks[n]]:[],r);return s instanceof Promise?s.finally(()=>{this._after&&o&&ps(this._after,o)}):(this._after&&o&&ps(this._after,o),s)}beforeEach(t){return this._before=this._before||[],this._before.push(t),()=>{if(this._before!==void 0){const n=this._before.indexOf(t);n!==-1&&this._before.splice(n,1)}}}afterEach(t){return this._after=this._after||[],this._after.push(t),()=>{if(this._after!==void 0){const n=this._after.indexOf(t);n!==-1&&this._after.splice(n,1)}}}}function lf(){return new Iv}var Rv=Object.create,uf=Object.defineProperty,Lv=Object.getOwnPropertyDescriptor,Ti=Object.getOwnPropertyNames,Dv=Object.getPrototypeOf,Vv=Object.prototype.hasOwnProperty,Nv=(e,t)=>function(){return e&&(t=(0,e[Ti(e)[0]])(e=0)),t},cf=(e,t)=>function(){return t||(0,e[Ti(e)[0]])((t={exports:{}}).exports,t),t.exports},Mv=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ti(t))!Vv.call(e,o)&&o!==n&&uf(e,o,{get:()=>t[o],enumerable:!(r=Lv(t,o))||r.enumerable});return e},Fv=(e,t,n)=>(n=e!=null?Rv(Dv(e)):{},Mv(uf(n,"default",{value:e,enumerable:!0}),e)),P=Nv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),Hv=cf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js"(e,t){P(),function(n){var r={À:"A",Á:"A",Â:"A",Ã:"A",Ä:"Ae",Å:"A",Æ:"AE",Ç:"C",È:"E",É:"E",Ê:"E",Ë:"E",Ì:"I",Í:"I",Î:"I",Ï:"I",Ð:"D",Ñ:"N",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"Oe",Ő:"O",Ø:"O",Ù:"U",Ú:"U",Û:"U",Ü:"Ue",Ű:"U",Ý:"Y",Þ:"TH",ß:"ss",à:"a",á:"a",â:"a",ã:"a",ä:"ae",å:"a",æ:"ae",ç:"c",è:"e",é:"e",ê:"e",ë:"e",ì:"i",í:"i",î:"i",ï:"i",ð:"d",ñ:"n",ò:"o",ó:"o",ô:"o",õ:"o",ö:"oe",ő:"o",ø:"o",ù:"u",ú:"u",û:"u",ü:"ue",ű:"u",ý:"y",þ:"th",ÿ:"y","ẞ":"SS",ا:"a",أ:"a",إ:"i",آ:"aa",ؤ:"u",ئ:"e",ء:"a",ب:"b",ت:"t",ث:"th",ج:"j",ح:"h",خ:"kh",د:"d",ذ:"th",ر:"r",ز:"z",س:"s",ش:"sh",ص:"s",ض:"dh",ط:"t",ظ:"z",ع:"a",غ:"gh",ف:"f",ق:"q",ك:"k",ل:"l",م:"m",ن:"n",ه:"h",و:"w",ي:"y",ى:"a",ة:"h",ﻻ:"la",ﻷ:"laa",ﻹ:"lai",ﻵ:"laa",گ:"g",چ:"ch",پ:"p",ژ:"zh",ک:"k",ی:"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9",က:"k",ခ:"kh",ဂ:"g",ဃ:"ga",င:"ng",စ:"s",ဆ:"sa",ဇ:"z","စျ":"za",ည:"ny",ဋ:"t",ဌ:"ta",ဍ:"d",ဎ:"da",ဏ:"na",တ:"t",ထ:"ta",ဒ:"d",ဓ:"da",န:"n",ပ:"p",ဖ:"pa",ဗ:"b",ဘ:"ba",မ:"m",ယ:"y",ရ:"ya",လ:"l",ဝ:"w",သ:"th",ဟ:"h",ဠ:"la",အ:"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h",ဧ:"e","၏":"-e",ဣ:"i",ဤ:"-i",ဉ:"u",ဦ:"-u",ဩ:"aw","သြော":"aw",ဪ:"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"",č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z",ހ:"h",ށ:"sh",ނ:"n",ރ:"r",ބ:"b",ޅ:"lh",ކ:"k",އ:"a",ވ:"v",މ:"m",ފ:"f",ދ:"dh",ތ:"th",ލ:"l",ގ:"g",ޏ:"gn",ސ:"s",ޑ:"d",ޒ:"z",ޓ:"t",ޔ:"y",ޕ:"p",ޖ:"j",ޗ:"ch",ޘ:"tt",ޙ:"hh",ޚ:"kh",ޛ:"th",ޜ:"z",ޝ:"sh",ޞ:"s",ޟ:"d",ޠ:"t",ޡ:"z",ޢ:"a",ޣ:"gh",ޤ:"q",ޥ:"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"",ა:"a",ბ:"b",გ:"g",დ:"d",ე:"e",ვ:"v",ზ:"z",თ:"t",ი:"i",კ:"k",ლ:"l",მ:"m",ნ:"n",ო:"o",პ:"p",ჟ:"zh",რ:"r",ს:"s",ტ:"t",უ:"u",ფ:"p",ქ:"k",ღ:"gh",ყ:"q",შ:"sh",ჩ:"ch",ც:"ts",ძ:"dz",წ:"ts",ჭ:"ch",ხ:"kh",ჯ:"j",ჰ:"h",α:"a",β:"v",γ:"g",δ:"d",ε:"e",ζ:"z",η:"i",θ:"th",ι:"i",κ:"k",λ:"l",μ:"m",ν:"n",ξ:"ks",ο:"o",π:"p",ρ:"r",σ:"s",τ:"t",υ:"y",φ:"f",χ:"x",ψ:"ps",ω:"o",ά:"a",έ:"e",ί:"i",ό:"o",ύ:"y",ή:"i",ώ:"o",ς:"s",ϊ:"i",ΰ:"y",ϋ:"y",ΐ:"i",Α:"A",Β:"B",Γ:"G",Δ:"D",Ε:"E",Ζ:"Z",Η:"I",Θ:"TH",Ι:"I",Κ:"K",Λ:"L",Μ:"M",Ν:"N",Ξ:"KS",Ο:"O",Π:"P",Ρ:"R",Σ:"S",Τ:"T",Υ:"Y",Φ:"F",Χ:"X",Ψ:"PS",Ω:"O",Ά:"A",Έ:"E",Ί:"I",Ό:"O",Ύ:"Y",Ή:"I",Ώ:"O",Ϊ:"I",Ϋ:"Y",ā:"a",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",ū:"u",Ā:"A",Ē:"E",Ģ:"G",Ī:"I",Ķ:"k",Ļ:"L",Ņ:"N",Ū:"U",Ќ:"Kj",ќ:"kj",Љ:"Lj",љ:"lj",Њ:"Nj",њ:"nj",Тс:"Ts",тс:"ts",ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"E",Ł:"L",Ń:"N",Ś:"S",Ź:"Z",Ż:"Z",Є:"Ye",І:"I",Ї:"Yi",Ґ:"G",є:"ye",і:"i",ї:"yi",ґ:"g",ă:"a",Ă:"A",ș:"s",Ș:"S",ț:"t",Ț:"T",ţ:"t",Ţ:"T",а:"a",б:"b",в:"v",г:"g",д:"d",е:"e",ё:"yo",ж:"zh",з:"z",и:"i",й:"i",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",х:"kh",ц:"c",ч:"ch",ш:"sh",щ:"sh",ъ:"",ы:"y",ь:"",э:"e",ю:"yu",я:"ya",А:"A",Б:"B",В:"V",Г:"G",Д:"D",Е:"E",Ё:"Yo",Ж:"Zh",З:"Z",И:"I",Й:"I",К:"K",Л:"L",М:"M",Н:"N",О:"O",П:"P",Р:"R",С:"S",Т:"T",У:"U",Ф:"F",Х:"Kh",Ц:"C",Ч:"Ch",Ш:"Sh",Щ:"Sh",Ъ:"",Ы:"Y",Ь:"",Э:"E",Ю:"Yu",Я:"Ya",ђ:"dj",ј:"j",ћ:"c",џ:"dz",Ђ:"Dj",Ј:"j",Ћ:"C",Џ:"Dz",ľ:"l",ĺ:"l",ŕ:"r",Ľ:"L",Ĺ:"L",Ŕ:"R",ş:"s",Ş:"S",ı:"i",İ:"I",ğ:"g",Ğ:"G",ả:"a",Ả:"A",ẳ:"a",Ẳ:"A",ẩ:"a",Ẩ:"A",đ:"d",Đ:"D",ẹ:"e",Ẹ:"E",ẽ:"e",Ẽ:"E",ẻ:"e",Ẻ:"E",ế:"e",Ế:"E",ề:"e",Ề:"E",ệ:"e",Ệ:"E",ễ:"e",Ễ:"E",ể:"e",Ể:"E",ỏ:"o",ọ:"o",Ọ:"o",ố:"o",Ố:"O",ồ:"o",Ồ:"O",ổ:"o",Ổ:"O",ộ:"o",Ộ:"O",ỗ:"o",Ỗ:"O",ơ:"o",Ơ:"O",ớ:"o",Ớ:"O",ờ:"o",Ờ:"O",ợ:"o",Ợ:"O",ỡ:"o",Ỡ:"O",Ở:"o",ở:"o",ị:"i",Ị:"I",ĩ:"i",Ĩ:"I",ỉ:"i",Ỉ:"i",ủ:"u",Ủ:"U",ụ:"u",Ụ:"U",ũ:"u",Ũ:"U",ư:"u",Ư:"U",ứ:"u",Ứ:"U",ừ:"u",Ừ:"U",ự:"u",Ự:"U",ữ:"u",Ữ:"U",ử:"u",Ử:"ư",ỷ:"y",Ỷ:"y",ỳ:"y",Ỳ:"Y",ỵ:"y",Ỵ:"Y",ỹ:"y",Ỹ:"Y",ạ:"a",Ạ:"A",ấ:"a",Ấ:"A",ầ:"a",Ầ:"A",ậ:"a",Ậ:"A",ẫ:"a",Ẫ:"A",ắ:"a",Ắ:"A",ằ:"a",Ằ:"A",ặ:"a",Ặ:"A",ẵ:"a",Ẵ:"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d",ƒ:"f","™":"(TM)","©":"(C)",œ:"oe",Œ:"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o",º:"o",ª:"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY",元:"CNY",円:"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN",лв:"BGN","៛":"KHR","₡":"CRC","₸":"KZT",ден:"MKD",zł:"PLN","₽":"RUB","₾":"GEL"},o=["်","ް"],s={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},i={en:{},az:{ç:"c",ə:"e",ğ:"g",ı:"i",ö:"o",ş:"s",ü:"u",Ç:"C",Ə:"E",Ğ:"G",İ:"I",Ö:"O",Ş:"S",Ü:"U"},cs:{č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z"},fi:{ä:"a",Ä:"A",ö:"o",Ö:"O"},hu:{ä:"a",Ä:"A",ö:"o",Ö:"O",ü:"u",Ü:"U",ű:"u",Ű:"U"},lt:{ą:"a",č:"c",ę:"e",ė:"e",į:"i",š:"s",ų:"u",ū:"u",ž:"z",Ą:"A",Č:"C",Ę:"E",Ė:"E",Į:"I",Š:"S",Ų:"U",Ū:"U"},lv:{ā:"a",č:"c",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",š:"s",ū:"u",ž:"z",Ā:"A",Č:"C",Ē:"E",Ģ:"G",Ī:"i",Ķ:"k",Ļ:"L",Ņ:"N",Š:"S",Ū:"u",Ž:"Z"},pl:{ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ó:"o",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"e",Ł:"L",Ń:"N",Ó:"O",Ś:"S",Ź:"Z",Ż:"Z"},sv:{ä:"a",Ä:"A",ö:"o",Ö:"O"},sk:{ä:"a",Ä:"A"},sr:{љ:"lj",њ:"nj",Љ:"Lj",Њ:"Nj",đ:"dj",Đ:"Dj"},tr:{Ü:"U",Ö:"O",ü:"u",ö:"o"}},a={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},l=[";","?",":","@","&","=","+","$",",","/"].join(""),u=[";","?",":","@","&","=","+","$",","].join(""),c=[".","!","~","*","'","(",")"].join(""),f=function(b,E){var A="-",m="",y="",L=!0,W={},M,T,U,O,B,C,D,Z,ee,I,V,z,le,ye,ke="";if(typeof b!="string")return"";if(typeof E=="string"&&(A=E),D=a.en,Z=i.en,typeof E=="object"){M=E.maintainCase||!1,W=E.custom&&typeof E.custom=="object"?E.custom:W,U=+E.truncate>1&&E.truncate||!1,O=E.uric||!1,B=E.uricNoSlash||!1,C=E.mark||!1,L=!(E.symbols===!1||E.lang===!1),A=E.separator||A,O&&(ke+=l),B&&(ke+=u),C&&(ke+=c),D=E.lang&&a[E.lang]&&L?a[E.lang]:L?a.en:{},Z=E.lang&&i[E.lang]?i[E.lang]:E.lang===!1||E.lang===!0?{}:i.en,E.titleCase&&typeof E.titleCase.length=="number"&&Array.prototype.toString.call(E.titleCase)?(E.titleCase.forEach(function(be){W[be+""]=be+""}),T=!0):T=!!E.titleCase,E.custom&&typeof E.custom.length=="number"&&Array.prototype.toString.call(E.custom)&&E.custom.forEach(function(be){W[be+""]=be+""}),Object.keys(W).forEach(function(be){var et;be.length>1?et=new RegExp("\\b"+p(be)+"\\b","gi"):et=new RegExp(p(be),"gi"),b=b.replace(et,W[be])});for(V in W)ke+=V}for(ke+=A,ke=p(ke),b=b.replace(/(^\s+|\s+$)/g,""),le=!1,ye=!1,I=0,z=b.length;I=0?(y+=V,V=""):ye===!0?(V=s[y]+r[V],y=""):V=le&&r[V].match(/[A-Za-z0-9]/)?" "+r[V]:r[V],le=!1,ye=!1):V in s?(y+=V,V="",I===z-1&&(V=s[y]),ye=!0):D[V]&&!(O&&l.indexOf(V)!==-1)&&!(B&&u.indexOf(V)!==-1)?(V=le||m.substr(-1).match(/[A-Za-z0-9]/)?A+D[V]:D[V],V+=b[I+1]!==void 0&&b[I+1].match(/[A-Za-z0-9]/)?A:"",le=!0):(ye===!0?(V=s[y]+V,y="",ye=!1):le&&(/[A-Za-z0-9]/.test(V)||m.substr(-1).match(/A-Za-z0-9]/))&&(V=" "+V),le=!1),m+=V.replace(new RegExp("[^\\w\\s"+ke+"_-]","g"),A);return T&&(m=m.replace(/(\w)(\S*)/g,function(be,et,_t){var tt=et.toUpperCase()+(_t!==null?_t:"");return Object.keys(W).indexOf(tt.toLowerCase())<0?tt:tt.toLowerCase()})),m=m.replace(/\s+/g,A).replace(new RegExp("\\"+A+"+","g"),A).replace(new RegExp("(^\\"+A+"+|\\"+A+"+$)","g"),""),U&&m.length>U&&(ee=m.charAt(U)===A,m=m.slice(0,U),ee||(m=m.slice(0,m.lastIndexOf(A)))),!M&&!T&&(m=m.toLowerCase()),m},d=function(b){return function(A){return f(A,b)}},p=function(b){return b.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},_=function(v,b){for(var E in b)if(b[E]===v)return!0};if(typeof t<"u"&&t.exports)t.exports=f,t.exports.createSlug=d;else if(typeof define<"u"&&define.amd)define([],function(){return f});else try{if(n.getSlug||n.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";n.getSlug=f,n.createSlug=d}catch{}}(e)}}),Bv=cf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js"(e,t){P(),t.exports=Hv()}});P();P();P();P();P();P();P();P();function $v(e){var t;const n=e.name||e._componentTag||e.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__||e.__name;return n==="index"&&((t=e.__file)!=null&&t.endsWith("index.vue"))?"":n}function Uv(e){const t=e.__file;if(t)return Av(Cv(t,".vue"))}function tl(e,t){return e.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__=t,t}function Uo(e){if(e.__VUE_DEVTOOLS_NEXT_APP_RECORD__)return e.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(e.root)return e.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__}async function jv(e){const{app:t,uid:n,instance:r}=e;try{if(r.__VUE_DEVTOOLS_NEXT_UID__)return r.__VUE_DEVTOOLS_NEXT_UID__;const o=await Uo(t);if(!o)return null;const s=o.rootInstance===r;return`${o.id}:${s?"root":n}`}catch{}}function ff(e){var t,n;const r=(t=e.subTree)==null?void 0:t.type,o=Uo(e);return o?((n=o==null?void 0:o.types)==null?void 0:n.Fragment)===r:!1}function jo(e){var t,n,r;const o=$v((e==null?void 0:e.type)||{});if(o)return o;if((e==null?void 0:e.root)===e)return"Root";for(const i in(n=(t=e.parent)==null?void 0:t.type)==null?void 0:n.components)if(e.parent.type.components[i]===(e==null?void 0:e.type))return tl(e,i);for(const i in(r=e.appContext)==null?void 0:r.components)if(e.appContext.components[i]===(e==null?void 0:e.type))return tl(e,i);const s=Uv((e==null?void 0:e.type)||{});return s||"Anonymous Component"}function Ms(e,t){return t=t||`${e.id}:root`,e.instanceMap.get(t)||e.instanceMap.get(":root")}function zv(){const e={top:0,bottom:0,left:0,right:0,get width(){return e.right-e.left},get height(){return e.bottom-e.top}};return e}var so;function Kv(e){return so||(so=document.createRange()),so.selectNode(e),so.getBoundingClientRect()}function Wv(e){const t=zv();if(!e.children)return t;for(let n=0,r=e.children.length;ne.bottom)&&(e.bottom=t.bottom),(!e.left||t.lefte.right)&&(e.right=t.right),e}var nl={top:0,left:0,right:0,bottom:0,width:0,height:0};function Tn(e){const t=e.subTree.el;return typeof window>"u"?nl:ff(e)?Wv(e.subTree):(t==null?void 0:t.nodeType)===1?t==null?void 0:t.getBoundingClientRect():e.subTree.component?Tn(e.subTree.component):nl}P();function wi(e){return ff(e)?Gv(e.subTree):e.subTree?[e.subTree.el]:[]}function Gv(e){if(!e.children)return[];const t=[];return e.children.forEach(n=>{n.component?t.push(...wi(n.component)):n!=null&&n.el&&t.push(n.el)}),t}var df="__vue-devtools-component-inspector__",pf="__vue-devtools-component-inspector__card__",hf="__vue-devtools-component-inspector__name__",mf="__vue-devtools-component-inspector__indicator__",gf={display:"block",zIndex:2147483640,position:"fixed",backgroundColor:"#42b88325",border:"1px solid #42b88350",borderRadius:"5px",transition:"all 0.1s ease-in",pointerEvents:"none"},Yv={fontFamily:"Arial, Helvetica, sans-serif",padding:"5px 8px",borderRadius:"4px",textAlign:"left",position:"absolute",left:0,color:"#e9e9e9",fontSize:"14px",fontWeight:600,lineHeight:"24px",backgroundColor:"#42b883",boxShadow:"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"},Xv={display:"inline-block",fontWeight:400,fontStyle:"normal",fontSize:"12px",opacity:.7};function nr(){return document.getElementById(df)}function Zv(){return document.getElementById(pf)}function Jv(){return document.getElementById(mf)}function Qv(){return document.getElementById(hf)}function Oi(e){return{left:`${Math.round(e.left*100)/100}px`,top:`${Math.round(e.top*100)/100}px`,width:`${Math.round(e.width*100)/100}px`,height:`${Math.round(e.height*100)/100}px`}}function xi(e){var t;const n=document.createElement("div");n.id=(t=e.elementId)!=null?t:df,Object.assign(n.style,{...gf,...Oi(e.bounds),...e.style});const r=document.createElement("span");r.id=pf,Object.assign(r.style,{...Yv,top:e.bounds.top<35?0:"-35px"});const o=document.createElement("span");o.id=hf,o.innerHTML=`<${e.name}>  `;const s=document.createElement("i");return s.id=mf,s.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`,Object.assign(s.style,Xv),r.appendChild(o),r.appendChild(s),n.appendChild(r),document.body.appendChild(n),n}function ki(e){const t=nr(),n=Zv(),r=Qv(),o=Jv();t&&(Object.assign(t.style,{...gf,...Oi(e.bounds)}),Object.assign(n.style,{top:e.bounds.top<35?0:"-35px"}),r.innerHTML=`<${e.name}>  `,o.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`)}function e0(e){const t=Tn(e);if(!t.width&&!t.height)return;const n=jo(e);nr()?ki({bounds:t,name:n}):xi({bounds:t,name:n})}function _f(){const e=nr();e&&(e.style.display="none")}var Fs=null;function Hs(e){const t=e.target;if(t){const n=t.__vueParentComponent;if(n&&(Fs=n,n.vnode.el)){const o=Tn(n),s=jo(n);nr()?ki({bounds:o,name:s}):xi({bounds:o,name:s})}}}function t0(e,t){var n;if(e.preventDefault(),e.stopPropagation(),Fs){const r=(n=Ge.value)==null?void 0:n.app;jv({app:r,uid:r.uid,instance:Fs}).then(o=>{t(o)})}}var Co=null;function n0(){_f(),window.removeEventListener("mouseover",Hs),window.removeEventListener("click",Co,!0),Co=null}function r0(){return window.addEventListener("mouseover",Hs),new Promise(e=>{function t(n){n.preventDefault(),n.stopPropagation(),t0(n,r=>{window.removeEventListener("click",t,!0),Co=null,window.removeEventListener("mouseover",Hs);const o=nr();o&&(o.style.display="none"),e(JSON.stringify({id:r}))})}Co=t,window.addEventListener("click",t,!0)})}function o0(e){const t=Ms(Ge.value,e.id);if(t){const[n]=wi(t);if(typeof n.scrollIntoView=="function")n.scrollIntoView({behavior:"smooth"});else{const r=Tn(t),o=document.createElement("div"),s={...Oi(r),position:"absolute"};Object.assign(o.style,s),document.body.appendChild(o),o.scrollIntoView({behavior:"smooth"}),setTimeout(()=>{document.body.removeChild(o)},2e3)}setTimeout(()=>{const r=Tn(t);if(r.width||r.height){const o=jo(t),s=nr();s?ki({...e,name:o,bounds:r}):xi({...e,name:o,bounds:r}),setTimeout(()=>{s&&(s.style.display="none")},1500)}},1200)}}P();var rl,ol;(ol=(rl=X).__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__)!=null||(rl.__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__=!0);function s0(e){let t=0;const n=setInterval(()=>{X.__VUE_INSPECTOR__&&(clearInterval(n),t+=30,e()),t>=5e3&&clearInterval(n)},30)}function i0(){const e=X.__VUE_INSPECTOR__,t=e.openInEditor;e.openInEditor=async(...n)=>{e.disable(),t(...n)}}function a0(){return new Promise(e=>{function t(){i0(),e(X.__VUE_INSPECTOR__)}X.__VUE_INSPECTOR__?t():s0(()=>{t()})})}P();P();function l0(e){return!!(e&&e.__v_isReadonly)}function vf(e){return l0(e)?vf(e.__v_raw):!!(e&&e.__v_isReactive)}function hs(e){return!!(e&&e.__v_isRef===!0)}function hr(e){const t=e&&e.__v_raw;return t?hr(t):e}var u0=class{constructor(){this.refEditor=new c0}set(e,t,n,r){const o=Array.isArray(t)?t:t.split(".");for(;o.length>1;){const a=o.shift();e instanceof Map&&(e=e.get(a)),e instanceof Set?e=Array.from(e.values())[a]:e=e[a],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}const s=o[0],i=this.refEditor.get(e)[s];r?r(e,s,n):this.refEditor.isRef(i)?this.refEditor.set(i,n):e[s]=n}get(e,t){const n=Array.isArray(t)?t:t.split(".");for(let r=0;r"u")return!1;const r=Array.isArray(t)?t.slice():t.split("."),o=n?2:1;for(;e&&r.length>o;){const s=r.shift();e=e[s],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}return e!=null&&Object.prototype.hasOwnProperty.call(e,r[0])}createDefaultSetCallback(e){return(t,n,r)=>{if((e.remove||e.newKey)&&(Array.isArray(t)?t.splice(n,1):hr(t)instanceof Map?t.delete(n):hr(t)instanceof Set?t.delete(Array.from(t.values())[n]):Reflect.deleteProperty(t,n)),!e.remove){const o=t[e.newKey||n];this.refEditor.isRef(o)?this.refEditor.set(o,r):hr(t)instanceof Map?t.set(e.newKey||n,r):hr(t)instanceof Set?t.add(r):t[e.newKey||n]=r}}}},c0=class{set(e,t){if(hs(e))e.value=t;else{if(e instanceof Set&&Array.isArray(t)){e.clear(),t.forEach(o=>e.add(o));return}const n=Object.keys(t);if(e instanceof Map){const o=new Set(e.keys());n.forEach(s=>{e.set(s,Reflect.get(t,s)),o.delete(s)}),o.forEach(s=>e.delete(s));return}const r=new Set(Object.keys(e));n.forEach(o=>{Reflect.set(e,o,Reflect.get(t,o)),r.delete(o)}),r.forEach(o=>Reflect.deleteProperty(e,o))}}get(e){return hs(e)?e.value:e}isRef(e){return hs(e)||vf(e)}};P();P();P();var f0="__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__";function d0(){if(!sf||typeof localStorage>"u"||localStorage===null)return{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""};const e=localStorage.getItem(f0);return e?JSON.parse(e):{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""}}P();P();P();var sl,il;(il=(sl=X).__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS)!=null||(sl.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS=[]);var p0=new Proxy(X.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS,{get(e,t,n){return Reflect.get(e,t,n)}});function h0(e,t){Fe.timelineLayersState[t.id]=!1,p0.push({...e,descriptorId:t.id,appRecord:Uo(t.app)})}var al,ll;(ll=(al=X).__VUE_DEVTOOLS_KIT_INSPECTOR__)!=null||(al.__VUE_DEVTOOLS_KIT_INSPECTOR__=[]);var Pi=new Proxy(X.__VUE_DEVTOOLS_KIT_INSPECTOR__,{get(e,t,n){return Reflect.get(e,t,n)}}),yf=Kn(()=>{rr.hooks.callHook("sendInspectorToClient",bf())});function m0(e,t){var n,r;Pi.push({options:e,descriptor:t,treeFilterPlaceholder:(n=e.treeFilterPlaceholder)!=null?n:"Search tree...",stateFilterPlaceholder:(r=e.stateFilterPlaceholder)!=null?r:"Search state...",treeFilter:"",selectedNodeId:"",appRecord:Uo(t.app)}),yf()}function bf(){return Pi.filter(e=>e.descriptor.app===Ge.value.app).filter(e=>e.descriptor.id!=="components").map(e=>{var t;const n=e.descriptor,r=e.options;return{id:r.id,label:r.label,logo:n.logo,icon:`custom-ic-baseline-${(t=r==null?void 0:r.icon)==null?void 0:t.replace(/_/g,"-")}`,packageName:n.packageName,homepage:n.homepage,pluginId:n.id}})}function co(e,t){return Pi.find(n=>n.options.id===e&&(t?n.descriptor.app===t:!0))}function g0(){const e=lf();e.hook("addInspector",({inspector:r,plugin:o})=>{m0(r,o.descriptor)});const t=Kn(async({inspectorId:r,plugin:o})=>{var s;if(!r||!((s=o==null?void 0:o.descriptor)!=null&&s.app)||Fe.highPerfModeEnabled)return;const i=co(r,o.descriptor.app),a={app:o.descriptor.app,inspectorId:r,filter:(i==null?void 0:i.treeFilter)||"",rootNodes:[]};await new Promise(l=>{e.callHookWith(async u=>{await Promise.all(u.map(c=>c(a))),l()},"getInspectorTree")}),e.callHookWith(async l=>{await Promise.all(l.map(u=>u({inspectorId:r,rootNodes:a.rootNodes})))},"sendInspectorTreeToClient")},120);e.hook("sendInspectorTree",t);const n=Kn(async({inspectorId:r,plugin:o})=>{var s;if(!r||!((s=o==null?void 0:o.descriptor)!=null&&s.app)||Fe.highPerfModeEnabled)return;const i=co(r,o.descriptor.app),a={app:o.descriptor.app,inspectorId:r,nodeId:(i==null?void 0:i.selectedNodeId)||"",state:null},l={currentTab:`custom-inspector:${r}`};a.nodeId&&await new Promise(u=>{e.callHookWith(async c=>{await Promise.all(c.map(f=>f(a,l))),u()},"getInspectorState")}),e.callHookWith(async u=>{await Promise.all(u.map(c=>c({inspectorId:r,nodeId:a.nodeId,state:a.state})))},"sendInspectorStateToClient")},120);return e.hook("sendInspectorState",n),e.hook("customInspectorSelectNode",({inspectorId:r,nodeId:o,plugin:s})=>{const i=co(r,s.descriptor.app);i&&(i.selectedNodeId=o)}),e.hook("timelineLayerAdded",({options:r,plugin:o})=>{h0(r,o.descriptor)}),e.hook("timelineEventAdded",({options:r,plugin:o})=>{var s;const i=["performance","component-event","keyboard","mouse"];Fe.highPerfModeEnabled||!((s=Fe.timelineLayersState)!=null&&s[o.descriptor.id])&&!i.includes(r.layerId)||e.callHookWith(async a=>{await Promise.all(a.map(l=>l(r)))},"sendTimelineEventToClient")}),e.hook("getComponentInstances",async({app:r})=>{const o=r.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(!o)return null;const s=o.id.toString();return[...o.instanceMap].filter(([a])=>a.split(":")[0]===s).map(([,a])=>a)}),e.hook("getComponentBounds",async({instance:r})=>Tn(r)),e.hook("getComponentName",({instance:r})=>jo(r)),e.hook("componentHighlight",({uid:r})=>{const o=Ge.value.instanceMap.get(r);o&&e0(o)}),e.hook("componentUnhighlight",()=>{_f()}),e}var ul,cl;(cl=(ul=X).__VUE_DEVTOOLS_KIT_APP_RECORDS__)!=null||(ul.__VUE_DEVTOOLS_KIT_APP_RECORDS__=[]);var fl,dl;(dl=(fl=X).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__)!=null||(fl.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__={});var pl,hl;(hl=(pl=X).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__)!=null||(pl.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__="");var ml,gl;(gl=(ml=X).__VUE_DEVTOOLS_KIT_CUSTOM_TABS__)!=null||(ml.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__=[]);var _l,vl;(vl=(_l=X).__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__)!=null||(_l.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__=[]);var bn="__VUE_DEVTOOLS_KIT_GLOBAL_STATE__";function _0(){return{connected:!1,clientConnected:!1,vitePluginDetected:!0,appRecords:[],activeAppRecordId:"",tabs:[],commands:[],highPerfModeEnabled:!0,devtoolsClientDetected:{},perfUniqueGroupId:0,timelineLayersState:d0()}}var yl,bl;(bl=(yl=X)[bn])!=null||(yl[bn]=_0());var v0=Kn(e=>{rr.hooks.callHook("devtoolsStateUpdated",{state:e})});Kn((e,t)=>{rr.hooks.callHook("devtoolsConnectedUpdated",{state:e,oldState:t})});var zo=new Proxy(X.__VUE_DEVTOOLS_KIT_APP_RECORDS__,{get(e,t,n){return t==="value"?X.__VUE_DEVTOOLS_KIT_APP_RECORDS__:X.__VUE_DEVTOOLS_KIT_APP_RECORDS__[t]}}),Ge=new Proxy(X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__,{get(e,t,n){return t==="value"?X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__:t==="id"?X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__:X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__[t]}});function Ef(){v0({...X[bn],appRecords:zo.value,activeAppRecordId:Ge.id,tabs:X.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__,commands:X.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__})}function y0(e){X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__=e,Ef()}function b0(e){X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__=e,Ef()}var Fe=new Proxy(X[bn],{get(e,t){return t==="appRecords"?zo:t==="activeAppRecordId"?Ge.id:t==="tabs"?X.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__:t==="commands"?X.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__:X[bn][t]},deleteProperty(e,t){return delete e[t],!0},set(e,t,n){return{...X[bn]},e[t]=n,X[bn][t]=n,!0}});function E0(e={}){var t,n,r;const{file:o,host:s,baseUrl:i=window.location.origin,line:a=0,column:l=0}=e;if(o){if(s==="chrome-extension"){const u=o.replace(/\\/g,"\\\\"),c=(n=(t=window.VUE_DEVTOOLS_CONFIG)==null?void 0:t.openInEditorHost)!=null?n:"/";fetch(`${c}__open-in-editor?file=${encodeURI(o)}`).then(f=>{if(!f.ok){const d=`Opening component ${u} failed`;console.log(`%c${d}`,"color:red")}})}else if(Fe.vitePluginDetected){const u=(r=X.__VUE_DEVTOOLS_OPEN_IN_EDITOR_BASE_URL__)!=null?r:i;X.__VUE_INSPECTOR__.openInEditor(u,o,a,l)}}}P();P();P();P();P();var El,Sl;(Sl=(El=X).__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__)!=null||(El.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__=[]);var Ii=new Proxy(X.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__,{get(e,t,n){return Reflect.get(e,t,n)}});function Bs(e){const t={};return Object.keys(e).forEach(n=>{t[n]=e[n].defaultValue}),t}function Ri(e){return`__VUE_DEVTOOLS_NEXT_PLUGIN_SETTINGS__${e}__`}function S0(e){var t,n,r;const o=(n=(t=Ii.find(s=>{var i;return s[0].id===e&&!!((i=s[0])!=null&&i.settings)}))==null?void 0:t[0])!=null?n:null;return(r=o==null?void 0:o.settings)!=null?r:null}function Sf(e,t){var n,r,o;const s=Ri(e);if(s){const i=localStorage.getItem(s);if(i)return JSON.parse(i)}if(e){const i=(r=(n=Ii.find(a=>a[0].id===e))==null?void 0:n[0])!=null?r:null;return Bs((o=i==null?void 0:i.settings)!=null?o:{})}return Bs(t)}function A0(e,t){const n=Ri(e);localStorage.getItem(n)||localStorage.setItem(n,JSON.stringify(Bs(t)))}function C0(e,t,n){const r=Ri(e),o=localStorage.getItem(r),s=JSON.parse(o||"{}"),i={...s,[t]:n};localStorage.setItem(r,JSON.stringify(i)),rr.hooks.callHookWith(a=>{a.forEach(l=>l({pluginId:e,key:t,oldValue:s[t],newValue:n,settings:i}))},"setPluginSettings")}P();P();P();P();P();P();P();P();P();P();P();var Al,Cl,ut=(Cl=(Al=X).__VUE_DEVTOOLS_HOOK)!=null?Cl:Al.__VUE_DEVTOOLS_HOOK=lf(),T0={vueAppInit(e){ut.hook("app:init",e)},vueAppUnmount(e){ut.hook("app:unmount",e)},vueAppConnected(e){ut.hook("app:connected",e)},componentAdded(e){return ut.hook("component:added",e)},componentEmit(e){return ut.hook("component:emit",e)},componentUpdated(e){return ut.hook("component:updated",e)},componentRemoved(e){return ut.hook("component:removed",e)},setupDevtoolsPlugin(e){ut.hook("devtools-plugin:setup",e)},perfStart(e){return ut.hook("perf:start",e)},perfEnd(e){return ut.hook("perf:end",e)}},Af={on:T0,setupDevToolsPlugin(e,t){return ut.callHook("devtools-plugin:setup",e,t)}},w0=class{constructor({plugin:e,ctx:t}){this.hooks=t.hooks,this.plugin=e}get on(){return{visitComponentTree:e=>{this.hooks.hook("visitComponentTree",e)},inspectComponent:e=>{this.hooks.hook("inspectComponent",e)},editComponentState:e=>{this.hooks.hook("editComponentState",e)},getInspectorTree:e=>{this.hooks.hook("getInspectorTree",e)},getInspectorState:e=>{this.hooks.hook("getInspectorState",e)},editInspectorState:e=>{this.hooks.hook("editInspectorState",e)},inspectTimelineEvent:e=>{this.hooks.hook("inspectTimelineEvent",e)},timelineCleared:e=>{this.hooks.hook("timelineCleared",e)},setPluginSettings:e=>{this.hooks.hook("setPluginSettings",e)}}}notifyComponentUpdate(e){var t;if(Fe.highPerfModeEnabled)return;const n=bf().find(r=>r.packageName===this.plugin.descriptor.packageName);if(n!=null&&n.id){if(e){const r=[e.appContext.app,e.uid,(t=e.parent)==null?void 0:t.uid,e];ut.callHook("component:updated",...r)}else ut.callHook("component:updated");this.hooks.callHook("sendInspectorState",{inspectorId:n.id,plugin:this.plugin})}}addInspector(e){this.hooks.callHook("addInspector",{inspector:e,plugin:this.plugin}),this.plugin.descriptor.settings&&A0(e.id,this.plugin.descriptor.settings)}sendInspectorTree(e){Fe.highPerfModeEnabled||this.hooks.callHook("sendInspectorTree",{inspectorId:e,plugin:this.plugin})}sendInspectorState(e){Fe.highPerfModeEnabled||this.hooks.callHook("sendInspectorState",{inspectorId:e,plugin:this.plugin})}selectInspectorNode(e,t){this.hooks.callHook("customInspectorSelectNode",{inspectorId:e,nodeId:t,plugin:this.plugin})}visitComponentTree(e){return this.hooks.callHook("visitComponentTree",e)}now(){return Fe.highPerfModeEnabled?0:Date.now()}addTimelineLayer(e){this.hooks.callHook("timelineLayerAdded",{options:e,plugin:this.plugin})}addTimelineEvent(e){Fe.highPerfModeEnabled||this.hooks.callHook("timelineEventAdded",{options:e,plugin:this.plugin})}getSettings(e){return Sf(e??this.plugin.descriptor.id,this.plugin.descriptor.settings)}getComponentInstances(e){return this.hooks.callHook("getComponentInstances",{app:e})}getComponentBounds(e){return this.hooks.callHook("getComponentBounds",{instance:e})}getComponentName(e){return this.hooks.callHook("getComponentName",{instance:e})}highlightElement(e){const t=e.__VUE_DEVTOOLS_NEXT_UID__;return this.hooks.callHook("componentHighlight",{uid:t})}unhighlightElement(){return this.hooks.callHook("componentUnhighlight")}},O0=w0;P();P();P();P();var x0="__vue_devtool_undefined__",k0="__vue_devtool_infinity__",P0="__vue_devtool_negative_infinity__",I0="__vue_devtool_nan__";P();P();var R0={[x0]:"undefined",[I0]:"NaN",[k0]:"Infinity",[P0]:"-Infinity"};Object.entries(R0).reduce((e,[t,n])=>(e[n]=t,e),{});P();P();P();P();P();var Tl,wl;(wl=(Tl=X).__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__)!=null||(Tl.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__=new Set);function L0(e,t){return Af.setupDevToolsPlugin(e,t)}function D0(e,t){const[n,r]=e;if(n.app!==t)return;const o=new O0({plugin:{setupFn:r,descriptor:n},ctx:rr});n.packageName==="vuex"&&o.on.editInspectorState(s=>{o.sendInspectorState(s.inspectorId)}),r(o)}function Cf(e){X.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.has(e)||Fe.highPerfModeEnabled||(X.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.add(e),Ii.forEach(t=>{D0(t,e)}))}P();P();var Dr="__VUE_DEVTOOLS_ROUTER__",Wn="__VUE_DEVTOOLS_ROUTER_INFO__",Ol,xl;(xl=(Ol=X)[Wn])!=null||(Ol[Wn]={currentRoute:null,routes:[]});var kl,Pl;(Pl=(kl=X)[Dr])!=null||(kl[Dr]={});new Proxy(X[Wn],{get(e,t){return X[Wn][t]}});new Proxy(X[Dr],{get(e,t){if(t==="value")return X[Dr]}});function V0(e){const t=new Map;return((e==null?void 0:e.getRoutes())||[]).filter(n=>!t.has(n.path)&&t.set(n.path,1))}function Li(e){return e.map(t=>{let{path:n,name:r,children:o,meta:s}=t;return o!=null&&o.length&&(o=Li(o)),{path:n,name:r,children:o,meta:s}})}function N0(e){if(e){const{fullPath:t,hash:n,href:r,path:o,name:s,matched:i,params:a,query:l}=e;return{fullPath:t,hash:n,href:r,path:o,name:s,params:a,query:l,matched:Li(i)}}return e}function M0(e,t){function n(){var r;const o=(r=e.app)==null?void 0:r.config.globalProperties.$router,s=N0(o==null?void 0:o.currentRoute.value),i=Li(V0(o)),a=console.warn;console.warn=()=>{},X[Wn]={currentRoute:s?el(s):{},routes:el(i)},X[Dr]=o,console.warn=a}n(),Af.on.componentUpdated(Kn(()=>{var r;((r=t.value)==null?void 0:r.app)===e.app&&(n(),!Fe.highPerfModeEnabled&&rr.hooks.callHook("routerInfoUpdated",{state:X[Wn]}))},200))}function F0(e){return{async getInspectorTree(t){const n={...t,app:Ge.value.app,rootNodes:[]};return await new Promise(r=>{e.callHookWith(async o=>{await Promise.all(o.map(s=>s(n))),r()},"getInspectorTree")}),n.rootNodes},async getInspectorState(t){const n={...t,app:Ge.value.app,state:null},r={currentTab:`custom-inspector:${t.inspectorId}`};return await new Promise(o=>{e.callHookWith(async s=>{await Promise.all(s.map(i=>i(n,r))),o()},"getInspectorState")}),n.state},editInspectorState(t){const n=new u0,r={...t,app:Ge.value.app,set:(o,s=t.path,i=t.state.value,a)=>{n.set(o,s,i,a||n.createDefaultSetCallback(t.state))}};e.callHookWith(o=>{o.forEach(s=>s(r))},"editInspectorState")},sendInspectorState(t){const n=co(t);e.callHook("sendInspectorState",{inspectorId:t,plugin:{descriptor:n.descriptor,setupFn:()=>({})}})},inspectComponentInspector(){return r0()},cancelInspectComponentInspector(){return n0()},getComponentRenderCode(t){const n=Ms(Ge.value,t);if(n)return(n==null?void 0:n.type)instanceof Function?n.type.toString():n.render.toString()},scrollToComponent(t){return o0({id:t})},openInEditor:E0,getVueInspector:a0,toggleApp(t){const n=zo.value.find(r=>r.id===t);n&&(b0(t),y0(n),M0(n,Ge),yf(),Cf(n.app))},inspectDOM(t){const n=Ms(Ge.value,t);if(n){const[r]=wi(n);r&&(X.__VUE_DEVTOOLS_INSPECT_DOM_TARGET__=r)}},updatePluginSettings(t,n,r){C0(t,n,r)},getPluginSettings(t){return{options:S0(t),values:Sf(t)}}}}P();var Il,Rl;(Rl=(Il=X).__VUE_DEVTOOLS_ENV__)!=null||(Il.__VUE_DEVTOOLS_ENV__={vitePluginDetected:!1});var Ll=g0(),Dl,Vl;(Vl=(Dl=X).__VUE_DEVTOOLS_KIT_CONTEXT__)!=null||(Dl.__VUE_DEVTOOLS_KIT_CONTEXT__={hooks:Ll,get state(){return{...Fe,activeAppRecordId:Ge.id,activeAppRecord:Ge.value,appRecords:zo.value}},api:F0(Ll)});var rr=X.__VUE_DEVTOOLS_KIT_CONTEXT__;P();Fv(Bv());var Nl,Ml;(Ml=(Nl=X).__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__)!=null||(Nl.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__={id:0,appIds:new Set});P();function H0(e){Fe.highPerfModeEnabled=e??!Fe.highPerfModeEnabled,!e&&Ge.value&&Cf(Ge.value.app)}P();P();P();function B0(e){Fe.devtoolsClientDetected={...Fe.devtoolsClientDetected,...e};const t=Object.values(Fe.devtoolsClientDetected).some(Boolean);H0(!t)}var Fl,Hl;(Hl=(Fl=X).__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__)!=null||(Fl.__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__=B0);P();P();P();P();P();P();P();var $0=class{constructor(){this.keyToValue=new Map,this.valueToKey=new Map}set(e,t){this.keyToValue.set(e,t),this.valueToKey.set(t,e)}getByKey(e){return this.keyToValue.get(e)}getByValue(e){return this.valueToKey.get(e)}clear(){this.keyToValue.clear(),this.valueToKey.clear()}},Tf=class{constructor(e){this.generateIdentifier=e,this.kv=new $0}register(e,t){this.kv.getByValue(e)||(t||(t=this.generateIdentifier(e)),this.kv.set(t,e))}clear(){this.kv.clear()}getIdentifier(e){return this.kv.getByValue(e)}getValue(e){return this.kv.getByKey(e)}},U0=class extends Tf{constructor(){super(e=>e.name),this.classToAllowedProps=new Map}register(e,t){typeof t=="object"?(t.allowProps&&this.classToAllowedProps.set(e,t.allowProps),super.register(e,t.identifier)):super.register(e,t)}getAllowedProps(e){return this.classToAllowedProps.get(e)}};P();P();function j0(e){if("values"in Object)return Object.values(e);const t=[];for(const n in e)e.hasOwnProperty(n)&&t.push(e[n]);return t}function z0(e,t){const n=j0(e);if("find"in n)return n.find(t);const r=n;for(let o=0;ot(r,n))}function fo(e,t){return e.indexOf(t)!==-1}function Bl(e,t){for(let n=0;nt.isApplicable(e))}findByName(e){return this.transfomers[e]}};P();P();var W0=e=>Object.prototype.toString.call(e).slice(8,-1),wf=e=>typeof e>"u",q0=e=>e===null,Vr=e=>typeof e!="object"||e===null||e===Object.prototype?!1:Object.getPrototypeOf(e)===null?!0:Object.getPrototypeOf(e)===Object.prototype,$s=e=>Vr(e)&&Object.keys(e).length===0,ln=e=>Array.isArray(e),G0=e=>typeof e=="string",Y0=e=>typeof e=="number"&&!isNaN(e),X0=e=>typeof e=="boolean",Z0=e=>e instanceof RegExp,Nr=e=>e instanceof Map,Mr=e=>e instanceof Set,Of=e=>W0(e)==="Symbol",J0=e=>e instanceof Date&&!isNaN(e.valueOf()),Q0=e=>e instanceof Error,$l=e=>typeof e=="number"&&isNaN(e),e1=e=>X0(e)||q0(e)||wf(e)||Y0(e)||G0(e)||Of(e),t1=e=>typeof e=="bigint",n1=e=>e===1/0||e===-1/0,r1=e=>ArrayBuffer.isView(e)&&!(e instanceof DataView),o1=e=>e instanceof URL;P();var xf=e=>e.replace(/\./g,"\\."),ms=e=>e.map(String).map(xf).join("."),Ar=e=>{const t=[];let n="";for(let o=0;onull,()=>{}),wt(t1,"bigint",e=>e.toString(),e=>typeof BigInt<"u"?BigInt(e):(console.error("Please add a BigInt polyfill."),e)),wt(J0,"Date",e=>e.toISOString(),e=>new Date(e)),wt(Q0,"Error",(e,t)=>{const n={name:e.name,message:e.message};return t.allowedErrorProps.forEach(r=>{n[r]=e[r]}),n},(e,t)=>{const n=new Error(e.message);return n.name=e.name,n.stack=e.stack,t.allowedErrorProps.forEach(r=>{n[r]=e[r]}),n}),wt(Z0,"regexp",e=>""+e,e=>{const t=e.slice(1,e.lastIndexOf("/")),n=e.slice(e.lastIndexOf("/")+1);return new RegExp(t,n)}),wt(Mr,"set",e=>[...e.values()],e=>new Set(e)),wt(Nr,"map",e=>[...e.entries()],e=>new Map(e)),wt(e=>$l(e)||n1(e),"number",e=>$l(e)?"NaN":e>0?"Infinity":"-Infinity",Number),wt(e=>e===0&&1/e===-1/0,"number",()=>"-0",Number),wt(o1,"URL",e=>e.toString(),e=>new URL(e))];function Ko(e,t,n,r){return{isApplicable:e,annotation:t,transform:n,untransform:r}}var Pf=Ko((e,t)=>Of(e)?!!t.symbolRegistry.getIdentifier(e):!1,(e,t)=>["symbol",t.symbolRegistry.getIdentifier(e)],e=>e.description,(e,t,n)=>{const r=n.symbolRegistry.getValue(t[1]);if(!r)throw new Error("Trying to deserialize unknown symbol");return r}),s1=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,Uint8ClampedArray].reduce((e,t)=>(e[t.name]=t,e),{}),If=Ko(r1,e=>["typed-array",e.constructor.name],e=>[...e],(e,t)=>{const n=s1[t[1]];if(!n)throw new Error("Trying to deserialize unknown typed array");return new n(e)});function Rf(e,t){return e!=null&&e.constructor?!!t.classRegistry.getIdentifier(e.constructor):!1}var Lf=Ko(Rf,(e,t)=>["class",t.classRegistry.getIdentifier(e.constructor)],(e,t)=>{const n=t.classRegistry.getAllowedProps(e.constructor);if(!n)return{...e};const r={};return n.forEach(o=>{r[o]=e[o]}),r},(e,t,n)=>{const r=n.classRegistry.getValue(t[1]);if(!r)throw new Error("Trying to deserialize unknown class - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564");return Object.assign(Object.create(r.prototype),e)}),Df=Ko((e,t)=>!!t.customTransformerRegistry.findApplicable(e),(e,t)=>["custom",t.customTransformerRegistry.findApplicable(e).name],(e,t)=>t.customTransformerRegistry.findApplicable(e).serialize(e),(e,t,n)=>{const r=n.customTransformerRegistry.findByName(t[1]);if(!r)throw new Error("Trying to deserialize unknown custom value");return r.deserialize(e)}),i1=[Lf,Pf,Df,If],Ul=(e,t)=>{const n=Bl(i1,o=>o.isApplicable(e,t));if(n)return{value:n.transform(e,t),type:n.annotation(e,t)};const r=Bl(kf,o=>o.isApplicable(e,t));if(r)return{value:r.transform(e,t),type:r.annotation}},Vf={};kf.forEach(e=>{Vf[e.annotation]=e});var a1=(e,t,n)=>{if(ln(t))switch(t[0]){case"symbol":return Pf.untransform(e,t,n);case"class":return Lf.untransform(e,t,n);case"custom":return Df.untransform(e,t,n);case"typed-array":return If.untransform(e,t,n);default:throw new Error("Unknown transformation: "+t)}else{const r=Vf[t];if(!r)throw new Error("Unknown transformation: "+t);return r.untransform(e,n)}};P();var In=(e,t)=>{const n=e.keys();for(;t>0;)n.next(),t--;return n.next().value};function Nf(e){if(fo(e,"__proto__"))throw new Error("__proto__ is not allowed as a property");if(fo(e,"prototype"))throw new Error("prototype is not allowed as a property");if(fo(e,"constructor"))throw new Error("constructor is not allowed as a property")}var l1=(e,t)=>{Nf(t);for(let n=0;n{if(Nf(t),t.length===0)return n(e);let r=e;for(let s=0;sjs(s,t,[...n,...Ar(i)]));return}const[r,o]=e;o&&qn(o,(s,i)=>{js(s,t,[...n,...Ar(i)])}),t(r,n)}function u1(e,t,n){return js(t,(r,o)=>{e=Us(e,o,s=>a1(s,r,n))}),e}function c1(e,t){function n(r,o){const s=l1(e,Ar(o));r.map(Ar).forEach(i=>{e=Us(e,i,()=>s)})}if(ln(t)){const[r,o]=t;r.forEach(s=>{e=Us(e,Ar(s),()=>e)}),o&&qn(o,n)}else qn(t,n);return e}var f1=(e,t)=>Vr(e)||ln(e)||Nr(e)||Mr(e)||Rf(e,t);function d1(e,t,n){const r=n.get(e);r?r.push(t):n.set(e,[t])}function p1(e,t){const n={};let r;return e.forEach(o=>{if(o.length<=1)return;t||(o=o.map(a=>a.map(String)).sort((a,l)=>a.length-l.length));const[s,...i]=o;s.length===0?r=i.map(ms):n[ms(s)]=i.map(ms)}),r?$s(n)?[r]:[r,n]:$s(n)?void 0:n}var Mf=(e,t,n,r,o=[],s=[],i=new Map)=>{var a;const l=e1(e);if(!l){d1(e,o,t);const _=i.get(e);if(_)return r?{transformedValue:null}:_}if(!f1(e,n)){const _=Ul(e,n),v=_?{transformedValue:_.value,annotations:[_.type]}:{transformedValue:e};return l||i.set(e,v),v}if(fo(s,e))return{transformedValue:null};const u=Ul(e,n),c=(a=u==null?void 0:u.value)!=null?a:e,f=ln(c)?[]:{},d={};qn(c,(_,v)=>{if(v==="__proto__"||v==="constructor"||v==="prototype")throw new Error(`Detected property ${v}. This is a prototype pollution risk, please remove it from your object.`);const b=Mf(_,t,n,r,[...o,v],[...s,e],i);f[v]=b.transformedValue,ln(b.annotations)?d[v]=b.annotations:Vr(b.annotations)&&qn(b.annotations,(E,A)=>{d[xf(v)+"."+A]=E})});const p=$s(d)?{transformedValue:f,annotations:u?[u.type]:void 0}:{transformedValue:f,annotations:u?[u.type,d]:d};return l||i.set(e,p),p};P();P();function Ff(e){return Object.prototype.toString.call(e).slice(8,-1)}function jl(e){return Ff(e)==="Array"}function h1(e){if(Ff(e)!=="Object")return!1;const t=Object.getPrototypeOf(e);return!!t&&t.constructor===Object&&t===Object.prototype}function m1(e,t,n,r,o){const s={}.propertyIsEnumerable.call(r,t)?"enumerable":"nonenumerable";s==="enumerable"&&(e[t]=n),o&&s==="nonenumerable"&&Object.defineProperty(e,t,{value:n,enumerable:!1,writable:!0,configurable:!0})}function zs(e,t={}){if(jl(e))return e.map(o=>zs(o,t));if(!h1(e))return e;const n=Object.getOwnPropertyNames(e),r=Object.getOwnPropertySymbols(e);return[...n,...r].reduce((o,s)=>{if(jl(t.props)&&!t.props.includes(s))return o;const i=e[s],a=zs(i,t);return m1(o,s,a,e,t.nonenumerable),o},{})}var we=class{constructor({dedupe:e=!1}={}){this.classRegistry=new U0,this.symbolRegistry=new Tf(t=>{var n;return(n=t.description)!=null?n:""}),this.customTransformerRegistry=new K0,this.allowedErrorProps=[],this.dedupe=e}serialize(e){const t=new Map,n=Mf(e,t,this,this.dedupe),r={json:n.transformedValue};n.annotations&&(r.meta={...r.meta,values:n.annotations});const o=p1(t,this.dedupe);return o&&(r.meta={...r.meta,referentialEqualities:o}),r}deserialize(e){const{json:t,meta:n}=e;let r=zs(t);return n!=null&&n.values&&(r=u1(r,n.values,this)),n!=null&&n.referentialEqualities&&(r=c1(r,n.referentialEqualities)),r}stringify(e){return JSON.stringify(this.serialize(e))}parse(e){return this.deserialize(JSON.parse(e))}registerClass(e,t){this.classRegistry.register(e,t)}registerSymbol(e,t){this.symbolRegistry.register(e,t)}registerCustom(e,t){this.customTransformerRegistry.register({name:t,...e})}allowErrorProps(...e){this.allowedErrorProps.push(...e)}};we.defaultInstance=new we;we.serialize=we.defaultInstance.serialize.bind(we.defaultInstance);we.deserialize=we.defaultInstance.deserialize.bind(we.defaultInstance);we.stringify=we.defaultInstance.stringify.bind(we.defaultInstance);we.parse=we.defaultInstance.parse.bind(we.defaultInstance);we.registerClass=we.defaultInstance.registerClass.bind(we.defaultInstance);we.registerSymbol=we.defaultInstance.registerSymbol.bind(we.defaultInstance);we.registerCustom=we.defaultInstance.registerCustom.bind(we.defaultInstance);we.allowErrorProps=we.defaultInstance.allowErrorProps.bind(we.defaultInstance);P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();var zl,Kl;(Kl=(zl=X).__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__)!=null||(zl.__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__=[]);var Wl,ql;(ql=(Wl=X).__VUE_DEVTOOLS_KIT_RPC_CLIENT__)!=null||(Wl.__VUE_DEVTOOLS_KIT_RPC_CLIENT__=null);var Gl,Yl;(Yl=(Gl=X).__VUE_DEVTOOLS_KIT_RPC_SERVER__)!=null||(Gl.__VUE_DEVTOOLS_KIT_RPC_SERVER__=null);var Xl,Zl;(Zl=(Xl=X).__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__)!=null||(Xl.__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__=null);var Jl,Ql;(Ql=(Jl=X).__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__)!=null||(Jl.__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__=null);var eu,tu;(tu=(eu=X).__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__)!=null||(eu.__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__=null);P();P();P();P();P();P();P();const g1=JSON.parse(`{"locales":{"/":{"selectLanguageName":"简体中文","selectLanguageText":"选择语言","selectLanguageAriaselectLanguageName":"选择语言","editLink":true,"editLinkText":"在 GitHub 上编辑此页","lastUpdatedText":"上次更新"}},"repo":"Cactus-proj/A-Philosophy-of-Software-Design-zh","docsRepo":"Cactus-proj/A-Philosophy-of-Software-Design-zh","docsBranch":"main","docsDir":"docs","contributors":false,"sidebarDepth":2,"sidebar":"auto","colorMode":"auto","colorModeSwitch":true,"navbar":[],"logo":null,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","editLink":true,"editLinkText":"Edit this page","lastUpdated":true,"lastUpdatedText":"Last Updated","contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),_1=ve(g1),Hf=()=>_1,Bf=Symbol(""),v1=()=>{const e=Je(Bf);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},y1=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},b1=un({enhance({app:e}){const t=Hf(),n=e._context.provides[mi],r=F(()=>y1(t.value,n.routeLocale.value));e.provide(Bf,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}}),L0({app:e,id:"org.vuejs.vuepress.plugin-theme-data",label:"VuePress Theme Data Plugin",packageName:"@vuepress/plugin-theme-data",homepage:"https://v2.vuepress.vuejs.org",logo:"https://v2.vuepress.vuejs.org/images/hero.png",componentStateTypes:["VuePress"]},o=>{o.on.inspectComponent(s=>{s.instanceData.state.push({type:"VuePress",key:"themeData",editable:!1,value:t.value},{type:"VuePress",key:"themeLocaleData",editable:!1,value:r.value})})})}}),E1=Object.freeze(Object.defineProperty({__proto__:null,default:b1},Symbol.toStringTag,{value:"Module"})),S1=()=>Hf(),Be=()=>v1(),$f=Symbol(""),A1=e=>{const t=(n=e.value)=>{const r=window.document.documentElement;r.dataset.theme=n?"dark":"light"};Qe(()=>{je(e,t,{immediate:!0})}),Lo(()=>{t()})},Di=()=>{const e=Je($f);if(!e)throw new Error("useDarkMode() is called without provider.");return e},C1=()=>{const e=Be(),t=o_(),n=Ai("vuepress-color-scheme",e.value.colorMode),r=F({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});An($f,r),A1(r)};let gs=null,cr=null;const T1={wait:()=>gs,pending:()=>{gs=new Promise(e=>{cr=e})},resolve:()=>{cr==null||cr(),gs=null,cr=null}},Uf=()=>T1,Gn=(e,t)=>{const{notFound:n,meta:r,path:o}=Lr(e,t);return n?{text:o,link:o}:{text:r.title||o,link:o}},nu=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),w1=(e,t)=>{if(t.hash===e)return!0;const n=nu(t.path),r=nu(e);return n===r},jf=(e,t)=>e.link&&w1(e.link,t)?!0:"children"in e?e.children.some(n=>jf(n,t)):!1,zf=e=>!em(e)&&!Kr(e),Kf=e=>!No(e)||e.includes("github.com")?"GitHub":e.includes("bitbucket.org")?"Bitbucket":e.includes("gitlab.com")?"GitLab":e.includes("gitee.com")?"Gitee":null,O1={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},x1=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Kf(e);return n!==null?O1[n]:null},k1=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=x1({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,No(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,Tc(`${Cc(n)}/${r}`)):null},Bn=(e="",t="")=>rf(t)||Kr(t)?t:`${um(e)}${t}`,po=ve([]),P1=()=>{const e=wn(),t=Be(),n=Ct(),r=F(()=>n.value.sidebarDepth??t.value.sidebarDepth??2);e.beforeEach((s,i)=>{s.path!==i.path&&(po.value=[])});const o=()=>{if(r.value<=0){po.value=[];return}po.value=b_({levels:[2,r.value+1],ignore:[".vp-badge"]})};je(r,o),Qe(o)},I1=()=>po,R1=e=>({text:e.title,link:e.link,children:Vi(e.children)}),Vi=e=>e?e.map(t=>R1(t)):[],Wf=(e,t)=>[{text:e.title,children:Vi(t)}],qf=(e,t,n,r="")=>{const o=(s,i)=>{var l;const a=bt(s)?Gn(Bn(i,s)):bt(s.link)?{...s,link:zf(s.link)?Gn(Bn(i,s.link)).link:s.link}:s;if("children"in a)return{...a,children:a.children.map(u=>o(u,Bn(i,a.prefix)))};if(a.link===n){const u=((l=t[0])==null?void 0:l.level)===1?t[0].children:t;return{...a,children:Vi(u)}}return a};return e.map(s=>o(s,r))},L1=(e,t,n,r)=>{const o=A_(e).sort((s,i)=>i.length-s.length);for(const s of o)if(nf(decodeURI(r),s)){const i=e[s];return i?i==="heading"?Wf(t,n):qf(i,n,r,s):[]}return console.warn(`${decodeURI(r)} is missing sidebar config.`),[]},Gf=Symbol("sidebarItems"),Ni=()=>{const e=Je(Gf);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},D1=(e,t,n,r,o)=>e===!1?[]:e==="heading"?Wf(t,o):Array.isArray(e)?qf(e,o,n,r):di(e)?L1(e,t,o,n):[],V1=()=>{const e=Be(),t=Ct(),n=er(),r=Wt(),o=Wr(),s=I1(),i=F(()=>t.value.home?!1:t.value.sidebar??e.value.sidebar??"heading"),a=F(()=>D1(i.value,n.value,r.path,o.value,s.value));An(Gf,a)},N1=he({__name:"Badge",props:{type:{default:"tip"},text:{default:""},vertical:{default:void 0}},setup(e,{expose:t}){t();const n={};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),Pe=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n};function M1(e,t,n,r,o,s){return Y(),ne("span",{class:rt(["vp-badge",n.type]),style:Xn({verticalAlign:n.vertical})},[Le(e.$slots,"default",{},()=>[on(Re(n.text),1)])],6)}const F1=Pe(N1,[["render",M1],["__file","Badge.vue"]]),H1=he({__name:"VPHomeFeatures",setup(e,{expose:t}){t();const n=Ct(),r=F(()=>n.value.features??[]),o={frontmatter:n,features:r};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}}),B1={key:0,class:"vp-features"};function $1(e,t,n,r,o,s){return r.features.length?(Y(),ne("div",B1,[(Y(!0),ne(ge,null,nn(r.features,i=>(Y(),ne("div",{key:i.title,class:"vp-feature"},[ie("h2",null,Re(i.title),1),ie("p",null,Re(i.details),1)]))),128))])):He("",!0)}const U1=Pe(H1,[["render",$1],["__file","VPHomeFeatures.vue"]]),j1=he({__name:"VPHomeFooter",setup(e,{expose:t}){t();const n=Ct(),r=F(()=>n.value.footer),o=F(()=>n.value.footerHtml),s={frontmatter:n,footer:r,footerHtml:o};return Object.defineProperty(s,"__isScriptSetup",{enumerable:!1,value:!0}),s}}),z1=["innerHTML"],K1=["textContent"];function W1(e,t,n,r,o,s){return r.footer?(Y(),ne(ge,{key:0},[r.footerHtml?(Y(),ne("div",{key:0,class:"vp-footer","vp-footer":"",innerHTML:r.footer},null,8,z1)):(Y(),ne("div",{key:1,class:"vp-footer","vp-footer":"",textContent:Re(r.footer)},null,8,K1))],64)):He("",!0)}const q1=Pe(j1,[["render",W1],["__file","VPHomeFooter.vue"]]),G1=he({__name:"VPHomeHero",setup(e,{expose:t}){t();const n=Ct(),r=gi(),o=Di(),s=F(()=>n.value.heroText===null?null:n.value.heroText||r.value.title||"Hello"),i=F(()=>n.value.tagline===null?null:n.value.tagline||r.value.description||"Welcome to your VuePress site"),a=F(()=>o.value&&n.value.heroImageDark!==void 0?n.value.heroImageDark:n.value.heroImage),l=F(()=>n.value.heroAlt||s.value||"hero"),u=F(()=>n.value.heroHeight??280),c=F(()=>Array.isArray(n.value.actions)?n.value.actions.map(({text:p,link:_,type:v="primary"})=>({text:p,link:_,type:v})):[]),d={frontmatter:n,siteLocale:r,isDarkMode:o,heroText:s,tagline:i,heroImage:a,heroAlt:l,heroHeight:u,actions:c,HomeHeroImage:()=>{if(!a.value)return null;const p=_e("img",{class:"vp-hero-image",src:yi(a.value),alt:l.value,height:u.value});return n.value.heroImageDark===void 0?p:_e(_i,()=>p)},get AutoLink(){return tr}};return Object.defineProperty(d,"__isScriptSetup",{enumerable:!1,value:!0}),d}}),Y1={class:"vp-hero"},X1={key:0,id:"main-title"},Z1={key:1,class:"vp-hero-description"},J1={key:2,class:"vp-hero-actions"};function Q1(e,t,n,r,o,s){return Y(),ne("header",Y1,[ae(r.HomeHeroImage),r.heroText?(Y(),ne("h1",X1,Re(r.heroText),1)):He("",!0),r.tagline?(Y(),ne("p",Z1,Re(r.tagline),1)):He("",!0),r.actions.length?(Y(),ne("p",J1,[(Y(!0),ne(ge,null,nn(r.actions,i=>(Y(),De(r.AutoLink,{key:i.text,class:rt(["vp-hero-action-button",[i.type]]),config:i},null,8,["class","config"]))),128))])):He("",!0)])}const ey=Pe(G1,[["render",Q1],["__file","VPHomeHero.vue"]]),ty=he({__name:"VPHome",setup(e,{expose:t}){t();const n={VPHomeFeatures:U1,VPHomeFooter:q1,VPHomeHero:ey,get Content(){return vi}};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),ny={class:"vp-home"},ry={class:"theme-default-content","vp-content":""};function oy(e,t,n,r,o,s){return Y(),ne("main",ny,[ae(r.VPHomeHero),ae(r.VPHomeFeatures),ie("div",ry,[ae(r.Content)]),ae(r.VPHomeFooter)])}const sy=Pe(ty,[["render",oy],["__file","VPHome.vue"]]),iy=he({__name:"VPNavbarBrand",setup(e,{expose:t}){t();const n=Wr(),r=gi(),o=Be(),s=Di(),i=F(()=>o.value.home||n.value),a=F(()=>r.value.title),l=F(()=>s.value&&o.value.logoDark!==void 0?o.value.logoDark:o.value.logo),u=F(()=>o.value.logoAlt??a.value),c=F(()=>a.value.toLocaleUpperCase().trim()===u.value.toLocaleUpperCase().trim()),d={routeLocale:n,siteLocale:r,themeLocale:o,isDarkMode:s,navbarBrandLink:i,navbarBrandTitle:a,navbarBrandLogo:l,navbarBrandLogoAlt:u,navBarLogoAltMatchesTitle:c,NavbarBrandLogo:()=>{if(!l.value)return null;const p=_e("img",{class:"vp-site-logo",src:yi(l.value),alt:u.value});return o.value.logoDark===void 0?p:_e(_i,()=>p)},get RouteLink(){return Ho}};return Object.defineProperty(d,"__isScriptSetup",{enumerable:!1,value:!0}),d}}),ay=["aria-hidden"];function ly(e,t,n,r,o,s){return Y(),De(r.RouteLink,{to:r.navbarBrandLink},{default:Me(()=>[ae(r.NavbarBrandLogo),r.navbarBrandTitle?(Y(),ne("span",{key:0,class:rt(["vp-site-name",{"vp-hide-mobile":r.navbarBrandLogo}]),"aria-hidden":r.navBarLogoAltMatchesTitle},Re(r.navbarBrandTitle),11,ay)):He("",!0)]),_:1},8,["to"])}const uy=Pe(iy,[["render",ly],["__file","VPNavbarBrand.vue"]]),cy=he({__name:"VPDropdownTransition",setup(e,{expose:t}){t();const o={setHeight:s=>{s.style.height=`${s.scrollHeight}px`},unsetHeight:s=>{s.style.height=""}};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}});function fy(e,t,n,r,o,s){return Y(),De(fi,{name:"vp-dropdown",onEnter:r.setHeight,onAfterEnter:r.unsetHeight,onBeforeLeave:r.setHeight},{default:Me(()=>[Le(e.$slots,"default")]),_:3})}const Yf=Pe(cy,[["render",fy],["__file","VPDropdownTransition.vue"]]),dy=he({__name:"VPNavbarDropdown",props:{config:{}},setup(e,{expose:t}){t();const n=e,{config:r}=ku(n),o=Wt(),[s,i]=Zc(!1),a=F(()=>r.value.ariaLabel||r.value.text),l=(f,d)=>d[d.length-1]===f,u=f=>{f.detail===0?i():i(!1)};je(()=>o.path,()=>{i(!1)});const c={props:n,config:r,route:o,open:s,toggleOpen:i,dropdownAriaLabel:a,isLastItemOfArray:l,handleDropdown:u,VPDropdownTransition:Yf,get AutoLink(){return tr}};return Object.defineProperty(c,"__isScriptSetup",{enumerable:!1,value:!0}),c}}),py=["aria-label"],hy={class:"title"},my=["aria-label"],gy={class:"title"},_y={class:"vp-navbar-dropdown"},vy={class:"vp-navbar-dropdown-subtitle"},yy={key:1},by={class:"vp-navbar-dropdown-subitem-wrapper"};function Ey(e,t,n,r,o,s){return Y(),ne("div",{class:rt(["vp-navbar-dropdown-wrapper",{open:r.open}])},[ie("button",{class:"vp-navbar-dropdown-title",type:"button","aria-label":r.dropdownAriaLabel,onClick:r.handleDropdown},[ie("span",hy,Re(r.config.text),1),t[1]||(t[1]=ie("span",{class:"arrow down"},null,-1))],8,py),ie("button",{class:"vp-navbar-dropdown-title-mobile",type:"button","aria-label":r.dropdownAriaLabel,onClick:t[0]||(t[0]=()=>r.toggleOpen())},[ie("span",gy,Re(r.config.text),1),ie("span",{class:rt(["arrow",r.open?"down":"right"])},null,2)],8,my),ae(r.VPDropdownTransition,null,{default:Me(()=>[yo(ie("ul",_y,[(Y(!0),ne(ge,null,nn(r.config.children,i=>(Y(),ne("li",{key:i.text,class:"vp-navbar-dropdown-item"},["children"in i?(Y(),ne(ge,{key:0},[ie("h4",vy,[i.link?(Y(),De(r.AutoLink,{key:0,config:i,onFocusout:()=>{r.isLastItemOfArray(i,r.config.children)&&i.children.length===0&&(r.open=!1)}},null,8,["config","onFocusout"])):(Y(),ne("span",yy,Re(i.text),1))]),ie("ul",by,[(Y(!0),ne(ge,null,nn(i.children,a=>(Y(),ne("li",{key:a.link,class:"vp-navbar-dropdown-subitem"},[ae(r.AutoLink,{config:a,onFocusout:()=>{r.isLastItemOfArray(a,i.children)&&r.isLastItemOfArray(i,r.config.children)&&r.toggleOpen(!1)}},null,8,["config","onFocusout"])]))),128))])],64)):(Y(),De(r.AutoLink,{key:1,config:i,onFocusout:()=>{r.isLastItemOfArray(i,r.config.children)&&r.toggleOpen(!1)}},null,8,["config","onFocusout"]))]))),128))],512),[[Ao,r.open]])]),_:1})],2)}const Sy=Pe(dy,[["render",Ey],["__file","VPNavbarDropdown.vue"]]),Xf=(e,t="")=>bt(e)?Gn(Bn(t,e)):"children"in e?{...e,children:e.children.map(n=>Xf(n,Bn(t,e.prefix)))}:{...e,link:zf(e.link)?Gn(Bn(t,e.link)).link:e.link},Ay=()=>{const e=Be();return F(()=>(e.value.navbar||[]).map(t=>Xf(t)))},Cy=()=>{const e=Be(),t=F(()=>e.value.repo),n=F(()=>t.value?Kf(t.value):null),r=F(()=>t.value&&!No(t.value)?`https://github.com/${t.value}`:t.value),o=F(()=>r.value?e.value.repoLabel?e.value.repoLabel:n.value===null?"Source":n.value:null);return F(()=>!r.value||!o.value?[]:[{text:o.value,link:r.value}])},Ty=()=>{const e=Wt(),t=E_(),n=Wr(),r=qc(),o=gi(),s=S1(),i=Be();return F(()=>{const a=Object.keys(r.value.locales);if(a.length<2)return[];const l=e.path,u=e.fullPath;return[{text:`${i.value.selectLanguageText}`,ariaLabel:`${i.value.selectLanguageAriaLabel??i.value.selectLanguageText}`,children:a.map(f=>{var E,A;const d=((E=r.value.locales)==null?void 0:E[f])??{},p=((A=s.value.locales)==null?void 0:A[f])??{},_=`${d.lang}`,v=p.selectLanguageName??_;if(_===o.value.lang)return{text:v,activeMatch:".",link:e.fullPath};const b=l.replace(n.value,f);return{text:v,link:t.value.some(m=>m===b)?u.replace(l,b):p.home??f}})}]})},wy="719px",Oy={mobile:wy};var Fr;(function(e){e.Mobile="mobile"})(Fr||(Fr={}));const xy={[Fr.Mobile]:Number.parseInt(Oy.mobile.replace("px",""),10)},Zf=(e,t)=>{const n=xy[e];Number.isInteger(n)&&(dt("orientationchange",()=>{t(n)},!1),dt("resize",()=>{t(n)},!1),Qe(()=>{t(n)}))},ky=he({__name:"VPNavbarItems",setup(e,{expose:t}){t();const n=Ay(),r=Ty(),o=Cy(),s=ve(!1),i=F(()=>Be().value.navbarLabel??"site navigation"),a=F(()=>[...n.value,...r.value,...o.value]);Zf(Fr.Mobile,u=>{s.value=window.innerWidth(Y(),ne("div",{key:i.text,class:"vp-navbar-item"},["children"in i?(Y(),De(r.VPNavbarDropdown,{key:0,class:rt({mobile:r.isMobile}),config:i},null,8,["class","config"])):(Y(),De(r.AutoLink,{key:1,config:i},null,8,["config"]))]))),128))],8,Py)):He("",!0)}const Jf=Pe(ky,[["render",Iy],["__file","VPNavbarItems.vue"]]),Ry={},Ly={class:"dark-icon",viewBox:"0 0 32 32"};function Dy(e,t){return Y(),ne("svg",Ly,t[0]||(t[0]=[ie("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1)]))}const Vy=Pe(Ry,[["render",Dy],["__file","VPDarkIcon.vue"]]),Ny={},My={class:"light-icon",viewBox:"0 0 32 32"};function Fy(e,t){return Y(),ne("svg",My,t[0]||(t[0]=[ch('',9)]))}const Hy=Pe(Ny,[["render",Fy],["__file","VPLightIcon.vue"]]),By=he({__name:"VPToggleColorModeButton",setup(e,{expose:t}){t();const n=Be(),r=Di(),s={themeLocale:n,isDarkMode:r,toggleColorMode:()=>{r.value=!r.value},VPDarkIcon:Vy,VPLightIcon:Hy};return Object.defineProperty(s,"__isScriptSetup",{enumerable:!1,value:!0}),s}}),$y=["title"];function Uy(e,t,n,r,o,s){return Y(),ne("button",{type:"button",class:"vp-toggle-color-mode-button",title:r.themeLocale.toggleColorMode,onClick:r.toggleColorMode},[yo(ae(r.VPLightIcon,null,null,512),[[Ao,!r.isDarkMode]]),yo(ae(r.VPDarkIcon,null,null,512),[[Ao,r.isDarkMode]])],8,$y)}const jy=Pe(By,[["render",Uy],["__file","VPToggleColorModeButton.vue"]]),zy=he({__name:"VPToggleSidebarButton",emits:["toggle"],setup(e,{expose:t}){t();const r={themeLocale:Be()};return Object.defineProperty(r,"__isScriptSetup",{enumerable:!1,value:!0}),r}}),Ky=["title"];function Wy(e,t,n,r,o,s){return Y(),ne("div",{class:"vp-toggle-sidebar-button",title:r.themeLocale.toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:t[0]||(t[0]=i=>e.$emit("toggle"))},t[1]||(t[1]=[ie("div",{class:"icon","aria-hidden":"true"},[ie("span"),ie("span"),ie("span")],-1)]),8,Ky)}const qy=Pe(zy,[["render",Wy],["__file","VPToggleSidebarButton.vue"]]),Gy=he({__name:"VPNavbar",emits:["toggleSidebar"],setup(e,{expose:t}){t();const n=Qc("SearchBox")?ii("SearchBox"):()=>null,r=Be(),o=Wi("navbar"),s=Wi("navbar-brand"),i=ve(0),a=F(()=>i.value?{maxWidth:`${i.value}px`}:{}),l=(c,f)=>{var _;const d=(_=c==null?void 0:c.ownerDocument.defaultView)==null?void 0:_.getComputedStyle(c,null)[f],p=Number.parseInt(d,10);return Number.isNaN(p)?0:p};Zf(Fr.Mobile,c=>{var d;const f=l(o.value,"paddingLeft")+l(o.value,"paddingRight");window.innerWidthe.$emit("toggleSidebar"))}),ie("span",Xy,[ae(r.VPNavbarBrand)],512),ie("div",{class:"vp-navbar-items-wrapper",style:Xn(r.linksWrapperStyle)},[Le(e.$slots,"before"),ae(r.VPNavbarItems,{class:"vp-hide-mobile"}),Le(e.$slots,"after"),r.themeLocale.colorModeSwitch?(Y(),De(r.VPToggleColorModeButton,{key:0})):He("",!0),ae(r.SearchBox)],4)],512)}const Jy=Pe(Gy,[["render",Zy],["__file","VPNavbar.vue"]]),Qy={},eb={class:"edit-icon",viewBox:"0 0 1024 1024"};function tb(e,t){return Y(),ne("svg",eb,t[0]||(t[0]=[ie("g",{fill:"currentColor"},[ie("path",{d:"M430.818 653.65a60.46 60.46 0 0 1-50.96-93.281l71.69-114.012 7.773-10.365L816.038 80.138A60.46 60.46 0 0 1 859.225 62a60.46 60.46 0 0 1 43.186 18.138l43.186 43.186a60.46 60.46 0 0 1 0 86.373L588.879 565.55l-8.637 8.637-117.466 68.234a60.46 60.46 0 0 1-31.958 11.229z"}),ie("path",{d:"M728.802 962H252.891A190.883 190.883 0 0 1 62.008 771.98V296.934a190.883 190.883 0 0 1 190.883-192.61h267.754a60.46 60.46 0 0 1 0 120.92H252.891a69.962 69.962 0 0 0-69.098 69.099V771.98a69.962 69.962 0 0 0 69.098 69.098h475.911A69.962 69.962 0 0 0 797.9 771.98V503.363a60.46 60.46 0 1 1 120.922 0V771.98A190.883 190.883 0 0 1 728.802 962z"})],-1)]))}const nb=Pe(Qy,[["render",tb],["__file","VPEditIcon.vue"]]),rb=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{var o;return n.value.contributors??e.value.contributors??!0?((o=t.value.git)==null?void 0:o.contributors)??null:null})},ob=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{if(!(n.value.editLink??e.value.editLink??!0))return null;const{repo:o,docsRepo:s=o,docsBranch:i="main",docsDir:a="",editLinkText:l}=e.value;if(!s)return null;const u=k1({docsRepo:s,docsBranch:i,docsDir:a,filePathRelative:t.value.filePathRelative,editLinkPattern:n.value.editLinkPattern??e.value.editLinkPattern});return u?{text:l??"Edit this page",link:u}:null})},sb=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{var s;return!(n.value.lastUpdated??e.value.lastUpdated??!0)||!((s=t.value.git)!=null&&s.updatedTime)?null:new Date(t.value.git.updatedTime).toLocaleString()})},ib=he({__name:"VPPageMeta",setup(e,{expose:t}){t();const n=Be(),r=ob(),o=sb(),s=rb(),i={themeLocale:n,editLink:r,lastUpdated:o,contributors:s,VPEditIcon:nb,get AutoLink(){return tr}};return Object.defineProperty(i,"__isScriptSetup",{enumerable:!1,value:!0}),i}}),ab={class:"vp-page-meta"},lb={key:0,class:"vp-meta-item edit-link"},ub={class:"vp-meta-item git-info"},cb={key:0,class:"vp-meta-item last-updated"},fb={class:"meta-item-label"},db={class:"meta-item-info"},pb={key:1,class:"vp-meta-item contributors"},hb={class:"meta-item-label"},mb={class:"meta-item-info"},gb=["title"];function _b(e,t,n,r,o,s){const i=ii("ClientOnly");return Y(),ne("footer",ab,[r.editLink?(Y(),ne("div",lb,[ae(r.AutoLink,{class:"label",config:r.editLink},{before:Me(()=>[ae(r.VPEditIcon)]),_:1},8,["config"])])):He("",!0),ie("div",ub,[r.lastUpdated?(Y(),ne("div",cb,[ie("span",fb,Re(r.themeLocale.lastUpdatedText)+": ",1),ae(i,null,{default:Me(()=>[ie("span",db,Re(r.lastUpdated),1)]),_:1})])):He("",!0),r.contributors&&r.contributors.length?(Y(),ne("div",pb,[ie("span",hb,Re(r.themeLocale.contributorsText)+": ",1),ie("span",mb,[(Y(!0),ne(ge,null,nn(r.contributors,(a,l)=>(Y(),ne(ge,{key:l},[ie("span",{class:"contributor",title:`email: ${a.email}`},Re(a.name),9,gb),l!==r.contributors.length-1?(Y(),ne(ge,{key:0},[on(", ")],64)):He("",!0)],64))),128))])])):He("",!0)])])}const vb=Pe(ib,[["render",_b],["__file","VPPageMeta.vue"]]),yb=()=>{const e=wn(),t=Wt();return n=>{n&&(rf(n)?t.path!==n&&e.push(n):Kr(n)?window.open(n):e.push(encodeURI(n)))}},ru=(e,t)=>e===!1?!1:bt(e)?Gn(e,t):di(e)?{...e,link:Gn(e.link,t).link}:null,Ks=(e,t,n)=>{const r=e.findIndex(s=>s.link===t);if(r!==-1){const s=e[r+n];return s?s.link?s:"prefix"in s&&!Lr(s.prefix).notFound?{...s,link:s.prefix}:null:null}for(const s of e)if("children"in s){const i=Ks(s.children,t,n);if(i)return i}const o=e.findIndex(s=>"prefix"in s&&s.prefix===t);if(o!==-1){const s=e[o+n];return s?s.link?s:"prefix"in s&&!Lr(s.prefix).notFound?{...s,link:s.prefix}:null:null}return null},bb=()=>{const e=Ct(),t=Be(),n=Ni(),r=Wt(),o=F(()=>{const i=ru(e.value.prev,r.path);return i===!1?null:i??(t.value.prev===!1?null:Ks(n.value,r.path,-1))}),s=F(()=>{const i=ru(e.value.next,r.path);return i===!1?null:i??(t.value.next===!1?null:Ks(n.value,r.path,1))});return{prevLink:o,nextLink:s}},Eb=he({__name:"VPPageNav",setup(e,{expose:t}){t();const n=Be(),r=yb(),{prevLink:o,nextLink:s}=bb(),i=F(()=>n.value.pageNavbarLabel??"page navigation");dt("keydown",l=>{l.altKey&&(l.key==="ArrowRight"?s.value&&(r(s.value.link),l.preventDefault()):l.key==="ArrowLeft"&&o.value&&(r(o.value.link),l.preventDefault()))});const a={themeLocale:n,navigate:r,prevLink:o,nextLink:s,navbarLabel:i,get AutoLink(){return tr}};return Object.defineProperty(a,"__isScriptSetup",{enumerable:!1,value:!0}),a}}),Sb=["aria-label"],Ab={class:"hint"},Cb={class:"link"},Tb={class:"hint"},wb={class:"link"};function Ob(e,t,n,r,o,s){return r.prevLink||r.nextLink?(Y(),ne("nav",{key:0,class:"vp-page-nav","aria-label":r.navbarLabel},[r.prevLink?(Y(),De(r.AutoLink,{key:0,class:"prev",config:r.prevLink},{default:Me(()=>[ie("div",Ab,[t[0]||(t[0]=ie("span",{class:"arrow left"},null,-1)),on(" "+Re(r.themeLocale.prev??"Prev"),1)]),ie("div",Cb,[ie("span",null,Re(r.prevLink.text),1)])]),_:1},8,["config"])):He("",!0),r.nextLink?(Y(),De(r.AutoLink,{key:1,class:"next",config:r.nextLink},{default:Me(()=>[ie("div",Tb,[on(Re(r.themeLocale.next??"Next")+" ",1),t[1]||(t[1]=ie("span",{class:"arrow right"},null,-1))]),ie("div",wb,[ie("span",null,Re(r.nextLink.text),1)])]),_:1},8,["config"])):He("",!0)],8,Sb)):He("",!0)}const xb=Pe(Eb,[["render",Ob],["__file","VPPageNav.vue"]]),kb=he({__name:"VPPage",setup(e,{expose:t}){t(),P1();const n={VPPageMeta:vb,VPPageNav:xb,get Content(){return vi}};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),Pb={class:"vp-page"},Ib={class:"theme-default-content","vp-content":""};function Rb(e,t,n,r,o,s){return Y(),ne("main",Pb,[Le(e.$slots,"top"),ie("div",Ib,[Le(e.$slots,"content-top"),ae(r.Content),Le(e.$slots,"content-bottom")]),ae(r.VPPageMeta),ae(r.VPPageNav),Le(e.$slots,"bottom")])}const Lb=Pe(kb,[["render",Rb],["__file","VPPage.vue"]]),Db=he({__name:"VPSidebarItem",props:{item:{},depth:{default:0}},setup(e,{expose:t}){t();const n=e,{item:r,depth:o}=ku(n),s=Wt(),i=wn(),a=F(()=>"collapsible"in r.value&&r.value.collapsible),l=F(()=>jf(r.value,s)),u=F(()=>({"vp-sidebar-item":!0,"vp-sidebar-heading":o.value===0,active:l.value,collapsible:a.value})),c=F(()=>a.value?l.value:!0),[f,d]=Zc(c.value),p=b=>{a.value&&(b.preventDefault(),d())},_=i.afterEach(()=>{Jn(()=>{f.value=c.value})});si(()=>{_()});const v={props:n,item:r,depth:o,route:s,router:i,collapsible:a,isActive:l,itemClass:u,isOpenDefault:c,isOpen:f,toggleIsOpen:d,onClick:p,unregisterRouterHook:_,VPDropdownTransition:Yf,get AutoLink(){return tr}};return Object.defineProperty(v,"__isScriptSetup",{enumerable:!1,value:!0}),v}}),Vb={class:"vp-sidebar-children"};function Nb(e,t,n,r,o,s){const i=ii("VPSidebarItem",!0);return Y(),ne("li",null,[r.item.link?(Y(),De(r.AutoLink,{key:0,class:rt(r.itemClass),config:r.item},null,8,["class","config"])):(Y(),ne("p",{key:1,tabindex:"0",class:rt(r.itemClass),onClick:r.onClick,onKeydown:qh(r.onClick,["enter"])},[on(Re(r.item.text)+" ",1),r.collapsible?(Y(),ne("span",{key:0,class:rt(["arrow",r.isOpen?"down":"right"])},null,2)):He("",!0)],34)),"children"in r.item&&r.item.children.length?(Y(),De(r.VPDropdownTransition,{key:2},{default:Me(()=>[yo(ie("ul",Vb,[(Y(!0),ne(ge,null,nn(r.item.children,a=>(Y(),De(i,{key:`${r.depth}${a.text}${a.link}`,item:a,depth:r.depth+1},null,8,["item","depth"]))),128))],512),[[Ao,r.isOpen]])]),_:1})):He("",!0)])}const Mb=Pe(Db,[["render",Nb],["__file","VPSidebarItem.vue"]]),Fb=he({__name:"VPSidebarItems",setup(e,{expose:t}){t();const n=Wt(),r=Ni();Qe(()=>{je(()=>n.hash,s=>{const i=document.querySelector(".vp-sidebar");if(!i)return;const a=document.querySelector(`.vp-sidebar a.vp-sidebar-item[href="${n.path}${s}"]`);if(!a)return;const{top:l,height:u}=i.getBoundingClientRect(),{top:c,height:f}=a.getBoundingClientRect();cl+u&&a.scrollIntoView(!1)})});const o={route:n,sidebarItems:r,VPSidebarItem:Mb};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}}),Hb={key:0,class:"vp-sidebar-items"};function Bb(e,t,n,r,o,s){return r.sidebarItems.length?(Y(),ne("ul",Hb,[(Y(!0),ne(ge,null,nn(r.sidebarItems,i=>(Y(),De(r.VPSidebarItem,{key:`${i.text}${i.link}`,item:i},null,8,["item"]))),128))])):He("",!0)}const $b=Pe(Fb,[["render",Bb],["__file","VPSidebarItems.vue"]]),Ub=he({__name:"VPSidebar",setup(e,{expose:t}){t();const n={VPNavbarItems:Jf,VPSidebarItems:$b};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),jb={class:"vp-sidebar","vp-sidebar":""};function zb(e,t,n,r,o,s){return Y(),ne("aside",jb,[ae(r.VPNavbarItems),Le(e.$slots,"top"),ae(r.VPSidebarItems),Le(e.$slots,"bottom")])}const Kb=Pe(Ub,[["render",zb],["__file","VPSidebar.vue"]]),Wb=he({__name:"Layout",setup(e,{expose:t}){t();const n=er(),r=Ct(),o=Be(),s=F(()=>r.value.navbar!==!1&&o.value.navbar!==!1),i=Ni(),a=ve(!1),l=m=>{a.value=typeof m=="boolean"?m:!a.value},u={x:0,y:0},c=m=>{u.x=m.changedTouches[0].clientX,u.y=m.changedTouches[0].clientY},f=m=>{const y=m.changedTouches[0].clientX-u.x,L=m.changedTouches[0].clientY-u.y;Math.abs(y)>Math.abs(L)&&Math.abs(y)>40&&(y>0&&u.x<=80?l(!0):l(!1))},d=F(()=>r.value.externalLinkIcon??o.value.externalLinkIcon??!0),p=F(()=>[{"no-navbar":!s.value,"no-sidebar":!i.value.length,"sidebar-open":a.value,"external-link-icon":d.value},r.value.pageClass]);let _;Qe(()=>{_=wn().afterEach(()=>{l(!1)})}),Lo(()=>{_()});const v=Uf(),b=v.resolve,E=v.pending,A={page:n,frontmatter:r,themeLocale:o,shouldShowNavbar:s,sidebarItems:i,isSidebarOpen:a,toggleSidebar:l,touchStart:u,onTouchStart:c,onTouchEnd:f,enableExternalLinkIcon:d,containerClass:p,get unregisterRouterHook(){return _},set unregisterRouterHook(m){_=m},scrollPromise:v,onBeforeEnter:b,onBeforeLeave:E,VPHome:sy,VPNavbar:Jy,VPPage:Lb,VPSidebar:Kb};return Object.defineProperty(A,"__isScriptSetup",{enumerable:!1,value:!0}),A}});function qb(e,t,n,r,o,s){return Y(),ne("div",{class:rt(["vp-theme-container",r.containerClass]),"vp-container":"",onTouchstart:r.onTouchStart,onTouchend:r.onTouchEnd},[Le(e.$slots,"navbar",{},()=>[r.shouldShowNavbar?(Y(),De(r.VPNavbar,{key:0,onToggleSidebar:r.toggleSidebar},{before:Me(()=>[Le(e.$slots,"navbar-before")]),after:Me(()=>[Le(e.$slots,"navbar-after")]),_:3})):He("",!0)]),ie("div",{class:"vp-sidebar-mask",onClick:t[0]||(t[0]=i=>r.toggleSidebar(!1))}),Le(e.$slots,"sidebar",{},()=>[ae(r.VPSidebar,null,{top:Me(()=>[Le(e.$slots,"sidebar-top")]),bottom:Me(()=>[Le(e.$slots,"sidebar-bottom")]),_:3})]),Le(e.$slots,"page",{},()=>[r.frontmatter.home?(Y(),De(r.VPHome,{key:0})):(Y(),De(fi,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:r.onBeforeEnter,onBeforeLeave:r.onBeforeLeave},{default:Me(()=>[(Y(),De(r.VPPage,{key:r.page.path},{top:Me(()=>[Le(e.$slots,"page-top")]),"content-top":Me(()=>[Le(e.$slots,"page-content-top")]),"content-bottom":Me(()=>[Le(e.$slots,"page-content-bottom")]),bottom:Me(()=>[Le(e.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34)}const Gb=Pe(Wb,[["render",qb],["__file","Layout.vue"]]),Yb=he({__name:"NotFound",setup(e,{expose:t}){t();const n=Wr(),r=Be(),o=r.value.notFound??["Not Found"],s=()=>o[Math.floor(Math.random()*o.length)],i=r.value.home??n.value,a=r.value.backToHome??"Back to home",l={routeLocale:n,themeLocale:r,messages:o,getMsg:s,homeLink:i,homeText:a,get RouteLink(){return Ho}};return Object.defineProperty(l,"__isScriptSetup",{enumerable:!1,value:!0}),l}}),Xb={class:"vp-theme-container","vp-container":""},Zb={class:"page"},Jb={class:"theme-default-content","vp-content":""};function Qb(e,t,n,r,o,s){return Y(),ne("div",Xb,[ie("main",Zb,[ie("div",Jb,[t[0]||(t[0]=ie("h1",null,"404",-1)),ie("blockquote",null,Re(r.getMsg()),1),ae(r.RouteLink,{to:r.homeLink},{default:Me(()=>[on(Re(r.homeText),1)]),_:1},8,["to"])])])])}const eE=Pe(Yb,[["render",Qb],["__scopeId","data-v-67c08c1d"],["__file","NotFound.vue"]]),tE=un({enhance({app:e,router:t}){Qc("Badge")||e.component("Badge",F1);const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Uf().wait(),n(...r))},setup(){C1(),V1()},layouts:{Layout:Gb,NotFound:eE}}),nE=Object.freeze(Object.defineProperty({__proto__:null,default:tE},Symbol.toStringTag,{value:"Module"})),io=[__,O_,D_,N_,Y_,tv,ov,cv,E1,nE].map(e=>e.default).filter(Boolean),rE=JSON.parse('{"base":"/A-Philosophy-of-Software-Design-zh/","lang":"en-US","title":"","description":"","head":[],"locales":{"/":{"lang":"zh-CN","title":"《软件设计的哲学》","description":"《软件设计的哲学》中文翻译"}}}');var mr=Et(rE),oE=Xm,sE=()=>{const e=Ig({history:oE(Cc("/A-Philosophy-of-Software-Design-zh/")),routes:[{name:"vuepress-route",path:"/:catchAll(.*)",components:{}}],scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{if(t.path!==n.path||n===Nt){const r=Lr(t.fullPath);if(r.path!==t.fullPath)return r.path;const o=await r.loader();t.meta={...r.meta,_pageChunk:o}}else t.path===n.path&&(t.meta=n.meta)}),e},iE=e=>{e.component("ClientOnly",_i),e.component("Content",vi),e.component("RouteLink",Ho)},aE=(e,t,n)=>{const r=F(()=>t.currentRoute.value.path),o=zd((b,E)=>({get(){return b(),t.currentRoute.value.meta._pageChunk},set(A){t.currentRoute.value.meta._pageChunk=A,E()}})),s=F(()=>hn.resolveLayouts(n)),i=F(()=>hn.resolveRouteLocale(mr.value.locales,r.value)),a=F(()=>hn.resolveSiteLocaleData(mr.value,i.value)),l=F(()=>o.value.comp),u=F(()=>o.value.data),c=F(()=>u.value.frontmatter),f=F(()=>hn.resolvePageHeadTitle(u.value,a.value)),d=F(()=>hn.resolvePageHead(f.value,c.value,a.value)),p=F(()=>hn.resolvePageLang(u.value,a.value)),_=F(()=>hn.resolvePageLayout(u.value,s.value)),v={layouts:s,pageData:u,pageComponent:l,pageFrontmatter:c,pageHead:d,pageHeadTitle:f,pageLang:p,pageLayout:_,redirects:Ds,routeLocale:i,routePath:r,routes:Hn,siteData:mr,siteLocaleData:a};return e.provide(mi,v),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>c.value},$head:{get:()=>d.value},$headTitle:{get:()=>f.value},$lang:{get:()=>p.value},$page:{get:()=>u.value},$routeLocale:{get:()=>i.value},$site:{get:()=>mr.value},$siteLocale:{get:()=>a.value},$withBase:{get:()=>yi}}),v},lE=([e,t,n=""])=>{const r=Object.entries(t).map(([a,l])=>bt(l)?`[${a}=${JSON.stringify(l)}]`:l?`[${a}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(a=>a.innerText===n)??null},uE=([e,t,n])=>{if(!bt(e))return null;const r=document.createElement(e);return di(t)&&Object.entries(t).forEach(([o,s])=>{bt(s)?r.setAttribute(o,s):s&&r.setAttribute(o,"")}),bt(n)&&r.appendChild(document.createTextNode(n)),r},cE=()=>{const e=Dg(),t=Vg();let n=[];const r=()=>{e.value.forEach(i=>{const a=lE(i);a&&n.push(a)})},o=()=>{const i=[];return e.value.forEach(a=>{const l=uE(a);l&&i.push(l)}),i},s=()=>{document.documentElement.lang=t.value;const i=o();n.forEach((a,l)=>{const u=i.findIndex(c=>a.isEqualNode(c));u===-1?(a.remove(),delete n[l]):i.splice(u,1)}),i.forEach(a=>document.head.appendChild(a)),n=[...n.filter(a=>!!a),...i]};An(Fg,s),Qe(()=>{r(),je(e,s,{immediate:!1})})},fE=Xh,dE=async()=>{var r;const e=fE({name:"Vuepress",setup(){var i;cE();for(const a of io)(i=a.setup)==null||i.call(a);const o=io.flatMap(({rootComponents:a=[]})=>a.map(l=>_e(l))),s=Ng();return()=>[_e(s.value),o]}}),t=sE();iE(e);const n=aE(e,t,io);{const{setupDevtools:o}=await Ce(async()=>{const{setupDevtools:s}=await import("./setupDevtools-7MC2TMWH-DY3SRJV5.js");return{setupDevtools:s}},[]);o(e,n)}for(const o of io)await((r=o.enhance)==null?void 0:r.call(o,{app:e,router:t,siteData:mr}));return e.use(t),{app:e,router:t}};dE().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Pe as _,ie as a,ae as b,ne as c,dE as createVueApp,Me as d,on as e,ch as f,Y as o,ii as r,L0 as s,je as w}; + */const Xa=(e,t)=>{e.classList.add(t)},Za=(e,t)=>{e.classList.remove(t)},X_=e=>{var t;(t=e==null?void 0:e.parentNode)==null||t.removeChild(e)},fs=(e,t,n)=>en?n:e,Ja=e=>(-1+e)*100,Z_=(()=>{const e=[],t=()=>{const n=e.shift();n&&n(t)};return n=>{e.push(n),e.length===1&&t()}})(),J_=e=>e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(t,n)=>n.toUpperCase()),ro=(()=>{const e=["Webkit","O","Moz","ms"],t={},n=s=>{const{style:i}=document.body;if(s in i)return s;const a=s.charAt(0).toUpperCase()+s.slice(1);let l=e.length;for(;l--;){const u=`${e[l]}${a}`;if(u in i)return u}return s},r=s=>{const i=J_(s);return t[i]??(t[i]=n(i))},o=(s,i,a)=>{s.style[r(i)]=a};return(s,i)=>{for(const a in i){const l=i[a];Object.hasOwn(i,a)&&S_(l)&&o(s,a,l)}}})(),Vt={minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},Ie={percent:null,isRendered:()=>!!document.getElementById("nprogress"),set:e=>{const{speed:t,easing:n}=Vt,r=Ie.isStarted(),o=fs(e,Vt.minimum,1);Ie.percent=o===1?null:o;const s=Ie.render(!r),i=s.querySelector(Vt.barSelector);return s.offsetWidth,Z_(a=>{ro(i,{transform:`translate3d(${Ja(o)}%,0,0)`,transition:`all ${t}ms ${n}`}),o===1?(ro(s,{transition:"none",opacity:"1"}),s.offsetWidth,setTimeout(()=>{ro(s,{transition:`all ${t}ms linear`,opacity:"0"}),setTimeout(()=>{Ie.remove(),a()},t)},t)):setTimeout(()=>{a()},t)}),Ie},isStarted:()=>typeof Ie.percent=="number",start:()=>{Ie.percent||Ie.set(0);const e=()=>{setTimeout(()=>{Ie.percent&&(Ie.trickle(),e())},Vt.trickleSpeed)};return e(),Ie},done:e=>!e&&!Ie.percent?Ie:Ie.increase(.3+.5*Math.random()).set(1),increase:e=>{let{percent:t}=Ie;return t?(t=fs(t+(typeof e=="number"?e:(1-t)*fs(Math.random()*t,.1,.95)),0,.994),Ie.set(t)):Ie.start()},trickle:()=>Ie.increase(Math.random()*Vt.trickleRate),render:e=>{if(Ie.isRendered())return document.getElementById("nprogress");Xa(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=Vt.template;const n=t.querySelector(Vt.barSelector),r=document.querySelector(Vt.parent),o=e?"-100":Ja(Ie.percent??0);return ro(n,{transition:"all 0 linear",transform:`translate3d(${o}%,0,0)`}),r&&(r!==document.body&&Xa(r,"nprogress-custom-parent"),r.appendChild(t)),t},remove:()=>{Za(document.documentElement,"nprogress-busy"),Za(document.querySelector(Vt.parent),"nprogress-custom-parent"),X_(document.getElementById("nprogress"))}},Q_=()=>{Qe(()=>{const e=wn(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||Ie.start()}),e.afterEach(n=>{t.add(n.path),Ie.done()})})},ev=un({setup(){Q_()}}),tv=Object.freeze(Object.defineProperty({__proto__:null,default:ev},Symbol.toStringTag,{value:"Module"})),nv=({selector:e='div[class*="language-"].has-collapsed-lines > .collapsed-lines'}={})=>{dt("click",t=>{const n=t.target;if(n.matches(e)){const r=n.parentElement;r!=null&&r.classList.toggle("collapsed")&&r.scrollIntoView({block:"center",behavior:"instant"})}})},rv={setup(){nv()}},ov=Object.freeze(Object.defineProperty({__proto__:null,default:rv},Symbol.toStringTag,{value:"Module"})),sv="VUEPRESS_CODE_TAB_STORE",oo=Ai(sv,{}),iv=he({name:"CodeTabs",props:{active:{type:Number,default:0},data:{type:Array,required:!0},id:{type:String,required:!0},tabId:{type:String,default:""}},slots:Object,setup(e,{slots:t}){const n=ve(e.active),r=Et([]),o=()=>{e.tabId&&(oo.value[e.tabId]=e.data[n.value].id)},s=(u=n.value)=>{n.value=u{n.value=u>0?u-1:r.value.length-1,r.value[n.value].focus()},a=(u,c)=>{u.key===" "||u.key==="Enter"?(u.preventDefault(),n.value=c):u.key==="ArrowRight"?(u.preventDefault(),s()):u.key==="ArrowLeft"&&(u.preventDefault(),i()),e.tabId&&(oo.value[e.tabId]=e.data[n.value].id)},l=()=>{if(e.tabId){const u=e.data.findIndex(({id:c})=>oo.value[e.tabId]===c);if(u!==-1)return u}return e.active};return Qe(()=>{n.value=l(),je(()=>oo.value[e.tabId],(u,c)=>{if(e.tabId&&u!==c){const f=e.data.findIndex(({id:d})=>d===u);f!==-1&&(n.value=f)}})}),()=>e.data.length?_e("div",{class:"vp-code-tabs"},[_e("div",{class:"vp-code-tabs-nav",role:"tablist"},e.data.map(({id:u},c)=>{const f=c===n.value;return _e("button",{type:"button",ref:d=>{d&&(r.value[c]=d)},class:["vp-code-tab-nav",{active:f}],role:"tab","aria-controls":`codetab-${e.id}-${c}`,"aria-selected":f,onClick:()=>{n.value=c,o()},onKeydown:d=>{a(d,c)}},t[`title${c}`]({value:u,isActive:f}))})),e.data.map(({id:u},c)=>{const f=c===n.value;return _e("div",{class:["vp-code-tab",{active:f}],id:`codetab-${e.id}-${c}`,role:"tabpanel","aria-expanded":f},[_e("div",{class:"vp-code-tab-title"},t[`title${c}`]({value:u,isActive:f})),t[`tab${c}`]({value:u,isActive:f})])})]):null}}),av="VUEPRESS_TAB_STORE",ds=Ai(av,{}),lv=he({name:"Tabs",props:{active:{type:Number,default:0},data:{type:Array,required:!0},id:{type:String,required:!0},tabId:{type:String,default:""}},slots:Object,setup(e,{slots:t}){const n=ve(e.active),r=Et([]),o=()=>{e.tabId&&(ds.value[e.tabId]=e.data[n.value].id)},s=(u=n.value)=>{n.value=u{n.value=u>0?u-1:r.value.length-1,r.value[n.value].focus()},a=(u,c)=>{u.key===" "||u.key==="Enter"?(u.preventDefault(),n.value=c):u.key==="ArrowRight"?(u.preventDefault(),s()):u.key==="ArrowLeft"&&(u.preventDefault(),i()),o()},l=()=>{if(e.tabId){const u=e.data.findIndex(({id:c})=>ds.value[e.tabId]===c);if(u!==-1)return u}return e.active};return Qe(()=>{n.value=l(),je(()=>ds.value[e.tabId],(u,c)=>{if(e.tabId&&u!==c){const f=e.data.findIndex(({id:d})=>d===u);f!==-1&&(n.value=f)}})}),()=>e.data.length?_e("div",{class:"vp-tabs"},[_e("div",{class:"vp-tabs-nav",role:"tablist"},e.data.map(({id:u},c)=>{const f=c===n.value;return _e("button",{type:"button",ref:d=>{d&&(r.value[c]=d)},class:["vp-tab-nav",{active:f}],role:"tab","aria-controls":`tab-${e.id}-${c}`,"aria-selected":f,onClick:()=>{n.value=c,o()},onKeydown:d=>{a(d,c)}},t[`title${c}`]({value:u,isActive:f}))})),e.data.map(({id:u},c)=>{const f=c===n.value;return _e("div",{class:["vp-tab",{active:f}],id:`tab-${e.id}-${c}`,role:"tabpanel","aria-expanded":f},[_e("div",{class:"vp-tab-title"},t[`title${c}`]({value:u,isActive:f})),t[`tab${c}`]({value:u,isActive:f})])})]):null}}),uv={enhance:({app:e})=>{e.component("CodeTabs",iv),e.component("Tabs",lv)}},cv=Object.freeze(Object.defineProperty({__proto__:null,default:uv},Symbol.toStringTag,{value:"Module"}));var fv=Object.create,of=Object.defineProperty,dv=Object.getOwnPropertyDescriptor,Ci=Object.getOwnPropertyNames,pv=Object.getPrototypeOf,hv=Object.prototype.hasOwnProperty,mv=(e,t)=>function(){return e&&(t=(0,e[Ci(e)[0]])(e=0)),t},gv=(e,t)=>function(){return t||(0,e[Ci(e)[0]])((t={exports:{}}).exports,t),t.exports},_v=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ci(t))!hv.call(e,o)&&o!==n&&of(e,o,{get:()=>t[o],enumerable:!(r=dv(t,o))||r.enumerable});return e},vv=(e,t,n)=>(n=e!=null?fv(pv(e)):{},_v(of(n,"default",{value:e,enumerable:!0}),e)),qr=mv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),yv=gv({"../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(e,t){qr(),t.exports=r;function n(s){return s instanceof Buffer?Buffer.from(s):new s.constructor(s.buffer.slice(),s.byteOffset,s.length)}function r(s){if(s=s||{},s.circles)return o(s);const i=new Map;if(i.set(Date,f=>new Date(f)),i.set(Map,(f,d)=>new Map(l(Array.from(f),d))),i.set(Set,(f,d)=>new Set(l(Array.from(f),d))),s.constructorHandlers)for(const f of s.constructorHandlers)i.set(f[0],f[1]);let a=null;return s.proto?c:u;function l(f,d){const p=Object.keys(f),_=new Array(p.length);for(let v=0;vnew Date(p)),l.set(Map,(p,_)=>new Map(c(Array.from(p),_))),l.set(Set,(p,_)=>new Set(c(Array.from(p),_))),s.constructorHandlers)for(const p of s.constructorHandlers)l.set(p[0],p[1]);let u=null;return s.proto?d:f;function c(p,_){const v=Object.keys(p),b=new Array(v.length);for(let E=0;E(i=wv(e,u,c),i.finally(()=>{if(i=null,n.trailing&&a&&!o){const f=l(u,a);return a=null,f}}),i);return function(...u){return i?(n.trailing&&(a=u),i):new Promise(c=>{const f=!o&&n.leading;clearTimeout(o),o=setTimeout(()=>{o=null;const d=n.leading?r:l(this,u);for(const p of s)p(d);s=[]},t),f?(r=l(this,u),c(r)):s.push(c)})}}async function wv(e,t,n){return await e.apply(t,n)}function Ns(e,t={},n){for(const r in e){const o=e[r],s=n?`${n}:${r}`:r;typeof o=="object"&&o!==null?Ns(o,t,s):typeof o=="function"&&(t[s]=o)}return t}const Ov={run:e=>e()},xv=()=>Ov,af=typeof console.createTask<"u"?console.createTask:xv;function kv(e,t){const n=t.shift(),r=af(n);return e.reduce((o,s)=>o.then(()=>r.run(()=>s(...t))),Promise.resolve())}function Pv(e,t){const n=t.shift(),r=af(n);return Promise.all(e.map(o=>r.run(()=>o(...t))))}function ps(e,t){for(const n of[...e])n(t)}class Iv{constructor(){this._hooks={},this._before=void 0,this._after=void 0,this._deprecatedMessages=void 0,this._deprecatedHooks={},this.hook=this.hook.bind(this),this.callHook=this.callHook.bind(this),this.callHookWith=this.callHookWith.bind(this)}hook(t,n,r={}){if(!t||typeof n!="function")return()=>{};const o=t;let s;for(;this._deprecatedHooks[t];)s=this._deprecatedHooks[t],t=s.to;if(s&&!r.allowDeprecated){let i=s.message;i||(i=`${o} hook has been deprecated`+(s.to?`, please use ${s.to}`:"")),this._deprecatedMessages||(this._deprecatedMessages=new Set),this._deprecatedMessages.has(i)||(console.warn(i),this._deprecatedMessages.add(i))}if(!n.name)try{Object.defineProperty(n,"name",{get:()=>"_"+t.replace(/\W+/g,"_")+"_hook_cb",configurable:!0})}catch{}return this._hooks[t]=this._hooks[t]||[],this._hooks[t].push(n),()=>{n&&(this.removeHook(t,n),n=void 0)}}hookOnce(t,n){let r,o=(...s)=>(typeof r=="function"&&r(),r=void 0,o=void 0,n(...s));return r=this.hook(t,o),r}removeHook(t,n){if(this._hooks[t]){const r=this._hooks[t].indexOf(n);r!==-1&&this._hooks[t].splice(r,1),this._hooks[t].length===0&&delete this._hooks[t]}}deprecateHook(t,n){this._deprecatedHooks[t]=typeof n=="string"?{to:n}:n;const r=this._hooks[t]||[];delete this._hooks[t];for(const o of r)this.hook(t,o)}deprecateHooks(t){Object.assign(this._deprecatedHooks,t);for(const n in t)this.deprecateHook(n,t[n])}addHooks(t){const n=Ns(t),r=Object.keys(n).map(o=>this.hook(o,n[o]));return()=>{for(const o of r.splice(0,r.length))o()}}removeHooks(t){const n=Ns(t);for(const r in n)this.removeHook(r,n[r])}removeAllHooks(){for(const t in this._hooks)delete this._hooks[t]}callHook(t,...n){return n.unshift(t),this.callHookWith(kv,t,...n)}callHookParallel(t,...n){return n.unshift(t),this.callHookWith(Pv,t,...n)}callHookWith(t,n,...r){const o=this._before||this._after?{name:n,args:r,context:{}}:void 0;this._before&&ps(this._before,o);const s=t(n in this._hooks?[...this._hooks[n]]:[],r);return s instanceof Promise?s.finally(()=>{this._after&&o&&ps(this._after,o)}):(this._after&&o&&ps(this._after,o),s)}beforeEach(t){return this._before=this._before||[],this._before.push(t),()=>{if(this._before!==void 0){const n=this._before.indexOf(t);n!==-1&&this._before.splice(n,1)}}}afterEach(t){return this._after=this._after||[],this._after.push(t),()=>{if(this._after!==void 0){const n=this._after.indexOf(t);n!==-1&&this._after.splice(n,1)}}}}function lf(){return new Iv}var Rv=Object.create,uf=Object.defineProperty,Lv=Object.getOwnPropertyDescriptor,Ti=Object.getOwnPropertyNames,Dv=Object.getPrototypeOf,Vv=Object.prototype.hasOwnProperty,Nv=(e,t)=>function(){return e&&(t=(0,e[Ti(e)[0]])(e=0)),t},cf=(e,t)=>function(){return t||(0,e[Ti(e)[0]])((t={exports:{}}).exports,t),t.exports},Mv=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Ti(t))!Vv.call(e,o)&&o!==n&&uf(e,o,{get:()=>t[o],enumerable:!(r=Lv(t,o))||r.enumerable});return e},Fv=(e,t,n)=>(n=e!=null?Rv(Dv(e)):{},Mv(uf(n,"default",{value:e,enumerable:!0}),e)),P=Nv({"../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.0_@types+node@22.10.1__@swc+core@1.5.29_jiti@2.0.0_p_swvvp2d4pgn6xuiiec4l4x2i7a/node_modules/tsup/assets/esm_shims.js"(){}}),Hv=cf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js"(e,t){P(),function(n){var r={À:"A",Á:"A",Â:"A",Ã:"A",Ä:"Ae",Å:"A",Æ:"AE",Ç:"C",È:"E",É:"E",Ê:"E",Ë:"E",Ì:"I",Í:"I",Î:"I",Ï:"I",Ð:"D",Ñ:"N",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"Oe",Ő:"O",Ø:"O",Ù:"U",Ú:"U",Û:"U",Ü:"Ue",Ű:"U",Ý:"Y",Þ:"TH",ß:"ss",à:"a",á:"a",â:"a",ã:"a",ä:"ae",å:"a",æ:"ae",ç:"c",è:"e",é:"e",ê:"e",ë:"e",ì:"i",í:"i",î:"i",ï:"i",ð:"d",ñ:"n",ò:"o",ó:"o",ô:"o",õ:"o",ö:"oe",ő:"o",ø:"o",ù:"u",ú:"u",û:"u",ü:"ue",ű:"u",ý:"y",þ:"th",ÿ:"y","ẞ":"SS",ا:"a",أ:"a",إ:"i",آ:"aa",ؤ:"u",ئ:"e",ء:"a",ب:"b",ت:"t",ث:"th",ج:"j",ح:"h",خ:"kh",د:"d",ذ:"th",ر:"r",ز:"z",س:"s",ش:"sh",ص:"s",ض:"dh",ط:"t",ظ:"z",ع:"a",غ:"gh",ف:"f",ق:"q",ك:"k",ل:"l",م:"m",ن:"n",ه:"h",و:"w",ي:"y",ى:"a",ة:"h",ﻻ:"la",ﻷ:"laa",ﻹ:"lai",ﻵ:"laa",گ:"g",چ:"ch",پ:"p",ژ:"zh",ک:"k",ی:"y","َ":"a","ً":"an","ِ":"e","ٍ":"en","ُ":"u","ٌ":"on","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9",က:"k",ခ:"kh",ဂ:"g",ဃ:"ga",င:"ng",စ:"s",ဆ:"sa",ဇ:"z","စျ":"za",ည:"ny",ဋ:"t",ဌ:"ta",ဍ:"d",ဎ:"da",ဏ:"na",တ:"t",ထ:"ta",ဒ:"d",ဓ:"da",န:"n",ပ:"p",ဖ:"pa",ဗ:"b",ဘ:"ba",မ:"m",ယ:"y",ရ:"ya",လ:"l",ဝ:"w",သ:"th",ဟ:"h",ဠ:"la",အ:"a","ြ":"y","ျ":"ya","ွ":"w","ြွ":"yw","ျွ":"ywa","ှ":"h",ဧ:"e","၏":"-e",ဣ:"i",ဤ:"-i",ဉ:"u",ဦ:"-u",ဩ:"aw","သြော":"aw",ဪ:"aw","၀":"0","၁":"1","၂":"2","၃":"3","၄":"4","၅":"5","၆":"6","၇":"7","၈":"8","၉":"9","္":"","့":"","း":"",č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z",ހ:"h",ށ:"sh",ނ:"n",ރ:"r",ބ:"b",ޅ:"lh",ކ:"k",އ:"a",ވ:"v",މ:"m",ފ:"f",ދ:"dh",ތ:"th",ލ:"l",ގ:"g",ޏ:"gn",ސ:"s",ޑ:"d",ޒ:"z",ޓ:"t",ޔ:"y",ޕ:"p",ޖ:"j",ޗ:"ch",ޘ:"tt",ޙ:"hh",ޚ:"kh",ޛ:"th",ޜ:"z",ޝ:"sh",ޞ:"s",ޟ:"d",ޠ:"t",ޡ:"z",ޢ:"a",ޣ:"gh",ޤ:"q",ޥ:"w","ަ":"a","ާ":"aa","ި":"i","ީ":"ee","ު":"u","ޫ":"oo","ެ":"e","ޭ":"ey","ޮ":"o","ޯ":"oa","ް":"",ა:"a",ბ:"b",გ:"g",დ:"d",ე:"e",ვ:"v",ზ:"z",თ:"t",ი:"i",კ:"k",ლ:"l",მ:"m",ნ:"n",ო:"o",პ:"p",ჟ:"zh",რ:"r",ს:"s",ტ:"t",უ:"u",ფ:"p",ქ:"k",ღ:"gh",ყ:"q",შ:"sh",ჩ:"ch",ც:"ts",ძ:"dz",წ:"ts",ჭ:"ch",ხ:"kh",ჯ:"j",ჰ:"h",α:"a",β:"v",γ:"g",δ:"d",ε:"e",ζ:"z",η:"i",θ:"th",ι:"i",κ:"k",λ:"l",μ:"m",ν:"n",ξ:"ks",ο:"o",π:"p",ρ:"r",σ:"s",τ:"t",υ:"y",φ:"f",χ:"x",ψ:"ps",ω:"o",ά:"a",έ:"e",ί:"i",ό:"o",ύ:"y",ή:"i",ώ:"o",ς:"s",ϊ:"i",ΰ:"y",ϋ:"y",ΐ:"i",Α:"A",Β:"B",Γ:"G",Δ:"D",Ε:"E",Ζ:"Z",Η:"I",Θ:"TH",Ι:"I",Κ:"K",Λ:"L",Μ:"M",Ν:"N",Ξ:"KS",Ο:"O",Π:"P",Ρ:"R",Σ:"S",Τ:"T",Υ:"Y",Φ:"F",Χ:"X",Ψ:"PS",Ω:"O",Ά:"A",Έ:"E",Ί:"I",Ό:"O",Ύ:"Y",Ή:"I",Ώ:"O",Ϊ:"I",Ϋ:"Y",ā:"a",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",ū:"u",Ā:"A",Ē:"E",Ģ:"G",Ī:"I",Ķ:"k",Ļ:"L",Ņ:"N",Ū:"U",Ќ:"Kj",ќ:"kj",Љ:"Lj",љ:"lj",Њ:"Nj",њ:"nj",Тс:"Ts",тс:"ts",ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"E",Ł:"L",Ń:"N",Ś:"S",Ź:"Z",Ż:"Z",Є:"Ye",І:"I",Ї:"Yi",Ґ:"G",є:"ye",і:"i",ї:"yi",ґ:"g",ă:"a",Ă:"A",ș:"s",Ș:"S",ț:"t",Ț:"T",ţ:"t",Ţ:"T",а:"a",б:"b",в:"v",г:"g",д:"d",е:"e",ё:"yo",ж:"zh",з:"z",и:"i",й:"i",к:"k",л:"l",м:"m",н:"n",о:"o",п:"p",р:"r",с:"s",т:"t",у:"u",ф:"f",х:"kh",ц:"c",ч:"ch",ш:"sh",щ:"sh",ъ:"",ы:"y",ь:"",э:"e",ю:"yu",я:"ya",А:"A",Б:"B",В:"V",Г:"G",Д:"D",Е:"E",Ё:"Yo",Ж:"Zh",З:"Z",И:"I",Й:"I",К:"K",Л:"L",М:"M",Н:"N",О:"O",П:"P",Р:"R",С:"S",Т:"T",У:"U",Ф:"F",Х:"Kh",Ц:"C",Ч:"Ch",Ш:"Sh",Щ:"Sh",Ъ:"",Ы:"Y",Ь:"",Э:"E",Ю:"Yu",Я:"Ya",ђ:"dj",ј:"j",ћ:"c",џ:"dz",Ђ:"Dj",Ј:"j",Ћ:"C",Џ:"Dz",ľ:"l",ĺ:"l",ŕ:"r",Ľ:"L",Ĺ:"L",Ŕ:"R",ş:"s",Ş:"S",ı:"i",İ:"I",ğ:"g",Ğ:"G",ả:"a",Ả:"A",ẳ:"a",Ẳ:"A",ẩ:"a",Ẩ:"A",đ:"d",Đ:"D",ẹ:"e",Ẹ:"E",ẽ:"e",Ẽ:"E",ẻ:"e",Ẻ:"E",ế:"e",Ế:"E",ề:"e",Ề:"E",ệ:"e",Ệ:"E",ễ:"e",Ễ:"E",ể:"e",Ể:"E",ỏ:"o",ọ:"o",Ọ:"o",ố:"o",Ố:"O",ồ:"o",Ồ:"O",ổ:"o",Ổ:"O",ộ:"o",Ộ:"O",ỗ:"o",Ỗ:"O",ơ:"o",Ơ:"O",ớ:"o",Ớ:"O",ờ:"o",Ờ:"O",ợ:"o",Ợ:"O",ỡ:"o",Ỡ:"O",Ở:"o",ở:"o",ị:"i",Ị:"I",ĩ:"i",Ĩ:"I",ỉ:"i",Ỉ:"i",ủ:"u",Ủ:"U",ụ:"u",Ụ:"U",ũ:"u",Ũ:"U",ư:"u",Ư:"U",ứ:"u",Ứ:"U",ừ:"u",Ừ:"U",ự:"u",Ự:"U",ữ:"u",Ữ:"U",ử:"u",Ử:"ư",ỷ:"y",Ỷ:"y",ỳ:"y",Ỳ:"Y",ỵ:"y",Ỵ:"Y",ỹ:"y",Ỹ:"Y",ạ:"a",Ạ:"A",ấ:"a",Ấ:"A",ầ:"a",Ầ:"A",ậ:"a",Ậ:"A",ẫ:"a",Ẫ:"A",ắ:"a",Ắ:"A",ằ:"a",Ằ:"A",ặ:"a",Ặ:"A",ẵ:"a",Ẵ:"A","⓪":"0","①":"1","②":"2","③":"3","④":"4","⑤":"5","⑥":"6","⑦":"7","⑧":"8","⑨":"9","⑩":"10","⑪":"11","⑫":"12","⑬":"13","⑭":"14","⑮":"15","⑯":"16","⑰":"17","⑱":"18","⑲":"18","⑳":"18","⓵":"1","⓶":"2","⓷":"3","⓸":"4","⓹":"5","⓺":"6","⓻":"7","⓼":"8","⓽":"9","⓾":"10","⓿":"0","⓫":"11","⓬":"12","⓭":"13","⓮":"14","⓯":"15","⓰":"16","⓱":"17","⓲":"18","⓳":"19","⓴":"20","Ⓐ":"A","Ⓑ":"B","Ⓒ":"C","Ⓓ":"D","Ⓔ":"E","Ⓕ":"F","Ⓖ":"G","Ⓗ":"H","Ⓘ":"I","Ⓙ":"J","Ⓚ":"K","Ⓛ":"L","Ⓜ":"M","Ⓝ":"N","Ⓞ":"O","Ⓟ":"P","Ⓠ":"Q","Ⓡ":"R","Ⓢ":"S","Ⓣ":"T","Ⓤ":"U","Ⓥ":"V","Ⓦ":"W","Ⓧ":"X","Ⓨ":"Y","Ⓩ":"Z","ⓐ":"a","ⓑ":"b","ⓒ":"c","ⓓ":"d","ⓔ":"e","ⓕ":"f","ⓖ":"g","ⓗ":"h","ⓘ":"i","ⓙ":"j","ⓚ":"k","ⓛ":"l","ⓜ":"m","ⓝ":"n","ⓞ":"o","ⓟ":"p","ⓠ":"q","ⓡ":"r","ⓢ":"s","ⓣ":"t","ⓤ":"u","ⓦ":"v","ⓥ":"w","ⓧ":"x","ⓨ":"y","ⓩ":"z","“":'"',"”":'"',"‘":"'","’":"'","∂":"d",ƒ:"f","™":"(TM)","©":"(C)",œ:"oe",Œ:"OE","®":"(R)","†":"+","℠":"(SM)","…":"...","˚":"o",º:"o",ª:"a","•":"*","၊":",","။":".",$:"USD","€":"EUR","₢":"BRN","₣":"FRF","£":"GBP","₤":"ITL","₦":"NGN","₧":"ESP","₩":"KRW","₪":"ILS","₫":"VND","₭":"LAK","₮":"MNT","₯":"GRD","₱":"ARS","₲":"PYG","₳":"ARA","₴":"UAH","₵":"GHS","¢":"cent","¥":"CNY",元:"CNY",円:"YEN","﷼":"IRR","₠":"EWE","฿":"THB","₨":"INR","₹":"INR","₰":"PF","₺":"TRY","؋":"AFN","₼":"AZN",лв:"BGN","៛":"KHR","₡":"CRC","₸":"KZT",ден:"MKD",zł:"PLN","₽":"RUB","₾":"GEL"},o=["်","ް"],s={"ာ":"a","ါ":"a","ေ":"e","ဲ":"e","ိ":"i","ီ":"i","ို":"o","ု":"u","ူ":"u","ေါင်":"aung","ော":"aw","ော်":"aw","ေါ":"aw","ေါ်":"aw","်":"်","က်":"et","ိုက်":"aik","ောက်":"auk","င်":"in","ိုင်":"aing","ောင်":"aung","စ်":"it","ည်":"i","တ်":"at","ိတ်":"eik","ုတ်":"ok","ွတ်":"ut","ေတ်":"it","ဒ်":"d","ိုဒ်":"ok","ုဒ်":"ait","န်":"an","ာန်":"an","ိန်":"ein","ုန်":"on","ွန်":"un","ပ်":"at","ိပ်":"eik","ုပ်":"ok","ွပ်":"ut","န်ုပ်":"nub","မ်":"an","ိမ်":"ein","ုမ်":"on","ွမ်":"un","ယ်":"e","ိုလ်":"ol","ဉ်":"in","ံ":"an","ိံ":"ein","ုံ":"on","ައް":"ah","ަށް":"ah"},i={en:{},az:{ç:"c",ə:"e",ğ:"g",ı:"i",ö:"o",ş:"s",ü:"u",Ç:"C",Ə:"E",Ğ:"G",İ:"I",Ö:"O",Ş:"S",Ü:"U"},cs:{č:"c",ď:"d",ě:"e",ň:"n",ř:"r",š:"s",ť:"t",ů:"u",ž:"z",Č:"C",Ď:"D",Ě:"E",Ň:"N",Ř:"R",Š:"S",Ť:"T",Ů:"U",Ž:"Z"},fi:{ä:"a",Ä:"A",ö:"o",Ö:"O"},hu:{ä:"a",Ä:"A",ö:"o",Ö:"O",ü:"u",Ü:"U",ű:"u",Ű:"U"},lt:{ą:"a",č:"c",ę:"e",ė:"e",į:"i",š:"s",ų:"u",ū:"u",ž:"z",Ą:"A",Č:"C",Ę:"E",Ė:"E",Į:"I",Š:"S",Ų:"U",Ū:"U"},lv:{ā:"a",č:"c",ē:"e",ģ:"g",ī:"i",ķ:"k",ļ:"l",ņ:"n",š:"s",ū:"u",ž:"z",Ā:"A",Č:"C",Ē:"E",Ģ:"G",Ī:"i",Ķ:"k",Ļ:"L",Ņ:"N",Š:"S",Ū:"u",Ž:"Z"},pl:{ą:"a",ć:"c",ę:"e",ł:"l",ń:"n",ó:"o",ś:"s",ź:"z",ż:"z",Ą:"A",Ć:"C",Ę:"e",Ł:"L",Ń:"N",Ó:"O",Ś:"S",Ź:"Z",Ż:"Z"},sv:{ä:"a",Ä:"A",ö:"o",Ö:"O"},sk:{ä:"a",Ä:"A"},sr:{љ:"lj",њ:"nj",Љ:"Lj",Њ:"Nj",đ:"dj",Đ:"Dj"},tr:{Ü:"U",Ö:"O",ü:"u",ö:"o"}},a={ar:{"∆":"delta","∞":"la-nihaya","♥":"hob","&":"wa","|":"aw","<":"aqal-men",">":"akbar-men","∑":"majmou","¤":"omla"},az:{},ca:{"∆":"delta","∞":"infinit","♥":"amor","&":"i","|":"o","<":"menys que",">":"mes que","∑":"suma dels","¤":"moneda"},cs:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"nebo","<":"mensi nez",">":"vetsi nez","∑":"soucet","¤":"mena"},de:{"∆":"delta","∞":"unendlich","♥":"Liebe","&":"und","|":"oder","<":"kleiner als",">":"groesser als","∑":"Summe von","¤":"Waehrung"},dv:{"∆":"delta","∞":"kolunulaa","♥":"loabi","&":"aai","|":"noonee","<":"ah vure kuda",">":"ah vure bodu","∑":"jumula","¤":"faisaa"},en:{"∆":"delta","∞":"infinity","♥":"love","&":"and","|":"or","<":"less than",">":"greater than","∑":"sum","¤":"currency"},es:{"∆":"delta","∞":"infinito","♥":"amor","&":"y","|":"u","<":"menos que",">":"mas que","∑":"suma de los","¤":"moneda"},fa:{"∆":"delta","∞":"bi-nahayat","♥":"eshgh","&":"va","|":"ya","<":"kamtar-az",">":"bishtar-az","∑":"majmooe","¤":"vahed"},fi:{"∆":"delta","∞":"aarettomyys","♥":"rakkaus","&":"ja","|":"tai","<":"pienempi kuin",">":"suurempi kuin","∑":"summa","¤":"valuutta"},fr:{"∆":"delta","∞":"infiniment","♥":"Amour","&":"et","|":"ou","<":"moins que",">":"superieure a","∑":"somme des","¤":"monnaie"},ge:{"∆":"delta","∞":"usasruloba","♥":"siqvaruli","&":"da","|":"an","<":"naklebi",">":"meti","∑":"jami","¤":"valuta"},gr:{},hu:{"∆":"delta","∞":"vegtelen","♥":"szerelem","&":"es","|":"vagy","<":"kisebb mint",">":"nagyobb mint","∑":"szumma","¤":"penznem"},it:{"∆":"delta","∞":"infinito","♥":"amore","&":"e","|":"o","<":"minore di",">":"maggiore di","∑":"somma","¤":"moneta"},lt:{"∆":"delta","∞":"begalybe","♥":"meile","&":"ir","|":"ar","<":"maziau nei",">":"daugiau nei","∑":"suma","¤":"valiuta"},lv:{"∆":"delta","∞":"bezgaliba","♥":"milestiba","&":"un","|":"vai","<":"mazak neka",">":"lielaks neka","∑":"summa","¤":"valuta"},my:{"∆":"kwahkhyaet","∞":"asaonasme","♥":"akhyait","&":"nhin","|":"tho","<":"ngethaw",">":"kyithaw","∑":"paungld","¤":"ngwekye"},mk:{},nl:{"∆":"delta","∞":"oneindig","♥":"liefde","&":"en","|":"of","<":"kleiner dan",">":"groter dan","∑":"som","¤":"valuta"},pl:{"∆":"delta","∞":"nieskonczonosc","♥":"milosc","&":"i","|":"lub","<":"mniejsze niz",">":"wieksze niz","∑":"suma","¤":"waluta"},pt:{"∆":"delta","∞":"infinito","♥":"amor","&":"e","|":"ou","<":"menor que",">":"maior que","∑":"soma","¤":"moeda"},ro:{"∆":"delta","∞":"infinit","♥":"dragoste","&":"si","|":"sau","<":"mai mic ca",">":"mai mare ca","∑":"suma","¤":"valuta"},ru:{"∆":"delta","∞":"beskonechno","♥":"lubov","&":"i","|":"ili","<":"menshe",">":"bolshe","∑":"summa","¤":"valjuta"},sk:{"∆":"delta","∞":"nekonecno","♥":"laska","&":"a","|":"alebo","<":"menej ako",">":"viac ako","∑":"sucet","¤":"mena"},sr:{},tr:{"∆":"delta","∞":"sonsuzluk","♥":"ask","&":"ve","|":"veya","<":"kucuktur",">":"buyuktur","∑":"toplam","¤":"para birimi"},uk:{"∆":"delta","∞":"bezkinechnist","♥":"lubov","&":"i","|":"abo","<":"menshe",">":"bilshe","∑":"suma","¤":"valjuta"},vn:{"∆":"delta","∞":"vo cuc","♥":"yeu","&":"va","|":"hoac","<":"nho hon",">":"lon hon","∑":"tong","¤":"tien te"}},l=[";","?",":","@","&","=","+","$",",","/"].join(""),u=[";","?",":","@","&","=","+","$",","].join(""),c=[".","!","~","*","'","(",")"].join(""),f=function(b,E){var A="-",m="",y="",L=!0,W={},M,T,U,O,B,C,D,Z,ee,I,V,z,le,ye,ke="";if(typeof b!="string")return"";if(typeof E=="string"&&(A=E),D=a.en,Z=i.en,typeof E=="object"){M=E.maintainCase||!1,W=E.custom&&typeof E.custom=="object"?E.custom:W,U=+E.truncate>1&&E.truncate||!1,O=E.uric||!1,B=E.uricNoSlash||!1,C=E.mark||!1,L=!(E.symbols===!1||E.lang===!1),A=E.separator||A,O&&(ke+=l),B&&(ke+=u),C&&(ke+=c),D=E.lang&&a[E.lang]&&L?a[E.lang]:L?a.en:{},Z=E.lang&&i[E.lang]?i[E.lang]:E.lang===!1||E.lang===!0?{}:i.en,E.titleCase&&typeof E.titleCase.length=="number"&&Array.prototype.toString.call(E.titleCase)?(E.titleCase.forEach(function(be){W[be+""]=be+""}),T=!0):T=!!E.titleCase,E.custom&&typeof E.custom.length=="number"&&Array.prototype.toString.call(E.custom)&&E.custom.forEach(function(be){W[be+""]=be+""}),Object.keys(W).forEach(function(be){var et;be.length>1?et=new RegExp("\\b"+p(be)+"\\b","gi"):et=new RegExp(p(be),"gi"),b=b.replace(et,W[be])});for(V in W)ke+=V}for(ke+=A,ke=p(ke),b=b.replace(/(^\s+|\s+$)/g,""),le=!1,ye=!1,I=0,z=b.length;I=0?(y+=V,V=""):ye===!0?(V=s[y]+r[V],y=""):V=le&&r[V].match(/[A-Za-z0-9]/)?" "+r[V]:r[V],le=!1,ye=!1):V in s?(y+=V,V="",I===z-1&&(V=s[y]),ye=!0):D[V]&&!(O&&l.indexOf(V)!==-1)&&!(B&&u.indexOf(V)!==-1)?(V=le||m.substr(-1).match(/[A-Za-z0-9]/)?A+D[V]:D[V],V+=b[I+1]!==void 0&&b[I+1].match(/[A-Za-z0-9]/)?A:"",le=!0):(ye===!0?(V=s[y]+V,y="",ye=!1):le&&(/[A-Za-z0-9]/.test(V)||m.substr(-1).match(/A-Za-z0-9]/))&&(V=" "+V),le=!1),m+=V.replace(new RegExp("[^\\w\\s"+ke+"_-]","g"),A);return T&&(m=m.replace(/(\w)(\S*)/g,function(be,et,_t){var tt=et.toUpperCase()+(_t!==null?_t:"");return Object.keys(W).indexOf(tt.toLowerCase())<0?tt:tt.toLowerCase()})),m=m.replace(/\s+/g,A).replace(new RegExp("\\"+A+"+","g"),A).replace(new RegExp("(^\\"+A+"+|\\"+A+"+$)","g"),""),U&&m.length>U&&(ee=m.charAt(U)===A,m=m.slice(0,U),ee||(m=m.slice(0,m.lastIndexOf(A)))),!M&&!T&&(m=m.toLowerCase()),m},d=function(b){return function(A){return f(A,b)}},p=function(b){return b.replace(/[-\\^$*+?.()|[\]{}\/]/g,"\\$&")},_=function(v,b){for(var E in b)if(b[E]===v)return!0};if(typeof t<"u"&&t.exports)t.exports=f,t.exports.createSlug=d;else if(typeof define<"u"&&define.amd)define([],function(){return f});else try{if(n.getSlug||n.createSlug)throw"speakingurl: globals exists /(getSlug|createSlug)/";n.getSlug=f,n.createSlug=d}catch{}}(e)}}),Bv=cf({"../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js"(e,t){P(),t.exports=Hv()}});P();P();P();P();P();P();P();P();function $v(e){var t;const n=e.name||e._componentTag||e.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__||e.__name;return n==="index"&&((t=e.__file)!=null&&t.endsWith("index.vue"))?"":n}function Uv(e){const t=e.__file;if(t)return Av(Cv(t,".vue"))}function tl(e,t){return e.type.__VUE_DEVTOOLS_COMPONENT_GUSSED_NAME__=t,t}function Uo(e){if(e.__VUE_DEVTOOLS_NEXT_APP_RECORD__)return e.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(e.root)return e.appContext.app.__VUE_DEVTOOLS_NEXT_APP_RECORD__}async function jv(e){const{app:t,uid:n,instance:r}=e;try{if(r.__VUE_DEVTOOLS_NEXT_UID__)return r.__VUE_DEVTOOLS_NEXT_UID__;const o=await Uo(t);if(!o)return null;const s=o.rootInstance===r;return`${o.id}:${s?"root":n}`}catch{}}function ff(e){var t,n;const r=(t=e.subTree)==null?void 0:t.type,o=Uo(e);return o?((n=o==null?void 0:o.types)==null?void 0:n.Fragment)===r:!1}function jo(e){var t,n,r;const o=$v((e==null?void 0:e.type)||{});if(o)return o;if((e==null?void 0:e.root)===e)return"Root";for(const i in(n=(t=e.parent)==null?void 0:t.type)==null?void 0:n.components)if(e.parent.type.components[i]===(e==null?void 0:e.type))return tl(e,i);for(const i in(r=e.appContext)==null?void 0:r.components)if(e.appContext.components[i]===(e==null?void 0:e.type))return tl(e,i);const s=Uv((e==null?void 0:e.type)||{});return s||"Anonymous Component"}function Ms(e,t){return t=t||`${e.id}:root`,e.instanceMap.get(t)||e.instanceMap.get(":root")}function zv(){const e={top:0,bottom:0,left:0,right:0,get width(){return e.right-e.left},get height(){return e.bottom-e.top}};return e}var so;function Kv(e){return so||(so=document.createRange()),so.selectNode(e),so.getBoundingClientRect()}function Wv(e){const t=zv();if(!e.children)return t;for(let n=0,r=e.children.length;ne.bottom)&&(e.bottom=t.bottom),(!e.left||t.lefte.right)&&(e.right=t.right),e}var nl={top:0,left:0,right:0,bottom:0,width:0,height:0};function Tn(e){const t=e.subTree.el;return typeof window>"u"?nl:ff(e)?Wv(e.subTree):(t==null?void 0:t.nodeType)===1?t==null?void 0:t.getBoundingClientRect():e.subTree.component?Tn(e.subTree.component):nl}P();function wi(e){return ff(e)?Gv(e.subTree):e.subTree?[e.subTree.el]:[]}function Gv(e){if(!e.children)return[];const t=[];return e.children.forEach(n=>{n.component?t.push(...wi(n.component)):n!=null&&n.el&&t.push(n.el)}),t}var df="__vue-devtools-component-inspector__",pf="__vue-devtools-component-inspector__card__",hf="__vue-devtools-component-inspector__name__",mf="__vue-devtools-component-inspector__indicator__",gf={display:"block",zIndex:2147483640,position:"fixed",backgroundColor:"#42b88325",border:"1px solid #42b88350",borderRadius:"5px",transition:"all 0.1s ease-in",pointerEvents:"none"},Yv={fontFamily:"Arial, Helvetica, sans-serif",padding:"5px 8px",borderRadius:"4px",textAlign:"left",position:"absolute",left:0,color:"#e9e9e9",fontSize:"14px",fontWeight:600,lineHeight:"24px",backgroundColor:"#42b883",boxShadow:"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)"},Xv={display:"inline-block",fontWeight:400,fontStyle:"normal",fontSize:"12px",opacity:.7};function nr(){return document.getElementById(df)}function Zv(){return document.getElementById(pf)}function Jv(){return document.getElementById(mf)}function Qv(){return document.getElementById(hf)}function Oi(e){return{left:`${Math.round(e.left*100)/100}px`,top:`${Math.round(e.top*100)/100}px`,width:`${Math.round(e.width*100)/100}px`,height:`${Math.round(e.height*100)/100}px`}}function xi(e){var t;const n=document.createElement("div");n.id=(t=e.elementId)!=null?t:df,Object.assign(n.style,{...gf,...Oi(e.bounds),...e.style});const r=document.createElement("span");r.id=pf,Object.assign(r.style,{...Yv,top:e.bounds.top<35?0:"-35px"});const o=document.createElement("span");o.id=hf,o.innerHTML=`<${e.name}>  `;const s=document.createElement("i");return s.id=mf,s.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`,Object.assign(s.style,Xv),r.appendChild(o),r.appendChild(s),n.appendChild(r),document.body.appendChild(n),n}function ki(e){const t=nr(),n=Zv(),r=Qv(),o=Jv();t&&(Object.assign(t.style,{...gf,...Oi(e.bounds)}),Object.assign(n.style,{top:e.bounds.top<35?0:"-35px"}),r.innerHTML=`<${e.name}>  `,o.innerHTML=`${Math.round(e.bounds.width*100)/100} x ${Math.round(e.bounds.height*100)/100}`)}function e0(e){const t=Tn(e);if(!t.width&&!t.height)return;const n=jo(e);nr()?ki({bounds:t,name:n}):xi({bounds:t,name:n})}function _f(){const e=nr();e&&(e.style.display="none")}var Fs=null;function Hs(e){const t=e.target;if(t){const n=t.__vueParentComponent;if(n&&(Fs=n,n.vnode.el)){const o=Tn(n),s=jo(n);nr()?ki({bounds:o,name:s}):xi({bounds:o,name:s})}}}function t0(e,t){var n;if(e.preventDefault(),e.stopPropagation(),Fs){const r=(n=Ge.value)==null?void 0:n.app;jv({app:r,uid:r.uid,instance:Fs}).then(o=>{t(o)})}}var Co=null;function n0(){_f(),window.removeEventListener("mouseover",Hs),window.removeEventListener("click",Co,!0),Co=null}function r0(){return window.addEventListener("mouseover",Hs),new Promise(e=>{function t(n){n.preventDefault(),n.stopPropagation(),t0(n,r=>{window.removeEventListener("click",t,!0),Co=null,window.removeEventListener("mouseover",Hs);const o=nr();o&&(o.style.display="none"),e(JSON.stringify({id:r}))})}Co=t,window.addEventListener("click",t,!0)})}function o0(e){const t=Ms(Ge.value,e.id);if(t){const[n]=wi(t);if(typeof n.scrollIntoView=="function")n.scrollIntoView({behavior:"smooth"});else{const r=Tn(t),o=document.createElement("div"),s={...Oi(r),position:"absolute"};Object.assign(o.style,s),document.body.appendChild(o),o.scrollIntoView({behavior:"smooth"}),setTimeout(()=>{document.body.removeChild(o)},2e3)}setTimeout(()=>{const r=Tn(t);if(r.width||r.height){const o=jo(t),s=nr();s?ki({...e,name:o,bounds:r}):xi({...e,name:o,bounds:r}),setTimeout(()=>{s&&(s.style.display="none")},1500)}},1200)}}P();var rl,ol;(ol=(rl=X).__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__)!=null||(rl.__VUE_DEVTOOLS_COMPONENT_INSPECTOR_ENABLED__=!0);function s0(e){let t=0;const n=setInterval(()=>{X.__VUE_INSPECTOR__&&(clearInterval(n),t+=30,e()),t>=5e3&&clearInterval(n)},30)}function i0(){const e=X.__VUE_INSPECTOR__,t=e.openInEditor;e.openInEditor=async(...n)=>{e.disable(),t(...n)}}function a0(){return new Promise(e=>{function t(){i0(),e(X.__VUE_INSPECTOR__)}X.__VUE_INSPECTOR__?t():s0(()=>{t()})})}P();P();function l0(e){return!!(e&&e.__v_isReadonly)}function vf(e){return l0(e)?vf(e.__v_raw):!!(e&&e.__v_isReactive)}function hs(e){return!!(e&&e.__v_isRef===!0)}function hr(e){const t=e&&e.__v_raw;return t?hr(t):e}var u0=class{constructor(){this.refEditor=new c0}set(e,t,n,r){const o=Array.isArray(t)?t:t.split(".");for(;o.length>1;){const a=o.shift();e instanceof Map&&(e=e.get(a)),e instanceof Set?e=Array.from(e.values())[a]:e=e[a],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}const s=o[0],i=this.refEditor.get(e)[s];r?r(e,s,n):this.refEditor.isRef(i)?this.refEditor.set(i,n):e[s]=n}get(e,t){const n=Array.isArray(t)?t:t.split(".");for(let r=0;r"u")return!1;const r=Array.isArray(t)?t.slice():t.split("."),o=n?2:1;for(;e&&r.length>o;){const s=r.shift();e=e[s],this.refEditor.isRef(e)&&(e=this.refEditor.get(e))}return e!=null&&Object.prototype.hasOwnProperty.call(e,r[0])}createDefaultSetCallback(e){return(t,n,r)=>{if((e.remove||e.newKey)&&(Array.isArray(t)?t.splice(n,1):hr(t)instanceof Map?t.delete(n):hr(t)instanceof Set?t.delete(Array.from(t.values())[n]):Reflect.deleteProperty(t,n)),!e.remove){const o=t[e.newKey||n];this.refEditor.isRef(o)?this.refEditor.set(o,r):hr(t)instanceof Map?t.set(e.newKey||n,r):hr(t)instanceof Set?t.add(r):t[e.newKey||n]=r}}}},c0=class{set(e,t){if(hs(e))e.value=t;else{if(e instanceof Set&&Array.isArray(t)){e.clear(),t.forEach(o=>e.add(o));return}const n=Object.keys(t);if(e instanceof Map){const o=new Set(e.keys());n.forEach(s=>{e.set(s,Reflect.get(t,s)),o.delete(s)}),o.forEach(s=>e.delete(s));return}const r=new Set(Object.keys(e));n.forEach(o=>{Reflect.set(e,o,Reflect.get(t,o)),r.delete(o)}),r.forEach(o=>Reflect.deleteProperty(e,o))}}get(e){return hs(e)?e.value:e}isRef(e){return hs(e)||vf(e)}};P();P();P();var f0="__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS_STATE__";function d0(){if(!sf||typeof localStorage>"u"||localStorage===null)return{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""};const e=localStorage.getItem(f0);return e?JSON.parse(e):{recordingState:!1,mouseEventEnabled:!1,keyboardEventEnabled:!1,componentEventEnabled:!1,performanceEventEnabled:!1,selected:""}}P();P();P();var sl,il;(il=(sl=X).__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS)!=null||(sl.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS=[]);var p0=new Proxy(X.__VUE_DEVTOOLS_KIT_TIMELINE_LAYERS,{get(e,t,n){return Reflect.get(e,t,n)}});function h0(e,t){Fe.timelineLayersState[t.id]=!1,p0.push({...e,descriptorId:t.id,appRecord:Uo(t.app)})}var al,ll;(ll=(al=X).__VUE_DEVTOOLS_KIT_INSPECTOR__)!=null||(al.__VUE_DEVTOOLS_KIT_INSPECTOR__=[]);var Pi=new Proxy(X.__VUE_DEVTOOLS_KIT_INSPECTOR__,{get(e,t,n){return Reflect.get(e,t,n)}}),yf=Kn(()=>{rr.hooks.callHook("sendInspectorToClient",bf())});function m0(e,t){var n,r;Pi.push({options:e,descriptor:t,treeFilterPlaceholder:(n=e.treeFilterPlaceholder)!=null?n:"Search tree...",stateFilterPlaceholder:(r=e.stateFilterPlaceholder)!=null?r:"Search state...",treeFilter:"",selectedNodeId:"",appRecord:Uo(t.app)}),yf()}function bf(){return Pi.filter(e=>e.descriptor.app===Ge.value.app).filter(e=>e.descriptor.id!=="components").map(e=>{var t;const n=e.descriptor,r=e.options;return{id:r.id,label:r.label,logo:n.logo,icon:`custom-ic-baseline-${(t=r==null?void 0:r.icon)==null?void 0:t.replace(/_/g,"-")}`,packageName:n.packageName,homepage:n.homepage,pluginId:n.id}})}function co(e,t){return Pi.find(n=>n.options.id===e&&(t?n.descriptor.app===t:!0))}function g0(){const e=lf();e.hook("addInspector",({inspector:r,plugin:o})=>{m0(r,o.descriptor)});const t=Kn(async({inspectorId:r,plugin:o})=>{var s;if(!r||!((s=o==null?void 0:o.descriptor)!=null&&s.app)||Fe.highPerfModeEnabled)return;const i=co(r,o.descriptor.app),a={app:o.descriptor.app,inspectorId:r,filter:(i==null?void 0:i.treeFilter)||"",rootNodes:[]};await new Promise(l=>{e.callHookWith(async u=>{await Promise.all(u.map(c=>c(a))),l()},"getInspectorTree")}),e.callHookWith(async l=>{await Promise.all(l.map(u=>u({inspectorId:r,rootNodes:a.rootNodes})))},"sendInspectorTreeToClient")},120);e.hook("sendInspectorTree",t);const n=Kn(async({inspectorId:r,plugin:o})=>{var s;if(!r||!((s=o==null?void 0:o.descriptor)!=null&&s.app)||Fe.highPerfModeEnabled)return;const i=co(r,o.descriptor.app),a={app:o.descriptor.app,inspectorId:r,nodeId:(i==null?void 0:i.selectedNodeId)||"",state:null},l={currentTab:`custom-inspector:${r}`};a.nodeId&&await new Promise(u=>{e.callHookWith(async c=>{await Promise.all(c.map(f=>f(a,l))),u()},"getInspectorState")}),e.callHookWith(async u=>{await Promise.all(u.map(c=>c({inspectorId:r,nodeId:a.nodeId,state:a.state})))},"sendInspectorStateToClient")},120);return e.hook("sendInspectorState",n),e.hook("customInspectorSelectNode",({inspectorId:r,nodeId:o,plugin:s})=>{const i=co(r,s.descriptor.app);i&&(i.selectedNodeId=o)}),e.hook("timelineLayerAdded",({options:r,plugin:o})=>{h0(r,o.descriptor)}),e.hook("timelineEventAdded",({options:r,plugin:o})=>{var s;const i=["performance","component-event","keyboard","mouse"];Fe.highPerfModeEnabled||!((s=Fe.timelineLayersState)!=null&&s[o.descriptor.id])&&!i.includes(r.layerId)||e.callHookWith(async a=>{await Promise.all(a.map(l=>l(r)))},"sendTimelineEventToClient")}),e.hook("getComponentInstances",async({app:r})=>{const o=r.__VUE_DEVTOOLS_NEXT_APP_RECORD__;if(!o)return null;const s=o.id.toString();return[...o.instanceMap].filter(([a])=>a.split(":")[0]===s).map(([,a])=>a)}),e.hook("getComponentBounds",async({instance:r})=>Tn(r)),e.hook("getComponentName",({instance:r})=>jo(r)),e.hook("componentHighlight",({uid:r})=>{const o=Ge.value.instanceMap.get(r);o&&e0(o)}),e.hook("componentUnhighlight",()=>{_f()}),e}var ul,cl;(cl=(ul=X).__VUE_DEVTOOLS_KIT_APP_RECORDS__)!=null||(ul.__VUE_DEVTOOLS_KIT_APP_RECORDS__=[]);var fl,dl;(dl=(fl=X).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__)!=null||(fl.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__={});var pl,hl;(hl=(pl=X).__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__)!=null||(pl.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__="");var ml,gl;(gl=(ml=X).__VUE_DEVTOOLS_KIT_CUSTOM_TABS__)!=null||(ml.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__=[]);var _l,vl;(vl=(_l=X).__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__)!=null||(_l.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__=[]);var bn="__VUE_DEVTOOLS_KIT_GLOBAL_STATE__";function _0(){return{connected:!1,clientConnected:!1,vitePluginDetected:!0,appRecords:[],activeAppRecordId:"",tabs:[],commands:[],highPerfModeEnabled:!0,devtoolsClientDetected:{},perfUniqueGroupId:0,timelineLayersState:d0()}}var yl,bl;(bl=(yl=X)[bn])!=null||(yl[bn]=_0());var v0=Kn(e=>{rr.hooks.callHook("devtoolsStateUpdated",{state:e})});Kn((e,t)=>{rr.hooks.callHook("devtoolsConnectedUpdated",{state:e,oldState:t})});var zo=new Proxy(X.__VUE_DEVTOOLS_KIT_APP_RECORDS__,{get(e,t,n){return t==="value"?X.__VUE_DEVTOOLS_KIT_APP_RECORDS__:X.__VUE_DEVTOOLS_KIT_APP_RECORDS__[t]}}),Ge=new Proxy(X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__,{get(e,t,n){return t==="value"?X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__:t==="id"?X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__:X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__[t]}});function Ef(){v0({...X[bn],appRecords:zo.value,activeAppRecordId:Ge.id,tabs:X.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__,commands:X.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__})}function y0(e){X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD__=e,Ef()}function b0(e){X.__VUE_DEVTOOLS_KIT_ACTIVE_APP_RECORD_ID__=e,Ef()}var Fe=new Proxy(X[bn],{get(e,t){return t==="appRecords"?zo:t==="activeAppRecordId"?Ge.id:t==="tabs"?X.__VUE_DEVTOOLS_KIT_CUSTOM_TABS__:t==="commands"?X.__VUE_DEVTOOLS_KIT_CUSTOM_COMMANDS__:X[bn][t]},deleteProperty(e,t){return delete e[t],!0},set(e,t,n){return{...X[bn]},e[t]=n,X[bn][t]=n,!0}});function E0(e={}){var t,n,r;const{file:o,host:s,baseUrl:i=window.location.origin,line:a=0,column:l=0}=e;if(o){if(s==="chrome-extension"){const u=o.replace(/\\/g,"\\\\"),c=(n=(t=window.VUE_DEVTOOLS_CONFIG)==null?void 0:t.openInEditorHost)!=null?n:"/";fetch(`${c}__open-in-editor?file=${encodeURI(o)}`).then(f=>{if(!f.ok){const d=`Opening component ${u} failed`;console.log(`%c${d}`,"color:red")}})}else if(Fe.vitePluginDetected){const u=(r=X.__VUE_DEVTOOLS_OPEN_IN_EDITOR_BASE_URL__)!=null?r:i;X.__VUE_INSPECTOR__.openInEditor(u,o,a,l)}}}P();P();P();P();P();var El,Sl;(Sl=(El=X).__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__)!=null||(El.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__=[]);var Ii=new Proxy(X.__VUE_DEVTOOLS_KIT_PLUGIN_BUFFER__,{get(e,t,n){return Reflect.get(e,t,n)}});function Bs(e){const t={};return Object.keys(e).forEach(n=>{t[n]=e[n].defaultValue}),t}function Ri(e){return`__VUE_DEVTOOLS_NEXT_PLUGIN_SETTINGS__${e}__`}function S0(e){var t,n,r;const o=(n=(t=Ii.find(s=>{var i;return s[0].id===e&&!!((i=s[0])!=null&&i.settings)}))==null?void 0:t[0])!=null?n:null;return(r=o==null?void 0:o.settings)!=null?r:null}function Sf(e,t){var n,r,o;const s=Ri(e);if(s){const i=localStorage.getItem(s);if(i)return JSON.parse(i)}if(e){const i=(r=(n=Ii.find(a=>a[0].id===e))==null?void 0:n[0])!=null?r:null;return Bs((o=i==null?void 0:i.settings)!=null?o:{})}return Bs(t)}function A0(e,t){const n=Ri(e);localStorage.getItem(n)||localStorage.setItem(n,JSON.stringify(Bs(t)))}function C0(e,t,n){const r=Ri(e),o=localStorage.getItem(r),s=JSON.parse(o||"{}"),i={...s,[t]:n};localStorage.setItem(r,JSON.stringify(i)),rr.hooks.callHookWith(a=>{a.forEach(l=>l({pluginId:e,key:t,oldValue:s[t],newValue:n,settings:i}))},"setPluginSettings")}P();P();P();P();P();P();P();P();P();P();P();var Al,Cl,ut=(Cl=(Al=X).__VUE_DEVTOOLS_HOOK)!=null?Cl:Al.__VUE_DEVTOOLS_HOOK=lf(),T0={vueAppInit(e){ut.hook("app:init",e)},vueAppUnmount(e){ut.hook("app:unmount",e)},vueAppConnected(e){ut.hook("app:connected",e)},componentAdded(e){return ut.hook("component:added",e)},componentEmit(e){return ut.hook("component:emit",e)},componentUpdated(e){return ut.hook("component:updated",e)},componentRemoved(e){return ut.hook("component:removed",e)},setupDevtoolsPlugin(e){ut.hook("devtools-plugin:setup",e)},perfStart(e){return ut.hook("perf:start",e)},perfEnd(e){return ut.hook("perf:end",e)}},Af={on:T0,setupDevToolsPlugin(e,t){return ut.callHook("devtools-plugin:setup",e,t)}},w0=class{constructor({plugin:e,ctx:t}){this.hooks=t.hooks,this.plugin=e}get on(){return{visitComponentTree:e=>{this.hooks.hook("visitComponentTree",e)},inspectComponent:e=>{this.hooks.hook("inspectComponent",e)},editComponentState:e=>{this.hooks.hook("editComponentState",e)},getInspectorTree:e=>{this.hooks.hook("getInspectorTree",e)},getInspectorState:e=>{this.hooks.hook("getInspectorState",e)},editInspectorState:e=>{this.hooks.hook("editInspectorState",e)},inspectTimelineEvent:e=>{this.hooks.hook("inspectTimelineEvent",e)},timelineCleared:e=>{this.hooks.hook("timelineCleared",e)},setPluginSettings:e=>{this.hooks.hook("setPluginSettings",e)}}}notifyComponentUpdate(e){var t;if(Fe.highPerfModeEnabled)return;const n=bf().find(r=>r.packageName===this.plugin.descriptor.packageName);if(n!=null&&n.id){if(e){const r=[e.appContext.app,e.uid,(t=e.parent)==null?void 0:t.uid,e];ut.callHook("component:updated",...r)}else ut.callHook("component:updated");this.hooks.callHook("sendInspectorState",{inspectorId:n.id,plugin:this.plugin})}}addInspector(e){this.hooks.callHook("addInspector",{inspector:e,plugin:this.plugin}),this.plugin.descriptor.settings&&A0(e.id,this.plugin.descriptor.settings)}sendInspectorTree(e){Fe.highPerfModeEnabled||this.hooks.callHook("sendInspectorTree",{inspectorId:e,plugin:this.plugin})}sendInspectorState(e){Fe.highPerfModeEnabled||this.hooks.callHook("sendInspectorState",{inspectorId:e,plugin:this.plugin})}selectInspectorNode(e,t){this.hooks.callHook("customInspectorSelectNode",{inspectorId:e,nodeId:t,plugin:this.plugin})}visitComponentTree(e){return this.hooks.callHook("visitComponentTree",e)}now(){return Fe.highPerfModeEnabled?0:Date.now()}addTimelineLayer(e){this.hooks.callHook("timelineLayerAdded",{options:e,plugin:this.plugin})}addTimelineEvent(e){Fe.highPerfModeEnabled||this.hooks.callHook("timelineEventAdded",{options:e,plugin:this.plugin})}getSettings(e){return Sf(e??this.plugin.descriptor.id,this.plugin.descriptor.settings)}getComponentInstances(e){return this.hooks.callHook("getComponentInstances",{app:e})}getComponentBounds(e){return this.hooks.callHook("getComponentBounds",{instance:e})}getComponentName(e){return this.hooks.callHook("getComponentName",{instance:e})}highlightElement(e){const t=e.__VUE_DEVTOOLS_NEXT_UID__;return this.hooks.callHook("componentHighlight",{uid:t})}unhighlightElement(){return this.hooks.callHook("componentUnhighlight")}},O0=w0;P();P();P();P();var x0="__vue_devtool_undefined__",k0="__vue_devtool_infinity__",P0="__vue_devtool_negative_infinity__",I0="__vue_devtool_nan__";P();P();var R0={[x0]:"undefined",[I0]:"NaN",[k0]:"Infinity",[P0]:"-Infinity"};Object.entries(R0).reduce((e,[t,n])=>(e[n]=t,e),{});P();P();P();P();P();var Tl,wl;(wl=(Tl=X).__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__)!=null||(Tl.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__=new Set);function L0(e,t){return Af.setupDevToolsPlugin(e,t)}function D0(e,t){const[n,r]=e;if(n.app!==t)return;const o=new O0({plugin:{setupFn:r,descriptor:n},ctx:rr});n.packageName==="vuex"&&o.on.editInspectorState(s=>{o.sendInspectorState(s.inspectorId)}),r(o)}function Cf(e){X.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.has(e)||Fe.highPerfModeEnabled||(X.__VUE_DEVTOOLS_KIT__REGISTERED_PLUGIN_APPS__.add(e),Ii.forEach(t=>{D0(t,e)}))}P();P();var Dr="__VUE_DEVTOOLS_ROUTER__",Wn="__VUE_DEVTOOLS_ROUTER_INFO__",Ol,xl;(xl=(Ol=X)[Wn])!=null||(Ol[Wn]={currentRoute:null,routes:[]});var kl,Pl;(Pl=(kl=X)[Dr])!=null||(kl[Dr]={});new Proxy(X[Wn],{get(e,t){return X[Wn][t]}});new Proxy(X[Dr],{get(e,t){if(t==="value")return X[Dr]}});function V0(e){const t=new Map;return((e==null?void 0:e.getRoutes())||[]).filter(n=>!t.has(n.path)&&t.set(n.path,1))}function Li(e){return e.map(t=>{let{path:n,name:r,children:o,meta:s}=t;return o!=null&&o.length&&(o=Li(o)),{path:n,name:r,children:o,meta:s}})}function N0(e){if(e){const{fullPath:t,hash:n,href:r,path:o,name:s,matched:i,params:a,query:l}=e;return{fullPath:t,hash:n,href:r,path:o,name:s,params:a,query:l,matched:Li(i)}}return e}function M0(e,t){function n(){var r;const o=(r=e.app)==null?void 0:r.config.globalProperties.$router,s=N0(o==null?void 0:o.currentRoute.value),i=Li(V0(o)),a=console.warn;console.warn=()=>{},X[Wn]={currentRoute:s?el(s):{},routes:el(i)},X[Dr]=o,console.warn=a}n(),Af.on.componentUpdated(Kn(()=>{var r;((r=t.value)==null?void 0:r.app)===e.app&&(n(),!Fe.highPerfModeEnabled&&rr.hooks.callHook("routerInfoUpdated",{state:X[Wn]}))},200))}function F0(e){return{async getInspectorTree(t){const n={...t,app:Ge.value.app,rootNodes:[]};return await new Promise(r=>{e.callHookWith(async o=>{await Promise.all(o.map(s=>s(n))),r()},"getInspectorTree")}),n.rootNodes},async getInspectorState(t){const n={...t,app:Ge.value.app,state:null},r={currentTab:`custom-inspector:${t.inspectorId}`};return await new Promise(o=>{e.callHookWith(async s=>{await Promise.all(s.map(i=>i(n,r))),o()},"getInspectorState")}),n.state},editInspectorState(t){const n=new u0,r={...t,app:Ge.value.app,set:(o,s=t.path,i=t.state.value,a)=>{n.set(o,s,i,a||n.createDefaultSetCallback(t.state))}};e.callHookWith(o=>{o.forEach(s=>s(r))},"editInspectorState")},sendInspectorState(t){const n=co(t);e.callHook("sendInspectorState",{inspectorId:t,plugin:{descriptor:n.descriptor,setupFn:()=>({})}})},inspectComponentInspector(){return r0()},cancelInspectComponentInspector(){return n0()},getComponentRenderCode(t){const n=Ms(Ge.value,t);if(n)return(n==null?void 0:n.type)instanceof Function?n.type.toString():n.render.toString()},scrollToComponent(t){return o0({id:t})},openInEditor:E0,getVueInspector:a0,toggleApp(t){const n=zo.value.find(r=>r.id===t);n&&(b0(t),y0(n),M0(n,Ge),yf(),Cf(n.app))},inspectDOM(t){const n=Ms(Ge.value,t);if(n){const[r]=wi(n);r&&(X.__VUE_DEVTOOLS_INSPECT_DOM_TARGET__=r)}},updatePluginSettings(t,n,r){C0(t,n,r)},getPluginSettings(t){return{options:S0(t),values:Sf(t)}}}}P();var Il,Rl;(Rl=(Il=X).__VUE_DEVTOOLS_ENV__)!=null||(Il.__VUE_DEVTOOLS_ENV__={vitePluginDetected:!1});var Ll=g0(),Dl,Vl;(Vl=(Dl=X).__VUE_DEVTOOLS_KIT_CONTEXT__)!=null||(Dl.__VUE_DEVTOOLS_KIT_CONTEXT__={hooks:Ll,get state(){return{...Fe,activeAppRecordId:Ge.id,activeAppRecord:Ge.value,appRecords:zo.value}},api:F0(Ll)});var rr=X.__VUE_DEVTOOLS_KIT_CONTEXT__;P();Fv(Bv());var Nl,Ml;(Ml=(Nl=X).__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__)!=null||(Nl.__VUE_DEVTOOLS_NEXT_APP_RECORD_INFO__={id:0,appIds:new Set});P();function H0(e){Fe.highPerfModeEnabled=e??!Fe.highPerfModeEnabled,!e&&Ge.value&&Cf(Ge.value.app)}P();P();P();function B0(e){Fe.devtoolsClientDetected={...Fe.devtoolsClientDetected,...e};const t=Object.values(Fe.devtoolsClientDetected).some(Boolean);H0(!t)}var Fl,Hl;(Hl=(Fl=X).__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__)!=null||(Fl.__VUE_DEVTOOLS_UPDATE_CLIENT_DETECTED__=B0);P();P();P();P();P();P();P();var $0=class{constructor(){this.keyToValue=new Map,this.valueToKey=new Map}set(e,t){this.keyToValue.set(e,t),this.valueToKey.set(t,e)}getByKey(e){return this.keyToValue.get(e)}getByValue(e){return this.valueToKey.get(e)}clear(){this.keyToValue.clear(),this.valueToKey.clear()}},Tf=class{constructor(e){this.generateIdentifier=e,this.kv=new $0}register(e,t){this.kv.getByValue(e)||(t||(t=this.generateIdentifier(e)),this.kv.set(t,e))}clear(){this.kv.clear()}getIdentifier(e){return this.kv.getByValue(e)}getValue(e){return this.kv.getByKey(e)}},U0=class extends Tf{constructor(){super(e=>e.name),this.classToAllowedProps=new Map}register(e,t){typeof t=="object"?(t.allowProps&&this.classToAllowedProps.set(e,t.allowProps),super.register(e,t.identifier)):super.register(e,t)}getAllowedProps(e){return this.classToAllowedProps.get(e)}};P();P();function j0(e){if("values"in Object)return Object.values(e);const t=[];for(const n in e)e.hasOwnProperty(n)&&t.push(e[n]);return t}function z0(e,t){const n=j0(e);if("find"in n)return n.find(t);const r=n;for(let o=0;ot(r,n))}function fo(e,t){return e.indexOf(t)!==-1}function Bl(e,t){for(let n=0;nt.isApplicable(e))}findByName(e){return this.transfomers[e]}};P();P();var W0=e=>Object.prototype.toString.call(e).slice(8,-1),wf=e=>typeof e>"u",q0=e=>e===null,Vr=e=>typeof e!="object"||e===null||e===Object.prototype?!1:Object.getPrototypeOf(e)===null?!0:Object.getPrototypeOf(e)===Object.prototype,$s=e=>Vr(e)&&Object.keys(e).length===0,ln=e=>Array.isArray(e),G0=e=>typeof e=="string",Y0=e=>typeof e=="number"&&!isNaN(e),X0=e=>typeof e=="boolean",Z0=e=>e instanceof RegExp,Nr=e=>e instanceof Map,Mr=e=>e instanceof Set,Of=e=>W0(e)==="Symbol",J0=e=>e instanceof Date&&!isNaN(e.valueOf()),Q0=e=>e instanceof Error,$l=e=>typeof e=="number"&&isNaN(e),e1=e=>X0(e)||q0(e)||wf(e)||Y0(e)||G0(e)||Of(e),t1=e=>typeof e=="bigint",n1=e=>e===1/0||e===-1/0,r1=e=>ArrayBuffer.isView(e)&&!(e instanceof DataView),o1=e=>e instanceof URL;P();var xf=e=>e.replace(/\./g,"\\."),ms=e=>e.map(String).map(xf).join("."),Ar=e=>{const t=[];let n="";for(let o=0;onull,()=>{}),wt(t1,"bigint",e=>e.toString(),e=>typeof BigInt<"u"?BigInt(e):(console.error("Please add a BigInt polyfill."),e)),wt(J0,"Date",e=>e.toISOString(),e=>new Date(e)),wt(Q0,"Error",(e,t)=>{const n={name:e.name,message:e.message};return t.allowedErrorProps.forEach(r=>{n[r]=e[r]}),n},(e,t)=>{const n=new Error(e.message);return n.name=e.name,n.stack=e.stack,t.allowedErrorProps.forEach(r=>{n[r]=e[r]}),n}),wt(Z0,"regexp",e=>""+e,e=>{const t=e.slice(1,e.lastIndexOf("/")),n=e.slice(e.lastIndexOf("/")+1);return new RegExp(t,n)}),wt(Mr,"set",e=>[...e.values()],e=>new Set(e)),wt(Nr,"map",e=>[...e.entries()],e=>new Map(e)),wt(e=>$l(e)||n1(e),"number",e=>$l(e)?"NaN":e>0?"Infinity":"-Infinity",Number),wt(e=>e===0&&1/e===-1/0,"number",()=>"-0",Number),wt(o1,"URL",e=>e.toString(),e=>new URL(e))];function Ko(e,t,n,r){return{isApplicable:e,annotation:t,transform:n,untransform:r}}var Pf=Ko((e,t)=>Of(e)?!!t.symbolRegistry.getIdentifier(e):!1,(e,t)=>["symbol",t.symbolRegistry.getIdentifier(e)],e=>e.description,(e,t,n)=>{const r=n.symbolRegistry.getValue(t[1]);if(!r)throw new Error("Trying to deserialize unknown symbol");return r}),s1=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,Uint8ClampedArray].reduce((e,t)=>(e[t.name]=t,e),{}),If=Ko(r1,e=>["typed-array",e.constructor.name],e=>[...e],(e,t)=>{const n=s1[t[1]];if(!n)throw new Error("Trying to deserialize unknown typed array");return new n(e)});function Rf(e,t){return e!=null&&e.constructor?!!t.classRegistry.getIdentifier(e.constructor):!1}var Lf=Ko(Rf,(e,t)=>["class",t.classRegistry.getIdentifier(e.constructor)],(e,t)=>{const n=t.classRegistry.getAllowedProps(e.constructor);if(!n)return{...e};const r={};return n.forEach(o=>{r[o]=e[o]}),r},(e,t,n)=>{const r=n.classRegistry.getValue(t[1]);if(!r)throw new Error("Trying to deserialize unknown class - check https://github.com/blitz-js/superjson/issues/116#issuecomment-773996564");return Object.assign(Object.create(r.prototype),e)}),Df=Ko((e,t)=>!!t.customTransformerRegistry.findApplicable(e),(e,t)=>["custom",t.customTransformerRegistry.findApplicable(e).name],(e,t)=>t.customTransformerRegistry.findApplicable(e).serialize(e),(e,t,n)=>{const r=n.customTransformerRegistry.findByName(t[1]);if(!r)throw new Error("Trying to deserialize unknown custom value");return r.deserialize(e)}),i1=[Lf,Pf,Df,If],Ul=(e,t)=>{const n=Bl(i1,o=>o.isApplicable(e,t));if(n)return{value:n.transform(e,t),type:n.annotation(e,t)};const r=Bl(kf,o=>o.isApplicable(e,t));if(r)return{value:r.transform(e,t),type:r.annotation}},Vf={};kf.forEach(e=>{Vf[e.annotation]=e});var a1=(e,t,n)=>{if(ln(t))switch(t[0]){case"symbol":return Pf.untransform(e,t,n);case"class":return Lf.untransform(e,t,n);case"custom":return Df.untransform(e,t,n);case"typed-array":return If.untransform(e,t,n);default:throw new Error("Unknown transformation: "+t)}else{const r=Vf[t];if(!r)throw new Error("Unknown transformation: "+t);return r.untransform(e,n)}};P();var In=(e,t)=>{const n=e.keys();for(;t>0;)n.next(),t--;return n.next().value};function Nf(e){if(fo(e,"__proto__"))throw new Error("__proto__ is not allowed as a property");if(fo(e,"prototype"))throw new Error("prototype is not allowed as a property");if(fo(e,"constructor"))throw new Error("constructor is not allowed as a property")}var l1=(e,t)=>{Nf(t);for(let n=0;n{if(Nf(t),t.length===0)return n(e);let r=e;for(let s=0;sjs(s,t,[...n,...Ar(i)]));return}const[r,o]=e;o&&qn(o,(s,i)=>{js(s,t,[...n,...Ar(i)])}),t(r,n)}function u1(e,t,n){return js(t,(r,o)=>{e=Us(e,o,s=>a1(s,r,n))}),e}function c1(e,t){function n(r,o){const s=l1(e,Ar(o));r.map(Ar).forEach(i=>{e=Us(e,i,()=>s)})}if(ln(t)){const[r,o]=t;r.forEach(s=>{e=Us(e,Ar(s),()=>e)}),o&&qn(o,n)}else qn(t,n);return e}var f1=(e,t)=>Vr(e)||ln(e)||Nr(e)||Mr(e)||Rf(e,t);function d1(e,t,n){const r=n.get(e);r?r.push(t):n.set(e,[t])}function p1(e,t){const n={};let r;return e.forEach(o=>{if(o.length<=1)return;t||(o=o.map(a=>a.map(String)).sort((a,l)=>a.length-l.length));const[s,...i]=o;s.length===0?r=i.map(ms):n[ms(s)]=i.map(ms)}),r?$s(n)?[r]:[r,n]:$s(n)?void 0:n}var Mf=(e,t,n,r,o=[],s=[],i=new Map)=>{var a;const l=e1(e);if(!l){d1(e,o,t);const _=i.get(e);if(_)return r?{transformedValue:null}:_}if(!f1(e,n)){const _=Ul(e,n),v=_?{transformedValue:_.value,annotations:[_.type]}:{transformedValue:e};return l||i.set(e,v),v}if(fo(s,e))return{transformedValue:null};const u=Ul(e,n),c=(a=u==null?void 0:u.value)!=null?a:e,f=ln(c)?[]:{},d={};qn(c,(_,v)=>{if(v==="__proto__"||v==="constructor"||v==="prototype")throw new Error(`Detected property ${v}. This is a prototype pollution risk, please remove it from your object.`);const b=Mf(_,t,n,r,[...o,v],[...s,e],i);f[v]=b.transformedValue,ln(b.annotations)?d[v]=b.annotations:Vr(b.annotations)&&qn(b.annotations,(E,A)=>{d[xf(v)+"."+A]=E})});const p=$s(d)?{transformedValue:f,annotations:u?[u.type]:void 0}:{transformedValue:f,annotations:u?[u.type,d]:d};return l||i.set(e,p),p};P();P();function Ff(e){return Object.prototype.toString.call(e).slice(8,-1)}function jl(e){return Ff(e)==="Array"}function h1(e){if(Ff(e)!=="Object")return!1;const t=Object.getPrototypeOf(e);return!!t&&t.constructor===Object&&t===Object.prototype}function m1(e,t,n,r,o){const s={}.propertyIsEnumerable.call(r,t)?"enumerable":"nonenumerable";s==="enumerable"&&(e[t]=n),o&&s==="nonenumerable"&&Object.defineProperty(e,t,{value:n,enumerable:!1,writable:!0,configurable:!0})}function zs(e,t={}){if(jl(e))return e.map(o=>zs(o,t));if(!h1(e))return e;const n=Object.getOwnPropertyNames(e),r=Object.getOwnPropertySymbols(e);return[...n,...r].reduce((o,s)=>{if(jl(t.props)&&!t.props.includes(s))return o;const i=e[s],a=zs(i,t);return m1(o,s,a,e,t.nonenumerable),o},{})}var we=class{constructor({dedupe:e=!1}={}){this.classRegistry=new U0,this.symbolRegistry=new Tf(t=>{var n;return(n=t.description)!=null?n:""}),this.customTransformerRegistry=new K0,this.allowedErrorProps=[],this.dedupe=e}serialize(e){const t=new Map,n=Mf(e,t,this,this.dedupe),r={json:n.transformedValue};n.annotations&&(r.meta={...r.meta,values:n.annotations});const o=p1(t,this.dedupe);return o&&(r.meta={...r.meta,referentialEqualities:o}),r}deserialize(e){const{json:t,meta:n}=e;let r=zs(t);return n!=null&&n.values&&(r=u1(r,n.values,this)),n!=null&&n.referentialEqualities&&(r=c1(r,n.referentialEqualities)),r}stringify(e){return JSON.stringify(this.serialize(e))}parse(e){return this.deserialize(JSON.parse(e))}registerClass(e,t){this.classRegistry.register(e,t)}registerSymbol(e,t){this.symbolRegistry.register(e,t)}registerCustom(e,t){this.customTransformerRegistry.register({name:t,...e})}allowErrorProps(...e){this.allowedErrorProps.push(...e)}};we.defaultInstance=new we;we.serialize=we.defaultInstance.serialize.bind(we.defaultInstance);we.deserialize=we.defaultInstance.deserialize.bind(we.defaultInstance);we.stringify=we.defaultInstance.stringify.bind(we.defaultInstance);we.parse=we.defaultInstance.parse.bind(we.defaultInstance);we.registerClass=we.defaultInstance.registerClass.bind(we.defaultInstance);we.registerSymbol=we.defaultInstance.registerSymbol.bind(we.defaultInstance);we.registerCustom=we.defaultInstance.registerCustom.bind(we.defaultInstance);we.allowErrorProps=we.defaultInstance.allowErrorProps.bind(we.defaultInstance);P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();P();var zl,Kl;(Kl=(zl=X).__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__)!=null||(zl.__VUE_DEVTOOLS_KIT_MESSAGE_CHANNELS__=[]);var Wl,ql;(ql=(Wl=X).__VUE_DEVTOOLS_KIT_RPC_CLIENT__)!=null||(Wl.__VUE_DEVTOOLS_KIT_RPC_CLIENT__=null);var Gl,Yl;(Yl=(Gl=X).__VUE_DEVTOOLS_KIT_RPC_SERVER__)!=null||(Gl.__VUE_DEVTOOLS_KIT_RPC_SERVER__=null);var Xl,Zl;(Zl=(Xl=X).__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__)!=null||(Xl.__VUE_DEVTOOLS_KIT_VITE_RPC_CLIENT__=null);var Jl,Ql;(Ql=(Jl=X).__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__)!=null||(Jl.__VUE_DEVTOOLS_KIT_VITE_RPC_SERVER__=null);var eu,tu;(tu=(eu=X).__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__)!=null||(eu.__VUE_DEVTOOLS_KIT_BROADCAST_RPC_SERVER__=null);P();P();P();P();P();P();P();const g1=JSON.parse(`{"locales":{"/":{"selectLanguageName":"简体中文","selectLanguageText":"选择语言","selectLanguageAriaselectLanguageName":"选择语言","editLink":true,"editLinkText":"在 GitHub 上编辑此页","lastUpdatedText":"上次更新"}},"repo":"Cactus-proj/A-Philosophy-of-Software-Design-zh","docsRepo":"Cactus-proj/A-Philosophy-of-Software-Design-zh","docsBranch":"main","docsDir":"docs","contributors":false,"sidebarDepth":2,"sidebar":"auto","colorMode":"auto","colorModeSwitch":true,"navbar":[],"logo":null,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","editLink":true,"editLinkText":"Edit this page","lastUpdated":true,"lastUpdatedText":"Last Updated","contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),_1=ve(g1),Hf=()=>_1,Bf=Symbol(""),v1=()=>{const e=Je(Bf);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},y1=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},b1=un({enhance({app:e}){const t=Hf(),n=e._context.provides[mi],r=F(()=>y1(t.value,n.routeLocale.value));e.provide(Bf,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}}),L0({app:e,id:"org.vuejs.vuepress.plugin-theme-data",label:"VuePress Theme Data Plugin",packageName:"@vuepress/plugin-theme-data",homepage:"https://v2.vuepress.vuejs.org",logo:"https://v2.vuepress.vuejs.org/images/hero.png",componentStateTypes:["VuePress"]},o=>{o.on.inspectComponent(s=>{s.instanceData.state.push({type:"VuePress",key:"themeData",editable:!1,value:t.value},{type:"VuePress",key:"themeLocaleData",editable:!1,value:r.value})})})}}),E1=Object.freeze(Object.defineProperty({__proto__:null,default:b1},Symbol.toStringTag,{value:"Module"})),S1=()=>Hf(),Be=()=>v1(),$f=Symbol(""),A1=e=>{const t=(n=e.value)=>{const r=window.document.documentElement;r.dataset.theme=n?"dark":"light"};Qe(()=>{je(e,t,{immediate:!0})}),Lo(()=>{t()})},Di=()=>{const e=Je($f);if(!e)throw new Error("useDarkMode() is called without provider.");return e},C1=()=>{const e=Be(),t=o_(),n=Ai("vuepress-color-scheme",e.value.colorMode),r=F({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});An($f,r),A1(r)};let gs=null,cr=null;const T1={wait:()=>gs,pending:()=>{gs=new Promise(e=>{cr=e})},resolve:()=>{cr==null||cr(),gs=null,cr=null}},Uf=()=>T1,Gn=(e,t)=>{const{notFound:n,meta:r,path:o}=Lr(e,t);return n?{text:o,link:o}:{text:r.title||o,link:o}},nu=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),w1=(e,t)=>{if(t.hash===e)return!0;const n=nu(t.path),r=nu(e);return n===r},jf=(e,t)=>e.link&&w1(e.link,t)?!0:"children"in e?e.children.some(n=>jf(n,t)):!1,zf=e=>!em(e)&&!Kr(e),Kf=e=>!No(e)||e.includes("github.com")?"GitHub":e.includes("bitbucket.org")?"Bitbucket":e.includes("gitlab.com")?"GitLab":e.includes("gitee.com")?"Gitee":null,O1={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},x1=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Kf(e);return n!==null?O1[n]:null},k1=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=x1({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,No(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,Tc(`${Cc(n)}/${r}`)):null},Bn=(e="",t="")=>rf(t)||Kr(t)?t:`${um(e)}${t}`,po=ve([]),P1=()=>{const e=wn(),t=Be(),n=Ct(),r=F(()=>n.value.sidebarDepth??t.value.sidebarDepth??2);e.beforeEach((s,i)=>{s.path!==i.path&&(po.value=[])});const o=()=>{if(r.value<=0){po.value=[];return}po.value=b_({levels:[2,r.value+1],ignore:[".vp-badge"]})};je(r,o),Qe(o)},I1=()=>po,R1=e=>({text:e.title,link:e.link,children:Vi(e.children)}),Vi=e=>e?e.map(t=>R1(t)):[],Wf=(e,t)=>[{text:e.title,children:Vi(t)}],qf=(e,t,n,r="")=>{const o=(s,i)=>{var l;const a=bt(s)?Gn(Bn(i,s)):bt(s.link)?{...s,link:zf(s.link)?Gn(Bn(i,s.link)).link:s.link}:s;if("children"in a)return{...a,children:a.children.map(u=>o(u,Bn(i,a.prefix)))};if(a.link===n){const u=((l=t[0])==null?void 0:l.level)===1?t[0].children:t;return{...a,children:Vi(u)}}return a};return e.map(s=>o(s,r))},L1=(e,t,n,r)=>{const o=A_(e).sort((s,i)=>i.length-s.length);for(const s of o)if(nf(decodeURI(r),s)){const i=e[s];return i?i==="heading"?Wf(t,n):qf(i,n,r,s):[]}return console.warn(`${decodeURI(r)} is missing sidebar config.`),[]},Gf=Symbol("sidebarItems"),Ni=()=>{const e=Je(Gf);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},D1=(e,t,n,r,o)=>e===!1?[]:e==="heading"?Wf(t,o):Array.isArray(e)?qf(e,o,n,r):di(e)?L1(e,t,o,n):[],V1=()=>{const e=Be(),t=Ct(),n=er(),r=Wt(),o=Wr(),s=I1(),i=F(()=>t.value.home?!1:t.value.sidebar??e.value.sidebar??"heading"),a=F(()=>D1(i.value,n.value,r.path,o.value,s.value));An(Gf,a)},N1=he({__name:"Badge",props:{type:{default:"tip"},text:{default:""},vertical:{default:void 0}},setup(e,{expose:t}){t();const n={};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),Pe=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n};function M1(e,t,n,r,o,s){return Y(),ne("span",{class:rt(["vp-badge",n.type]),style:Xn({verticalAlign:n.vertical})},[Le(e.$slots,"default",{},()=>[on(Re(n.text),1)])],6)}const F1=Pe(N1,[["render",M1],["__file","Badge.vue"]]),H1=he({__name:"VPHomeFeatures",setup(e,{expose:t}){t();const n=Ct(),r=F(()=>n.value.features??[]),o={frontmatter:n,features:r};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}}),B1={key:0,class:"vp-features"};function $1(e,t,n,r,o,s){return r.features.length?(Y(),ne("div",B1,[(Y(!0),ne(ge,null,nn(r.features,i=>(Y(),ne("div",{key:i.title,class:"vp-feature"},[ie("h2",null,Re(i.title),1),ie("p",null,Re(i.details),1)]))),128))])):He("",!0)}const U1=Pe(H1,[["render",$1],["__file","VPHomeFeatures.vue"]]),j1=he({__name:"VPHomeFooter",setup(e,{expose:t}){t();const n=Ct(),r=F(()=>n.value.footer),o=F(()=>n.value.footerHtml),s={frontmatter:n,footer:r,footerHtml:o};return Object.defineProperty(s,"__isScriptSetup",{enumerable:!1,value:!0}),s}}),z1=["innerHTML"],K1=["textContent"];function W1(e,t,n,r,o,s){return r.footer?(Y(),ne(ge,{key:0},[r.footerHtml?(Y(),ne("div",{key:0,class:"vp-footer","vp-footer":"",innerHTML:r.footer},null,8,z1)):(Y(),ne("div",{key:1,class:"vp-footer","vp-footer":"",textContent:Re(r.footer)},null,8,K1))],64)):He("",!0)}const q1=Pe(j1,[["render",W1],["__file","VPHomeFooter.vue"]]),G1=he({__name:"VPHomeHero",setup(e,{expose:t}){t();const n=Ct(),r=gi(),o=Di(),s=F(()=>n.value.heroText===null?null:n.value.heroText||r.value.title||"Hello"),i=F(()=>n.value.tagline===null?null:n.value.tagline||r.value.description||"Welcome to your VuePress site"),a=F(()=>o.value&&n.value.heroImageDark!==void 0?n.value.heroImageDark:n.value.heroImage),l=F(()=>n.value.heroAlt||s.value||"hero"),u=F(()=>n.value.heroHeight??280),c=F(()=>Array.isArray(n.value.actions)?n.value.actions.map(({text:p,link:_,type:v="primary"})=>({text:p,link:_,type:v})):[]),d={frontmatter:n,siteLocale:r,isDarkMode:o,heroText:s,tagline:i,heroImage:a,heroAlt:l,heroHeight:u,actions:c,HomeHeroImage:()=>{if(!a.value)return null;const p=_e("img",{class:"vp-hero-image",src:yi(a.value),alt:l.value,height:u.value});return n.value.heroImageDark===void 0?p:_e(_i,()=>p)},get AutoLink(){return tr}};return Object.defineProperty(d,"__isScriptSetup",{enumerable:!1,value:!0}),d}}),Y1={class:"vp-hero"},X1={key:0,id:"main-title"},Z1={key:1,class:"vp-hero-description"},J1={key:2,class:"vp-hero-actions"};function Q1(e,t,n,r,o,s){return Y(),ne("header",Y1,[ae(r.HomeHeroImage),r.heroText?(Y(),ne("h1",X1,Re(r.heroText),1)):He("",!0),r.tagline?(Y(),ne("p",Z1,Re(r.tagline),1)):He("",!0),r.actions.length?(Y(),ne("p",J1,[(Y(!0),ne(ge,null,nn(r.actions,i=>(Y(),De(r.AutoLink,{key:i.text,class:rt(["vp-hero-action-button",[i.type]]),config:i},null,8,["class","config"]))),128))])):He("",!0)])}const ey=Pe(G1,[["render",Q1],["__file","VPHomeHero.vue"]]),ty=he({__name:"VPHome",setup(e,{expose:t}){t();const n={VPHomeFeatures:U1,VPHomeFooter:q1,VPHomeHero:ey,get Content(){return vi}};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),ny={class:"vp-home"},ry={class:"theme-default-content","vp-content":""};function oy(e,t,n,r,o,s){return Y(),ne("main",ny,[ae(r.VPHomeHero),ae(r.VPHomeFeatures),ie("div",ry,[ae(r.Content)]),ae(r.VPHomeFooter)])}const sy=Pe(ty,[["render",oy],["__file","VPHome.vue"]]),iy=he({__name:"VPNavbarBrand",setup(e,{expose:t}){t();const n=Wr(),r=gi(),o=Be(),s=Di(),i=F(()=>o.value.home||n.value),a=F(()=>r.value.title),l=F(()=>s.value&&o.value.logoDark!==void 0?o.value.logoDark:o.value.logo),u=F(()=>o.value.logoAlt??a.value),c=F(()=>a.value.toLocaleUpperCase().trim()===u.value.toLocaleUpperCase().trim()),d={routeLocale:n,siteLocale:r,themeLocale:o,isDarkMode:s,navbarBrandLink:i,navbarBrandTitle:a,navbarBrandLogo:l,navbarBrandLogoAlt:u,navBarLogoAltMatchesTitle:c,NavbarBrandLogo:()=>{if(!l.value)return null;const p=_e("img",{class:"vp-site-logo",src:yi(l.value),alt:u.value});return o.value.logoDark===void 0?p:_e(_i,()=>p)},get RouteLink(){return Ho}};return Object.defineProperty(d,"__isScriptSetup",{enumerable:!1,value:!0}),d}}),ay=["aria-hidden"];function ly(e,t,n,r,o,s){return Y(),De(r.RouteLink,{to:r.navbarBrandLink},{default:Me(()=>[ae(r.NavbarBrandLogo),r.navbarBrandTitle?(Y(),ne("span",{key:0,class:rt(["vp-site-name",{"vp-hide-mobile":r.navbarBrandLogo}]),"aria-hidden":r.navBarLogoAltMatchesTitle},Re(r.navbarBrandTitle),11,ay)):He("",!0)]),_:1},8,["to"])}const uy=Pe(iy,[["render",ly],["__file","VPNavbarBrand.vue"]]),cy=he({__name:"VPDropdownTransition",setup(e,{expose:t}){t();const o={setHeight:s=>{s.style.height=`${s.scrollHeight}px`},unsetHeight:s=>{s.style.height=""}};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}});function fy(e,t,n,r,o,s){return Y(),De(fi,{name:"vp-dropdown",onEnter:r.setHeight,onAfterEnter:r.unsetHeight,onBeforeLeave:r.setHeight},{default:Me(()=>[Le(e.$slots,"default")]),_:3})}const Yf=Pe(cy,[["render",fy],["__file","VPDropdownTransition.vue"]]),dy=he({__name:"VPNavbarDropdown",props:{config:{}},setup(e,{expose:t}){t();const n=e,{config:r}=ku(n),o=Wt(),[s,i]=Zc(!1),a=F(()=>r.value.ariaLabel||r.value.text),l=(f,d)=>d[d.length-1]===f,u=f=>{f.detail===0?i():i(!1)};je(()=>o.path,()=>{i(!1)});const c={props:n,config:r,route:o,open:s,toggleOpen:i,dropdownAriaLabel:a,isLastItemOfArray:l,handleDropdown:u,VPDropdownTransition:Yf,get AutoLink(){return tr}};return Object.defineProperty(c,"__isScriptSetup",{enumerable:!1,value:!0}),c}}),py=["aria-label"],hy={class:"title"},my=["aria-label"],gy={class:"title"},_y={class:"vp-navbar-dropdown"},vy={class:"vp-navbar-dropdown-subtitle"},yy={key:1},by={class:"vp-navbar-dropdown-subitem-wrapper"};function Ey(e,t,n,r,o,s){return Y(),ne("div",{class:rt(["vp-navbar-dropdown-wrapper",{open:r.open}])},[ie("button",{class:"vp-navbar-dropdown-title",type:"button","aria-label":r.dropdownAriaLabel,onClick:r.handleDropdown},[ie("span",hy,Re(r.config.text),1),t[1]||(t[1]=ie("span",{class:"arrow down"},null,-1))],8,py),ie("button",{class:"vp-navbar-dropdown-title-mobile",type:"button","aria-label":r.dropdownAriaLabel,onClick:t[0]||(t[0]=()=>r.toggleOpen())},[ie("span",gy,Re(r.config.text),1),ie("span",{class:rt(["arrow",r.open?"down":"right"])},null,2)],8,my),ae(r.VPDropdownTransition,null,{default:Me(()=>[yo(ie("ul",_y,[(Y(!0),ne(ge,null,nn(r.config.children,i=>(Y(),ne("li",{key:i.text,class:"vp-navbar-dropdown-item"},["children"in i?(Y(),ne(ge,{key:0},[ie("h4",vy,[i.link?(Y(),De(r.AutoLink,{key:0,config:i,onFocusout:()=>{r.isLastItemOfArray(i,r.config.children)&&i.children.length===0&&(r.open=!1)}},null,8,["config","onFocusout"])):(Y(),ne("span",yy,Re(i.text),1))]),ie("ul",by,[(Y(!0),ne(ge,null,nn(i.children,a=>(Y(),ne("li",{key:a.link,class:"vp-navbar-dropdown-subitem"},[ae(r.AutoLink,{config:a,onFocusout:()=>{r.isLastItemOfArray(a,i.children)&&r.isLastItemOfArray(i,r.config.children)&&r.toggleOpen(!1)}},null,8,["config","onFocusout"])]))),128))])],64)):(Y(),De(r.AutoLink,{key:1,config:i,onFocusout:()=>{r.isLastItemOfArray(i,r.config.children)&&r.toggleOpen(!1)}},null,8,["config","onFocusout"]))]))),128))],512),[[Ao,r.open]])]),_:1})],2)}const Sy=Pe(dy,[["render",Ey],["__file","VPNavbarDropdown.vue"]]),Xf=(e,t="")=>bt(e)?Gn(Bn(t,e)):"children"in e?{...e,children:e.children.map(n=>Xf(n,Bn(t,e.prefix)))}:{...e,link:zf(e.link)?Gn(Bn(t,e.link)).link:e.link},Ay=()=>{const e=Be();return F(()=>(e.value.navbar||[]).map(t=>Xf(t)))},Cy=()=>{const e=Be(),t=F(()=>e.value.repo),n=F(()=>t.value?Kf(t.value):null),r=F(()=>t.value&&!No(t.value)?`https://github.com/${t.value}`:t.value),o=F(()=>r.value?e.value.repoLabel?e.value.repoLabel:n.value===null?"Source":n.value:null);return F(()=>!r.value||!o.value?[]:[{text:o.value,link:r.value}])},Ty=()=>{const e=Wt(),t=E_(),n=Wr(),r=qc(),o=gi(),s=S1(),i=Be();return F(()=>{const a=Object.keys(r.value.locales);if(a.length<2)return[];const l=e.path,u=e.fullPath;return[{text:`${i.value.selectLanguageText}`,ariaLabel:`${i.value.selectLanguageAriaLabel??i.value.selectLanguageText}`,children:a.map(f=>{var E,A;const d=((E=r.value.locales)==null?void 0:E[f])??{},p=((A=s.value.locales)==null?void 0:A[f])??{},_=`${d.lang}`,v=p.selectLanguageName??_;if(_===o.value.lang)return{text:v,activeMatch:".",link:e.fullPath};const b=l.replace(n.value,f);return{text:v,link:t.value.some(m=>m===b)?u.replace(l,b):p.home??f}})}]})},wy="719px",Oy={mobile:wy};var Fr;(function(e){e.Mobile="mobile"})(Fr||(Fr={}));const xy={[Fr.Mobile]:Number.parseInt(Oy.mobile.replace("px",""),10)},Zf=(e,t)=>{const n=xy[e];Number.isInteger(n)&&(dt("orientationchange",()=>{t(n)},!1),dt("resize",()=>{t(n)},!1),Qe(()=>{t(n)}))},ky=he({__name:"VPNavbarItems",setup(e,{expose:t}){t();const n=Ay(),r=Ty(),o=Cy(),s=ve(!1),i=F(()=>Be().value.navbarLabel??"site navigation"),a=F(()=>[...n.value,...r.value,...o.value]);Zf(Fr.Mobile,u=>{s.value=window.innerWidth(Y(),ne("div",{key:i.text,class:"vp-navbar-item"},["children"in i?(Y(),De(r.VPNavbarDropdown,{key:0,class:rt({mobile:r.isMobile}),config:i},null,8,["class","config"])):(Y(),De(r.AutoLink,{key:1,config:i},null,8,["config"]))]))),128))],8,Py)):He("",!0)}const Jf=Pe(ky,[["render",Iy],["__file","VPNavbarItems.vue"]]),Ry={},Ly={class:"dark-icon",viewBox:"0 0 32 32"};function Dy(e,t){return Y(),ne("svg",Ly,t[0]||(t[0]=[ie("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1)]))}const Vy=Pe(Ry,[["render",Dy],["__file","VPDarkIcon.vue"]]),Ny={},My={class:"light-icon",viewBox:"0 0 32 32"};function Fy(e,t){return Y(),ne("svg",My,t[0]||(t[0]=[ch('',9)]))}const Hy=Pe(Ny,[["render",Fy],["__file","VPLightIcon.vue"]]),By=he({__name:"VPToggleColorModeButton",setup(e,{expose:t}){t();const n=Be(),r=Di(),s={themeLocale:n,isDarkMode:r,toggleColorMode:()=>{r.value=!r.value},VPDarkIcon:Vy,VPLightIcon:Hy};return Object.defineProperty(s,"__isScriptSetup",{enumerable:!1,value:!0}),s}}),$y=["title"];function Uy(e,t,n,r,o,s){return Y(),ne("button",{type:"button",class:"vp-toggle-color-mode-button",title:r.themeLocale.toggleColorMode,onClick:r.toggleColorMode},[yo(ae(r.VPLightIcon,null,null,512),[[Ao,!r.isDarkMode]]),yo(ae(r.VPDarkIcon,null,null,512),[[Ao,r.isDarkMode]])],8,$y)}const jy=Pe(By,[["render",Uy],["__file","VPToggleColorModeButton.vue"]]),zy=he({__name:"VPToggleSidebarButton",emits:["toggle"],setup(e,{expose:t}){t();const r={themeLocale:Be()};return Object.defineProperty(r,"__isScriptSetup",{enumerable:!1,value:!0}),r}}),Ky=["title"];function Wy(e,t,n,r,o,s){return Y(),ne("div",{class:"vp-toggle-sidebar-button",title:r.themeLocale.toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:t[0]||(t[0]=i=>e.$emit("toggle"))},t[1]||(t[1]=[ie("div",{class:"icon","aria-hidden":"true"},[ie("span"),ie("span"),ie("span")],-1)]),8,Ky)}const qy=Pe(zy,[["render",Wy],["__file","VPToggleSidebarButton.vue"]]),Gy=he({__name:"VPNavbar",emits:["toggleSidebar"],setup(e,{expose:t}){t();const n=Qc("SearchBox")?ii("SearchBox"):()=>null,r=Be(),o=Wi("navbar"),s=Wi("navbar-brand"),i=ve(0),a=F(()=>i.value?{maxWidth:`${i.value}px`}:{}),l=(c,f)=>{var _;const d=(_=c==null?void 0:c.ownerDocument.defaultView)==null?void 0:_.getComputedStyle(c,null)[f],p=Number.parseInt(d,10);return Number.isNaN(p)?0:p};Zf(Fr.Mobile,c=>{var d;const f=l(o.value,"paddingLeft")+l(o.value,"paddingRight");window.innerWidthe.$emit("toggleSidebar"))}),ie("span",Xy,[ae(r.VPNavbarBrand)],512),ie("div",{class:"vp-navbar-items-wrapper",style:Xn(r.linksWrapperStyle)},[Le(e.$slots,"before"),ae(r.VPNavbarItems,{class:"vp-hide-mobile"}),Le(e.$slots,"after"),r.themeLocale.colorModeSwitch?(Y(),De(r.VPToggleColorModeButton,{key:0})):He("",!0),ae(r.SearchBox)],4)],512)}const Jy=Pe(Gy,[["render",Zy],["__file","VPNavbar.vue"]]),Qy={},eb={class:"edit-icon",viewBox:"0 0 1024 1024"};function tb(e,t){return Y(),ne("svg",eb,t[0]||(t[0]=[ie("g",{fill:"currentColor"},[ie("path",{d:"M430.818 653.65a60.46 60.46 0 0 1-50.96-93.281l71.69-114.012 7.773-10.365L816.038 80.138A60.46 60.46 0 0 1 859.225 62a60.46 60.46 0 0 1 43.186 18.138l43.186 43.186a60.46 60.46 0 0 1 0 86.373L588.879 565.55l-8.637 8.637-117.466 68.234a60.46 60.46 0 0 1-31.958 11.229z"}),ie("path",{d:"M728.802 962H252.891A190.883 190.883 0 0 1 62.008 771.98V296.934a190.883 190.883 0 0 1 190.883-192.61h267.754a60.46 60.46 0 0 1 0 120.92H252.891a69.962 69.962 0 0 0-69.098 69.099V771.98a69.962 69.962 0 0 0 69.098 69.098h475.911A69.962 69.962 0 0 0 797.9 771.98V503.363a60.46 60.46 0 1 1 120.922 0V771.98A190.883 190.883 0 0 1 728.802 962z"})],-1)]))}const nb=Pe(Qy,[["render",tb],["__file","VPEditIcon.vue"]]),rb=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{var o;return n.value.contributors??e.value.contributors??!0?((o=t.value.git)==null?void 0:o.contributors)??null:null})},ob=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{if(!(n.value.editLink??e.value.editLink??!0))return null;const{repo:o,docsRepo:s=o,docsBranch:i="main",docsDir:a="",editLinkText:l}=e.value;if(!s)return null;const u=k1({docsRepo:s,docsBranch:i,docsDir:a,filePathRelative:t.value.filePathRelative,editLinkPattern:n.value.editLinkPattern??e.value.editLinkPattern});return u?{text:l??"Edit this page",link:u}:null})},sb=()=>{const e=Be(),t=er(),n=Ct();return F(()=>{var s;return!(n.value.lastUpdated??e.value.lastUpdated??!0)||!((s=t.value.git)!=null&&s.updatedTime)?null:new Date(t.value.git.updatedTime).toLocaleString()})},ib=he({__name:"VPPageMeta",setup(e,{expose:t}){t();const n=Be(),r=ob(),o=sb(),s=rb(),i={themeLocale:n,editLink:r,lastUpdated:o,contributors:s,VPEditIcon:nb,get AutoLink(){return tr}};return Object.defineProperty(i,"__isScriptSetup",{enumerable:!1,value:!0}),i}}),ab={class:"vp-page-meta"},lb={key:0,class:"vp-meta-item edit-link"},ub={class:"vp-meta-item git-info"},cb={key:0,class:"vp-meta-item last-updated"},fb={class:"meta-item-label"},db={class:"meta-item-info"},pb={key:1,class:"vp-meta-item contributors"},hb={class:"meta-item-label"},mb={class:"meta-item-info"},gb=["title"];function _b(e,t,n,r,o,s){const i=ii("ClientOnly");return Y(),ne("footer",ab,[r.editLink?(Y(),ne("div",lb,[ae(r.AutoLink,{class:"label",config:r.editLink},{before:Me(()=>[ae(r.VPEditIcon)]),_:1},8,["config"])])):He("",!0),ie("div",ub,[r.lastUpdated?(Y(),ne("div",cb,[ie("span",fb,Re(r.themeLocale.lastUpdatedText)+": ",1),ae(i,null,{default:Me(()=>[ie("span",db,Re(r.lastUpdated),1)]),_:1})])):He("",!0),r.contributors&&r.contributors.length?(Y(),ne("div",pb,[ie("span",hb,Re(r.themeLocale.contributorsText)+": ",1),ie("span",mb,[(Y(!0),ne(ge,null,nn(r.contributors,(a,l)=>(Y(),ne(ge,{key:l},[ie("span",{class:"contributor",title:`email: ${a.email}`},Re(a.name),9,gb),l!==r.contributors.length-1?(Y(),ne(ge,{key:0},[on(", ")],64)):He("",!0)],64))),128))])])):He("",!0)])])}const vb=Pe(ib,[["render",_b],["__file","VPPageMeta.vue"]]),yb=()=>{const e=wn(),t=Wt();return n=>{n&&(rf(n)?t.path!==n&&e.push(n):Kr(n)?window.open(n):e.push(encodeURI(n)))}},ru=(e,t)=>e===!1?!1:bt(e)?Gn(e,t):di(e)?{...e,link:Gn(e.link,t).link}:null,Ks=(e,t,n)=>{const r=e.findIndex(s=>s.link===t);if(r!==-1){const s=e[r+n];return s?s.link?s:"prefix"in s&&!Lr(s.prefix).notFound?{...s,link:s.prefix}:null:null}for(const s of e)if("children"in s){const i=Ks(s.children,t,n);if(i)return i}const o=e.findIndex(s=>"prefix"in s&&s.prefix===t);if(o!==-1){const s=e[o+n];return s?s.link?s:"prefix"in s&&!Lr(s.prefix).notFound?{...s,link:s.prefix}:null:null}return null},bb=()=>{const e=Ct(),t=Be(),n=Ni(),r=Wt(),o=F(()=>{const i=ru(e.value.prev,r.path);return i===!1?null:i??(t.value.prev===!1?null:Ks(n.value,r.path,-1))}),s=F(()=>{const i=ru(e.value.next,r.path);return i===!1?null:i??(t.value.next===!1?null:Ks(n.value,r.path,1))});return{prevLink:o,nextLink:s}},Eb=he({__name:"VPPageNav",setup(e,{expose:t}){t();const n=Be(),r=yb(),{prevLink:o,nextLink:s}=bb(),i=F(()=>n.value.pageNavbarLabel??"page navigation");dt("keydown",l=>{l.altKey&&(l.key==="ArrowRight"?s.value&&(r(s.value.link),l.preventDefault()):l.key==="ArrowLeft"&&o.value&&(r(o.value.link),l.preventDefault()))});const a={themeLocale:n,navigate:r,prevLink:o,nextLink:s,navbarLabel:i,get AutoLink(){return tr}};return Object.defineProperty(a,"__isScriptSetup",{enumerable:!1,value:!0}),a}}),Sb=["aria-label"],Ab={class:"hint"},Cb={class:"link"},Tb={class:"hint"},wb={class:"link"};function Ob(e,t,n,r,o,s){return r.prevLink||r.nextLink?(Y(),ne("nav",{key:0,class:"vp-page-nav","aria-label":r.navbarLabel},[r.prevLink?(Y(),De(r.AutoLink,{key:0,class:"prev",config:r.prevLink},{default:Me(()=>[ie("div",Ab,[t[0]||(t[0]=ie("span",{class:"arrow left"},null,-1)),on(" "+Re(r.themeLocale.prev??"Prev"),1)]),ie("div",Cb,[ie("span",null,Re(r.prevLink.text),1)])]),_:1},8,["config"])):He("",!0),r.nextLink?(Y(),De(r.AutoLink,{key:1,class:"next",config:r.nextLink},{default:Me(()=>[ie("div",Tb,[on(Re(r.themeLocale.next??"Next")+" ",1),t[1]||(t[1]=ie("span",{class:"arrow right"},null,-1))]),ie("div",wb,[ie("span",null,Re(r.nextLink.text),1)])]),_:1},8,["config"])):He("",!0)],8,Sb)):He("",!0)}const xb=Pe(Eb,[["render",Ob],["__file","VPPageNav.vue"]]),kb=he({__name:"VPPage",setup(e,{expose:t}){t(),P1();const n={VPPageMeta:vb,VPPageNav:xb,get Content(){return vi}};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),Pb={class:"vp-page"},Ib={class:"theme-default-content","vp-content":""};function Rb(e,t,n,r,o,s){return Y(),ne("main",Pb,[Le(e.$slots,"top"),ie("div",Ib,[Le(e.$slots,"content-top"),ae(r.Content),Le(e.$slots,"content-bottom")]),ae(r.VPPageMeta),ae(r.VPPageNav),Le(e.$slots,"bottom")])}const Lb=Pe(kb,[["render",Rb],["__file","VPPage.vue"]]),Db=he({__name:"VPSidebarItem",props:{item:{},depth:{default:0}},setup(e,{expose:t}){t();const n=e,{item:r,depth:o}=ku(n),s=Wt(),i=wn(),a=F(()=>"collapsible"in r.value&&r.value.collapsible),l=F(()=>jf(r.value,s)),u=F(()=>({"vp-sidebar-item":!0,"vp-sidebar-heading":o.value===0,active:l.value,collapsible:a.value})),c=F(()=>a.value?l.value:!0),[f,d]=Zc(c.value),p=b=>{a.value&&(b.preventDefault(),d())},_=i.afterEach(()=>{Jn(()=>{f.value=c.value})});si(()=>{_()});const v={props:n,item:r,depth:o,route:s,router:i,collapsible:a,isActive:l,itemClass:u,isOpenDefault:c,isOpen:f,toggleIsOpen:d,onClick:p,unregisterRouterHook:_,VPDropdownTransition:Yf,get AutoLink(){return tr}};return Object.defineProperty(v,"__isScriptSetup",{enumerable:!1,value:!0}),v}}),Vb={class:"vp-sidebar-children"};function Nb(e,t,n,r,o,s){const i=ii("VPSidebarItem",!0);return Y(),ne("li",null,[r.item.link?(Y(),De(r.AutoLink,{key:0,class:rt(r.itemClass),config:r.item},null,8,["class","config"])):(Y(),ne("p",{key:1,tabindex:"0",class:rt(r.itemClass),onClick:r.onClick,onKeydown:qh(r.onClick,["enter"])},[on(Re(r.item.text)+" ",1),r.collapsible?(Y(),ne("span",{key:0,class:rt(["arrow",r.isOpen?"down":"right"])},null,2)):He("",!0)],34)),"children"in r.item&&r.item.children.length?(Y(),De(r.VPDropdownTransition,{key:2},{default:Me(()=>[yo(ie("ul",Vb,[(Y(!0),ne(ge,null,nn(r.item.children,a=>(Y(),De(i,{key:`${r.depth}${a.text}${a.link}`,item:a,depth:r.depth+1},null,8,["item","depth"]))),128))],512),[[Ao,r.isOpen]])]),_:1})):He("",!0)])}const Mb=Pe(Db,[["render",Nb],["__file","VPSidebarItem.vue"]]),Fb=he({__name:"VPSidebarItems",setup(e,{expose:t}){t();const n=Wt(),r=Ni();Qe(()=>{je(()=>n.hash,s=>{const i=document.querySelector(".vp-sidebar");if(!i)return;const a=document.querySelector(`.vp-sidebar a.vp-sidebar-item[href="${n.path}${s}"]`);if(!a)return;const{top:l,height:u}=i.getBoundingClientRect(),{top:c,height:f}=a.getBoundingClientRect();cl+u&&a.scrollIntoView(!1)})});const o={route:n,sidebarItems:r,VPSidebarItem:Mb};return Object.defineProperty(o,"__isScriptSetup",{enumerable:!1,value:!0}),o}}),Hb={key:0,class:"vp-sidebar-items"};function Bb(e,t,n,r,o,s){return r.sidebarItems.length?(Y(),ne("ul",Hb,[(Y(!0),ne(ge,null,nn(r.sidebarItems,i=>(Y(),De(r.VPSidebarItem,{key:`${i.text}${i.link}`,item:i},null,8,["item"]))),128))])):He("",!0)}const $b=Pe(Fb,[["render",Bb],["__file","VPSidebarItems.vue"]]),Ub=he({__name:"VPSidebar",setup(e,{expose:t}){t();const n={VPNavbarItems:Jf,VPSidebarItems:$b};return Object.defineProperty(n,"__isScriptSetup",{enumerable:!1,value:!0}),n}}),jb={class:"vp-sidebar","vp-sidebar":""};function zb(e,t,n,r,o,s){return Y(),ne("aside",jb,[ae(r.VPNavbarItems),Le(e.$slots,"top"),ae(r.VPSidebarItems),Le(e.$slots,"bottom")])}const Kb=Pe(Ub,[["render",zb],["__file","VPSidebar.vue"]]),Wb=he({__name:"Layout",setup(e,{expose:t}){t();const n=er(),r=Ct(),o=Be(),s=F(()=>r.value.navbar!==!1&&o.value.navbar!==!1),i=Ni(),a=ve(!1),l=m=>{a.value=typeof m=="boolean"?m:!a.value},u={x:0,y:0},c=m=>{u.x=m.changedTouches[0].clientX,u.y=m.changedTouches[0].clientY},f=m=>{const y=m.changedTouches[0].clientX-u.x,L=m.changedTouches[0].clientY-u.y;Math.abs(y)>Math.abs(L)&&Math.abs(y)>40&&(y>0&&u.x<=80?l(!0):l(!1))},d=F(()=>r.value.externalLinkIcon??o.value.externalLinkIcon??!0),p=F(()=>[{"no-navbar":!s.value,"no-sidebar":!i.value.length,"sidebar-open":a.value,"external-link-icon":d.value},r.value.pageClass]);let _;Qe(()=>{_=wn().afterEach(()=>{l(!1)})}),Lo(()=>{_()});const v=Uf(),b=v.resolve,E=v.pending,A={page:n,frontmatter:r,themeLocale:o,shouldShowNavbar:s,sidebarItems:i,isSidebarOpen:a,toggleSidebar:l,touchStart:u,onTouchStart:c,onTouchEnd:f,enableExternalLinkIcon:d,containerClass:p,get unregisterRouterHook(){return _},set unregisterRouterHook(m){_=m},scrollPromise:v,onBeforeEnter:b,onBeforeLeave:E,VPHome:sy,VPNavbar:Jy,VPPage:Lb,VPSidebar:Kb};return Object.defineProperty(A,"__isScriptSetup",{enumerable:!1,value:!0}),A}});function qb(e,t,n,r,o,s){return Y(),ne("div",{class:rt(["vp-theme-container",r.containerClass]),"vp-container":"",onTouchstart:r.onTouchStart,onTouchend:r.onTouchEnd},[Le(e.$slots,"navbar",{},()=>[r.shouldShowNavbar?(Y(),De(r.VPNavbar,{key:0,onToggleSidebar:r.toggleSidebar},{before:Me(()=>[Le(e.$slots,"navbar-before")]),after:Me(()=>[Le(e.$slots,"navbar-after")]),_:3})):He("",!0)]),ie("div",{class:"vp-sidebar-mask",onClick:t[0]||(t[0]=i=>r.toggleSidebar(!1))}),Le(e.$slots,"sidebar",{},()=>[ae(r.VPSidebar,null,{top:Me(()=>[Le(e.$slots,"sidebar-top")]),bottom:Me(()=>[Le(e.$slots,"sidebar-bottom")]),_:3})]),Le(e.$slots,"page",{},()=>[r.frontmatter.home?(Y(),De(r.VPHome,{key:0})):(Y(),De(fi,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:r.onBeforeEnter,onBeforeLeave:r.onBeforeLeave},{default:Me(()=>[(Y(),De(r.VPPage,{key:r.page.path},{top:Me(()=>[Le(e.$slots,"page-top")]),"content-top":Me(()=>[Le(e.$slots,"page-content-top")]),"content-bottom":Me(()=>[Le(e.$slots,"page-content-bottom")]),bottom:Me(()=>[Le(e.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34)}const Gb=Pe(Wb,[["render",qb],["__file","Layout.vue"]]),Yb=he({__name:"NotFound",setup(e,{expose:t}){t();const n=Wr(),r=Be(),o=r.value.notFound??["Not Found"],s=()=>o[Math.floor(Math.random()*o.length)],i=r.value.home??n.value,a=r.value.backToHome??"Back to home",l={routeLocale:n,themeLocale:r,messages:o,getMsg:s,homeLink:i,homeText:a,get RouteLink(){return Ho}};return Object.defineProperty(l,"__isScriptSetup",{enumerable:!1,value:!0}),l}}),Xb={class:"vp-theme-container","vp-container":""},Zb={class:"page"},Jb={class:"theme-default-content","vp-content":""};function Qb(e,t,n,r,o,s){return Y(),ne("div",Xb,[ie("main",Zb,[ie("div",Jb,[t[0]||(t[0]=ie("h1",null,"404",-1)),ie("blockquote",null,Re(r.getMsg()),1),ae(r.RouteLink,{to:r.homeLink},{default:Me(()=>[on(Re(r.homeText),1)]),_:1},8,["to"])])])])}const eE=Pe(Yb,[["render",Qb],["__scopeId","data-v-67c08c1d"],["__file","NotFound.vue"]]),tE=un({enhance({app:e,router:t}){Qc("Badge")||e.component("Badge",F1);const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Uf().wait(),n(...r))},setup(){C1(),V1()},layouts:{Layout:Gb,NotFound:eE}}),nE=Object.freeze(Object.defineProperty({__proto__:null,default:tE},Symbol.toStringTag,{value:"Module"})),io=[__,O_,D_,N_,Y_,tv,ov,cv,E1,nE].map(e=>e.default).filter(Boolean),rE=JSON.parse('{"base":"/A-Philosophy-of-Software-Design-zh/","lang":"en-US","title":"","description":"","head":[],"locales":{"/":{"lang":"zh-CN","title":"《软件设计的哲学》","description":"《软件设计的哲学》中文翻译"}}}');var mr=Et(rE),oE=Xm,sE=()=>{const e=Ig({history:oE(Cc("/A-Philosophy-of-Software-Design-zh/")),routes:[{name:"vuepress-route",path:"/:catchAll(.*)",components:{}}],scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{if(t.path!==n.path||n===Nt){const r=Lr(t.fullPath);if(r.path!==t.fullPath)return r.path;const o=await r.loader();t.meta={...r.meta,_pageChunk:o}}else t.path===n.path&&(t.meta=n.meta)}),e},iE=e=>{e.component("ClientOnly",_i),e.component("Content",vi),e.component("RouteLink",Ho)},aE=(e,t,n)=>{const r=F(()=>t.currentRoute.value.path),o=zd((b,E)=>({get(){return b(),t.currentRoute.value.meta._pageChunk},set(A){t.currentRoute.value.meta._pageChunk=A,E()}})),s=F(()=>hn.resolveLayouts(n)),i=F(()=>hn.resolveRouteLocale(mr.value.locales,r.value)),a=F(()=>hn.resolveSiteLocaleData(mr.value,i.value)),l=F(()=>o.value.comp),u=F(()=>o.value.data),c=F(()=>u.value.frontmatter),f=F(()=>hn.resolvePageHeadTitle(u.value,a.value)),d=F(()=>hn.resolvePageHead(f.value,c.value,a.value)),p=F(()=>hn.resolvePageLang(u.value,a.value)),_=F(()=>hn.resolvePageLayout(u.value,s.value)),v={layouts:s,pageData:u,pageComponent:l,pageFrontmatter:c,pageHead:d,pageHeadTitle:f,pageLang:p,pageLayout:_,redirects:Ds,routeLocale:i,routePath:r,routes:Hn,siteData:mr,siteLocaleData:a};return e.provide(mi,v),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>c.value},$head:{get:()=>d.value},$headTitle:{get:()=>f.value},$lang:{get:()=>p.value},$page:{get:()=>u.value},$routeLocale:{get:()=>i.value},$site:{get:()=>mr.value},$siteLocale:{get:()=>a.value},$withBase:{get:()=>yi}}),v},lE=([e,t,n=""])=>{const r=Object.entries(t).map(([a,l])=>bt(l)?`[${a}=${JSON.stringify(l)}]`:l?`[${a}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(a=>a.innerText===n)??null},uE=([e,t,n])=>{if(!bt(e))return null;const r=document.createElement(e);return di(t)&&Object.entries(t).forEach(([o,s])=>{bt(s)?r.setAttribute(o,s):s&&r.setAttribute(o,"")}),bt(n)&&r.appendChild(document.createTextNode(n)),r},cE=()=>{const e=Dg(),t=Vg();let n=[];const r=()=>{e.value.forEach(i=>{const a=lE(i);a&&n.push(a)})},o=()=>{const i=[];return e.value.forEach(a=>{const l=uE(a);l&&i.push(l)}),i},s=()=>{document.documentElement.lang=t.value;const i=o();n.forEach((a,l)=>{const u=i.findIndex(c=>a.isEqualNode(c));u===-1?(a.remove(),delete n[l]):i.splice(u,1)}),i.forEach(a=>document.head.appendChild(a)),n=[...n.filter(a=>!!a),...i]};An(Fg,s),Qe(()=>{r(),je(e,s,{immediate:!1})})},fE=Xh,dE=async()=>{var r;const e=fE({name:"Vuepress",setup(){var i;cE();for(const a of io)(i=a.setup)==null||i.call(a);const o=io.flatMap(({rootComponents:a=[]})=>a.map(l=>_e(l))),s=Ng();return()=>[_e(s.value),o]}}),t=sE();iE(e);const n=aE(e,t,io);{const{setupDevtools:o}=await Ce(async()=>{const{setupDevtools:s}=await import("./setupDevtools-7MC2TMWH-Cw84T5yU.js");return{setupDevtools:s}},[]);o(e,n)}for(const o of io)await((r=o.enhance)==null?void 0:r.call(o,{app:e,router:t,siteData:mr}));return e.use(t),{app:e,router:t}};dE().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Pe as _,ie as a,ae as b,ne as c,dE as createVueApp,Me as d,on as e,ch as f,Y as o,ii as r,L0 as s,je as w}; diff --git a/assets/ch01.html-D6ExKKlk.js b/assets/ch01.html-BM9hxgpH.js similarity index 99% rename from assets/ch01.html-D6ExKKlk.js rename to assets/ch01.html-BM9hxgpH.js index c1da2b73..fa1fdcd5 100644 --- a/assets/ch01.html-D6ExKKlk.js +++ b/assets/ch01.html-BM9hxgpH.js @@ -1 +1 @@ -import{_ as t,c as o,f as s,o as a}from"./app-CvqtBB8Z.js";const i={};function n(r,e){return a(),o("div",null,e[0]||(e[0]=[s('

第 1 章 介绍

Chapter 1 Introduction (It’s All About Complexity)

Writing computer software is one of the purest creative activities in the history of the human race. Programmers aren’t bound by practical limitations such as the laws of physics; we can create exciting virtual worlds with behaviors that could never exist in the real world. Programming doesn’t require great physical skill or coordination, like ballet or basketball. All programming requires is a creative mind and the ability to organize your thoughts. If you can visualize a system, you can probably implement it in a computer program.

编写计算机软件是人类历史上最纯粹的创作活动之一。程序员不受诸如物理定律等实际限制的约束。我们可以用现实世界中永远不会存在的行为创建令人兴奋的虚拟世界。编程不需要很高的身体技能或协调能力,例如芭蕾或篮球。所有编程都需要具有创造力的头脑和组织思想的能力。如果您能够将一个系统具象化,就可以在计算机程序中将它实现。

This means that the greatest limitation in writing software is our ability to understand the systems we are creating. As a program evolves and acquires more features, it becomes complicated, with subtle dependencies between its components. Over time, complexity accumulates, and it becomes harder and harder for programmers to keep all of the relevant factors in their minds as they modify the system. This slows down development and leads to bugs, which slow development even more and add to its cost. Complexity increases inevitably over the life of any program. The larger the program, and the more people that work on it, the more difficult it is to manage complexity.

这意味着编写软件的最大限制是我们了解所创建系统的能力。随着程序的发展和获得更多功能,它变得复杂,其组件之间具有微妙的依赖性。随着时间的流逝,复杂性不断累积,程序员在修改系统时将所有相关因素牢记在心中变得越来越难。这会减慢开发速度并导致错误,从而进一步延缓开发速度并增加成本。在任何程序的生命周期中,复杂性都会不可避免地增加。程序越大,工作的人越多,管理复杂性就越困难。

Good development tools can help us deal with complexity, and many great tools have been created over the last several decades. But there is a limit to what we can do with tools alone. If we want to make it easier to write software, so that we can build more powerful systems more cheaply, we must find ways to make software simpler. Complexity will still increase over time, in spite of our best efforts, but simpler designs allow us to build larger and more powerful systems before complexity becomes overwhelming.

好的开发工具可以帮助我们应对复杂性,并且在过去的几十年中已经创建了许多出色的工具。但是,仅凭工具我们只能做些事情。如果我们想简化编写软件的过程,从而可以更便宜地构建功能更强大的系统,则必须找到简化软件的方法。尽管我们尽了最大努力,但复杂度仍会随着时间的推移而增加,但是更简单的设计使我们能够在复杂性压倒性优势之前构建更大,功能更强大的系统。

There are two general approaches to fighting complexity, both of which will be discussed in this book. The first approach is to eliminate complexity by making code simpler and more obvious. For example, complexity can be reduced by eliminating special cases or using identifiers in a consistent fashion.

有两种解决复杂性的通用方法,这两种方法都将在本书中进行讨论。第一种方法是通过使代码更简单和更明显来消除复杂性。例如,可以通过消除特殊情况或以一致的方式使用标识符来降低复杂性。

The second approach to complexity is to encapsulate it, so that programmers can work on a system without being exposed to all of its complexity at once. This approach is called modular design. In modular design, a software system is divided up into modules, such as classes in an object-oriented language. The modules are designed to be relatively independent of each other, so that a programmer can work on one module without having to understand the details of other modules.

解决复杂性的第二种方法是封装它,以便程序员可以在系统上工作而不会立即暴露其所有复杂性。这种方法称为模块化设计。在模块化设计中,软件系统分为模块,例如面向对象语言的类。这些模块被设计为彼此相对独立(低耦合),以便程序员可以在一个模块上工作而不必了解其他模块的细节

Because software is so malleable, software design is a continuous process that spans the entire lifecycle of a software system; this makes software design different from the design of physical systems such as buildings, ships, or bridges. However, software design has not always been viewed this way. For much of the history of programming, design was concentrated at the beginning of a project, as it is in other engineering disciplines. The extreme of this approach is called the waterfall model, in which a project is divided into discrete phases such as requirements definition, design, coding, testing, and maintenance. In the waterfall model, each phase completes before the next phase starts; in many cases different people are responsible for each phase. The entire system is designed at once, during the design phase. The design is frozen at the end of this phase, and the role of the subsequent phases is to flesh out and implement that design.

由于软件具有很好的延展性,因此软件设计是一个贯穿软件系统整个生命周期的连续过程。这使得软件设计与诸如建筑物,船舶或桥梁的物理系统的设计不同。但是,并非总是以这种方式查看软件设计。在编程的大部分历史中,设计都集中在项目的开始,就像其他工程学科一样。这种方法的极端称为瀑布模型,该模型将项目划分为离散的阶段,例如需求定义,设计,编码,测试和维护。在瀑布模型中,每个阶段都在下一阶段开始之前完成;在许多情况下,每个阶段都由不同的人负责。在设计阶段,立即设计整个系统。

Unfortunately, the waterfall model rarely works well for software. Software systems are intrinsically more complex than physical systems; it isn’t possible to visualize the design for a large software system well enough to understand all of its implications before building anything. As a result, the initial design will have many problems. The problems do not become apparent until implementation is well underway. However, the waterfall model is not structured to accommodate major design changes at this point (for example, the designers may have moved on to other projects). Thus, developers try to patch around the problems without changing the overall design. This results in an explosion of complexity.

不幸的是,瀑布模型很少适用于软件。软件系统本质上比物理系统复杂。在构建任何东西之前,不可能充分具象化出大型软件系统的设计,以了解其所有含义。结果,初始设计将有许多问题。在实施良好之前,问题不会变得明显。但是,瀑布模型的结构此时无法适应主要的设计更改(例如,设计师可能已转移到其他项目)。因此,开发人员尝试在不改变整体设计的情况下解决问题。这导致复杂性的爆炸式增长。

Because of these issues, most software development projects today use an incremental approach such as agile development, in which the initial design focuses on a small subset of the overall functionality. This subset is designed, implemented, and then evaluated. Problems with the original design are discovered and corrected, then a few more features are designed, implemented and evaluated. Each iteration exposes problems with the existing design, which are fixed before the next set of features is designed. By spreading out the design in this way, problems with the initial design can be fixed while the system is still small; later features benefit from experience gained during the implementation of earlier features, so they have fewer problems.

由于这些问题,当今大多数软件开发项目都使用诸如敏捷开发之类的增量方法,其中初始设计着重于整体功能的一小部分。设计,实施和评估此子集。发现和纠正原始设计的问题,然后设计,实施和评估更多功能。每次迭代都会暴露现有设计的问题,这些问题在设计下一组功能之前就已得到解决。通过以这种方式扩展设计,可以在系统仍然很小的情况下解决初始设计的问题。较新的功能受益于较早功能的实施过程中获得的经验,因此问题较少。

The incremental approach works for software because software is malleable enough to allow significant design changes partway through implementation. In contrast, major design changes are much more challenging for physical systems: for example, it would not be practical to change the number of towers supporting a bridge in the middle of construction.

增量方法适用于软件,因为软件具有足够的延展性,可以在实施过程中进行重大的设计更改。相比之下,对物理系统而言,主要的设计更改更具挑战性:例如,在建筑过程中更改支撑桥梁的塔架数量不切实际。

Incremental development means that software design is never done. Design happens continuously over the life of a system: developers should always be thinking about design issues. Incremental development also means continuous redesign. The initial design for a system or component is almost never the best one; experience inevitably shows better ways to do things. As a software developer, you should always be on the lookout for opportunities to improve the design of the system you are working on, and you should plan on spending some fraction of your time on design improvements.

增量开发意味着永远不会完成软件设计设计在系统的整个生命周期中不断发生:开发人员应始终在思考设计问题。增量开发还意味着不断的重新设计。系统或组件的初始设计几乎从来都不是最好的。随着经验累积,不可避免地会产生更好的做事方式。作为软件开发人员,您应该始终在寻找机会来改进正在开发的系统的设计,并且应该计划将部分时间花费在设计改进上。

If software developers should always be thinking about design issues, and reducing complexity is the most important element of software design, then software developers should always be thinking about complexity. This book is about how to use complexity to guide the design of software throughout its lifetime.

如果软件开发人员应始终考虑设计问题,而降低复杂性是软件设计中最重要的要素,则软件开发人员应始终考虑复杂性。这本书是关于如何使用复杂性来指导软件设计的整个生命周期。

This book has two overall goals. The first is to describe the nature of software complexity: what does “complexity” mean, why does it matter, and how can you recognize when a program has unnecessary complexity? The book’s second, and more challenging, goal is to present techniques you can use during the software development process to minimize complexity. Unfortunately, there isn’t a simple recipe that will guarantee great software designs. Instead, I will present a collection of higher-level concepts that border on the philosophical, such as “classes should be deep” or “define errors out of existence.” These concepts may not immediately identify the best design, but you can use them to compare design alternatives and guide your exploration of the design space.

这本书有两个总体目标。首先是描述软件复杂性的性质:“复杂性”是什么意思,为什么重要,以及当程序具有不必要的复杂性时如何识别?本书的第二个也是更具挑战性的目标是介绍可在软件开发过程中使用的技术,以最大程度地减少复杂性。不幸的是,没有简单的方法可以保证出色的软件设计。取而代之的是,我将提出一些与哲学紧密相关的高级概念,例如“类应该很深”或“定义不存在的错误”。这些概念可能不会立即确定最佳设计,但您可以使用它们来比较设计备选方案并指导您探索设计空间。

1.1 How to use this book 如何使用这本书

Many of the design principles described here are somewhat abstract, so they may be hard to appreciate without looking at actual code. It has been a challenge to find examples that are small enough to include in the book, yet large enough to illustrate problems with real systems (if you encounter good examples, please send them to me). Thus, this book may not be sufficient by itself for you to learn how to apply the principles.

这里描述的许多设计原则有些抽象,因此如果不看实际的代码,可能很难理解它们。找到足够小的示例以包含在书中,但是又足够大以说明真实系统的问题是一个挑战(如果遇到好的示例,请发给我)。因此,这本书可能不足以让您学习如何应用这些原理。

The best way to use this book is in conjunction with code reviews. When you read other people’s code, think about whether it conforms to the concepts discussed here and how that relates to the complexity of the code. It’s easier to see design problems in someone else’s code than your own. You can use the red flags described here to identify problems and suggest improvements. Reviewing code will also expose you to new design approaches and programming techniques.

使用本书的最佳方法是与代码审查结合使用。阅读其他人的代码时,请考虑它是否符合此处讨论的概念,以及它与代码的复杂性之间的关系。在别人的代码中比在您的代码中更容易看到设计问题。您可以使用此处描述的红色标记来发现问题并提出改进建议。查看代码还将使您接触到新的设计方法和编程技术。

One of the best ways to improve your design skills is to learn to recognize red flags: signs that a piece of code is probably more complicated than it needs to be. Over the course of this book I will point out red flags that suggest problems related to each major design issue; the most important ones are summarized at the back of the book. You can then use these when you are coding: when you see a red flag, stop and look for an alternate design that eliminates the problem. When you first try this approach, you may have to try several design alternatives before you find one that eliminates the red flag. Don’t give up easily: the more alternatives you try before fixing the problem, the more you will learn. Over time, you will find that your code has fewer and fewer red flags, and your designs are cleaner and cleaner. Your experience will also show you other red flags that you can use to identify design problems (I’d be happy to hear about these).

改善设计技能的最好方法之一就是学会识别危险信号:信号表明一段代码可能比需要的复杂。在本书的过程中,我将指出一些危险信号,这些危险信号指示与每个主要设计问题有关的问题;最重要的内容总结在书的后面。然后,您可以在编码时使用它们:当看到红色标记时,停下来寻找可消除问题的替代设计。当您第一次尝试这种方法时,您可能必须尝试几种设计替代方案,然后才能找到消除危险信号的方案。不要轻易放弃:解决问题之前尝试的替代方法越多,您就会学到更多。随着时间的流逝,您会发现代码中的危险信号越来越少,并且您的设计越来越清晰。

When applying the ideas from this book, it’s important to use moderation and discretion. Every rule has its exceptions, and every principle has its limits. If you take any design idea to its extreme, you will probably end up in a bad place. Beautiful designs reflect a balance between competing ideas and approaches. Several chapters have sections titled “Taking it too far,” which describe how to recognize when you are overdoing a good thing.

在应用本书中的思想时,务必要节制和谨慎。每条规则都有例外,每条原则都有其局限性。如果您将任何设计创意都发挥到极致,那么您可能会陷入困境。精美的设计反映了相互竞争的思想和方法之间的平衡。有几章的标题为“太过分”,它们描述了如何在做得过大的事情上识别自己。

Almost all of the examples in this book are in Java or C++, and much of the discussion is in terms of designing classes in an object-oriented language. However, the ideas apply in other domains as well. Almost all of the ideas related to methods can also be applied to functions in a language without object-oriented features, such as C. The design ideas also apply to modules other than classes, such as subsystems or network services.

本书中几乎所有示例都是使用 Java 或 C++ 编写的,并且大部分讨论都是针对以面向对象的语言设计类的。但是,这些想法也适用于其他领域。几乎所有与方法有关的思想也可以应用于没有面向对象功能的语言中的功能,例如 C。设计思想还适用于除类之外的模块,例如子系统或网络服务。

With this background, let’s discuss in more detail what causes complexity, and how to make software systems simpler.

在这种背景下,让我们详细讨论导致复杂性的原因以及如何简化软件系统。

解决复杂性的方法 1.使代码更简单和更明显。 2.封装它,使用时不必了解细节。

',40)]))}const h=t(i,[["render",n],["__file","ch01.html.vue"]]),c=JSON.parse('{"path":"/ch01.html","title":"第 1 章 介绍","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"1.1 How to use this book 如何使用这本书","slug":"_1-1-how-to-use-this-book-如何使用这本书","link":"#_1-1-how-to-use-this-book-如何使用这本书","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch01.md"}');export{h as comp,c as data}; +import{_ as t,c as o,f as s,o as a}from"./app-BQdhfat9.js";const i={};function n(r,e){return a(),o("div",null,e[0]||(e[0]=[s('

第 1 章 介绍

Chapter 1 Introduction (It’s All About Complexity)

Writing computer software is one of the purest creative activities in the history of the human race. Programmers aren’t bound by practical limitations such as the laws of physics; we can create exciting virtual worlds with behaviors that could never exist in the real world. Programming doesn’t require great physical skill or coordination, like ballet or basketball. All programming requires is a creative mind and the ability to organize your thoughts. If you can visualize a system, you can probably implement it in a computer program.

编写计算机软件是人类历史上最纯粹的创作活动之一。程序员不受诸如物理定律等实际限制的约束。我们可以用现实世界中永远不会存在的行为创建令人兴奋的虚拟世界。编程不需要很高的身体技能或协调能力,例如芭蕾或篮球。所有编程都需要具有创造力的头脑和组织思想的能力。如果您能够将一个系统具象化,就可以在计算机程序中将它实现。

This means that the greatest limitation in writing software is our ability to understand the systems we are creating. As a program evolves and acquires more features, it becomes complicated, with subtle dependencies between its components. Over time, complexity accumulates, and it becomes harder and harder for programmers to keep all of the relevant factors in their minds as they modify the system. This slows down development and leads to bugs, which slow development even more and add to its cost. Complexity increases inevitably over the life of any program. The larger the program, and the more people that work on it, the more difficult it is to manage complexity.

这意味着编写软件的最大限制是我们了解所创建系统的能力。随着程序的发展和获得更多功能,它变得复杂,其组件之间具有微妙的依赖性。随着时间的流逝,复杂性不断累积,程序员在修改系统时将所有相关因素牢记在心中变得越来越难。这会减慢开发速度并导致错误,从而进一步延缓开发速度并增加成本。在任何程序的生命周期中,复杂性都会不可避免地增加。程序越大,工作的人越多,管理复杂性就越困难。

Good development tools can help us deal with complexity, and many great tools have been created over the last several decades. But there is a limit to what we can do with tools alone. If we want to make it easier to write software, so that we can build more powerful systems more cheaply, we must find ways to make software simpler. Complexity will still increase over time, in spite of our best efforts, but simpler designs allow us to build larger and more powerful systems before complexity becomes overwhelming.

好的开发工具可以帮助我们应对复杂性,并且在过去的几十年中已经创建了许多出色的工具。但是,仅凭工具我们只能做些事情。如果我们想简化编写软件的过程,从而可以更便宜地构建功能更强大的系统,则必须找到简化软件的方法。尽管我们尽了最大努力,但复杂度仍会随着时间的推移而增加,但是更简单的设计使我们能够在复杂性压倒性优势之前构建更大,功能更强大的系统。

There are two general approaches to fighting complexity, both of which will be discussed in this book. The first approach is to eliminate complexity by making code simpler and more obvious. For example, complexity can be reduced by eliminating special cases or using identifiers in a consistent fashion.

有两种解决复杂性的通用方法,这两种方法都将在本书中进行讨论。第一种方法是通过使代码更简单和更明显来消除复杂性。例如,可以通过消除特殊情况或以一致的方式使用标识符来降低复杂性。

The second approach to complexity is to encapsulate it, so that programmers can work on a system without being exposed to all of its complexity at once. This approach is called modular design. In modular design, a software system is divided up into modules, such as classes in an object-oriented language. The modules are designed to be relatively independent of each other, so that a programmer can work on one module without having to understand the details of other modules.

解决复杂性的第二种方法是封装它,以便程序员可以在系统上工作而不会立即暴露其所有复杂性。这种方法称为模块化设计。在模块化设计中,软件系统分为模块,例如面向对象语言的类。这些模块被设计为彼此相对独立(低耦合),以便程序员可以在一个模块上工作而不必了解其他模块的细节

Because software is so malleable, software design is a continuous process that spans the entire lifecycle of a software system; this makes software design different from the design of physical systems such as buildings, ships, or bridges. However, software design has not always been viewed this way. For much of the history of programming, design was concentrated at the beginning of a project, as it is in other engineering disciplines. The extreme of this approach is called the waterfall model, in which a project is divided into discrete phases such as requirements definition, design, coding, testing, and maintenance. In the waterfall model, each phase completes before the next phase starts; in many cases different people are responsible for each phase. The entire system is designed at once, during the design phase. The design is frozen at the end of this phase, and the role of the subsequent phases is to flesh out and implement that design.

由于软件具有很好的延展性,因此软件设计是一个贯穿软件系统整个生命周期的连续过程。这使得软件设计与诸如建筑物,船舶或桥梁的物理系统的设计不同。但是,并非总是以这种方式查看软件设计。在编程的大部分历史中,设计都集中在项目的开始,就像其他工程学科一样。这种方法的极端称为瀑布模型,该模型将项目划分为离散的阶段,例如需求定义,设计,编码,测试和维护。在瀑布模型中,每个阶段都在下一阶段开始之前完成;在许多情况下,每个阶段都由不同的人负责。在设计阶段,立即设计整个系统。

Unfortunately, the waterfall model rarely works well for software. Software systems are intrinsically more complex than physical systems; it isn’t possible to visualize the design for a large software system well enough to understand all of its implications before building anything. As a result, the initial design will have many problems. The problems do not become apparent until implementation is well underway. However, the waterfall model is not structured to accommodate major design changes at this point (for example, the designers may have moved on to other projects). Thus, developers try to patch around the problems without changing the overall design. This results in an explosion of complexity.

不幸的是,瀑布模型很少适用于软件。软件系统本质上比物理系统复杂。在构建任何东西之前,不可能充分具象化出大型软件系统的设计,以了解其所有含义。结果,初始设计将有许多问题。在实施良好之前,问题不会变得明显。但是,瀑布模型的结构此时无法适应主要的设计更改(例如,设计师可能已转移到其他项目)。因此,开发人员尝试在不改变整体设计的情况下解决问题。这导致复杂性的爆炸式增长。

Because of these issues, most software development projects today use an incremental approach such as agile development, in which the initial design focuses on a small subset of the overall functionality. This subset is designed, implemented, and then evaluated. Problems with the original design are discovered and corrected, then a few more features are designed, implemented and evaluated. Each iteration exposes problems with the existing design, which are fixed before the next set of features is designed. By spreading out the design in this way, problems with the initial design can be fixed while the system is still small; later features benefit from experience gained during the implementation of earlier features, so they have fewer problems.

由于这些问题,当今大多数软件开发项目都使用诸如敏捷开发之类的增量方法,其中初始设计着重于整体功能的一小部分。设计,实施和评估此子集。发现和纠正原始设计的问题,然后设计,实施和评估更多功能。每次迭代都会暴露现有设计的问题,这些问题在设计下一组功能之前就已得到解决。通过以这种方式扩展设计,可以在系统仍然很小的情况下解决初始设计的问题。较新的功能受益于较早功能的实施过程中获得的经验,因此问题较少。

The incremental approach works for software because software is malleable enough to allow significant design changes partway through implementation. In contrast, major design changes are much more challenging for physical systems: for example, it would not be practical to change the number of towers supporting a bridge in the middle of construction.

增量方法适用于软件,因为软件具有足够的延展性,可以在实施过程中进行重大的设计更改。相比之下,对物理系统而言,主要的设计更改更具挑战性:例如,在建筑过程中更改支撑桥梁的塔架数量不切实际。

Incremental development means that software design is never done. Design happens continuously over the life of a system: developers should always be thinking about design issues. Incremental development also means continuous redesign. The initial design for a system or component is almost never the best one; experience inevitably shows better ways to do things. As a software developer, you should always be on the lookout for opportunities to improve the design of the system you are working on, and you should plan on spending some fraction of your time on design improvements.

增量开发意味着永远不会完成软件设计设计在系统的整个生命周期中不断发生:开发人员应始终在思考设计问题。增量开发还意味着不断的重新设计。系统或组件的初始设计几乎从来都不是最好的。随着经验累积,不可避免地会产生更好的做事方式。作为软件开发人员,您应该始终在寻找机会来改进正在开发的系统的设计,并且应该计划将部分时间花费在设计改进上。

If software developers should always be thinking about design issues, and reducing complexity is the most important element of software design, then software developers should always be thinking about complexity. This book is about how to use complexity to guide the design of software throughout its lifetime.

如果软件开发人员应始终考虑设计问题,而降低复杂性是软件设计中最重要的要素,则软件开发人员应始终考虑复杂性。这本书是关于如何使用复杂性来指导软件设计的整个生命周期。

This book has two overall goals. The first is to describe the nature of software complexity: what does “complexity” mean, why does it matter, and how can you recognize when a program has unnecessary complexity? The book’s second, and more challenging, goal is to present techniques you can use during the software development process to minimize complexity. Unfortunately, there isn’t a simple recipe that will guarantee great software designs. Instead, I will present a collection of higher-level concepts that border on the philosophical, such as “classes should be deep” or “define errors out of existence.” These concepts may not immediately identify the best design, but you can use them to compare design alternatives and guide your exploration of the design space.

这本书有两个总体目标。首先是描述软件复杂性的性质:“复杂性”是什么意思,为什么重要,以及当程序具有不必要的复杂性时如何识别?本书的第二个也是更具挑战性的目标是介绍可在软件开发过程中使用的技术,以最大程度地减少复杂性。不幸的是,没有简单的方法可以保证出色的软件设计。取而代之的是,我将提出一些与哲学紧密相关的高级概念,例如“类应该很深”或“定义不存在的错误”。这些概念可能不会立即确定最佳设计,但您可以使用它们来比较设计备选方案并指导您探索设计空间。

1.1 How to use this book 如何使用这本书

Many of the design principles described here are somewhat abstract, so they may be hard to appreciate without looking at actual code. It has been a challenge to find examples that are small enough to include in the book, yet large enough to illustrate problems with real systems (if you encounter good examples, please send them to me). Thus, this book may not be sufficient by itself for you to learn how to apply the principles.

这里描述的许多设计原则有些抽象,因此如果不看实际的代码,可能很难理解它们。找到足够小的示例以包含在书中,但是又足够大以说明真实系统的问题是一个挑战(如果遇到好的示例,请发给我)。因此,这本书可能不足以让您学习如何应用这些原理。

The best way to use this book is in conjunction with code reviews. When you read other people’s code, think about whether it conforms to the concepts discussed here and how that relates to the complexity of the code. It’s easier to see design problems in someone else’s code than your own. You can use the red flags described here to identify problems and suggest improvements. Reviewing code will also expose you to new design approaches and programming techniques.

使用本书的最佳方法是与代码审查结合使用。阅读其他人的代码时,请考虑它是否符合此处讨论的概念,以及它与代码的复杂性之间的关系。在别人的代码中比在您的代码中更容易看到设计问题。您可以使用此处描述的红色标记来发现问题并提出改进建议。查看代码还将使您接触到新的设计方法和编程技术。

One of the best ways to improve your design skills is to learn to recognize red flags: signs that a piece of code is probably more complicated than it needs to be. Over the course of this book I will point out red flags that suggest problems related to each major design issue; the most important ones are summarized at the back of the book. You can then use these when you are coding: when you see a red flag, stop and look for an alternate design that eliminates the problem. When you first try this approach, you may have to try several design alternatives before you find one that eliminates the red flag. Don’t give up easily: the more alternatives you try before fixing the problem, the more you will learn. Over time, you will find that your code has fewer and fewer red flags, and your designs are cleaner and cleaner. Your experience will also show you other red flags that you can use to identify design problems (I’d be happy to hear about these).

改善设计技能的最好方法之一就是学会识别危险信号:信号表明一段代码可能比需要的复杂。在本书的过程中,我将指出一些危险信号,这些危险信号指示与每个主要设计问题有关的问题;最重要的内容总结在书的后面。然后,您可以在编码时使用它们:当看到红色标记时,停下来寻找可消除问题的替代设计。当您第一次尝试这种方法时,您可能必须尝试几种设计替代方案,然后才能找到消除危险信号的方案。不要轻易放弃:解决问题之前尝试的替代方法越多,您就会学到更多。随着时间的流逝,您会发现代码中的危险信号越来越少,并且您的设计越来越清晰。

When applying the ideas from this book, it’s important to use moderation and discretion. Every rule has its exceptions, and every principle has its limits. If you take any design idea to its extreme, you will probably end up in a bad place. Beautiful designs reflect a balance between competing ideas and approaches. Several chapters have sections titled “Taking it too far,” which describe how to recognize when you are overdoing a good thing.

在应用本书中的思想时,务必要节制和谨慎。每条规则都有例外,每条原则都有其局限性。如果您将任何设计创意都发挥到极致,那么您可能会陷入困境。精美的设计反映了相互竞争的思想和方法之间的平衡。有几章的标题为“太过分”,它们描述了如何在做得过大的事情上识别自己。

Almost all of the examples in this book are in Java or C++, and much of the discussion is in terms of designing classes in an object-oriented language. However, the ideas apply in other domains as well. Almost all of the ideas related to methods can also be applied to functions in a language without object-oriented features, such as C. The design ideas also apply to modules other than classes, such as subsystems or network services.

本书中几乎所有示例都是使用 Java 或 C++ 编写的,并且大部分讨论都是针对以面向对象的语言设计类的。但是,这些想法也适用于其他领域。几乎所有与方法有关的思想也可以应用于没有面向对象功能的语言中的功能,例如 C。设计思想还适用于除类之外的模块,例如子系统或网络服务。

With this background, let’s discuss in more detail what causes complexity, and how to make software systems simpler.

在这种背景下,让我们详细讨论导致复杂性的原因以及如何简化软件系统。

解决复杂性的方法 1.使代码更简单和更明显。 2.封装它,使用时不必了解细节。

',40)]))}const h=t(i,[["render",n],["__file","ch01.html.vue"]]),c=JSON.parse('{"path":"/ch01.html","title":"第 1 章 介绍","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"1.1 How to use this book 如何使用这本书","slug":"_1-1-how-to-use-this-book-如何使用这本书","link":"#_1-1-how-to-use-this-book-如何使用这本书","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch01.md"}');export{h as comp,c as data}; diff --git a/assets/ch02.html-Dk8nl67z.js b/assets/ch02.html-CPlVQyLd.js similarity index 99% rename from assets/ch02.html-Dk8nl67z.js rename to assets/ch02.html-CPlVQyLd.js index 0eb5463d..9950b6e7 100644 --- a/assets/ch02.html-Dk8nl67z.js +++ b/assets/ch02.html-CPlVQyLd.js @@ -1 +1 @@ -import{_ as t,c as o,f as i,o as a}from"./app-CvqtBB8Z.js";const s="data:image/gif;base64,R0lGODlhTwAhAOcAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwMDcwKbK8P/w1P/isf/Ujv/Ga/+4SP+qJf+qANySALl6AJZiAHNKAFAyAP/j1P/Hsf+rjv+Pa/9zSP9XJf9VANxJALk9AJYxAHMlAFAZAP/U1P+xsf+Ojv9ra/9ISP8lJf4AANwAALkAAJYAAHMAAFAAAP/U4/+xx/+Oq/9rj/9Ic/8lV/8AVdwASbkAPZYAMXMAJVAAGf/U8P+x4v+O1P9rxv9IuP8lqv8AqtwAkrkAepYAYnMASlAAMv/U//+x//+O//9r//9I//8l//4A/twA3LkAuZYAlnMAc1AAUPDU/+Kx/9SO/8Zr/7hI/6ol/6oA/5IA3HoAuWIAlkoAczIAUOPU/8ex/6uO/49r/3NI/1cl/1UA/0kA3D0AuTEAliUAcxkAUNTU/7Gx/46O/2tr/0hI/yUl/wAA/gAA3AAAuQAAlgAAcwAAUNTj/7HH/46r/2uP/0hz/yVX/wBV/wBJ3AA9uQAxlgAlcwAZUNTw/7Hi/47U/2vG/0i4/yWq/wCq/wCS3AB6uQBilgBKcwAyUNT//7H//47//2v//0j//yX//wD+/gDc3AC5uQCWlgBzcwBQUNT/8LH/4o7/1Gv/xkj/uCX/qgD/qgDckgC5egCWYgBzSgBQMtT/47H/x47/q2v/j0j/cyX/VwD/VQDcSQC5PQCWMQBzJQBQGdT/1LH/sY7/jmv/a0j/SCX/JQD+AADcAAC5AACWAABzAABQAOP/1Mf/sav/jo//a3P/SFf/JVX/AEncAD25ADGWACVzABlQAPD/1OL/sdT/jsb/a7j/SKr/Jar/AJLcAHq5AGKWAEpzADJQAP//1P//sf//jv//a///SP//Jf7+ANzcALm5AJaWAHNzAFBQAPLy8ubm5tra2s7OzsLCwra2tqqqqp6enpKSkoaGhnp6em5ubmJiYlZWVkpKSj4+PjIyMiYmJhoaGg4ODv/78KCgpICAgP8AAAD/AP//AAAA//8A/wD//////yH5BAAAAP8ALAAAAABPACEAAAj+AP8JHEiwoMGDA88BWMiwIUNyCCNKnEix4j9x8gCUQ6gQosWPIEMKHAdA3riD5diJE8myZURzANqtdEnz37h17di1e4eu5r90ANT5bHlO3kaB6tINbQfg3FCC4tr1fAnAo0BzVmli1PhUIDmuCMUBUNq1IEmTFM+xk/du5sF0Gd+9yzoQHYCTZQseiOnWoDh2TsX2NbhWYjt5Fh0yJDsRKLuI7B5fnShPMkIAlvMSPID24Fe6EcVOvdxOc8Fynd8CGCzOHLty5uSR/Xr0omvYstmtrqh4IWOE497hRagb6sW96M7BFKjw5EpxyM/t/drOObnRPsfBG47QrlNy6Zy9/vva81zpn3fvHSX/z/z4tfKkcqcZfP5BceoA8HR7ru0/dgd4FZ9V/a0EoGkjzUXTOj2Nk5lB6jT4YFna2ZeQeBK9I486GCKkIYcI2iRchhMWJJaFJt4VokAZ9eZQiQR9VZGMK/6jTmQ45phjhweNEyBFPtYo5JBEFmnkkUgmWRB46Yyz02BK1kTOO+qsI1ZtUQ7FljgnZvnUlf+U446XT5GD2H9QksmSQuygk6aaIjEIZ1fv/DinT+e8uWJAADsA",n="/A-Philosophy-of-Software-Design-zh/assets/00010-BHBDpSUQ.jpeg",r={};function c(l,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 2 章 复杂性的本质

Chapter 2 The Nature of Complexity

This book is about how to design software systems to minimize their complexity. The first step is to understand the enemy. Exactly what is “complexity”? How can you tell if a system is unnecessarily complex? What causes systems to become complex? This chapter will address those questions at a high level; subsequent chapters will show you how to recognize complexity at a lower level, in terms of specific structural features.

这本书是关于如何设计软件系统以最小化其复杂性。第一步是了解敌人。究竟什么是“复杂性”?您如何判断系统是否过于复杂?是什么导致系统变得复杂?本章将在较高层次上解决这些问题。后续章节将向您展示如何从较低的层次上根据特定的结构特征来识别复杂性。

The ability to recognize complexity is a crucial design skill. It allows you to identify problems before you invest a lot of effort in them, and it allows you to make good choices among alternatives. It is easier to tell whether a design is simple than it is to create a simple design, but once you can recognize that a system is too complicated, you can use that ability to guide your design philosophy towards simplicity. If a design appears complicated, try a different approach and see if that is simpler. Over time, you will notice that certain techniques tend to result in simpler designs, while others correlate with complexity. This will allow you to produce simpler designs more quickly.

识别复杂性的能力是至关重要的设计技能。它使您可以先找出问题,然后再付出大量努力,并可以在其他选择中做出正确的选择。判断一个设计是否简单比创建一个简单的设计要容易得多,但是一旦您认识到一个系统过于复杂,就可以使用该功能指导您的设计哲学走向简单。如果设计看起来很复杂,请尝试其他方法,看看是否更简单。随着时间的流逝,您会注意到某些技术往往会导致设计更简单,而其他技术则与复杂性相关。这将使您更快地制作更简单的设计。

This chapter also lays out some basic assumptions that provide a foundation for the rest of the book. Later chapters take the material of this chapter as given and use it to justify a variety of refinements and conclusions.

本章还列出了一些基本假设,这些基本假设为本书的其余部分奠定了基础。后面的章节将采用本章的内容,并用其论证各种改进和结论。

2.1 Complexity defined 复杂性的定义

For the purposes of this book, I define “complexity” in a practical way. Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system. Complexity can take many forms. For example, it might be hard to understand how a piece of code works; it might take a lot of effort to implement a small improvement, or it might not be clear which parts of the system must be modified to make the improvement; it might be difficult to fix one bug without introducing another. If a software system is hard to understand and modify, then it is complicated; if it is easy to understand and modify, then it is simple.

出于本书的目的,我以实用的方式定义“复杂性”。复杂性与软件系统的结构有关,这使它很难理解和修改系统(复杂性是指那些让系统难以理解或修改的与系统相关的任何事物)。复杂性可以采取多种形式。例如,可能很难理解一段代码是如何工作的。可能需要花费很多精力才能实现较小的改进,或者可能不清楚必须修改系统的哪些部分才能进行改进;如果不引入其他错误,可能很难修复(也可以是不引入额外问题的情况下,很难修复一个bug)。如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。

You can also think of complexity in terms of cost and benefit. In a complex system, it takes a lot of work to implement even small improvements. In a simple system, larger improvements can be implemented with less effort.

您还可以考虑成本和收益方面的复杂性(你还可以根据成本和收益来评估复杂性)。在复杂的系统中,要实施甚至很小的改进都需要大量的工作。在一个简单的系统中,可以用更少的精力实现更大的改进。

Complexity is what a developer experiences at a particular point in time when trying to achieve a particular goal. It doesn’t necessarily relate to the overall size or functionality of the system. People often use the word “complex” to describe large systems with sophisticated features, but if such a system is easy to work on, then, for the purposes of this book, it is not complex. Of course, almost all large and sophisticated software systems are in fact hard to work on, so they also meet my definition of complexity, but this need not necessarily be the case. It is also possible for a small and unsophisticated system to be quite complex.

复杂性是开发人员在尝试实现特定目标时在特定时间点所经历的。它不一定与系统的整体大小或功能有关。人们通常使用“复杂”一词来描述具有复杂功能的大型系统,但是如果这样的系统易于使用,那么就本书而言,它并不复杂。当然,实际上几乎所有大型复杂的软件系统都很难使用,因此它们也符合我对复杂性的定义,但这不一定是事实。小型而不复杂的系统也可能非常复杂。

Complexity is determined by the activities that are most common. If a system has a few parts that are very complicated, but those parts almost never need to be touched, then they don’t have much impact on the overall complexity of the system. To characterize this in a crude mathematical way:

复杂性取决于最常见的活动。如果系统中有一些非常复杂的部分,但是几乎不需要触摸这些部分,那么它们对系统的整体复杂性不会有太大影响。为了用粗略的数学方法来表征:

The overall complexity of a system (C) is determined by the complexity of each part p (cp) weighted by the fraction of time developers spend working on that part (tp). Isolating complexity in a place where it will never be seen is almost as good as eliminating the complexity entirely.

系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。

Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex. When you find yourself in situations like this, it’s worth probing the other developers to find out why the code seems complex to them; there are probably some interesting lessons to learn from the disconnect between your opinion and theirs. Your job as a developer is not just to create code that you can work with easily, but to create code that others can also work with easily.

读者比作家更容易理解复杂性。如果您编写了一段代码,对您来说似乎很简单,但是其他人则认为它很复杂,那么它就是复杂的。当您遇到这种情况时,有必要对其他开发人员进行调查,以找出为什么代码对他们而言似乎很复杂;从您的观点与观点之间的脱节中可能可以学习一些有趣的课程。作为开发人员,您的工作不仅是创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码。

2.2 Symptoms of complexity 复杂性的症状

Complexity manifests itself in three general ways, which are described in the paragraphs below. Each of these manifestations makes it harder to carry out development tasks.

复杂性通过以下三种段落中描述的三种一般方式体现出来。这些表现形式中的每一个都使执行开发任务变得更加困难。

Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places. For example, consider a Web site containing several pages, each of which displays a banner with a background color. In many early Web sites, the color was specified explicitly on each page, as shown in Figure 2.1(a). In order to change the background for such a Web site, a developer might have to modify every existing page by hand; this would be nearly impossible for a large site with thousands of pages. Fortunately, modern Web sites use an approach like that in Figure 2.1(b), where the banner color is specified once in a central place, and all of the individual pages reference that shared value. With this approach, the banner color of the entire Web site can be changed with a single modification. One of the goals of good design is to reduce the amount of code that is affected by each design decision, so design changes don’t require very many code modifications.

变更放大:复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。例如,考虑一个包含几个页面的网站,每个页面显示带有背景色的横幅。在许多早期的网站中,颜色是在每个页面上明确指定的,如图 2.1(a)所示。为了更改此类网站的背景,开发人员可能必须手动修改每个现有页面;对于拥有数千个页面的大型网站而言,这几乎是不可能的。幸运的是,现代网站使用的方法类似于图 2.1(b),其中横幅颜色一次在中心位置指定,并且所有各个页面均引用该共享值。使用这种方法,可以通过一次修改来更改整个网站的标题颜色。

Cognitive load: The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task. A higher cognitive load means that developers have to spend more time learning the required information, and there is a greater risk of bugs because they have missed something important. For example, suppose a function in C allocates memory, returns a pointer to that memory, and assumes that the caller will free the memory. This adds to the cognitive load of developers using the function; if a developer fails to free the memory, there will be a memory leak. If the system can be restructured so that the caller doesn’t need to worry about freeing the memory (the same module that allocates the memory also takes responsibility for freeing it), it will reduce the cognitive load. Cognitive load arises in many ways, such as APIs with many methods, global variables, inconsistencies, and dependencies between modules.

认知负荷:复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。较高的认知负担意味着开发人员必须花更多的时间来学习所需的信息,并且由于错过了重要的东西而导致错误的风险也更大。例如,假设 C 中的一个函数分配了内存,返回了指向该内存的指针,并假定调用者将释放该内存。这增加了使用该功能的开发人员的认知负担。如果开发人员无法释放内存,则会发生内存泄漏。如果可以对系统进行重组,以使调用者不必担心释放内存(分配内存的同一模块也负责释放内存),它将减少认知负担。(认知负荷出现在很多方面,例如很多方法的API,全局变量,不一致和模块间依赖)

System designers sometimes assume that complexity can be measured by lines of code. They assume that if one implementation is shorter than another, then it must be simpler; if it only takes a few lines of code to make a change, then the change must be easy. However, this view ignores the costs associated with cognitive load. I have seen frameworks that allowed applications to be written with only a few lines of code, but it was extremely difficult to figure out what those lines were. Sometimes an approach that requires more lines of code is actually simpler, because it reduces cognitive load.

系统设计人员有时会假设可以通过代码行来衡量复杂性。他们认为,如果一个实现比另一个实现短,那么它必须更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。但是,这种观点忽略了与认知负荷相关的成本。我已经看到了仅允许使用几行代码编写应用程序的框架,但是要弄清楚这些行是什么极其困难。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。

Figure 2.1: Each page in a Web site displays a colored banner. In (a) the background color for the banner is specified explicitly in each page. In (b) a shared variable holds the background color and each page references that variable. In (c) some pages display an additional color for emphasis, which is a darker shade of the banner background color; if the background color changes, the emphasis color must also change.

图 2.1:网站中的每个页面都显示一个彩色横幅。在(a)中,横幅的背景色在每页中都明确指定。在(b)中,共享变量保留背景色,并且每个页面都引用该变量。在(c)中,某些页面会显示其他用于强调的颜色,即横幅背景颜色的暗色;如果背景颜色改变,则强调颜色也必须改变。

Unknown unknowns: The third symptom of complexity is that it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully. Figure 2.1(c) illustrates this problem. The Web site uses a central variable to determine the banner background color, so it appears to be easy to change. However, a few Web pages use a darker shade of the background color for emphasis, and that darker color is specified explicitly in the individual pages. If the background color changes, then the the emphasis color must change to match. Unfortunately, developers are unlikely to realize this, so they may change the central bannerBg variable without updating the emphasis color. Even if a developer is aware of the problem, it won’t be obvious which pages use the emphasis color, so the developer may have to search every page in the Web site.

未知的未知: 复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。图 2.1(c)说明了这个问题。网站使用一个中心变量来确定横幅的背景颜色,所以它看起来很容易改变。但是,一些 Web 页面使用较暗的背景色来强调,并且在各个页面中明确指定了较暗的颜色。如果背景颜色改变,那么强调的颜色必须改变以匹配。不幸的是,开发人员不太可能意识到这一点,所以他们可能会更改中央 bannerBg 变量而不更新强调颜色。即使开发人员意识到这个问题,也不清楚哪些页面使用了强调色,因此开发人员可能必须搜索 Web 站点中的每个页面。

Of the three manifestations of complexity, unknown unknowns are the worst. An unknown unknown means that there is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won’t find out about it until bugs appear after you make a change. Change amplification is annoying, but as long as it is clear which code needs to be modified, the system will work once the change has been completed. Similarly, a high cognitive load will increase the cost of a change, but if it is clear which information to read, the change is still likely to be correct. With unknown unknowns, it is unclear what to do or whether a proposed solution will even work. The only way to be certain is to read every line of code in the system, which is impossible for systems of any size. Even this may not be sufficient, because a change may depend on a subtle design decision that was never documented.

在复杂性的三种表现形式中,未知的未知是最糟糕的。一个未知的未知意味着你需要知道一些事情,但是你没有办法找到它是什么,甚至是否有一个问题。你不会发现它,直到错误出现后,你做了一个改变。更改放大是令人恼火的,但是只要清楚哪些代码需要修改,一旦更改完成,系统就会工作。同样,高的认知负荷会增加改变的成本,但如果明确要阅读哪些信息,改变仍然可能是正确的。对于未知的未知,不清楚该做什么,或者提出的解决方案是否有效。唯一确定的方法是读取系统中的每一行代码,这对于任何大小的系统都是不可能的。甚至这可能还不够,因为更改可能依赖于一个从未记录的细微设计决策。

One of the most important goals of good design is for a system to be obvious. This is the opposite of high cognitive load and unknown unknowns. In an obvious system, a developer can quickly understand how the existing code works and what is required to make a change. An obvious system is one where a developer can make a quick guess about what to do, without thinking very hard, and yet be confident that the guess is correct. Chapter 18 discusses techniques for making code more obvious.

良好设计的最重要目标之一就是使系统显而易见。这与高认知负荷和未知未知数相反。在一个显而易见的系统中,开发人员可以快速了解现有代码的工作方式以及进行更改所需的内容。一个显而易见的系统是,开发人员可以在不费力地思考的情况下快速猜测要做什么,同时又可以确信该猜测是正确的。第 18 章讨论使代码更明显的技术。

2.3 Causes of complexity 复杂性的原因

Now that you know the high-level symptoms of complexity and why complexity makes software development difficult, the next step is to understand what causes complexity, so that we can design systems to avoid the problems. Complexity is caused by two things: dependencies and obscurity. This section discusses these factors at a high level; subsequent chapters will discuss how they relate to lower-level design decisions.

既然您已经了解了复杂性的高级症状以及为什么复杂性会使软件开发变得困难,那么下一步就是了解导致复杂性的原因,以便我们设计系统来避免这些问题。复杂性是由两件事引起的:依赖性和模糊性。本节从高层次讨论这些因素。随后的章节将讨论它们与低级设计决策之间的关系。

For the purposes of this book, a dependency exists when a given piece of code cannot be understood and modified in isolation; the code relates in some way to other code, and the other code must be considered and/or modified if the given code is changed. In the Web site example of Figure 2.1(a), the background color creates dependencies between all of the pages. All of the pages need to have the same background, so if the background is changed for one page, then it must be changed for all of them. Another example of dependencies occurs in network protocols. Typically there is separate code for the sender and receiver for the protocol, but they must each conform to the protocol; changing the code for the sender almost always requires corresponding changes at the receiver, and vice versa. The signature of a method creates a dependency between the implementation of that method and the code that invokes it: if a new parameter is added to a method, all of the invocations of that method must be modified to specify that parameter.

就本书而言,当无法孤立地理解和修改给定的一段代码时,便存在依赖关系。该代码以某种方式与其他代码相关,如果更改了给定代码,则必须考虑和/或修改其他代码。在图 2.1(a)的网站示例中,背景色在所有页面之间创建了依赖关系。所有页面都必须具有相同的背景,因此,如果更改一页的背景,则必须更改所有背景。依赖关系的另一个示例发生在网络协议中。通常,协议的发送方和接收方有单独的代码,但是它们必须分别符合协议。更改发送方的代码几乎总是需要在接收方进行相应的更改,反之亦然。

Dependencies are a fundamental part of software and can’t be completely eliminated. In fact, we intentionally introduce dependencies as part of the software design process. Every time you write a new class you create dependencies around the API for that class. However, one of the goals of software design is to reduce the number of dependencies and to make the dependencies that remain as simple and obvious as possible.

依赖关系是软件的基本组成部分,不能完全消除。实际上,我们在软件设计过程中有意引入了依赖性。每次编写新类时,都会围绕该类的 API 创建依赖关系。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。

Consider the Web site example. In the old Web site with the background specified separately on each page, all of the Web pages were dependent on each other. The new Web site fixed this problem by specifying the background color in a central place and providing an API that individual pages use to retrieve that color when they are rendered. The new Web site eliminated the dependency between the pages, but it created a new dependency around the API for retrieving the background color. Fortunately, the new dependency is more obvious: it is clear that each individual Web page depends on the bannerBg color, and a developer can easily find all the places where the variable is used by searching for its name. Furthermore, compilers help to manage API dependencies: if the name of the shared variable changes, compilation errors will occur in any code that still uses the old name. The new Web site replaced a nonobvious and difficult-to-manage dependency with a simpler and more obvious one.

考虑网站示例。在每个页面分别指定背景的旧网站中,所有网页都是相互依赖的。新的网站通过在中心位置指定背景色并提供一个 API,供各个页面在呈现它们时检索该颜色,从而解决了该问题。新的网站消除了页面之间的依赖关系,但是它围绕 API 创建了一个新的依赖关系以检索背景色。幸运的是,新的依赖性更加明显:很明显,每个单独的网页都取决于 bannerBg 颜色,并且开发人员可以通过搜索其名称轻松找到使用该变量的所有位置。此外,编译器还有助于管理 API 依赖性:如果共享变量的名称发生变化,任何仍使用旧名称的代码都将发生编译错误。新的网站用一种更简单,更明显的方式代替了一种不明显且难以管理的依赖性。

The second cause of complexity is obscurity. Obscurity occurs when important information is not obvious. A simple example is a variable name that is so generic that it doesn’t carry much useful information (e.g., time). Or, the documentation for a variable might not specify its units, so the only way to find out is to scan code for places where the variable is used. Obscurity is often associated with dependencies, where it is not obvious that a dependency exists. For example, if a new error status is added to a system, it may be necessary to add an entry to a table holding string messages for each status, but the existence of the message table might not be obvious to a programmer looking at the status declaration. Inconsistency is also a major contributor to obscurity: if the same variable name is used for two different purposes, it won’t be obvious to developer which of these purposes a particular variable serves.

复杂性的第二个原因是晦涩。当重要的信息不明显时,就会发生模糊。一个简单的例子是一个变量名,它是如此的通用,以至于它没有携带太多有用的信息(例如,时间)。或者,一个变量的文档可能没有指定它的单位,所以找到它的惟一方法是扫描代码,查找使用该变量的位置。晦涩常常与依赖项相关联,在这种情况下,依赖项的存在并不明显。例如,如果向系统添加了一个新的错误状态,可能需要向一个包含每个状态的字符串消息的表添加一个条目,但是对于查看状态声明的程序员来说,消息表的存在可能并不明显。不一致性也是造成不透明性的一个主要原因:如果同一个变量名用于两个不同的目的,那么开发人员就无法清楚地知道某个特定变量的目的是什么。

In many cases, obscurity comes about because of inadequate documentation; Chapter 13 deals with this topic. However, obscurity is also a design issue. If a system has a clean and obvious design, then it will need less documentation. The need for extensive documentation is often a red flag that the design isn’t quite right. The best way to reduce obscurity is by simplifying the system design.

在许多情况下,由于文档不足而导致模糊不清。第 13 章讨论了这个主题。但是,模糊性也是设计问题。如果系统设计简洁明了,则所需的文档将更少。对大量文档的需求通常是一个警告,即设计不正确。减少模糊性的最佳方法是简化系统设计。

Together, dependencies and obscurity account for the three manifestations of complexity described in Section 2.2. Dependencies lead to change amplification and a high cognitive load. Obscurity creates unknown unknowns, and also contributes to cognitive load. If we can find design techniques that minimize dependencies and obscurity, then we can reduce the complexity of software.

依赖性和模糊性共同构成了第 2.2 节中描述的三种复杂性表现。依赖性导致变化放大和高认知负荷。晦涩会产生未知的未知数,还会增加认知负担。如果我们找到最小化依赖关系和模糊性的设计技术,那么我们就可以降低软件的复杂性。

2.4 Complexity is incremental 复杂度是递增的

Complexity isn’t caused by a single catastrophic error; it accumulates in lots of small chunks. A single dependency or obscurity, by itself, is unlikely to affect significantly the maintainability of a software system. Complexity comes about because hundreds or thousands of small dependencies and obscurities build up over time. Eventually, there are so many of these small issues that every possible change to the system is affected by several of them.

复杂性不是由单个灾难性错误引起的;它堆积成许多小块。单个依赖项或模糊性本身不太可能显着影响软件系统的可维护性。之所以会出现复杂性,是因为随着时间的流逝,成千上万的小依赖性和模糊性逐渐形成。最终,这些小问题太多了,以至于对系统的每次可能更改都会受到其中几个问题的影响。

The incremental nature of complexity makes it hard to control. It’s easy to convince yourself that a little bit of complexity introduced by your current change is no big deal. However, if every developer takes this approach for every change, complexity accumulates rapidly. Once complexity has accumulated, it is hard to eliminate, since fixing a single dependency or obscurity will not, by itself, make a big difference. In order to slow the growth of complexity, you must adopt a “zero tolerance” philosophy, as discussed in Chapter 3.

复杂性的增量性质使其难以控制。可以很容易地说服自己,当前更改所带来的一点点复杂性没什么大不了的。但是,如果每个开发人员对每种更改都采用这种方法,那么复杂性就会迅速累积。一旦积累了复杂性,就很难消除它,因为修复单个依赖项或模糊性本身不会产生很大的变化。为了减缓复杂性的增长,您必须采用第 3 章中讨论的“零容忍”理念。

2.5 Conclusion 结论

Complexity comes from an accumulation of dependencies and obscurities. As complexity increases, it leads to change amplification, a high cognitive load, and unknown unknowns. As a result, it takes more code modifications to implement each new feature. In addition, developers spend more time acquiring enough information to make the change safely and, in the worst case, they can’t even find all the information they need. The bottom line is that complexity makes it difficult and risky to modify an existing code base.

复杂性来自于依赖性和模糊性的积累。随着复杂性的增加,它会导致变化放大,高认知负荷和未知的未知数。结果,需要更多的代码修改才能实现每个新功能。此外,开发人员花费更多时间获取足够的信息以安全地进行更改,在最坏的情况下,他们甚至找不到所需的所有信息。最重要的是,复杂性使得修改现有代码库变得困难且冒险。

',63)]))}const d=t(r,[["render",c],["__file","ch02.html.vue"]]),p=JSON.parse('{"path":"/ch02.html","title":"第 2 章 复杂性的本质","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"2.1 Complexity defined 复杂性的定义","slug":"_2-1-complexity-defined-复杂性的定义","link":"#_2-1-complexity-defined-复杂性的定义","children":[]},{"level":2,"title":"2.2 Symptoms of complexity 复杂性的症状","slug":"_2-2-symptoms-of-complexity-复杂性的症状","link":"#_2-2-symptoms-of-complexity-复杂性的症状","children":[]},{"level":2,"title":"2.3 Causes of complexity 复杂性的原因","slug":"_2-3-causes-of-complexity-复杂性的原因","link":"#_2-3-causes-of-complexity-复杂性的原因","children":[]},{"level":2,"title":"2.4 Complexity is incremental 复杂度是递增的","slug":"_2-4-complexity-is-incremental-复杂度是递增的","link":"#_2-4-complexity-is-incremental-复杂度是递增的","children":[]},{"level":2,"title":"2.5 Conclusion 结论","slug":"_2-5-conclusion-结论","link":"#_2-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch02.md"}');export{d as comp,p as data}; +import{_ as t,c as o,f as i,o as a}from"./app-BQdhfat9.js";const s="data:image/gif;base64,R0lGODlhTwAhAOcAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwMDcwKbK8P/w1P/isf/Ujv/Ga/+4SP+qJf+qANySALl6AJZiAHNKAFAyAP/j1P/Hsf+rjv+Pa/9zSP9XJf9VANxJALk9AJYxAHMlAFAZAP/U1P+xsf+Ojv9ra/9ISP8lJf4AANwAALkAAJYAAHMAAFAAAP/U4/+xx/+Oq/9rj/9Ic/8lV/8AVdwASbkAPZYAMXMAJVAAGf/U8P+x4v+O1P9rxv9IuP8lqv8AqtwAkrkAepYAYnMASlAAMv/U//+x//+O//9r//9I//8l//4A/twA3LkAuZYAlnMAc1AAUPDU/+Kx/9SO/8Zr/7hI/6ol/6oA/5IA3HoAuWIAlkoAczIAUOPU/8ex/6uO/49r/3NI/1cl/1UA/0kA3D0AuTEAliUAcxkAUNTU/7Gx/46O/2tr/0hI/yUl/wAA/gAA3AAAuQAAlgAAcwAAUNTj/7HH/46r/2uP/0hz/yVX/wBV/wBJ3AA9uQAxlgAlcwAZUNTw/7Hi/47U/2vG/0i4/yWq/wCq/wCS3AB6uQBilgBKcwAyUNT//7H//47//2v//0j//yX//wD+/gDc3AC5uQCWlgBzcwBQUNT/8LH/4o7/1Gv/xkj/uCX/qgD/qgDckgC5egCWYgBzSgBQMtT/47H/x47/q2v/j0j/cyX/VwD/VQDcSQC5PQCWMQBzJQBQGdT/1LH/sY7/jmv/a0j/SCX/JQD+AADcAAC5AACWAABzAABQAOP/1Mf/sav/jo//a3P/SFf/JVX/AEncAD25ADGWACVzABlQAPD/1OL/sdT/jsb/a7j/SKr/Jar/AJLcAHq5AGKWAEpzADJQAP//1P//sf//jv//a///SP//Jf7+ANzcALm5AJaWAHNzAFBQAPLy8ubm5tra2s7OzsLCwra2tqqqqp6enpKSkoaGhnp6em5ubmJiYlZWVkpKSj4+PjIyMiYmJhoaGg4ODv/78KCgpICAgP8AAAD/AP//AAAA//8A/wD//////yH5BAAAAP8ALAAAAABPACEAAAj+AP8JHEiwoMGDA88BWMiwIUNyCCNKnEix4j9x8gCUQ6gQosWPIEMKHAdA3riD5diJE8myZURzANqtdEnz37h17di1e4eu5r90ANT5bHlO3kaB6tINbQfg3FCC4tr1fAnAo0BzVmli1PhUIDmuCMUBUNq1IEmTFM+xk/du5sF0Gd+9yzoQHYCTZQseiOnWoDh2TsX2NbhWYjt5Fh0yJDsRKLuI7B5fnShPMkIAlvMSPID24Fe6EcVOvdxOc8Fynd8CGCzOHLty5uSR/Xr0omvYstmtrqh4IWOE497hRagb6sW96M7BFKjw5EpxyM/t/drOObnRPsfBG47QrlNy6Zy9/vva81zpn3fvHSX/z/z4tfKkcqcZfP5BceoA8HR7ru0/dgd4FZ9V/a0EoGkjzUXTOj2Nk5lB6jT4YFna2ZeQeBK9I486GCKkIYcI2iRchhMWJJaFJt4VokAZ9eZQiQR9VZGMK/6jTmQ45phjhweNEyBFPtYo5JBEFmnkkUgmWRB46Yyz02BK1kTOO+qsI1ZtUQ7FljgnZvnUlf+U446XT5GD2H9QksmSQuygk6aaIjEIZ1fv/DinT+e8uWJAADsA",n="/A-Philosophy-of-Software-Design-zh/assets/00010-BHBDpSUQ.jpeg",r={};function c(l,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 2 章 复杂性的本质

Chapter 2 The Nature of Complexity

This book is about how to design software systems to minimize their complexity. The first step is to understand the enemy. Exactly what is “complexity”? How can you tell if a system is unnecessarily complex? What causes systems to become complex? This chapter will address those questions at a high level; subsequent chapters will show you how to recognize complexity at a lower level, in terms of specific structural features.

这本书是关于如何设计软件系统以最小化其复杂性。第一步是了解敌人。究竟什么是“复杂性”?您如何判断系统是否过于复杂?是什么导致系统变得复杂?本章将在较高层次上解决这些问题。后续章节将向您展示如何从较低的层次上根据特定的结构特征来识别复杂性。

The ability to recognize complexity is a crucial design skill. It allows you to identify problems before you invest a lot of effort in them, and it allows you to make good choices among alternatives. It is easier to tell whether a design is simple than it is to create a simple design, but once you can recognize that a system is too complicated, you can use that ability to guide your design philosophy towards simplicity. If a design appears complicated, try a different approach and see if that is simpler. Over time, you will notice that certain techniques tend to result in simpler designs, while others correlate with complexity. This will allow you to produce simpler designs more quickly.

识别复杂性的能力是至关重要的设计技能。它使您可以先找出问题,然后再付出大量努力,并可以在其他选择中做出正确的选择。判断一个设计是否简单比创建一个简单的设计要容易得多,但是一旦您认识到一个系统过于复杂,就可以使用该功能指导您的设计哲学走向简单。如果设计看起来很复杂,请尝试其他方法,看看是否更简单。随着时间的流逝,您会注意到某些技术往往会导致设计更简单,而其他技术则与复杂性相关。这将使您更快地制作更简单的设计。

This chapter also lays out some basic assumptions that provide a foundation for the rest of the book. Later chapters take the material of this chapter as given and use it to justify a variety of refinements and conclusions.

本章还列出了一些基本假设,这些基本假设为本书的其余部分奠定了基础。后面的章节将采用本章的内容,并用其论证各种改进和结论。

2.1 Complexity defined 复杂性的定义

For the purposes of this book, I define “complexity” in a practical way. Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system. Complexity can take many forms. For example, it might be hard to understand how a piece of code works; it might take a lot of effort to implement a small improvement, or it might not be clear which parts of the system must be modified to make the improvement; it might be difficult to fix one bug without introducing another. If a software system is hard to understand and modify, then it is complicated; if it is easy to understand and modify, then it is simple.

出于本书的目的,我以实用的方式定义“复杂性”。复杂性与软件系统的结构有关,这使它很难理解和修改系统(复杂性是指那些让系统难以理解或修改的与系统相关的任何事物)。复杂性可以采取多种形式。例如,可能很难理解一段代码是如何工作的。可能需要花费很多精力才能实现较小的改进,或者可能不清楚必须修改系统的哪些部分才能进行改进;如果不引入其他错误,可能很难修复(也可以是不引入额外问题的情况下,很难修复一个bug)。如果一个软件系统难以理解和修改,那就很复杂。如果很容易理解和修改,那就很简单。

You can also think of complexity in terms of cost and benefit. In a complex system, it takes a lot of work to implement even small improvements. In a simple system, larger improvements can be implemented with less effort.

您还可以考虑成本和收益方面的复杂性(你还可以根据成本和收益来评估复杂性)。在复杂的系统中,要实施甚至很小的改进都需要大量的工作。在一个简单的系统中,可以用更少的精力实现更大的改进。

Complexity is what a developer experiences at a particular point in time when trying to achieve a particular goal. It doesn’t necessarily relate to the overall size or functionality of the system. People often use the word “complex” to describe large systems with sophisticated features, but if such a system is easy to work on, then, for the purposes of this book, it is not complex. Of course, almost all large and sophisticated software systems are in fact hard to work on, so they also meet my definition of complexity, but this need not necessarily be the case. It is also possible for a small and unsophisticated system to be quite complex.

复杂性是开发人员在尝试实现特定目标时在特定时间点所经历的。它不一定与系统的整体大小或功能有关。人们通常使用“复杂”一词来描述具有复杂功能的大型系统,但是如果这样的系统易于使用,那么就本书而言,它并不复杂。当然,实际上几乎所有大型复杂的软件系统都很难使用,因此它们也符合我对复杂性的定义,但这不一定是事实。小型而不复杂的系统也可能非常复杂。

Complexity is determined by the activities that are most common. If a system has a few parts that are very complicated, but those parts almost never need to be touched, then they don’t have much impact on the overall complexity of the system. To characterize this in a crude mathematical way:

复杂性取决于最常见的活动。如果系统中有一些非常复杂的部分,但是几乎不需要触摸这些部分,那么它们对系统的整体复杂性不会有太大影响。为了用粗略的数学方法来表征:

The overall complexity of a system (C) is determined by the complexity of each part p (cp) weighted by the fraction of time developers spend working on that part (tp). Isolating complexity in a place where it will never be seen is almost as good as eliminating the complexity entirely.

系统的总体复杂度(C)由每个部分的复杂度(cp)乘以开发人员在该部分上花费的时间(tp)加权。在一个永远不会被看到的地方隔离复杂性几乎和完全消除复杂性一样好。

Complexity is more apparent to readers than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex. When you find yourself in situations like this, it’s worth probing the other developers to find out why the code seems complex to them; there are probably some interesting lessons to learn from the disconnect between your opinion and theirs. Your job as a developer is not just to create code that you can work with easily, but to create code that others can also work with easily.

读者比作家更容易理解复杂性。如果您编写了一段代码,对您来说似乎很简单,但是其他人则认为它很复杂,那么它就是复杂的。当您遇到这种情况时,有必要对其他开发人员进行调查,以找出为什么代码对他们而言似乎很复杂;从您的观点与观点之间的脱节中可能可以学习一些有趣的课程。作为开发人员,您的工作不仅是创建可以轻松使用的代码,而且还要创建其他人也可以轻松使用的代码。

2.2 Symptoms of complexity 复杂性的症状

Complexity manifests itself in three general ways, which are described in the paragraphs below. Each of these manifestations makes it harder to carry out development tasks.

复杂性通过以下三种段落中描述的三种一般方式体现出来。这些表现形式中的每一个都使执行开发任务变得更加困难。

Change amplification: The first symptom of complexity is that a seemingly simple change requires code modifications in many different places. For example, consider a Web site containing several pages, each of which displays a banner with a background color. In many early Web sites, the color was specified explicitly on each page, as shown in Figure 2.1(a). In order to change the background for such a Web site, a developer might have to modify every existing page by hand; this would be nearly impossible for a large site with thousands of pages. Fortunately, modern Web sites use an approach like that in Figure 2.1(b), where the banner color is specified once in a central place, and all of the individual pages reference that shared value. With this approach, the banner color of the entire Web site can be changed with a single modification. One of the goals of good design is to reduce the amount of code that is affected by each design decision, so design changes don’t require very many code modifications.

变更放大:复杂性的第一个征兆是,看似简单的变更需要在许多不同地方进行代码修改。例如,考虑一个包含几个页面的网站,每个页面显示带有背景色的横幅。在许多早期的网站中,颜色是在每个页面上明确指定的,如图 2.1(a)所示。为了更改此类网站的背景,开发人员可能必须手动修改每个现有页面;对于拥有数千个页面的大型网站而言,这几乎是不可能的。幸运的是,现代网站使用的方法类似于图 2.1(b),其中横幅颜色一次在中心位置指定,并且所有各个页面均引用该共享值。使用这种方法,可以通过一次修改来更改整个网站的标题颜色。

Cognitive load: The second symptom of complexity is cognitive load, which refers to how much a developer needs to know in order to complete a task. A higher cognitive load means that developers have to spend more time learning the required information, and there is a greater risk of bugs because they have missed something important. For example, suppose a function in C allocates memory, returns a pointer to that memory, and assumes that the caller will free the memory. This adds to the cognitive load of developers using the function; if a developer fails to free the memory, there will be a memory leak. If the system can be restructured so that the caller doesn’t need to worry about freeing the memory (the same module that allocates the memory also takes responsibility for freeing it), it will reduce the cognitive load. Cognitive load arises in many ways, such as APIs with many methods, global variables, inconsistencies, and dependencies between modules.

认知负荷:复杂性的第二个症状是认知负荷,这是指开发人员需要多少知识才能完成一项任务。较高的认知负担意味着开发人员必须花更多的时间来学习所需的信息,并且由于错过了重要的东西而导致错误的风险也更大。例如,假设 C 中的一个函数分配了内存,返回了指向该内存的指针,并假定调用者将释放该内存。这增加了使用该功能的开发人员的认知负担。如果开发人员无法释放内存,则会发生内存泄漏。如果可以对系统进行重组,以使调用者不必担心释放内存(分配内存的同一模块也负责释放内存),它将减少认知负担。(认知负荷出现在很多方面,例如很多方法的API,全局变量,不一致和模块间依赖)

System designers sometimes assume that complexity can be measured by lines of code. They assume that if one implementation is shorter than another, then it must be simpler; if it only takes a few lines of code to make a change, then the change must be easy. However, this view ignores the costs associated with cognitive load. I have seen frameworks that allowed applications to be written with only a few lines of code, but it was extremely difficult to figure out what those lines were. Sometimes an approach that requires more lines of code is actually simpler, because it reduces cognitive load.

系统设计人员有时会假设可以通过代码行来衡量复杂性。他们认为,如果一个实现比另一个实现短,那么它必须更简单;如果只需要几行代码就可以进行更改,那么更改必须很容易。但是,这种观点忽略了与认知负荷相关的成本。我已经看到了仅允许使用几行代码编写应用程序的框架,但是要弄清楚这些行是什么极其困难。有时,需要更多代码行的方法实际上更简单,因为它减少了认知负担。

Figure 2.1: Each page in a Web site displays a colored banner. In (a) the background color for the banner is specified explicitly in each page. In (b) a shared variable holds the background color and each page references that variable. In (c) some pages display an additional color for emphasis, which is a darker shade of the banner background color; if the background color changes, the emphasis color must also change.

图 2.1:网站中的每个页面都显示一个彩色横幅。在(a)中,横幅的背景色在每页中都明确指定。在(b)中,共享变量保留背景色,并且每个页面都引用该变量。在(c)中,某些页面会显示其他用于强调的颜色,即横幅背景颜色的暗色;如果背景颜色改变,则强调颜色也必须改变。

Unknown unknowns: The third symptom of complexity is that it is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully. Figure 2.1(c) illustrates this problem. The Web site uses a central variable to determine the banner background color, so it appears to be easy to change. However, a few Web pages use a darker shade of the background color for emphasis, and that darker color is specified explicitly in the individual pages. If the background color changes, then the the emphasis color must change to match. Unfortunately, developers are unlikely to realize this, so they may change the central bannerBg variable without updating the emphasis color. Even if a developer is aware of the problem, it won’t be obvious which pages use the emphasis color, so the developer may have to search every page in the Web site.

未知的未知: 复杂性的第三个症状是,必须修改哪些代码才能完成任务,或者开发人员必须获得哪些信息才能成功地执行任务,这些都是不明显的。图 2.1(c)说明了这个问题。网站使用一个中心变量来确定横幅的背景颜色,所以它看起来很容易改变。但是,一些 Web 页面使用较暗的背景色来强调,并且在各个页面中明确指定了较暗的颜色。如果背景颜色改变,那么强调的颜色必须改变以匹配。不幸的是,开发人员不太可能意识到这一点,所以他们可能会更改中央 bannerBg 变量而不更新强调颜色。即使开发人员意识到这个问题,也不清楚哪些页面使用了强调色,因此开发人员可能必须搜索 Web 站点中的每个页面。

Of the three manifestations of complexity, unknown unknowns are the worst. An unknown unknown means that there is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won’t find out about it until bugs appear after you make a change. Change amplification is annoying, but as long as it is clear which code needs to be modified, the system will work once the change has been completed. Similarly, a high cognitive load will increase the cost of a change, but if it is clear which information to read, the change is still likely to be correct. With unknown unknowns, it is unclear what to do or whether a proposed solution will even work. The only way to be certain is to read every line of code in the system, which is impossible for systems of any size. Even this may not be sufficient, because a change may depend on a subtle design decision that was never documented.

在复杂性的三种表现形式中,未知的未知是最糟糕的。一个未知的未知意味着你需要知道一些事情,但是你没有办法找到它是什么,甚至是否有一个问题。你不会发现它,直到错误出现后,你做了一个改变。更改放大是令人恼火的,但是只要清楚哪些代码需要修改,一旦更改完成,系统就会工作。同样,高的认知负荷会增加改变的成本,但如果明确要阅读哪些信息,改变仍然可能是正确的。对于未知的未知,不清楚该做什么,或者提出的解决方案是否有效。唯一确定的方法是读取系统中的每一行代码,这对于任何大小的系统都是不可能的。甚至这可能还不够,因为更改可能依赖于一个从未记录的细微设计决策。

One of the most important goals of good design is for a system to be obvious. This is the opposite of high cognitive load and unknown unknowns. In an obvious system, a developer can quickly understand how the existing code works and what is required to make a change. An obvious system is one where a developer can make a quick guess about what to do, without thinking very hard, and yet be confident that the guess is correct. Chapter 18 discusses techniques for making code more obvious.

良好设计的最重要目标之一就是使系统显而易见。这与高认知负荷和未知未知数相反。在一个显而易见的系统中,开发人员可以快速了解现有代码的工作方式以及进行更改所需的内容。一个显而易见的系统是,开发人员可以在不费力地思考的情况下快速猜测要做什么,同时又可以确信该猜测是正确的。第 18 章讨论使代码更明显的技术。

2.3 Causes of complexity 复杂性的原因

Now that you know the high-level symptoms of complexity and why complexity makes software development difficult, the next step is to understand what causes complexity, so that we can design systems to avoid the problems. Complexity is caused by two things: dependencies and obscurity. This section discusses these factors at a high level; subsequent chapters will discuss how they relate to lower-level design decisions.

既然您已经了解了复杂性的高级症状以及为什么复杂性会使软件开发变得困难,那么下一步就是了解导致复杂性的原因,以便我们设计系统来避免这些问题。复杂性是由两件事引起的:依赖性和模糊性。本节从高层次讨论这些因素。随后的章节将讨论它们与低级设计决策之间的关系。

For the purposes of this book, a dependency exists when a given piece of code cannot be understood and modified in isolation; the code relates in some way to other code, and the other code must be considered and/or modified if the given code is changed. In the Web site example of Figure 2.1(a), the background color creates dependencies between all of the pages. All of the pages need to have the same background, so if the background is changed for one page, then it must be changed for all of them. Another example of dependencies occurs in network protocols. Typically there is separate code for the sender and receiver for the protocol, but they must each conform to the protocol; changing the code for the sender almost always requires corresponding changes at the receiver, and vice versa. The signature of a method creates a dependency between the implementation of that method and the code that invokes it: if a new parameter is added to a method, all of the invocations of that method must be modified to specify that parameter.

就本书而言,当无法孤立地理解和修改给定的一段代码时,便存在依赖关系。该代码以某种方式与其他代码相关,如果更改了给定代码,则必须考虑和/或修改其他代码。在图 2.1(a)的网站示例中,背景色在所有页面之间创建了依赖关系。所有页面都必须具有相同的背景,因此,如果更改一页的背景,则必须更改所有背景。依赖关系的另一个示例发生在网络协议中。通常,协议的发送方和接收方有单独的代码,但是它们必须分别符合协议。更改发送方的代码几乎总是需要在接收方进行相应的更改,反之亦然。

Dependencies are a fundamental part of software and can’t be completely eliminated. In fact, we intentionally introduce dependencies as part of the software design process. Every time you write a new class you create dependencies around the API for that class. However, one of the goals of software design is to reduce the number of dependencies and to make the dependencies that remain as simple and obvious as possible.

依赖关系是软件的基本组成部分,不能完全消除。实际上,我们在软件设计过程中有意引入了依赖性。每次编写新类时,都会围绕该类的 API 创建依赖关系。但是,软件设计的目标之一是减少依赖关系的数量,并使依赖关系保持尽可能简单和明显。

Consider the Web site example. In the old Web site with the background specified separately on each page, all of the Web pages were dependent on each other. The new Web site fixed this problem by specifying the background color in a central place and providing an API that individual pages use to retrieve that color when they are rendered. The new Web site eliminated the dependency between the pages, but it created a new dependency around the API for retrieving the background color. Fortunately, the new dependency is more obvious: it is clear that each individual Web page depends on the bannerBg color, and a developer can easily find all the places where the variable is used by searching for its name. Furthermore, compilers help to manage API dependencies: if the name of the shared variable changes, compilation errors will occur in any code that still uses the old name. The new Web site replaced a nonobvious and difficult-to-manage dependency with a simpler and more obvious one.

考虑网站示例。在每个页面分别指定背景的旧网站中,所有网页都是相互依赖的。新的网站通过在中心位置指定背景色并提供一个 API,供各个页面在呈现它们时检索该颜色,从而解决了该问题。新的网站消除了页面之间的依赖关系,但是它围绕 API 创建了一个新的依赖关系以检索背景色。幸运的是,新的依赖性更加明显:很明显,每个单独的网页都取决于 bannerBg 颜色,并且开发人员可以通过搜索其名称轻松找到使用该变量的所有位置。此外,编译器还有助于管理 API 依赖性:如果共享变量的名称发生变化,任何仍使用旧名称的代码都将发生编译错误。新的网站用一种更简单,更明显的方式代替了一种不明显且难以管理的依赖性。

The second cause of complexity is obscurity. Obscurity occurs when important information is not obvious. A simple example is a variable name that is so generic that it doesn’t carry much useful information (e.g., time). Or, the documentation for a variable might not specify its units, so the only way to find out is to scan code for places where the variable is used. Obscurity is often associated with dependencies, where it is not obvious that a dependency exists. For example, if a new error status is added to a system, it may be necessary to add an entry to a table holding string messages for each status, but the existence of the message table might not be obvious to a programmer looking at the status declaration. Inconsistency is also a major contributor to obscurity: if the same variable name is used for two different purposes, it won’t be obvious to developer which of these purposes a particular variable serves.

复杂性的第二个原因是晦涩。当重要的信息不明显时,就会发生模糊。一个简单的例子是一个变量名,它是如此的通用,以至于它没有携带太多有用的信息(例如,时间)。或者,一个变量的文档可能没有指定它的单位,所以找到它的惟一方法是扫描代码,查找使用该变量的位置。晦涩常常与依赖项相关联,在这种情况下,依赖项的存在并不明显。例如,如果向系统添加了一个新的错误状态,可能需要向一个包含每个状态的字符串消息的表添加一个条目,但是对于查看状态声明的程序员来说,消息表的存在可能并不明显。不一致性也是造成不透明性的一个主要原因:如果同一个变量名用于两个不同的目的,那么开发人员就无法清楚地知道某个特定变量的目的是什么。

In many cases, obscurity comes about because of inadequate documentation; Chapter 13 deals with this topic. However, obscurity is also a design issue. If a system has a clean and obvious design, then it will need less documentation. The need for extensive documentation is often a red flag that the design isn’t quite right. The best way to reduce obscurity is by simplifying the system design.

在许多情况下,由于文档不足而导致模糊不清。第 13 章讨论了这个主题。但是,模糊性也是设计问题。如果系统设计简洁明了,则所需的文档将更少。对大量文档的需求通常是一个警告,即设计不正确。减少模糊性的最佳方法是简化系统设计。

Together, dependencies and obscurity account for the three manifestations of complexity described in Section 2.2. Dependencies lead to change amplification and a high cognitive load. Obscurity creates unknown unknowns, and also contributes to cognitive load. If we can find design techniques that minimize dependencies and obscurity, then we can reduce the complexity of software.

依赖性和模糊性共同构成了第 2.2 节中描述的三种复杂性表现。依赖性导致变化放大和高认知负荷。晦涩会产生未知的未知数,还会增加认知负担。如果我们找到最小化依赖关系和模糊性的设计技术,那么我们就可以降低软件的复杂性。

2.4 Complexity is incremental 复杂度是递增的

Complexity isn’t caused by a single catastrophic error; it accumulates in lots of small chunks. A single dependency or obscurity, by itself, is unlikely to affect significantly the maintainability of a software system. Complexity comes about because hundreds or thousands of small dependencies and obscurities build up over time. Eventually, there are so many of these small issues that every possible change to the system is affected by several of them.

复杂性不是由单个灾难性错误引起的;它堆积成许多小块。单个依赖项或模糊性本身不太可能显着影响软件系统的可维护性。之所以会出现复杂性,是因为随着时间的流逝,成千上万的小依赖性和模糊性逐渐形成。最终,这些小问题太多了,以至于对系统的每次可能更改都会受到其中几个问题的影响。

The incremental nature of complexity makes it hard to control. It’s easy to convince yourself that a little bit of complexity introduced by your current change is no big deal. However, if every developer takes this approach for every change, complexity accumulates rapidly. Once complexity has accumulated, it is hard to eliminate, since fixing a single dependency or obscurity will not, by itself, make a big difference. In order to slow the growth of complexity, you must adopt a “zero tolerance” philosophy, as discussed in Chapter 3.

复杂性的增量性质使其难以控制。可以很容易地说服自己,当前更改所带来的一点点复杂性没什么大不了的。但是,如果每个开发人员对每种更改都采用这种方法,那么复杂性就会迅速累积。一旦积累了复杂性,就很难消除它,因为修复单个依赖项或模糊性本身不会产生很大的变化。为了减缓复杂性的增长,您必须采用第 3 章中讨论的“零容忍”理念。

2.5 Conclusion 结论

Complexity comes from an accumulation of dependencies and obscurities. As complexity increases, it leads to change amplification, a high cognitive load, and unknown unknowns. As a result, it takes more code modifications to implement each new feature. In addition, developers spend more time acquiring enough information to make the change safely and, in the worst case, they can’t even find all the information they need. The bottom line is that complexity makes it difficult and risky to modify an existing code base.

复杂性来自于依赖性和模糊性的积累。随着复杂性的增加,它会导致变化放大,高认知负荷和未知的未知数。结果,需要更多的代码修改才能实现每个新功能。此外,开发人员花费更多时间获取足够的信息以安全地进行更改,在最坏的情况下,他们甚至找不到所需的所有信息。最重要的是,复杂性使得修改现有代码库变得困难且冒险。

',63)]))}const d=t(r,[["render",c],["__file","ch02.html.vue"]]),p=JSON.parse('{"path":"/ch02.html","title":"第 2 章 复杂性的本质","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"2.1 Complexity defined 复杂性的定义","slug":"_2-1-complexity-defined-复杂性的定义","link":"#_2-1-complexity-defined-复杂性的定义","children":[]},{"level":2,"title":"2.2 Symptoms of complexity 复杂性的症状","slug":"_2-2-symptoms-of-complexity-复杂性的症状","link":"#_2-2-symptoms-of-complexity-复杂性的症状","children":[]},{"level":2,"title":"2.3 Causes of complexity 复杂性的原因","slug":"_2-3-causes-of-complexity-复杂性的原因","link":"#_2-3-causes-of-complexity-复杂性的原因","children":[]},{"level":2,"title":"2.4 Complexity is incremental 复杂度是递增的","slug":"_2-4-complexity-is-incremental-复杂度是递增的","link":"#_2-4-complexity-is-incremental-复杂度是递增的","children":[]},{"level":2,"title":"2.5 Conclusion 结论","slug":"_2-5-conclusion-结论","link":"#_2-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch02.md"}');export{d as comp,p as data}; diff --git a/assets/ch03.html-umPzp8To.js b/assets/ch03.html-V7G6GftO.js similarity index 99% rename from assets/ch03.html-umPzp8To.js rename to assets/ch03.html-V7G6GftO.js index 58b2a994..cda7a49b 100644 --- a/assets/ch03.html-umPzp8To.js +++ b/assets/ch03.html-V7G6GftO.js @@ -1 +1 @@ -import{_ as t,c as o,f as a,o as i}from"./app-CvqtBB8Z.js";const s="/A-Philosophy-of-Software-Design-zh/assets/00011-CNtqviGr.jpeg",n={};function r(l,e){return i(),o("div",null,e[0]||(e[0]=[a('

第 3 章 工作代码是不够的

Chapter 3 Working Code Isn’t Enough (Strategic vs. Tactical Programming)

One of the most important elements of good software design is the mindset you adopt when you approach a programming task. Many organizations encourage a tactical mindset, focused on getting features working as quickly as possible. However, if you want a good design, you must take a more strategic approach where you invest time to produce clean designs and fix problems. This chapter discusses why the strategic approach produces better designs and is actually cheaper than the tactical approach over the long run.

好的软件设计中最重要的元素之一是您在执行编程任务时所采用的思维方式。许多组织都鼓励采取战术思维方式,着眼于使功能尽快运行。但是,如果您想要一个好的设计,则必须采取更具战略性的方法,在此上花费时间来制作干净的设计并解决问题。本章讨论了从长远来看,为什么战略方法可以产生更好的设计,而实际上却比战术方法便宜。

3.1 Tactical programming 战术编程

Most programmers approach software development with a mindset I call tactical programming. In the tactical approach, your main focus is to get something working, such as a new feature or a bug fix. At first glance this seems totally reasonable: what could be more important than writing code that works? However, tactical programming makes it nearly impossible to produce a good system design.

大多数程序员以我称为战术编程的心态来进行软件开发(针对大多数的开发人员的编码时的思维方式,作者称之为战术式编码)。在战术方法中,您的主要重点是使某些功能正常工作,例如新功能或错误修复。乍一看,这似乎是完全合理的:还有什么比编写有效的代码更重要的呢?但是,战术编程几乎不可能产生出良好的系统设计。

The problem with tactical programming is that it is short-sighted. If you’re programming tactically, you’re trying to finish a task as quickly as possible. Perhaps you have a hard deadline. As a result, planning for the future isn’t a priority. You don’t spend much time looking for the best design; you just want to get something working soon. You tell yourself that it’s OK to add a bit of complexity or introduce a small kludge or two, if that allows the current task to be completed more quickly.

战术编程的问题是它是短视的。如果您是战术编程人员,那么您将尝试尽快完成任务。也许您有一个艰难的期限。因此,为未来做计划不是优先事项。您不会花费太多时间来寻找最佳设计。您只想尽快使某件事起作用。您告诉自己,可以增加一些复杂性或引入一两个小错误,如果这样可以使当前任务更快地完成,则可以。(多数人还会自我安慰,如果可以让功能尽快上线的话,提高一些复杂度或者引入一两个小问题不是什么大不了的事情)

This is how systems become complicated. As discussed in the previous chapter, complexity is incremental. It’s not one particular thing that makes a system complicated, but the accumulation of dozens or hundreds of small things. If you program tactically, each programming task will contribute a few of these complexities. Each of them probably seems like a reasonable compromise in order to finish the current task quickly. However, the complexities accumulate rapidly, especially if everyone is programming tactically.

这就是系统变得复杂的方式。如上一章所述,复杂度是递增的。不是使系统复杂的特定事物,而是数十或数百个小事物的积累(复杂性的提升,不是由一个特定的事物引起的,而是由数十或成百的小事物积累导致的)。如果您进行战术编程(如果编码时总是使用战术式思维方式),则每个编程任务都会带来一些此类复杂性。为了快速完成当前任务,他们每个人似乎都是一个合理的折衷方案。但是,复杂性迅速累积,尤其是如果每个人都在战术上进行编程的时候。

Before long, some of the complexities will start causing problems, and you will begin to wish you hadn’t taken those early shortcuts. But, you will tell yourself that it’s more important to get the next feature working than to go back and refactor existing code. Refactoring may help out in the long run, but it will definitely slow down the current task. So, you look for quick patches to work around any problems you encounter. This just creates more complexity, which then requires more patches. Pretty soon the code is a mess, but by this point things are so bad that it would take months of work to clean it up. There’s no way your schedule can tolerate that kind of delay, and fixing one or two of the problems doesn’t seem like it will make much difference, so you just keep programming tactically.

不久之后,某些复杂性将开始引起问题,并且您将开始希望您没有采用这些早期的捷径。但是,您会告诉自己,使下一个功能正常工作比返回并重构现有代码更为重要。从长远来看,重构可能会有所帮助,但是肯定会减慢当前的任务。因此,您需要快速修补程序来解决遇到的任何问题。这只会增加复杂性,然后需要更多补丁。很快代码变得一团糟,但是到现在为止,情况已经很糟糕了,清理它需要花费数月的时间。您的日程安排无法容忍这种延迟,解决一个或两个问题似乎并没有太大的区别,因此您只是在战术上保持编程。

If you have worked on a large software project for very long, I suspect you have seen tactical programming at work and have experienced the problems that result. Once you start down the tactical path, it’s difficult to change.

如果您从事大型软件项目的时间很长,我怀疑您在工作中已经看到了战术编程,并且遇到了导致的问题。一旦您沿着战术路线走,就很难改变。

Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado. The tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. When it comes to implementing a quick feature, nobody gets it done faster than the tactical tornado. In some organizations, management treats tactical tornadoes as heroes. However, tactical tornadoes leave behind a wake of destruction. They are rarely considered heroes by the engineers who must work with their code in the future. Typically, other engineers must clean up the messes left behind by the tactical tornado, which makes it appear that those engineers (who are the real heroes) are making slower progress than the tactical tornado.

几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:战术龙卷风。战术龙卷风是一位多产的程序员,他抽出代码的速度比其他人快得多,但完全以战术方式工作。实施快速功能时,没有人能比战术龙卷风更快地完成任务。在某些组织中,管理层将战术龙卷风视为英雄。但是,战术龙卷风留下了毁灭的痕迹。他们很少被将来必须使用其代码的工程师视为英雄。通常,其他工程师必须清理战术龙卷风留下的混乱局面,这使得那些工程师(他们是真正的英雄)的进步似乎比战术龙卷风慢。

3.2 Strategic programming 战略规划

The first step towards becoming a good software designer is to realize that working code isn’t enough. It’s not acceptable to introduce unnecessary complexities in order to finish your current task faster. The most important thing is the long-term structure of the system. Most of the code in any system is written by extending the existing code base, so your most important job as a developer is to facilitate those future extensions. Thus, you should not think of “working code” as your primary goal, though of course your code must work. Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.

成为一名优秀的软件设计师的第一步是要意识到 能跑起来的的代码是不够的。引入不必要的复杂性以更快地完成当前任务是不可接受的。最重要的是系统的长期结构。任何系统中的大多数代码都是通过扩展现有代码库编写的,因此,作为开发人员,最重要的工作就是促进这些将来的扩展。因此,尽管您的代码当然必须工作,但您不应将“工作代码”视为主要目标。您的主要目标必须是制作出出色的设计,并且这种设计也会起作用。这是 战略计划

Strategic programming requires an investment mindset. Rather than taking the fastest path to finish your current project, you must invest time to improve the design of the system. These investments will slow you down a bit in the short term, but they will speed you up in the long term, as illustrated in Figure 3.1.

战略性编程需要一种投资心态。您必须花费时间来改进系统的设计,而不是采取最快的方式来完成当前的项目。这些投资会在短期内让您放慢脚步,但从长远来看会加快您的速度,如图 3.1 所示。

Some of the investments will be proactive. For example, it’s worth taking a little extra time to find a simple design for each new class; rather than implementing the first idea that comes to mind, try a couple of alternative designs and pick the cleanest one. Try to imagine a few ways in which the system might need to be changed in the future and make sure that will be easy with your design. Writing good documentation is another example of a proactive investment.

一些投资将是积极的。例如,值得花一些时间为每个新类找到一个简单的设计。而不是实施想到的第一个想法,请尝试几种替代设计并选择最简洁的设计。试想一下将来可能需要更改系统的几种方式,并确保设计容易。编写好的文档是主动投资的另一个例子。

Other investments will be reactive. No matter how much you invest up front, there will inevitably be mistakes in your design decisions. Over time, these mistakes will become obvious. When you discover a design problem, don’t just ignore it or patch around it; take a little extra time to fix it. If you program strategically, you will continually make small improvements to the system design. This is the opposite of tactical programming, where you are continually adding small bits of complexity that cause problems in the future.

其他投资将是被动的。无论您预先投入多少,设计决策中都不可避免地会出现错误。随着时间的流逝,这些错误将变得显而易见。发现设计问题时,不要只是忽略它或对其进行修补。花一些额外的时间来修复它。如果您进行战略性编程,则将不断对系统设计进行小幅改进。这与战术编程相反,在战术编程中,您不断增加一些复杂性,这些复杂性将来会引起问题。

3.3 How much to invest? 投资多少?

So, what is the right amount of investment? A huge up-front investment, such as trying to design the entire system, won’t be effective. This is the waterfall method, and we know it doesn’t work. The ideal design tends to emerge in bits and pieces, as you get experience with the system. Thus, the best approach is to make lots of small investments on a continual basis. I suggest spending about 10–20% of your total development time on investments. This amount is small enough that it won’t impact your schedules significantly, but large enough to produce significant benefits over time. Your initial projects will thus take 10–20% longer than they would in a purely tactical approach. That extra time will result in a better software design, and you will start experiencing the benefits within a few months. It won’t be long before you’re developing at least 10–20% faster than you would if you had programmed tactically. At this point your investments become free: the benefits from your past investments will save enough time to cover the cost of future investments. You will quickly recover the cost of the initial investment. Figure 3.1 illustrates this phenomenon.

那么,正确的投资额是多少?大量的前期投资(例如尝试设计整个系统)将不会有效。这是瀑布方法,我们知道它不起作用。随着您对系统的了解,理想的设计趋于零碎出现。因此,最好的方法是连续进行大量小额投资 ​​。我建议您将总开发时间的 10%到 20%用于投资。该金额足够小,不会对您的日程安排产生重大影响,但又足够大,可以随着时间的推移产生重大收益。因此,您的初始项目将比纯战术方法花费 10-20%的时间。额外的时间将带来更好的软件设计,并且您将在几个月内开始体验到这些好处。不久之后,您的开发速度将比战术编程快至少 10–20%。在这一点上,您的投资将免费:您过去投资的收益将节省足够的时间来支付未来投资的费用。您将迅速收回初始投资的成本。图 3.1 说明了这种现象。

Figure 3.1: At the beginning, a tactical approach to programming will make progress more quickly than a strategic approach. However, complexity accumulates more rapidly under the tactical approach, which reduces productivity. Over time, the strategic approach results in greater progress. Note: this figure is intended only as a qualitative illustration; I am not aware of any empirical measurements of the precise shapes of the curves.

图 3.1:一开始,战术性的编程方法将比战略性方法更快地取得进展。但是,在战术方法下,复杂性积累得更快,从而降低了生产率。随着时间的流逝,战略方针会带来更大的进步。注意:此图仅用于定性说明;我不知道对曲线精确形状的任何经验测量。

Conversely, if you program tactically, you will finish your first projects 10–20% faster, but over time your development speed will slow as complexity accumulates. It won’t be long before you’re programming at least 10–20% slower. You will quickly give back all of the time you saved at the beginning, and for the rest of system’s lifetime you will be developing more slowly than if you had taken the strategic approach. If you haven’t ever worked in a badly degraded code base, talk to someone who has; they will tell you that poor code quality slows development by at least 20%.

相反,如果您进行战术编程,则可以将第一个项目完成的速度提高 10%到 20%,但是随着时间的推移,复杂性的累积会降低开发速度。不久之后,您的编程速度至少会降低 10–20%。您将很快退回在开始时节省的所有时间,并且在系统的整个生命周期中,与采用策略性方法相比,您的开发速度将更加缓慢。如果您从未使用过严重降级的代码库,请与有经验的人联系。他们会告诉您不良的代码质量会使开发速度至少降低 20%。

3.4 Startups and investment 创业与投资

In some environments there are strong forces working against the strategic approach. For example, early-stage startups feel tremendous pressure to get their early releases out quickly. In these companies, it might seem that even a 10–20% investment isn’t affordable. As a result, many startups take a tactical approach, spending little effort on design and even less on cleanup when problems pop up. They rationalize this with the thought that, if they are successful, they’ll have enough money to hire extra engineers to clean things up.

在某些环境中,强大的力量与战略方法背道而驰。例如,早期的初创公司感到巨大的压力,需要尽快发布其早期版本。在这些公司中,甚至 10%至 20%的投资似乎也负担不起。结果,许多初创公司采取了战术性的方法,在设计上花费了很少的精力,而在问题出现时则花费了更少的精力进行清理。他们认为,如果成功,他们将有足够的钱聘请额外的工程师来清理问题,从而使其合理化。

If you are in a company leaning in this direction, you should realize that once a code base turns to spaghetti, it is nearly impossible to fix. You will probably pay high development costs for the life of the product. Furthermore, the payoff for good (or bad) design comes pretty quickly, so there’s a good chance that the tactical approach won’t even speed up your first product release.

如果您是一家朝着这个方向发展的公司,则应该意识到,一旦代码库变成了意大利面条,几乎是不可能修复的。您可能会为产品的使用寿命付出高昂的开发成本。此外,好的(或坏的)设计的回报很快就会到来,因此战术方法很有可能甚至不会加快您的首个产品发布的速度。

Another thing to consider is that one of the most important factors for success of a company is the quality of its engineers. The best way to lower development costs is to hire great engineers: they don’t cost much more than mediocre engineers but have tremendously higher productivity. However, the best engineers care deeply about good design. If your code base is a wreck, word will get out, and this will make it harder for you to recruit. As a result, you are likely to end up with mediocre engineers. This will increase your future costs and probably cause the system structure to degrade even more.

要考虑的另一件事是,公司成功的最重要因素之一就是工程师的素质。降低开发成本的最佳方法是聘请优秀的工程师:他们的成本不会比普通工程师高很多,但生产率却高得多。但是,最好的工程师对良好的设计深感兴趣。如果你的代码库很糟糕,消息传出去,你将更更难招募到好的工程师。最终可能还是只能使用普通的工程师。这将增加您的未来成本,并可能导致系统结构进一步退化。

Facebook is an example of a startup that encouraged tactical programming. For many years the company’s motto was “Move fast and break things.” New engineers fresh out of college were encouraged to dive immediately into the company’s code base; it was normal for engineers to push commits into production in their first week on the job. On the positive side, Facebook developed a reputation as a company that empowered its employees. Engineers had tremendous latitude, and there were few rules and restrictions to get in their way.

Facebook 是一个鼓励战术编程的创业公司的例子。多年来,公司的座右铭是“快速行动并打破困境”。鼓励刚大学毕业的新工程师立即深入公司的代码库;工程师在工作的第一周将承诺投入生产是很正常的。从积极的一面来看,Facebook 作为一家赋予员工权力的公司而享有声誉。工程师拥有极大的自由度,并且几乎没有任何规则和限制。

Facebook has been spectacularly successful as a company, but its code base suffered because of the company’s tactical approach; much of the code was unstable and hard to understand, with few comments or tests, and painful to work with. Over time the company realized that its culture was unsustainable. Eventually, Facebook changed its motto to “Move fast with solid infrastructure” to encourage its engineers to invest more in good design. It remains to be seen whether Facebook can successfully clean up the problems that accumulated over years of tactical programming.

Facebook 作为一家公司已经取得了令人瞩目的成功,但是由于该公司的战术方法,其代码库受到了影响(同样由于公司的战术编程推广,Facebook的代码库深受其害)。许多代码不稳定且难以理解,几乎没有注释或测试,并且使用起来很痛苦。随着时间的流逝,该公司意识到其文化是不可持续的。最终,Facebook 改变了座右铭,即“以坚实的基础架构快速移动”,以鼓励其工程师在良好的设计上进行更多的投资。Facebook 是否能够成功清除多年来战术编程中积累的问题还有待观察。

In fairness to Facebook, I should point out that Facebook’s code probably isn’t much worse than average among startups. Tactical programming is commonplace among startups; Facebook just happens to be a particularly visible example.

为了公平起见,我应该指出,Facebook 的代码可能并不比初创公司的平均水平差很多。战术编程在初创企业中司空见惯。Facebook 恰好是一个特别明显的例子。

Fortunately, it is also possible to succeed in Silicon Valley with a strategic approach. Google and VMware grew up around the same time as Facebook, but both of these companies embraced a more strategic approach. Both companies placed a heavy emphasis on high quality code and good design, and both companies built sophisticated products that solved complex problems with reliable software systems. The companies’ strong technical cultures became well known in Silicon Valley. Few other companies could compete with them for hiring the top technical talent.

幸运的是,通过战略方法也有可能在硅谷取得成功。Google 和 VMware 与 Facebook 差不多同时成长,但是这两家公司都采用了更具战略意义的方法。两家公司都非常重视高质量的代码和良好的设计,并且两家公司都开发了复杂的产品,这些产品通过可靠的软件系统解决了复杂的问题。公司的强大技术文化在硅谷广为人知。很少有其他公司可以与他们竞争聘请顶级技术人才。

These examples show that a company can succeed with either approach. However, it’s a lot more fun to work in a company that cares about software design and has a clean code base.

这些例子表明,一家公司可以成功使用任何一种方法。但是,在一家关心软件设计并拥有清晰代码基础的公司中工作会有趣得多。

3.5 Conclusion 结论

Good design doesn’t come for free. It has to be something you invest in continually, so that small problems don’t accumulate into big ones. Fortunately, good design eventually pays for itself, and sooner than you might think.

好的设计不是免费的。它必须是您不断投资的东西,这样小问题才不会累积成大问题。幸运的是,好的设计最终会收回成本,而且比您想象的要早。

It’s crucial to be consistent in applying the strategic approach and to think of investment as something to do today, not tomorrow. When you get in a crunch it will be tempting to put off cleanups until after the crunch is over. However, this is a slippery slope; after the current crunch there will almost certainly be another one, and another after that. Once you start delaying design improvements, it’s easy for the delays to become permanent and for your culture to slip into the tactical approach. The longer you wait to address design problems, the bigger they become; the solutions become more intimidating, which makes it easy to put them off even more. The most effective approach is one where every engineer makes continuous small investments in good design.

始终如一地运用战略方法并将投资视为当下而不是未来要做的事情至关重要。当您陷入危机时,很容易推迟清理,直到危机结束之后。但是,这是滑坡效应。在当延迟之后,几乎肯定会再出现一次。一旦开始延迟设计改进,就很容易使延迟永久化,并使您的文化陷入战术方法中。您等待解决设计问题的时间越长,问题就会变得越大;解决方案变得更加令人生畏,这使得轻松推迟解决方案变得更加容易。最有效的方法是,每位工程师都对良好的设计进行连续的少量投资。

',56)]))}const h=t(n,[["render",r],["__file","ch03.html.vue"]]),p=JSON.parse('{"path":"/ch03.html","title":"第 3 章 工作代码是不够的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"3.1 Tactical programming 战术编程","slug":"_3-1-tactical-programming-战术编程","link":"#_3-1-tactical-programming-战术编程","children":[]},{"level":2,"title":"3.2 Strategic programming 战略规划","slug":"_3-2-strategic-programming-战略规划","link":"#_3-2-strategic-programming-战略规划","children":[]},{"level":2,"title":"3.3 How much to invest? 投资多少?","slug":"_3-3-how-much-to-invest-投资多少","link":"#_3-3-how-much-to-invest-投资多少","children":[]},{"level":2,"title":"3.4 Startups and investment 创业与投资","slug":"_3-4-startups-and-investment-创业与投资","link":"#_3-4-startups-and-investment-创业与投资","children":[]},{"level":2,"title":"3.5 Conclusion 结论","slug":"_3-5-conclusion-结论","link":"#_3-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch03.md"}');export{h as comp,p as data}; +import{_ as t,c as o,f as a,o as i}from"./app-BQdhfat9.js";const s="/A-Philosophy-of-Software-Design-zh/assets/00011-CNtqviGr.jpeg",n={};function r(l,e){return i(),o("div",null,e[0]||(e[0]=[a('

第 3 章 工作代码是不够的

Chapter 3 Working Code Isn’t Enough (Strategic vs. Tactical Programming)

One of the most important elements of good software design is the mindset you adopt when you approach a programming task. Many organizations encourage a tactical mindset, focused on getting features working as quickly as possible. However, if you want a good design, you must take a more strategic approach where you invest time to produce clean designs and fix problems. This chapter discusses why the strategic approach produces better designs and is actually cheaper than the tactical approach over the long run.

好的软件设计中最重要的元素之一是您在执行编程任务时所采用的思维方式。许多组织都鼓励采取战术思维方式,着眼于使功能尽快运行。但是,如果您想要一个好的设计,则必须采取更具战略性的方法,在此上花费时间来制作干净的设计并解决问题。本章讨论了从长远来看,为什么战略方法可以产生更好的设计,而实际上却比战术方法便宜。

3.1 Tactical programming 战术编程

Most programmers approach software development with a mindset I call tactical programming. In the tactical approach, your main focus is to get something working, such as a new feature or a bug fix. At first glance this seems totally reasonable: what could be more important than writing code that works? However, tactical programming makes it nearly impossible to produce a good system design.

大多数程序员以我称为战术编程的心态来进行软件开发(针对大多数的开发人员的编码时的思维方式,作者称之为战术式编码)。在战术方法中,您的主要重点是使某些功能正常工作,例如新功能或错误修复。乍一看,这似乎是完全合理的:还有什么比编写有效的代码更重要的呢?但是,战术编程几乎不可能产生出良好的系统设计。

The problem with tactical programming is that it is short-sighted. If you’re programming tactically, you’re trying to finish a task as quickly as possible. Perhaps you have a hard deadline. As a result, planning for the future isn’t a priority. You don’t spend much time looking for the best design; you just want to get something working soon. You tell yourself that it’s OK to add a bit of complexity or introduce a small kludge or two, if that allows the current task to be completed more quickly.

战术编程的问题是它是短视的。如果您是战术编程人员,那么您将尝试尽快完成任务。也许您有一个艰难的期限。因此,为未来做计划不是优先事项。您不会花费太多时间来寻找最佳设计。您只想尽快使某件事起作用。您告诉自己,可以增加一些复杂性或引入一两个小错误,如果这样可以使当前任务更快地完成,则可以。(多数人还会自我安慰,如果可以让功能尽快上线的话,提高一些复杂度或者引入一两个小问题不是什么大不了的事情)

This is how systems become complicated. As discussed in the previous chapter, complexity is incremental. It’s not one particular thing that makes a system complicated, but the accumulation of dozens or hundreds of small things. If you program tactically, each programming task will contribute a few of these complexities. Each of them probably seems like a reasonable compromise in order to finish the current task quickly. However, the complexities accumulate rapidly, especially if everyone is programming tactically.

这就是系统变得复杂的方式。如上一章所述,复杂度是递增的。不是使系统复杂的特定事物,而是数十或数百个小事物的积累(复杂性的提升,不是由一个特定的事物引起的,而是由数十或成百的小事物积累导致的)。如果您进行战术编程(如果编码时总是使用战术式思维方式),则每个编程任务都会带来一些此类复杂性。为了快速完成当前任务,他们每个人似乎都是一个合理的折衷方案。但是,复杂性迅速累积,尤其是如果每个人都在战术上进行编程的时候。

Before long, some of the complexities will start causing problems, and you will begin to wish you hadn’t taken those early shortcuts. But, you will tell yourself that it’s more important to get the next feature working than to go back and refactor existing code. Refactoring may help out in the long run, but it will definitely slow down the current task. So, you look for quick patches to work around any problems you encounter. This just creates more complexity, which then requires more patches. Pretty soon the code is a mess, but by this point things are so bad that it would take months of work to clean it up. There’s no way your schedule can tolerate that kind of delay, and fixing one or two of the problems doesn’t seem like it will make much difference, so you just keep programming tactically.

不久之后,某些复杂性将开始引起问题,并且您将开始希望您没有采用这些早期的捷径。但是,您会告诉自己,使下一个功能正常工作比返回并重构现有代码更为重要。从长远来看,重构可能会有所帮助,但是肯定会减慢当前的任务。因此,您需要快速修补程序来解决遇到的任何问题。这只会增加复杂性,然后需要更多补丁。很快代码变得一团糟,但是到现在为止,情况已经很糟糕了,清理它需要花费数月的时间。您的日程安排无法容忍这种延迟,解决一个或两个问题似乎并没有太大的区别,因此您只是在战术上保持编程。

If you have worked on a large software project for very long, I suspect you have seen tactical programming at work and have experienced the problems that result. Once you start down the tactical path, it’s difficult to change.

如果您从事大型软件项目的时间很长,我怀疑您在工作中已经看到了战术编程,并且遇到了导致的问题。一旦您沿着战术路线走,就很难改变。

Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado. The tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. When it comes to implementing a quick feature, nobody gets it done faster than the tactical tornado. In some organizations, management treats tactical tornadoes as heroes. However, tactical tornadoes leave behind a wake of destruction. They are rarely considered heroes by the engineers who must work with their code in the future. Typically, other engineers must clean up the messes left behind by the tactical tornado, which makes it appear that those engineers (who are the real heroes) are making slower progress than the tactical tornado.

几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:战术龙卷风。战术龙卷风是一位多产的程序员,他抽出代码的速度比其他人快得多,但完全以战术方式工作。实施快速功能时,没有人能比战术龙卷风更快地完成任务。在某些组织中,管理层将战术龙卷风视为英雄。但是,战术龙卷风留下了毁灭的痕迹。他们很少被将来必须使用其代码的工程师视为英雄。通常,其他工程师必须清理战术龙卷风留下的混乱局面,这使得那些工程师(他们是真正的英雄)的进步似乎比战术龙卷风慢。

3.2 Strategic programming 战略规划

The first step towards becoming a good software designer is to realize that working code isn’t enough. It’s not acceptable to introduce unnecessary complexities in order to finish your current task faster. The most important thing is the long-term structure of the system. Most of the code in any system is written by extending the existing code base, so your most important job as a developer is to facilitate those future extensions. Thus, you should not think of “working code” as your primary goal, though of course your code must work. Your primary goal must be to produce a great design, which also happens to work. This is strategic programming.

成为一名优秀的软件设计师的第一步是要意识到 能跑起来的的代码是不够的。引入不必要的复杂性以更快地完成当前任务是不可接受的。最重要的是系统的长期结构。任何系统中的大多数代码都是通过扩展现有代码库编写的,因此,作为开发人员,最重要的工作就是促进这些将来的扩展。因此,尽管您的代码当然必须工作,但您不应将“工作代码”视为主要目标。您的主要目标必须是制作出出色的设计,并且这种设计也会起作用。这是 战略计划

Strategic programming requires an investment mindset. Rather than taking the fastest path to finish your current project, you must invest time to improve the design of the system. These investments will slow you down a bit in the short term, but they will speed you up in the long term, as illustrated in Figure 3.1.

战略性编程需要一种投资心态。您必须花费时间来改进系统的设计,而不是采取最快的方式来完成当前的项目。这些投资会在短期内让您放慢脚步,但从长远来看会加快您的速度,如图 3.1 所示。

Some of the investments will be proactive. For example, it’s worth taking a little extra time to find a simple design for each new class; rather than implementing the first idea that comes to mind, try a couple of alternative designs and pick the cleanest one. Try to imagine a few ways in which the system might need to be changed in the future and make sure that will be easy with your design. Writing good documentation is another example of a proactive investment.

一些投资将是积极的。例如,值得花一些时间为每个新类找到一个简单的设计。而不是实施想到的第一个想法,请尝试几种替代设计并选择最简洁的设计。试想一下将来可能需要更改系统的几种方式,并确保设计容易。编写好的文档是主动投资的另一个例子。

Other investments will be reactive. No matter how much you invest up front, there will inevitably be mistakes in your design decisions. Over time, these mistakes will become obvious. When you discover a design problem, don’t just ignore it or patch around it; take a little extra time to fix it. If you program strategically, you will continually make small improvements to the system design. This is the opposite of tactical programming, where you are continually adding small bits of complexity that cause problems in the future.

其他投资将是被动的。无论您预先投入多少,设计决策中都不可避免地会出现错误。随着时间的流逝,这些错误将变得显而易见。发现设计问题时,不要只是忽略它或对其进行修补。花一些额外的时间来修复它。如果您进行战略性编程,则将不断对系统设计进行小幅改进。这与战术编程相反,在战术编程中,您不断增加一些复杂性,这些复杂性将来会引起问题。

3.3 How much to invest? 投资多少?

So, what is the right amount of investment? A huge up-front investment, such as trying to design the entire system, won’t be effective. This is the waterfall method, and we know it doesn’t work. The ideal design tends to emerge in bits and pieces, as you get experience with the system. Thus, the best approach is to make lots of small investments on a continual basis. I suggest spending about 10–20% of your total development time on investments. This amount is small enough that it won’t impact your schedules significantly, but large enough to produce significant benefits over time. Your initial projects will thus take 10–20% longer than they would in a purely tactical approach. That extra time will result in a better software design, and you will start experiencing the benefits within a few months. It won’t be long before you’re developing at least 10–20% faster than you would if you had programmed tactically. At this point your investments become free: the benefits from your past investments will save enough time to cover the cost of future investments. You will quickly recover the cost of the initial investment. Figure 3.1 illustrates this phenomenon.

那么,正确的投资额是多少?大量的前期投资(例如尝试设计整个系统)将不会有效。这是瀑布方法,我们知道它不起作用。随着您对系统的了解,理想的设计趋于零碎出现。因此,最好的方法是连续进行大量小额投资 ​​。我建议您将总开发时间的 10%到 20%用于投资。该金额足够小,不会对您的日程安排产生重大影响,但又足够大,可以随着时间的推移产生重大收益。因此,您的初始项目将比纯战术方法花费 10-20%的时间。额外的时间将带来更好的软件设计,并且您将在几个月内开始体验到这些好处。不久之后,您的开发速度将比战术编程快至少 10–20%。在这一点上,您的投资将免费:您过去投资的收益将节省足够的时间来支付未来投资的费用。您将迅速收回初始投资的成本。图 3.1 说明了这种现象。

Figure 3.1: At the beginning, a tactical approach to programming will make progress more quickly than a strategic approach. However, complexity accumulates more rapidly under the tactical approach, which reduces productivity. Over time, the strategic approach results in greater progress. Note: this figure is intended only as a qualitative illustration; I am not aware of any empirical measurements of the precise shapes of the curves.

图 3.1:一开始,战术性的编程方法将比战略性方法更快地取得进展。但是,在战术方法下,复杂性积累得更快,从而降低了生产率。随着时间的流逝,战略方针会带来更大的进步。注意:此图仅用于定性说明;我不知道对曲线精确形状的任何经验测量。

Conversely, if you program tactically, you will finish your first projects 10–20% faster, but over time your development speed will slow as complexity accumulates. It won’t be long before you’re programming at least 10–20% slower. You will quickly give back all of the time you saved at the beginning, and for the rest of system’s lifetime you will be developing more slowly than if you had taken the strategic approach. If you haven’t ever worked in a badly degraded code base, talk to someone who has; they will tell you that poor code quality slows development by at least 20%.

相反,如果您进行战术编程,则可以将第一个项目完成的速度提高 10%到 20%,但是随着时间的推移,复杂性的累积会降低开发速度。不久之后,您的编程速度至少会降低 10–20%。您将很快退回在开始时节省的所有时间,并且在系统的整个生命周期中,与采用策略性方法相比,您的开发速度将更加缓慢。如果您从未使用过严重降级的代码库,请与有经验的人联系。他们会告诉您不良的代码质量会使开发速度至少降低 20%。

3.4 Startups and investment 创业与投资

In some environments there are strong forces working against the strategic approach. For example, early-stage startups feel tremendous pressure to get their early releases out quickly. In these companies, it might seem that even a 10–20% investment isn’t affordable. As a result, many startups take a tactical approach, spending little effort on design and even less on cleanup when problems pop up. They rationalize this with the thought that, if they are successful, they’ll have enough money to hire extra engineers to clean things up.

在某些环境中,强大的力量与战略方法背道而驰。例如,早期的初创公司感到巨大的压力,需要尽快发布其早期版本。在这些公司中,甚至 10%至 20%的投资似乎也负担不起。结果,许多初创公司采取了战术性的方法,在设计上花费了很少的精力,而在问题出现时则花费了更少的精力进行清理。他们认为,如果成功,他们将有足够的钱聘请额外的工程师来清理问题,从而使其合理化。

If you are in a company leaning in this direction, you should realize that once a code base turns to spaghetti, it is nearly impossible to fix. You will probably pay high development costs for the life of the product. Furthermore, the payoff for good (or bad) design comes pretty quickly, so there’s a good chance that the tactical approach won’t even speed up your first product release.

如果您是一家朝着这个方向发展的公司,则应该意识到,一旦代码库变成了意大利面条,几乎是不可能修复的。您可能会为产品的使用寿命付出高昂的开发成本。此外,好的(或坏的)设计的回报很快就会到来,因此战术方法很有可能甚至不会加快您的首个产品发布的速度。

Another thing to consider is that one of the most important factors for success of a company is the quality of its engineers. The best way to lower development costs is to hire great engineers: they don’t cost much more than mediocre engineers but have tremendously higher productivity. However, the best engineers care deeply about good design. If your code base is a wreck, word will get out, and this will make it harder for you to recruit. As a result, you are likely to end up with mediocre engineers. This will increase your future costs and probably cause the system structure to degrade even more.

要考虑的另一件事是,公司成功的最重要因素之一就是工程师的素质。降低开发成本的最佳方法是聘请优秀的工程师:他们的成本不会比普通工程师高很多,但生产率却高得多。但是,最好的工程师对良好的设计深感兴趣。如果你的代码库很糟糕,消息传出去,你将更更难招募到好的工程师。最终可能还是只能使用普通的工程师。这将增加您的未来成本,并可能导致系统结构进一步退化。

Facebook is an example of a startup that encouraged tactical programming. For many years the company’s motto was “Move fast and break things.” New engineers fresh out of college were encouraged to dive immediately into the company’s code base; it was normal for engineers to push commits into production in their first week on the job. On the positive side, Facebook developed a reputation as a company that empowered its employees. Engineers had tremendous latitude, and there were few rules and restrictions to get in their way.

Facebook 是一个鼓励战术编程的创业公司的例子。多年来,公司的座右铭是“快速行动并打破困境”。鼓励刚大学毕业的新工程师立即深入公司的代码库;工程师在工作的第一周将承诺投入生产是很正常的。从积极的一面来看,Facebook 作为一家赋予员工权力的公司而享有声誉。工程师拥有极大的自由度,并且几乎没有任何规则和限制。

Facebook has been spectacularly successful as a company, but its code base suffered because of the company’s tactical approach; much of the code was unstable and hard to understand, with few comments or tests, and painful to work with. Over time the company realized that its culture was unsustainable. Eventually, Facebook changed its motto to “Move fast with solid infrastructure” to encourage its engineers to invest more in good design. It remains to be seen whether Facebook can successfully clean up the problems that accumulated over years of tactical programming.

Facebook 作为一家公司已经取得了令人瞩目的成功,但是由于该公司的战术方法,其代码库受到了影响(同样由于公司的战术编程推广,Facebook的代码库深受其害)。许多代码不稳定且难以理解,几乎没有注释或测试,并且使用起来很痛苦。随着时间的流逝,该公司意识到其文化是不可持续的。最终,Facebook 改变了座右铭,即“以坚实的基础架构快速移动”,以鼓励其工程师在良好的设计上进行更多的投资。Facebook 是否能够成功清除多年来战术编程中积累的问题还有待观察。

In fairness to Facebook, I should point out that Facebook’s code probably isn’t much worse than average among startups. Tactical programming is commonplace among startups; Facebook just happens to be a particularly visible example.

为了公平起见,我应该指出,Facebook 的代码可能并不比初创公司的平均水平差很多。战术编程在初创企业中司空见惯。Facebook 恰好是一个特别明显的例子。

Fortunately, it is also possible to succeed in Silicon Valley with a strategic approach. Google and VMware grew up around the same time as Facebook, but both of these companies embraced a more strategic approach. Both companies placed a heavy emphasis on high quality code and good design, and both companies built sophisticated products that solved complex problems with reliable software systems. The companies’ strong technical cultures became well known in Silicon Valley. Few other companies could compete with them for hiring the top technical talent.

幸运的是,通过战略方法也有可能在硅谷取得成功。Google 和 VMware 与 Facebook 差不多同时成长,但是这两家公司都采用了更具战略意义的方法。两家公司都非常重视高质量的代码和良好的设计,并且两家公司都开发了复杂的产品,这些产品通过可靠的软件系统解决了复杂的问题。公司的强大技术文化在硅谷广为人知。很少有其他公司可以与他们竞争聘请顶级技术人才。

These examples show that a company can succeed with either approach. However, it’s a lot more fun to work in a company that cares about software design and has a clean code base.

这些例子表明,一家公司可以成功使用任何一种方法。但是,在一家关心软件设计并拥有清晰代码基础的公司中工作会有趣得多。

3.5 Conclusion 结论

Good design doesn’t come for free. It has to be something you invest in continually, so that small problems don’t accumulate into big ones. Fortunately, good design eventually pays for itself, and sooner than you might think.

好的设计不是免费的。它必须是您不断投资的东西,这样小问题才不会累积成大问题。幸运的是,好的设计最终会收回成本,而且比您想象的要早。

It’s crucial to be consistent in applying the strategic approach and to think of investment as something to do today, not tomorrow. When you get in a crunch it will be tempting to put off cleanups until after the crunch is over. However, this is a slippery slope; after the current crunch there will almost certainly be another one, and another after that. Once you start delaying design improvements, it’s easy for the delays to become permanent and for your culture to slip into the tactical approach. The longer you wait to address design problems, the bigger they become; the solutions become more intimidating, which makes it easy to put them off even more. The most effective approach is one where every engineer makes continuous small investments in good design.

始终如一地运用战略方法并将投资视为当下而不是未来要做的事情至关重要。当您陷入危机时,很容易推迟清理,直到危机结束之后。但是,这是滑坡效应。在当延迟之后,几乎肯定会再出现一次。一旦开始延迟设计改进,就很容易使延迟永久化,并使您的文化陷入战术方法中。您等待解决设计问题的时间越长,问题就会变得越大;解决方案变得更加令人生畏,这使得轻松推迟解决方案变得更加容易。最有效的方法是,每位工程师都对良好的设计进行连续的少量投资。

',56)]))}const h=t(n,[["render",r],["__file","ch03.html.vue"]]),p=JSON.parse('{"path":"/ch03.html","title":"第 3 章 工作代码是不够的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"3.1 Tactical programming 战术编程","slug":"_3-1-tactical-programming-战术编程","link":"#_3-1-tactical-programming-战术编程","children":[]},{"level":2,"title":"3.2 Strategic programming 战略规划","slug":"_3-2-strategic-programming-战略规划","link":"#_3-2-strategic-programming-战略规划","children":[]},{"level":2,"title":"3.3 How much to invest? 投资多少?","slug":"_3-3-how-much-to-invest-投资多少","link":"#_3-3-how-much-to-invest-投资多少","children":[]},{"level":2,"title":"3.4 Startups and investment 创业与投资","slug":"_3-4-startups-and-investment-创业与投资","link":"#_3-4-startups-and-investment-创业与投资","children":[]},{"level":2,"title":"3.5 Conclusion 结论","slug":"_3-5-conclusion-结论","link":"#_3-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch03.md"}');export{h as comp,p as data}; diff --git a/assets/ch04.html-DkQGekeV.js b/assets/ch04.html-C8UCSGA_.js similarity index 99% rename from assets/ch04.html-DkQGekeV.js rename to assets/ch04.html-C8UCSGA_.js index ac2878ad..da42c41a 100644 --- a/assets/ch04.html-DkQGekeV.js +++ b/assets/ch04.html-C8UCSGA_.js @@ -1,4 +1,4 @@ -import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o="/A-Philosophy-of-Software-Design-zh/assets/00012-CcqL0Jgd.jpeg",i={};function l(r,e){return n(),a("div",null,e[0]||(e[0]=[s('

第 4 章 模块应该是深的

Chapter 4 Modules Should Be Deep

One of the most important techniques for managing software complexity is to design systems so that developers only need to face a small fraction of the overall complexity at any given time. This approach is called modular design, and this chapter presents its basic principles.

管理软件复杂性最重要的技术之一就是设计系统,以便开发人员在任何给定时间 只需要面对整体复杂性的一小部分。这种方法称为模块化设计,本章介绍其基本原理。

4.1 Modular design 模块化设计

In modular design, a software system is decomposed into a collection of modules that are relatively independent. Modules can take many forms, such as classes, subsystems, or services. In an ideal world, each module would be completely independent of the others: a developer could work in any of the modules without knowing anything about any of the other modules. In this world, the complexity of a system would be the complexity of its worst module.

在模块化设计中,软件系统被分解为相对独立的模块集合。模块可以采用多种形式,例如类,子系统或服务。在理想的世界中,每个模块都将完全独立于其他模块:开发人员可以在任何模块中工作,而无需了解任何其他模块。在这种情况下,系统的复杂性就是其最糟糕的模块的复杂性。

Unfortunately, this ideal is not achievable. Modules must work together by calling each others’s functions or methods. As a result, modules must know something about each other. There will be dependencies between the modules: if one module changes, other modules may need to change to match. For example, the arguments for a method create a dependency between the method and any code that invokes the method. If the required arguments change, all invocations of the method must be modified to conform to the new signature. Dependencies can take many other forms, and they can be quite subtle. The goal of modular design is to minimize the dependencies between modules.

不幸的是,这种理想是无法实现的。模块必须通过调用彼此的函数或方法来协同工作。结果,模块必须相互了解。模块之间将存在依赖关系:如果一个模块发生更改,则可能需要更改其他模块以进行匹配。例如,方法的参数在方法与调用该方法的任何代码之间创建依赖关系。如果必需的参数更改,则必须修改该方法的所有调用以符合新的签名。依赖关系可以采用许多其他形式,并且它们可能非常微妙。模块化设计的目标是最大程度地 减少模块之间的依赖性

In order to manage dependencies, we think of each module in two parts: an interface and an implementation. The interface consists of everything that a developer working in a different module must know in order to use the given module. Typically, the interface describes what the module does but not how it does it. The implementation consists of the code that carries out the promises made by the interface. A developer working in a particular module must understand the interface and implementation of that module, plus the interfaces of any other modules invoked by the given module. A developer should not need to understand the implementations of modules other than the one he or she is working in.

为了管理依赖关系,我们将每个模块分为两个部分:接口和实现。接口包括了在不同模块工作的开发者为了使用给定模块必须知道的所有内容。通常,接口描述模块做什么,而不描述模块如何做。该实现由执行接口所承诺的代码组成。在特定模块中工作的开发人员必须了解该模块的接口和实现,以及由给定模块调用的任何其他模块的接口。除了正在使用的模块以外,开发人员无需了解其他模块的实现。

Consider a module that implements balanced trees. The module probably contains sophisticated code for ensuring that the tree remains balanced. However, this complexity is not visible to users of the module. Users see a relatively simple interface for invoking operations to insert, remove, and fetch nodes in the tree. To invoke an insert operation, the caller need only provide the key and value for the new node; the mechanisms for traversing the tree and splitting nodes are not visible in the interface.

考虑一个实现平衡树的模块。该模块可能包含复杂的代码,以确保树保持平衡。但是,此复杂性对于模块使用者而言是不可见的。用户可以看到一个相对简单的接口,用于调用在树中插入,删除和获取节点的操作。要调用插入操作,调用者只需提供新节点的键和值即可。遍历树和拆分节点的机制在接口中不可见。

For the purposes of this book, a module is any unit of code that has an interface and an implementation. Each class in an object-oriented programming language is a module. Methods within a class, or functions in a language that isn’t object-oriented, can also be thought of as modules: each of these has an interface and an implementation, and modular design techniques can be applied to them. Higher-level subsystems and services are also modules; their interfaces may take different forms, such as kernel calls or HTTP requests. Much of the discussion about modular design in this book focuses on designing classes, but the techniques and concepts apply to other kinds of modules as well.

就本书而言,模块是具有接口和实现的任何代码单元。面向对象编程语言中的每个类都是一个模块。类中的方法或非面向对象语言中的函数也可以视为模块:每个模块都有一个接口和一个实现,并且可以将模块化设计技术应用于它们。更高级别的子系统和服务也是模块。它们的接口可能采用不同的形式,例如内核调用或 HTTP 请求。本书中有关模块化设计的许多讨论都集中在设计类上,但是技术和概念也适用于其他种类的模块。

The best modules are those whose interfaces are much simpler than their implementations. Such modules have two advantages. First, a simple interface minimizes the complexity that a module imposes on the rest of the system. Second, if a module is modified in a way that does not change its interface, then no other module will be affected by the modification. If a module’s interface is much simpler than its implementation, there will be many aspects of the module that can be changed without affecting other modules.

最好的模块是那些其接口比其实现简单得多的模块。这样的模块具有两个优点。首先,一个简单的接口可以将模块强加于系统其余部分的复杂性降至最低。其次,如果以不更改其接口的方式修改了一个模块,则该修改不会影响其他模块。如果模块的接口比其实现简单得多,则可以在不影响其他模块的情况下更改模块的许多方面。

4.2 What’s in an interface? 接口中有什么?

The interface to a module contains two kinds of information: formal and informal. The formal parts of an interface are specified explicitly in the code, and some of these can be checked for correctness by the programming language. For example, the formal interface for a method is its signature, which includes the names and types of its parameters, the type of its return value, and information about exceptions thrown by the method. Most programming languages ensure that each invocation of a method provides the right number and types of arguments to match its signature. The formal interface for a class consists of the signatures for all of its public methods, plus the names and types of any public variables.

模块的接口包含两种信息:正式信息和非正式信息。接口的形式部分在代码中明确指定,并且其中一些可以通过编程语言检查其正确性。例如,方法的形式接口是其签名,其中包括其参数的名称和类型,其返回值的类型以及有关该方法引发的异常的信息。大多数编程语言都确保对方法的每次调用都提供正确数量和类型的参数以匹配其签名。类的形式接口包括其所有公共方法的签名以及任何公共变量的名称和类型。

Each interface also includes informal elements. These are not specified in a way that can be understood or enforced by the programming language. The informal parts of an interface include its high-level behavior, such as the fact that a function deletes the file named by one of its arguments. If there are constraints on the usage of a class (perhaps one method must be called before another), these are also part of the class’s interface. In general, if a developer needs to know a particular piece of information in order to use a module, then that information is part of the module’s interface. The informal aspects of an interface can only be described using comments, and the programming language cannot ensure that the description is complete or accurate1. For most interfaces the informal aspects are larger and more complex than the formal aspects.

每个接口还包括非正式元素。这些没有以编程语言可以理解或执行的方式指定。接口的非正式部分包括其高级行为,例如,函数删除由其参数之一命名的文件的事实。如果对类的使用存在限制(也许必须先调用一种方法),则这些约束也是类接口的一部分。通常,如果开发人员需要了解特定信息才能使用模块,则该信息是模块接口的一部分。接口的非正式方面只能使用注释来描述,而编程语言不能确保描述是完整或准确的 ^1。对于大多数接口,非正式方面比正式方面更大,更复杂。

One of the benefits of a clearly specified interface is that it indicates exactly what developers need to know in order to use the associated module. This helps to eliminate the “unknown unknowns” problem described in Section 2.2.

明确指定接口的好处之一是,它可以准确指示开发人员使用关联模块所需要知道的内容。这有助于消除第 2.2 节中描述的“未知的未知”问题。

4.3 Abstractions 抽象

The term abstraction is closely related to the idea of modular design. An abstraction is a simplified view of an entity, which omits unimportant details. Abstractions are useful because they make it easier for us to think about and manipulate complex things.

术语抽象与模块化设计的思想紧密相关。抽象是实体的简化视图,其中省略了不重要的细节。抽象是有用的,因为它们使我们更容易思考和操纵复杂的事物。

In modular programming, each module provides an abstraction in form of its interface. The interface presents a simplified view of the module’s functionality; the details of the implementation are unimportant from the standpoint of the module’s abstraction, so they are omitted from the interface.

在模块化编程中,每个模块以其接口的形式提供抽象。该接口提供了模块功能的简化视图;从模块抽象的角度来看,实现的细节并不重要,因此在接口中将其省略。

In the definition of abstraction, the word “unimportant” is crucial. The more unimportant details that are omitted from an abstraction, the better. However, a detail can only be omitted from an abstraction if it is unimportant. An abstraction can go wrong in two ways. First, it can include details that are not really important; when this happens, it makes the abstraction more complicated than necessary, which increases the cognitive load on developers using the abstraction. The second error is when an abstraction omits details that really are important. This results in obscurity: developers looking only at the abstraction will not have all the information they need to use the abstraction correctly. An abstraction that omits important details is a false abstraction: it might appear simple, but in reality it isn’t. The key to designing abstractions is to understand what is important, and to look for designs that minimize the amount of information that is important.

在抽象的定义中,“无关紧要”一词至关重要。从抽象中忽略的不重要的细节越多越好。但是,一个细节只有在不重要的情况下才能从抽象中省略。有两种错误的抽象方式。首先,它包含并非真正重要的细节。当这种情况发生时,它会使抽象变得不必要的复杂,从而增加了使用抽象的开发人员的认知负担。第二个错误是抽象忽略了真正重要的细节。这导致模糊不清:仅查看抽象的开发人员将不会获得正确使用抽象所需的全部信息。忽略重要细节的抽象是错误的抽象:它可能看起来很简单,但实际上并非如此。(设计抽象的重要一点就是识别重要性,并在设计过程中,将重要信息的数量尽量减到最少)

As an example, consider a file system. The abstraction provided by a file system omits many details, such as the mechanism for choosing which blocks on a storage device to use for the data in a given file. These details are unimportant to users of the file system (as long as the system provides adequate performance). However, some of the details of a file system’s implementation are important to users. Most file systems cache data in main memory, and they may delay writing new data to the storage device in order to improve performance. Some applications, such as databases, need to know exactly when data is written through to storage, so they can ensure that data will be preserved after system crashes. Thus, the rules for flushing data to secondary storage must be visible in the file system’s interface.

例如,考虑一个文件系统。文件系统提供的抽象省略了许多细节,例如用于选择存储设备上的哪些块用于给定文件中的数据的机制。这些详细信息对于文件系统的用户而言并不重要(只要系统提供足够的性能即可)。但是,文件系统实现的一些细节对用户很重要。大多数文件系统将数据缓存在主内存中,并且它们可能会延迟将新数据写入存储设备以提高性能。一些应用程序(例如数据库)需要确切地知道何时将数据写入存储设备,因此它们可以确保在系统崩溃后将保留数据。因此,将数据刷新到辅助存储的规则必须在文件系统的接口中可见。

We depend on abstractions to manage complexity not just in programming, but pervasively in our everyday lives. A microwave oven contains complex electronics to convert alternating current into microwave radiation and distribute that radiation throughout the cooking cavity. Fortunately, users see a much simpler abstraction, consisting of a few buttons to control the timing and intensity of the microwaves. Cars provide a simple abstraction that allows us to drive them without understanding the mechanisms for electrical motors, battery power management, anti-lock brakes, cruise control, and so on.

我们依赖抽象来管理复杂性,这不仅仅体现在编程中,而且在我们日常生活的方方面面普遍存在。微波炉包含复杂的电子设备,可将交流电转换为微波辐射并将该辐射分布到整个烹饪腔中。幸运的是,用户看到了一个简单得多的抽象,它由几个按钮控制微波的定时和强度。汽车提供了一种简单的抽象概念,使我们可以在不了解电动机,电池电源管理,防抱死制动,巡航控制等机制的情况下驾驶它们。

4.4 Deep modules 深度模块

The best modules are those that provide powerful functionality yet have simple interfaces. I use the term deep to describe such modules. To visualize the notion of depth, imagine that each module is represented by a rectangle, as shown in Figure 4.1. The area of each rectangle is proportional to the functionality implemented by the module. The top edge of a rectangle represents the module’s interface; the length of that edge indicates the complexity of the interface. The best modules are deep: they have a lot of functionality hidden behind a simple interface. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users.

最好的模块是那些提供强大功能但具有简单接口的模块。我用“深入”一词来描述这样的模块。为了形象化深度的概念,假设每个模块都由一个矩形表示,如图 4.1 所示。每个矩形的面积与模块实现的功能成比例。矩形的顶部边缘代表模块的接口;边缘的长度表示接口的复杂性。最好的模块很深:它们在简单的接口后隐藏了许多功能。深度模块是一个很好的抽象,因为其内部复杂性的很小一部分对其用户可见。

Figure 4.1: Deep and shallow modules. The best modules are deep: they allow a lot of functionality to be accessed through a simple interface. A shallow module is one with a relatively complex interface, but not much functionality: it doesn’t hide much complexity.

图 4.1:深浅模块。最好的模块很深:它们允许通过简单的接口访问许多功能。浅层模块是具有相对复杂的接口的模块,但功能不多:它不会掩盖太多的复杂性。

Module depth is a way of thinking about cost versus benefit. The benefit provided by a module is its functionality. The cost of a module (in terms of system complexity) is its interface. A module’s interface represents the complexity that the module imposes on the rest of the system: the smaller and simpler the interface, the less complexity that it introduces. The best modules are those with the greatest benefit and the least cost. Interfaces are good, but more, or larger, interfaces are not necessarily better!

模块深度是考虑成本与收益的一种方式。模块提供的好处是其功能。模块的成本(就系统复杂性而言)是其接口。模块的接口代表了模块强加给系统其余部分的复杂性:接口越小越简单,引入的复杂性就越小。最好的模块是那些收益最大,成本最低的模块。接口是好的,但更多或更大的接口不一定更好!

The mechanism for file I/O provided by the Unix operating system and its descendants, such as Linux, is a beautiful example of a deep interface. There are only five basic system calls for I/O, with simple signatures:

Unix 操作系统及其后代(例如 Linux)提供的文件 I/O 机制是深层接口的一个很好的例子。I/O 只有五个基本系统调用,带有简单签名:

int open(const char* path, int flags, mode_t permissions);
+import{_ as t,c as a,f as s,o as n}from"./app-BQdhfat9.js";const o="/A-Philosophy-of-Software-Design-zh/assets/00012-CcqL0Jgd.jpeg",i={};function l(r,e){return n(),a("div",null,e[0]||(e[0]=[s('

第 4 章 模块应该是深的

Chapter 4 Modules Should Be Deep

One of the most important techniques for managing software complexity is to design systems so that developers only need to face a small fraction of the overall complexity at any given time. This approach is called modular design, and this chapter presents its basic principles.

管理软件复杂性最重要的技术之一就是设计系统,以便开发人员在任何给定时间 只需要面对整体复杂性的一小部分。这种方法称为模块化设计,本章介绍其基本原理。

4.1 Modular design 模块化设计

In modular design, a software system is decomposed into a collection of modules that are relatively independent. Modules can take many forms, such as classes, subsystems, or services. In an ideal world, each module would be completely independent of the others: a developer could work in any of the modules without knowing anything about any of the other modules. In this world, the complexity of a system would be the complexity of its worst module.

在模块化设计中,软件系统被分解为相对独立的模块集合。模块可以采用多种形式,例如类,子系统或服务。在理想的世界中,每个模块都将完全独立于其他模块:开发人员可以在任何模块中工作,而无需了解任何其他模块。在这种情况下,系统的复杂性就是其最糟糕的模块的复杂性。

Unfortunately, this ideal is not achievable. Modules must work together by calling each others’s functions or methods. As a result, modules must know something about each other. There will be dependencies between the modules: if one module changes, other modules may need to change to match. For example, the arguments for a method create a dependency between the method and any code that invokes the method. If the required arguments change, all invocations of the method must be modified to conform to the new signature. Dependencies can take many other forms, and they can be quite subtle. The goal of modular design is to minimize the dependencies between modules.

不幸的是,这种理想是无法实现的。模块必须通过调用彼此的函数或方法来协同工作。结果,模块必须相互了解。模块之间将存在依赖关系:如果一个模块发生更改,则可能需要更改其他模块以进行匹配。例如,方法的参数在方法与调用该方法的任何代码之间创建依赖关系。如果必需的参数更改,则必须修改该方法的所有调用以符合新的签名。依赖关系可以采用许多其他形式,并且它们可能非常微妙。模块化设计的目标是最大程度地 减少模块之间的依赖性

In order to manage dependencies, we think of each module in two parts: an interface and an implementation. The interface consists of everything that a developer working in a different module must know in order to use the given module. Typically, the interface describes what the module does but not how it does it. The implementation consists of the code that carries out the promises made by the interface. A developer working in a particular module must understand the interface and implementation of that module, plus the interfaces of any other modules invoked by the given module. A developer should not need to understand the implementations of modules other than the one he or she is working in.

为了管理依赖关系,我们将每个模块分为两个部分:接口和实现。接口包括了在不同模块工作的开发者为了使用给定模块必须知道的所有内容。通常,接口描述模块做什么,而不描述模块如何做。该实现由执行接口所承诺的代码组成。在特定模块中工作的开发人员必须了解该模块的接口和实现,以及由给定模块调用的任何其他模块的接口。除了正在使用的模块以外,开发人员无需了解其他模块的实现。

Consider a module that implements balanced trees. The module probably contains sophisticated code for ensuring that the tree remains balanced. However, this complexity is not visible to users of the module. Users see a relatively simple interface for invoking operations to insert, remove, and fetch nodes in the tree. To invoke an insert operation, the caller need only provide the key and value for the new node; the mechanisms for traversing the tree and splitting nodes are not visible in the interface.

考虑一个实现平衡树的模块。该模块可能包含复杂的代码,以确保树保持平衡。但是,此复杂性对于模块使用者而言是不可见的。用户可以看到一个相对简单的接口,用于调用在树中插入,删除和获取节点的操作。要调用插入操作,调用者只需提供新节点的键和值即可。遍历树和拆分节点的机制在接口中不可见。

For the purposes of this book, a module is any unit of code that has an interface and an implementation. Each class in an object-oriented programming language is a module. Methods within a class, or functions in a language that isn’t object-oriented, can also be thought of as modules: each of these has an interface and an implementation, and modular design techniques can be applied to them. Higher-level subsystems and services are also modules; their interfaces may take different forms, such as kernel calls or HTTP requests. Much of the discussion about modular design in this book focuses on designing classes, but the techniques and concepts apply to other kinds of modules as well.

就本书而言,模块是具有接口和实现的任何代码单元。面向对象编程语言中的每个类都是一个模块。类中的方法或非面向对象语言中的函数也可以视为模块:每个模块都有一个接口和一个实现,并且可以将模块化设计技术应用于它们。更高级别的子系统和服务也是模块。它们的接口可能采用不同的形式,例如内核调用或 HTTP 请求。本书中有关模块化设计的许多讨论都集中在设计类上,但是技术和概念也适用于其他种类的模块。

The best modules are those whose interfaces are much simpler than their implementations. Such modules have two advantages. First, a simple interface minimizes the complexity that a module imposes on the rest of the system. Second, if a module is modified in a way that does not change its interface, then no other module will be affected by the modification. If a module’s interface is much simpler than its implementation, there will be many aspects of the module that can be changed without affecting other modules.

最好的模块是那些其接口比其实现简单得多的模块。这样的模块具有两个优点。首先,一个简单的接口可以将模块强加于系统其余部分的复杂性降至最低。其次,如果以不更改其接口的方式修改了一个模块,则该修改不会影响其他模块。如果模块的接口比其实现简单得多,则可以在不影响其他模块的情况下更改模块的许多方面。

4.2 What’s in an interface? 接口中有什么?

The interface to a module contains two kinds of information: formal and informal. The formal parts of an interface are specified explicitly in the code, and some of these can be checked for correctness by the programming language. For example, the formal interface for a method is its signature, which includes the names and types of its parameters, the type of its return value, and information about exceptions thrown by the method. Most programming languages ensure that each invocation of a method provides the right number and types of arguments to match its signature. The formal interface for a class consists of the signatures for all of its public methods, plus the names and types of any public variables.

模块的接口包含两种信息:正式信息和非正式信息。接口的形式部分在代码中明确指定,并且其中一些可以通过编程语言检查其正确性。例如,方法的形式接口是其签名,其中包括其参数的名称和类型,其返回值的类型以及有关该方法引发的异常的信息。大多数编程语言都确保对方法的每次调用都提供正确数量和类型的参数以匹配其签名。类的形式接口包括其所有公共方法的签名以及任何公共变量的名称和类型。

Each interface also includes informal elements. These are not specified in a way that can be understood or enforced by the programming language. The informal parts of an interface include its high-level behavior, such as the fact that a function deletes the file named by one of its arguments. If there are constraints on the usage of a class (perhaps one method must be called before another), these are also part of the class’s interface. In general, if a developer needs to know a particular piece of information in order to use a module, then that information is part of the module’s interface. The informal aspects of an interface can only be described using comments, and the programming language cannot ensure that the description is complete or accurate1. For most interfaces the informal aspects are larger and more complex than the formal aspects.

每个接口还包括非正式元素。这些没有以编程语言可以理解或执行的方式指定。接口的非正式部分包括其高级行为,例如,函数删除由其参数之一命名的文件的事实。如果对类的使用存在限制(也许必须先调用一种方法),则这些约束也是类接口的一部分。通常,如果开发人员需要了解特定信息才能使用模块,则该信息是模块接口的一部分。接口的非正式方面只能使用注释来描述,而编程语言不能确保描述是完整或准确的 ^1。对于大多数接口,非正式方面比正式方面更大,更复杂。

One of the benefits of a clearly specified interface is that it indicates exactly what developers need to know in order to use the associated module. This helps to eliminate the “unknown unknowns” problem described in Section 2.2.

明确指定接口的好处之一是,它可以准确指示开发人员使用关联模块所需要知道的内容。这有助于消除第 2.2 节中描述的“未知的未知”问题。

4.3 Abstractions 抽象

The term abstraction is closely related to the idea of modular design. An abstraction is a simplified view of an entity, which omits unimportant details. Abstractions are useful because they make it easier for us to think about and manipulate complex things.

术语抽象与模块化设计的思想紧密相关。抽象是实体的简化视图,其中省略了不重要的细节。抽象是有用的,因为它们使我们更容易思考和操纵复杂的事物。

In modular programming, each module provides an abstraction in form of its interface. The interface presents a simplified view of the module’s functionality; the details of the implementation are unimportant from the standpoint of the module’s abstraction, so they are omitted from the interface.

在模块化编程中,每个模块以其接口的形式提供抽象。该接口提供了模块功能的简化视图;从模块抽象的角度来看,实现的细节并不重要,因此在接口中将其省略。

In the definition of abstraction, the word “unimportant” is crucial. The more unimportant details that are omitted from an abstraction, the better. However, a detail can only be omitted from an abstraction if it is unimportant. An abstraction can go wrong in two ways. First, it can include details that are not really important; when this happens, it makes the abstraction more complicated than necessary, which increases the cognitive load on developers using the abstraction. The second error is when an abstraction omits details that really are important. This results in obscurity: developers looking only at the abstraction will not have all the information they need to use the abstraction correctly. An abstraction that omits important details is a false abstraction: it might appear simple, but in reality it isn’t. The key to designing abstractions is to understand what is important, and to look for designs that minimize the amount of information that is important.

在抽象的定义中,“无关紧要”一词至关重要。从抽象中忽略的不重要的细节越多越好。但是,一个细节只有在不重要的情况下才能从抽象中省略。有两种错误的抽象方式。首先,它包含并非真正重要的细节。当这种情况发生时,它会使抽象变得不必要的复杂,从而增加了使用抽象的开发人员的认知负担。第二个错误是抽象忽略了真正重要的细节。这导致模糊不清:仅查看抽象的开发人员将不会获得正确使用抽象所需的全部信息。忽略重要细节的抽象是错误的抽象:它可能看起来很简单,但实际上并非如此。(设计抽象的重要一点就是识别重要性,并在设计过程中,将重要信息的数量尽量减到最少)

As an example, consider a file system. The abstraction provided by a file system omits many details, such as the mechanism for choosing which blocks on a storage device to use for the data in a given file. These details are unimportant to users of the file system (as long as the system provides adequate performance). However, some of the details of a file system’s implementation are important to users. Most file systems cache data in main memory, and they may delay writing new data to the storage device in order to improve performance. Some applications, such as databases, need to know exactly when data is written through to storage, so they can ensure that data will be preserved after system crashes. Thus, the rules for flushing data to secondary storage must be visible in the file system’s interface.

例如,考虑一个文件系统。文件系统提供的抽象省略了许多细节,例如用于选择存储设备上的哪些块用于给定文件中的数据的机制。这些详细信息对于文件系统的用户而言并不重要(只要系统提供足够的性能即可)。但是,文件系统实现的一些细节对用户很重要。大多数文件系统将数据缓存在主内存中,并且它们可能会延迟将新数据写入存储设备以提高性能。一些应用程序(例如数据库)需要确切地知道何时将数据写入存储设备,因此它们可以确保在系统崩溃后将保留数据。因此,将数据刷新到辅助存储的规则必须在文件系统的接口中可见。

We depend on abstractions to manage complexity not just in programming, but pervasively in our everyday lives. A microwave oven contains complex electronics to convert alternating current into microwave radiation and distribute that radiation throughout the cooking cavity. Fortunately, users see a much simpler abstraction, consisting of a few buttons to control the timing and intensity of the microwaves. Cars provide a simple abstraction that allows us to drive them without understanding the mechanisms for electrical motors, battery power management, anti-lock brakes, cruise control, and so on.

我们依赖抽象来管理复杂性,这不仅仅体现在编程中,而且在我们日常生活的方方面面普遍存在。微波炉包含复杂的电子设备,可将交流电转换为微波辐射并将该辐射分布到整个烹饪腔中。幸运的是,用户看到了一个简单得多的抽象,它由几个按钮控制微波的定时和强度。汽车提供了一种简单的抽象概念,使我们可以在不了解电动机,电池电源管理,防抱死制动,巡航控制等机制的情况下驾驶它们。

4.4 Deep modules 深度模块

The best modules are those that provide powerful functionality yet have simple interfaces. I use the term deep to describe such modules. To visualize the notion of depth, imagine that each module is represented by a rectangle, as shown in Figure 4.1. The area of each rectangle is proportional to the functionality implemented by the module. The top edge of a rectangle represents the module’s interface; the length of that edge indicates the complexity of the interface. The best modules are deep: they have a lot of functionality hidden behind a simple interface. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users.

最好的模块是那些提供强大功能但具有简单接口的模块。我用“深入”一词来描述这样的模块。为了形象化深度的概念,假设每个模块都由一个矩形表示,如图 4.1 所示。每个矩形的面积与模块实现的功能成比例。矩形的顶部边缘代表模块的接口;边缘的长度表示接口的复杂性。最好的模块很深:它们在简单的接口后隐藏了许多功能。深度模块是一个很好的抽象,因为其内部复杂性的很小一部分对其用户可见。

Figure 4.1: Deep and shallow modules. The best modules are deep: they allow a lot of functionality to be accessed through a simple interface. A shallow module is one with a relatively complex interface, but not much functionality: it doesn’t hide much complexity.

图 4.1:深浅模块。最好的模块很深:它们允许通过简单的接口访问许多功能。浅层模块是具有相对复杂的接口的模块,但功能不多:它不会掩盖太多的复杂性。

Module depth is a way of thinking about cost versus benefit. The benefit provided by a module is its functionality. The cost of a module (in terms of system complexity) is its interface. A module’s interface represents the complexity that the module imposes on the rest of the system: the smaller and simpler the interface, the less complexity that it introduces. The best modules are those with the greatest benefit and the least cost. Interfaces are good, but more, or larger, interfaces are not necessarily better!

模块深度是考虑成本与收益的一种方式。模块提供的好处是其功能。模块的成本(就系统复杂性而言)是其接口。模块的接口代表了模块强加给系统其余部分的复杂性:接口越小越简单,引入的复杂性就越小。最好的模块是那些收益最大,成本最低的模块。接口是好的,但更多或更大的接口不一定更好!

The mechanism for file I/O provided by the Unix operating system and its descendants, such as Linux, is a beautiful example of a deep interface. There are only five basic system calls for I/O, with simple signatures:

Unix 操作系统及其后代(例如 Linux)提供的文件 I/O 机制是深层接口的一个很好的例子。I/O 只有五个基本系统调用,带有简单签名:

int open(const char* path, int flags, mode_t permissions);
 ssize_t read(int fd, void* buffer, size_t count);
 ssize_t write(int fd, const void* buffer, size_t count);
 off_t lseek(int fd, off_t offset, int referencePosition);
@@ -11,4 +11,4 @@ import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o="/A-Philosoph
 BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
 
 ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
-

A FileInputStream object provides only rudimentary I/O: it is not capable of performing buffered I/O, nor can it read or write serialized objects. The BufferedInputStream object adds buffering to a FileInputStream, and the ObjectInputStream adds the ability to read and write serialized objects. The first two objects in the code above, fileStream and bufferedStream, are never used once the file has been opened; all future operations use objectStream.

FileInputStream 对象仅提供基本的 I/O:它不能执行缓冲的 I/O,也不能读取或写入序列化的对象。BufferedInputStream 对象将缓冲添加到 FileInputStream,而 ObjectInputStream 添加了读取和写入序列化对象的功能。一旦文件被打开,上面代码中的前两个对象 fileStream 和 bufferedStream 将永远不会被使用。以后的所有操作都使用 objectStream。

It is particularly annoying (and error-prone) that buffering must be requested explicitly by creating a separate BufferedInputStream object; if a developer forgets to create this object, there will be no buffering and I/O will be slow. Perhaps the Java developers would argue that not everyone wants to use buffering for file I/O, so it shouldn’t be built into the base mechanism. They might argue that it’s better to keep buffering separate, so people can choose whether or not to use it. Providing choice is good, but interfaces should be designed to make the common case as simple as possible (see the formula on page 6). Almost every user of file I/O will want buffering, so it should be provided by default. For those few situations where buffering is not desirable, the library can provide a mechanism to disable it. Any mechanism for disabling buffering should be cleanly separated in the interface (for example, by providing a different constructor for FileInputStream, or through a method that disables or replaces the buffering mechanism), so that most developers do not even need to be aware of its existence.

特别令人烦恼(并且容易出错)的是,必须通过创建一个单独的 BufferedInputStream 对象来显式请求缓冲。如果开发人员忘记创建该对象,将没有缓冲,并且 I/O 将变慢。也许 Java 开发人员会争辩说,并不是每个人都希望对文件 I/O 使用缓冲,因此不应将其内置到基本机制中。他们可能会争辩说,最好分开保持缓冲,以便人们可以选择是否使用它。提供选择是好的,但是 应该设计接口以使常见情况尽可能简单 (请参阅第 6 页的公式)。几乎每个文件 I/O 用户都希望缓冲,因此默认情况下应提供缓冲。对于不需要缓冲的少数情况,该库可以提供一种禁用它的机制。

In contrast, the designers of the Unix system calls made the common case simple. For example, they recognized that sequential I/O is most common, so they made that the default behavior. Random access is still relatively easy to do, using the lseek system call, but a developer doing only sequential access need not be aware of that mechanism. If an interface has many features, but most developers only need to be aware of a few of them, the effective complexity of that interface is just the complexity of the commonly used features.

相反,Unix 系统调用的设计者使常见情况变得简单。例如,他们认识到顺序 I/O 是最常见的,因此他们将其作为默认行为。使用 lseek 系统调用,随机访问仍然相对容易实现,但是仅执行顺序访问的开发人员无需了解该机制。如果一个接口具有许多功能,但是大多数开发人员只需要了解其中的一些功能,那么该接口的有效复杂性就是常用功能的复杂性。

4.8 Conclusion 结论

By separating the interface of a module from its implementation, we can hide the complexity of the implementation from the rest of the system. Users of a module need only understand the abstraction provided by its interface. The most important issue in designing classes and other modules is to make them deep, so that they have simple interfaces for the common use cases, yet still provide significant functionality. This maximizes the amount of complexity that is concealed.

通过将模块的接口与其实现分开,我们可以将实现的复杂性从系统的其余部分中隐藏出来。模块的用户只需要了解其接口提供的抽象。设计类和其他模块时,最重要的问题是使它们更深,以使它们具有适用于常见用例的简单接口,但仍提供重要的功能。这样做能最大限度地隐藏复杂性。

1 There exist languages, mostly in the research community, where the overall behavior of a method or function can be described formally using a specification language. The specification can be checked automatically to ensure that it matches the implementation. An interesting question is whether such a formal specification could replace the informal parts of an interface. My current opinion is that an interface described in English is likely to be more intuitive and understandable for developers than one written in a formal specification language.

^1 存在语言,主要是在研究社区中,在其中可以使用规范语言来正式描述方法或功能的整体行为。可以自动检查该规范以确保它与实现相匹配。一个有趣的问题是,这样的正式规范是否可以代替接口的非正式部分。我目前的观点是,用英语描述的接口比使用正式规范语言编写的接口对开发人员来说更直观和易于理解。

`,90)]))}const p=t(i,[["render",l],["__file","ch04.html.vue"]]),d=JSON.parse('{"path":"/ch04.html","title":"第 4 章 模块应该是深的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"4.1 Modular design 模块化设计","slug":"_4-1-modular-design-模块化设计","link":"#_4-1-modular-design-模块化设计","children":[]},{"level":2,"title":"4.2 What’s in an interface? 接口中有什么?","slug":"_4-2-what-s-in-an-interface-接口中有什么","link":"#_4-2-what-s-in-an-interface-接口中有什么","children":[]},{"level":2,"title":"4.3 Abstractions 抽象","slug":"_4-3-abstractions-抽象","link":"#_4-3-abstractions-抽象","children":[]},{"level":2,"title":"4.4 Deep modules 深度模块","slug":"_4-4-deep-modules-深度模块","link":"#_4-4-deep-modules-深度模块","children":[]},{"level":2,"title":"4.5 Shallow modules 浅模块","slug":"_4-5-shallow-modules-浅模块","link":"#_4-5-shallow-modules-浅模块","children":[]},{"level":2,"title":"4.6 Classitis","slug":"_4-6-classitis","link":"#_4-6-classitis","children":[]},{"level":2,"title":"4.7 Examples: Java and Unix I/O 示例:Java 和 Unix I/O","slug":"_4-7-examples-java-and-unix-i-o-示例-java-和-unix-i-o","link":"#_4-7-examples-java-and-unix-i-o-示例-java-和-unix-i-o","children":[]},{"level":2,"title":"4.8 Conclusion 结论","slug":"_4-8-conclusion-结论","link":"#_4-8-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch04.md"}');export{p as comp,d as data}; +

A FileInputStream object provides only rudimentary I/O: it is not capable of performing buffered I/O, nor can it read or write serialized objects. The BufferedInputStream object adds buffering to a FileInputStream, and the ObjectInputStream adds the ability to read and write serialized objects. The first two objects in the code above, fileStream and bufferedStream, are never used once the file has been opened; all future operations use objectStream.

FileInputStream 对象仅提供基本的 I/O:它不能执行缓冲的 I/O,也不能读取或写入序列化的对象。BufferedInputStream 对象将缓冲添加到 FileInputStream,而 ObjectInputStream 添加了读取和写入序列化对象的功能。一旦文件被打开,上面代码中的前两个对象 fileStream 和 bufferedStream 将永远不会被使用。以后的所有操作都使用 objectStream。

It is particularly annoying (and error-prone) that buffering must be requested explicitly by creating a separate BufferedInputStream object; if a developer forgets to create this object, there will be no buffering and I/O will be slow. Perhaps the Java developers would argue that not everyone wants to use buffering for file I/O, so it shouldn’t be built into the base mechanism. They might argue that it’s better to keep buffering separate, so people can choose whether or not to use it. Providing choice is good, but interfaces should be designed to make the common case as simple as possible (see the formula on page 6). Almost every user of file I/O will want buffering, so it should be provided by default. For those few situations where buffering is not desirable, the library can provide a mechanism to disable it. Any mechanism for disabling buffering should be cleanly separated in the interface (for example, by providing a different constructor for FileInputStream, or through a method that disables or replaces the buffering mechanism), so that most developers do not even need to be aware of its existence.

特别令人烦恼(并且容易出错)的是,必须通过创建一个单独的 BufferedInputStream 对象来显式请求缓冲。如果开发人员忘记创建该对象,将没有缓冲,并且 I/O 将变慢。也许 Java 开发人员会争辩说,并不是每个人都希望对文件 I/O 使用缓冲,因此不应将其内置到基本机制中。他们可能会争辩说,最好分开保持缓冲,以便人们可以选择是否使用它。提供选择是好的,但是 应该设计接口以使常见情况尽可能简单 (请参阅第 6 页的公式)。几乎每个文件 I/O 用户都希望缓冲,因此默认情况下应提供缓冲。对于不需要缓冲的少数情况,该库可以提供一种禁用它的机制。

In contrast, the designers of the Unix system calls made the common case simple. For example, they recognized that sequential I/O is most common, so they made that the default behavior. Random access is still relatively easy to do, using the lseek system call, but a developer doing only sequential access need not be aware of that mechanism. If an interface has many features, but most developers only need to be aware of a few of them, the effective complexity of that interface is just the complexity of the commonly used features.

相反,Unix 系统调用的设计者使常见情况变得简单。例如,他们认识到顺序 I/O 是最常见的,因此他们将其作为默认行为。使用 lseek 系统调用,随机访问仍然相对容易实现,但是仅执行顺序访问的开发人员无需了解该机制。如果一个接口具有许多功能,但是大多数开发人员只需要了解其中的一些功能,那么该接口的有效复杂性就是常用功能的复杂性。

4.8 Conclusion 结论

By separating the interface of a module from its implementation, we can hide the complexity of the implementation from the rest of the system. Users of a module need only understand the abstraction provided by its interface. The most important issue in designing classes and other modules is to make them deep, so that they have simple interfaces for the common use cases, yet still provide significant functionality. This maximizes the amount of complexity that is concealed.

通过将模块的接口与其实现分开,我们可以将实现的复杂性从系统的其余部分中隐藏出来。模块的用户只需要了解其接口提供的抽象。设计类和其他模块时,最重要的问题是使它们更深,以使它们具有适用于常见用例的简单接口,但仍提供重要的功能。这样做能最大限度地隐藏复杂性。

1 There exist languages, mostly in the research community, where the overall behavior of a method or function can be described formally using a specification language. The specification can be checked automatically to ensure that it matches the implementation. An interesting question is whether such a formal specification could replace the informal parts of an interface. My current opinion is that an interface described in English is likely to be more intuitive and understandable for developers than one written in a formal specification language.

^1 存在语言,主要是在研究社区中,在其中可以使用规范语言来正式描述方法或功能的整体行为。可以自动检查该规范以确保它与实现相匹配。一个有趣的问题是,这样的正式规范是否可以代替接口的非正式部分。我目前的观点是,用英语描述的接口比使用正式规范语言编写的接口对开发人员来说更直观和易于理解。

`,90)]))}const p=t(i,[["render",l],["__file","ch04.html.vue"]]),d=JSON.parse('{"path":"/ch04.html","title":"第 4 章 模块应该是深的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"4.1 Modular design 模块化设计","slug":"_4-1-modular-design-模块化设计","link":"#_4-1-modular-design-模块化设计","children":[]},{"level":2,"title":"4.2 What’s in an interface? 接口中有什么?","slug":"_4-2-what-s-in-an-interface-接口中有什么","link":"#_4-2-what-s-in-an-interface-接口中有什么","children":[]},{"level":2,"title":"4.3 Abstractions 抽象","slug":"_4-3-abstractions-抽象","link":"#_4-3-abstractions-抽象","children":[]},{"level":2,"title":"4.4 Deep modules 深度模块","slug":"_4-4-deep-modules-深度模块","link":"#_4-4-deep-modules-深度模块","children":[]},{"level":2,"title":"4.5 Shallow modules 浅模块","slug":"_4-5-shallow-modules-浅模块","link":"#_4-5-shallow-modules-浅模块","children":[]},{"level":2,"title":"4.6 Classitis","slug":"_4-6-classitis","link":"#_4-6-classitis","children":[]},{"level":2,"title":"4.7 Examples: Java and Unix I/O 示例:Java 和 Unix I/O","slug":"_4-7-examples-java-and-unix-i-o-示例-java-和-unix-i-o","link":"#_4-7-examples-java-and-unix-i-o-示例-java-和-unix-i-o","children":[]},{"level":2,"title":"4.8 Conclusion 结论","slug":"_4-8-conclusion-结论","link":"#_4-8-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch04.md"}');export{p as comp,d as data}; diff --git a/assets/ch05.html-CdTpMeLr.js b/assets/ch05.html-DkKNi3Y4.js similarity index 99% rename from assets/ch05.html-CdTpMeLr.js rename to assets/ch05.html-DkKNi3Y4.js index 74350132..7980185d 100644 --- a/assets/ch05.html-CdTpMeLr.js +++ b/assets/ch05.html-DkKNi3Y4.js @@ -1,7 +1,7 @@ -import{_ as t,c as a,f as o,o as s}from"./app-CvqtBB8Z.js";const n="/A-Philosophy-of-Software-Design-zh/assets/00014-CH0PQhb9.gif",i={};function r(l,e){return s(),a("div",null,e[0]||(e[0]=[o('

第 5 章 信息隐藏(和泄漏)

Chapter 5 Information Hiding (and Leakage)

Chapter 4 argued that modules should be deep. This chapter, and the next few that follow, discuss techniques for creating deep modules.

第四章认为模块应该很深。本章及随后的其他章节讨论了创建深层模块的技术。

5.1 Information hiding 信息隐藏

The most important technique for achieving deep modules is information hiding. This technique was first described by David Parnas1. The basic idea is that each module should encapsulate a few pieces of knowledge, which represent design decisions. The knowledge is embedded in the module’s implementation but does not appear in its interface, so it is not visible to other modules.

实现深层模块最重要的技术是信息隐藏。该技术最早由 David Parnas1 描述。基本思想是每个模块应封装一些知识,这些知识代表设计决策。该知识嵌入在模块的实现中,但不会出现在其接口中,因此其他模块不可见。

The information hidden within a module usually consists of details about how to implement some mechanism. Here are some examples of information that might be hidden within a module:

隐藏在模块中的信息通常包含有关如何实现某种机制的详细信息。以下是一些信息可能隐藏在模块中的示例:

  • How to store information in a B-tree, and how to access it efficiently.
  • How to identify the physical disk block corresponding to each logical block within a file.
  • How to implement the TCP network protocol.
  • How to schedule threads on a multi-core processor.
  • How to parse JSON documents.

  • 如何在 B 树中存储信息,以及如何有效地访问它。
  • 如何识别文件中每个逻辑块相对应的物理磁盘块。
  • 如何实现 TCP 网络协议。
  • 如何在多核处理器上调度线程。
  • 如何解析 JSON 文档。

The hidden information includes data structures and algorithms related to the mechanism. It can also include lower-level details such as the size of a page, and it can include higher-level concepts that are more abstract, such as an assumption that most files are small.

隐藏的信息包括与该机制有关的数据结构和算法。它还可以包含较低级别的详细信息(例如页面大小),还可以包含更抽象的较高级别的概念,例如大多数文件较小的假设。

Information hiding reduces complexity in two ways. First, it simplifies the interface to a module. The interface reflects a simpler, more abstract view of the module’s functionality and hides the details; this reduces the cognitive load on developers who use the module. For instance, a developer using a B-tree class need not worry about the ideal fanout for nodes in the tree or how to keep the tree balanced. Second, information hiding makes it easier to evolve the system. If a piece of information is hidden, there are no dependencies on that information outside the module containing the information, so a design change related to that information will affect only the one module. For example, if the TCP protocol changes (to introduce a new mechanism for congestion control, for instance), the protocol’s implementation will have to be modified, but no changes should be needed in higher-level code that uses TCP to send and receive data.

信息隐藏在两个方面降低了复杂性。首先,它简化了模块的接口。接口用更简单、更抽象的方式反映了模块的功能,并隐藏了细节。这减少了使用该模块的开发人员的认知负担。例如,使用 B-tree 类的开发人员不需要考虑树中节点的理想扇出,也不需要考虑如何保持树的平衡。其次,信息隐藏使系统更容易演化。如果隐藏了一段信息,那么在包含该信息的模块之外就不存在对该信息的依赖,因此与该信息相关的设计更改将只影响一个模块。例如,如果 TCP 协议发生了变化(例如,为了引入一种新的拥塞控制机制),协议的实现就必须进行修改,但是在使用 TCP 发送和接收数据的高级代码中不需要进行任何修改。

When designing a new module, you should think carefully about what information can be hidden in that module. If you can hide more information, you should also be able to simplify the module’s interface, and this makes the module deeper.

设计新模块时,应仔细考虑可以在该模块中隐藏哪些信息。如果您可以隐藏更多信息,则还应该能够简化模块的接口,这会使模块更深。

Note: hiding variables and methods in a class by declaring them private isn’t the same thing as information hiding. Private elements can help with information hiding, since they make it impossible for the items to be accessed directly from outside the class. However, information about the private items can still be exposed through public methods such as getter and setter methods. When this happens the nature and usage of the variables are just as exposed as if the variables were public.

注意:通过声明变量和方法为私有来隐藏类中的变量和方法与信息隐藏不是同一回事。私有元素可以帮助隐藏信息,因为它们使无法从类外部直接访问项目。但是,私有属性仍可以通过公共方法(如 getter 和 setter 方法)公开。发生这种情况时,私有属性的性质和用法就如同公共属性一样是公开的。

The best form of information hiding is when information is totally hidden within a module, so that it is irrelevant and invisible to users of the module. However, partial information hiding also has value. For example, if a particular feature or piece of information is only needed by a few of a class’s users, and it is accessed through separate methods so that it isn’t visible in the most common use cases, then that information is mostly hidden. Such information will create fewer dependencies than information that is visible to every user of the class.

信息隐藏的最佳形式是将信息完全隐藏在模块中,从而使该信息对模块的用户无关且不可见。但是,部分信息隐藏也具有价值。例如,如果一个特定的特性或信息只被少数的类使用者所需要,并且它是通过不同的方法访问的,所以在最常见的用例中它是不可见的,那么这个信息大部分是隐藏的。与将信息暴露给所有类使用者相比, 这种方式会产生更少的依赖.

5.2 Information leakage 信息泄漏

The opposite of information hiding is information leakage. Information leakage occurs when a design decision is reflected in multiple modules. This creates a dependency between the modules: any change to that design decision will require changes to all of the involved modules. If a piece of information is reflected in the interface for a module, then by definition it has been leaked; thus, simpler interfaces tend to correlate with better information hiding. However, information can be leaked even if it doesn’t appear in a module’s interface. Suppose two classes both have knowledge of a particular file format (perhaps one class reads files in that format and the other class writes them). Even if neither class exposes that information in its interface, they both depend on the file format: if the format changes, both classes will need to be modified. Back-door leakage like this is more pernicious than leakage through an interface, because it isn’t obvious.

信息隐藏的反面是信息泄漏。当一个设计决策反映在多个模块中时,就会发生信息泄漏。这在模块之间创建了依赖关系:对该设计决策的任何更改都将要求对所有涉及的模块进行更改。如果一条信息反映在模块的接口中,则根据定义,该信息已经泄漏;因此,更简单的接口往往与更好的信息隐藏相关。但是,即使信息未出现在模块的接口中,也可能会泄漏信息。假设两个类都具有特定文件格式的知识(也许一个类读取该格式的文件,而另一个类写入它们)。即使两个类都不在其接口中公开该信息,它们都依赖于文件格式:如果格式更改,则两个类都将需要修改。像这样的后门泄漏比通过接口泄漏更有害,因为它并不明显。

Information leakage is one of the most important red flags in software design. One of the best skills you can learn as a software designer is a high level of sensitivity to information leakage. If you encounter information leakage between classes, ask yourself “How can I reorganize these classes so that this particular piece of knowledge only affects a single class?” If the affected classes are relatively small and closely tied to the leaked information, it may make sense to merge them into a single class. Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).

信息泄漏是软件设计中最重要的危险信号之一。作为一个软件设计师,你能学到的最好的技能之一就是对信息泄露的高度敏感性。如果您在类之间遇到信息泄漏,请自问“我如何才能重新组织这些类,使这些特定的知识只影响一个类?”如果受影响的类相对较小,并且与泄漏的信息紧密相关,那么将它们合并到一个类中是有意义的。另一种可能的方法是从所有受影响的类中提取信息,并创建一个只封装这些信息的新类。但是,这种方法只有在您能够找到一个从细节中抽象出来的简单接口时才有效;如果新类通过其接口公开了大部分知识,那么它就不会提供太多的价值(您只是用通过接口的泄漏替换了后门泄漏)。

img Red Flag: Information Leakage img

Information leakage occurs when the same knowledge is used in multiple places, such as two different classes that both understand the format of a particular type of file.

当在多个地方使用相同的知识时,例如两个都理解特定类型文件格式的不同类,就会发生信息泄漏。

5.3 Temporal decomposition 时间分解

One common cause of information leakage is a design style I call temporal decomposition. In temporal decomposition, the structure of a system corresponds to the time order in which operations will occur. Consider an application that reads a file in a particular format, modifies the contents of the file, and then writes the file out again. With temporal decomposition, this application might be broken into three classes: one to read the file, another to perform the modifications, and a third to write out the new version. Both the file reading and file writing steps have knowledge about the file format, which results in information leakage. The solution is to combine the core mechanisms for reading and writing files into a single class. This class will get used during both the reading and writing phases of the application. It’s easy to fall into the trap of temporal decomposition, because the order in which operations must occur is often on your mind when you code. However, most design decisions manifest themselves at several different times over the life of the application; as a result, temporal decomposition often results in information leakage.

信息泄漏的一个常见原因是我称为时间分解的设计风格。在时间分解中,系统的结构对应于操作将发生的时间顺序。考虑一个应用程序,该应用程序以特定格式读取文件,修改文件内容,然后再次将文件写出。通过时间分解,该应用程序可以分为三类:一类用于读取文件,另一类用于执行修改,第三类用于写出新版本。文件读取和文件写入步骤都具有有关文件格式的知识,这会导致信息泄漏。解决方案是将用于读写文件的核心机制结合到一个类中。该类将在应用程序的读取和写入阶段使用。因为在编写代码时通常会想到必须执行操作的顺序, 所以很容易陷入时间分解的陷阱。但是,大多数设计决策会在应用程序的整个生命周期中的多个不同时刻表现出来。结果,时间分解常常导致信息泄漏。

Order usually does matter, so it will be reflected somewhere in the application. However, it shouldn’t be reflected in the module structure unless that structure is consistent with information hiding (perhaps the different stages use totally different information). When designing modules, focus on the knowledge that’s needed to perform each task, not the order in which tasks occur.

顺序通常很重要,因此它将反映在应用程序中的某个位置。但是,除非该结构与信息隐藏保持一致(也许不同阶段使用完全不同的信息),否则不应将其反映在模块结构中。在设计模块时,应专注于执行每个任务所需的知识,而不是任务发生的顺序

img Red Flag: Temporal Decomposition img

In temporal decomposition, execution order is reflected in the code structure: operations that happen at different times are in different methods or classes. If the same knowledge is used at different points in execution, it gets encoded in multiple places, resulting in information leakage.

在时间分解中,执行顺序反映在代码结构中:在不同时间发生的操作在不同的方法或类中。如果相同的知识在不同的执行点使用,它会在多个位置被编码,从而导致信息泄漏。

5.4 Example: HTTP server 示例:HTTP 服务器

To illustrate the issues in information hiding, let’s consider the design decisions made by students implementing the HTTP protocol in a software design course. It’s useful to see both the things they did well and they areas where they had problems.

为了说明信息隐藏中的问题,让我们考虑由学生在软件设计课程中实现 HTTP 协议的设计决策。看到他们做得好的事情以及遇到问题的地方都是很有用的。

HTTP is a mechanism used by Web browsers to communicate with Web servers. When a user clicks on a link in a Web browser or submits a form, the browser uses HTTP to send a request over the network to a Web server. Once the server has processed the request, it sends a response back to the browser; the response normally contains a new Web page to display. The HTTP protocol specifies the format of requests and responses, both of which are represented textually. Figure 5.1 shows a sample HTTP request describing a form submission. The students in the course were asked to implement one or more classes to make it easy for Web servers to receive incoming HTTP requests and send responses.

HTTP 是 Web 浏览器用来与 Web 服务器通信的机制。当用户单击 Web 浏览器中的链接或提交表单时,浏览器使用 HTTP 通过网络将请求发送到 Web 服务器。服务器处理完请求后,会将响应发送回浏览器。该响应通常包含要显示的新网页。HTTP 协议指定了请求和响应的格式,两者均以文本形式表示。图 5.1 显示了描述表单提交的 HTTP 请求示例。要求课程中的学生实现一个或多个类,以使 Web 服务器可以轻松地接收传入的 HTTP 请求并发送响应。

Figure 5.1: A POST request in the HTTP protocol consists of text sent over a TCP socket. Each request contains an initial line, a collection of headers terminated by an empty line, and an optional body. The initial line contains the request type (POST is used for submitting form data), a URL indicating an operation (/comments/create) and optional parameters (photo_id has the value 246), and the HTTP protocol version used by the sender. Each header line consists of a name such as Content-Length followed by its value. For this request, the body contains additional parameters (comment and priority).

图 5.1:HTTP 协议中的 POST 请求包含通过 TCP 套接字发送的文本。每个请求都包含一个初始行,一个由空行终止的标头集合以及一个可选主体。初始行包含请求类型(POST 用于提交表单数据),指示操作(/comments/create)和可选参数(photo_id 的值为 246)的 URL,以及发送方使用的 HTTP 协议版本。每个标题行由一个名称(例如 Content-Length)及其后的值组成。对于此请求,正文包含其他参数(注释和优先级)。

5.5 Example: too many classes 示例:太多的类

The most common mistake made by students was to divide their code into a large number of shallow classes, which led to information leakage between the classes. One team used two different classes for receiving HTTP requests; the first class read the request from the network connection into a string, and the second class parsed the string. This is an example of a temporal decomposition (“first we read the request, then we parse it”). Information leakage occurred because an HTTP request can’t be read without parsing much of the message; for example, the Content-Length header specifies the length of the request body, so the headers must be parsed in order to compute the total request length. As a result, both classes needed to understand most of the structure of HTTP requests, and parsing code was duplicated in both classes. This approach also created extra complexity for callers, who had to invoke two methods in different classes, in a particular order, to receive a request.

学生最常犯的错误是将他们的代码分成大量的浅层类,这导致了类之间的信息泄漏。一个组使用两种不同的类来接收 HTTP 请求。第一类将来自网络连接的请求读取为字符串,第二类将字符串解析。这是时间分解的一个示例(“首先读取请求,然后解析它”)。发生信息泄漏是因为无法解析大量消息就无法读取 HTTP 请求。例如,Content-Length 标头指定了请求主体的长度,因此必须对标头进行解析才能计算总请求长度。结果,这两个类都需要了解 HTTP 请求的大多数结构,并且解析代码在两个类中都是重复的。这种方法也给调用方带来了额外的复杂性,他们必须以特定的顺序调用不同类中的两个方法来接收请求。

Because the classes shared so much information, it would have been better to merge them into a single class that handles both request reading and parsing. This provides better information hiding, since it isolates all knowledge of the request format in one class, and it also provides a simpler interface to callers (just one method to invoke).

由于这些类共享大量信息,因此最好将它们合并为一个同时处理请求读取和解析的类。由于它将请求格式的所有知识隔离在一个类中,因此它提供了更好的信息隐藏,并且还为调用者提供了一个更简单的接口(只是一种调用方法)。

This example illustrates a general theme in software design: information hiding can often be improved by making a class slightly larger. One reason for doing this is to bring together all of the code related to a particular capability (such as parsing an HTTP request), so that the resulting class contains everything related to that capability. A second reason for increasing the size of a class is to raise the level of the interface; for example, rather than having separate methods for each of three steps of a computation, have a single method that performs the entire computation. This can result in a simpler interface. Both of these benefits apply in the example of the previous paragraph: combining the classes brings together all of the code related to parsing an HTTP request, and it replaces two externally-visible methods with one. The combined class is deeper than the original classes.

此示例说明了软件设计中的一般主题:通常可以通过使类稍大一些来改善信息隐藏。这样做的一个原因是将与特定功能相关的所有代码(例如,解析 HTTP 请求)组合在一起,以便生成的类包含与该功能相关的所有内容。增加类大小的第二个原因是提高接口的级别。例如,与其为计算的三个步骤中的每一个步骤使用单独的方法,不如使用一种方法来执行整个计算。这样可以简化接口。这两个优点都适用于上一段的示例:组合类将与解析 HTTP 请求相关的所有代码组合在一起,并且用一个替换了两个外部可见的方法。组合后的类比原有的类都更深。

Of course, it is possible to take the notion of larger classes too far (such as a single class for the entire application). Chapter 9 will discuss conditions under which it makes sense to separate code into multiple smaller classes.

当然,可以将较大的类的概念考虑得太远(例如整个应用程序的单个类)。第 9 章将讨论将代码分成多个较小的类的合理条件。

5.6 Example: HTTP parameter handling 示例:HTTP 参数处理

After an HTTP request has been received by a server, the server needs to access some of the information from the request. The code that handles the request in Figure 5.1 might need to know the value of the photo_id parameter. Parameters can be specified in the first line of the request (photo_id in Figure 5.1) or, sometimes, in the body (comment and priority in Figure 5.1). Each parameter has a name and a value. The values of parameters use a special encoding called URL encoding; for example, in the value for comment in Figure 5.1, “+” is used to represent a space character, and “%21” is used instead of “!”. In order to process a request, the server will need the values for some of the parameters, and it will want them in unencoded form.

服务器收到 HTTP 请求后,服务器需要访问该请求中的某些信息。图 5.1 中处理请求的代码可能需要知道 photo_id 参数的值。参数可以在请求的第一行中指定(图 5.1 中的 photo_id),有时也可以在正文中指定(图 5.1 中的注释和优先级)。每个参数都有一个名称和一个值。参数的值使用一种称为 URL 编码的特殊编码。例如,在图 5.1 中的注释值中,“ +”代表空格字符,“%21”代替“!”。为了处理请求,服务器将需要某些参数的值,并且希望它们采用未编码的形式。

Most of the student projects made two good choices with respect to parameter handling. First, they recognized that server applications don’t care whether a parameter is specified in the header line or the body of the request, so they hid this distinction from callers and merged the parameters from both locations together. Second, they hid knowledge of URL encoding: the HTTP parser decodes parameter values before returning them to the Web server, so that the value of the comment parameter in Figure 5.1 will be returned as “What a cute baby!”, not “What+a+cute+baby%21”). In both of these cases, information hiding resulted in simpler APIs for the code using the HTTP module.

关于参数处理,大多数学生项目都做出了两个不错的选择。首先,他们认识到服务器应用程序不在乎是否在标题行或请求的正文中指定了参数,因此他们对调用者隐藏了这种区别,并将两个位置的参数合并在一起。其次,他们隐藏了 URL 编码的知识:HTTP 解析器在将参数值返回到 Web 服务器之前先对其进行解码,以便图 5.1 中的 comment 参数的值将返回 “What a cute baby!”,而不是 “What+a+cute+baby%21”)。在这两种情况下,信息隐藏都使使用 HTTP 模块的代码的 API 更加简单。

However, most of the students used an interface for returning parameters that was too shallow, and this resulted in lost opportunities for information hiding. Most projects used an object of type HTTPRequest to hold the parsed HTTP request, and the HTTPRequest class had a single method like the following one to return parameters:

但是,大多数学生使用的接口返回的参数太浅,这导致丢失信息隐藏的机会。大多数项目使用 HTTPRequest 类型的对象来保存已解析的 HTTP 请求,并且 HTTPRequest 类具有一种类似于以下方法的单个方法来返回参数:

public Map<String, String> getParams() {
+import{_ as t,c as a,f as o,o as s}from"./app-BQdhfat9.js";const n="/A-Philosophy-of-Software-Design-zh/assets/00014-CH0PQhb9.gif",i={};function r(l,e){return s(),a("div",null,e[0]||(e[0]=[o('

第 5 章 信息隐藏(和泄漏)

Chapter 5 Information Hiding (and Leakage)

Chapter 4 argued that modules should be deep. This chapter, and the next few that follow, discuss techniques for creating deep modules.

第四章认为模块应该很深。本章及随后的其他章节讨论了创建深层模块的技术。

5.1 Information hiding 信息隐藏

The most important technique for achieving deep modules is information hiding. This technique was first described by David Parnas1. The basic idea is that each module should encapsulate a few pieces of knowledge, which represent design decisions. The knowledge is embedded in the module’s implementation but does not appear in its interface, so it is not visible to other modules.

实现深层模块最重要的技术是信息隐藏。该技术最早由 David Parnas1 描述。基本思想是每个模块应封装一些知识,这些知识代表设计决策。该知识嵌入在模块的实现中,但不会出现在其接口中,因此其他模块不可见。

The information hidden within a module usually consists of details about how to implement some mechanism. Here are some examples of information that might be hidden within a module:

隐藏在模块中的信息通常包含有关如何实现某种机制的详细信息。以下是一些信息可能隐藏在模块中的示例:

  • How to store information in a B-tree, and how to access it efficiently.
  • How to identify the physical disk block corresponding to each logical block within a file.
  • How to implement the TCP network protocol.
  • How to schedule threads on a multi-core processor.
  • How to parse JSON documents.

  • 如何在 B 树中存储信息,以及如何有效地访问它。
  • 如何识别文件中每个逻辑块相对应的物理磁盘块。
  • 如何实现 TCP 网络协议。
  • 如何在多核处理器上调度线程。
  • 如何解析 JSON 文档。

The hidden information includes data structures and algorithms related to the mechanism. It can also include lower-level details such as the size of a page, and it can include higher-level concepts that are more abstract, such as an assumption that most files are small.

隐藏的信息包括与该机制有关的数据结构和算法。它还可以包含较低级别的详细信息(例如页面大小),还可以包含更抽象的较高级别的概念,例如大多数文件较小的假设。

Information hiding reduces complexity in two ways. First, it simplifies the interface to a module. The interface reflects a simpler, more abstract view of the module’s functionality and hides the details; this reduces the cognitive load on developers who use the module. For instance, a developer using a B-tree class need not worry about the ideal fanout for nodes in the tree or how to keep the tree balanced. Second, information hiding makes it easier to evolve the system. If a piece of information is hidden, there are no dependencies on that information outside the module containing the information, so a design change related to that information will affect only the one module. For example, if the TCP protocol changes (to introduce a new mechanism for congestion control, for instance), the protocol’s implementation will have to be modified, but no changes should be needed in higher-level code that uses TCP to send and receive data.

信息隐藏在两个方面降低了复杂性。首先,它简化了模块的接口。接口用更简单、更抽象的方式反映了模块的功能,并隐藏了细节。这减少了使用该模块的开发人员的认知负担。例如,使用 B-tree 类的开发人员不需要考虑树中节点的理想扇出,也不需要考虑如何保持树的平衡。其次,信息隐藏使系统更容易演化。如果隐藏了一段信息,那么在包含该信息的模块之外就不存在对该信息的依赖,因此与该信息相关的设计更改将只影响一个模块。例如,如果 TCP 协议发生了变化(例如,为了引入一种新的拥塞控制机制),协议的实现就必须进行修改,但是在使用 TCP 发送和接收数据的高级代码中不需要进行任何修改。

When designing a new module, you should think carefully about what information can be hidden in that module. If you can hide more information, you should also be able to simplify the module’s interface, and this makes the module deeper.

设计新模块时,应仔细考虑可以在该模块中隐藏哪些信息。如果您可以隐藏更多信息,则还应该能够简化模块的接口,这会使模块更深。

Note: hiding variables and methods in a class by declaring them private isn’t the same thing as information hiding. Private elements can help with information hiding, since they make it impossible for the items to be accessed directly from outside the class. However, information about the private items can still be exposed through public methods such as getter and setter methods. When this happens the nature and usage of the variables are just as exposed as if the variables were public.

注意:通过声明变量和方法为私有来隐藏类中的变量和方法与信息隐藏不是同一回事。私有元素可以帮助隐藏信息,因为它们使无法从类外部直接访问项目。但是,私有属性仍可以通过公共方法(如 getter 和 setter 方法)公开。发生这种情况时,私有属性的性质和用法就如同公共属性一样是公开的。

The best form of information hiding is when information is totally hidden within a module, so that it is irrelevant and invisible to users of the module. However, partial information hiding also has value. For example, if a particular feature or piece of information is only needed by a few of a class’s users, and it is accessed through separate methods so that it isn’t visible in the most common use cases, then that information is mostly hidden. Such information will create fewer dependencies than information that is visible to every user of the class.

信息隐藏的最佳形式是将信息完全隐藏在模块中,从而使该信息对模块的用户无关且不可见。但是,部分信息隐藏也具有价值。例如,如果一个特定的特性或信息只被少数的类使用者所需要,并且它是通过不同的方法访问的,所以在最常见的用例中它是不可见的,那么这个信息大部分是隐藏的。与将信息暴露给所有类使用者相比, 这种方式会产生更少的依赖.

5.2 Information leakage 信息泄漏

The opposite of information hiding is information leakage. Information leakage occurs when a design decision is reflected in multiple modules. This creates a dependency between the modules: any change to that design decision will require changes to all of the involved modules. If a piece of information is reflected in the interface for a module, then by definition it has been leaked; thus, simpler interfaces tend to correlate with better information hiding. However, information can be leaked even if it doesn’t appear in a module’s interface. Suppose two classes both have knowledge of a particular file format (perhaps one class reads files in that format and the other class writes them). Even if neither class exposes that information in its interface, they both depend on the file format: if the format changes, both classes will need to be modified. Back-door leakage like this is more pernicious than leakage through an interface, because it isn’t obvious.

信息隐藏的反面是信息泄漏。当一个设计决策反映在多个模块中时,就会发生信息泄漏。这在模块之间创建了依赖关系:对该设计决策的任何更改都将要求对所有涉及的模块进行更改。如果一条信息反映在模块的接口中,则根据定义,该信息已经泄漏;因此,更简单的接口往往与更好的信息隐藏相关。但是,即使信息未出现在模块的接口中,也可能会泄漏信息。假设两个类都具有特定文件格式的知识(也许一个类读取该格式的文件,而另一个类写入它们)。即使两个类都不在其接口中公开该信息,它们都依赖于文件格式:如果格式更改,则两个类都将需要修改。像这样的后门泄漏比通过接口泄漏更有害,因为它并不明显。

Information leakage is one of the most important red flags in software design. One of the best skills you can learn as a software designer is a high level of sensitivity to information leakage. If you encounter information leakage between classes, ask yourself “How can I reorganize these classes so that this particular piece of knowledge only affects a single class?” If the affected classes are relatively small and closely tied to the leaked information, it may make sense to merge them into a single class. Another possible approach is to pull the information out of all of the affected classes and create a new class that encapsulates just that information. However, this approach will be effective only if you can find a simple interface that abstracts away from the details; if the new class exposes most of the knowledge through its interface, then it won’t provide much value (you’ve simply replaced back-door leakage with leakage through an interface).

信息泄漏是软件设计中最重要的危险信号之一。作为一个软件设计师,你能学到的最好的技能之一就是对信息泄露的高度敏感性。如果您在类之间遇到信息泄漏,请自问“我如何才能重新组织这些类,使这些特定的知识只影响一个类?”如果受影响的类相对较小,并且与泄漏的信息紧密相关,那么将它们合并到一个类中是有意义的。另一种可能的方法是从所有受影响的类中提取信息,并创建一个只封装这些信息的新类。但是,这种方法只有在您能够找到一个从细节中抽象出来的简单接口时才有效;如果新类通过其接口公开了大部分知识,那么它就不会提供太多的价值(您只是用通过接口的泄漏替换了后门泄漏)。

img Red Flag: Information Leakage img

Information leakage occurs when the same knowledge is used in multiple places, such as two different classes that both understand the format of a particular type of file.

当在多个地方使用相同的知识时,例如两个都理解特定类型文件格式的不同类,就会发生信息泄漏。

5.3 Temporal decomposition 时间分解

One common cause of information leakage is a design style I call temporal decomposition. In temporal decomposition, the structure of a system corresponds to the time order in which operations will occur. Consider an application that reads a file in a particular format, modifies the contents of the file, and then writes the file out again. With temporal decomposition, this application might be broken into three classes: one to read the file, another to perform the modifications, and a third to write out the new version. Both the file reading and file writing steps have knowledge about the file format, which results in information leakage. The solution is to combine the core mechanisms for reading and writing files into a single class. This class will get used during both the reading and writing phases of the application. It’s easy to fall into the trap of temporal decomposition, because the order in which operations must occur is often on your mind when you code. However, most design decisions manifest themselves at several different times over the life of the application; as a result, temporal decomposition often results in information leakage.

信息泄漏的一个常见原因是我称为时间分解的设计风格。在时间分解中,系统的结构对应于操作将发生的时间顺序。考虑一个应用程序,该应用程序以特定格式读取文件,修改文件内容,然后再次将文件写出。通过时间分解,该应用程序可以分为三类:一类用于读取文件,另一类用于执行修改,第三类用于写出新版本。文件读取和文件写入步骤都具有有关文件格式的知识,这会导致信息泄漏。解决方案是将用于读写文件的核心机制结合到一个类中。该类将在应用程序的读取和写入阶段使用。因为在编写代码时通常会想到必须执行操作的顺序, 所以很容易陷入时间分解的陷阱。但是,大多数设计决策会在应用程序的整个生命周期中的多个不同时刻表现出来。结果,时间分解常常导致信息泄漏。

Order usually does matter, so it will be reflected somewhere in the application. However, it shouldn’t be reflected in the module structure unless that structure is consistent with information hiding (perhaps the different stages use totally different information). When designing modules, focus on the knowledge that’s needed to perform each task, not the order in which tasks occur.

顺序通常很重要,因此它将反映在应用程序中的某个位置。但是,除非该结构与信息隐藏保持一致(也许不同阶段使用完全不同的信息),否则不应将其反映在模块结构中。在设计模块时,应专注于执行每个任务所需的知识,而不是任务发生的顺序

img Red Flag: Temporal Decomposition img

In temporal decomposition, execution order is reflected in the code structure: operations that happen at different times are in different methods or classes. If the same knowledge is used at different points in execution, it gets encoded in multiple places, resulting in information leakage.

在时间分解中,执行顺序反映在代码结构中:在不同时间发生的操作在不同的方法或类中。如果相同的知识在不同的执行点使用,它会在多个位置被编码,从而导致信息泄漏。

5.4 Example: HTTP server 示例:HTTP 服务器

To illustrate the issues in information hiding, let’s consider the design decisions made by students implementing the HTTP protocol in a software design course. It’s useful to see both the things they did well and they areas where they had problems.

为了说明信息隐藏中的问题,让我们考虑由学生在软件设计课程中实现 HTTP 协议的设计决策。看到他们做得好的事情以及遇到问题的地方都是很有用的。

HTTP is a mechanism used by Web browsers to communicate with Web servers. When a user clicks on a link in a Web browser or submits a form, the browser uses HTTP to send a request over the network to a Web server. Once the server has processed the request, it sends a response back to the browser; the response normally contains a new Web page to display. The HTTP protocol specifies the format of requests and responses, both of which are represented textually. Figure 5.1 shows a sample HTTP request describing a form submission. The students in the course were asked to implement one or more classes to make it easy for Web servers to receive incoming HTTP requests and send responses.

HTTP 是 Web 浏览器用来与 Web 服务器通信的机制。当用户单击 Web 浏览器中的链接或提交表单时,浏览器使用 HTTP 通过网络将请求发送到 Web 服务器。服务器处理完请求后,会将响应发送回浏览器。该响应通常包含要显示的新网页。HTTP 协议指定了请求和响应的格式,两者均以文本形式表示。图 5.1 显示了描述表单提交的 HTTP 请求示例。要求课程中的学生实现一个或多个类,以使 Web 服务器可以轻松地接收传入的 HTTP 请求并发送响应。

Figure 5.1: A POST request in the HTTP protocol consists of text sent over a TCP socket. Each request contains an initial line, a collection of headers terminated by an empty line, and an optional body. The initial line contains the request type (POST is used for submitting form data), a URL indicating an operation (/comments/create) and optional parameters (photo_id has the value 246), and the HTTP protocol version used by the sender. Each header line consists of a name such as Content-Length followed by its value. For this request, the body contains additional parameters (comment and priority).

图 5.1:HTTP 协议中的 POST 请求包含通过 TCP 套接字发送的文本。每个请求都包含一个初始行,一个由空行终止的标头集合以及一个可选主体。初始行包含请求类型(POST 用于提交表单数据),指示操作(/comments/create)和可选参数(photo_id 的值为 246)的 URL,以及发送方使用的 HTTP 协议版本。每个标题行由一个名称(例如 Content-Length)及其后的值组成。对于此请求,正文包含其他参数(注释和优先级)。

5.5 Example: too many classes 示例:太多的类

The most common mistake made by students was to divide their code into a large number of shallow classes, which led to information leakage between the classes. One team used two different classes for receiving HTTP requests; the first class read the request from the network connection into a string, and the second class parsed the string. This is an example of a temporal decomposition (“first we read the request, then we parse it”). Information leakage occurred because an HTTP request can’t be read without parsing much of the message; for example, the Content-Length header specifies the length of the request body, so the headers must be parsed in order to compute the total request length. As a result, both classes needed to understand most of the structure of HTTP requests, and parsing code was duplicated in both classes. This approach also created extra complexity for callers, who had to invoke two methods in different classes, in a particular order, to receive a request.

学生最常犯的错误是将他们的代码分成大量的浅层类,这导致了类之间的信息泄漏。一个组使用两种不同的类来接收 HTTP 请求。第一类将来自网络连接的请求读取为字符串,第二类将字符串解析。这是时间分解的一个示例(“首先读取请求,然后解析它”)。发生信息泄漏是因为无法解析大量消息就无法读取 HTTP 请求。例如,Content-Length 标头指定了请求主体的长度,因此必须对标头进行解析才能计算总请求长度。结果,这两个类都需要了解 HTTP 请求的大多数结构,并且解析代码在两个类中都是重复的。这种方法也给调用方带来了额外的复杂性,他们必须以特定的顺序调用不同类中的两个方法来接收请求。

Because the classes shared so much information, it would have been better to merge them into a single class that handles both request reading and parsing. This provides better information hiding, since it isolates all knowledge of the request format in one class, and it also provides a simpler interface to callers (just one method to invoke).

由于这些类共享大量信息,因此最好将它们合并为一个同时处理请求读取和解析的类。由于它将请求格式的所有知识隔离在一个类中,因此它提供了更好的信息隐藏,并且还为调用者提供了一个更简单的接口(只是一种调用方法)。

This example illustrates a general theme in software design: information hiding can often be improved by making a class slightly larger. One reason for doing this is to bring together all of the code related to a particular capability (such as parsing an HTTP request), so that the resulting class contains everything related to that capability. A second reason for increasing the size of a class is to raise the level of the interface; for example, rather than having separate methods for each of three steps of a computation, have a single method that performs the entire computation. This can result in a simpler interface. Both of these benefits apply in the example of the previous paragraph: combining the classes brings together all of the code related to parsing an HTTP request, and it replaces two externally-visible methods with one. The combined class is deeper than the original classes.

此示例说明了软件设计中的一般主题:通常可以通过使类稍大一些来改善信息隐藏。这样做的一个原因是将与特定功能相关的所有代码(例如,解析 HTTP 请求)组合在一起,以便生成的类包含与该功能相关的所有内容。增加类大小的第二个原因是提高接口的级别。例如,与其为计算的三个步骤中的每一个步骤使用单独的方法,不如使用一种方法来执行整个计算。这样可以简化接口。这两个优点都适用于上一段的示例:组合类将与解析 HTTP 请求相关的所有代码组合在一起,并且用一个替换了两个外部可见的方法。组合后的类比原有的类都更深。

Of course, it is possible to take the notion of larger classes too far (such as a single class for the entire application). Chapter 9 will discuss conditions under which it makes sense to separate code into multiple smaller classes.

当然,可以将较大的类的概念考虑得太远(例如整个应用程序的单个类)。第 9 章将讨论将代码分成多个较小的类的合理条件。

5.6 Example: HTTP parameter handling 示例:HTTP 参数处理

After an HTTP request has been received by a server, the server needs to access some of the information from the request. The code that handles the request in Figure 5.1 might need to know the value of the photo_id parameter. Parameters can be specified in the first line of the request (photo_id in Figure 5.1) or, sometimes, in the body (comment and priority in Figure 5.1). Each parameter has a name and a value. The values of parameters use a special encoding called URL encoding; for example, in the value for comment in Figure 5.1, “+” is used to represent a space character, and “%21” is used instead of “!”. In order to process a request, the server will need the values for some of the parameters, and it will want them in unencoded form.

服务器收到 HTTP 请求后,服务器需要访问该请求中的某些信息。图 5.1 中处理请求的代码可能需要知道 photo_id 参数的值。参数可以在请求的第一行中指定(图 5.1 中的 photo_id),有时也可以在正文中指定(图 5.1 中的注释和优先级)。每个参数都有一个名称和一个值。参数的值使用一种称为 URL 编码的特殊编码。例如,在图 5.1 中的注释值中,“ +”代表空格字符,“%21”代替“!”。为了处理请求,服务器将需要某些参数的值,并且希望它们采用未编码的形式。

Most of the student projects made two good choices with respect to parameter handling. First, they recognized that server applications don’t care whether a parameter is specified in the header line or the body of the request, so they hid this distinction from callers and merged the parameters from both locations together. Second, they hid knowledge of URL encoding: the HTTP parser decodes parameter values before returning them to the Web server, so that the value of the comment parameter in Figure 5.1 will be returned as “What a cute baby!”, not “What+a+cute+baby%21”). In both of these cases, information hiding resulted in simpler APIs for the code using the HTTP module.

关于参数处理,大多数学生项目都做出了两个不错的选择。首先,他们认识到服务器应用程序不在乎是否在标题行或请求的正文中指定了参数,因此他们对调用者隐藏了这种区别,并将两个位置的参数合并在一起。其次,他们隐藏了 URL 编码的知识:HTTP 解析器在将参数值返回到 Web 服务器之前先对其进行解码,以便图 5.1 中的 comment 参数的值将返回 “What a cute baby!”,而不是 “What+a+cute+baby%21”)。在这两种情况下,信息隐藏都使使用 HTTP 模块的代码的 API 更加简单。

However, most of the students used an interface for returning parameters that was too shallow, and this resulted in lost opportunities for information hiding. Most projects used an object of type HTTPRequest to hold the parsed HTTP request, and the HTTPRequest class had a single method like the following one to return parameters:

但是,大多数学生使用的接口返回的参数太浅,这导致丢失信息隐藏的机会。大多数项目使用 HTTPRequest 类型的对象来保存已解析的 HTTP 请求,并且 HTTPRequest 类具有一种类似于以下方法的单个方法来返回参数:

public Map<String, String> getParams() {
     return this.params;
 }
 

Rather than returning a single parameter, the method returns a reference to the Map used internally to store all of the parameters. This method is shallow, and it exposes the internal representation used by the HTTPRequest class to store parameters. Any change to that representation will result in a change to the interface, which will require modifications to all callers. When implementations are modified, the changes often involve changes in the representation of key data structures (to improve performance, for example). Thus, it’s important to avoid exposing internal data structures as much as possible. This approach also makes more work for callers: a caller must first invoke getParams, then it must call another method to retrieve a specific parameter from the Map. Finally, callers must realize that they should not modify the Map returned by getParams, since that will affect the internal state of the HTTPRequest.

该方法不是返回单个参数,而是返回内部用于存储所有参数的映射的引用。这个方法是浅层的,它公开了 HTTPRequest 类用来存储参数的内部表示。对该表示的任何更改都将导致接口的更改,这将需要对所有调用者进行修改。在修改实现时,更改通常涉及关键数据结构表示的更改(例如,为了提高性能)。因此,尽量避免暴露内部数据结构是很重要的。这种方法还为调用者提供了更多的工作:调用者必须首先调用 getParams,然后必须调用另一个方法来从映射中检索特定的参数。最后,调用者必须意识到他们不应该修改 getParams 返回的映射,因为这会影响 HTTPRequest 的内部状态。

Here is a better interface for retrieving parameter values:

这是一个用于检索参数值的更好的接口:

public String getParameter(String name) { ... }
 
 public int getIntParameter(String name) { ... }
-

getParameter returns a parameter value as a string. It provides a slightly deeper interface than getParams above; more importantly, it hides the internal representation of parameters. getIntParameter converts the value of a parameter from its string form in the HTTP request to an integer (e.g., the photo_id parameter in Figure 5.1). This saves the caller from having to request string-to-integer conversion separately, and hides that mechanism from the caller. Additional methods for other data types, such as getDoubleParameter, could be defined if needed. (All of these methods will throw exceptions if the desired parameter doesn’t exist, or if it can’t be converted to the requested type; the exception declarations have been omitted in the code above).

getParameter 以字符串形式返回参数值。它提供了一个比上面的 getParams 更深的接口;更重要的是,它隐藏了参数的内部表示。getIntParameter 将参数的值从 HTTP 请求中的字符串形式转换为整数(例如,图 5.1 中的 photo_id 参数)。这使调用者不必单独请求字符串到整数的转换,并且对调用者隐藏了该机制。如果需要,可以定义其他数据类型的其他方法,例如 getDoubleParameter。(如果所需的参数不存在,或者无法将其转换为所请求的类型,则所有这些方法都将引发异常;上面的代码中省略了异常声明)。

5.7 Example: defaults in HTTP responses 示例:HTTP 响应中的默认值

The HTTP projects also had to provide support for generating HTTP responses. The most common mistake students made in this area was inadequate defaults. Each HTTP response must specify an HTTP protocol version; one team required callers to specify this version explicitly when creating a response object. However, the response version must correspond to that in the request object, and the request must already be passed as an argument when sending the response (it indicates where to send the response). Thus, it makes more sense for the HTTP classes to provide the response version automatically. The caller is unlikely to know what version to specify, and if the caller does specify a value, it probably results in information leakage between the HTTP library and the caller. HTTP responses also include a Date header specifying the time when the response was sent; the HTTP library should provide a sensible default for this as well.

HTTP 项目还必须提供对生成 HTTP 响应的支持。学生在该领域中最常见的错误是默认值不足。每个 HTTP 响应必须指定一个 HTTP 协议版本。一个组要求呼叫者在创建响应对象时明确指定此版本。但是,响应版本必须与请求对象中的版本相对应,并且在发送响应时必须已将请求作为参数传递(它指示将响应发送到何处)。因此,HTTP 类自动提供响应版本更为有意义。调用者不太可能知道要指定哪个版本,并且如果调用者确实指定了一个值,则可能导致 HTTP 库和调用者之间的信息泄漏。HTTP 响应还包括一个 Date 标头,用于指定发送响应的时间;HTTP 库也应该为此提供一个合理的默认值。

Defaults illustrate the principle that interfaces should be designed to make the common case as simple as possible. They are also an example of partial information hiding: in the normal case, the caller need not be aware of the existence of the defaulted item. In the rare cases where a caller needs to override a default, it will have to know about the value, and it can invoke a special method to modify it.

默认值说明了应该设计接口以使常见情况尽可能简单的原则。它们还是隐藏部分信息的一个示例:在正常情况下,调用者无需知道默认项的存在。在极少数情况下,调用方需要覆盖默认值,它必须知道该值,并且可以调用特殊方法来对其进行修改。

Whenever possible, classes should “do the right thing” without being explicitly asked. Defaults are an example of this. The Java I/O example on page 26 illustrates this point in a negative way. Buffering in file I/O is so universally desirable that noone should ever have to ask explicitly for it, or even be aware of its existence; the I/O classes should do the right thing and provide it automatically. The best features are the ones you get without even knowing they exist.

只要有可能,类就应该“做正确的事”,而无需明确要求。默认值就是一个例子。第 26 页上的 Java I/O 示例以负面方式说明了这一点。普遍希望在文件 I/O 中缓冲,以至于没有人需要明确要求它,甚至不知道它的存在。I/O 类应该做正确的事情并自动提供它。最好的功能是您甚至不知道它们存在的功能。

img Red Flag: Overexposure img

If the API for a commonly used feature forces users to learn about other features that are rarely used, this increases the cognitive load on users who don’t need the rarely used features.

如果一个常用特性的 API 迫使用户了解其他很少使用的特性,这将增加不需要这些很少使用的特性的用户的认知负荷。

5.8 Information hiding within a class 信息隐藏在类中

The examples in this chapter focused on information hiding as it relates to the externally visible APIs for classes, but information hiding can also be applied at other levels in the system, such as within a class. Try to design the private methods within a class so that each method encapsulates some information or capability and hides it from the rest of the class. In addition, try to minimize the number of places where each instance variable is used. Some variables may need to be accessed widely across the class, but others may be needed in only a few places; if you can reduce the number of places where a variable is used, you will eliminate dependencies within the class and reduce its complexity.

本章中的示例着重于信息隐藏,因为它与类的外部可见 API 有关,但是信息隐藏也可以应用于系统中的其他级别,例如类内。尝试在一个类中设计私有方法,以便每个方法都封装一些信息或功能,并将其隐藏在类的其余部分中。此外,请尽量减少使用每个实例变量的位置数量。有些变量可能需要在整个类中广泛使用,但是其他变量可能只需要在少数地方使用;如果可以减少使用变量的位置的数量,则将消除类内的依赖关系并降低其复杂性。

5.9 Taking it too far 走得太远

Information hiding only makes sense when the information being hidden is not needed outside its module. If the information is needed outside the module, then you must not hide it. Suppose that the performance of a module is affected by certain configuration parameters, and that different uses of the module will require different settings of the parameters. In this case it is important that the parameters are exposed in the interface of the module, so that they can be turned appropriately. As a software designer, your goal should be to minimize the amount of information needed outside a module; for example, if a module can automatically adjust its configuration, that is better than exposing configuration parameters. But, it’s important to recognize which information is needed outside a module and make sure it is exposed.

仅当在其模块外部不需要隐藏信息时,隐藏信息才有意义。如果模块外部需要该信息,则不得隐藏它。假设模块的性能受某些配置参数的影响,并且模块的不同用途将需要对参数进行不同的设置。在这种情况下,将参数暴露在模块的接口中很重要,以便可以对其进行适当的调整。作为软件设计师,您的目标应该是最大程度地减少模块外部所需的信息量。例如,如果模块可以自动调整其配置,那将比公开配置参数更好。但是,重要的是要识别模块外部需要哪些信息,并确保将其公开。

5.10 Conclusion 结论

Information hiding and deep modules are closely related. If a module hides a lot of information, that tends to increase the amount of functionality provided by the module while also reducing its interface. This makes the module deeper. Conversely, if a module doesn’t hide much information, then either it doesn’t have much functionality, or it has a complex interface; either way, the module is shallow.

信息隐藏和深层模块密切相关。如果模块隐藏了很多信息,则往往会增加模块提供的功能,同时还会减少其接口。这使模块更深。相反,如果一个模块没有隐藏太多信息,则它要么功能不多,要么接口复杂。无论哪种方式,模块都是浅的。

When decomposing a system into modules, try not to be influenced by the order in which operations will occur at runtime; that will lead you down the path of temporal decomposition, which will result in information leakage and shallow modules. Instead, think about the different pieces of knowledge that are needed to carry out the tasks of your application, and design each module to encapsulate one or a few of those pieces of knowledge. This will produce a clean and simple design with deep modules.

将系统分解为模块时,请尽量不要受运行时操作顺序的影响。这将使您沿着时间分解的路径前进,这将导致信息泄漏和模块浅。相反,请考虑执行应用程序任务所需的不同知识,并设计每个模块以封装这些知识中的一个或几个。这将产生一个干净简单的深模块设计。

1 David Parnas, “On the Criteria to be Used in Decomposing Systems into Modules,” Communications of the ACM, December 1972.

1 David Parnas,“关于将系统分解为模块的标准”,ACM 通讯,1972 年 12 月。

`,93)]))}const c=t(i,[["render",r],["__file","ch05.html.vue"]]),p=JSON.parse('{"path":"/ch05.html","title":"第 5 章 信息隐藏(和泄漏)","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"5.1 Information hiding 信息隐藏","slug":"_5-1-information-hiding-信息隐藏","link":"#_5-1-information-hiding-信息隐藏","children":[]},{"level":2,"title":"5.2 Information leakage 信息泄漏","slug":"_5-2-information-leakage-信息泄漏","link":"#_5-2-information-leakage-信息泄漏","children":[]},{"level":2,"title":"5.3 Temporal decomposition 时间分解","slug":"_5-3-temporal-decomposition-时间分解","link":"#_5-3-temporal-decomposition-时间分解","children":[]},{"level":2,"title":"5.4 Example: HTTP server 示例:HTTP 服务器","slug":"_5-4-example-http-server-示例-http-服务器","link":"#_5-4-example-http-server-示例-http-服务器","children":[]},{"level":2,"title":"5.5 Example: too many classes 示例:太多的类","slug":"_5-5-example-too-many-classes-示例-太多的类","link":"#_5-5-example-too-many-classes-示例-太多的类","children":[]},{"level":2,"title":"5.6 Example: HTTP parameter handling 示例:HTTP 参数处理","slug":"_5-6-example-http-parameter-handling-示例-http-参数处理","link":"#_5-6-example-http-parameter-handling-示例-http-参数处理","children":[]},{"level":2,"title":"5.7 Example: defaults in HTTP responses 示例:HTTP 响应中的默认值","slug":"_5-7-example-defaults-in-http-responses-示例-http-响应中的默认值","link":"#_5-7-example-defaults-in-http-responses-示例-http-响应中的默认值","children":[]},{"level":2,"title":"5.8 Information hiding within a class 信息隐藏在类中","slug":"_5-8-information-hiding-within-a-class-信息隐藏在类中","link":"#_5-8-information-hiding-within-a-class-信息隐藏在类中","children":[]},{"level":2,"title":"5.9 Taking it too far 走得太远","slug":"_5-9-taking-it-too-far-走得太远","link":"#_5-9-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"5.10 Conclusion 结论","slug":"_5-10-conclusion-结论","link":"#_5-10-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch05.md"}');export{c as comp,p as data}; +

getParameter returns a parameter value as a string. It provides a slightly deeper interface than getParams above; more importantly, it hides the internal representation of parameters. getIntParameter converts the value of a parameter from its string form in the HTTP request to an integer (e.g., the photo_id parameter in Figure 5.1). This saves the caller from having to request string-to-integer conversion separately, and hides that mechanism from the caller. Additional methods for other data types, such as getDoubleParameter, could be defined if needed. (All of these methods will throw exceptions if the desired parameter doesn’t exist, or if it can’t be converted to the requested type; the exception declarations have been omitted in the code above).

getParameter 以字符串形式返回参数值。它提供了一个比上面的 getParams 更深的接口;更重要的是,它隐藏了参数的内部表示。getIntParameter 将参数的值从 HTTP 请求中的字符串形式转换为整数(例如,图 5.1 中的 photo_id 参数)。这使调用者不必单独请求字符串到整数的转换,并且对调用者隐藏了该机制。如果需要,可以定义其他数据类型的其他方法,例如 getDoubleParameter。(如果所需的参数不存在,或者无法将其转换为所请求的类型,则所有这些方法都将引发异常;上面的代码中省略了异常声明)。

5.7 Example: defaults in HTTP responses 示例:HTTP 响应中的默认值

The HTTP projects also had to provide support for generating HTTP responses. The most common mistake students made in this area was inadequate defaults. Each HTTP response must specify an HTTP protocol version; one team required callers to specify this version explicitly when creating a response object. However, the response version must correspond to that in the request object, and the request must already be passed as an argument when sending the response (it indicates where to send the response). Thus, it makes more sense for the HTTP classes to provide the response version automatically. The caller is unlikely to know what version to specify, and if the caller does specify a value, it probably results in information leakage between the HTTP library and the caller. HTTP responses also include a Date header specifying the time when the response was sent; the HTTP library should provide a sensible default for this as well.

HTTP 项目还必须提供对生成 HTTP 响应的支持。学生在该领域中最常见的错误是默认值不足。每个 HTTP 响应必须指定一个 HTTP 协议版本。一个组要求呼叫者在创建响应对象时明确指定此版本。但是,响应版本必须与请求对象中的版本相对应,并且在发送响应时必须已将请求作为参数传递(它指示将响应发送到何处)。因此,HTTP 类自动提供响应版本更为有意义。调用者不太可能知道要指定哪个版本,并且如果调用者确实指定了一个值,则可能导致 HTTP 库和调用者之间的信息泄漏。HTTP 响应还包括一个 Date 标头,用于指定发送响应的时间;HTTP 库也应该为此提供一个合理的默认值。

Defaults illustrate the principle that interfaces should be designed to make the common case as simple as possible. They are also an example of partial information hiding: in the normal case, the caller need not be aware of the existence of the defaulted item. In the rare cases where a caller needs to override a default, it will have to know about the value, and it can invoke a special method to modify it.

默认值说明了应该设计接口以使常见情况尽可能简单的原则。它们还是隐藏部分信息的一个示例:在正常情况下,调用者无需知道默认项的存在。在极少数情况下,调用方需要覆盖默认值,它必须知道该值,并且可以调用特殊方法来对其进行修改。

Whenever possible, classes should “do the right thing” without being explicitly asked. Defaults are an example of this. The Java I/O example on page 26 illustrates this point in a negative way. Buffering in file I/O is so universally desirable that noone should ever have to ask explicitly for it, or even be aware of its existence; the I/O classes should do the right thing and provide it automatically. The best features are the ones you get without even knowing they exist.

只要有可能,类就应该“做正确的事”,而无需明确要求。默认值就是一个例子。第 26 页上的 Java I/O 示例以负面方式说明了这一点。普遍希望在文件 I/O 中缓冲,以至于没有人需要明确要求它,甚至不知道它的存在。I/O 类应该做正确的事情并自动提供它。最好的功能是您甚至不知道它们存在的功能。

img Red Flag: Overexposure img

If the API for a commonly used feature forces users to learn about other features that are rarely used, this increases the cognitive load on users who don’t need the rarely used features.

如果一个常用特性的 API 迫使用户了解其他很少使用的特性,这将增加不需要这些很少使用的特性的用户的认知负荷。

5.8 Information hiding within a class 信息隐藏在类中

The examples in this chapter focused on information hiding as it relates to the externally visible APIs for classes, but information hiding can also be applied at other levels in the system, such as within a class. Try to design the private methods within a class so that each method encapsulates some information or capability and hides it from the rest of the class. In addition, try to minimize the number of places where each instance variable is used. Some variables may need to be accessed widely across the class, but others may be needed in only a few places; if you can reduce the number of places where a variable is used, you will eliminate dependencies within the class and reduce its complexity.

本章中的示例着重于信息隐藏,因为它与类的外部可见 API 有关,但是信息隐藏也可以应用于系统中的其他级别,例如类内。尝试在一个类中设计私有方法,以便每个方法都封装一些信息或功能,并将其隐藏在类的其余部分中。此外,请尽量减少使用每个实例变量的位置数量。有些变量可能需要在整个类中广泛使用,但是其他变量可能只需要在少数地方使用;如果可以减少使用变量的位置的数量,则将消除类内的依赖关系并降低其复杂性。

5.9 Taking it too far 走得太远

Information hiding only makes sense when the information being hidden is not needed outside its module. If the information is needed outside the module, then you must not hide it. Suppose that the performance of a module is affected by certain configuration parameters, and that different uses of the module will require different settings of the parameters. In this case it is important that the parameters are exposed in the interface of the module, so that they can be turned appropriately. As a software designer, your goal should be to minimize the amount of information needed outside a module; for example, if a module can automatically adjust its configuration, that is better than exposing configuration parameters. But, it’s important to recognize which information is needed outside a module and make sure it is exposed.

仅当在其模块外部不需要隐藏信息时,隐藏信息才有意义。如果模块外部需要该信息,则不得隐藏它。假设模块的性能受某些配置参数的影响,并且模块的不同用途将需要对参数进行不同的设置。在这种情况下,将参数暴露在模块的接口中很重要,以便可以对其进行适当的调整。作为软件设计师,您的目标应该是最大程度地减少模块外部所需的信息量。例如,如果模块可以自动调整其配置,那将比公开配置参数更好。但是,重要的是要识别模块外部需要哪些信息,并确保将其公开。

5.10 Conclusion 结论

Information hiding and deep modules are closely related. If a module hides a lot of information, that tends to increase the amount of functionality provided by the module while also reducing its interface. This makes the module deeper. Conversely, if a module doesn’t hide much information, then either it doesn’t have much functionality, or it has a complex interface; either way, the module is shallow.

信息隐藏和深层模块密切相关。如果模块隐藏了很多信息,则往往会增加模块提供的功能,同时还会减少其接口。这使模块更深。相反,如果一个模块没有隐藏太多信息,则它要么功能不多,要么接口复杂。无论哪种方式,模块都是浅的。

When decomposing a system into modules, try not to be influenced by the order in which operations will occur at runtime; that will lead you down the path of temporal decomposition, which will result in information leakage and shallow modules. Instead, think about the different pieces of knowledge that are needed to carry out the tasks of your application, and design each module to encapsulate one or a few of those pieces of knowledge. This will produce a clean and simple design with deep modules.

将系统分解为模块时,请尽量不要受运行时操作顺序的影响。这将使您沿着时间分解的路径前进,这将导致信息泄漏和模块浅。相反,请考虑执行应用程序任务所需的不同知识,并设计每个模块以封装这些知识中的一个或几个。这将产生一个干净简单的深模块设计。

1 David Parnas, “On the Criteria to be Used in Decomposing Systems into Modules,” Communications of the ACM, December 1972.

1 David Parnas,“关于将系统分解为模块的标准”,ACM 通讯,1972 年 12 月。

`,93)]))}const c=t(i,[["render",r],["__file","ch05.html.vue"]]),p=JSON.parse('{"path":"/ch05.html","title":"第 5 章 信息隐藏(和泄漏)","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"5.1 Information hiding 信息隐藏","slug":"_5-1-information-hiding-信息隐藏","link":"#_5-1-information-hiding-信息隐藏","children":[]},{"level":2,"title":"5.2 Information leakage 信息泄漏","slug":"_5-2-information-leakage-信息泄漏","link":"#_5-2-information-leakage-信息泄漏","children":[]},{"level":2,"title":"5.3 Temporal decomposition 时间分解","slug":"_5-3-temporal-decomposition-时间分解","link":"#_5-3-temporal-decomposition-时间分解","children":[]},{"level":2,"title":"5.4 Example: HTTP server 示例:HTTP 服务器","slug":"_5-4-example-http-server-示例-http-服务器","link":"#_5-4-example-http-server-示例-http-服务器","children":[]},{"level":2,"title":"5.5 Example: too many classes 示例:太多的类","slug":"_5-5-example-too-many-classes-示例-太多的类","link":"#_5-5-example-too-many-classes-示例-太多的类","children":[]},{"level":2,"title":"5.6 Example: HTTP parameter handling 示例:HTTP 参数处理","slug":"_5-6-example-http-parameter-handling-示例-http-参数处理","link":"#_5-6-example-http-parameter-handling-示例-http-参数处理","children":[]},{"level":2,"title":"5.7 Example: defaults in HTTP responses 示例:HTTP 响应中的默认值","slug":"_5-7-example-defaults-in-http-responses-示例-http-响应中的默认值","link":"#_5-7-example-defaults-in-http-responses-示例-http-响应中的默认值","children":[]},{"level":2,"title":"5.8 Information hiding within a class 信息隐藏在类中","slug":"_5-8-information-hiding-within-a-class-信息隐藏在类中","link":"#_5-8-information-hiding-within-a-class-信息隐藏在类中","children":[]},{"level":2,"title":"5.9 Taking it too far 走得太远","slug":"_5-9-taking-it-too-far-走得太远","link":"#_5-9-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"5.10 Conclusion 结论","slug":"_5-10-conclusion-结论","link":"#_5-10-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch05.md"}');export{c as comp,p as data}; diff --git a/assets/ch06.html-DA1mEo-t.js b/assets/ch06.html-BJln56Ye.js similarity index 99% rename from assets/ch06.html-DA1mEo-t.js rename to assets/ch06.html-BJln56Ye.js index 0d96bc72..91dd09c5 100644 --- a/assets/ch06.html-DA1mEo-t.js +++ b/assets/ch06.html-BJln56Ye.js @@ -1,4 +1,4 @@ -import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o={};function i(r,e){return n(),a("div",null,e[0]||(e[0]=[s(`

第 6 章 通用模块更深入

Chapter 6 General-Purpose Modules are Deeper

One of the most common decisions that you will face when designing a new module is whether to implement it in a general-purpose or special-purpose fashion. Some might argue that you should take a general-purpose approach, in which you implement a mechanism that can be used to address a broad range of problems, not just the ones that are important today. In this case, the new mechanism may find unanticipated uses in the future, thereby saving time. The general-purpose approach seems consistent with the investment mindset discussed in Chapter 3, where you spend a bit more time up front to save time later on.

设计新模块时,您将面临的最普遍的决定之一就是是以通用还是专用方式实现它。有人可能会争辩说,您应该采用通用方式,在这种方式中,您将实现一种可用于解决广泛问题的机制,而不仅是当今重要的问题。在这种情况下,新机制可能会在将来发现意外用途,从而节省时间。通用方式似乎与第 3 章中讨论的投资思路一致,在这里您花了更多时间在前面,以节省以后的时间。

On the other hand, we know that it’s hard to predict the future needs of a software system, so a general-purpose solution might include facilities that are never actually needed. Furthermore, if you implement something that is too general-purpose, it might not do a good job of solving the particular problem you have today. As a result, some might argue that it’s better to focus on today’s needs, building just what you know you need, and specializing it for the way you plan to use it today. If you take the special-purpose approach and discover additional uses later, you can always refactor it to make it general-purpose. The special-purpose approach seems consistent with an incremental approach to software development.

另一方面,我们很难预测软件系统的未来需求,因此通用解决方案可能包含从未真正需要的功能。此外,如果您实现的东西过于通用,那么可能无法很好地解决您今天遇到的特定问题。因此,有些人可能会争辩说,最好只关注当今的需求,构建您所知道的需求,并针对您今天打算使用的方式进行专门化处理。如果您采用特殊用途的方式并在以后发现更多用途,则始终可以对其进行重构以使其通用。专用方式似乎与软件开发的增量方式一致。

6.1 Make classes somewhat general-purpose 使类变得通用

In my experience, the sweet spot is to implement new modules in a somewhat general-purpose fashion. The phrase “somewhat general-purpose” means that the module’s functionality should reflect your current needs, but its interface should not. Instead, the interface should be general enough to support multiple uses. The interface should be easy to use for today’s needs without being tied specifically to them. The word “somewhat” is important: don’t get carried away and build something so general-purpose that it is difficult to use for your current needs.

以我的经验,最有效的办法是以某种通用的方式实现新模块。短语“somewhat general-purpose(有点通用)”表示该模块的功能应反映您当前的需求,但其接口则不应该反映您当前的需求。相反,该接口应该足够通用以支持多种用途。该接口应易于使用,以满足当今的需求,而不必专门与它们联系在一起。“有点”这个词很重要:不要忘乎所以,建立一些太过通用的东西,以至于很难满足你当前的需求。

The most important (and perhaps surprising) benefit of the general-purpose approach is that it results in simpler and deeper interfaces than a special-purpose approach. The general-purpose approach can also save you time in the future, if you reuse the class for other purposes. However, even if the module is only used for its original purpose, the general-purpose approach is still better because of its simplicity.

通用方式最重要的(也许是令人惊讶的)好处是,与专用方式相比,它的接口更简单、更深。如果您将该类用于其他目的,则通用方式还可以节省将来的时间。但是,即使该模块仅用于其原始用途,由于其简单性,通用方式仍然更好。

6.2 Example: storing text for an editor 示例:为编辑器存储文本

Let’s consider an example from a software design class in which students were asked to build simple GUI text editors. The editors had to display a file and allow users to point, click, and type to edit the file. The editors had to support multiple simultaneous views of the same file in different windows; they also had to support multi-level undo and redo for modifications to the file.

让我们考虑一个软件设计课程的示例,其中要求学生构建简单的 GUI 文本编辑器。编辑器必须显示一个文件,并允许用户瞄准,单击并键入以编辑该文件。编辑器必须支持同一文件在不同窗口中的多个同时视图;他们还必须支持文件修改的多级撤销和重做。

Each of the student projects included a class that managed the underlying text of the file. The text classes typically provided methods for loading a file into memory, reading and modifying the text of the file, and writing the modified text back to a file.

每个学生项目都包括一个管理文件的基础文本的类。文本类通常提供以下方法:将文件加载到内存,读取和修改文件的文本以及将修改后的文本写回到文件。

Many of the student teams implemented special-purpose APIs for the text class. They knew that the class was going to be used in an interactive editor, so they thought about the features that the editor had to provide and tailored the API of the text class to those specific features. For example, if a user of the editor typed the backspace key, the editor deleted the character immediately to the left of the cursor; if the user typed the delete key, the editor deleted the character immediately to the right of the cursor. Knowing this, some of the teams created one method in the text class to support each of these specific features:

许多学生团队为文本类实现了专用的 API。他们知道该类将在交互式编辑器中被使用,因此他们考虑了编辑器必须提供的功能,并针对这些特定功能定制了文本类的 API。例如,如果编辑者的用户键入了退格键,则编辑者会立即删除光标左侧的字符;如果用户键入删除键,则编辑器立即删除光标右侧的字符。知道这一点后,一些团队在文本类中创建了一个方法来支持以下每个特定功能:

void backspace(Cursor cursor);
+import{_ as t,c as a,f as s,o as n}from"./app-BQdhfat9.js";const o={};function i(r,e){return n(),a("div",null,e[0]||(e[0]=[s(`

第 6 章 通用模块更深入

Chapter 6 General-Purpose Modules are Deeper

One of the most common decisions that you will face when designing a new module is whether to implement it in a general-purpose or special-purpose fashion. Some might argue that you should take a general-purpose approach, in which you implement a mechanism that can be used to address a broad range of problems, not just the ones that are important today. In this case, the new mechanism may find unanticipated uses in the future, thereby saving time. The general-purpose approach seems consistent with the investment mindset discussed in Chapter 3, where you spend a bit more time up front to save time later on.

设计新模块时,您将面临的最普遍的决定之一就是是以通用还是专用方式实现它。有人可能会争辩说,您应该采用通用方式,在这种方式中,您将实现一种可用于解决广泛问题的机制,而不仅是当今重要的问题。在这种情况下,新机制可能会在将来发现意外用途,从而节省时间。通用方式似乎与第 3 章中讨论的投资思路一致,在这里您花了更多时间在前面,以节省以后的时间。

On the other hand, we know that it’s hard to predict the future needs of a software system, so a general-purpose solution might include facilities that are never actually needed. Furthermore, if you implement something that is too general-purpose, it might not do a good job of solving the particular problem you have today. As a result, some might argue that it’s better to focus on today’s needs, building just what you know you need, and specializing it for the way you plan to use it today. If you take the special-purpose approach and discover additional uses later, you can always refactor it to make it general-purpose. The special-purpose approach seems consistent with an incremental approach to software development.

另一方面,我们很难预测软件系统的未来需求,因此通用解决方案可能包含从未真正需要的功能。此外,如果您实现的东西过于通用,那么可能无法很好地解决您今天遇到的特定问题。因此,有些人可能会争辩说,最好只关注当今的需求,构建您所知道的需求,并针对您今天打算使用的方式进行专门化处理。如果您采用特殊用途的方式并在以后发现更多用途,则始终可以对其进行重构以使其通用。专用方式似乎与软件开发的增量方式一致。

6.1 Make classes somewhat general-purpose 使类变得通用

In my experience, the sweet spot is to implement new modules in a somewhat general-purpose fashion. The phrase “somewhat general-purpose” means that the module’s functionality should reflect your current needs, but its interface should not. Instead, the interface should be general enough to support multiple uses. The interface should be easy to use for today’s needs without being tied specifically to them. The word “somewhat” is important: don’t get carried away and build something so general-purpose that it is difficult to use for your current needs.

以我的经验,最有效的办法是以某种通用的方式实现新模块。短语“somewhat general-purpose(有点通用)”表示该模块的功能应反映您当前的需求,但其接口则不应该反映您当前的需求。相反,该接口应该足够通用以支持多种用途。该接口应易于使用,以满足当今的需求,而不必专门与它们联系在一起。“有点”这个词很重要:不要忘乎所以,建立一些太过通用的东西,以至于很难满足你当前的需求。

The most important (and perhaps surprising) benefit of the general-purpose approach is that it results in simpler and deeper interfaces than a special-purpose approach. The general-purpose approach can also save you time in the future, if you reuse the class for other purposes. However, even if the module is only used for its original purpose, the general-purpose approach is still better because of its simplicity.

通用方式最重要的(也许是令人惊讶的)好处是,与专用方式相比,它的接口更简单、更深。如果您将该类用于其他目的,则通用方式还可以节省将来的时间。但是,即使该模块仅用于其原始用途,由于其简单性,通用方式仍然更好。

6.2 Example: storing text for an editor 示例:为编辑器存储文本

Let’s consider an example from a software design class in which students were asked to build simple GUI text editors. The editors had to display a file and allow users to point, click, and type to edit the file. The editors had to support multiple simultaneous views of the same file in different windows; they also had to support multi-level undo and redo for modifications to the file.

让我们考虑一个软件设计课程的示例,其中要求学生构建简单的 GUI 文本编辑器。编辑器必须显示一个文件,并允许用户瞄准,单击并键入以编辑该文件。编辑器必须支持同一文件在不同窗口中的多个同时视图;他们还必须支持文件修改的多级撤销和重做。

Each of the student projects included a class that managed the underlying text of the file. The text classes typically provided methods for loading a file into memory, reading and modifying the text of the file, and writing the modified text back to a file.

每个学生项目都包括一个管理文件的基础文本的类。文本类通常提供以下方法:将文件加载到内存,读取和修改文件的文本以及将修改后的文本写回到文件。

Many of the student teams implemented special-purpose APIs for the text class. They knew that the class was going to be used in an interactive editor, so they thought about the features that the editor had to provide and tailored the API of the text class to those specific features. For example, if a user of the editor typed the backspace key, the editor deleted the character immediately to the left of the cursor; if the user typed the delete key, the editor deleted the character immediately to the right of the cursor. Knowing this, some of the teams created one method in the text class to support each of these specific features:

许多学生团队为文本类实现了专用的 API。他们知道该类将在交互式编辑器中被使用,因此他们考虑了编辑器必须提供的功能,并针对这些特定功能定制了文本类的 API。例如,如果编辑者的用户键入了退格键,则编辑者会立即删除光标左侧的字符;如果用户键入删除键,则编辑器立即删除光标右侧的字符。知道这一点后,一些团队在文本类中创建了一个方法来支持以下每个特定功能:

void backspace(Cursor cursor);
 
 void delete(Cursor cursor);
 

Each of these methods takes the cursor position as its argument; a special type Cursor represents this position. The editor also had to support a selection that could be copied or deleted. The students handled this by defining a Selection class and passing an object of this class to the text class during deletions:

这些方法中的每一个都以光标位置作为参数。特殊类型的光标表示此位置。编辑器还必须支持复制或删除选中的区域。学生通过定义选择类并在删除过程中将该类的对象传递给文本类来解决此问题:

void deleteSelection(Selection selection);
@@ -9,4 +9,4 @@ import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o={};function i
 

This method returns a new position that is a given number of characters away from a given position. If the numChars argument is positive, the new position is later in the file than position; if numChars is negative, the new position is before position. The method automatically skips to the next or previous line when necessary. With these methods, the delete key can be implemented with the following code (assuming the cursor variable holds the current cursor position):

此方法返回一个新位置,该位置与给定位置相距给定字符数。如果 numChars 参数为正,则新位置在文件中比位置晚;如果 numChars 为负,则新位置在位置之前。必要时,该方法会自动跳到下一行或上一行。使用这些方法,可以使用以下代码来实现删除键(假定 cursor 变量保留当前光标的位置):

text.delete(cursor, text.changePosition(cursor, 1));
 

Similarly, the backspace key can be implemented as follows:

同样,可以按以下方式实现退格键:

text.delete(text.changePosition(cursor, -1), cursor);
 

With the general-purpose text API, the code to implement user interface functions such as delete and backspace is a bit longer than with the original approach using a specialized text API. However, the new code is more obvious than the old code. A developer working in the user interface module probably cares about which characters are deleted by the backspace key. With the new code, this is obvious. With the old code, the developer had to go to the text class and read the documentation and/or code of the backspace method to verify the behavior. Furthermore, the general-purpose approach has less code overall than the specialized approach, since it replaces a large number of special-purpose methods in the text class with a smaller number of general-purpose ones.

使用通用文本 API,实现用户界面功能(如删除和退格)的代码比使用专用文本 API 的原始方法要长一些。但是,新代码比旧代码更容易理解。在用户界面模块工作的开发者很关心退格键会删掉哪些字符。通过新代码,这点一目了然。而旧代码下,开发者必须去阅读文本类的文档或代码,才能明白退格键的作用。而且,采用通用方法比特定方法减少了很多代码,因为它用较少的通用方法代替了文本类中许多特定功能的方法。

A text class implemented with the general-purpose interface could potentially be used for other purposes besides an interactive editor. As one example, suppose you were building an application that modified a specified file by replacing all occurrences of a particular string with another string. Methods from the specialized text class, such as backspace and delete, would have little value for this application. However, the general-purpose text class would already have most of the functionality needed for the new application. All that is missing is a method to search for the next occurrence of a given string, such as this:

使用通用接口实现的文本类,除了可实现交互式编辑器外,还可以用于其他目的。作为一个示例,假设您正在构建一个应用程序,该应用程序通过将所有出现的特定字符串替换为另一个字符串来修改指定文件。专用文本类中的方法(例如,退格键和 Delete)对于此应用程序几乎没有价值。但是,通用文本类已经具有新应用程序所需的大多数功能。缺少的只是一种搜索给定字符串的下一个匹配项的方法,例如:

Position findNext(Position start, String string);
-

Of course, an interactive text editor is likely to have a mechanism for searching and replacing, in which case the text class would already include this method.

当然,交互式文本编辑器可能实现了搜索和替换的机制,在这种情况下,文本类将已经包含此方法。

6.4 Generality leads to better information hiding 通用性可以更好地隐藏信息

The general-purpose approach provides a cleaner separation between the text and user interface classes, which results in better information hiding. The text class need not be aware of specifics of the user interface, such as how the backspace key is handled; these details are now encapsulated in the user interface class. New user interface features can be added without creating new supporting functions in the text class. The general-purpose interface also reduces cognitive load: a developer working on the user interface only needs to learn a few simple methods, which can be reused for a variety of purposes.

通用方法在文本和用户界面类之间提供了更清晰的分隔,从而可以更好地隐藏信息。文本类不需要知道用户界面的详细信息,例如如何处理退格键。这些细节现在封装在用户界面类中。可以添加新的用户界面功能,而无需在文本类中创建新的支持功能。通用界面还减轻了认知负担:使用用户界面的开发人员只需要学习一些简单的方法,就可以将其重复用于各种目的。

The backspace method in the original version of the text class was a false abstraction. It purported to hide information about which characters are deleted, but the user interface module really needs to know this; user interface developers are likely to read the code of the backspace method in order to confirm its precise behavior. Putting the method in the text class just makes it harder for user interface developers to get the information they need. One of the most important elements of software design is determining who needs to know what, and when. When the details are important, it is better to make them explicit and as obvious as possible, such as the revised implementation of the backspace operation. Hiding this information behind an interface just creates obscurity.

文本类原始版本中的 backspace 方法是错误的抽象。它旨在隐藏有关删除哪些字符的信息,但是用户界面模块确实需要知道这一点。用户界面开发人员可能会阅读退格方法的代码,以确认其精确的行为。将方法放在文本类中只会使用户界面开发人员更难获得所需的信息。软件设计最重要的元素之一就是确定谁需要知道什么以及何时知道。当细节很重要时,最好使它们明确且尽可能明显,例如修订的 Backspace 操作实现。将这些信息隐藏在界面后面只会产生晦涩感。

6.5 Questions to ask yourself 问自己的问题

It is easier to recognize a clean general-purpose class design than it is to create one. Here are some questions you can ask yourself, which will help you to find the right balance between general-purpose and special-purpose for an interface.

识别干净的通用类设计要比创建一个简单。您可以问自己一些问题,这将帮助您在接口的通用和专用之间找到适当的平衡。

What is the simplest interface that will cover all my current needs? If you reduce the number of methods in an API without reducing its overall capabilities, then you are probably creating more general-purpose methods. The special-purpose text API had at least three methods for deleting text: backspace, delete, and deleteSelection. The more general-purpose API had only one method for deleting text, which served all three purposes. Reducing the number of methods makes sense only as long as the API for each individual method stays simple; if you have to introduce lots of additional arguments in order to reduce the number of methods, then you may not really be simplifying things.

满足我当前所有需求的最简单的接口是什么?如果减少 API 中的方法数量而不降低其整体功能,则可能正在创建更多通用的方法。专用文本 API 至少具有三种删除文本的方法:退格,删除和 deleteSelection。通用性更强的 API 只有一种删除文本的方法,可同时满足所有三个目的。仅在每种方法的 API 保持简单的前提下,减少方法的数量才有意义。如果您必须引入许多其他参数以减少方法数量,那么您可能并没有真正简化事情。

In how many situations will this method be used? If a method is designed for one particular use, such as the backspace method, that is a red flag that it may be too special-purpose. See if you can replace several special-purpose methods with a single general-purpose method.

在多少情况下会使用此方法?如果一种方法是为特定用途而设计的,例如退格方法,那是一个危险信号,它可能太特殊了。看看是否可以用一个通用方法替换几种专用方法。

Is this API easy to use for my current needs? This question can help you to determine when you have gone too far in making an API simple and general-purpose. If you have to write a lot of additional code to use a class for your current purpose, that’s a red flag that the interface doesn’t provide the right functionality. For example, one approach for the text class would be to design it around single-character operations: insert inserts a single character and delete deletes a single character. This API is both simple and general-purpose. However, it would not be particularly easy to use for a text editor: higher-level code would contain lots of loops to insert or delete ranges of characters. The single-character approach would also be inefficient for large operations. Thus it’s better for the text class to have built-in support for operations on ranges of characters.

这个 API 对我当前的需求来说容易使用吗?这个问题可以帮助你确定什么时候你在让一个 API 变得简单和通用方面走得太远了。如果您必须编写许多其他代码才能将类用于当前用途,那么这是一个危险信号,即该接口未提供正确的功能。例如,针对文本类的一种方式是围绕单字符操作进行设计:insert 插入单个字符 和 delete 删除单个字符。该 API 既简单又通用。但是,对于文本编辑器来说并不是特别容易使用:更高级别的代码将包含许多循环,用于插入或删除字符范围。单字符方法对于大型操作也将是低效的。因此,文本类最好内置对字符范围操作的支持。

6.6 Conclusion 结论

General-purpose interfaces have many advantages over special-purpose ones. They tend to be simpler, with fewer methods that are deeper. They also provide a cleaner separation between classes, whereas special-purpose interfaces tend to leak information between classes. Making your modules somewhat general-purpose is one of the best ways to reduce overall system complexity.

通用接口相比于特定目的的接口有许多优势。它们往往更简单,拥有更少但更深入的方法。它们还提供了类之间的更清晰的分隔,而专用接口则倾向于在类之间泄漏信息。使模块具有某种通用性是降低整体系统复杂性的最佳方法之一。

`,63)]))}const p=t(o,[["render",i],["__file","ch06.html.vue"]]),c=JSON.parse('{"path":"/ch06.html","title":"第 6 章 通用模块更深入","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"6.1 Make classes somewhat general-purpose 使类变得通用","slug":"_6-1-make-classes-somewhat-general-purpose-使类变得通用","link":"#_6-1-make-classes-somewhat-general-purpose-使类变得通用","children":[]},{"level":2,"title":"6.2 Example: storing text for an editor 示例:为编辑器存储文本","slug":"_6-2-example-storing-text-for-an-editor-示例-为编辑器存储文本","link":"#_6-2-example-storing-text-for-an-editor-示例-为编辑器存储文本","children":[]},{"level":2,"title":"6.3 A more general-purpose API 更通用的 API","slug":"_6-3-a-more-general-purpose-api-更通用的-api","link":"#_6-3-a-more-general-purpose-api-更通用的-api","children":[]},{"level":2,"title":"6.4 Generality leads to better information hiding 通用性可以更好地隐藏信息","slug":"_6-4-generality-leads-to-better-information-hiding-通用性可以更好地隐藏信息","link":"#_6-4-generality-leads-to-better-information-hiding-通用性可以更好地隐藏信息","children":[]},{"level":2,"title":"6.5 Questions to ask yourself 问自己的问题","slug":"_6-5-questions-to-ask-yourself-问自己的问题","link":"#_6-5-questions-to-ask-yourself-问自己的问题","children":[]},{"level":2,"title":"6.6 Conclusion 结论","slug":"_6-6-conclusion-结论","link":"#_6-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch06.md"}');export{p as comp,c as data}; +

Of course, an interactive text editor is likely to have a mechanism for searching and replacing, in which case the text class would already include this method.

当然,交互式文本编辑器可能实现了搜索和替换的机制,在这种情况下,文本类将已经包含此方法。

6.4 Generality leads to better information hiding 通用性可以更好地隐藏信息

The general-purpose approach provides a cleaner separation between the text and user interface classes, which results in better information hiding. The text class need not be aware of specifics of the user interface, such as how the backspace key is handled; these details are now encapsulated in the user interface class. New user interface features can be added without creating new supporting functions in the text class. The general-purpose interface also reduces cognitive load: a developer working on the user interface only needs to learn a few simple methods, which can be reused for a variety of purposes.

通用方法在文本和用户界面类之间提供了更清晰的分隔,从而可以更好地隐藏信息。文本类不需要知道用户界面的详细信息,例如如何处理退格键。这些细节现在封装在用户界面类中。可以添加新的用户界面功能,而无需在文本类中创建新的支持功能。通用界面还减轻了认知负担:使用用户界面的开发人员只需要学习一些简单的方法,就可以将其重复用于各种目的。

The backspace method in the original version of the text class was a false abstraction. It purported to hide information about which characters are deleted, but the user interface module really needs to know this; user interface developers are likely to read the code of the backspace method in order to confirm its precise behavior. Putting the method in the text class just makes it harder for user interface developers to get the information they need. One of the most important elements of software design is determining who needs to know what, and when. When the details are important, it is better to make them explicit and as obvious as possible, such as the revised implementation of the backspace operation. Hiding this information behind an interface just creates obscurity.

文本类原始版本中的 backspace 方法是错误的抽象。它旨在隐藏有关删除哪些字符的信息,但是用户界面模块确实需要知道这一点。用户界面开发人员可能会阅读退格方法的代码,以确认其精确的行为。将方法放在文本类中只会使用户界面开发人员更难获得所需的信息。软件设计最重要的元素之一就是确定谁需要知道什么以及何时知道。当细节很重要时,最好使它们明确且尽可能明显,例如修订的 Backspace 操作实现。将这些信息隐藏在界面后面只会产生晦涩感。

6.5 Questions to ask yourself 问自己的问题

It is easier to recognize a clean general-purpose class design than it is to create one. Here are some questions you can ask yourself, which will help you to find the right balance between general-purpose and special-purpose for an interface.

识别干净的通用类设计要比创建一个简单。您可以问自己一些问题,这将帮助您在接口的通用和专用之间找到适当的平衡。

What is the simplest interface that will cover all my current needs? If you reduce the number of methods in an API without reducing its overall capabilities, then you are probably creating more general-purpose methods. The special-purpose text API had at least three methods for deleting text: backspace, delete, and deleteSelection. The more general-purpose API had only one method for deleting text, which served all three purposes. Reducing the number of methods makes sense only as long as the API for each individual method stays simple; if you have to introduce lots of additional arguments in order to reduce the number of methods, then you may not really be simplifying things.

满足我当前所有需求的最简单的接口是什么?如果减少 API 中的方法数量而不降低其整体功能,则可能正在创建更多通用的方法。专用文本 API 至少具有三种删除文本的方法:退格,删除和 deleteSelection。通用性更强的 API 只有一种删除文本的方法,可同时满足所有三个目的。仅在每种方法的 API 保持简单的前提下,减少方法的数量才有意义。如果您必须引入许多其他参数以减少方法数量,那么您可能并没有真正简化事情。

In how many situations will this method be used? If a method is designed for one particular use, such as the backspace method, that is a red flag that it may be too special-purpose. See if you can replace several special-purpose methods with a single general-purpose method.

在多少情况下会使用此方法?如果一种方法是为特定用途而设计的,例如退格方法,那是一个危险信号,它可能太特殊了。看看是否可以用一个通用方法替换几种专用方法。

Is this API easy to use for my current needs? This question can help you to determine when you have gone too far in making an API simple and general-purpose. If you have to write a lot of additional code to use a class for your current purpose, that’s a red flag that the interface doesn’t provide the right functionality. For example, one approach for the text class would be to design it around single-character operations: insert inserts a single character and delete deletes a single character. This API is both simple and general-purpose. However, it would not be particularly easy to use for a text editor: higher-level code would contain lots of loops to insert or delete ranges of characters. The single-character approach would also be inefficient for large operations. Thus it’s better for the text class to have built-in support for operations on ranges of characters.

这个 API 对我当前的需求来说容易使用吗?这个问题可以帮助你确定什么时候你在让一个 API 变得简单和通用方面走得太远了。如果您必须编写许多其他代码才能将类用于当前用途,那么这是一个危险信号,即该接口未提供正确的功能。例如,针对文本类的一种方式是围绕单字符操作进行设计:insert 插入单个字符 和 delete 删除单个字符。该 API 既简单又通用。但是,对于文本编辑器来说并不是特别容易使用:更高级别的代码将包含许多循环,用于插入或删除字符范围。单字符方法对于大型操作也将是低效的。因此,文本类最好内置对字符范围操作的支持。

6.6 Conclusion 结论

General-purpose interfaces have many advantages over special-purpose ones. They tend to be simpler, with fewer methods that are deeper. They also provide a cleaner separation between classes, whereas special-purpose interfaces tend to leak information between classes. Making your modules somewhat general-purpose is one of the best ways to reduce overall system complexity.

通用接口相比于特定目的的接口有许多优势。它们往往更简单,拥有更少但更深入的方法。它们还提供了类之间的更清晰的分隔,而专用接口则倾向于在类之间泄漏信息。使模块具有某种通用性是降低整体系统复杂性的最佳方法之一。

`,63)]))}const p=t(o,[["render",i],["__file","ch06.html.vue"]]),c=JSON.parse('{"path":"/ch06.html","title":"第 6 章 通用模块更深入","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"6.1 Make classes somewhat general-purpose 使类变得通用","slug":"_6-1-make-classes-somewhat-general-purpose-使类变得通用","link":"#_6-1-make-classes-somewhat-general-purpose-使类变得通用","children":[]},{"level":2,"title":"6.2 Example: storing text for an editor 示例:为编辑器存储文本","slug":"_6-2-example-storing-text-for-an-editor-示例-为编辑器存储文本","link":"#_6-2-example-storing-text-for-an-editor-示例-为编辑器存储文本","children":[]},{"level":2,"title":"6.3 A more general-purpose API 更通用的 API","slug":"_6-3-a-more-general-purpose-api-更通用的-api","link":"#_6-3-a-more-general-purpose-api-更通用的-api","children":[]},{"level":2,"title":"6.4 Generality leads to better information hiding 通用性可以更好地隐藏信息","slug":"_6-4-generality-leads-to-better-information-hiding-通用性可以更好地隐藏信息","link":"#_6-4-generality-leads-to-better-information-hiding-通用性可以更好地隐藏信息","children":[]},{"level":2,"title":"6.5 Questions to ask yourself 问自己的问题","slug":"_6-5-questions-to-ask-yourself-问自己的问题","link":"#_6-5-questions-to-ask-yourself-问自己的问题","children":[]},{"level":2,"title":"6.6 Conclusion 结论","slug":"_6-6-conclusion-结论","link":"#_6-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch06.md"}');export{p as comp,c as data}; diff --git a/assets/ch07.html-DK38cxa_.js b/assets/ch07.html-BAAHFTXC.js similarity index 99% rename from assets/ch07.html-DK38cxa_.js rename to assets/ch07.html-BAAHFTXC.js index 5d93020e..e8a85fcf 100644 --- a/assets/ch07.html-DK38cxa_.js +++ b/assets/ch07.html-BAAHFTXC.js @@ -1,4 +1,4 @@ -import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o="/A-Philosophy-of-Software-Design-zh/assets/00015-BLheUT8l.jpeg",i="/A-Philosophy-of-Software-Design-zh/assets/00016-DkDR8uf8.gif",l={};function r(c,e){return n(),a("div",null,e[0]||(e[0]=[s(`

第 7 章 不同的层,不同的抽象

Chapter 7 Different Layer, Different Abstraction

Software systems are composed in layers, where higher layers use the facilities provided by lower layers. In a well-designed system, each layer provides a different abstraction from the layers above and below it; if you follow a single operation as it moves up and down through layers by invoking methods, the abstractions change with each method call. For example:

软件系统由层组成,其中较高的层使用较低层提供的功能。在设计良好的系统中,每一层都提供与其上,下两层不同的抽象。如果您通过调用方法来跟踪一个在层中上下移动的操作,那么抽象会随着每次方法调用而改变。例如:

  • In a file system, the uppermost layer implements a file abstraction. A file consists of a variable-length array of bytes, which can be updated by reading and writing variable-length byte ranges. The next lower layer in the file system implements a cache in memory of fixed-size disk blocks; callers can assume that frequently used blocks will stay in memory where they can be accessed quickly. The lowest layer consists of device drivers, which move blocks between secondary storage devices and memory.
  • In a network transport protocol such as TCP, the abstraction provided by the topmost layer is a stream of bytes delivered reliably from one machine to another. This level is built on a lower level that transmits packets of bounded size between machines on a best-effort basis: most packets will be delivered successfully, but some packets may be lost or delivered out of order.

  • 在文件系统中,最上层实现文件抽象。文件由可变长度的字节数组组成,可以通过读写可变长度的字节范围来更新该字节。文件系统的下一层在固定大小的磁盘块的内存中实现了高速缓存。调用者可以假定经常使用的块将保留在内存中,以便可以快速访问它们。最低层由设备驱动程序组成,它们在辅助存储设备和内存之间移动块。
  • 在诸如 TCP 的网络传输协议中,最顶层提供的抽象是从一台机器可靠地传递到另一台机器的字节流。这个级别建立在一个较低的级别上,它在机器之间尽最大努力传输有限大小的数据包:大多数数据包会成功传递,但有些数据包可能会丢失或传递顺序错误。

If a system contains adjacent layers with similar abstractions, this is a red flag that suggests a problem with the class decomposition. This chapter discusses situations where this happens, the problems that result, and how to refactor to eliminate the problems.

如果系统包含具有相似抽象的相邻层,则这是一个红色标记,表明类分解存在问题。本章讨论了发生这种情况的情况,导致的问题以及如何重构以消除问题。(如果一个系统中相邻的分层,存在了相似的抽象概念,这就表明分类拆解可能存在问题)

7.1 Pass-through methods 透传方法

When adjacent layers have similar abstractions, the problem often manifests itself in the form of pass-through methods. A pass-through method is one that does little except invoke another method, whose signature is similar or identical to that of the calling method. For example, a student project implementing a GUI text editor contained a class consisting almost entirely of pass-through methods. Here is an extract from that class:

当相邻的层具有相似的抽象时,问题通常以透传的形式表现出来。透传是一种除了调用另一个方法(其签名与调用方法的签名相似或相同)之外,很少功能的方法。例如,一个实现 GUI 文本编辑器的学生项目包含一个几乎完全由透传方法组成的类。这是该类的摘录:

public class TextDocument ... {
+import{_ as t,c as a,f as s,o as n}from"./app-BQdhfat9.js";const o="/A-Philosophy-of-Software-Design-zh/assets/00015-BLheUT8l.jpeg",i="/A-Philosophy-of-Software-Design-zh/assets/00016-DkDR8uf8.gif",l={};function r(c,e){return n(),a("div",null,e[0]||(e[0]=[s(`

第 7 章 不同的层,不同的抽象

Chapter 7 Different Layer, Different Abstraction

Software systems are composed in layers, where higher layers use the facilities provided by lower layers. In a well-designed system, each layer provides a different abstraction from the layers above and below it; if you follow a single operation as it moves up and down through layers by invoking methods, the abstractions change with each method call. For example:

软件系统由层组成,其中较高的层使用较低层提供的功能。在设计良好的系统中,每一层都提供与其上,下两层不同的抽象。如果您通过调用方法来跟踪一个在层中上下移动的操作,那么抽象会随着每次方法调用而改变。例如:

  • In a file system, the uppermost layer implements a file abstraction. A file consists of a variable-length array of bytes, which can be updated by reading and writing variable-length byte ranges. The next lower layer in the file system implements a cache in memory of fixed-size disk blocks; callers can assume that frequently used blocks will stay in memory where they can be accessed quickly. The lowest layer consists of device drivers, which move blocks between secondary storage devices and memory.
  • In a network transport protocol such as TCP, the abstraction provided by the topmost layer is a stream of bytes delivered reliably from one machine to another. This level is built on a lower level that transmits packets of bounded size between machines on a best-effort basis: most packets will be delivered successfully, but some packets may be lost or delivered out of order.

  • 在文件系统中,最上层实现文件抽象。文件由可变长度的字节数组组成,可以通过读写可变长度的字节范围来更新该字节。文件系统的下一层在固定大小的磁盘块的内存中实现了高速缓存。调用者可以假定经常使用的块将保留在内存中,以便可以快速访问它们。最低层由设备驱动程序组成,它们在辅助存储设备和内存之间移动块。
  • 在诸如 TCP 的网络传输协议中,最顶层提供的抽象是从一台机器可靠地传递到另一台机器的字节流。这个级别建立在一个较低的级别上,它在机器之间尽最大努力传输有限大小的数据包:大多数数据包会成功传递,但有些数据包可能会丢失或传递顺序错误。

If a system contains adjacent layers with similar abstractions, this is a red flag that suggests a problem with the class decomposition. This chapter discusses situations where this happens, the problems that result, and how to refactor to eliminate the problems.

如果系统包含具有相似抽象的相邻层,则这是一个红色标记,表明类分解存在问题。本章讨论了发生这种情况的情况,导致的问题以及如何重构以消除问题。(如果一个系统中相邻的分层,存在了相似的抽象概念,这就表明分类拆解可能存在问题)

7.1 Pass-through methods 透传方法

When adjacent layers have similar abstractions, the problem often manifests itself in the form of pass-through methods. A pass-through method is one that does little except invoke another method, whose signature is similar or identical to that of the calling method. For example, a student project implementing a GUI text editor contained a class consisting almost entirely of pass-through methods. Here is an extract from that class:

当相邻的层具有相似的抽象时,问题通常以透传的形式表现出来。透传是一种除了调用另一个方法(其签名与调用方法的签名相似或相同)之外,很少功能的方法。例如,一个实现 GUI 文本编辑器的学生项目包含一个几乎完全由透传方法组成的类。这是该类的摘录:

public class TextDocument ... {
     private TextArea textArea;
     private TextDocumentListener listener;
     ...
@@ -18,4 +18,4 @@ import{_ as t,c as a,f as s,o as n}from"./app-CvqtBB8Z.js";const o="/A-Philosoph
     }
     ...
 }
-

13 of the 15 public methods in that class were pass-through methods.

该类别中 15 个公共方法中的 13 个是透传方法。

img Red Flag: Pass-Through Method img

A pass-through method is one that does nothing except pass its arguments to another method, usually with the same API as the pass-through method. This typically indicates that there is not a clean division of responsibility between the classes.

透传方法除了将参数传递给另外一个与其有相同 API 的方法外,不执行任何操作。这通常表示各类之间没有明确的职责划分。

Pass-through methods make classes shallower: they increase the interface complexity of the class, which adds complexity, but they don’t increase the total functionality of the system. Of the four methods above, only the last one has any functionality, and even there it is trivial: the method checks the validity of one variable. Pass-through methods also create dependencies between classes: if the signature changes for the insertString method in TextArea, then the insertString method in TextDocument will have to change to match.

透传方法使类变浅:它们增加了类的接口复杂性,从而增加了复杂性,但是并没有增加系统的整体功能。在上述四个方法中,只有最后一个具有极少的功能,即使有也微乎其微:该方法检查一个变量的有效性。透传方法还会在类之间创建依赖关系:如果针对 TextArea 中的 insertString 方法更改了签名,则必须更改 TextDocument 中的 insertString 方法以进行匹配。

Pass-through methods indicate that there is confusion over the division of responsibility between classes. In the example above, the TextDocument class offers an insertString method, but the functionality for inserting text is implemented entirely in TextArea. This is usually a bad idea: the interface to a piece of functionality should be in the same class that implements the functionality. When you see pass-through methods from one class to another, consider the two classes and ask yourself “Exactly which features and abstractions is each of these classes responsible for?” You will probably notice that there is an overlap in responsibility between the classes.

透传方法表明类之间的责任划分存在混淆。在上面的示例中,TextDocument 类提供了 insertString 方法,但是用于插入文本的功能完全在 TextArea 中实现。这通常是一个坏主意:某个功能的接口应该在实现该功能的同一类中。当您看到从一个类到另一个类的透传方法时,请考虑这两个类,并问自己“这些类分别负责哪些功能和抽象?” 您可能会注意到,各类之间的职责重叠。

The solution is to refactor the classes so that each class has a distinct and coherent set of responsibilities. Figure 7.1 illustrates several ways to do this. One approach, shown in Figure 7.1(b), is to expose the lower level class directly to the callers of the higher level class, removing all responsibility for the feature from the higher level class. Another approach is to redistribute the functionality between the classes, as in Figure 7.1(c). Finally, if the classes can’t be disentangled, the best solution may be to merge them as in Figure 7.1(d).

解决方案是重构类,以使每个类都有各自不同且连贯的职责。图 7.1 说明了几种方法。一种方法,如图 7.1(b)所示,是将较低级别的类直接暴露给较高级别的类的调用者,而从较高级别的类中删除对该功能的所有责任。另一种方法是在类之间重新分配功能,如图 7.1(c)所示。最后,如果无法解开这些类,最好的解决方案可能是如图 7.1(d)所示合并它们。

Figure 7.1: Pass-through methods. In (a), class C1 contains three pass-through methods, which do nothing but invoke methods with the same signature in C2 (each symbol represents a particular method signature). The pass-through methods can be eliminated by having C1’s callers invoke C2 directly as in (b), by redistributing functionality between C1 and C2 to avoid calls between the classes as in (c), or by combining the classes as in (d).

图 7.1:透传方法。在(a)中,类 C1 包含三个透传方法,这些方法只调用 C2 中具有相同签名的方法(每个符号代表一个特定的方法签名)。可以通过使 C1 的调用方像在(b)中那样直接调用 C2,通过在 C1 和 C2 之间重新分配功能以避免在(c)中的类之间进行调用,或者通过组合在(d)中的类来消除透传方法。 。

In the example above, there were three classes with intertwined responsibilities: TextDocument, TextArea, and TextDocumentListener. The student eliminated the pass-through methods by moving methods between classes and collapsing the three classes into just two, whose responsibilities were more clearly differentiated.

在上面的示例中,职责交织的三个类为:TextDocument,TextArea 和 TextDocumentListener。学生通过在类之间移动方法并将三个类缩减为两个类来消除透传方法,这两个类的职责更加明确。

7.2 When is interface duplication OK? 什么时候可以有重复的接口?

Having methods with the same signature is not always bad. The important thing is that each new method should contribute significant functionality. Pass-through methods are bad because they contribute no new functionality.

具有相同签名的方法并不总是不好的。重要的是,每种新方法都应贡献重要的功能。透传方法很糟糕,因为它们不提供任何新功能。

One example where it’s useful for a method to call another method with the same signature is a dispatcher. A dispatcher is a method that uses its arguments to select one of several other methods to invoke; then it passes most or all of its arguments to the chosen method. The signature for the dispatcher is often the same as the signature for the methods that it calls. Even so, the dispatcher provides useful functionality: it chooses which of several other methods should carry out each task.

一个方法调用另一个具有相同签名的方法很有用的例子是调度器。调度器是一种方法,它使用自己的参数从其他几种方法中选择一种来调用;然后,它将其大部分或全部参数传递给选定的方法。调度程序的签名通常与其调用的方法的签名相同。尽管如此,调度程序还是提供了有用的功能:它选择其他几种方法中的哪一种来执行每个任务。

For example, when a Web server receives an incoming HTTP request from a Web browser, it invokes a dispatcher that examines the URL in the incoming request and selects a specific method to handle the request. Some URLs might be handled by returning the contents of a file on disk; others might be handled by invoking a procedure in a language such as PHP or JavaScript. The dispatch process can be quite intricate, and is usually driven by a set of rules that are matched against the incoming URL.

例如,当 Web 服务器从 Web 浏览器接收到传入的 HTTP 请求时,它将调用一个调度器来检查传入请求中的 URL 并选择一种特定的方法来处理该请求。某些 URL 可以通过返回磁盘上文件的内容来处理;其他的则可能通过调用诸如 PHP 或 JavaScript 之类的语言的程序来处理。分发过程可能非常复杂,通常由与传入 URL 匹配的一组规则来驱动。

It is fine for several methods to have the same signature as long as each of them provides useful and distinct functionality. The methods invoked by a dispatcher have this property. Another example is interfaces with multiple implementations, such as disk drivers in an operating system. Each driver provides support for a different kind of disk, but they all have the same interface. When several methods provide different implementations of the same interface, it reduces cognitive load. Once you have worked with one of these methods, it’s easier to work with the others, since you don’t need to learn a new interface. Methods like this are usually in the same layer and they don’t invoke each other.

只要每种方法都提供有用且独特的功能,几种方法都具有相同的签名是可以接受的。调度程序调用的方法具有此属性。另一个示例是具有多种实现方式的接口,例如操作系统中的磁盘驱动程序。每个驱动程序都支持不同类型的磁盘,但是它们都有相同的接口。当几种方法提供同一接口的不同实现时,它将减少认知负担。使用其中一种方法后,与其他方法一起使用会更容易,因为您无需学习新的接口。像这样的方法通常位于同一层,并且它们不会相互调用。

7.3 Decorators 装饰器

The decorator design pattern (also known as a “wrapper”) is one that encourages API duplication across layers. A decorator object takes an existing object and extends its functionality; it provides an API similar or identical to the underlying object, and its methods invoke the methods of the underlying object. In the Java I/O example from Chapter 4, the BufferedInputStream class is a decorator: given an InputStream object, it provides the same API but introduces buffering. For example, when its read method is invoked to read a single character, it invokes read on the underlying InputStream to read a much larger block, and saves the extra characters to satisfy future read calls. Another example occurs in windowing systems: a Window class implements a simple form of window that is not scrollable, and a ScrollableWindow class decorates the Window class by adding horizontal and vertical scrollbars.

装饰器设计模式(也称为“包装器”)是一种鼓励跨层复制 API 的模式。装饰对象接受现有对象并扩展其功能;它提供一个与底层对象相似或相同的 API,它的方法调用底层对象的方法。在第 4 章的 Java I/O 示例中,BufferedInputStream 类是一个装饰器:给定一个 InputStream 对象,它提供了相同的 API,但是引入了缓冲。例如,当它的 read 方法被调用来读取单个字符时,它会调用底层 InputStream 上的 read 来读取更大的块,并保存额外的字符来满足未来的 read 调用。另一个例子出现在窗口系统中:Window 类实现了一个不能滚动的窗口的简单形式,而 ScrollableWindow 类通过添加水平和垂直滚动条来装饰窗口类。

The motivation for decorators is to separate special-purpose extensions of a class from a more generic core. However, decorator classes tend to be shallow: they introduce a large amount of boilerplate for a small amount of new functionality. Decorator classes often contain many pass-through methods. It’s easy to overuse the decorator pattern, creating a new class for every small new feature. This results in an explosion of shallow classes, such as the Java I/O example.

装饰器的动机是将类的专用扩展与更通用的核心分开。但是,装饰器类往往很浅:它们引入了大量的样板,以实现少量的新功能。装饰器类通常包含许多透传方法。过度使用装饰器模式很容易,为每个小的新功能创建一个新类。这导致诸如 Java I/O 示例之类的浅层类激增。

Before creating a decorator class, consider alternatives such as the following:

创建装饰器类之前,请考虑以下替代方法:

  • Could you add the new functionality directly to the underlying class, rather than creating a decorator class? This makes sense if the new functionality is relatively general-purpose, or if it is logically related to the underlying class, or if most uses of the underlying class will also use the new functionality. For example, virtually everyone who creates a Java InputStream will also create a BufferedInputStream, and buffering is a natural part of I/O, so these classes should have been combined.
  • If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class?
  • Could you merge the new functionality with an existing decorator, rather than creating a new decorator? This would result in a single deeper decorator class rather than multiple shallow ones.
  • Finally, ask yourself whether the new functionality really needs to wrap the existing functionality: could you implement it as a stand-alone class that is independent of the base class? In the windowing example, the scrollbars could probably be implemented separately from the main window, without wrapping all of its existing functionality.

  • 您能否将新功能直接添加到基础类,而不是创建装饰器类?如果新功能是相对通用的,或者在逻辑上与基础类相关,或者如果基础类的大多数使用也将使用新功能,则这是有意义的。例如,几乎每个创建 Java InputStream 的人都会创建一个 BufferedInputStream,并且缓冲是 I/O 的自然组成部分,因此应该合并这些类。
  • 如果新功能专用于特定用例,将其与用例合并而不是创建单独的类是否有意义?
  • 您可以将新功能与现有的装饰器合并,而不是创建新的装饰器吗?这将产生一个更深的装饰器类,而不是多个浅的装饰器类。
  • 最后,问问自己新功能是否真的需要包装现有功能:是否可以将其实现为独立于基类的独立类?在窗口示例中,滚动条可能与主窗口分开实现,而无需包装其所有现有功能。

Sometimes decorators make sense, but there is usually a better alternative.

有时装饰者很有意义,但通常有更好的选择。

7.4 Interface versus implementation 接口与实现

Another application of the “different layer, different abstraction” rule is that the interface of a class should normally be different from its implementation: the representations used internally should be different from the abstractions that appear in the interface. If the two have similar abstractions, then the class probably isn’t very deep. For example, in the text editor project discussed in Chapter 6, most of the teams implemented the text module in terms of lines of text, with each line stored separately. Some of the teams also designed the APIs for the text class around lines, with methods such as getLine and putLine. However, this made the text class shallow and awkward to use. In the higher-level user interface code, it’s common to insert text in the middle of a line (e.g., when the user is typing) or to delete a range of text that spans lines. With a line-oriented API for the text class, callers were forced to split and join lines to implement the user-interface operations. This code was nontrivial and it was duplicated and scattered across the implementation of the user interface.

“不同层,不同抽象”规则的另一个应用是,类的接口通常应与其实现不同:内部使用的表示形式应与接口中出现的抽象形式不同。如果两者具有相似的抽象,则该类可能不是很深。例如,在第 6 章讨论的文本编辑器项目中,大多数团队都以文本行的形式实现了文本模块,每行分别存储。一些团队还使用 getLine 和 putLine 之类的方法围绕行设计了文本类的 API。但是,这使文本类使用起来较浅且笨拙。在较高级别的用户界面代码中,通常在行中间插入文本(例如,当用户键入内容时)或删除跨行的文本范围。通过用于文本类的面向行的 API,调用者被迫拆分和合并行以实现用户界面操作。这段代码很简单,并且在用户界面的实现中被复制和散布。

The text classes were much easier to use when they provided a character-oriented interface, such as an insert method that inserts an arbitrary string of text (which may include newlines) at an arbitrary position in the text and a delete method that deletes the text between two arbitrary positions in the text. Internally, the text was still represented in terms of lines. A character-oriented interface encapsulates the complexity of line splitting and joining inside the text class, which makes the text class deeper and simplifies higher level code that uses the class. With this approach, the text API is quite different from the line-oriented storage mechanism; the difference represents valuable functionality provided by the class.

文本类提供面向字符的接口时,使用起来要容易得多,例如,insert 方法可在文本的任意位置插入任意文本字符串(可能包括换行符),而 delete 方法则删除文本在文本中的两个任意位置之间。在内部,文本仍以行表示。面向字符的接口封装了文本类内部的行拆分和连接的复杂性,这使文本类更深,并简化了使用该类的高级代码。通过这种方法,文本 API 与面向行的存储机制大不相同。差异表示该类提供的有价值的功能。

7.5 Pass-through variables 传递变量

Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods. Figure 7.2(a) shows an example from a datacenter service. A command-line argument describes certificates to use for secure communication. This information is only needed by a low-level method m3, which calls a library method to open a socket, but it is passed down through all the methods on the path between main and m3. The cert variable appears in the signature of each of the intermediate methods.

跨层 API 重复的另一种形式是传递变量,该变量是通过一长串方法向下传递的变量。图 7.2(a)显示了数据中心服务的示例。命令行参数描述用于安全通信的证书。只有底层方法 m3 才需要此信息,该方法调用一个库方法来打开套接字,但是该信息会通过 main 和 m3 之间路径上的所有方法向下传递。cert 变量出现在每个中间方法的签名中。

Pass-through variables add complexity because they force all of the intermediate methods to be aware of their existence, even though the methods have no use for the variables. Furthermore, if a new variable comes into existence (for example, a system is initially built without support for certificates, but you later decide to add that support), you may have to modify a large number of interfaces and methods to pass the variable through all of the relevant paths.

传递变量增加了复杂性,因为它们强制所有中间方法知道它们的存在,即使这些方法对变量没有用处。此外,如果存在一个新变量(例如,最初构建的系统不支持证书,但是您后来决定添加该支持),则可能必须修改大量的接口和方法才能将变量传递给所有相关路径。

Eliminating pass-through variables can be challenging. One approach is to see if there is already an object shared between the topmost and bottommost methods. In the datacenter service example of Figure 7.2, perhaps there is an object containing other information about network communication, which is available to both main and m3. If so, main can store the certificate information in that object, so it needn’t be passed through all of the intervening methods on the path to m3 (see Figure 7.2(b)). However, if there is such an object, then it may itself be a pass-through variable (how else does m3 get access to it?).

消除传递变量可能具有挑战性。一种方法是查看最顶层和最底层方法之间是否已共享对象。在图 7.2 的数据中心服务示例中,也许存在一个对象,其中包含有关网络通信的其他信息,这对于 main 和 m3 都是可用的。如果是这样,main 可以将证书信息存储在该对象中,因此不必通过通往 m3 的路径上的所有干预方法来传递证书(请参见图 7.2(b))。但是,如果存在这样的对象,则它本身可能是传递变量(m3 还将如何访问它?)。

Another approach is to store the information in a global variable, as in Figure 7.2(c). This avoids the need to pass the information from method to method, but global variables almost always create other problems. For example, global variables make it impossible to create two independent instances of the same system in the same process, since accesses to the global variables will conflict. It may seem unlikely that you would need multiple instances in production, but they are often useful in testing.

另一种方法是将信息存储在全局变量中,如图 7.2(c)所示。这避免了将信息从一个方法传递到另一个方法的需要,但是全局变量几乎总是会产生其他问题。例如,全局变量使得不可能在同一过程中创建同一系统的两个独立实例,因为对全局变量的访问会发生冲突。在生产中似乎不太可能需要多个实例,但是它们通常在测试中很有用。

The solution I use most often is to introduce a context object as in Figure 7.2(d). A context stores all of the application’s global state (anything that would otherwise be a pass-through variable or global variable). Most applications have multiple variables in their global state, representing things such as configuration options, shared subsystems, and performance counters. There is one context object per instance of the system. The context allows multiple instances of the system to coexist in a single process, each with its own context.

我最常使用的解决方案是引入一个上下文对象,如图 7.2(d)所示。上下文存储应用程序的所有全局状态(否则将是传递变量或全局变量的任何状态)。大多数应用程序在其全局状态下具有多个变量,这些变量表示诸如配置选项,共享子系统和性能计数器之类的内容。每个系统实例只有一个上下文对象。上下文允许系统的多个实例在单个进程中共存,每个实例都有自己的上下文。

Unfortunately, the context will probably be needed in many places, so it can potentially become a pass-through variable. To reduce the number of methods that must be aware of it, a reference to the context can be saved in most of the system’s major objects. In the example of Figure 7.2(d), the class containing m3 stores a reference to the context as an instance variable in its objects. When a new object is created, the creating method retrieves the context reference from its object and passes it to the constructor for the new object. With this approach, the context is available everywhere, but it only appears as an explicit argument in constructors.

不幸的是,在许多地方可能都需要上下文,因此它有可能成为传递变量。为了减少必须意识到的方法数量,可以将上下文的引用保存在系统的大多数主要对象中。在图 7.2(d)的示例中,包含 m3 的类将对上下文的引用作为实例变量存储在其对象中。创建新对象时,创建方法将从其对象中检索上下文引用,并将其传递给新对象的构造函数。使用这种方法,上下文随处可见,但在构造函数中仅作为显式参数出现。

Figure 7.2: Possible techniques for dealing with a pass-through variable. In (a), cert is passed through methods m1 and m2 even though they don’t use it. In (b), main and m3 have shared access to an object, so the variable can be stored there instead of passing it through m1 and m2. In (c), cert is stored as a global variable. In (d), cert is stored in a context object along with other system-wide information, such as a timeout value and performance counters; a reference to the context is stored in all objects whose methods need access to it.

图 7.2:处理传递变量的可能技术。在(a)中,证书通过方法 m1 和 m2 传递,即使它们不使用它也是如此。在(b)中,main 和 m3 具有对一个对象的共享访问权,因此可以将变量存储在此处,而不用将其传递给 m1 和 m2。在(c)中,cert 存储为全局变量。在(d)中,证书与其他系统范围的信息(例如超时值和性能计数器)一起存储在上下文对象中;对上下文的引用存储在其方法需要访问它的所有对象中。

The context object unifies the handling of all system-global information and eliminates the need for pass-through variables. If a new variable needs to be added, it can be added to the context object; no existing code is affected except for the constructor and destructor for the context. The context makes it easy to identify and manage the global state of the system, since it is all stored in one place. The context is also convenient for testing: test code can change the global configuration of the application by modifying fields in the context. It would be much more difficult to implement such changes if the system used pass-through variables.

上下文对象统一了所有系统全局信息的处理,并且不需要传递变量。如果需要添加新变量,则可以将其添加到上下文对象;除了上下文的构造函数和析构函数外,现有代码均不受影响。由于上下文全部存储在一个位置,因此上下文可以轻松识别和管理系统的全局状态。上下文也便于测试:测试代码可以通过修改上下文中的字段来更改应用程序的全局配置。如果系统使用传递变量,则实施此类更改将更加困难。

Contexts are far from an ideal solution. The variables stored in a context have most of the disadvantages of global variables; for example, it may not be obvious why a particular variable is present, or where it is used. Without discipline, a context can turn into a huge grab-bag of data that creates nonobvious dependencies throughout the system. Contexts may also create thread-safety issues; the best way to avoid problems is for variables in a context to be immutable. Unfortunately, I haven’t found a better solution than contexts.

上下文远非理想的解决方案。存储在上下文中的变量具有全局变量的大多数缺点。例如,为什么存在特定变量或在何处使用特定变量可能并不明显。没有纪律,上下文会变成巨大的数据抓包,从而在整个系统中创建不明显的依赖关系。上下文也可能产生线程安全问题;避免问题的最佳方法是使上下文中的变量不可变。不幸的是,我没有找到比上下文更好的解决方案。

7.6 Conclusion 结论

Each piece of design infrastructure added to a system, such as an interface, argument, function, class, or definition, adds complexity, since developers must learn about this element. In order for an element to provide a net gain against complexity, it must eliminate some complexity that would be present in the absence of the design element. Otherwise, you are better off implementing the system without that particular element. For example, a class can reduce complexity by encapsulating functionality so that users of the class needn’t be aware of it.

添加到系统中的每一个设计基础设施,如接口、参数、函数、类或定义,都会增加复杂性,因为开发人员必须了解这个元素。为了使一个元素提供对抗复杂性的净增益,它必须消除在没有设计元素的情况下出现的一些复杂性。否则,您最好在没有该特定元素的情况下实现该系统。例如,一个类可以通过封装功能来降低复杂性,这样该类的用户就不必知道它了。

The “different layer, different abstraction” rule is just an application of this idea: if different layers have the same abstraction, such as pass-through methods or decorators, then there’s a good chance that they haven’t provided enough benefit to compensate for the additional infrastructure they represent. Similarly, pass-through arguments require each of several methods to be aware of their existence (which adds to complexity) without contributing additional functionality.

“不同的层,不同的抽象”规则只是此思想的一种应用:如果不同的层具有相同的抽象,例如透传方法或装饰器,则很有可能它们没有提供足够的利益来补偿它们代表的其他基础结构。类似地,传递参数要求几种方法中的每一种都知道它们的存在(这增加了复杂性),而又不提供其他功能。

',80)]))}const p=t(l,[["render",r],["__file","ch07.html.vue"]]),u=JSON.parse('{"path":"/ch07.html","title":"第 7 章 不同的层,不同的抽象","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"7.1 Pass-through methods 透传方法","slug":"_7-1-pass-through-methods-透传方法","link":"#_7-1-pass-through-methods-透传方法","children":[]},{"level":2,"title":"7.2 When is interface duplication OK? 什么时候可以有重复的接口?","slug":"_7-2-when-is-interface-duplication-ok-什么时候可以有重复的接口","link":"#_7-2-when-is-interface-duplication-ok-什么时候可以有重复的接口","children":[]},{"level":2,"title":"7.3 Decorators 装饰器","slug":"_7-3-decorators-装饰器","link":"#_7-3-decorators-装饰器","children":[]},{"level":2,"title":"7.4 Interface versus implementation 接口与实现","slug":"_7-4-interface-versus-implementation-接口与实现","link":"#_7-4-interface-versus-implementation-接口与实现","children":[]},{"level":2,"title":"7.5 Pass-through variables 传递变量","slug":"_7-5-pass-through-variables-传递变量","link":"#_7-5-pass-through-variables-传递变量","children":[]},{"level":2,"title":"7.6 Conclusion 结论","slug":"_7-6-conclusion-结论","link":"#_7-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch07.md"}');export{p as comp,u as data}; +

13 of the 15 public methods in that class were pass-through methods.

该类别中 15 个公共方法中的 13 个是透传方法。

img Red Flag: Pass-Through Method img

A pass-through method is one that does nothing except pass its arguments to another method, usually with the same API as the pass-through method. This typically indicates that there is not a clean division of responsibility between the classes.

透传方法除了将参数传递给另外一个与其有相同 API 的方法外,不执行任何操作。这通常表示各类之间没有明确的职责划分。

Pass-through methods make classes shallower: they increase the interface complexity of the class, which adds complexity, but they don’t increase the total functionality of the system. Of the four methods above, only the last one has any functionality, and even there it is trivial: the method checks the validity of one variable. Pass-through methods also create dependencies between classes: if the signature changes for the insertString method in TextArea, then the insertString method in TextDocument will have to change to match.

透传方法使类变浅:它们增加了类的接口复杂性,从而增加了复杂性,但是并没有增加系统的整体功能。在上述四个方法中,只有最后一个具有极少的功能,即使有也微乎其微:该方法检查一个变量的有效性。透传方法还会在类之间创建依赖关系:如果针对 TextArea 中的 insertString 方法更改了签名,则必须更改 TextDocument 中的 insertString 方法以进行匹配。

Pass-through methods indicate that there is confusion over the division of responsibility between classes. In the example above, the TextDocument class offers an insertString method, but the functionality for inserting text is implemented entirely in TextArea. This is usually a bad idea: the interface to a piece of functionality should be in the same class that implements the functionality. When you see pass-through methods from one class to another, consider the two classes and ask yourself “Exactly which features and abstractions is each of these classes responsible for?” You will probably notice that there is an overlap in responsibility between the classes.

透传方法表明类之间的责任划分存在混淆。在上面的示例中,TextDocument 类提供了 insertString 方法,但是用于插入文本的功能完全在 TextArea 中实现。这通常是一个坏主意:某个功能的接口应该在实现该功能的同一类中。当您看到从一个类到另一个类的透传方法时,请考虑这两个类,并问自己“这些类分别负责哪些功能和抽象?” 您可能会注意到,各类之间的职责重叠。

The solution is to refactor the classes so that each class has a distinct and coherent set of responsibilities. Figure 7.1 illustrates several ways to do this. One approach, shown in Figure 7.1(b), is to expose the lower level class directly to the callers of the higher level class, removing all responsibility for the feature from the higher level class. Another approach is to redistribute the functionality between the classes, as in Figure 7.1(c). Finally, if the classes can’t be disentangled, the best solution may be to merge them as in Figure 7.1(d).

解决方案是重构类,以使每个类都有各自不同且连贯的职责。图 7.1 说明了几种方法。一种方法,如图 7.1(b)所示,是将较低级别的类直接暴露给较高级别的类的调用者,而从较高级别的类中删除对该功能的所有责任。另一种方法是在类之间重新分配功能,如图 7.1(c)所示。最后,如果无法解开这些类,最好的解决方案可能是如图 7.1(d)所示合并它们。

Figure 7.1: Pass-through methods. In (a), class C1 contains three pass-through methods, which do nothing but invoke methods with the same signature in C2 (each symbol represents a particular method signature). The pass-through methods can be eliminated by having C1’s callers invoke C2 directly as in (b), by redistributing functionality between C1 and C2 to avoid calls between the classes as in (c), or by combining the classes as in (d).

图 7.1:透传方法。在(a)中,类 C1 包含三个透传方法,这些方法只调用 C2 中具有相同签名的方法(每个符号代表一个特定的方法签名)。可以通过使 C1 的调用方像在(b)中那样直接调用 C2,通过在 C1 和 C2 之间重新分配功能以避免在(c)中的类之间进行调用,或者通过组合在(d)中的类来消除透传方法。 。

In the example above, there were three classes with intertwined responsibilities: TextDocument, TextArea, and TextDocumentListener. The student eliminated the pass-through methods by moving methods between classes and collapsing the three classes into just two, whose responsibilities were more clearly differentiated.

在上面的示例中,职责交织的三个类为:TextDocument,TextArea 和 TextDocumentListener。学生通过在类之间移动方法并将三个类缩减为两个类来消除透传方法,这两个类的职责更加明确。

7.2 When is interface duplication OK? 什么时候可以有重复的接口?

Having methods with the same signature is not always bad. The important thing is that each new method should contribute significant functionality. Pass-through methods are bad because they contribute no new functionality.

具有相同签名的方法并不总是不好的。重要的是,每种新方法都应贡献重要的功能。透传方法很糟糕,因为它们不提供任何新功能。

One example where it’s useful for a method to call another method with the same signature is a dispatcher. A dispatcher is a method that uses its arguments to select one of several other methods to invoke; then it passes most or all of its arguments to the chosen method. The signature for the dispatcher is often the same as the signature for the methods that it calls. Even so, the dispatcher provides useful functionality: it chooses which of several other methods should carry out each task.

一个方法调用另一个具有相同签名的方法很有用的例子是调度器。调度器是一种方法,它使用自己的参数从其他几种方法中选择一种来调用;然后,它将其大部分或全部参数传递给选定的方法。调度程序的签名通常与其调用的方法的签名相同。尽管如此,调度程序还是提供了有用的功能:它选择其他几种方法中的哪一种来执行每个任务。

For example, when a Web server receives an incoming HTTP request from a Web browser, it invokes a dispatcher that examines the URL in the incoming request and selects a specific method to handle the request. Some URLs might be handled by returning the contents of a file on disk; others might be handled by invoking a procedure in a language such as PHP or JavaScript. The dispatch process can be quite intricate, and is usually driven by a set of rules that are matched against the incoming URL.

例如,当 Web 服务器从 Web 浏览器接收到传入的 HTTP 请求时,它将调用一个调度器来检查传入请求中的 URL 并选择一种特定的方法来处理该请求。某些 URL 可以通过返回磁盘上文件的内容来处理;其他的则可能通过调用诸如 PHP 或 JavaScript 之类的语言的程序来处理。分发过程可能非常复杂,通常由与传入 URL 匹配的一组规则来驱动。

It is fine for several methods to have the same signature as long as each of them provides useful and distinct functionality. The methods invoked by a dispatcher have this property. Another example is interfaces with multiple implementations, such as disk drivers in an operating system. Each driver provides support for a different kind of disk, but they all have the same interface. When several methods provide different implementations of the same interface, it reduces cognitive load. Once you have worked with one of these methods, it’s easier to work with the others, since you don’t need to learn a new interface. Methods like this are usually in the same layer and they don’t invoke each other.

只要每种方法都提供有用且独特的功能,几种方法都具有相同的签名是可以接受的。调度程序调用的方法具有此属性。另一个示例是具有多种实现方式的接口,例如操作系统中的磁盘驱动程序。每个驱动程序都支持不同类型的磁盘,但是它们都有相同的接口。当几种方法提供同一接口的不同实现时,它将减少认知负担。使用其中一种方法后,与其他方法一起使用会更容易,因为您无需学习新的接口。像这样的方法通常位于同一层,并且它们不会相互调用。

7.3 Decorators 装饰器

The decorator design pattern (also known as a “wrapper”) is one that encourages API duplication across layers. A decorator object takes an existing object and extends its functionality; it provides an API similar or identical to the underlying object, and its methods invoke the methods of the underlying object. In the Java I/O example from Chapter 4, the BufferedInputStream class is a decorator: given an InputStream object, it provides the same API but introduces buffering. For example, when its read method is invoked to read a single character, it invokes read on the underlying InputStream to read a much larger block, and saves the extra characters to satisfy future read calls. Another example occurs in windowing systems: a Window class implements a simple form of window that is not scrollable, and a ScrollableWindow class decorates the Window class by adding horizontal and vertical scrollbars.

装饰器设计模式(也称为“包装器”)是一种鼓励跨层复制 API 的模式。装饰对象接受现有对象并扩展其功能;它提供一个与底层对象相似或相同的 API,它的方法调用底层对象的方法。在第 4 章的 Java I/O 示例中,BufferedInputStream 类是一个装饰器:给定一个 InputStream 对象,它提供了相同的 API,但是引入了缓冲。例如,当它的 read 方法被调用来读取单个字符时,它会调用底层 InputStream 上的 read 来读取更大的块,并保存额外的字符来满足未来的 read 调用。另一个例子出现在窗口系统中:Window 类实现了一个不能滚动的窗口的简单形式,而 ScrollableWindow 类通过添加水平和垂直滚动条来装饰窗口类。

The motivation for decorators is to separate special-purpose extensions of a class from a more generic core. However, decorator classes tend to be shallow: they introduce a large amount of boilerplate for a small amount of new functionality. Decorator classes often contain many pass-through methods. It’s easy to overuse the decorator pattern, creating a new class for every small new feature. This results in an explosion of shallow classes, such as the Java I/O example.

装饰器的动机是将类的专用扩展与更通用的核心分开。但是,装饰器类往往很浅:它们引入了大量的样板,以实现少量的新功能。装饰器类通常包含许多透传方法。过度使用装饰器模式很容易,为每个小的新功能创建一个新类。这导致诸如 Java I/O 示例之类的浅层类激增。

Before creating a decorator class, consider alternatives such as the following:

创建装饰器类之前,请考虑以下替代方法:

  • Could you add the new functionality directly to the underlying class, rather than creating a decorator class? This makes sense if the new functionality is relatively general-purpose, or if it is logically related to the underlying class, or if most uses of the underlying class will also use the new functionality. For example, virtually everyone who creates a Java InputStream will also create a BufferedInputStream, and buffering is a natural part of I/O, so these classes should have been combined.
  • If the new functionality is specialized for a particular use case, would it make sense to merge it with the use case, rather than creating a separate class?
  • Could you merge the new functionality with an existing decorator, rather than creating a new decorator? This would result in a single deeper decorator class rather than multiple shallow ones.
  • Finally, ask yourself whether the new functionality really needs to wrap the existing functionality: could you implement it as a stand-alone class that is independent of the base class? In the windowing example, the scrollbars could probably be implemented separately from the main window, without wrapping all of its existing functionality.

  • 您能否将新功能直接添加到基础类,而不是创建装饰器类?如果新功能是相对通用的,或者在逻辑上与基础类相关,或者如果基础类的大多数使用也将使用新功能,则这是有意义的。例如,几乎每个创建 Java InputStream 的人都会创建一个 BufferedInputStream,并且缓冲是 I/O 的自然组成部分,因此应该合并这些类。
  • 如果新功能专用于特定用例,将其与用例合并而不是创建单独的类是否有意义?
  • 您可以将新功能与现有的装饰器合并,而不是创建新的装饰器吗?这将产生一个更深的装饰器类,而不是多个浅的装饰器类。
  • 最后,问问自己新功能是否真的需要包装现有功能:是否可以将其实现为独立于基类的独立类?在窗口示例中,滚动条可能与主窗口分开实现,而无需包装其所有现有功能。

Sometimes decorators make sense, but there is usually a better alternative.

有时装饰者很有意义,但通常有更好的选择。

7.4 Interface versus implementation 接口与实现

Another application of the “different layer, different abstraction” rule is that the interface of a class should normally be different from its implementation: the representations used internally should be different from the abstractions that appear in the interface. If the two have similar abstractions, then the class probably isn’t very deep. For example, in the text editor project discussed in Chapter 6, most of the teams implemented the text module in terms of lines of text, with each line stored separately. Some of the teams also designed the APIs for the text class around lines, with methods such as getLine and putLine. However, this made the text class shallow and awkward to use. In the higher-level user interface code, it’s common to insert text in the middle of a line (e.g., when the user is typing) or to delete a range of text that spans lines. With a line-oriented API for the text class, callers were forced to split and join lines to implement the user-interface operations. This code was nontrivial and it was duplicated and scattered across the implementation of the user interface.

“不同层,不同抽象”规则的另一个应用是,类的接口通常应与其实现不同:内部使用的表示形式应与接口中出现的抽象形式不同。如果两者具有相似的抽象,则该类可能不是很深。例如,在第 6 章讨论的文本编辑器项目中,大多数团队都以文本行的形式实现了文本模块,每行分别存储。一些团队还使用 getLine 和 putLine 之类的方法围绕行设计了文本类的 API。但是,这使文本类使用起来较浅且笨拙。在较高级别的用户界面代码中,通常在行中间插入文本(例如,当用户键入内容时)或删除跨行的文本范围。通过用于文本类的面向行的 API,调用者被迫拆分和合并行以实现用户界面操作。这段代码很简单,并且在用户界面的实现中被复制和散布。

The text classes were much easier to use when they provided a character-oriented interface, such as an insert method that inserts an arbitrary string of text (which may include newlines) at an arbitrary position in the text and a delete method that deletes the text between two arbitrary positions in the text. Internally, the text was still represented in terms of lines. A character-oriented interface encapsulates the complexity of line splitting and joining inside the text class, which makes the text class deeper and simplifies higher level code that uses the class. With this approach, the text API is quite different from the line-oriented storage mechanism; the difference represents valuable functionality provided by the class.

文本类提供面向字符的接口时,使用起来要容易得多,例如,insert 方法可在文本的任意位置插入任意文本字符串(可能包括换行符),而 delete 方法则删除文本在文本中的两个任意位置之间。在内部,文本仍以行表示。面向字符的接口封装了文本类内部的行拆分和连接的复杂性,这使文本类更深,并简化了使用该类的高级代码。通过这种方法,文本 API 与面向行的存储机制大不相同。差异表示该类提供的有价值的功能。

7.5 Pass-through variables 传递变量

Another form of API duplication across layers is a pass-through variable, which is a variable that is passed down through a long chain of methods. Figure 7.2(a) shows an example from a datacenter service. A command-line argument describes certificates to use for secure communication. This information is only needed by a low-level method m3, which calls a library method to open a socket, but it is passed down through all the methods on the path between main and m3. The cert variable appears in the signature of each of the intermediate methods.

跨层 API 重复的另一种形式是传递变量,该变量是通过一长串方法向下传递的变量。图 7.2(a)显示了数据中心服务的示例。命令行参数描述用于安全通信的证书。只有底层方法 m3 才需要此信息,该方法调用一个库方法来打开套接字,但是该信息会通过 main 和 m3 之间路径上的所有方法向下传递。cert 变量出现在每个中间方法的签名中。

Pass-through variables add complexity because they force all of the intermediate methods to be aware of their existence, even though the methods have no use for the variables. Furthermore, if a new variable comes into existence (for example, a system is initially built without support for certificates, but you later decide to add that support), you may have to modify a large number of interfaces and methods to pass the variable through all of the relevant paths.

传递变量增加了复杂性,因为它们强制所有中间方法知道它们的存在,即使这些方法对变量没有用处。此外,如果存在一个新变量(例如,最初构建的系统不支持证书,但是您后来决定添加该支持),则可能必须修改大量的接口和方法才能将变量传递给所有相关路径。

Eliminating pass-through variables can be challenging. One approach is to see if there is already an object shared between the topmost and bottommost methods. In the datacenter service example of Figure 7.2, perhaps there is an object containing other information about network communication, which is available to both main and m3. If so, main can store the certificate information in that object, so it needn’t be passed through all of the intervening methods on the path to m3 (see Figure 7.2(b)). However, if there is such an object, then it may itself be a pass-through variable (how else does m3 get access to it?).

消除传递变量可能具有挑战性。一种方法是查看最顶层和最底层方法之间是否已共享对象。在图 7.2 的数据中心服务示例中,也许存在一个对象,其中包含有关网络通信的其他信息,这对于 main 和 m3 都是可用的。如果是这样,main 可以将证书信息存储在该对象中,因此不必通过通往 m3 的路径上的所有干预方法来传递证书(请参见图 7.2(b))。但是,如果存在这样的对象,则它本身可能是传递变量(m3 还将如何访问它?)。

Another approach is to store the information in a global variable, as in Figure 7.2(c). This avoids the need to pass the information from method to method, but global variables almost always create other problems. For example, global variables make it impossible to create two independent instances of the same system in the same process, since accesses to the global variables will conflict. It may seem unlikely that you would need multiple instances in production, but they are often useful in testing.

另一种方法是将信息存储在全局变量中,如图 7.2(c)所示。这避免了将信息从一个方法传递到另一个方法的需要,但是全局变量几乎总是会产生其他问题。例如,全局变量使得不可能在同一过程中创建同一系统的两个独立实例,因为对全局变量的访问会发生冲突。在生产中似乎不太可能需要多个实例,但是它们通常在测试中很有用。

The solution I use most often is to introduce a context object as in Figure 7.2(d). A context stores all of the application’s global state (anything that would otherwise be a pass-through variable or global variable). Most applications have multiple variables in their global state, representing things such as configuration options, shared subsystems, and performance counters. There is one context object per instance of the system. The context allows multiple instances of the system to coexist in a single process, each with its own context.

我最常使用的解决方案是引入一个上下文对象,如图 7.2(d)所示。上下文存储应用程序的所有全局状态(否则将是传递变量或全局变量的任何状态)。大多数应用程序在其全局状态下具有多个变量,这些变量表示诸如配置选项,共享子系统和性能计数器之类的内容。每个系统实例只有一个上下文对象。上下文允许系统的多个实例在单个进程中共存,每个实例都有自己的上下文。

Unfortunately, the context will probably be needed in many places, so it can potentially become a pass-through variable. To reduce the number of methods that must be aware of it, a reference to the context can be saved in most of the system’s major objects. In the example of Figure 7.2(d), the class containing m3 stores a reference to the context as an instance variable in its objects. When a new object is created, the creating method retrieves the context reference from its object and passes it to the constructor for the new object. With this approach, the context is available everywhere, but it only appears as an explicit argument in constructors.

不幸的是,在许多地方可能都需要上下文,因此它有可能成为传递变量。为了减少必须意识到的方法数量,可以将上下文的引用保存在系统的大多数主要对象中。在图 7.2(d)的示例中,包含 m3 的类将对上下文的引用作为实例变量存储在其对象中。创建新对象时,创建方法将从其对象中检索上下文引用,并将其传递给新对象的构造函数。使用这种方法,上下文随处可见,但在构造函数中仅作为显式参数出现。

Figure 7.2: Possible techniques for dealing with a pass-through variable. In (a), cert is passed through methods m1 and m2 even though they don’t use it. In (b), main and m3 have shared access to an object, so the variable can be stored there instead of passing it through m1 and m2. In (c), cert is stored as a global variable. In (d), cert is stored in a context object along with other system-wide information, such as a timeout value and performance counters; a reference to the context is stored in all objects whose methods need access to it.

图 7.2:处理传递变量的可能技术。在(a)中,证书通过方法 m1 和 m2 传递,即使它们不使用它也是如此。在(b)中,main 和 m3 具有对一个对象的共享访问权,因此可以将变量存储在此处,而不用将其传递给 m1 和 m2。在(c)中,cert 存储为全局变量。在(d)中,证书与其他系统范围的信息(例如超时值和性能计数器)一起存储在上下文对象中;对上下文的引用存储在其方法需要访问它的所有对象中。

The context object unifies the handling of all system-global information and eliminates the need for pass-through variables. If a new variable needs to be added, it can be added to the context object; no existing code is affected except for the constructor and destructor for the context. The context makes it easy to identify and manage the global state of the system, since it is all stored in one place. The context is also convenient for testing: test code can change the global configuration of the application by modifying fields in the context. It would be much more difficult to implement such changes if the system used pass-through variables.

上下文对象统一了所有系统全局信息的处理,并且不需要传递变量。如果需要添加新变量,则可以将其添加到上下文对象;除了上下文的构造函数和析构函数外,现有代码均不受影响。由于上下文全部存储在一个位置,因此上下文可以轻松识别和管理系统的全局状态。上下文也便于测试:测试代码可以通过修改上下文中的字段来更改应用程序的全局配置。如果系统使用传递变量,则实施此类更改将更加困难。

Contexts are far from an ideal solution. The variables stored in a context have most of the disadvantages of global variables; for example, it may not be obvious why a particular variable is present, or where it is used. Without discipline, a context can turn into a huge grab-bag of data that creates nonobvious dependencies throughout the system. Contexts may also create thread-safety issues; the best way to avoid problems is for variables in a context to be immutable. Unfortunately, I haven’t found a better solution than contexts.

上下文远非理想的解决方案。存储在上下文中的变量具有全局变量的大多数缺点。例如,为什么存在特定变量或在何处使用特定变量可能并不明显。没有纪律,上下文会变成巨大的数据抓包,从而在整个系统中创建不明显的依赖关系。上下文也可能产生线程安全问题;避免问题的最佳方法是使上下文中的变量不可变。不幸的是,我没有找到比上下文更好的解决方案。

7.6 Conclusion 结论

Each piece of design infrastructure added to a system, such as an interface, argument, function, class, or definition, adds complexity, since developers must learn about this element. In order for an element to provide a net gain against complexity, it must eliminate some complexity that would be present in the absence of the design element. Otherwise, you are better off implementing the system without that particular element. For example, a class can reduce complexity by encapsulating functionality so that users of the class needn’t be aware of it.

添加到系统中的每一个设计基础设施,如接口、参数、函数、类或定义,都会增加复杂性,因为开发人员必须了解这个元素。为了使一个元素提供对抗复杂性的净增益,它必须消除在没有设计元素的情况下出现的一些复杂性。否则,您最好在没有该特定元素的情况下实现该系统。例如,一个类可以通过封装功能来降低复杂性,这样该类的用户就不必知道它了。

The “different layer, different abstraction” rule is just an application of this idea: if different layers have the same abstraction, such as pass-through methods or decorators, then there’s a good chance that they haven’t provided enough benefit to compensate for the additional infrastructure they represent. Similarly, pass-through arguments require each of several methods to be aware of their existence (which adds to complexity) without contributing additional functionality.

“不同的层,不同的抽象”规则只是此思想的一种应用:如果不同的层具有相同的抽象,例如透传方法或装饰器,则很有可能它们没有提供足够的利益来补偿它们代表的其他基础结构。类似地,传递参数要求几种方法中的每一种都知道它们的存在(这增加了复杂性),而又不提供其他功能。

',80)]))}const p=t(l,[["render",r],["__file","ch07.html.vue"]]),u=JSON.parse('{"path":"/ch07.html","title":"第 7 章 不同的层,不同的抽象","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"7.1 Pass-through methods 透传方法","slug":"_7-1-pass-through-methods-透传方法","link":"#_7-1-pass-through-methods-透传方法","children":[]},{"level":2,"title":"7.2 When is interface duplication OK? 什么时候可以有重复的接口?","slug":"_7-2-when-is-interface-duplication-ok-什么时候可以有重复的接口","link":"#_7-2-when-is-interface-duplication-ok-什么时候可以有重复的接口","children":[]},{"level":2,"title":"7.3 Decorators 装饰器","slug":"_7-3-decorators-装饰器","link":"#_7-3-decorators-装饰器","children":[]},{"level":2,"title":"7.4 Interface versus implementation 接口与实现","slug":"_7-4-interface-versus-implementation-接口与实现","link":"#_7-4-interface-versus-implementation-接口与实现","children":[]},{"level":2,"title":"7.5 Pass-through variables 传递变量","slug":"_7-5-pass-through-variables-传递变量","link":"#_7-5-pass-through-variables-传递变量","children":[]},{"level":2,"title":"7.6 Conclusion 结论","slug":"_7-6-conclusion-结论","link":"#_7-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch07.md"}');export{p as comp,u as data}; diff --git a/assets/ch08.html-BUqMm1Bk.js b/assets/ch08.html-CemH-4b2.js similarity index 99% rename from assets/ch08.html-BUqMm1Bk.js rename to assets/ch08.html-CemH-4b2.js index 06969444..714138de 100644 --- a/assets/ch08.html-BUqMm1Bk.js +++ b/assets/ch08.html-CemH-4b2.js @@ -1 +1 @@ -import{_ as t,c as o,f as a,o as i}from"./app-CvqtBB8Z.js";const s={};function r(n,e){return i(),o("div",null,e[0]||(e[0]=[a('

第 8 章 降低复杂性

Chapter 8 Pull Complexity Downwards

This chapter introduces another way of thinking about how to create deeper classes. Suppose that you are developing a new module, and you discover a piece of unavoidable complexity. Which is better: should you let users of the module deal with the complexity, or should you handle the complexity internally within the module? If the complexity is related to the functionality provided by the module, then the second answer is usually the right one. Most modules have more users than developers, so it is better for the developers to suffer than the users. As a module developer, you should strive to make life as easy as possible for the users of your module, even if that means extra work for you. Another way of expressing this idea is that it is more important for a module to have a simple interface than a simple implementation.

本章介绍了有关如何创建更深层类的另一种思考方式。假设您正在开发一个新模块,并且发现了一个不可避免的复杂性。哪个更好:应该让模块的使用者处理复杂性,还是应该在模块内部处理复杂性?如果复杂度与模块提供的功能有关,则第二个答案通常是正确的答案。大多数模块拥有的使用者多于开发人员,因此麻烦开发人员比麻烦使用者更好。作为模块开发人员,您应该努力使模块使用者的生活尽可能轻松,即使这对您来说意味着额外的工作。表达此想法的另一种方法是,模块具有简单的接口比简单的实现更为重要。

As a developer, it’s tempting to behave in the opposite fashion: solve the easy problems and punt the hard ones to someone else. If a condition arises that you’re not certain how to deal with, the easiest thing is to throw an exception and let the caller handle it. If you are not certain what policy to implement, you can define a few configuration parameters to control the policy and leave it up to the system administrator to figure out the best values for them.

作为开发人员,很容易以相反的方式行事:解决简单的问题,然后将困难的问题推给其他人。如果出现不确定如何处理的条件,最简单的方法是抛出异常并让调用者处理它。如果不确定要实施什么策略,则可以定义一些配置参数来控制该策略,然后由系统管理员自行确定最佳策略。

Approaches like these will make your life easier in the short term, but they amplify complexity, so that many people must deal with a problem, rather than just one person. For example, if a class throws an exception, every caller of the class will have to deal with it. If a class exports configuration parameters, every system administrator in every installation will have to learn how to set them.

这样的方法短期内会使您的生活更轻松,但它们会加剧复杂性,因此许多人必须处理一个问题,而不仅仅是一个人。例如,如果一个类抛出异常,则该类的每个调用者都必须处理该异常。如果一个类导出配置参数,则每个系统管理员在每次安装中都必须学习如何设置它们。

8.1 Example: editor text class 示例:编辑器文本类

Consider the class that manages the text of a file for a GUI text editor, which was discussed in Chapters 6 and 7. The class provides methods to read a file from disk into memory, query and modify the in-memory copy of the file, and write the modified version back to disk. When students had to implement this class, many of them chose a line-oriented interface, with methods to read, insert, and delete whole lines of text. This resulted in a simple implementation for the class, but it created complexity for higher level software. At the level of the user interface, operations rarely involve whole lines. For example, keystrokes cause individual characters to be inserted within an existing line; copying or deleting the selection can modify parts of several different lines. With the line-oriented text interface, higher-level software had to split and join lines in order to implement the user interface.

考虑为 GUI 文本编辑器管理文件文本的类,这在第 6 章和第 7 章中讨论过。该类提供了将文件从磁盘读入内存、查询和修改文件在内存中的副本以及将修改后的版本写回磁盘的方法。当学生必须实现这个类时,他们中的许多人选择了一个面向行的接口,该接口具有读取、插入和删除整行文本的方法。这导致了类实现起来很简单,但也为更高级别的软件带来了复杂性。在用户界面级别,操作很少涉及整行操作。例如,击键会导致在现有行中插入单个字符;复制或删除选择项可以修改几个不同行的部分。使用面向行的文本接口,为了实现用户界面,高级软件必须分割和连接行。

A character-oriented interface such as the one described in Section 6.3 pulls complexity downward. The user interface software can now insert and delete arbitrary ranges of text without splitting and merging lines, so it becomes simpler. The implementation of the text class probably becomes more complex: if it represents the text internally as a collection of lines, it will have to split and merge lines to implement the character-oriented operations. This approach is better because it encapsulates the complexity of splitting and merging within the text class, which reduces the overall complexity of the system.

面向字符的界面(如 6.3 节中所述)降低了复杂性。用户界面软件现在可以插入和删除任意范围的文本,而无需分割和合并行,因此变得更加简单。文本类的实现可能会变得更加复杂:如果内部将文本表示为行的集合,则必须拆分和合并行以实现面向字符的操作。这种方法更好,因为它封装了在文本类中拆分和合并的复杂性,从而降低了系统的整体复杂性。

8.2 Example: configuration parameters 示例:配置参数

Configuration parameters are an example of moving complexity upwards instead of down. Rather than determining a particular behavior internally, a class can export a few parameters that control its behavior, such as the size of a cache or the number of times to retry a request before giving up. Users of the class must then specify appropriate values for the parameters. Configuration parameters have become very popular in systems today; some systems have hundreds of them.

配置参数是提高复杂度而不是降低复杂度的一个示例。类可以在内部输出一些控制其行为的参数,而不是在内部确定特定的行为,例如高速缓存的大小或在放弃之前重试请求的次数。然后,该类的使用者必须为参数指定适当的值。在当今的系统中,配置参数已变得非常流行。有些系统有数百个。

Advocates argue that configuration parameters are good because they allow users to tune the system for their particular requirements and workloads. In some situations it is hard for low-level infrastructure code to know the best policy to apply, whereas users are much more familiar with their domains. For instance, a user might know that some requests are more time-critical than others, so it makes sense for the user to specify a higher priority for those requests. In situations like this, configuration parameters can result in better performance across a broader variety of domains.

拥护者认为配置参数不错,因为它们允许用户根据他们的特定要求和工作负载来调整系统。在某些情况下,低级基础结构代码很难知道要应用的最佳策略,而用户则对其领域更加熟悉。例如,用户可能知道某些请求比其他请求更紧迫,因此用户为这些请求指定更高的优先级是有意义的。在这种情况下,配置参数可以在更广泛的领域中带来更好的性能。

However, configuration parameters also provide an easy excuse to avoid dealing with important issues and pass them on to someone else. In many cases, it’s difficult or impossible for users or administrators to determine the right values for the parameters. In other cases, the right values could have been determined automatically with a little extra work in the system implementation. Consider a network protocol that must deal with lost packets. If it sends a request but doesn’t receive a response within a certain time period, it resends the request. One way to determine the retry interval is to introduce a configuration parameter. However, the transport protocol could compute a reasonable value on its own by measuring the response time for requests that succeed and then using a multiple of this for the retry interval. This approach pulls complexity downward and saves users from having to figure out the right retry interval. It has the additional advantage of computing the retry interval dynamically, so it will adjust automatically if operating conditions change. In contrast, configuration parameters can easily become out of date.

但是,配置参数还提供了一个轻松的借口,可以避免处理重要问题并将其传递给其他人。在许多情况下,用户或管理员很难或无法确定参数的正确值。在其他情况下,可以通过在系统实现中进行一些额外的工作来自动确定正确的值。考虑必须处理丢失数据包的网络协议。如果它发送请求但在一定时间内未收到响应,则重新发送该请求。确定重试间隔的一种方法是引入配置参数。但是,传输协议可以通过测量成功请求的响应时间,然后将其倍数用于重试间隔,自己计算出一个合理的值。这种方法降低了复杂性,使用户不必找出正确的重试间隔。它具有动态计算重试间隔的其他优点,因此,如果操作条件发生变化,它将自动进行调整。相反,配置参数很容易过时。

Thus, you should avoid configuration parameters as much as possible. Before exporting a configuration parameter, ask yourself: “will users (or higher-level modules) be able to determine a better value than we can determine here?” When you do create configuration parameters, see if you can compute reasonable defaults automatically, so users will only need to provide values under exceptional conditions. Ideally, each module should solve a problem completely; configuration parameters result in an incomplete solution, which adds to system complexity.

因此,您应尽可能避免使用配置参数。在导出配置参数之前,请问自己:“用户(或更高级别的模块)是否能够确定比我们在此确定的更好的值?” 当您创建配置参数时,请查看是否可以自动计算合理的默认值,因此用户仅需在特殊情况下提供值即可。理想情况下,每个模块都应完全解决问题。配置参数导致解决方案不完整,从而增加了系统复杂性。

8.3 Taking it too far 走得太远

Use discretion when pulling complexity downward; this is an idea that can easily be overdone. An extreme approach would be to pull all of the functionality of the entire application down into a single class, which clearly doesn’t make sense. Pulling complexity down makes the most sense if (a) the complexity being pulled down is closely related to the class’s existing functionality, (b) pulling the complexity down will result in many simplifications elsewhere in the application, and (c) pulling the complexity down simplifies the class’s interface. Remember that the goal is to minimize overall system complexity.

降低复杂性时要谨慎处理;这个想法很容易做过头。一种极端的方法是将整个应用程序的所有功能归为一个类,这显然没有意义。如果(a)被降低的复杂度与该类的现有功能密切相关,(b)降低复杂度将导致应用程序中其他地方的许多简化,则降低复杂度最有意义。简化了类的接口。请记住,目标是最大程度地降低整体系统复杂性。

Chapter 6 described how some students defined methods in the text class that reflected the user interface, such as a method that implements the functionality of the backspace key. It might seem that this is good, since it pulls complexity downward. However, adding knowledge of the user interface to the text class doesn’t simplify higher-level code very much, and the user-interface knowledge doesn’t relate to the core functions of the text class. In this case, pulling complexity down just resulted in information leakage.

第 6 章介绍了一些学生如何在文本类中定义反映用户界面的方法,例如实现退格键功能的方法。这似乎很好,因为它可以降低复杂性。但是,将用户界面的知识添加到文本类中并不会大大简化高层代码,并且用户界面的知识与文本类的核心功能无关。在这种情况下,降低复杂度只会导致信息泄漏。

8.4 Conclusion 结论

When developing a module, look for opportunities to take a little bit of extra suffering upon yourself in order to reduce the suffering of your users.

在开发模块时,为了减少用户的痛苦,要找机会给自己多吃一点苦。

',30)]))}const h=t(s,[["render",r],["__file","ch08.html.vue"]]),c=JSON.parse('{"path":"/ch08.html","title":"第 8 章 降低复杂性","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"8.1 Example: editor text class 示例:编辑器文本类","slug":"_8-1-example-editor-text-class-示例-编辑器文本类","link":"#_8-1-example-editor-text-class-示例-编辑器文本类","children":[]},{"level":2,"title":"8.2 Example: configuration parameters 示例:配置参数","slug":"_8-2-example-configuration-parameters-示例-配置参数","link":"#_8-2-example-configuration-parameters-示例-配置参数","children":[]},{"level":2,"title":"8.3 Taking it too far 走得太远","slug":"_8-3-taking-it-too-far-走得太远","link":"#_8-3-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"8.4 Conclusion 结论","slug":"_8-4-conclusion-结论","link":"#_8-4-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch08.md"}');export{h as comp,c as data}; +import{_ as t,c as o,f as a,o as i}from"./app-BQdhfat9.js";const s={};function r(n,e){return i(),o("div",null,e[0]||(e[0]=[a('

第 8 章 降低复杂性

Chapter 8 Pull Complexity Downwards

This chapter introduces another way of thinking about how to create deeper classes. Suppose that you are developing a new module, and you discover a piece of unavoidable complexity. Which is better: should you let users of the module deal with the complexity, or should you handle the complexity internally within the module? If the complexity is related to the functionality provided by the module, then the second answer is usually the right one. Most modules have more users than developers, so it is better for the developers to suffer than the users. As a module developer, you should strive to make life as easy as possible for the users of your module, even if that means extra work for you. Another way of expressing this idea is that it is more important for a module to have a simple interface than a simple implementation.

本章介绍了有关如何创建更深层类的另一种思考方式。假设您正在开发一个新模块,并且发现了一个不可避免的复杂性。哪个更好:应该让模块的使用者处理复杂性,还是应该在模块内部处理复杂性?如果复杂度与模块提供的功能有关,则第二个答案通常是正确的答案。大多数模块拥有的使用者多于开发人员,因此麻烦开发人员比麻烦使用者更好。作为模块开发人员,您应该努力使模块使用者的生活尽可能轻松,即使这对您来说意味着额外的工作。表达此想法的另一种方法是,模块具有简单的接口比简单的实现更为重要。

As a developer, it’s tempting to behave in the opposite fashion: solve the easy problems and punt the hard ones to someone else. If a condition arises that you’re not certain how to deal with, the easiest thing is to throw an exception and let the caller handle it. If you are not certain what policy to implement, you can define a few configuration parameters to control the policy and leave it up to the system administrator to figure out the best values for them.

作为开发人员,很容易以相反的方式行事:解决简单的问题,然后将困难的问题推给其他人。如果出现不确定如何处理的条件,最简单的方法是抛出异常并让调用者处理它。如果不确定要实施什么策略,则可以定义一些配置参数来控制该策略,然后由系统管理员自行确定最佳策略。

Approaches like these will make your life easier in the short term, but they amplify complexity, so that many people must deal with a problem, rather than just one person. For example, if a class throws an exception, every caller of the class will have to deal with it. If a class exports configuration parameters, every system administrator in every installation will have to learn how to set them.

这样的方法短期内会使您的生活更轻松,但它们会加剧复杂性,因此许多人必须处理一个问题,而不仅仅是一个人。例如,如果一个类抛出异常,则该类的每个调用者都必须处理该异常。如果一个类导出配置参数,则每个系统管理员在每次安装中都必须学习如何设置它们。

8.1 Example: editor text class 示例:编辑器文本类

Consider the class that manages the text of a file for a GUI text editor, which was discussed in Chapters 6 and 7. The class provides methods to read a file from disk into memory, query and modify the in-memory copy of the file, and write the modified version back to disk. When students had to implement this class, many of them chose a line-oriented interface, with methods to read, insert, and delete whole lines of text. This resulted in a simple implementation for the class, but it created complexity for higher level software. At the level of the user interface, operations rarely involve whole lines. For example, keystrokes cause individual characters to be inserted within an existing line; copying or deleting the selection can modify parts of several different lines. With the line-oriented text interface, higher-level software had to split and join lines in order to implement the user interface.

考虑为 GUI 文本编辑器管理文件文本的类,这在第 6 章和第 7 章中讨论过。该类提供了将文件从磁盘读入内存、查询和修改文件在内存中的副本以及将修改后的版本写回磁盘的方法。当学生必须实现这个类时,他们中的许多人选择了一个面向行的接口,该接口具有读取、插入和删除整行文本的方法。这导致了类实现起来很简单,但也为更高级别的软件带来了复杂性。在用户界面级别,操作很少涉及整行操作。例如,击键会导致在现有行中插入单个字符;复制或删除选择项可以修改几个不同行的部分。使用面向行的文本接口,为了实现用户界面,高级软件必须分割和连接行。

A character-oriented interface such as the one described in Section 6.3 pulls complexity downward. The user interface software can now insert and delete arbitrary ranges of text without splitting and merging lines, so it becomes simpler. The implementation of the text class probably becomes more complex: if it represents the text internally as a collection of lines, it will have to split and merge lines to implement the character-oriented operations. This approach is better because it encapsulates the complexity of splitting and merging within the text class, which reduces the overall complexity of the system.

面向字符的界面(如 6.3 节中所述)降低了复杂性。用户界面软件现在可以插入和删除任意范围的文本,而无需分割和合并行,因此变得更加简单。文本类的实现可能会变得更加复杂:如果内部将文本表示为行的集合,则必须拆分和合并行以实现面向字符的操作。这种方法更好,因为它封装了在文本类中拆分和合并的复杂性,从而降低了系统的整体复杂性。

8.2 Example: configuration parameters 示例:配置参数

Configuration parameters are an example of moving complexity upwards instead of down. Rather than determining a particular behavior internally, a class can export a few parameters that control its behavior, such as the size of a cache or the number of times to retry a request before giving up. Users of the class must then specify appropriate values for the parameters. Configuration parameters have become very popular in systems today; some systems have hundreds of them.

配置参数是提高复杂度而不是降低复杂度的一个示例。类可以在内部输出一些控制其行为的参数,而不是在内部确定特定的行为,例如高速缓存的大小或在放弃之前重试请求的次数。然后,该类的使用者必须为参数指定适当的值。在当今的系统中,配置参数已变得非常流行。有些系统有数百个。

Advocates argue that configuration parameters are good because they allow users to tune the system for their particular requirements and workloads. In some situations it is hard for low-level infrastructure code to know the best policy to apply, whereas users are much more familiar with their domains. For instance, a user might know that some requests are more time-critical than others, so it makes sense for the user to specify a higher priority for those requests. In situations like this, configuration parameters can result in better performance across a broader variety of domains.

拥护者认为配置参数不错,因为它们允许用户根据他们的特定要求和工作负载来调整系统。在某些情况下,低级基础结构代码很难知道要应用的最佳策略,而用户则对其领域更加熟悉。例如,用户可能知道某些请求比其他请求更紧迫,因此用户为这些请求指定更高的优先级是有意义的。在这种情况下,配置参数可以在更广泛的领域中带来更好的性能。

However, configuration parameters also provide an easy excuse to avoid dealing with important issues and pass them on to someone else. In many cases, it’s difficult or impossible for users or administrators to determine the right values for the parameters. In other cases, the right values could have been determined automatically with a little extra work in the system implementation. Consider a network protocol that must deal with lost packets. If it sends a request but doesn’t receive a response within a certain time period, it resends the request. One way to determine the retry interval is to introduce a configuration parameter. However, the transport protocol could compute a reasonable value on its own by measuring the response time for requests that succeed and then using a multiple of this for the retry interval. This approach pulls complexity downward and saves users from having to figure out the right retry interval. It has the additional advantage of computing the retry interval dynamically, so it will adjust automatically if operating conditions change. In contrast, configuration parameters can easily become out of date.

但是,配置参数还提供了一个轻松的借口,可以避免处理重要问题并将其传递给其他人。在许多情况下,用户或管理员很难或无法确定参数的正确值。在其他情况下,可以通过在系统实现中进行一些额外的工作来自动确定正确的值。考虑必须处理丢失数据包的网络协议。如果它发送请求但在一定时间内未收到响应,则重新发送该请求。确定重试间隔的一种方法是引入配置参数。但是,传输协议可以通过测量成功请求的响应时间,然后将其倍数用于重试间隔,自己计算出一个合理的值。这种方法降低了复杂性,使用户不必找出正确的重试间隔。它具有动态计算重试间隔的其他优点,因此,如果操作条件发生变化,它将自动进行调整。相反,配置参数很容易过时。

Thus, you should avoid configuration parameters as much as possible. Before exporting a configuration parameter, ask yourself: “will users (or higher-level modules) be able to determine a better value than we can determine here?” When you do create configuration parameters, see if you can compute reasonable defaults automatically, so users will only need to provide values under exceptional conditions. Ideally, each module should solve a problem completely; configuration parameters result in an incomplete solution, which adds to system complexity.

因此,您应尽可能避免使用配置参数。在导出配置参数之前,请问自己:“用户(或更高级别的模块)是否能够确定比我们在此确定的更好的值?” 当您创建配置参数时,请查看是否可以自动计算合理的默认值,因此用户仅需在特殊情况下提供值即可。理想情况下,每个模块都应完全解决问题。配置参数导致解决方案不完整,从而增加了系统复杂性。

8.3 Taking it too far 走得太远

Use discretion when pulling complexity downward; this is an idea that can easily be overdone. An extreme approach would be to pull all of the functionality of the entire application down into a single class, which clearly doesn’t make sense. Pulling complexity down makes the most sense if (a) the complexity being pulled down is closely related to the class’s existing functionality, (b) pulling the complexity down will result in many simplifications elsewhere in the application, and (c) pulling the complexity down simplifies the class’s interface. Remember that the goal is to minimize overall system complexity.

降低复杂性时要谨慎处理;这个想法很容易做过头。一种极端的方法是将整个应用程序的所有功能归为一个类,这显然没有意义。如果(a)被降低的复杂度与该类的现有功能密切相关,(b)降低复杂度将导致应用程序中其他地方的许多简化,则降低复杂度最有意义。简化了类的接口。请记住,目标是最大程度地降低整体系统复杂性。

Chapter 6 described how some students defined methods in the text class that reflected the user interface, such as a method that implements the functionality of the backspace key. It might seem that this is good, since it pulls complexity downward. However, adding knowledge of the user interface to the text class doesn’t simplify higher-level code very much, and the user-interface knowledge doesn’t relate to the core functions of the text class. In this case, pulling complexity down just resulted in information leakage.

第 6 章介绍了一些学生如何在文本类中定义反映用户界面的方法,例如实现退格键功能的方法。这似乎很好,因为它可以降低复杂性。但是,将用户界面的知识添加到文本类中并不会大大简化高层代码,并且用户界面的知识与文本类的核心功能无关。在这种情况下,降低复杂度只会导致信息泄漏。

8.4 Conclusion 结论

When developing a module, look for opportunities to take a little bit of extra suffering upon yourself in order to reduce the suffering of your users.

在开发模块时,为了减少用户的痛苦,要找机会给自己多吃一点苦。

',30)]))}const h=t(s,[["render",r],["__file","ch08.html.vue"]]),c=JSON.parse('{"path":"/ch08.html","title":"第 8 章 降低复杂性","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"8.1 Example: editor text class 示例:编辑器文本类","slug":"_8-1-example-editor-text-class-示例-编辑器文本类","link":"#_8-1-example-editor-text-class-示例-编辑器文本类","children":[]},{"level":2,"title":"8.2 Example: configuration parameters 示例:配置参数","slug":"_8-2-example-configuration-parameters-示例-配置参数","link":"#_8-2-example-configuration-parameters-示例-配置参数","children":[]},{"level":2,"title":"8.3 Taking it too far 走得太远","slug":"_8-3-taking-it-too-far-走得太远","link":"#_8-3-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"8.4 Conclusion 结论","slug":"_8-4-conclusion-结论","link":"#_8-4-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch08.md"}');export{h as comp,c as data}; diff --git a/assets/ch09.html-CdBSB_a2.js b/assets/ch09.html-DbK-CbmQ.js similarity index 99% rename from assets/ch09.html-CdBSB_a2.js rename to assets/ch09.html-DbK-CbmQ.js index f126fea0..01cc4ba6 100644 --- a/assets/ch09.html-CdBSB_a2.js +++ b/assets/ch09.html-DbK-CbmQ.js @@ -1,4 +1,4 @@ -import{_ as t,c as n,f as s,o}from"./app-CvqtBB8Z.js";const a="/A-Philosophy-of-Software-Design-zh/assets/00017-DPuvif3w.gif",i="/A-Philosophy-of-Software-Design-zh/assets/00018-DrRJ431J.gif",r="/A-Philosophy-of-Software-Design-zh/assets/00019-BXkohvDw.jpeg",l={};function c(p,e){return o(),n("div",null,e[0]||(e[0]=[s('

第 9 章 在一起更好还是分开更好?

Chapter 9 Better Together Or Better Apart?

One of the most fundamental questions in software design is this: given two pieces of functionality, should they be implemented together in the same place, or should their implementations be separated? This question applies at all levels in a system, such as functions, methods, classes, and services. For example, should buffering be included in the class that provides stream-oriented file I/O, or should it be in a separate class? Should the parsing of an HTTP request be implemented entirely in one method, or should it be divided among multiple methods (or even multiple classes)? This chapter discusses the factors to consider when making these decisions. Some of these factors have already been discussed in previous chapters, but they will be revisited here for completeness.

软件设计中最基本的问题之一是:给定两个功能,它们应该在同一位置一起实现,还是应该分开实现?这个问题适用于系统中的所有级别,例如功能,方法,类和服务。例如,应该在提供面向流的文件 I/O 的类中包括缓冲,还是应该在单独的类中?HTTP 请求的解析应该完全在一个方法中实现,还是应该在多个方法(甚至多个类)之间划分?本章讨论做出这些决定时要考虑的因素。这些因素中的一些已经在前面的章节中进行了讨论,但是为了完整起见,这里将对其进行重新讨论。

When deciding whether to combine or separate, the goal is to reduce the complexity of the system as a whole and improve its modularity. It might appear that the best way to achieve this goal is to divide the system into a large number of small components: the smaller the components, the simpler each individual component is likely to be. However, the act of subdividing creates additional complexity that was not present before subdivision:

在决定是合并还是分开时,目标是降低整个系统的复杂性并改善其模块化。看来实现此目标的最佳方法是将系统划分为大量的小组件:组件越小,每个单独的组件可能越简单。但是,细分的行为会带来额外的复杂性,而这在细分之前是不存在的:

  • Some complexity comes just from the number of components: the more components, the harder to keep track of them all and the harder to find a desired component within the large collection. Subdivision usually results in more interfaces, and every new interface adds complexity.
  • Subdivision can result in additional code to manage the components. For example, a piece of code that used a single object before subdivision might now have to manage multiple objects.
  • Subdivision creates separation: the subdivided components will be farther apart than they were before subdivision. For example, methods that were together in a single class before subdivision may be in different classes after subdivision, and possibly in different files. Separation makes it harder for developers to see the components at the same time, or even to be aware of their existence. If the components are truly independent, then separation is good: it allows the developer to focus on a single component at a time, without being distracted by the other components. On the other hand, if there are dependencies between the components, then separation is bad: developers will end up flipping back and forth between the components. Even worse, they may not be aware of the dependencies, which can lead to bugs.
  • Subdivision can result in duplication: code that was present in a single instance before subdivision may need to be present in each of the subdivided components.

  • 一些组件的复杂性仅来自组件的数量:组件越多,就越难以追踪所有组件,也就越难在大型集合中找到所需的组件。细分通常会导致更多接口,并且每个新接口都会增加复杂性。
  • 细分可能会导致附加代码来管理组件。例如,在细分之前使用单个对象的一段代码现在可能必须管理多个对象。
  • 细分产生分离:细分后的组件将比细分前的组件相距更远。例如,在细分之前位于单个类中的方法可能在细分之后位于不同的类中,并且可能在不同的文件中。分离使开发人员更难于同时查看这些组件,甚至很难知道它们的存在。如果组件真正独立,那么分离是好的:它使开发人员可以一次专注于单个组件,而不会被其他组件分散注意力。另一方面,如果组件之间存在依赖性,则分离是不好的:开发人员最终将在组件之间来回翻转。更糟糕的是,他们可能不了解依赖关系,这可能导致错误。
  • 细分可能导致重复:细分之前的单个实例中存在的代码可能需要存在于每个细分的组件中。

Bringing pieces of code together is most beneficial if they are closely related. If the pieces are unrelated, they are probably better off apart. Here are a few indications that two pieces of code are related:

如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开。以下是两个代码相关的一些提示:

  • They share information; for example, both pieces of code might depend on the syntax of a particular type of document.
  • They are used together: anyone using one of the pieces of code is likely to use the other as well. This form of relationship is only compelling if it is bidirectional. As a counter-example, a disk block cache will almost always involve a hash table, but hash tables can be used in many situations that don’t involve block caches; thus, these modules should be separate.
  • They overlap conceptually, in that there is a simple higher-level category that includes both of the pieces of code. For example, searching for a substring and case conversion both fall under the category of string manipulation; flow control and reliable delivery both fall under the category of network communication.
  • It is hard to understand one of the pieces of code without looking at the other.

  • 他们共享信息;例如,这两段代码都可能取决于特定类型文档的语法。
  • 它们一起使用:任何使用其中一段代码的人都可能同时使用另一段代码。这种关系形式只有在双向关系中才具有吸引力。作为反例,磁盘块高速缓存几乎总是包含哈希表,但是哈希表可以在许多不涉及块高速缓存的情况下使用。因此,这些模块应该分开。
  • 它们在概念上重叠,因为存在一个简单的更高级别的类别,其中包括这两段代码。例如,搜索子字符串和大小写转换都属于字符串操作类别。流控制和可靠的交付都属于网络通信的范畴。
  • 不看其中的一段代码就很难理解。

The rest of this chapter uses more specific rules as well as examples to show when it makes sense to bring pieces of code together and when it makes sense to separate them.

本章的其余部分使用更具体的规则以及示例来说明何时将代码段组合在一起以及何时将它们分开是有意义的。

9.1 Bring together if information is shared 如果信息共享则汇聚在一起

Section 5.4 introduced this principle in the context of a project implementing an HTTP server. In its first implementation, the project used two different methods in different classes to read in and parse HTTP requests. The first method read the text of an incoming request from a network socket and placed it in a string object. The second method parsed the string to extract the various components of the request. With this decomposition, both of the methods ended up with considerable knowledge of the format of HTTP requests: the first method was only trying to read the request, not parse it, but it couldn’t identify the end of the request without doing most of the work of parsing it (for example, it had to parse header lines in order to identify the header containing the overall request length). Because of this shared information, it is better to both read and parse the request in the same place; when the two classes were combined into one, the code got shorter and simpler.

5.4 节在实现 HTTP 服务器的项目上下文中介绍了此原则。在其第一个实现中,该项目使用了两个位于不同类的方法来读取和解析 HTTP 请求。第一个方法从网络套接字读取传入请求的文本,并将其放置在字符串对象中。第二个方法解析字符串以提取请求的各个组成部分。经过这种分解,这两种方法最终都对 HTTP 请求的格式有了相当的了解:第一个方法只是尝试读取请求,而不是解析请求,但是如果不执行大多数操作,就无法确定请求的结束解析它的工作(例如,它必须解析标头行才能识别包含整个请求长度的标头)。由于此共享信息,最好在同一位置读取和解析请求;当两个类合而为一时,代码变得更短,更简单。

9.2 Bring together if it will simplify the interface 如果可以简化接口则汇集在一起

When two or more modules are combined into a single module, it may be possible to define an interface for the new module that is simpler or easier to use than the original interfaces. This often happens when the original modules each implement part of the solution to a problem. In the HTTP server example from the preceding section, the original methods required an interface to return the HTTP request string from the first method and pass it to the second. When the methods were combined, these interfaces were eliminated.

当两个或多个模块组合成一个模块时,可以为新模块定义一个比原始接口更简单或更易于使用的接口。当原始模块各自实现问题解决方案的一部分时,通常会发生这种情况。在上一部分的 HTTP 服务器示例中,原始方法需要一个接口来从第一个方法返回 HTTP 请求字符串并将其传递给第二个方法。当这些方法结合在一起时,这些接口就被淘汰了。

In addition, when the functionality of two or more classes is combined, it may be possible to perform some functions automatically, so that most users need not be aware of them. The Java I/O library illustrates this opportunity. If the FileInputStream and BufferedInputStream classes were combined and buffering were provided by default, the vast majority of users would never even need to be aware of the existence of buffering. A combined FileInputStream class might provide methods to disable or replace the default buffering mechanism, but most users would not need to learn about them.

另外,将两个或更多类的功能组合在一起时,可能会自动执行某些功能,因此大多数用户无需了解它们。Java I/O 库说明了这种机会。如果将 FileInputStream 和 BufferedInputStream 类组合在一起,并且默认情况下提供了缓冲,则绝大多数用户甚至都不需要知道缓冲的存在。组合后的 FileInputStream 类可能提供禁用或替换默认缓冲机制的方法,但是大多数用户不需要了解它们。

9.3 Bring together to eliminate duplication 消除重复

If you find the same pattern of code repeated over and over, see if you can reorganize the code to eliminate the repetition. One approach is to factor the repeated code out into a separate method and replace the repeated code snippets with calls to the method. This approach is most effective if the repeated code snippet is long and the replacement method has a simple signature. If the snippet is only one or two lines long, there may not be much benefit in replacing it with a method call. If the snippet interacts in complex ways with its environment (such as by accessing numerous local variables), then the replacement method might require a complex signature (such as many pass-by-reference arguments), which would reduce its value.

如果发现反复重复的代码模式,请查看是否可以重新组织代码以消除重复。一种方法是将重复的代码分解为一个单独的方法,并用对该方法的调用替换重复的代码段。如果重复的代码段很长并且用来替换方法具有简单的签名,则此方法最有效。如果代码段只有一两行,那么用方法调用替换它可能不会有太多好处。如果代码段与其环境以复杂的方式进行交互(例如,通过访问多个局部变量),则替换方法可能需要复杂的签名(例如,许多“按引用传递”参数),这会降低其价值。

Another way to eliminate duplication is to refactor the code so that the snippet in question only needs to be executed in one place. Suppose you are writing a method that needs to return errors at several different points, and the same cleanup actions need to be performed at each of these points before returning (see Figure 9.1 for an example). If the programming language supports goto, you can move the cleanup code to the very end of the method and then goto that snippet at each of the points where an error return is required, as in Figure 9.2. Goto statements are generally considered a bad idea, and they can result in indecipherable code if used indiscriminately, but they are useful in situations like this where they are used to escape from nested code.

消除重复的另一种方法是重构代码,使相关代码段仅需要在一个地方执行。假设您正在编写一种方法,该方法需要在几个不同的点返回错误,并且在返回之前需要在每个这些点执行相同的清除操作(示例请参见图 9.1)。如果编程语言支持 goto,则可以将清除代码移到方法的最后,然后在需要返回错误的每个点处转到该片段,如图 9.2 所示。Goto 语句通常被认为是一个坏主意,如果不加选择地使用它们,可能会导致无法识别的代码,但是在诸如此类的情况下,它们可用于从嵌套代码中脱离,因此它们非常有用。

9.4 Separate general-purpose and special-purpose code 单独的通用代码和专用代码

If a module contains a mechanism that can be used for several different purposes, then it should provide just that one general-purpose mechanism. It should not include code that specializes the mechanism for a particular use, nor should it contain other general-purpose mechanisms. Special-purpose code associated with a general-purpose mechanism should normally go in a different module (typically one associated with the particular purpose). The GUI editor discussion in Chapter 6 illustrated this principle: the best design was one where the text class provided general-purpose text operations, while operations particular to the user interface (such as deleting the selection) were implemented in the user interface module. This approach eliminated information leakage and additional interfaces that were present in an earlier design where the specialized user interface operations were implemented in the text class.

如果模块包含可用于多种不同目的的机制,则它应仅提供一种通用机制。它不应包含专门针对特定用途的机制的代码,也不应包含其他通用机制。与通用机制关联的专用代码通常应放在不同的模块中(通常是与特定用途关联的模块)。第 6 章中的 GUI 编辑器讨论阐明了这一原则:最佳设计是文本类提供通用文本操作,而特定于用户界面的操作(例如删除所选内容)则在用户界面模块中实现。这种方法消除了早期设计中存在的信息泄漏和附加接口,在早期设计中,专门的用户界面操作是在文本类中实现的。

img Red Flag: Repetition img

If the same piece of code (or code that is almost the same) appears over and over again, that’s a red flag that you haven’t found the right abstractions.

如果相同的代码(或几乎相同的代码)一遍又一遍地出现,那是一个危险信号,您没有找到正确的抽象。

Figure 9.1: This code processes incoming network packets of different types; for each type, if the packet is too short for that type, a message gets logged. In this version of the code, the LOG statement is duplicated for several different packet types.

图 9.1:此代码处理不同类型的传入网络数据包。对于每种类型,如果数据包对于该类型而言太短,则会记录一条消息。在此版本的代码中,LOG 语句对于几种不同的数据包类型是重复的。

Figure 9.2: A reorganization of the code from Figure 9.1 so that there is only one copy of the LOG statement.

图 9.2:对图 9.1 中的代码进行了重新组织,因此只有 LOG 语句的一个副本。

In general, the lower layers of a system tend to be more general-purpose and the upper layers more special-purpose. For example, the topmost layer of an application consists of features totally specific to that application. The way to separate special-purpose code from general-purpose code is to pull the special-purpose code upwards, into the higher layers, leaving the lower layers general-purpose. When you encounter a class that includes both general-purpose and special-purpose features for the same abstraction, see if the class can be separated into two classes, one containing the general-purpose features, and the other layered on top of it to provide the special-purpose features.

通常,系统的下层倾向于更通用,而上层则更专用。例如,应用程序的最顶层包含完全特定于该应用程序的功能。将专用代码与通用代码分开的方法是将专用代码向上拉到较高的层,而将较低的层保留为通用。当您遇到同时包含通用功能和专用功能的同一类的类时,请查看该类是否可以分为两个类,一个包含通用功能,另一个在其上分层以提供特殊功能

9.5 Example: insertion cursor and selection 示例:插入光标和选择

The next sections work through three examples that illustrate the principles discussed above. In two of the examples the best approach is to separate the relevant pieces of code; in the third example it is better to join them together.

下一节将通过三个示例说明上述原则。在两个示例中,最好的方法是分离相关的代码段。在第三个示例中,最好将它们结合在一起。

The first example consists of the insertion cursor and the selection in the GUI editor project from Chapter 6. The editor displayed a blinking vertical line indicating where text typed by the user would appear in the document. It also displayed a highlighted range of characters called the selection, which was used for copying or deleting text. The insertion cursor was always visible, but there could be times when no text was selected. If the selection existed, the insertion cursor was always positioned at one end of it.

第一个示例由插入光标和第 6 章的 GUI 编辑器项目中的选择组成。编辑器显示闪烁的垂直线,指示用户键入的文本将出现在文档中的何处。它还显示了一个突出显示的字符范围,称为选择,用于复制或删除文本。插入光标始终可见,但是有时可能没有选择文本。如果存在选择,则插入光标始终位于其一端。

The selection and insertion cursor are related in some ways. For example, the cursor is always positioned at one end of the selection, and the cursor and selection tend to be manipulated together: clicking and dragging the mouse sets both of them, and text insertion first deletes the selected text, if there is any, and then inserts new text at the cursor position. Thus, it might seem logical to use a single object to manage both the selection and the cursor, and one project team took this approach. The object stored two positions in the file, along with booleans indicating which end was the cursor and whether the selection existed.

选择和插入光标在某些方面相关。例如,光标始终位于所选内容的一端,并且倾向于将光标和所选内容一起操作:单击并拖动鼠标将它们都设置,然后插入文本会首先删除所选的文本(如果有),然后在光标位置插入新文本。因此,使用单个对象管理选择和光标似乎合乎逻辑,并且一个项目团队采用了这种方法。该对象在文件中存储了两个位置,以及布尔值,它们指示光标的哪一端以及选择是否存在。

However, the combined object was awkward. It provided no benefit for higher-level code, since the higher-level code still needed to be aware of the selection and cursor as distinct entities, and it manipulated them separately (during text insertion, it first invoked a method on the combined object to delete the selected text; then it invoked another method to retrieve the cursor position in order to insert new text). The combined object was actually more complex to implement than separate objects. It avoided storing the cursor position as a separate entity, but instead had to store a boolean indicating which end of the selection was the cursor. In order to retrieve the cursor position, the combined object had to first test the boolean and then choose the appropriate end of the selection.

但是,合并的对象很尴尬。它对高级代码没有任何好处,因为高级代码仍然需要将选择和游标视为不同的实体,并且对它们进行单独操作(在插入文本期间,它首先在组合对象上调用一个方法来删除选定的文本;然后调用另一个方法来检索光标位置,以插入新文本)。实际上,组合对象比单独的对象实现起来要复杂得多。它避免了将光标位置存储为单独的实体,而是不得不存储一个布尔值,该布尔值指示选择的哪一端是光标。为了检索光标位置,组合对象必须首先测试布尔值,然后选择选择的适当结尾。

img Red Flag: Special-General Mixture img

This red flag occurs when a general-purpose mechanism also contains code specialized for a particular use of that mechanism. This makes the mechanism more complicated and creates information leakage between the mechanism and the particular use case: future modifications to the use case are likely to require changes to the underlying mechanism as well.

当通用机制还包含专门用于该机制的特定用途的代码时,就会出现此红色标志。这使该机制更加复杂,并在该机制与特定用例之间造成了信息泄漏:对用例的未来修改也可能需要对基础机制进行更改。

In this case, the selection and cursor were not closely enough related to combine them. When the code was revised to separate the selection and the cursor, both the usage and the implementation became simpler. Separate objects provided a simpler interface than a combined object from which selection and cursor information had to be extracted. The cursor implementation also got simpler because the cursor position was represented directly, rather than indirectly through a selection and a boolean. In fact, in the revised version no special classes were used for either the selection or the cursor. Instead, a new Position class was introduced to represent a location in the file (a line number and character within line). The selection was represented with two Positions and the cursor with one. Positions also found other uses in the project. This example also demonstrates the benefits of a lower-level but more general-purpose interface, which were discussed in Chapter 6.

在这种情况下,选择和光标之间的关联度不足以将它们组合在一起。当修改代码以分隔选择和光标时,用法和实现都变得更加简单。与必须从中提取选择和光标信息的组合对象相比,单独的对象提供了更简单的接口。游标的实现也变得更加简单,因为游标的位置是直接表示的,而不是通过选择和布尔值间接表示的。实际上,在修订版中,没有特殊的类用于选择或游标。相反,引入了一个新的 Position 类来表示文件中的位置(行号和行内的字符)。选择用两个位置表示,光标用一个位置表示。Position 类还在项目中找到了其他用途。这个例子也展示了第 6 章讨论过的一个更低级但更通用的接口的好处。

9.6 Example: separate class for logging 示例:单独的日志记录类

The second example involved error logging in a student project. A class contained several code sequences like the following:

第二个示例涉及学生项目中的关于记录错误日志的部分。一个类包含几个代码序列,如下所示:

try {
+import{_ as t,c as n,f as s,o}from"./app-BQdhfat9.js";const a="/A-Philosophy-of-Software-Design-zh/assets/00017-DPuvif3w.gif",i="/A-Philosophy-of-Software-Design-zh/assets/00018-DrRJ431J.gif",r="/A-Philosophy-of-Software-Design-zh/assets/00019-BXkohvDw.jpeg",l={};function c(p,e){return o(),n("div",null,e[0]||(e[0]=[s('

第 9 章 在一起更好还是分开更好?

Chapter 9 Better Together Or Better Apart?

One of the most fundamental questions in software design is this: given two pieces of functionality, should they be implemented together in the same place, or should their implementations be separated? This question applies at all levels in a system, such as functions, methods, classes, and services. For example, should buffering be included in the class that provides stream-oriented file I/O, or should it be in a separate class? Should the parsing of an HTTP request be implemented entirely in one method, or should it be divided among multiple methods (or even multiple classes)? This chapter discusses the factors to consider when making these decisions. Some of these factors have already been discussed in previous chapters, but they will be revisited here for completeness.

软件设计中最基本的问题之一是:给定两个功能,它们应该在同一位置一起实现,还是应该分开实现?这个问题适用于系统中的所有级别,例如功能,方法,类和服务。例如,应该在提供面向流的文件 I/O 的类中包括缓冲,还是应该在单独的类中?HTTP 请求的解析应该完全在一个方法中实现,还是应该在多个方法(甚至多个类)之间划分?本章讨论做出这些决定时要考虑的因素。这些因素中的一些已经在前面的章节中进行了讨论,但是为了完整起见,这里将对其进行重新讨论。

When deciding whether to combine or separate, the goal is to reduce the complexity of the system as a whole and improve its modularity. It might appear that the best way to achieve this goal is to divide the system into a large number of small components: the smaller the components, the simpler each individual component is likely to be. However, the act of subdividing creates additional complexity that was not present before subdivision:

在决定是合并还是分开时,目标是降低整个系统的复杂性并改善其模块化。看来实现此目标的最佳方法是将系统划分为大量的小组件:组件越小,每个单独的组件可能越简单。但是,细分的行为会带来额外的复杂性,而这在细分之前是不存在的:

  • Some complexity comes just from the number of components: the more components, the harder to keep track of them all and the harder to find a desired component within the large collection. Subdivision usually results in more interfaces, and every new interface adds complexity.
  • Subdivision can result in additional code to manage the components. For example, a piece of code that used a single object before subdivision might now have to manage multiple objects.
  • Subdivision creates separation: the subdivided components will be farther apart than they were before subdivision. For example, methods that were together in a single class before subdivision may be in different classes after subdivision, and possibly in different files. Separation makes it harder for developers to see the components at the same time, or even to be aware of their existence. If the components are truly independent, then separation is good: it allows the developer to focus on a single component at a time, without being distracted by the other components. On the other hand, if there are dependencies between the components, then separation is bad: developers will end up flipping back and forth between the components. Even worse, they may not be aware of the dependencies, which can lead to bugs.
  • Subdivision can result in duplication: code that was present in a single instance before subdivision may need to be present in each of the subdivided components.

  • 一些组件的复杂性仅来自组件的数量:组件越多,就越难以追踪所有组件,也就越难在大型集合中找到所需的组件。细分通常会导致更多接口,并且每个新接口都会增加复杂性。
  • 细分可能会导致附加代码来管理组件。例如,在细分之前使用单个对象的一段代码现在可能必须管理多个对象。
  • 细分产生分离:细分后的组件将比细分前的组件相距更远。例如,在细分之前位于单个类中的方法可能在细分之后位于不同的类中,并且可能在不同的文件中。分离使开发人员更难于同时查看这些组件,甚至很难知道它们的存在。如果组件真正独立,那么分离是好的:它使开发人员可以一次专注于单个组件,而不会被其他组件分散注意力。另一方面,如果组件之间存在依赖性,则分离是不好的:开发人员最终将在组件之间来回翻转。更糟糕的是,他们可能不了解依赖关系,这可能导致错误。
  • 细分可能导致重复:细分之前的单个实例中存在的代码可能需要存在于每个细分的组件中。

Bringing pieces of code together is most beneficial if they are closely related. If the pieces are unrelated, they are probably better off apart. Here are a few indications that two pieces of code are related:

如果它们紧密相关,则将代码段组合在一起是最有益的。如果各部分无关,则最好分开。以下是两个代码相关的一些提示:

  • They share information; for example, both pieces of code might depend on the syntax of a particular type of document.
  • They are used together: anyone using one of the pieces of code is likely to use the other as well. This form of relationship is only compelling if it is bidirectional. As a counter-example, a disk block cache will almost always involve a hash table, but hash tables can be used in many situations that don’t involve block caches; thus, these modules should be separate.
  • They overlap conceptually, in that there is a simple higher-level category that includes both of the pieces of code. For example, searching for a substring and case conversion both fall under the category of string manipulation; flow control and reliable delivery both fall under the category of network communication.
  • It is hard to understand one of the pieces of code without looking at the other.

  • 他们共享信息;例如,这两段代码都可能取决于特定类型文档的语法。
  • 它们一起使用:任何使用其中一段代码的人都可能同时使用另一段代码。这种关系形式只有在双向关系中才具有吸引力。作为反例,磁盘块高速缓存几乎总是包含哈希表,但是哈希表可以在许多不涉及块高速缓存的情况下使用。因此,这些模块应该分开。
  • 它们在概念上重叠,因为存在一个简单的更高级别的类别,其中包括这两段代码。例如,搜索子字符串和大小写转换都属于字符串操作类别。流控制和可靠的交付都属于网络通信的范畴。
  • 不看其中的一段代码就很难理解。

The rest of this chapter uses more specific rules as well as examples to show when it makes sense to bring pieces of code together and when it makes sense to separate them.

本章的其余部分使用更具体的规则以及示例来说明何时将代码段组合在一起以及何时将它们分开是有意义的。

9.1 Bring together if information is shared 如果信息共享则汇聚在一起

Section 5.4 introduced this principle in the context of a project implementing an HTTP server. In its first implementation, the project used two different methods in different classes to read in and parse HTTP requests. The first method read the text of an incoming request from a network socket and placed it in a string object. The second method parsed the string to extract the various components of the request. With this decomposition, both of the methods ended up with considerable knowledge of the format of HTTP requests: the first method was only trying to read the request, not parse it, but it couldn’t identify the end of the request without doing most of the work of parsing it (for example, it had to parse header lines in order to identify the header containing the overall request length). Because of this shared information, it is better to both read and parse the request in the same place; when the two classes were combined into one, the code got shorter and simpler.

5.4 节在实现 HTTP 服务器的项目上下文中介绍了此原则。在其第一个实现中,该项目使用了两个位于不同类的方法来读取和解析 HTTP 请求。第一个方法从网络套接字读取传入请求的文本,并将其放置在字符串对象中。第二个方法解析字符串以提取请求的各个组成部分。经过这种分解,这两种方法最终都对 HTTP 请求的格式有了相当的了解:第一个方法只是尝试读取请求,而不是解析请求,但是如果不执行大多数操作,就无法确定请求的结束解析它的工作(例如,它必须解析标头行才能识别包含整个请求长度的标头)。由于此共享信息,最好在同一位置读取和解析请求;当两个类合而为一时,代码变得更短,更简单。

9.2 Bring together if it will simplify the interface 如果可以简化接口则汇集在一起

When two or more modules are combined into a single module, it may be possible to define an interface for the new module that is simpler or easier to use than the original interfaces. This often happens when the original modules each implement part of the solution to a problem. In the HTTP server example from the preceding section, the original methods required an interface to return the HTTP request string from the first method and pass it to the second. When the methods were combined, these interfaces were eliminated.

当两个或多个模块组合成一个模块时,可以为新模块定义一个比原始接口更简单或更易于使用的接口。当原始模块各自实现问题解决方案的一部分时,通常会发生这种情况。在上一部分的 HTTP 服务器示例中,原始方法需要一个接口来从第一个方法返回 HTTP 请求字符串并将其传递给第二个方法。当这些方法结合在一起时,这些接口就被淘汰了。

In addition, when the functionality of two or more classes is combined, it may be possible to perform some functions automatically, so that most users need not be aware of them. The Java I/O library illustrates this opportunity. If the FileInputStream and BufferedInputStream classes were combined and buffering were provided by default, the vast majority of users would never even need to be aware of the existence of buffering. A combined FileInputStream class might provide methods to disable or replace the default buffering mechanism, but most users would not need to learn about them.

另外,将两个或更多类的功能组合在一起时,可能会自动执行某些功能,因此大多数用户无需了解它们。Java I/O 库说明了这种机会。如果将 FileInputStream 和 BufferedInputStream 类组合在一起,并且默认情况下提供了缓冲,则绝大多数用户甚至都不需要知道缓冲的存在。组合后的 FileInputStream 类可能提供禁用或替换默认缓冲机制的方法,但是大多数用户不需要了解它们。

9.3 Bring together to eliminate duplication 消除重复

If you find the same pattern of code repeated over and over, see if you can reorganize the code to eliminate the repetition. One approach is to factor the repeated code out into a separate method and replace the repeated code snippets with calls to the method. This approach is most effective if the repeated code snippet is long and the replacement method has a simple signature. If the snippet is only one or two lines long, there may not be much benefit in replacing it with a method call. If the snippet interacts in complex ways with its environment (such as by accessing numerous local variables), then the replacement method might require a complex signature (such as many pass-by-reference arguments), which would reduce its value.

如果发现反复重复的代码模式,请查看是否可以重新组织代码以消除重复。一种方法是将重复的代码分解为一个单独的方法,并用对该方法的调用替换重复的代码段。如果重复的代码段很长并且用来替换方法具有简单的签名,则此方法最有效。如果代码段只有一两行,那么用方法调用替换它可能不会有太多好处。如果代码段与其环境以复杂的方式进行交互(例如,通过访问多个局部变量),则替换方法可能需要复杂的签名(例如,许多“按引用传递”参数),这会降低其价值。

Another way to eliminate duplication is to refactor the code so that the snippet in question only needs to be executed in one place. Suppose you are writing a method that needs to return errors at several different points, and the same cleanup actions need to be performed at each of these points before returning (see Figure 9.1 for an example). If the programming language supports goto, you can move the cleanup code to the very end of the method and then goto that snippet at each of the points where an error return is required, as in Figure 9.2. Goto statements are generally considered a bad idea, and they can result in indecipherable code if used indiscriminately, but they are useful in situations like this where they are used to escape from nested code.

消除重复的另一种方法是重构代码,使相关代码段仅需要在一个地方执行。假设您正在编写一种方法,该方法需要在几个不同的点返回错误,并且在返回之前需要在每个这些点执行相同的清除操作(示例请参见图 9.1)。如果编程语言支持 goto,则可以将清除代码移到方法的最后,然后在需要返回错误的每个点处转到该片段,如图 9.2 所示。Goto 语句通常被认为是一个坏主意,如果不加选择地使用它们,可能会导致无法识别的代码,但是在诸如此类的情况下,它们可用于从嵌套代码中脱离,因此它们非常有用。

9.4 Separate general-purpose and special-purpose code 单独的通用代码和专用代码

If a module contains a mechanism that can be used for several different purposes, then it should provide just that one general-purpose mechanism. It should not include code that specializes the mechanism for a particular use, nor should it contain other general-purpose mechanisms. Special-purpose code associated with a general-purpose mechanism should normally go in a different module (typically one associated with the particular purpose). The GUI editor discussion in Chapter 6 illustrated this principle: the best design was one where the text class provided general-purpose text operations, while operations particular to the user interface (such as deleting the selection) were implemented in the user interface module. This approach eliminated information leakage and additional interfaces that were present in an earlier design where the specialized user interface operations were implemented in the text class.

如果模块包含可用于多种不同目的的机制,则它应仅提供一种通用机制。它不应包含专门针对特定用途的机制的代码,也不应包含其他通用机制。与通用机制关联的专用代码通常应放在不同的模块中(通常是与特定用途关联的模块)。第 6 章中的 GUI 编辑器讨论阐明了这一原则:最佳设计是文本类提供通用文本操作,而特定于用户界面的操作(例如删除所选内容)则在用户界面模块中实现。这种方法消除了早期设计中存在的信息泄漏和附加接口,在早期设计中,专门的用户界面操作是在文本类中实现的。

img Red Flag: Repetition img

If the same piece of code (or code that is almost the same) appears over and over again, that’s a red flag that you haven’t found the right abstractions.

如果相同的代码(或几乎相同的代码)一遍又一遍地出现,那是一个危险信号,您没有找到正确的抽象。

Figure 9.1: This code processes incoming network packets of different types; for each type, if the packet is too short for that type, a message gets logged. In this version of the code, the LOG statement is duplicated for several different packet types.

图 9.1:此代码处理不同类型的传入网络数据包。对于每种类型,如果数据包对于该类型而言太短,则会记录一条消息。在此版本的代码中,LOG 语句对于几种不同的数据包类型是重复的。

Figure 9.2: A reorganization of the code from Figure 9.1 so that there is only one copy of the LOG statement.

图 9.2:对图 9.1 中的代码进行了重新组织,因此只有 LOG 语句的一个副本。

In general, the lower layers of a system tend to be more general-purpose and the upper layers more special-purpose. For example, the topmost layer of an application consists of features totally specific to that application. The way to separate special-purpose code from general-purpose code is to pull the special-purpose code upwards, into the higher layers, leaving the lower layers general-purpose. When you encounter a class that includes both general-purpose and special-purpose features for the same abstraction, see if the class can be separated into two classes, one containing the general-purpose features, and the other layered on top of it to provide the special-purpose features.

通常,系统的下层倾向于更通用,而上层则更专用。例如,应用程序的最顶层包含完全特定于该应用程序的功能。将专用代码与通用代码分开的方法是将专用代码向上拉到较高的层,而将较低的层保留为通用。当您遇到同时包含通用功能和专用功能的同一类的类时,请查看该类是否可以分为两个类,一个包含通用功能,另一个在其上分层以提供特殊功能

9.5 Example: insertion cursor and selection 示例:插入光标和选择

The next sections work through three examples that illustrate the principles discussed above. In two of the examples the best approach is to separate the relevant pieces of code; in the third example it is better to join them together.

下一节将通过三个示例说明上述原则。在两个示例中,最好的方法是分离相关的代码段。在第三个示例中,最好将它们结合在一起。

The first example consists of the insertion cursor and the selection in the GUI editor project from Chapter 6. The editor displayed a blinking vertical line indicating where text typed by the user would appear in the document. It also displayed a highlighted range of characters called the selection, which was used for copying or deleting text. The insertion cursor was always visible, but there could be times when no text was selected. If the selection existed, the insertion cursor was always positioned at one end of it.

第一个示例由插入光标和第 6 章的 GUI 编辑器项目中的选择组成。编辑器显示闪烁的垂直线,指示用户键入的文本将出现在文档中的何处。它还显示了一个突出显示的字符范围,称为选择,用于复制或删除文本。插入光标始终可见,但是有时可能没有选择文本。如果存在选择,则插入光标始终位于其一端。

The selection and insertion cursor are related in some ways. For example, the cursor is always positioned at one end of the selection, and the cursor and selection tend to be manipulated together: clicking and dragging the mouse sets both of them, and text insertion first deletes the selected text, if there is any, and then inserts new text at the cursor position. Thus, it might seem logical to use a single object to manage both the selection and the cursor, and one project team took this approach. The object stored two positions in the file, along with booleans indicating which end was the cursor and whether the selection existed.

选择和插入光标在某些方面相关。例如,光标始终位于所选内容的一端,并且倾向于将光标和所选内容一起操作:单击并拖动鼠标将它们都设置,然后插入文本会首先删除所选的文本(如果有),然后在光标位置插入新文本。因此,使用单个对象管理选择和光标似乎合乎逻辑,并且一个项目团队采用了这种方法。该对象在文件中存储了两个位置,以及布尔值,它们指示光标的哪一端以及选择是否存在。

However, the combined object was awkward. It provided no benefit for higher-level code, since the higher-level code still needed to be aware of the selection and cursor as distinct entities, and it manipulated them separately (during text insertion, it first invoked a method on the combined object to delete the selected text; then it invoked another method to retrieve the cursor position in order to insert new text). The combined object was actually more complex to implement than separate objects. It avoided storing the cursor position as a separate entity, but instead had to store a boolean indicating which end of the selection was the cursor. In order to retrieve the cursor position, the combined object had to first test the boolean and then choose the appropriate end of the selection.

但是,合并的对象很尴尬。它对高级代码没有任何好处,因为高级代码仍然需要将选择和游标视为不同的实体,并且对它们进行单独操作(在插入文本期间,它首先在组合对象上调用一个方法来删除选定的文本;然后调用另一个方法来检索光标位置,以插入新文本)。实际上,组合对象比单独的对象实现起来要复杂得多。它避免了将光标位置存储为单独的实体,而是不得不存储一个布尔值,该布尔值指示选择的哪一端是光标。为了检索光标位置,组合对象必须首先测试布尔值,然后选择选择的适当结尾。

img Red Flag: Special-General Mixture img

This red flag occurs when a general-purpose mechanism also contains code specialized for a particular use of that mechanism. This makes the mechanism more complicated and creates information leakage between the mechanism and the particular use case: future modifications to the use case are likely to require changes to the underlying mechanism as well.

当通用机制还包含专门用于该机制的特定用途的代码时,就会出现此红色标志。这使该机制更加复杂,并在该机制与特定用例之间造成了信息泄漏:对用例的未来修改也可能需要对基础机制进行更改。

In this case, the selection and cursor were not closely enough related to combine them. When the code was revised to separate the selection and the cursor, both the usage and the implementation became simpler. Separate objects provided a simpler interface than a combined object from which selection and cursor information had to be extracted. The cursor implementation also got simpler because the cursor position was represented directly, rather than indirectly through a selection and a boolean. In fact, in the revised version no special classes were used for either the selection or the cursor. Instead, a new Position class was introduced to represent a location in the file (a line number and character within line). The selection was represented with two Positions and the cursor with one. Positions also found other uses in the project. This example also demonstrates the benefits of a lower-level but more general-purpose interface, which were discussed in Chapter 6.

在这种情况下,选择和光标之间的关联度不足以将它们组合在一起。当修改代码以分隔选择和光标时,用法和实现都变得更加简单。与必须从中提取选择和光标信息的组合对象相比,单独的对象提供了更简单的接口。游标的实现也变得更加简单,因为游标的位置是直接表示的,而不是通过选择和布尔值间接表示的。实际上,在修订版中,没有特殊的类用于选择或游标。相反,引入了一个新的 Position 类来表示文件中的位置(行号和行内的字符)。选择用两个位置表示,光标用一个位置表示。Position 类还在项目中找到了其他用途。这个例子也展示了第 6 章讨论过的一个更低级但更通用的接口的好处。

9.6 Example: separate class for logging 示例:单独的日志记录类

The second example involved error logging in a student project. A class contained several code sequences like the following:

第二个示例涉及学生项目中的关于记录错误日志的部分。一个类包含几个代码序列,如下所示:

try {
     rpcConn = connectionPool.getConnection(dest);
 } catch (IOException e) {
     NetworkErrorLogger.logRpcOpenError(req, dest, e);
@@ -32,4 +32,4 @@ import{_ as t,c as n,f as s,o}from"./app-CvqtBB8Z.js";const a="/A-Philosophy-of-
     void undo() {...}
     void redo() {...}
 }
-

In this design, the History class manages a collection of objects that implement the interface History.Action. Each History.Action describes a single operation, such as a text insertion or a change in the cursor location, and it provides methods that can undo or redo the operation. The History class knows nothing about the information stored in the actions or how they implement their undo and redo methods. History maintains a history list describing all of the actions executed over the lifetime of an application, and it provides undo and redo methods that walk backwards and forwards through the list in response to user-requested undos and redos, calling undo and redo methods in the History.Actions.

在此设计中,History 类管理实现接口 History.Action 的对象的集合。每个 History.Action 描述一个操作,例如插入文本或更改光标位置,并且它提供了可以撤消或重做该操作的方法。History 类对操作中存储的信息或它们如何实现其撤消和重做方法一无所知。History 类维护一个历史记录列表,该列表描述了应用程序生命周期内执行的所有操作,它还提供了撤消和重做方法,这些方法响应用户请求的撤消和重做,在 History.Actions 中调用撤消和重做方法。

History.Actions are special-purpose objects: each one understands a particular kind of undoable operation. They are implemented outside the History class, in modules that understand particular kinds of undoable actions. The text class might implement UndoableInsert and UndoableDelete objects to describe text insertions and deletions. Whenever it inserts text, the text class creates a new UndoableInsert object describing the insertion and invokes History.addAction to add it to the history list. The editor’s user interface code might create UndoableSelection and UndoableCursor objects that describe changes to the selection and insertion cursor.

History.Actions 是特殊目的的对象:每个人都了解一种特殊的可撤销操作。它们在 History 类之外的模块中实现,这些模块可以理解特定类型的可撤销操作。文本类可能实现 UndoableInsert 和 UndoableDelete 对象,以描述文本的插入和删除。每当插入文本时,文本类都会创建一个描述该插入的新 UndoableInsert 对象,并调用 History.addAction 将其添加到历史列表中。编辑器的用户界面代码可能会创建 UndoableSelection 和 UndoableCursor 对象,这些对象描述对选择和插入光标的更改。

The History class also allows actions to be grouped so that, for example, a single undo request from the user can restore deleted text, reselect the deleted text, and reposition the insertion cursor. There are a number of ways to group actions; the History class uses fences, which are markers placed in the history list to separate groups of related actions. Each call to History.redo walks backwards through the history list, undoing actions until it reaches the next fence. The placement of fences is determined by higher-level code by invoking History.addFence.

History 类还允许对操作进行分组,例如,来自用户的单个撤消请求可以恢复已删除的文本,重新选择已删除的文本以及重新放置插入光标。有多种将动作分组的方法。历史类使用栅栏,栅栏是放置在历史列表中的标记,用于分隔相关动作的组。每次对 History.redo 的调用都会向后浏览历史记录列表,撤消操作,直到到达下一个栅栏。围栏的位置由更高级别的代码通过调用 History.addFence 确定。

This approach divides the functionality of undo into three categories, each of which is implemented in a different place:

这种方法将撤消功能分为三类,每类都在不同的地方实现:

A general-purpose mechanism for managing and grouping actions and invoking undo/redo operations (implemented by the History class). The specifics of particular actions (implemented by a variety of classes, each of which understands a small number of action types). The policy for grouping actions (implemented by high-level user interface code to provide the right overall application behavior). Each of these categories can be implemented without any understanding of the other categories. The History class does not know what kind of actions are being undone; it could be used in a variety of applications. Each action class understands only a single kind of action, and neither the History class nor the action classes needs to be aware of the policy for grouping actions.

一种用于管理和分组动作以及调用撤消/重做操作的通用机制(由 History 类实现)。特定操作的细节(由各种类实现,每个类都了解少量的操作类型)。分组操作的策略(由高级用户界面代码实现,以提供正确的整体应用程序行为)。这些类别中的每一个都可以在不了解其他类别的情况下实施。History 类不知道要撤消哪种操作;它可以用于多种应用。每个 Action 类仅理解一种动作,并且 History 类和 Action 类都不需要知道将动作分组的策略。

The key design decision was the one that separated the general-purpose part of the undo mechanism from the special-purpose parts and put the general-purpose part in a class by itself. Once that was done, the rest of the design fell out naturally.

关键的设计决策是将撤消机制的通用部分与专用部分分开,然后将通用部分单独放在一个类中。一旦完成,其余的设计就自然而然的出现了。

Note: the suggestion to separate general-purpose code from special-purpose code refers to code related to a particular mechanism. For example, special-purpose undo code (such as code to undo a text insertion) should be separated from general-purpose undo code (such as code to manage the history list). However, it often makes sense to combine special-purpose code for one mechanism with general-purpose code for another. The text class is an example of this: it implements a general-purpose mechanism for managing text, but it includes special-purpose code related to undoing. The undo code is special-purpose because it only handles undo operations for text modifications. It doesn’t make sense to combine this code with the general-purpose undo infrastructure in the History class, but it does make sense to put it in the text class, since it is closely related to other text functions.

注意:将通用代码与专用代码分离的建议是指与特定机制相关的代码。例如,特殊用途的撤消代码(例如撤消文本插入的代码)应该与通用用途的撤消代码(例如管理历史记录列表的代码)分开。然而,将一种机制的专用代码与另一种机制的通用代码组合起来通常是有意义的。text 类就是这样一个例子:它实现了一种管理文本的通用机制,但是它包含了与撤销相关的专用代码。撤消代码是专用的,因为它只处理文本修改的撤消操作。将这段代码与 History 类中通用的 undo 基础结构结合在一起是没有意义的,但是将它放在 text 类中是有意义的,因为它与其他文本函数密切相关。

9.8 Splitting and joining methods 拆分和合并方法

The issue of when to subdivide applies not just to classes, but also to methods: are there times when it is better to divide an existing method into multiple smaller methods? Or, should two smaller methods be combined into one larger one? Long methods tend to be more difficult to understand than shorter ones, so many people argue that length alone is a good justification for breaking up a method. Students in classes are often given rigid criteria, such as “Split up any method longer than 20 lines!”

何时细分的问题不仅适用于类,而且还适用于方法:是否有时最好将现有方法分为多个较小的方法?还是应该将两种较小的方法合并为一种较大的方法?长方法比短方法更难于理解,因此许多人认为仅长度是分解方法的一个很好的理由。课堂上的学生通常会获得严格的标准,例如“拆分超过 20 行的任何方法!”

However, length by itself is rarely a good reason for splitting up a method. In general, developers tend to break up methods too much. Splitting up a method introduces additional interfaces, which add to complexity. It also separates the pieces of the original method, which makes the code harder to read if the pieces are actually related. You shouldn’t break up a method unless it makes the overall system simpler; I’ll discuss how this might happen below.

但是,长度本身很少是拆分方法的一个很好的理由。通常,开发人员倾向于过多地分解方法。拆分方法会引入其他接口,从而增加了复杂性。它还将原始方法的各个部分分开,如果这些部分实际上是相关的,则使代码更难阅读。您不应该分解一种方法,除非它使整个系统更加简单;我将在下面讨论这种情况。

Long methods aren’t always bad. For example, suppose a method contains five 20-line blocks of code that are executed in order. If the blocks are relatively independent, then the method can be read and understood one block at a time; there’s not much benefit in moving each of the blocks into a separate method. If the blocks have complex interactions, it’s even more important to keep them together so readers can see all of the code at once; if each block is in a separate method, readers will have to flip back and forth between these spread-out methods in order to understand how they work together. Methods containing hundreds of lines of code are fine if they have a simple signature and are easy to read. These methods are deep (lots of functionality, simple interface), which is good.

长方法并不总是坏的。例如,假设一个方法包含按顺序执行的五个 20 行代码块。如果这些块是相对独立的,则可以一次读取并理解该方法的一个块。将每个块移动到单独的方法中并没有太大的好处。如果这些块具有复杂的交互作用,则将它们保持在一起就显得尤为重要,这样读者就可以一次看到所有代码。如果每个块使用单独的方法,则读者将不得不在这些扩展方法之间来回切换,以了解它们如何协同工作。如果方法具有简单的签名并且易于阅读,则包含数百行代码的方法是可以接受的。这些方法很深(功能多,接口简单),很好。

Figure 9.3: A method (a) can be split either by by extracting a subtask (b) or by dividing its functionality into two separate methods (c). A method should not be split if it results in shallow methods, as in (d).

图 9.3:方法(a)可以通过提取子任务(b)或将其功能划分为两个单独的方法(c)进行拆分。如果方法导致浅方法,则不应拆分该方法,如(d)所示。

When designing methods, the most important goal is to provide clean and simple abstractions. Each method should do one thing and do it completely. The method should have a clean and simple interface, so that users don’t need to have much information in their heads in order to use it correctly. The method should be deep: its interface should be much simpler than its implementation. If a method has all of these properties, then it probably doesn’t matter whether it is long or not.

设计方法时,最重要的目标是提供简洁的抽象。每种方法都应该做一件事并且完全做的彻底。该方法应该具有简洁的接口,以便用户无需费神就可以正确使用它。该方法应该很深:其接口应该比其实现简单得多。如果一个方法具有所有这些属性,那么它的长短与否无关紧要。

Splitting up a method only makes sense if it results in cleaner abstractions, overall. There are two ways to do this, which are diagrammed in Figure 9.3. The best way is by factoring out a subtask into a separate method, as shown in Figure 9.3(b). The subdivision results in a child method containing the subtask and a parent method containing the remainder of the original method; the parent invokes the child. The interface of the new parent method is the same as the original method. This form of subdivision makes sense if there is a subtask that is cleanly separable from the rest of the original method, which means (a) someone reading the child method doesn’t need to know anything about the parent method and (b) someone reading the parent method doesn’t need to understand the implementation of the child method. Typically this means that the child method is relatively general-purpose: it could conceivably be used by other methods besides the parent. If you make a split of this form and then find yourself flipping back and forth between the parent and child to understand how they work together, that is a red flag (“Conjoined Methods”) indicating that the split was probably a bad idea.

总体而言,分割一个方法只有在产生更清晰的抽象时才有意义。有两种方式可以做到这一点,如图 9.3 所示。最佳方法是将子任务分解为单独的方法,如图 9.3(b)所示。该细分产生一个包含该子任务的子方法和一个包含原始方法其余部分的父方法;父方法调用子字方法。新的父方法的接口与原始方法的接口相同。如果存在一个与原始方法的其余部分完全可分离的子任务,则这种细分形式是有意义的,这意味着(a)读取子方法的某人不需要了解有关父方法的任何信息,以及(b)某人在阅读父方法不需要了解子方法的实现。通常,这意味着子方法是相对通用的:可以想象除父方法外,其他方法也可以使用它。如果您做了这种形式进行拆分,然后发现自己在父方法和子方法之间来回跳转以了解他们如何一起工作,那是一个警告(“联合方法”),表明拆分可能不是一个好主意。

The second way to break up a method is to split it into two separate methods, each visible to callers of the original method, as in Figure 9.3(c). This makes sense if the original method had an overly complex interface because it tried to do multiple things that were not closely related. If this is the case, it may be possible to divide the method’s functionality into two or more smaller methods, each of which has only a part of the original method’s functionality. If you make a split like this, the interface for each of the resulting methods should be simpler than the interface of the original method. Ideally, most callers should only need to invoke one of the two new methods; if callers must invoke both of the new methods, then that adds complexity, which makes it less likely that the split is a good idea. The new methods will be more focused in what they do. It is a good sign if the new methods are more general-purpose than the original method (i.e., you can imagine using them separately in other situations).

分解方法的第二种方法是将其拆分为两个单独的方法,每个方法对原始方法的调用者都可见,如图 9.3(c)所示。如果原始方法的接口过于复杂,这是有道理的,因为该接口试图执行不密切相关的多项操作。在这种情况下,可以将方法的功能划分为两个或更多个较小的方法,每个方法仅具有原始方法功能的一部分。如果进行这样的拆分,则每个子方法的接口应该比原始方法的接口更简单。理想情况下,大多数调用者只需要调用两个新方法之一即可;如果调用者必须同时调用这两个新方法,则将增加复杂性,从而降低拆分是个好主意的可能性。新方法将更加专注于它们的工作。如果新方法比原始方法更具通用性,那么这是一个好兆头(例如,您可以想象在其他情况下单独使用它们)。

Splits of the form shown in Figure 9.3(c) don’t make sense very often, because they result in callers having to deal with multiple methods instead of one. When you split this way, you run the risk of ending up with several shallow methods, as in Figure 9.3(d). If the caller has to invoke each of the separate methods, passing state back and forth between them, then splitting is not a good idea. If you’re considering a split like the one in Figure 9.3(c), you should judge it based on whether it simplifies things for callers.

图 9.3(c)所示形式的拆分并不是很有意义,因为它们导致调用者不得不处理多个方法而不是一个方法。当您以这种方式拆分时,您可能会遇到几种浅层方法的风险,如图 9.3(d)所示。如果调用者必须调用每个单独的方法,并在它们之间来回传递状态,则拆分不是一个好主意。如果您正在考虑像图 9.3(c)所示的拆分,则应基于它是否简化了调用者的情况来进行判断。

There are also situations where a system can be made simpler by joining methods together. For example, joining methods might replace two shallow methods with one deeper method; it might eliminate duplication of code; it might eliminate dependencies between the original methods, or intermediate data structures; it might result in better encapsulation, so that knowledge that was previously present in multiple places is now isolated in a single place; or it might result in a simpler interface, as discussed in Section 9.2.

在某些情况下,通过将方法结合在一起可以简化系统。例如,连接方法可以用一种更深的方法代替两种浅的方法。它可以消除重复的代码;它可以消除原始方法或中间数据结构之间的依赖关系;它可能导致更好的封装,从而使以前在多个位置存在的知识现在被隔离在一个位置;也可能会使接口更简单,如 9.2 节所述。

img Red Flag: Conjoined Methods img

It should be possible to understand each method independently. If you can’t understand the implementation of one method without also understanding the implementation of another, that’s a red flag. This red flag can occur in other contexts as well: if two pieces of code are physically separated, but each can only be understood by looking at the other, that is a red flag.

应该可以独立理解每种方法。如果您不能不理解另一种方法的实现而导致无法理解一种方法的实现,那就是一个危险信号。该危险信号也可以在其他情况下发生:如果两段代码在物理上是分开的,但是只有通过查看另一段代码才能理解它们,这就是危险信号。

9.9 Conclusion 结论

The decision to split or join modules should be based on complexity. Pick the structure that results in the best information hiding, the fewest dependencies, and the deepest interfaces.

拆分或合并模块的决定应基于复杂性。选择一种结构,它可以最好的隐藏信息,产生最少的依赖关系和最深的接口。

',120)]))}const h=t(l,[["render",c],["__file","ch09.html.vue"]]),u=JSON.parse('{"path":"/ch09.html","title":"第 9 章 在一起更好还是分开更好?","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"9.1 Bring together if information is shared 如果信息共享则汇聚在一起","slug":"_9-1-bring-together-if-information-is-shared-如果信息共享则汇聚在一起","link":"#_9-1-bring-together-if-information-is-shared-如果信息共享则汇聚在一起","children":[]},{"level":2,"title":"9.2 Bring together if it will simplify the interface 如果可以简化接口则汇集在一起","slug":"_9-2-bring-together-if-it-will-simplify-the-interface-如果可以简化接口则汇集在一起","link":"#_9-2-bring-together-if-it-will-simplify-the-interface-如果可以简化接口则汇集在一起","children":[]},{"level":2,"title":"9.3 Bring together to eliminate duplication 消除重复","slug":"_9-3-bring-together-to-eliminate-duplication-消除重复","link":"#_9-3-bring-together-to-eliminate-duplication-消除重复","children":[]},{"level":2,"title":"9.4 Separate general-purpose and special-purpose code 单独的通用代码和专用代码","slug":"_9-4-separate-general-purpose-and-special-purpose-code-单独的通用代码和专用代码","link":"#_9-4-separate-general-purpose-and-special-purpose-code-单独的通用代码和专用代码","children":[]},{"level":2,"title":"9.5 Example: insertion cursor and selection 示例:插入光标和选择","slug":"_9-5-example-insertion-cursor-and-selection-示例-插入光标和选择","link":"#_9-5-example-insertion-cursor-and-selection-示例-插入光标和选择","children":[]},{"level":2,"title":"9.6 Example: separate class for logging 示例:单独的日志记录类","slug":"_9-6-example-separate-class-for-logging-示例-单独的日志记录类","link":"#_9-6-example-separate-class-for-logging-示例-单独的日志记录类","children":[]},{"level":2,"title":"9.7 Example: editor undo mechanism 示例:编辑器撤消机制","slug":"_9-7-example-editor-undo-mechanism-示例-编辑器撤消机制","link":"#_9-7-example-editor-undo-mechanism-示例-编辑器撤消机制","children":[]},{"level":2,"title":"9.8 Splitting and joining methods 拆分和合并方法","slug":"_9-8-splitting-and-joining-methods-拆分和合并方法","link":"#_9-8-splitting-and-joining-methods-拆分和合并方法","children":[]},{"level":2,"title":"9.9 Conclusion 结论","slug":"_9-9-conclusion-结论","link":"#_9-9-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch09.md"}');export{h as comp,u as data}; +

In this design, the History class manages a collection of objects that implement the interface History.Action. Each History.Action describes a single operation, such as a text insertion or a change in the cursor location, and it provides methods that can undo or redo the operation. The History class knows nothing about the information stored in the actions or how they implement their undo and redo methods. History maintains a history list describing all of the actions executed over the lifetime of an application, and it provides undo and redo methods that walk backwards and forwards through the list in response to user-requested undos and redos, calling undo and redo methods in the History.Actions.

在此设计中,History 类管理实现接口 History.Action 的对象的集合。每个 History.Action 描述一个操作,例如插入文本或更改光标位置,并且它提供了可以撤消或重做该操作的方法。History 类对操作中存储的信息或它们如何实现其撤消和重做方法一无所知。History 类维护一个历史记录列表,该列表描述了应用程序生命周期内执行的所有操作,它还提供了撤消和重做方法,这些方法响应用户请求的撤消和重做,在 History.Actions 中调用撤消和重做方法。

History.Actions are special-purpose objects: each one understands a particular kind of undoable operation. They are implemented outside the History class, in modules that understand particular kinds of undoable actions. The text class might implement UndoableInsert and UndoableDelete objects to describe text insertions and deletions. Whenever it inserts text, the text class creates a new UndoableInsert object describing the insertion and invokes History.addAction to add it to the history list. The editor’s user interface code might create UndoableSelection and UndoableCursor objects that describe changes to the selection and insertion cursor.

History.Actions 是特殊目的的对象:每个人都了解一种特殊的可撤销操作。它们在 History 类之外的模块中实现,这些模块可以理解特定类型的可撤销操作。文本类可能实现 UndoableInsert 和 UndoableDelete 对象,以描述文本的插入和删除。每当插入文本时,文本类都会创建一个描述该插入的新 UndoableInsert 对象,并调用 History.addAction 将其添加到历史列表中。编辑器的用户界面代码可能会创建 UndoableSelection 和 UndoableCursor 对象,这些对象描述对选择和插入光标的更改。

The History class also allows actions to be grouped so that, for example, a single undo request from the user can restore deleted text, reselect the deleted text, and reposition the insertion cursor. There are a number of ways to group actions; the History class uses fences, which are markers placed in the history list to separate groups of related actions. Each call to History.redo walks backwards through the history list, undoing actions until it reaches the next fence. The placement of fences is determined by higher-level code by invoking History.addFence.

History 类还允许对操作进行分组,例如,来自用户的单个撤消请求可以恢复已删除的文本,重新选择已删除的文本以及重新放置插入光标。有多种将动作分组的方法。历史类使用栅栏,栅栏是放置在历史列表中的标记,用于分隔相关动作的组。每次对 History.redo 的调用都会向后浏览历史记录列表,撤消操作,直到到达下一个栅栏。围栏的位置由更高级别的代码通过调用 History.addFence 确定。

This approach divides the functionality of undo into three categories, each of which is implemented in a different place:

这种方法将撤消功能分为三类,每类都在不同的地方实现:

A general-purpose mechanism for managing and grouping actions and invoking undo/redo operations (implemented by the History class). The specifics of particular actions (implemented by a variety of classes, each of which understands a small number of action types). The policy for grouping actions (implemented by high-level user interface code to provide the right overall application behavior). Each of these categories can be implemented without any understanding of the other categories. The History class does not know what kind of actions are being undone; it could be used in a variety of applications. Each action class understands only a single kind of action, and neither the History class nor the action classes needs to be aware of the policy for grouping actions.

一种用于管理和分组动作以及调用撤消/重做操作的通用机制(由 History 类实现)。特定操作的细节(由各种类实现,每个类都了解少量的操作类型)。分组操作的策略(由高级用户界面代码实现,以提供正确的整体应用程序行为)。这些类别中的每一个都可以在不了解其他类别的情况下实施。History 类不知道要撤消哪种操作;它可以用于多种应用。每个 Action 类仅理解一种动作,并且 History 类和 Action 类都不需要知道将动作分组的策略。

The key design decision was the one that separated the general-purpose part of the undo mechanism from the special-purpose parts and put the general-purpose part in a class by itself. Once that was done, the rest of the design fell out naturally.

关键的设计决策是将撤消机制的通用部分与专用部分分开,然后将通用部分单独放在一个类中。一旦完成,其余的设计就自然而然的出现了。

Note: the suggestion to separate general-purpose code from special-purpose code refers to code related to a particular mechanism. For example, special-purpose undo code (such as code to undo a text insertion) should be separated from general-purpose undo code (such as code to manage the history list). However, it often makes sense to combine special-purpose code for one mechanism with general-purpose code for another. The text class is an example of this: it implements a general-purpose mechanism for managing text, but it includes special-purpose code related to undoing. The undo code is special-purpose because it only handles undo operations for text modifications. It doesn’t make sense to combine this code with the general-purpose undo infrastructure in the History class, but it does make sense to put it in the text class, since it is closely related to other text functions.

注意:将通用代码与专用代码分离的建议是指与特定机制相关的代码。例如,特殊用途的撤消代码(例如撤消文本插入的代码)应该与通用用途的撤消代码(例如管理历史记录列表的代码)分开。然而,将一种机制的专用代码与另一种机制的通用代码组合起来通常是有意义的。text 类就是这样一个例子:它实现了一种管理文本的通用机制,但是它包含了与撤销相关的专用代码。撤消代码是专用的,因为它只处理文本修改的撤消操作。将这段代码与 History 类中通用的 undo 基础结构结合在一起是没有意义的,但是将它放在 text 类中是有意义的,因为它与其他文本函数密切相关。

9.8 Splitting and joining methods 拆分和合并方法

The issue of when to subdivide applies not just to classes, but also to methods: are there times when it is better to divide an existing method into multiple smaller methods? Or, should two smaller methods be combined into one larger one? Long methods tend to be more difficult to understand than shorter ones, so many people argue that length alone is a good justification for breaking up a method. Students in classes are often given rigid criteria, such as “Split up any method longer than 20 lines!”

何时细分的问题不仅适用于类,而且还适用于方法:是否有时最好将现有方法分为多个较小的方法?还是应该将两种较小的方法合并为一种较大的方法?长方法比短方法更难于理解,因此许多人认为仅长度是分解方法的一个很好的理由。课堂上的学生通常会获得严格的标准,例如“拆分超过 20 行的任何方法!”

However, length by itself is rarely a good reason for splitting up a method. In general, developers tend to break up methods too much. Splitting up a method introduces additional interfaces, which add to complexity. It also separates the pieces of the original method, which makes the code harder to read if the pieces are actually related. You shouldn’t break up a method unless it makes the overall system simpler; I’ll discuss how this might happen below.

但是,长度本身很少是拆分方法的一个很好的理由。通常,开发人员倾向于过多地分解方法。拆分方法会引入其他接口,从而增加了复杂性。它还将原始方法的各个部分分开,如果这些部分实际上是相关的,则使代码更难阅读。您不应该分解一种方法,除非它使整个系统更加简单;我将在下面讨论这种情况。

Long methods aren’t always bad. For example, suppose a method contains five 20-line blocks of code that are executed in order. If the blocks are relatively independent, then the method can be read and understood one block at a time; there’s not much benefit in moving each of the blocks into a separate method. If the blocks have complex interactions, it’s even more important to keep them together so readers can see all of the code at once; if each block is in a separate method, readers will have to flip back and forth between these spread-out methods in order to understand how they work together. Methods containing hundreds of lines of code are fine if they have a simple signature and are easy to read. These methods are deep (lots of functionality, simple interface), which is good.

长方法并不总是坏的。例如,假设一个方法包含按顺序执行的五个 20 行代码块。如果这些块是相对独立的,则可以一次读取并理解该方法的一个块。将每个块移动到单独的方法中并没有太大的好处。如果这些块具有复杂的交互作用,则将它们保持在一起就显得尤为重要,这样读者就可以一次看到所有代码。如果每个块使用单独的方法,则读者将不得不在这些扩展方法之间来回切换,以了解它们如何协同工作。如果方法具有简单的签名并且易于阅读,则包含数百行代码的方法是可以接受的。这些方法很深(功能多,接口简单),很好。

Figure 9.3: A method (a) can be split either by by extracting a subtask (b) or by dividing its functionality into two separate methods (c). A method should not be split if it results in shallow methods, as in (d).

图 9.3:方法(a)可以通过提取子任务(b)或将其功能划分为两个单独的方法(c)进行拆分。如果方法导致浅方法,则不应拆分该方法,如(d)所示。

When designing methods, the most important goal is to provide clean and simple abstractions. Each method should do one thing and do it completely. The method should have a clean and simple interface, so that users don’t need to have much information in their heads in order to use it correctly. The method should be deep: its interface should be much simpler than its implementation. If a method has all of these properties, then it probably doesn’t matter whether it is long or not.

设计方法时,最重要的目标是提供简洁的抽象。每种方法都应该做一件事并且完全做的彻底。该方法应该具有简洁的接口,以便用户无需费神就可以正确使用它。该方法应该很深:其接口应该比其实现简单得多。如果一个方法具有所有这些属性,那么它的长短与否无关紧要。

Splitting up a method only makes sense if it results in cleaner abstractions, overall. There are two ways to do this, which are diagrammed in Figure 9.3. The best way is by factoring out a subtask into a separate method, as shown in Figure 9.3(b). The subdivision results in a child method containing the subtask and a parent method containing the remainder of the original method; the parent invokes the child. The interface of the new parent method is the same as the original method. This form of subdivision makes sense if there is a subtask that is cleanly separable from the rest of the original method, which means (a) someone reading the child method doesn’t need to know anything about the parent method and (b) someone reading the parent method doesn’t need to understand the implementation of the child method. Typically this means that the child method is relatively general-purpose: it could conceivably be used by other methods besides the parent. If you make a split of this form and then find yourself flipping back and forth between the parent and child to understand how they work together, that is a red flag (“Conjoined Methods”) indicating that the split was probably a bad idea.

总体而言,分割一个方法只有在产生更清晰的抽象时才有意义。有两种方式可以做到这一点,如图 9.3 所示。最佳方法是将子任务分解为单独的方法,如图 9.3(b)所示。该细分产生一个包含该子任务的子方法和一个包含原始方法其余部分的父方法;父方法调用子字方法。新的父方法的接口与原始方法的接口相同。如果存在一个与原始方法的其余部分完全可分离的子任务,则这种细分形式是有意义的,这意味着(a)读取子方法的某人不需要了解有关父方法的任何信息,以及(b)某人在阅读父方法不需要了解子方法的实现。通常,这意味着子方法是相对通用的:可以想象除父方法外,其他方法也可以使用它。如果您做了这种形式进行拆分,然后发现自己在父方法和子方法之间来回跳转以了解他们如何一起工作,那是一个警告(“联合方法”),表明拆分可能不是一个好主意。

The second way to break up a method is to split it into two separate methods, each visible to callers of the original method, as in Figure 9.3(c). This makes sense if the original method had an overly complex interface because it tried to do multiple things that were not closely related. If this is the case, it may be possible to divide the method’s functionality into two or more smaller methods, each of which has only a part of the original method’s functionality. If you make a split like this, the interface for each of the resulting methods should be simpler than the interface of the original method. Ideally, most callers should only need to invoke one of the two new methods; if callers must invoke both of the new methods, then that adds complexity, which makes it less likely that the split is a good idea. The new methods will be more focused in what they do. It is a good sign if the new methods are more general-purpose than the original method (i.e., you can imagine using them separately in other situations).

分解方法的第二种方法是将其拆分为两个单独的方法,每个方法对原始方法的调用者都可见,如图 9.3(c)所示。如果原始方法的接口过于复杂,这是有道理的,因为该接口试图执行不密切相关的多项操作。在这种情况下,可以将方法的功能划分为两个或更多个较小的方法,每个方法仅具有原始方法功能的一部分。如果进行这样的拆分,则每个子方法的接口应该比原始方法的接口更简单。理想情况下,大多数调用者只需要调用两个新方法之一即可;如果调用者必须同时调用这两个新方法,则将增加复杂性,从而降低拆分是个好主意的可能性。新方法将更加专注于它们的工作。如果新方法比原始方法更具通用性,那么这是一个好兆头(例如,您可以想象在其他情况下单独使用它们)。

Splits of the form shown in Figure 9.3(c) don’t make sense very often, because they result in callers having to deal with multiple methods instead of one. When you split this way, you run the risk of ending up with several shallow methods, as in Figure 9.3(d). If the caller has to invoke each of the separate methods, passing state back and forth between them, then splitting is not a good idea. If you’re considering a split like the one in Figure 9.3(c), you should judge it based on whether it simplifies things for callers.

图 9.3(c)所示形式的拆分并不是很有意义,因为它们导致调用者不得不处理多个方法而不是一个方法。当您以这种方式拆分时,您可能会遇到几种浅层方法的风险,如图 9.3(d)所示。如果调用者必须调用每个单独的方法,并在它们之间来回传递状态,则拆分不是一个好主意。如果您正在考虑像图 9.3(c)所示的拆分,则应基于它是否简化了调用者的情况来进行判断。

There are also situations where a system can be made simpler by joining methods together. For example, joining methods might replace two shallow methods with one deeper method; it might eliminate duplication of code; it might eliminate dependencies between the original methods, or intermediate data structures; it might result in better encapsulation, so that knowledge that was previously present in multiple places is now isolated in a single place; or it might result in a simpler interface, as discussed in Section 9.2.

在某些情况下,通过将方法结合在一起可以简化系统。例如,连接方法可以用一种更深的方法代替两种浅的方法。它可以消除重复的代码;它可以消除原始方法或中间数据结构之间的依赖关系;它可能导致更好的封装,从而使以前在多个位置存在的知识现在被隔离在一个位置;也可能会使接口更简单,如 9.2 节所述。

img Red Flag: Conjoined Methods img

It should be possible to understand each method independently. If you can’t understand the implementation of one method without also understanding the implementation of another, that’s a red flag. This red flag can occur in other contexts as well: if two pieces of code are physically separated, but each can only be understood by looking at the other, that is a red flag.

应该可以独立理解每种方法。如果您不能不理解另一种方法的实现而导致无法理解一种方法的实现,那就是一个危险信号。该危险信号也可以在其他情况下发生:如果两段代码在物理上是分开的,但是只有通过查看另一段代码才能理解它们,这就是危险信号。

9.9 Conclusion 结论

The decision to split or join modules should be based on complexity. Pick the structure that results in the best information hiding, the fewest dependencies, and the deepest interfaces.

拆分或合并模块的决定应基于复杂性。选择一种结构,它可以最好的隐藏信息,产生最少的依赖关系和最深的接口。

',120)]))}const h=t(l,[["render",c],["__file","ch09.html.vue"]]),u=JSON.parse('{"path":"/ch09.html","title":"第 9 章 在一起更好还是分开更好?","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"9.1 Bring together if information is shared 如果信息共享则汇聚在一起","slug":"_9-1-bring-together-if-information-is-shared-如果信息共享则汇聚在一起","link":"#_9-1-bring-together-if-information-is-shared-如果信息共享则汇聚在一起","children":[]},{"level":2,"title":"9.2 Bring together if it will simplify the interface 如果可以简化接口则汇集在一起","slug":"_9-2-bring-together-if-it-will-simplify-the-interface-如果可以简化接口则汇集在一起","link":"#_9-2-bring-together-if-it-will-simplify-the-interface-如果可以简化接口则汇集在一起","children":[]},{"level":2,"title":"9.3 Bring together to eliminate duplication 消除重复","slug":"_9-3-bring-together-to-eliminate-duplication-消除重复","link":"#_9-3-bring-together-to-eliminate-duplication-消除重复","children":[]},{"level":2,"title":"9.4 Separate general-purpose and special-purpose code 单独的通用代码和专用代码","slug":"_9-4-separate-general-purpose-and-special-purpose-code-单独的通用代码和专用代码","link":"#_9-4-separate-general-purpose-and-special-purpose-code-单独的通用代码和专用代码","children":[]},{"level":2,"title":"9.5 Example: insertion cursor and selection 示例:插入光标和选择","slug":"_9-5-example-insertion-cursor-and-selection-示例-插入光标和选择","link":"#_9-5-example-insertion-cursor-and-selection-示例-插入光标和选择","children":[]},{"level":2,"title":"9.6 Example: separate class for logging 示例:单独的日志记录类","slug":"_9-6-example-separate-class-for-logging-示例-单独的日志记录类","link":"#_9-6-example-separate-class-for-logging-示例-单独的日志记录类","children":[]},{"level":2,"title":"9.7 Example: editor undo mechanism 示例:编辑器撤消机制","slug":"_9-7-example-editor-undo-mechanism-示例-编辑器撤消机制","link":"#_9-7-example-editor-undo-mechanism-示例-编辑器撤消机制","children":[]},{"level":2,"title":"9.8 Splitting and joining methods 拆分和合并方法","slug":"_9-8-splitting-and-joining-methods-拆分和合并方法","link":"#_9-8-splitting-and-joining-methods-拆分和合并方法","children":[]},{"level":2,"title":"9.9 Conclusion 结论","slug":"_9-9-conclusion-结论","link":"#_9-9-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch09.md"}');export{h as comp,u as data}; diff --git a/assets/ch10.html-B5eK6nuj.js b/assets/ch10.html-Cs5324Hi.js similarity index 99% rename from assets/ch10.html-B5eK6nuj.js rename to assets/ch10.html-Cs5324Hi.js index 0064292e..a0e567b0 100644 --- a/assets/ch10.html-B5eK6nuj.js +++ b/assets/ch10.html-Cs5324Hi.js @@ -1,4 +1,4 @@ -import{_ as t,c as a,f as n,o}from"./app-CvqtBB8Z.js";const s="/A-Philosophy-of-Software-Design-zh/assets/00020-9j5ufL2N.jpeg",i="/A-Philosophy-of-Software-Design-zh/assets/00021-CnXONOtr.jpeg",r={};function l(c,e){return o(),a("div",null,e[0]||(e[0]=[n(`

第 10 章 通过定义规避错误

Chapter 10 Define Errors Out Of Existence

Exception handling is one of the worst sources of complexity in software systems. Code that deals with special conditions is inherently harder to write than code that deals with normal cases, and developers often define exceptions without considering how they will be handled. This chapter discusses why exceptions contribute disproportionately to complexity, then it shows how to simplify exception handling. The key overall lesson from this chapter is to reduce the number of places where exceptions must be handled; in many cases the semantics of operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report (hence the title of this chapter).

异常处理是软件系统中最糟糕的复杂性来源之一。处理特殊情况的代码在本质上比处理正常情况的代码更难编写,并且开发人员经常在定义异常时不考虑异常的处理方式。本章讨论了为什么异常对复杂性的贡献不成比例,然后说明了如何简化异常处理。本章总的主要教训是减少必须处理异常的地方的数量。在许多情况下,可以修改操作的语义,以便正常行为可以处理所有情况,并且没有要报告的特殊条件(这就是本章的主题)。

10.1 Why exceptions add complexity 为什么异常会增加复杂性

I use the term exception to refer to any uncommon condition that alters the normal flow of control in a program. Many programming languages include a formal exception mechanism that allows exceptions to be thrown by lower-level code and caught by enclosing code. However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn’t complete its normal behavior. All of these forms of exceptions contribute to complexity.

我使用“异常”一词来指代任何会改变程序中正常控制流程的不常见条件。许多编程语言都包含一种正式的异常机制,该机制允许异常由低级代码引发并由捕获代码(try catch)捕获。但是,即使不使用正式的异常报告机制,异常也可能发生,例如,当某个方法返回一个特殊值指示其未完成其正常行为时。所有这些形式的异常都会增加复杂性。

A particular piece of code may encounter exceptions in several different ways:

一段特定的代码可能会以几种不同的方式遇到异常:

  • A caller may provide bad arguments or configuration information.
  • An invoked method may not be able to complete a requested operation. For example, an I/O operation may fail, or a required resource may not be available.
  • In a distributed system, network packets may be lost or delayed, servers may not respond in a timely fashion, or peers may communicate in unexpected ways.
  • The code may detect bugs, internal inconsistencies, or situations it is not prepared to handle.

  • 调用方可能会提供错误的参数或配置信息。
  • 调用的方法可能无法完成请求的操作。例如,I/O 操作可能失败,或者所需的资源可能不可用。
  • 在分布式系统中,网络数据包可能会丢失或延迟,服务器可能无法及时响应,或者节点间可能会以意想不到的方式进行通信。
  • 该代码可能会检测到错误,内部不一致或未准备处理的情况。

Large systems have to deal with many exceptional conditions, particularly if they are distributed or need to be fault-tolerant. Exception handling can account for a significant fraction of all the code in a system.

大型系统必须应对许多特殊情况,特别是在它们是分布式的或需要容错的情况下。异常处理可以占系统中所有代码的很大一部分。

Exception handling code is inherently more difficult to write than normal-case code. An exception disrupts the normal flow of the code; it usually means that something didn’t work as expected. When an exception occurs, the programmer can deal with it in two ways, each of which can be complicated. The first approach is to move forward and complete the work in progress in spite of the exception. For example, if a network packet is lost, it can be resent; if data is corrupted, perhaps it can be recovered from a redundant copy. The second approach is to abort the operation in progress and report the exception upwards. However, aborting can be complicated because the exception may have occurred at a point where system state is inconsistent (a data structure might have been partially initialized); the exception handling code must restore consistency, such as by unwinding any changes made before the exception occurred.

异常处理代码天生就比正常情况下的代码更难写。异常中断了正常的代码流;它通常意味着某事没有像预期的那样工作。当异常发生时,程序员可以用两种方法处理它,每种方法都很复杂。第一种方法是向前推进并完成正在进行的工作,尽管存在例外。例如,如果一个网络数据包丢失,它可以被重发;如果数据损坏了,也许可以从冗余副本中恢复数据。第二种方法是中止正在进行的操作,向上报告异常。但是,中止可能很复杂,因为异常可能发生在系统状态不一致的地方(数据结构可能已经部分初始化);异常处理代码必须恢复一致性,例如通过撤销发生异常之前所做的任何更改。

Furthermore, exception handling code creates opportunities for more exceptions. Consider the case of resending a lost network packet. Perhaps the packet wasn’t actually lost, but was simply delayed. In this case, resending the packet will result in duplicate packets arriving at the peer; this introduces a new exceptional condition that the peer must handle. Or, consider the case of recovering lost data from a redundant copy: what if the redundant copy has also been lost? Secondary exceptions occurring during recovery are often more subtle and complex than the primary exceptions. If an exception is handled by aborting the operation in progress, then this must be reported to the caller as another exception. To prevent an unending cascade of exceptions, the developer must eventually find a way to handle exceptions without introducing more exceptions.

此外,异常处理代码为更多异常创造了机会。考虑重新发送丢失的网络数据包的情况。也许该数据包实际上并没有丢失,但是只是被延迟了。在这种情况下,重新发送数据包将导致重复的数据包到达对节点;这引入了节点必须处理的新的例外条件。或者,考虑从冗余副本恢复丢失的数据的情况:如果冗余副本也丢失了怎么办?在恢复期间发生的次要异常通常比主要异常更加微妙和复杂。如果通过中止正在进行的操作来处理异常,则必须将此异常作为另一个异常报告给调用方。为了防止无休止的异常级联,开发人员最终必须找到一种在不引入更多异常的情况下处理异常的方法。

Language support for exceptions tends to be verbose and clunky, which makes exception handling code hard to read. For example, consider the following code, which reads a collection of tweets from a file using Java’s support for object serialization and deserialization:

语言对异常的支持往往是冗长而笨拙的,这使得异常处理代码难以阅读。例如,考虑以下代码,该代码使用 Java 对对象序列化和反序列化的支持从文件中读取 tweet 的集合:

try (
+import{_ as t,c as a,f as n,o}from"./app-BQdhfat9.js";const s="/A-Philosophy-of-Software-Design-zh/assets/00020-9j5ufL2N.jpeg",i="/A-Philosophy-of-Software-Design-zh/assets/00021-CnXONOtr.jpeg",r={};function l(c,e){return o(),a("div",null,e[0]||(e[0]=[n(`

第 10 章 通过定义规避错误

Chapter 10 Define Errors Out Of Existence

Exception handling is one of the worst sources of complexity in software systems. Code that deals with special conditions is inherently harder to write than code that deals with normal cases, and developers often define exceptions without considering how they will be handled. This chapter discusses why exceptions contribute disproportionately to complexity, then it shows how to simplify exception handling. The key overall lesson from this chapter is to reduce the number of places where exceptions must be handled; in many cases the semantics of operations can be modified so that the normal behavior handles all situations and there is no exceptional condition to report (hence the title of this chapter).

异常处理是软件系统中最糟糕的复杂性来源之一。处理特殊情况的代码在本质上比处理正常情况的代码更难编写,并且开发人员经常在定义异常时不考虑异常的处理方式。本章讨论了为什么异常对复杂性的贡献不成比例,然后说明了如何简化异常处理。本章总的主要教训是减少必须处理异常的地方的数量。在许多情况下,可以修改操作的语义,以便正常行为可以处理所有情况,并且没有要报告的特殊条件(这就是本章的主题)。

10.1 Why exceptions add complexity 为什么异常会增加复杂性

I use the term exception to refer to any uncommon condition that alters the normal flow of control in a program. Many programming languages include a formal exception mechanism that allows exceptions to be thrown by lower-level code and caught by enclosing code. However, exceptions can occur even without using a formal exception reporting mechanism, such as when a method returns a special value indicating that it didn’t complete its normal behavior. All of these forms of exceptions contribute to complexity.

我使用“异常”一词来指代任何会改变程序中正常控制流程的不常见条件。许多编程语言都包含一种正式的异常机制,该机制允许异常由低级代码引发并由捕获代码(try catch)捕获。但是,即使不使用正式的异常报告机制,异常也可能发生,例如,当某个方法返回一个特殊值指示其未完成其正常行为时。所有这些形式的异常都会增加复杂性。

A particular piece of code may encounter exceptions in several different ways:

一段特定的代码可能会以几种不同的方式遇到异常:

  • A caller may provide bad arguments or configuration information.
  • An invoked method may not be able to complete a requested operation. For example, an I/O operation may fail, or a required resource may not be available.
  • In a distributed system, network packets may be lost or delayed, servers may not respond in a timely fashion, or peers may communicate in unexpected ways.
  • The code may detect bugs, internal inconsistencies, or situations it is not prepared to handle.

  • 调用方可能会提供错误的参数或配置信息。
  • 调用的方法可能无法完成请求的操作。例如,I/O 操作可能失败,或者所需的资源可能不可用。
  • 在分布式系统中,网络数据包可能会丢失或延迟,服务器可能无法及时响应,或者节点间可能会以意想不到的方式进行通信。
  • 该代码可能会检测到错误,内部不一致或未准备处理的情况。

Large systems have to deal with many exceptional conditions, particularly if they are distributed or need to be fault-tolerant. Exception handling can account for a significant fraction of all the code in a system.

大型系统必须应对许多特殊情况,特别是在它们是分布式的或需要容错的情况下。异常处理可以占系统中所有代码的很大一部分。

Exception handling code is inherently more difficult to write than normal-case code. An exception disrupts the normal flow of the code; it usually means that something didn’t work as expected. When an exception occurs, the programmer can deal with it in two ways, each of which can be complicated. The first approach is to move forward and complete the work in progress in spite of the exception. For example, if a network packet is lost, it can be resent; if data is corrupted, perhaps it can be recovered from a redundant copy. The second approach is to abort the operation in progress and report the exception upwards. However, aborting can be complicated because the exception may have occurred at a point where system state is inconsistent (a data structure might have been partially initialized); the exception handling code must restore consistency, such as by unwinding any changes made before the exception occurred.

异常处理代码天生就比正常情况下的代码更难写。异常中断了正常的代码流;它通常意味着某事没有像预期的那样工作。当异常发生时,程序员可以用两种方法处理它,每种方法都很复杂。第一种方法是向前推进并完成正在进行的工作,尽管存在例外。例如,如果一个网络数据包丢失,它可以被重发;如果数据损坏了,也许可以从冗余副本中恢复数据。第二种方法是中止正在进行的操作,向上报告异常。但是,中止可能很复杂,因为异常可能发生在系统状态不一致的地方(数据结构可能已经部分初始化);异常处理代码必须恢复一致性,例如通过撤销发生异常之前所做的任何更改。

Furthermore, exception handling code creates opportunities for more exceptions. Consider the case of resending a lost network packet. Perhaps the packet wasn’t actually lost, but was simply delayed. In this case, resending the packet will result in duplicate packets arriving at the peer; this introduces a new exceptional condition that the peer must handle. Or, consider the case of recovering lost data from a redundant copy: what if the redundant copy has also been lost? Secondary exceptions occurring during recovery are often more subtle and complex than the primary exceptions. If an exception is handled by aborting the operation in progress, then this must be reported to the caller as another exception. To prevent an unending cascade of exceptions, the developer must eventually find a way to handle exceptions without introducing more exceptions.

此外,异常处理代码为更多异常创造了机会。考虑重新发送丢失的网络数据包的情况。也许该数据包实际上并没有丢失,但是只是被延迟了。在这种情况下,重新发送数据包将导致重复的数据包到达对节点;这引入了节点必须处理的新的例外条件。或者,考虑从冗余副本恢复丢失的数据的情况:如果冗余副本也丢失了怎么办?在恢复期间发生的次要异常通常比主要异常更加微妙和复杂。如果通过中止正在进行的操作来处理异常,则必须将此异常作为另一个异常报告给调用方。为了防止无休止的异常级联,开发人员最终必须找到一种在不引入更多异常的情况下处理异常的方法。

Language support for exceptions tends to be verbose and clunky, which makes exception handling code hard to read. For example, consider the following code, which reads a collection of tweets from a file using Java’s support for object serialization and deserialization:

语言对异常的支持往往是冗长而笨拙的,这使得异常处理代码难以阅读。例如,考虑以下代码,该代码使用 Java 对对象序列化和反序列化的支持从文件中读取 tweet 的集合:

try (
     FileInputStream fileStream = new FileInputStream(fileName);
     BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
     ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);
@@ -23,4 +23,4 @@ import{_ as t,c as a,f as n,o}from"./app-CvqtBB8Z.js";const s="/A-Philosophy-of-
 catch (ClassCastException e) {
     ...
 }
-

Just the basic try-catch boilerplate accounts for more lines of code than the code for normal-case operation, without even considering the code that actually handles the exceptions. It is hard to relate the exception handling code to the normal-case code: for example, it’s not obvious where each exception is generated. An alternative approach is to break up the code into many distinct try blocks; in the extreme case there could be a try for each line of code that can generate an exception. This would make it clear where exceptions occur, but the try blocks themselves break up the flow of the code and make it harder to read; in addition, some exception handling code might end up duplicated in multiple try blocks.

只是基本的 try-catch 样板代码比正常情况下的操作代码所占的代码行更多,甚至没有考虑实际处理异常的代码。很难将异常处理代码与普通情况代码相关联:例如,每个异常的生成位置都不明显。另一种方法是将代码分解为许多不同的 try 块。在极端情况下,每行可能产生异常的代码都需要单独的 try 块。这样可以清楚地说明异常发生的位置,但是 try 块本身会破坏代码流,并使代码难以阅读。此外,某些异常处理代码可能最终会在多个 try 块中重复。

It’s difficult to ensure that exception handling code really works. Some exceptions, such as I/O errors, can’t easily be generated in a test environment, so it’s hard to test the code that handles them. Exceptions don’t occur very often in running systems, so exception handling code rarely executes. Bugs can go undetected for a long time, and when the exception handling code is finally needed, there’s a good chance that it won’t work (one of my favorite sayings: “code that hasn’t been executed doesn’t work”). A recent study found that more than 90% of catastrophic failures in distributed data-intensive systems were caused by incorrect error handling1. When exception handling code fails, it’s difficult to debug the problem, since it occurs so infrequently.

确保异常处理代码是否会真正起作用是困难的。某些异常(例如 I/O 错误)在测试环境中不易生成,因此很难测试处理它们的代码。异常在运行的系统中很少发生,因此异常处理代码很少执行。错误可能会长时间未被发现,并且当最终需要异常处理代码时,它很有可能无法正常工作(我最喜欢的一句话是:“未执行的代码无效”) 。最近的一项研究发现,分布式数据密集型系统中超过 90%的灾难性故障是由错误的错误处理引起的 1。当异常处理代码失败时,很难调试该问题,因为它很少发生。

10.2 Too many exceptions 异常过多

Programmers exacerbate the problems related to exception handling by defining unnecessary exceptions. Most programmers are taught that it’s important to detect and report errors; they often interpret this to mean “the more errors detected, the better.” This leads to an over-defensive style where anything that looks even a bit suspicious is rejected with an exception, which results in a proliferation of unnecessary exceptions that increase the complexity of the system.

程序员通过定义不必要的异常加剧了与异常处理有关的问题。大多数程序员被教导检测和报告错误很重要。他们通常将其解释为“检测到的错误越多越好”。这导致了一种过度防御的风格,任何看起来有点可疑的东西都会被异常拒绝,从而导致不必要的异常激增,增加了系统的复杂性。

I made this mistake myself in the design of the Tcl scripting language. Tcl contains an unset command that can be used to remove a variable. I defined unset so that it throws an error if the variable doesn’t exist. At the time I thought that it must be a bug if someone tries to delete a variable that doesn’t exist, so Tcl should report it. However, one of the most common uses of unset is to clean up temporary state created by some previous operation. It’s often hard to predict exactly what state was created, particularly if the operation aborted partway through. Thus, the simplest thing is to delete all of the variables that might possibly have been created. The definition of unset makes this awkward: developers end up enclosing calls to unset in catch statements to catch and ignore errors thrown by unset. In retrospect, the definition of the unset command is one of the biggest mistakes I made in the design of Tcl.

在设计 Tcl 脚本语言时,我自己就犯了这个错误。Tcl 包含一个 unset 命令,可用于删除变量。我定义的 unset 会在变量不存在时抛出错误。当时我认为,如果有人试图删除一个不存在的变量,那么它一定是一个 bug,所以 Tcl 应该报告它。然而,unset 最常见的用途之一是清理以前操作创建的临时状态。通常很难准确预测创建了什么状态,尤其是如果操作中途中止。因此,最简单的方法是删除可能已经创建的所有变量。unset 的定义使得这种情况很尴尬:开发人员最终会在 catch 语句中再使用 try catch 以捕获并忽略 unset 抛出的错误。回顾过去,unset 命令的设计是我在 Tcl 设计中犯下的最大错误之一。

It’s tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller. Some might argue that this approach empowers callers, since it allows each caller to handle the exception in a different way. However, if you are having trouble figuring out what to do for the particular situation, there’s a good chance that the caller won’t know what to do either. Generating an exception in a situation like this just passes the problem to someone else and adds to the system’s complexity.

使用异常来避免处理困难的情况是很诱人的:与其想出一种干净的方法来处理它,不如抛出一个异常并将问题转移给调用者。有人可能会争辩说,这种方法可以赋予调用者权力,因为它允许每个调用者以不同的方式处理异常。然而,如果你不知道做什么去处理特殊情况,调用者也很有可能不知道该做什么。在这种情况下生成异常只会将问题传递给其他人,并增加系统的复杂性。

The exceptions thrown by a class are part of its interface; classes with lots of exceptions have complex interfaces, and they are shallower than classes with fewer exceptions. An exception is a particularly complex element of an interface. It can propagate up through several stack levels before being caught, so it affects not just the method’s caller, but potentially also higher-level callers (and their interfaces).

类抛出的异常是其接口的一部分;具有大量异常的类具有复杂的接口,并且比具有较少异常的类浅。异常是接口中特别复杂的元素。它可以在被捕获之前通过多个堆栈级别向上传播,因此它不仅影响方法的调用者,而且还可能影响更高级别的调用者(及其接口)。

Throwing exceptions is easy; handling them is hard. Thus, the complexity of exceptions comes from the exception handling code. The best way to reduce the complexity damage caused by exception handling is to reduce the number of places where exceptions have to be handled. The rest of this chapter will discuss four techniques for reducing the number of exception handlers.

抛出异常很容易;处理它们很困难。因此,异常的复杂性来自异常处理代码。减少由异常处理引起的复杂性破坏的最佳方法是减少必须处理异常的位置的数量。本章的其余部分将讨论减少异常处理程序数量的四种技术。

10.3 Define errors out of existence 通过定义规避错误

The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence. This may seem sacrilegious, but it is very effective in practice. Consider the Tcl unset command discussed above. Rather than throwing an error when unset is asked to delete an unknown variable, it should have simply returned without doing anything. I should have changed the definition of unset slightly: rather than deleting a variable, unset should ensure that a variable no longer exists. With the first definition, unset can’t do its job if the variable doesn’t exist, so generating an exception makes sense. With the second definition, it is perfectly natural for unset to be invoked with the name of a variable that doesn’t exist. In this case, its work is already done, so it can simply return. There is no longer an error case to report.

消除异常处理复杂性的最好方法是设计好您的 API,使其没有异常要处理:这就是 通过定义规避错误。这看似亵渎神灵,但在实践中非常有效。考虑上面讨论的 Tcl unset 命令。当unset被要求删除一个未知变量时,它不应该抛出一个错误,而应该简单地返回而不做任何事情。我应该稍微修改一下 unset 的定义:与其删除一个变量,不如用来确保一个变量不再存在。根据第一个定义,如果变量不存在,则 unset 不能执行其工作,因此生成异常是说的通的。使用第二个定义,对不存在的变量名调用 unset 是很自然的。在这种情况下,它的工作已经完成,因此可以简单地返回。不再有错误需要上报。

10.4 Example: file deletion in Windows 示例:Windows 中的文件删除

File deletion provides another example of how errors can be defined away. The Windows operating system does not permit a file to be deleted if it is open in a process. This is a continual source of frustration for developers and users. In order to delete a file that is in use, the user must search through the system to find the process that has the file open, and then kill that process. Sometimes users give up and reboot their system, just so they can delete a file.

文件删除提供了如何通过定义规避错误另一个示例。Windows 操作系统不允许删除文件(如果已在进程中打开文件)。对于开发人员和用户来说,这是不断沮丧的根源。为了删除正在使用的文件,用户必须在系统中搜索以找到已打开文件的进程,然后终止该进程。有时用户放弃并重新启动系统,只是为了删除文件。

The Unix operating system defines file deletion more elegantly. In Unix, if a file is open when it is deleted, Unix does not delete the file immediately. Instead, it marks the file for deletion, then the delete operation returns successfully. The file name has been removed from its directory, so no other processes can open the old file and a new file with the same name can be created, but the existing file data persists. Processes that already have the file open can continue to read it and write it normally. Once the file has been closed by all of the accessing processes, its data is freed.

Unix 操作系统更优雅地定义了文件删除。在 Unix 中,如果在删除文件时打开了文件,则 Unix 不会立即删除该文件。而是将文件标记为删除,然后删除操作成功返回。该文件名已从其目录中删除,因此其他进程无法打开该旧文件,并且可以创建具有相同名称的新文件,但现有文件数据将保留。已经打开文件的进程可以继续读取和正常写入文件。一旦所有访问进程都关闭了文件,便释放其数据。

The Unix approach defines away two different kinds of errors. First, the delete operation no longer returns an error if the file is currently in use; the delete succeeds, and the file will eventually be deleted. Second, deleting a file that’s in use does not create exceptions for the processes using the file. One possible approach to this problem would have been to delete the file immediately and mark all of the opens of the file to disable them; any attempts by other processes to read or write the deleted file would fail. However, this approach would create new errors for those processes to handle. Instead, Unix allows them to keep accessing the file normally; delaying the file deletion defines errors out of existence.

Unix 删除文件的方式规避了两种不同的错误。首先,如果文件当前正在使用中,则删除操作不再返回错误;删除成功,该文件最终将被删除。其次,删除正在使用的文件不会使正在使用该文件的进程抛出异常。解决此问题的一种可能方法是立即删除文件并标记所有打开的文件以禁用它们。其他进程读取或写入已删除文件的任何尝试均将失败。但是,此方法将产生需要那些进程处理的新的错误。相反,Unix 允许他们继续正常访问文件。延迟文件删除规避了这个问题。

It may seem strange that Unix allows a process to continue to read and write a doomed file, but I have never encountered a situation where this caused significant problems. The Unix definition of file deletion is much simpler to work with, both for developers and users, than the Windows definition.

Unix 允许进程继续读取和写入已损坏的文件可能看起来很奇怪,但是我从未遇到过因此引起严重问题的情况。对于开发人员和用户,Unix 删除文件的设计比 Windows 的设计要容易得多。

10.5 Example: Java substring method 示例:Java 子字符串方法

As a final example, consider the Java String class and its substring method. Given two indexes into a string, substring returns the substring starting at the character given by the first index and ending with the character just before the second index. However, if either index is outside the range of the string, then substring throws IndexOutOfBoundsException. This exception is unnecessary and complicates the use of this method. I often find myself in a situation where one or both of the indices may be outside the range of the string, and I would like to extract all of the characters in the string that overlap the specified range. Unfortunately, this requires me to check each of the indices and round them up to zero or down to the end of the string; a one-line method call now becomes 5–10 lines of code.

作为最后一个示例,请考虑 Java String 类及其子字符串方法。给定一个字符串中的两个索引,substring 方法返回从第一个索引给定的字符开始,以第二个索引之前的字符结束的子字符串。但是,如果两个索引中的任何一个超出字符串的范围,substring 方法将抛出 IndexOutOfBoundsException。此异常是不必要的,并且会使此方法的使用复杂化。我经常发现自己处于一个或两个索引可能不在字符串范围内的情况,并且我想提取字符串中与指定范围重叠的所有字符。不幸的是,这要求我检查每个索引并将它们向上舍入为零或向下舍入到字符串的末尾。现在,单行方法调用变成 5-10 行代码。

The Java substring method would be easier to use if it performed this adjustment automatically, so that it implemented the following API: “returns the characters of the string (if any) with index greater than or equal to beginIndex and less than endIndex.” This is a simple and natural API, and it defines the IndexOutOfBoundsException exception out of existence. The method’s behavior is now well-defined even if one or both of the indexes are negative, or if beginIndex is greater than endIndex. This approach simplifies the API for the method while increasing its functionality, so it makes the method deeper. Many other languages have taken the error-free approach; for example, Python returns an empty result for out-of-range list slices.

如果 Java 子字符串方法自动执行此调整,则将更易于使用,因此它实现了以下 API:“返回索引大于或等于 beginIndex 且小于 endIndex 的字符串的字符(如果有)。” 这是一个简单自然的 API,它规避了 IndexOutOfBoundsException 异常。现在,即使一个或两个索引均为负,或者 beginIndex 大于 endIndex,该方法的行为也已明确定义。这种方法简化了方法的 API,同时增加了其功能,因此使方法更深。许多其他语言都采用了这种无错误的方式。例如,Python 对于超出范围的列表切片返回空结果。

When I argue for defining errors out of existence, people sometimes counter that throwing errors will catch bugs; if errors are defined out of existence, won’t that result in buggier software? Perhaps this is why the Java developers decided that substring should throw exceptions. The error-ful approach may catch some bugs, but it also increases complexity, which results in other bugs. In the error-ful approach, developers must write additional code to avoid or ignore the errors, and this increases the likelihood of bugs; or, they may forget to write the additional code, in which case unexpected errors may be thrown at runtime. In contrast, defining errors out of existence simplifies APIs and it reduces the amount of code that must be written.

当我主张通过设计来规避异常时,人们有时会反驳说抛出异常会捕捉到 bug。如果异常都被设计规避了,那会不会导致古怪的软件出现?也许这就是 Java 开发人员任务 substring 方法应该抛出异常。尽量抛出异常的方式可能会捕获一些错误,但也会增加复杂性,从而导致其他错误。在尽量抛出异常的方式中,开发人员必须编写额外的代码来避免或忽略错误,这增加了出现 bug 的可能性。或者,他们可能会忘记编写额外的代码,在这种情况下,运行时可能会抛出意外的异常。相比之下,通过设计来规避异常将简化 API,并减少必须编写的代码量。

Overall, the best way to reduce bugs is to make software simpler.

总体而言,减少 bug 最好方法是简化软件。

10.6 Mask exceptions 屏蔽异常

The second technique for reducing the number of places where exceptions must be handled is exception masking. With this approach, an exceptional condition is detected and handled at a low level in the system, so that higher levels of software need not be aware of the condition. Exception masking is particularly common in distributed systems. For instance, in a network transport protocol such as TCP, packets can be dropped for various reasons such as corruption and congestion. TCP masks packet loss by resending lost packets within its implementation, so all data eventually gets through and clients are unaware of the dropped packets.

减少必须处理异常的地方数量的第二种技术是异常屏蔽。使用这种方法,可以在系统的较低级别上检测和处理异常情况,因此,更高级别的软件无需知道该情况。异常屏蔽在分布式系统中尤其常见。例如,在诸如 TCP 的网络传输协议中,由于各种原因(例如损坏和拥塞),可能会丢弃数据包。TCP 在其实现中通过重新发送丢失的数据包来掩盖数据包的丢失,因此所有数据最终都将送达,并且客户端不会察觉到丢失的数据包。

A more controversial example of masking occurs in the NFS network file system. If an NFS file server crashes or fails to respond for any reason, clients reissue their requests to the server over and over again until the problem is eventually resolved. The low-level file system code on the client does not report any exceptions to the invoking application. The operation in progress (and hence the application) just hangs until the operation can complete successfully. If the hang lasts more than a short time, the NFS client prints messages on the user’s console of the form “NFS server xyzzy not responding still trying.”

NFS 网络文件系统中出现了一个更具争议性的屏蔽异常的示例。如果 NFS 文件服务器由于任何原因崩溃或无法响应,客户端将一遍又一遍地向服务器发出请求,直到问题最终得到解决。客户端上的低级文件系统代码不会向调用应用程序报告任何异常。执行该操作的进程(及应用程序)只是挂起,直到操作可以成功完成。如果挂起持续的时间超过一小段时间,则 NFS 客户端将在用户控制台上输出“ NFS 服务器 xyzzy 无法响应仍在尝试访问” 之类的消息。

NFS users often complain about the fact that their applications hang while waiting for an NFS server to resume normal operation. Many people have suggested that NFS should abort operations with an exception rather than hanging. However, reporting exceptions would make things worse, not better. There’s not much an application can do if it loses access to its files. One possibility would be for the application to retry the file operation, but this would still hang the application, and it’s easier to perform the retry in one place in the NFS layer, rather than at every file system call in every application (a compiler shouldn’t have to worry about this!). The other alternative is for applications to abort and return errors to their callers. It’s unlikely that the callers would know what to do either, so they would abort as well, resulting in a collapse of the user’s working environment. Users still wouldn’t be able to get any work done while the file server was down, and they would have to restart all of their applications once the file server came back to life.

NFS用户经常抱怨他们的应用程序在等待NFS服务器恢复正常运行时挂起。许多人建议 NFS 应该异常终止操作并抛出异常而不是挂起。但是,报告异常会使情况更糟,而不是更好。应用程序在无法访问其文件的情况下也没什么好做的。一种可能性是应用程序重试文件操作,但这仍然会使应用程序挂起,并且在 NFS 层级中一个位置执行重试会比在每个应用程序中的每个文件系统调用处执行重试更容易(编译器应不必为此担心!)。另一种选择是让应用程序中止并将错误返回给调用者。调用者不太可能知道该怎么做,因此他们也将中止,导致用户工作环境崩溃。用户在文件服务器关闭时仍然无法完成任何工作,并且一旦文件服务器恢复工作,他们将不得不重新启动所有应用程序。

Thus, the best alternative is for NFS to mask the errors and hang applications. With this approach, applications don’t need any code to deal with server problems, and they can resume seamlessly once the server comes back to life. If users get tired of waiting, they can always abort applications manually.

因此,最好的替代方法是让 NFS 掩盖错误并挂起应用程序。通过这种方法,应用程序不需要任何代码来处理服务器问题,并且一旦服务器恢复运行,它们就可以无缝恢复。如果用户厌倦了等待,他们总是可以手动中止应用程序。

Exception masking doesn’t work in all situations, but it is a powerful tool in the situations where it works. It results in deeper classes, since it reduces the class’s interface (fewer exceptions for users to be aware of) and adds functionality in the form of the code that masks the exception. Exception masking is an example of pulling complexity downward.

异常屏蔽并非在所有情况下都有效,但是在它起作用的情况下它是一个强大的工具。它导致了更深的类,因为它减少了类的界面(用户需要注意的异常更少)并以掩盖异常的代码形式添加了功能。异常屏蔽是降低复杂性的一个例子。

10.7 Exception aggregation 异常聚合

The third technique for reducing complexity related to exceptions is exception aggregation. The idea behind exception aggregation is to handle many exceptions with a single piece of code; rather than writing distinct handlers for many individual exceptions, handle them all in one place with a single handler.

减少与异常相关的复杂性的第三种技术是异常聚合。异常聚合的思想是用一个代码段处理许多异常。与其为多个单独的异常编写不同的处理程序,不如用一个处理程序在一个地方将它们全部处理。

Consider how to handle missing parameters in a Web server. A Web server implements a collection of URLs. When the server receives an incoming URL, it dispatches to a URL-specific service method to process that URL and generate a response. The URL contains various parameters that are used to generate the response. Each service method will call a lower-level method (let’s call it getParameter) to extract the parameters that it needs from the URL. If the URL does not contain the desired parameter, getParameter throws an exception.

考虑如何处理 Web 服务器中缺少的参数的情况。Web 服务器实现 URL 的集合。服务器收到传入的 URL 时,会将分派到特定的服务方法来处理该 URL 并生成响应。该 URL 包含用于生成响应的各种参数。每个服务方法都将调用一个较低层的方法(将其称为 getParameter)以从 URL 中提取所需的参数。如果 URL 不包含所需的参数,则 getParameter 会抛出异常。

When students in a software design class implemented such a server, many of them wrapped each distinct call to getParameter in a separate exception handler to catch NoSuchParameter exceptions, as in Figure 10.1. This resulted in a large number of handlers, all of which did essentially the same thing (generate an error response).

当参加软件设计课程的学生实现这样的服务器时,他们中的许多人将对 getParameter 的每个不同调用包装在单独的异常处理程序中以捕获 NoSuchParameter 异常,如图 10.1 所示。这导致大量的处理程序,所有这些处理程序基本上都执行相同的操作(生成错误响应)。

Figure 10.1: The code at the top dispatches to one of several methods in a Web server, each of which handles a particular URL. Each of those methods (bottom) uses parameters from the incoming HTTP request. In this figure, there is a separate exception handler for each call to getParameter; this results in duplicated code.

图 10.1:顶部的代码将分派给 Web 服务器中的几种方法之一,每种方法都处理一个特定的 URL。每个方法(底部)都使用传入 HTTP 请求中的参数。在此图中,每个对 getParameter 的调用都有一个单独的异常处理程序。这导致重复的代码。

A better approach is to aggregate the exceptions. Instead of catching the exceptions in the individual service methods, let them propagate up to the top-level dispatch method for the Web server, as in Figure 10.2. A single handler in this method can catch all of the exceptions and generate an appropriate error response for missing parameters.

更好的方法是汇总异常。让它们传播到 Web 服务器的顶级调度方法,而不是在单个服务方法中捕获异常,如图 10.2 所示。此方法中的单个处理程序可以捕获所有异常,并为丢失的参数生成适当的错误响应。

The aggregation approach can be taken even further in the Web example. There are many other errors besides missing parameters that can occur while processing a Web page; for example, a parameter might not have the right syntax (the service method expected an integer, but the value was “xyz”), or the user might not have permission for the requested operation. In each case, the error should result in an error response; the errors differ only in the error message to include in the response (“parameter 'quantity' not present in URL” or “bad value 'xyz' for 'quantity' parameter; must be positive integer”). Thus, all conditions resulting in an error response can be handled with a single top-level exception handler. The error message can be generated at the time the exception is thrown and included as a variable in the exception record; for example, getParameter will generate the “parameter 'quantity' not present in URL” message. The top-level handler extracts the message from the exception and incorporates it into the error response.

聚合异常的方式可以在 Web 示例中更进一步。处理网页时,除了缺少参数外,还有许多其他错误;例如,参数可能没有正确的类型(服务方法期望的参数时整数,但值为“ xyz”),或者用户可能无权执行所请求的操作。在每种情况下,错误都应导致错误响应。错误仅在响应中包含的错误消息中有所不同(“ URL 中不存在参数'quantity'” 或 “'quantity'参数的值 'xyz' 不正确;必须为正整数”)。因此,所有导致错误响应的条件都可以使用单个顶级异常处理程序进行处理。错误消息可以在引发异常时生成,并作为变量包含在异常记录中。例如,getParameter 将生成“ URL 中不存在的参数'quantity'”消息。顶级处理程序从异常中提取消息,并将其合并到错误响应中。

Figure 10.2: This code is functionally equivalent to Figure 10.1, but exception handling has been aggregated: a single exception handler in the dispatcher catches all of the NoSuchParameter exceptions from all of the URL-specific methods.

图 10.2:此代码在功能上等效于图 10.1,但是异常处理已聚合:分派器中的单个异常处理程序从所有特定于 URL 的方法中捕获所有 NoSuchParameter 异常。

The aggregation described in the preceding paragraph has good properties from the standpoint of encapsulation and information hiding. The top-level exception handler encapsulates knowledge about how to generate error responses, but it knows nothing about specific errors; it just uses the error message provided in the exception. The getParameter method encapsulates knowledge about how to extract a parameter from a URL, and it also knows how to describe extraction errors in a human-readable form. These two pieces of information are closely related, so it makes sense for them to be in the same place. However, getParameter knows nothing about the syntax of an HTTP error response. As new functionality is added to the Web server, new methods like getParameter may be created with their own errors. If the new methods throw exceptions in the same way as getParameter (by generating exceptions that inherit from the same superclass and including an error message in each exception), they can plug into the existing system with no other changes: the top-level handler will automatically generate error responses for them.

从封装和信息隐藏的角度来看,上一段中描述的异常聚合具有良好的属性。顶级异常处理程序封装了有关如何生成错误响应的知识,但对特定错误一无所知。它仅使用异常中提供的错误消息。getParameter 方法封装了有关如何从 URL 提取参数的知识,并且还知道如何以人类可读的形式描述提取的错误。这两个信息密切相关,因此将它们放在同一位置是说得通的。但是,getParameter 对 HTTP 错误响应的语法一无所知。随着向 Web 服务器中添加了新功能,可能会创建具有类似 getParameter 有自己的异常的新方法。如果新方法抛出异常的方式和 getParameter 一样(继承自同一基类并且包含错误信息),现存系统不用做任何更改就可以集成新的方法:顶级异常处理程序会自动为新方法生成相应的错误响应。

This example illustrates a generally-useful design pattern for exception handling. If a system processes a series of requests, it’s useful to define an exception that aborts the current request, cleans up the system’s state, and continues with the next request. The exception is caught in a single place near the top of the system’s request-handling loop. This exception can be thrown at any point in the processing of a request to abort the request; different subclasses of the exception can be defined for different conditions. Exceptions of this type should be clearly distinguished from exceptions that are fatal to the entire system.

此示例说明了用于异常处理的通用设计模式。如果系统处理一系列请求,则定义一个异常以中止当前请求,清除系统状态并继续下一个请求非常有用。异常被捕获在系统请求处理循环顶部附近的单个位置。在处理中止请求的任何时候都可以抛出异常。可以为不同的条件定义异常的不同子类。应该将这种类型的异常与对整个系统致命的异常区分开来。

Exception aggregation works best if an exception propagates several levels up the stack before it is handled; this allows more exceptions from more methods to be handled in the same place. This is the opposite of exception masking: masking usually works best if an exception is handled in a low-level method. For masking, the low-level method is typically a library method used by many other methods, so allowing the exception to propagate would increase the number of places where it is handled. Masking and aggregation are similar in that both approaches position an exception handler where it can catch the most exceptions, eliminating many handlers that would otherwise need to be created.

如果异常在被处理之前在堆栈中传播到了多个级别,则异常集合最有效。这允许在同一个地方处理来自更多方法的更多异常。这与异常屏蔽相反:异常屏蔽通常在异常被低级代码处理的情况下效果最好。对于异常屏蔽,低级方法通常是被许多其他方法使用的库方法,因此,允许传播异常会增加需要处理该异常的位置数量。异常屏蔽和异常聚合的相似之处在于,这两种方式都将异常处理程序置于可以捕获最多异常的位置,从而消除了许多本来需要创建的异常处理程序。

Another example of exception aggregation occurs in the RAMCloud storage system for crash recovery. A RAMCloud system consists of a collection of storage servers that keep multiple copies of each object, so the system can recover from a variety of failures. For example, if a server crashes and loses all of its data, RAMCloud reconstructs the lost data using copies stored on other servers. Errors can also happen on a smaller scale; for example, a server may discover that an individual object is corrupted.

异常聚合的另一个例子是 RAMCloud 存储系统崩溃恢复。RAMCloud 系统由一组存储服务器组成,这些存储服务器保留每个对象的多个副本,因此系统可以从各种故障中恢复。例如,如果服务器崩溃并丢失其所有数据,RAMCloud 会使用存储在其他服务器上的副本来重建丢失的数据。错误也可能在较小的范围内发生。例如,服务器可能发现单个对象已损坏。

RAMCloud does not have separate recovery mechanisms for each different kind of error. Instead, RAMCloud “promotes” many smaller errors into larger ones. RAMCloud could, in principle, handle a corrupted object by restoring that one object from a backup copy. However, it doesn’t do this. Instead, if it discovers a corrupted object it crashes the server containing the object. RAMCloud uses this approach because crash recovery is quite complex and this approach minimized the number of different recovery mechanisms that had to be created. Creating a recovery mechanism for crashed servers was unavoidable, so RAMCloud uses the same mechanism for other kinds of recovery as well. This reduced the amount of code that had to be written, and it also meant that server crash recovery gets invoked more often. As a result, bugs in recovery are more likely to be discovered and fixed.

对于每种不同类型的错误,RAMCloud 没有单独的恢复机制。相反,RAMCloud 将许多较小的错误“提升”为较大的错误。原则上,RAMCloud 可以通过从备份副本中恢复一个损坏的对象来处理这个损坏的对象。然而,它并不这样做。相反,如果它发现一个损坏的对象,它会使包含该对象的服务器崩溃。RAMCloud 使用这种方法是因为崩溃恢复非常复杂,而且这种方法最小化了必须创建的不同恢复机制的数量。为崩溃的服务器创建恢复机制是不可避免的,因此 RAMCloud 对其他类型的恢复也使用相同的机制。这减少了必须编写的代码量,而且这还意味着服务器崩溃恢复将更频繁地被调用。因此,恢复中的 bug 更有可能被发现和修复。

One disadvantage of promoting a corrupted object into a server crash is that it increases the cost of recovery considerably. This is not a problem in RAMCloud, since object corruption is quite rare. However, error promotion may not make sense for errors that happen frequently. As one example, it would not be practical to crash a server anytime one of its network packets is lost.

将损坏的对象升级为服务器崩溃的一个缺点是,它大大增加了恢复成本。这在 RAMCloud 中不是问题,因为对象损坏非常罕见。但是,错误升级对于经常发生的错误可能没有意义。举一个例子,在服务器的任何网络数据包丢失时使服务器崩溃是不切实际的。

One way of thinking about exception aggregation is that it replaces several special-purpose mechanisms, each tailored for a particular situation, with a single general-purpose mechanism that can handle multiple situations. This provides another illustration of the benefits of general-purpose mechanisms.

考虑异常聚合的一种方法是,它用可以处理多种情况的单个通用机制替换了几种针对特定情况而量身定制的特殊用途的机制。这再次说明了通用机制的好处。

10.8 Just crash? 让程序崩溃?

The fourth technique for reducing complexity related to exception handling is to crash the application. In most applications there will be certain errors that it’s not worth trying to handle. Typically, these errors are difficult or impossible to handle and don’t occur very often. The simplest thing to do in response to these errors is to print diagnostic information and then abort the application.

减少与异常处理相关的复杂性的第四种技术是使应用程序崩溃。在大多数应用程序中,有些错误是不值去处理的。通常,这些错误很难或不可能处理,而且很少发生。针对这些错误的最简单的操作是打印诊断信息,然后中止应用程序。

One example is “out of memory” errors that occur during storage allocation. Consider the malloc function in C, which returns NULL if it cannot allocate the desired block of memory. This is an unfortunate behavior, because it assumes that every single caller of malloc will check the return value and take appropriate action if there is no memory. Applications contain numerous calls to malloc, so checking the result after each call would add significant complexity. If a programmer forgets the check (which is fairly likely), then the application will dereference a null pointer if memory runs out, resulting in a crash that camouflages the real problem.

一个示例是在存储分配期间发生的“内存不足”错误。考虑一下 C 语言中的 malloc 函数,如果它无法分配所需的内存块,则该函数将返回 NULL。这是一个不合适的行为,因为它假定 malloc 的每个调用者都将检查返回值并在没有内存的情况下采取适当的措施。应用程序包含许多对 malloc 的调用,因此在每次调用后检查结果将增加相当大的复杂性。如果程序员忘记了检查(这很有可能),那么如果内存用完,应用程序将取消引用空指针,从而导致崩溃,从而掩盖了实际问题。

Furthermore, there isn’t much an application can do when it discovers that memory is exhausted. In principle the application could look for unneeded memory to free, but if the application had unneeded memory it could already have freed it, which would have prevented the out-of-memory error in the first place. Today’s systems have so much memory that memory almost never runs out; if it does, it usually indicates a bug in the application. Thus, it rarely make sense to try to handle out-of-memory errors; this creates too much complexity for too little benefit.

此外,当应用程序发现内存已用完时,它也没什好做的了。原则上,应用程序可以寻找不需要的内存以释放它,但是,如果应用程序有不需要的内存,它可能已经释放了它,这将首先防止内存不足的错误。当今的系统具有如此大的内存,以至于内存几乎永远不会耗尽。如果是这样,通常表明应用程序中存在 bug。因此,尝试处理内存不足错误几乎没有道理。这会带来太多的复杂性,而带来的收益却太少。

A better approach is to define a new method ckalloc, which calls malloc, checks the result, and aborts the application with an error message if memory is exhausted. The application never invokes malloc directly; it always invokes ckalloc.

更好的方法是定义一个新的 ckalloc 方法,该方法调用 malloc,检查结果,在内存耗尽时中止应用程序并输出错误消息。该应用程序从不直接调用 malloc。它总是调用 ckalloc。

In newer languages such as C++ and Java, the new operator throws an exception if memory is exhausted. There’s not much point in catching this exception, since there’s a good chance that the exception handler will also try to allocate memory, which will also fail. Dynamically allocated memory is such a fundamental element of any modern application that it doesn’t make sense for the application to continue if memory is exhausted; it’s better to crash as soon as the error is detected.

在较新的语言(例如 C++ 和 Java)中,如果内存耗尽,则 new 运算符将引发异常。捕获此异常没有什么意义,因为异常处理程序很有可能还会尝试分配内存,这也会失败。动态分配的内存是任何现代应用程序中的基本元素,如果内存耗尽,则继续应用程序是没有意义的。最好在检测到错误后立即崩溃。

There are many other examples of errors where crashing the application makes sense. For most programs, if an I/O error occurs while reading or writing an open file (such as a disk hard error), or if a network socket cannot be opened, there’s not much the application can do to recover, so aborting with a clear error message is a sensible approach. These errors are infrequent, so they are unlikely to affect the overall usability of the application. Aborting with an error message is also appropriate if an application encounters an internal error such as an inconsistent data structure. Conditions like this probably indicate bugs in the program.

还有许多其他错误示例,当这些错误出现时使应用程序崩溃是说得通的。对于大多数程序,如果在读取或写入打开的文件时发生 I/O 错误(例如磁盘硬错误),或者无法打开网络套接字,则应用程序没有什么办法从在错误中恢复,因此中止程序并输出清晰的错误信息是明智之举。这些错误很少发生,因此它们不太可能影响应用程序的整体可用性。如果应用程序遇到内部错误(如数据结构不一致),则中止程序并输出清晰的错误信息也是合适的。这样的情况可能表明程序中存在 bug。

Whether or not it is acceptable to crash on a particular error depends on the application. For a replicated storage system, it isn’t appropriate to abort on an I/O error. Instead, the system must use replicated data to recover any information that was lost. The recovery mechanisms will add considerable complexity to the program, but recovering lost data is an essential part of the value the system provides to its users.

当特定错误出现时应用程序崩溃是否可以接受取决于应用程序。对于复制的存储系统,不适合因 I/O 错误而中止。相反,系统必须使用复制的数据来恢复丢失的任何信息。恢复机制将给程序增加相当大的复杂性,但是恢复丢失的数据是系统为用户提供的价值的重要组成部分。

10.9 Design special cases out of existence 通过设计规避特殊情况

For the same reason that it makes sense to define errors out of existence, it also makes sense to define other special cases out of existence. Special cases can result in code that is riddled with if statements, which make the code hard to understand and lead to bugs. Thus, special cases should be eliminated wherever possible. The best way to do this is by designing the normal case in a way that automatically handles the special cases without any extra code.

通过定义规避错误是说得通的,出于同样的原因,通过设计规避特殊情况也是说得通的。特殊情况可能导致代码中混入 if 语句,这使代码难以理解并导致错误。因此,应尽可能消除特殊情况。做到这一点的最好方法是以一种无需任何额外代码就能自动处理特殊情况的方式来设计正常情况。

In the text editor project described in Chapter 6, students had to implement a mechanism for selecting text and copying or deleting the selection. Most students introduced a state variable in their selection implementation to indicate whether or not the selection exists. They probably chose this approach because there are times when no selection is visible on the screen, so it seemed natural to represent this notion in the implementation. However, this approach resulted in numerous checks to detect the “no selection” condition and handle it specially.

在第 6 章中描述的文本编辑器项目中,学生必须实现一种选择文本以及复制或删除所选内容的机制。大多数学生在他们的选择实现中引入了状态变量,以表明选择是否存在。他们之所以选择这种方法,是因为有时屏幕上看不到任何选择,因此在实现中似乎很自然地代表了这一概念。但是,这种方法导致了大量的检查,以检测“没有选择”的情况,并专门处理它。。

The selection handling code can be simplified by eliminating the “no selection” special case, so that the selection always exists. When there is no selection visible on the screen, it can be represented internally with an empty selection, whose starting and ending positions are the same. With this approach, the selection management code can be written without any checks for “no selection”. When copying the selection, if the selection is empty then 0 bytes will be inserted at the new location (if implemented correctly, there will be no need to check for 0 bytes as a special case). Similarly, it should be possible to design the code for deleting the selection so that the empty case is handled without any special-case checks. Consider a selection all on a single line. To delete the selection, extract the portion of the line preceding the selection and concatenate it with the portion of the line following the selection to form the new line. If the selection is empty, this approach will regenerate the original line.

通过消除“不选择”的特殊情况,可以简化选择处理代码,从而使选择始终存在。当屏幕上没有可见的选择时,可以在内部用空的选择表示,其开始和结束位置相同。使用这种方法,可以编写选择管理代码,而无需对“不选择”进行任何检查。复制所选内容时,如果所选内容为空,则将在新位置插入 0 字节(如果正确实现,则在特殊情况下无需检查 0 字节)。同样,应该有可能设计用于删除选择的代码,以便无需任何特殊情况检查就可以处理空情况。考虑选择一整行的情况。要删除选择,提取选择之前的行的一部分,并将其与选择之后的行的部分连接起来以形成新行。如果选择为空,则此方法将重新生成原始行。

This example also illustrates the “different layer, different abstraction” idea from Chapter 7. The notion of “no selection” makes sense in terms of how the user thinks about the application’s interface, but that doesn’t mean it has to be represented explicitly inside the application. Having a selection that always exists, but is sometimes empty and thus invisible, results in a simpler implementation.

此示例还说明了第 7 章中的“不同的层,不同的抽象”概念。“无选择”的概念在用户对应用程序界面的看法方面很有意义,但这并不意味着必须明确在应用程序内部表示它。选择总是存在的,但有时是空的,因此是不可见的,这样可以简化实现。

10.10 Taking it too far 做过头了

Defining away exceptions, or masking them inside a module, only makes sense if the exception information isn’t needed outside the module. This was true for the examples in this chapter, such the Tcl unset command and the Java substring method; in the rare situations where a caller cares about the special cases detected by the exceptions, there are other ways for it to get this information.

通过定义规避错误或将其屏蔽在模块内部,仅在模块外部不需要异常信息时才有意义。对于本章中的示例,例如 Tcl unset 命令和 Java 子字符串方法,都是如此。在极少数情况下,调用者关心异常检测到的特殊情况,还有其他方法可以获取此信息。

However, it is possible to take this idea too far. In a module for network communication, a student team masked all network exceptions: if a network error occurred, the module caught it, discarded it, and continued as if there were no problem. This meant that applications using the module had no way to find out if messages were lost or a peer server failed; without this information, it was impossible to build robust applications. In this case, it is essential for the module to expose the exceptions, even though they add complexity to the module’s interface.

但是,有时候会做的过头。在用于网络通信的模块中,一个学生团队掩盖了所有网络异常:如果发生网络错误,则模块将其捕获,丢弃并继续进行,就好像没有问题一样。这意味着使用该模块的应用程序无法确定消息是否丢失或节点服务器是否发生故障;没有这些信息,就不可能构建健壮的应用程序。在这种情况下,模块必须公开异常,即使它们增加了模块接口的复杂性。

With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed.

异常与软件设计中的许多其他领域一样,您必须确定哪些是重要的,哪些是不重要的。不重要的事物应该被隐藏起来,它们越多越好。但是,当某件事很重要时,必须将其暴露出来。

10.11 Conclusion 结论

Special cases of any form make code harder to understand and increase the likelihood of bugs. This chapter focused on exceptions, which are one of the most significant sources of special-case code, and discussed how to reduce the number of places where exceptions must be handled. The best way to do this is by redefining semantics to eliminate error conditions. For exceptions that can’t be defined away, you should look for opportunities to mask them at a low level, so their impact is limited, or aggregate several special-case handlers into a single more generic handler. Together, these techniques can have a significant impact on overall system complexity.

任何形式的特殊情况都使代码更难以理解,并增加了发生 bug 的可能性。本章重点讨论异常,异常是特殊情况代码的最重要来源之一,并讨论了如何减少必须处理异常的地方的数量。做到这一点的最佳方法是重新定义语义以消除错误条件。对于无法通过设计规避的异常,您应该寻找机会将它们在底层屏蔽,以免影响有限,或者将多个特殊情况的处理程序聚合到一个更通用的处理程序中。总之,这些技术会对整个系统的复杂性产生重大影响。

1 Ding Yuan et. al., “Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems,” 2014 USENIX Conference on Operating System Design and Implementation.

1 丁元等 等人,“简单的测试可以防止最关键的故障:对分布式数据密集型系统中的生产故障的分析”,2014 USENIX 操作系统设计和实施大会。

',135)]))}const h=t(r,[["render",l],["__file","ch10.html.vue"]]),d=JSON.parse('{"path":"/ch10.html","title":"第 10 章 通过定义规避错误","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"10.1 Why exceptions add complexity 为什么异常会增加复杂性","slug":"_10-1-why-exceptions-add-complexity-为什么异常会增加复杂性","link":"#_10-1-why-exceptions-add-complexity-为什么异常会增加复杂性","children":[]},{"level":2,"title":"10.2 Too many exceptions 异常过多","slug":"_10-2-too-many-exceptions-异常过多","link":"#_10-2-too-many-exceptions-异常过多","children":[]},{"level":2,"title":"10.3 Define errors out of existence 通过定义规避错误","slug":"_10-3-define-errors-out-of-existence-通过定义规避错误","link":"#_10-3-define-errors-out-of-existence-通过定义规避错误","children":[]},{"level":2,"title":"10.4 Example: file deletion in Windows 示例:Windows 中的文件删除","slug":"_10-4-example-file-deletion-in-windows-示例-windows-中的文件删除","link":"#_10-4-example-file-deletion-in-windows-示例-windows-中的文件删除","children":[]},{"level":2,"title":"10.5 Example: Java substring method 示例:Java 子字符串方法","slug":"_10-5-example-java-substring-method-示例-java-子字符串方法","link":"#_10-5-example-java-substring-method-示例-java-子字符串方法","children":[]},{"level":2,"title":"10.6 Mask exceptions 屏蔽异常","slug":"_10-6-mask-exceptions-屏蔽异常","link":"#_10-6-mask-exceptions-屏蔽异常","children":[]},{"level":2,"title":"10.7 Exception aggregation 异常聚合","slug":"_10-7-exception-aggregation-异常聚合","link":"#_10-7-exception-aggregation-异常聚合","children":[]},{"level":2,"title":"10.8 Just crash? 让程序崩溃?","slug":"_10-8-just-crash-让程序崩溃","link":"#_10-8-just-crash-让程序崩溃","children":[]},{"level":2,"title":"10.9 Design special cases out of existence 通过设计规避特殊情况","slug":"_10-9-design-special-cases-out-of-existence-通过设计规避特殊情况","link":"#_10-9-design-special-cases-out-of-existence-通过设计规避特殊情况","children":[]},{"level":2,"title":"10.10 Taking it too far 做过头了","slug":"_10-10-taking-it-too-far-做过头了","link":"#_10-10-taking-it-too-far-做过头了","children":[]},{"level":2,"title":"10.11 Conclusion 结论","slug":"_10-11-conclusion-结论","link":"#_10-11-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch10.md"}');export{h as comp,d as data}; +

Just the basic try-catch boilerplate accounts for more lines of code than the code for normal-case operation, without even considering the code that actually handles the exceptions. It is hard to relate the exception handling code to the normal-case code: for example, it’s not obvious where each exception is generated. An alternative approach is to break up the code into many distinct try blocks; in the extreme case there could be a try for each line of code that can generate an exception. This would make it clear where exceptions occur, but the try blocks themselves break up the flow of the code and make it harder to read; in addition, some exception handling code might end up duplicated in multiple try blocks.

只是基本的 try-catch 样板代码比正常情况下的操作代码所占的代码行更多,甚至没有考虑实际处理异常的代码。很难将异常处理代码与普通情况代码相关联:例如,每个异常的生成位置都不明显。另一种方法是将代码分解为许多不同的 try 块。在极端情况下,每行可能产生异常的代码都需要单独的 try 块。这样可以清楚地说明异常发生的位置,但是 try 块本身会破坏代码流,并使代码难以阅读。此外,某些异常处理代码可能最终会在多个 try 块中重复。

It’s difficult to ensure that exception handling code really works. Some exceptions, such as I/O errors, can’t easily be generated in a test environment, so it’s hard to test the code that handles them. Exceptions don’t occur very often in running systems, so exception handling code rarely executes. Bugs can go undetected for a long time, and when the exception handling code is finally needed, there’s a good chance that it won’t work (one of my favorite sayings: “code that hasn’t been executed doesn’t work”). A recent study found that more than 90% of catastrophic failures in distributed data-intensive systems were caused by incorrect error handling1. When exception handling code fails, it’s difficult to debug the problem, since it occurs so infrequently.

确保异常处理代码是否会真正起作用是困难的。某些异常(例如 I/O 错误)在测试环境中不易生成,因此很难测试处理它们的代码。异常在运行的系统中很少发生,因此异常处理代码很少执行。错误可能会长时间未被发现,并且当最终需要异常处理代码时,它很有可能无法正常工作(我最喜欢的一句话是:“未执行的代码无效”) 。最近的一项研究发现,分布式数据密集型系统中超过 90%的灾难性故障是由错误的错误处理引起的 1。当异常处理代码失败时,很难调试该问题,因为它很少发生。

10.2 Too many exceptions 异常过多

Programmers exacerbate the problems related to exception handling by defining unnecessary exceptions. Most programmers are taught that it’s important to detect and report errors; they often interpret this to mean “the more errors detected, the better.” This leads to an over-defensive style where anything that looks even a bit suspicious is rejected with an exception, which results in a proliferation of unnecessary exceptions that increase the complexity of the system.

程序员通过定义不必要的异常加剧了与异常处理有关的问题。大多数程序员被教导检测和报告错误很重要。他们通常将其解释为“检测到的错误越多越好”。这导致了一种过度防御的风格,任何看起来有点可疑的东西都会被异常拒绝,从而导致不必要的异常激增,增加了系统的复杂性。

I made this mistake myself in the design of the Tcl scripting language. Tcl contains an unset command that can be used to remove a variable. I defined unset so that it throws an error if the variable doesn’t exist. At the time I thought that it must be a bug if someone tries to delete a variable that doesn’t exist, so Tcl should report it. However, one of the most common uses of unset is to clean up temporary state created by some previous operation. It’s often hard to predict exactly what state was created, particularly if the operation aborted partway through. Thus, the simplest thing is to delete all of the variables that might possibly have been created. The definition of unset makes this awkward: developers end up enclosing calls to unset in catch statements to catch and ignore errors thrown by unset. In retrospect, the definition of the unset command is one of the biggest mistakes I made in the design of Tcl.

在设计 Tcl 脚本语言时,我自己就犯了这个错误。Tcl 包含一个 unset 命令,可用于删除变量。我定义的 unset 会在变量不存在时抛出错误。当时我认为,如果有人试图删除一个不存在的变量,那么它一定是一个 bug,所以 Tcl 应该报告它。然而,unset 最常见的用途之一是清理以前操作创建的临时状态。通常很难准确预测创建了什么状态,尤其是如果操作中途中止。因此,最简单的方法是删除可能已经创建的所有变量。unset 的定义使得这种情况很尴尬:开发人员最终会在 catch 语句中再使用 try catch 以捕获并忽略 unset 抛出的错误。回顾过去,unset 命令的设计是我在 Tcl 设计中犯下的最大错误之一。

It’s tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller. Some might argue that this approach empowers callers, since it allows each caller to handle the exception in a different way. However, if you are having trouble figuring out what to do for the particular situation, there’s a good chance that the caller won’t know what to do either. Generating an exception in a situation like this just passes the problem to someone else and adds to the system’s complexity.

使用异常来避免处理困难的情况是很诱人的:与其想出一种干净的方法来处理它,不如抛出一个异常并将问题转移给调用者。有人可能会争辩说,这种方法可以赋予调用者权力,因为它允许每个调用者以不同的方式处理异常。然而,如果你不知道做什么去处理特殊情况,调用者也很有可能不知道该做什么。在这种情况下生成异常只会将问题传递给其他人,并增加系统的复杂性。

The exceptions thrown by a class are part of its interface; classes with lots of exceptions have complex interfaces, and they are shallower than classes with fewer exceptions. An exception is a particularly complex element of an interface. It can propagate up through several stack levels before being caught, so it affects not just the method’s caller, but potentially also higher-level callers (and their interfaces).

类抛出的异常是其接口的一部分;具有大量异常的类具有复杂的接口,并且比具有较少异常的类浅。异常是接口中特别复杂的元素。它可以在被捕获之前通过多个堆栈级别向上传播,因此它不仅影响方法的调用者,而且还可能影响更高级别的调用者(及其接口)。

Throwing exceptions is easy; handling them is hard. Thus, the complexity of exceptions comes from the exception handling code. The best way to reduce the complexity damage caused by exception handling is to reduce the number of places where exceptions have to be handled. The rest of this chapter will discuss four techniques for reducing the number of exception handlers.

抛出异常很容易;处理它们很困难。因此,异常的复杂性来自异常处理代码。减少由异常处理引起的复杂性破坏的最佳方法是减少必须处理异常的位置的数量。本章的其余部分将讨论减少异常处理程序数量的四种技术。

10.3 Define errors out of existence 通过定义规避错误

The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence. This may seem sacrilegious, but it is very effective in practice. Consider the Tcl unset command discussed above. Rather than throwing an error when unset is asked to delete an unknown variable, it should have simply returned without doing anything. I should have changed the definition of unset slightly: rather than deleting a variable, unset should ensure that a variable no longer exists. With the first definition, unset can’t do its job if the variable doesn’t exist, so generating an exception makes sense. With the second definition, it is perfectly natural for unset to be invoked with the name of a variable that doesn’t exist. In this case, its work is already done, so it can simply return. There is no longer an error case to report.

消除异常处理复杂性的最好方法是设计好您的 API,使其没有异常要处理:这就是 通过定义规避错误。这看似亵渎神灵,但在实践中非常有效。考虑上面讨论的 Tcl unset 命令。当unset被要求删除一个未知变量时,它不应该抛出一个错误,而应该简单地返回而不做任何事情。我应该稍微修改一下 unset 的定义:与其删除一个变量,不如用来确保一个变量不再存在。根据第一个定义,如果变量不存在,则 unset 不能执行其工作,因此生成异常是说的通的。使用第二个定义,对不存在的变量名调用 unset 是很自然的。在这种情况下,它的工作已经完成,因此可以简单地返回。不再有错误需要上报。

10.4 Example: file deletion in Windows 示例:Windows 中的文件删除

File deletion provides another example of how errors can be defined away. The Windows operating system does not permit a file to be deleted if it is open in a process. This is a continual source of frustration for developers and users. In order to delete a file that is in use, the user must search through the system to find the process that has the file open, and then kill that process. Sometimes users give up and reboot their system, just so they can delete a file.

文件删除提供了如何通过定义规避错误另一个示例。Windows 操作系统不允许删除文件(如果已在进程中打开文件)。对于开发人员和用户来说,这是不断沮丧的根源。为了删除正在使用的文件,用户必须在系统中搜索以找到已打开文件的进程,然后终止该进程。有时用户放弃并重新启动系统,只是为了删除文件。

The Unix operating system defines file deletion more elegantly. In Unix, if a file is open when it is deleted, Unix does not delete the file immediately. Instead, it marks the file for deletion, then the delete operation returns successfully. The file name has been removed from its directory, so no other processes can open the old file and a new file with the same name can be created, but the existing file data persists. Processes that already have the file open can continue to read it and write it normally. Once the file has been closed by all of the accessing processes, its data is freed.

Unix 操作系统更优雅地定义了文件删除。在 Unix 中,如果在删除文件时打开了文件,则 Unix 不会立即删除该文件。而是将文件标记为删除,然后删除操作成功返回。该文件名已从其目录中删除,因此其他进程无法打开该旧文件,并且可以创建具有相同名称的新文件,但现有文件数据将保留。已经打开文件的进程可以继续读取和正常写入文件。一旦所有访问进程都关闭了文件,便释放其数据。

The Unix approach defines away two different kinds of errors. First, the delete operation no longer returns an error if the file is currently in use; the delete succeeds, and the file will eventually be deleted. Second, deleting a file that’s in use does not create exceptions for the processes using the file. One possible approach to this problem would have been to delete the file immediately and mark all of the opens of the file to disable them; any attempts by other processes to read or write the deleted file would fail. However, this approach would create new errors for those processes to handle. Instead, Unix allows them to keep accessing the file normally; delaying the file deletion defines errors out of existence.

Unix 删除文件的方式规避了两种不同的错误。首先,如果文件当前正在使用中,则删除操作不再返回错误;删除成功,该文件最终将被删除。其次,删除正在使用的文件不会使正在使用该文件的进程抛出异常。解决此问题的一种可能方法是立即删除文件并标记所有打开的文件以禁用它们。其他进程读取或写入已删除文件的任何尝试均将失败。但是,此方法将产生需要那些进程处理的新的错误。相反,Unix 允许他们继续正常访问文件。延迟文件删除规避了这个问题。

It may seem strange that Unix allows a process to continue to read and write a doomed file, but I have never encountered a situation where this caused significant problems. The Unix definition of file deletion is much simpler to work with, both for developers and users, than the Windows definition.

Unix 允许进程继续读取和写入已损坏的文件可能看起来很奇怪,但是我从未遇到过因此引起严重问题的情况。对于开发人员和用户,Unix 删除文件的设计比 Windows 的设计要容易得多。

10.5 Example: Java substring method 示例:Java 子字符串方法

As a final example, consider the Java String class and its substring method. Given two indexes into a string, substring returns the substring starting at the character given by the first index and ending with the character just before the second index. However, if either index is outside the range of the string, then substring throws IndexOutOfBoundsException. This exception is unnecessary and complicates the use of this method. I often find myself in a situation where one or both of the indices may be outside the range of the string, and I would like to extract all of the characters in the string that overlap the specified range. Unfortunately, this requires me to check each of the indices and round them up to zero or down to the end of the string; a one-line method call now becomes 5–10 lines of code.

作为最后一个示例,请考虑 Java String 类及其子字符串方法。给定一个字符串中的两个索引,substring 方法返回从第一个索引给定的字符开始,以第二个索引之前的字符结束的子字符串。但是,如果两个索引中的任何一个超出字符串的范围,substring 方法将抛出 IndexOutOfBoundsException。此异常是不必要的,并且会使此方法的使用复杂化。我经常发现自己处于一个或两个索引可能不在字符串范围内的情况,并且我想提取字符串中与指定范围重叠的所有字符。不幸的是,这要求我检查每个索引并将它们向上舍入为零或向下舍入到字符串的末尾。现在,单行方法调用变成 5-10 行代码。

The Java substring method would be easier to use if it performed this adjustment automatically, so that it implemented the following API: “returns the characters of the string (if any) with index greater than or equal to beginIndex and less than endIndex.” This is a simple and natural API, and it defines the IndexOutOfBoundsException exception out of existence. The method’s behavior is now well-defined even if one or both of the indexes are negative, or if beginIndex is greater than endIndex. This approach simplifies the API for the method while increasing its functionality, so it makes the method deeper. Many other languages have taken the error-free approach; for example, Python returns an empty result for out-of-range list slices.

如果 Java 子字符串方法自动执行此调整,则将更易于使用,因此它实现了以下 API:“返回索引大于或等于 beginIndex 且小于 endIndex 的字符串的字符(如果有)。” 这是一个简单自然的 API,它规避了 IndexOutOfBoundsException 异常。现在,即使一个或两个索引均为负,或者 beginIndex 大于 endIndex,该方法的行为也已明确定义。这种方法简化了方法的 API,同时增加了其功能,因此使方法更深。许多其他语言都采用了这种无错误的方式。例如,Python 对于超出范围的列表切片返回空结果。

When I argue for defining errors out of existence, people sometimes counter that throwing errors will catch bugs; if errors are defined out of existence, won’t that result in buggier software? Perhaps this is why the Java developers decided that substring should throw exceptions. The error-ful approach may catch some bugs, but it also increases complexity, which results in other bugs. In the error-ful approach, developers must write additional code to avoid or ignore the errors, and this increases the likelihood of bugs; or, they may forget to write the additional code, in which case unexpected errors may be thrown at runtime. In contrast, defining errors out of existence simplifies APIs and it reduces the amount of code that must be written.

当我主张通过设计来规避异常时,人们有时会反驳说抛出异常会捕捉到 bug。如果异常都被设计规避了,那会不会导致古怪的软件出现?也许这就是 Java 开发人员任务 substring 方法应该抛出异常。尽量抛出异常的方式可能会捕获一些错误,但也会增加复杂性,从而导致其他错误。在尽量抛出异常的方式中,开发人员必须编写额外的代码来避免或忽略错误,这增加了出现 bug 的可能性。或者,他们可能会忘记编写额外的代码,在这种情况下,运行时可能会抛出意外的异常。相比之下,通过设计来规避异常将简化 API,并减少必须编写的代码量。

Overall, the best way to reduce bugs is to make software simpler.

总体而言,减少 bug 最好方法是简化软件。

10.6 Mask exceptions 屏蔽异常

The second technique for reducing the number of places where exceptions must be handled is exception masking. With this approach, an exceptional condition is detected and handled at a low level in the system, so that higher levels of software need not be aware of the condition. Exception masking is particularly common in distributed systems. For instance, in a network transport protocol such as TCP, packets can be dropped for various reasons such as corruption and congestion. TCP masks packet loss by resending lost packets within its implementation, so all data eventually gets through and clients are unaware of the dropped packets.

减少必须处理异常的地方数量的第二种技术是异常屏蔽。使用这种方法,可以在系统的较低级别上检测和处理异常情况,因此,更高级别的软件无需知道该情况。异常屏蔽在分布式系统中尤其常见。例如,在诸如 TCP 的网络传输协议中,由于各种原因(例如损坏和拥塞),可能会丢弃数据包。TCP 在其实现中通过重新发送丢失的数据包来掩盖数据包的丢失,因此所有数据最终都将送达,并且客户端不会察觉到丢失的数据包。

A more controversial example of masking occurs in the NFS network file system. If an NFS file server crashes or fails to respond for any reason, clients reissue their requests to the server over and over again until the problem is eventually resolved. The low-level file system code on the client does not report any exceptions to the invoking application. The operation in progress (and hence the application) just hangs until the operation can complete successfully. If the hang lasts more than a short time, the NFS client prints messages on the user’s console of the form “NFS server xyzzy not responding still trying.”

NFS 网络文件系统中出现了一个更具争议性的屏蔽异常的示例。如果 NFS 文件服务器由于任何原因崩溃或无法响应,客户端将一遍又一遍地向服务器发出请求,直到问题最终得到解决。客户端上的低级文件系统代码不会向调用应用程序报告任何异常。执行该操作的进程(及应用程序)只是挂起,直到操作可以成功完成。如果挂起持续的时间超过一小段时间,则 NFS 客户端将在用户控制台上输出“ NFS 服务器 xyzzy 无法响应仍在尝试访问” 之类的消息。

NFS users often complain about the fact that their applications hang while waiting for an NFS server to resume normal operation. Many people have suggested that NFS should abort operations with an exception rather than hanging. However, reporting exceptions would make things worse, not better. There’s not much an application can do if it loses access to its files. One possibility would be for the application to retry the file operation, but this would still hang the application, and it’s easier to perform the retry in one place in the NFS layer, rather than at every file system call in every application (a compiler shouldn’t have to worry about this!). The other alternative is for applications to abort and return errors to their callers. It’s unlikely that the callers would know what to do either, so they would abort as well, resulting in a collapse of the user’s working environment. Users still wouldn’t be able to get any work done while the file server was down, and they would have to restart all of their applications once the file server came back to life.

NFS用户经常抱怨他们的应用程序在等待NFS服务器恢复正常运行时挂起。许多人建议 NFS 应该异常终止操作并抛出异常而不是挂起。但是,报告异常会使情况更糟,而不是更好。应用程序在无法访问其文件的情况下也没什么好做的。一种可能性是应用程序重试文件操作,但这仍然会使应用程序挂起,并且在 NFS 层级中一个位置执行重试会比在每个应用程序中的每个文件系统调用处执行重试更容易(编译器应不必为此担心!)。另一种选择是让应用程序中止并将错误返回给调用者。调用者不太可能知道该怎么做,因此他们也将中止,导致用户工作环境崩溃。用户在文件服务器关闭时仍然无法完成任何工作,并且一旦文件服务器恢复工作,他们将不得不重新启动所有应用程序。

Thus, the best alternative is for NFS to mask the errors and hang applications. With this approach, applications don’t need any code to deal with server problems, and they can resume seamlessly once the server comes back to life. If users get tired of waiting, they can always abort applications manually.

因此,最好的替代方法是让 NFS 掩盖错误并挂起应用程序。通过这种方法,应用程序不需要任何代码来处理服务器问题,并且一旦服务器恢复运行,它们就可以无缝恢复。如果用户厌倦了等待,他们总是可以手动中止应用程序。

Exception masking doesn’t work in all situations, but it is a powerful tool in the situations where it works. It results in deeper classes, since it reduces the class’s interface (fewer exceptions for users to be aware of) and adds functionality in the form of the code that masks the exception. Exception masking is an example of pulling complexity downward.

异常屏蔽并非在所有情况下都有效,但是在它起作用的情况下它是一个强大的工具。它导致了更深的类,因为它减少了类的界面(用户需要注意的异常更少)并以掩盖异常的代码形式添加了功能。异常屏蔽是降低复杂性的一个例子。

10.7 Exception aggregation 异常聚合

The third technique for reducing complexity related to exceptions is exception aggregation. The idea behind exception aggregation is to handle many exceptions with a single piece of code; rather than writing distinct handlers for many individual exceptions, handle them all in one place with a single handler.

减少与异常相关的复杂性的第三种技术是异常聚合。异常聚合的思想是用一个代码段处理许多异常。与其为多个单独的异常编写不同的处理程序,不如用一个处理程序在一个地方将它们全部处理。

Consider how to handle missing parameters in a Web server. A Web server implements a collection of URLs. When the server receives an incoming URL, it dispatches to a URL-specific service method to process that URL and generate a response. The URL contains various parameters that are used to generate the response. Each service method will call a lower-level method (let’s call it getParameter) to extract the parameters that it needs from the URL. If the URL does not contain the desired parameter, getParameter throws an exception.

考虑如何处理 Web 服务器中缺少的参数的情况。Web 服务器实现 URL 的集合。服务器收到传入的 URL 时,会将分派到特定的服务方法来处理该 URL 并生成响应。该 URL 包含用于生成响应的各种参数。每个服务方法都将调用一个较低层的方法(将其称为 getParameter)以从 URL 中提取所需的参数。如果 URL 不包含所需的参数,则 getParameter 会抛出异常。

When students in a software design class implemented such a server, many of them wrapped each distinct call to getParameter in a separate exception handler to catch NoSuchParameter exceptions, as in Figure 10.1. This resulted in a large number of handlers, all of which did essentially the same thing (generate an error response).

当参加软件设计课程的学生实现这样的服务器时,他们中的许多人将对 getParameter 的每个不同调用包装在单独的异常处理程序中以捕获 NoSuchParameter 异常,如图 10.1 所示。这导致大量的处理程序,所有这些处理程序基本上都执行相同的操作(生成错误响应)。

Figure 10.1: The code at the top dispatches to one of several methods in a Web server, each of which handles a particular URL. Each of those methods (bottom) uses parameters from the incoming HTTP request. In this figure, there is a separate exception handler for each call to getParameter; this results in duplicated code.

图 10.1:顶部的代码将分派给 Web 服务器中的几种方法之一,每种方法都处理一个特定的 URL。每个方法(底部)都使用传入 HTTP 请求中的参数。在此图中,每个对 getParameter 的调用都有一个单独的异常处理程序。这导致重复的代码。

A better approach is to aggregate the exceptions. Instead of catching the exceptions in the individual service methods, let them propagate up to the top-level dispatch method for the Web server, as in Figure 10.2. A single handler in this method can catch all of the exceptions and generate an appropriate error response for missing parameters.

更好的方法是汇总异常。让它们传播到 Web 服务器的顶级调度方法,而不是在单个服务方法中捕获异常,如图 10.2 所示。此方法中的单个处理程序可以捕获所有异常,并为丢失的参数生成适当的错误响应。

The aggregation approach can be taken even further in the Web example. There are many other errors besides missing parameters that can occur while processing a Web page; for example, a parameter might not have the right syntax (the service method expected an integer, but the value was “xyz”), or the user might not have permission for the requested operation. In each case, the error should result in an error response; the errors differ only in the error message to include in the response (“parameter 'quantity' not present in URL” or “bad value 'xyz' for 'quantity' parameter; must be positive integer”). Thus, all conditions resulting in an error response can be handled with a single top-level exception handler. The error message can be generated at the time the exception is thrown and included as a variable in the exception record; for example, getParameter will generate the “parameter 'quantity' not present in URL” message. The top-level handler extracts the message from the exception and incorporates it into the error response.

聚合异常的方式可以在 Web 示例中更进一步。处理网页时,除了缺少参数外,还有许多其他错误;例如,参数可能没有正确的类型(服务方法期望的参数时整数,但值为“ xyz”),或者用户可能无权执行所请求的操作。在每种情况下,错误都应导致错误响应。错误仅在响应中包含的错误消息中有所不同(“ URL 中不存在参数'quantity'” 或 “'quantity'参数的值 'xyz' 不正确;必须为正整数”)。因此,所有导致错误响应的条件都可以使用单个顶级异常处理程序进行处理。错误消息可以在引发异常时生成,并作为变量包含在异常记录中。例如,getParameter 将生成“ URL 中不存在的参数'quantity'”消息。顶级处理程序从异常中提取消息,并将其合并到错误响应中。

Figure 10.2: This code is functionally equivalent to Figure 10.1, but exception handling has been aggregated: a single exception handler in the dispatcher catches all of the NoSuchParameter exceptions from all of the URL-specific methods.

图 10.2:此代码在功能上等效于图 10.1,但是异常处理已聚合:分派器中的单个异常处理程序从所有特定于 URL 的方法中捕获所有 NoSuchParameter 异常。

The aggregation described in the preceding paragraph has good properties from the standpoint of encapsulation and information hiding. The top-level exception handler encapsulates knowledge about how to generate error responses, but it knows nothing about specific errors; it just uses the error message provided in the exception. The getParameter method encapsulates knowledge about how to extract a parameter from a URL, and it also knows how to describe extraction errors in a human-readable form. These two pieces of information are closely related, so it makes sense for them to be in the same place. However, getParameter knows nothing about the syntax of an HTTP error response. As new functionality is added to the Web server, new methods like getParameter may be created with their own errors. If the new methods throw exceptions in the same way as getParameter (by generating exceptions that inherit from the same superclass and including an error message in each exception), they can plug into the existing system with no other changes: the top-level handler will automatically generate error responses for them.

从封装和信息隐藏的角度来看,上一段中描述的异常聚合具有良好的属性。顶级异常处理程序封装了有关如何生成错误响应的知识,但对特定错误一无所知。它仅使用异常中提供的错误消息。getParameter 方法封装了有关如何从 URL 提取参数的知识,并且还知道如何以人类可读的形式描述提取的错误。这两个信息密切相关,因此将它们放在同一位置是说得通的。但是,getParameter 对 HTTP 错误响应的语法一无所知。随着向 Web 服务器中添加了新功能,可能会创建具有类似 getParameter 有自己的异常的新方法。如果新方法抛出异常的方式和 getParameter 一样(继承自同一基类并且包含错误信息),现存系统不用做任何更改就可以集成新的方法:顶级异常处理程序会自动为新方法生成相应的错误响应。

This example illustrates a generally-useful design pattern for exception handling. If a system processes a series of requests, it’s useful to define an exception that aborts the current request, cleans up the system’s state, and continues with the next request. The exception is caught in a single place near the top of the system’s request-handling loop. This exception can be thrown at any point in the processing of a request to abort the request; different subclasses of the exception can be defined for different conditions. Exceptions of this type should be clearly distinguished from exceptions that are fatal to the entire system.

此示例说明了用于异常处理的通用设计模式。如果系统处理一系列请求,则定义一个异常以中止当前请求,清除系统状态并继续下一个请求非常有用。异常被捕获在系统请求处理循环顶部附近的单个位置。在处理中止请求的任何时候都可以抛出异常。可以为不同的条件定义异常的不同子类。应该将这种类型的异常与对整个系统致命的异常区分开来。

Exception aggregation works best if an exception propagates several levels up the stack before it is handled; this allows more exceptions from more methods to be handled in the same place. This is the opposite of exception masking: masking usually works best if an exception is handled in a low-level method. For masking, the low-level method is typically a library method used by many other methods, so allowing the exception to propagate would increase the number of places where it is handled. Masking and aggregation are similar in that both approaches position an exception handler where it can catch the most exceptions, eliminating many handlers that would otherwise need to be created.

如果异常在被处理之前在堆栈中传播到了多个级别,则异常集合最有效。这允许在同一个地方处理来自更多方法的更多异常。这与异常屏蔽相反:异常屏蔽通常在异常被低级代码处理的情况下效果最好。对于异常屏蔽,低级方法通常是被许多其他方法使用的库方法,因此,允许传播异常会增加需要处理该异常的位置数量。异常屏蔽和异常聚合的相似之处在于,这两种方式都将异常处理程序置于可以捕获最多异常的位置,从而消除了许多本来需要创建的异常处理程序。

Another example of exception aggregation occurs in the RAMCloud storage system for crash recovery. A RAMCloud system consists of a collection of storage servers that keep multiple copies of each object, so the system can recover from a variety of failures. For example, if a server crashes and loses all of its data, RAMCloud reconstructs the lost data using copies stored on other servers. Errors can also happen on a smaller scale; for example, a server may discover that an individual object is corrupted.

异常聚合的另一个例子是 RAMCloud 存储系统崩溃恢复。RAMCloud 系统由一组存储服务器组成,这些存储服务器保留每个对象的多个副本,因此系统可以从各种故障中恢复。例如,如果服务器崩溃并丢失其所有数据,RAMCloud 会使用存储在其他服务器上的副本来重建丢失的数据。错误也可能在较小的范围内发生。例如,服务器可能发现单个对象已损坏。

RAMCloud does not have separate recovery mechanisms for each different kind of error. Instead, RAMCloud “promotes” many smaller errors into larger ones. RAMCloud could, in principle, handle a corrupted object by restoring that one object from a backup copy. However, it doesn’t do this. Instead, if it discovers a corrupted object it crashes the server containing the object. RAMCloud uses this approach because crash recovery is quite complex and this approach minimized the number of different recovery mechanisms that had to be created. Creating a recovery mechanism for crashed servers was unavoidable, so RAMCloud uses the same mechanism for other kinds of recovery as well. This reduced the amount of code that had to be written, and it also meant that server crash recovery gets invoked more often. As a result, bugs in recovery are more likely to be discovered and fixed.

对于每种不同类型的错误,RAMCloud 没有单独的恢复机制。相反,RAMCloud 将许多较小的错误“提升”为较大的错误。原则上,RAMCloud 可以通过从备份副本中恢复一个损坏的对象来处理这个损坏的对象。然而,它并不这样做。相反,如果它发现一个损坏的对象,它会使包含该对象的服务器崩溃。RAMCloud 使用这种方法是因为崩溃恢复非常复杂,而且这种方法最小化了必须创建的不同恢复机制的数量。为崩溃的服务器创建恢复机制是不可避免的,因此 RAMCloud 对其他类型的恢复也使用相同的机制。这减少了必须编写的代码量,而且这还意味着服务器崩溃恢复将更频繁地被调用。因此,恢复中的 bug 更有可能被发现和修复。

One disadvantage of promoting a corrupted object into a server crash is that it increases the cost of recovery considerably. This is not a problem in RAMCloud, since object corruption is quite rare. However, error promotion may not make sense for errors that happen frequently. As one example, it would not be practical to crash a server anytime one of its network packets is lost.

将损坏的对象升级为服务器崩溃的一个缺点是,它大大增加了恢复成本。这在 RAMCloud 中不是问题,因为对象损坏非常罕见。但是,错误升级对于经常发生的错误可能没有意义。举一个例子,在服务器的任何网络数据包丢失时使服务器崩溃是不切实际的。

One way of thinking about exception aggregation is that it replaces several special-purpose mechanisms, each tailored for a particular situation, with a single general-purpose mechanism that can handle multiple situations. This provides another illustration of the benefits of general-purpose mechanisms.

考虑异常聚合的一种方法是,它用可以处理多种情况的单个通用机制替换了几种针对特定情况而量身定制的特殊用途的机制。这再次说明了通用机制的好处。

10.8 Just crash? 让程序崩溃?

The fourth technique for reducing complexity related to exception handling is to crash the application. In most applications there will be certain errors that it’s not worth trying to handle. Typically, these errors are difficult or impossible to handle and don’t occur very often. The simplest thing to do in response to these errors is to print diagnostic information and then abort the application.

减少与异常处理相关的复杂性的第四种技术是使应用程序崩溃。在大多数应用程序中,有些错误是不值去处理的。通常,这些错误很难或不可能处理,而且很少发生。针对这些错误的最简单的操作是打印诊断信息,然后中止应用程序。

One example is “out of memory” errors that occur during storage allocation. Consider the malloc function in C, which returns NULL if it cannot allocate the desired block of memory. This is an unfortunate behavior, because it assumes that every single caller of malloc will check the return value and take appropriate action if there is no memory. Applications contain numerous calls to malloc, so checking the result after each call would add significant complexity. If a programmer forgets the check (which is fairly likely), then the application will dereference a null pointer if memory runs out, resulting in a crash that camouflages the real problem.

一个示例是在存储分配期间发生的“内存不足”错误。考虑一下 C 语言中的 malloc 函数,如果它无法分配所需的内存块,则该函数将返回 NULL。这是一个不合适的行为,因为它假定 malloc 的每个调用者都将检查返回值并在没有内存的情况下采取适当的措施。应用程序包含许多对 malloc 的调用,因此在每次调用后检查结果将增加相当大的复杂性。如果程序员忘记了检查(这很有可能),那么如果内存用完,应用程序将取消引用空指针,从而导致崩溃,从而掩盖了实际问题。

Furthermore, there isn’t much an application can do when it discovers that memory is exhausted. In principle the application could look for unneeded memory to free, but if the application had unneeded memory it could already have freed it, which would have prevented the out-of-memory error in the first place. Today’s systems have so much memory that memory almost never runs out; if it does, it usually indicates a bug in the application. Thus, it rarely make sense to try to handle out-of-memory errors; this creates too much complexity for too little benefit.

此外,当应用程序发现内存已用完时,它也没什好做的了。原则上,应用程序可以寻找不需要的内存以释放它,但是,如果应用程序有不需要的内存,它可能已经释放了它,这将首先防止内存不足的错误。当今的系统具有如此大的内存,以至于内存几乎永远不会耗尽。如果是这样,通常表明应用程序中存在 bug。因此,尝试处理内存不足错误几乎没有道理。这会带来太多的复杂性,而带来的收益却太少。

A better approach is to define a new method ckalloc, which calls malloc, checks the result, and aborts the application with an error message if memory is exhausted. The application never invokes malloc directly; it always invokes ckalloc.

更好的方法是定义一个新的 ckalloc 方法,该方法调用 malloc,检查结果,在内存耗尽时中止应用程序并输出错误消息。该应用程序从不直接调用 malloc。它总是调用 ckalloc。

In newer languages such as C++ and Java, the new operator throws an exception if memory is exhausted. There’s not much point in catching this exception, since there’s a good chance that the exception handler will also try to allocate memory, which will also fail. Dynamically allocated memory is such a fundamental element of any modern application that it doesn’t make sense for the application to continue if memory is exhausted; it’s better to crash as soon as the error is detected.

在较新的语言(例如 C++ 和 Java)中,如果内存耗尽,则 new 运算符将引发异常。捕获此异常没有什么意义,因为异常处理程序很有可能还会尝试分配内存,这也会失败。动态分配的内存是任何现代应用程序中的基本元素,如果内存耗尽,则继续应用程序是没有意义的。最好在检测到错误后立即崩溃。

There are many other examples of errors where crashing the application makes sense. For most programs, if an I/O error occurs while reading or writing an open file (such as a disk hard error), or if a network socket cannot be opened, there’s not much the application can do to recover, so aborting with a clear error message is a sensible approach. These errors are infrequent, so they are unlikely to affect the overall usability of the application. Aborting with an error message is also appropriate if an application encounters an internal error such as an inconsistent data structure. Conditions like this probably indicate bugs in the program.

还有许多其他错误示例,当这些错误出现时使应用程序崩溃是说得通的。对于大多数程序,如果在读取或写入打开的文件时发生 I/O 错误(例如磁盘硬错误),或者无法打开网络套接字,则应用程序没有什么办法从在错误中恢复,因此中止程序并输出清晰的错误信息是明智之举。这些错误很少发生,因此它们不太可能影响应用程序的整体可用性。如果应用程序遇到内部错误(如数据结构不一致),则中止程序并输出清晰的错误信息也是合适的。这样的情况可能表明程序中存在 bug。

Whether or not it is acceptable to crash on a particular error depends on the application. For a replicated storage system, it isn’t appropriate to abort on an I/O error. Instead, the system must use replicated data to recover any information that was lost. The recovery mechanisms will add considerable complexity to the program, but recovering lost data is an essential part of the value the system provides to its users.

当特定错误出现时应用程序崩溃是否可以接受取决于应用程序。对于复制的存储系统,不适合因 I/O 错误而中止。相反,系统必须使用复制的数据来恢复丢失的任何信息。恢复机制将给程序增加相当大的复杂性,但是恢复丢失的数据是系统为用户提供的价值的重要组成部分。

10.9 Design special cases out of existence 通过设计规避特殊情况

For the same reason that it makes sense to define errors out of existence, it also makes sense to define other special cases out of existence. Special cases can result in code that is riddled with if statements, which make the code hard to understand and lead to bugs. Thus, special cases should be eliminated wherever possible. The best way to do this is by designing the normal case in a way that automatically handles the special cases without any extra code.

通过定义规避错误是说得通的,出于同样的原因,通过设计规避特殊情况也是说得通的。特殊情况可能导致代码中混入 if 语句,这使代码难以理解并导致错误。因此,应尽可能消除特殊情况。做到这一点的最好方法是以一种无需任何额外代码就能自动处理特殊情况的方式来设计正常情况。

In the text editor project described in Chapter 6, students had to implement a mechanism for selecting text and copying or deleting the selection. Most students introduced a state variable in their selection implementation to indicate whether or not the selection exists. They probably chose this approach because there are times when no selection is visible on the screen, so it seemed natural to represent this notion in the implementation. However, this approach resulted in numerous checks to detect the “no selection” condition and handle it specially.

在第 6 章中描述的文本编辑器项目中,学生必须实现一种选择文本以及复制或删除所选内容的机制。大多数学生在他们的选择实现中引入了状态变量,以表明选择是否存在。他们之所以选择这种方法,是因为有时屏幕上看不到任何选择,因此在实现中似乎很自然地代表了这一概念。但是,这种方法导致了大量的检查,以检测“没有选择”的情况,并专门处理它。。

The selection handling code can be simplified by eliminating the “no selection” special case, so that the selection always exists. When there is no selection visible on the screen, it can be represented internally with an empty selection, whose starting and ending positions are the same. With this approach, the selection management code can be written without any checks for “no selection”. When copying the selection, if the selection is empty then 0 bytes will be inserted at the new location (if implemented correctly, there will be no need to check for 0 bytes as a special case). Similarly, it should be possible to design the code for deleting the selection so that the empty case is handled without any special-case checks. Consider a selection all on a single line. To delete the selection, extract the portion of the line preceding the selection and concatenate it with the portion of the line following the selection to form the new line. If the selection is empty, this approach will regenerate the original line.

通过消除“不选择”的特殊情况,可以简化选择处理代码,从而使选择始终存在。当屏幕上没有可见的选择时,可以在内部用空的选择表示,其开始和结束位置相同。使用这种方法,可以编写选择管理代码,而无需对“不选择”进行任何检查。复制所选内容时,如果所选内容为空,则将在新位置插入 0 字节(如果正确实现,则在特殊情况下无需检查 0 字节)。同样,应该有可能设计用于删除选择的代码,以便无需任何特殊情况检查就可以处理空情况。考虑选择一整行的情况。要删除选择,提取选择之前的行的一部分,并将其与选择之后的行的部分连接起来以形成新行。如果选择为空,则此方法将重新生成原始行。

This example also illustrates the “different layer, different abstraction” idea from Chapter 7. The notion of “no selection” makes sense in terms of how the user thinks about the application’s interface, but that doesn’t mean it has to be represented explicitly inside the application. Having a selection that always exists, but is sometimes empty and thus invisible, results in a simpler implementation.

此示例还说明了第 7 章中的“不同的层,不同的抽象”概念。“无选择”的概念在用户对应用程序界面的看法方面很有意义,但这并不意味着必须明确在应用程序内部表示它。选择总是存在的,但有时是空的,因此是不可见的,这样可以简化实现。

10.10 Taking it too far 做过头了

Defining away exceptions, or masking them inside a module, only makes sense if the exception information isn’t needed outside the module. This was true for the examples in this chapter, such the Tcl unset command and the Java substring method; in the rare situations where a caller cares about the special cases detected by the exceptions, there are other ways for it to get this information.

通过定义规避错误或将其屏蔽在模块内部,仅在模块外部不需要异常信息时才有意义。对于本章中的示例,例如 Tcl unset 命令和 Java 子字符串方法,都是如此。在极少数情况下,调用者关心异常检测到的特殊情况,还有其他方法可以获取此信息。

However, it is possible to take this idea too far. In a module for network communication, a student team masked all network exceptions: if a network error occurred, the module caught it, discarded it, and continued as if there were no problem. This meant that applications using the module had no way to find out if messages were lost or a peer server failed; without this information, it was impossible to build robust applications. In this case, it is essential for the module to expose the exceptions, even though they add complexity to the module’s interface.

但是,有时候会做的过头。在用于网络通信的模块中,一个学生团队掩盖了所有网络异常:如果发生网络错误,则模块将其捕获,丢弃并继续进行,就好像没有问题一样。这意味着使用该模块的应用程序无法确定消息是否丢失或节点服务器是否发生故障;没有这些信息,就不可能构建健壮的应用程序。在这种情况下,模块必须公开异常,即使它们增加了模块接口的复杂性。

With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed.

异常与软件设计中的许多其他领域一样,您必须确定哪些是重要的,哪些是不重要的。不重要的事物应该被隐藏起来,它们越多越好。但是,当某件事很重要时,必须将其暴露出来。

10.11 Conclusion 结论

Special cases of any form make code harder to understand and increase the likelihood of bugs. This chapter focused on exceptions, which are one of the most significant sources of special-case code, and discussed how to reduce the number of places where exceptions must be handled. The best way to do this is by redefining semantics to eliminate error conditions. For exceptions that can’t be defined away, you should look for opportunities to mask them at a low level, so their impact is limited, or aggregate several special-case handlers into a single more generic handler. Together, these techniques can have a significant impact on overall system complexity.

任何形式的特殊情况都使代码更难以理解,并增加了发生 bug 的可能性。本章重点讨论异常,异常是特殊情况代码的最重要来源之一,并讨论了如何减少必须处理异常的地方的数量。做到这一点的最佳方法是重新定义语义以消除错误条件。对于无法通过设计规避的异常,您应该寻找机会将它们在底层屏蔽,以免影响有限,或者将多个特殊情况的处理程序聚合到一个更通用的处理程序中。总之,这些技术会对整个系统的复杂性产生重大影响。

1 Ding Yuan et. al., “Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems,” 2014 USENIX Conference on Operating System Design and Implementation.

1 丁元等 等人,“简单的测试可以防止最关键的故障:对分布式数据密集型系统中的生产故障的分析”,2014 USENIX 操作系统设计和实施大会。

',135)]))}const h=t(r,[["render",l],["__file","ch10.html.vue"]]),d=JSON.parse('{"path":"/ch10.html","title":"第 10 章 通过定义规避错误","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"10.1 Why exceptions add complexity 为什么异常会增加复杂性","slug":"_10-1-why-exceptions-add-complexity-为什么异常会增加复杂性","link":"#_10-1-why-exceptions-add-complexity-为什么异常会增加复杂性","children":[]},{"level":2,"title":"10.2 Too many exceptions 异常过多","slug":"_10-2-too-many-exceptions-异常过多","link":"#_10-2-too-many-exceptions-异常过多","children":[]},{"level":2,"title":"10.3 Define errors out of existence 通过定义规避错误","slug":"_10-3-define-errors-out-of-existence-通过定义规避错误","link":"#_10-3-define-errors-out-of-existence-通过定义规避错误","children":[]},{"level":2,"title":"10.4 Example: file deletion in Windows 示例:Windows 中的文件删除","slug":"_10-4-example-file-deletion-in-windows-示例-windows-中的文件删除","link":"#_10-4-example-file-deletion-in-windows-示例-windows-中的文件删除","children":[]},{"level":2,"title":"10.5 Example: Java substring method 示例:Java 子字符串方法","slug":"_10-5-example-java-substring-method-示例-java-子字符串方法","link":"#_10-5-example-java-substring-method-示例-java-子字符串方法","children":[]},{"level":2,"title":"10.6 Mask exceptions 屏蔽异常","slug":"_10-6-mask-exceptions-屏蔽异常","link":"#_10-6-mask-exceptions-屏蔽异常","children":[]},{"level":2,"title":"10.7 Exception aggregation 异常聚合","slug":"_10-7-exception-aggregation-异常聚合","link":"#_10-7-exception-aggregation-异常聚合","children":[]},{"level":2,"title":"10.8 Just crash? 让程序崩溃?","slug":"_10-8-just-crash-让程序崩溃","link":"#_10-8-just-crash-让程序崩溃","children":[]},{"level":2,"title":"10.9 Design special cases out of existence 通过设计规避特殊情况","slug":"_10-9-design-special-cases-out-of-existence-通过设计规避特殊情况","link":"#_10-9-design-special-cases-out-of-existence-通过设计规避特殊情况","children":[]},{"level":2,"title":"10.10 Taking it too far 做过头了","slug":"_10-10-taking-it-too-far-做过头了","link":"#_10-10-taking-it-too-far-做过头了","children":[]},{"level":2,"title":"10.11 Conclusion 结论","slug":"_10-11-conclusion-结论","link":"#_10-11-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch10.md"}');export{h as comp,d as data}; diff --git a/assets/ch11.html-CMvLx2cz.js b/assets/ch11.html-bgw0RYTe.js similarity index 99% rename from assets/ch11.html-CMvLx2cz.js rename to assets/ch11.html-bgw0RYTe.js index 97a55be1..be8a8b53 100644 --- a/assets/ch11.html-CMvLx2cz.js +++ b/assets/ch11.html-bgw0RYTe.js @@ -1 +1 @@ -import{_ as t,c as o,f as i,o as a}from"./app-CvqtBB8Z.js";const r={};function n(s,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 11 章 设计两次

Chapter 11 Design it Twice

Designing software is hard, so it’s unlikely that your first thoughts about how to structure a module or system will produce the best design. You’ll end up with a much better result if you consider multiple options for each major design decision: design it twice.

设计软件非常困难,因此您对如何构造模块或系统的初步思考不太可能会产生最佳的设计。如果为每个主要设计决策考虑多个选项,最终将获得更好的结果:设计两次。

Suppose you are designing the class that will manage the text of a file for a GUI text editor. The first step is to define the interface that the class will present to the rest of the editor; rather than picking the first idea that comes to mind, consider several possibilities. One choice is a line-oriented interface, with operations to insert, modify, and delete whole lines of text. Another option is an interface based on individual character insertions and deletions. A third choice is a string-oriented interface, which operates on arbitrary ranges of characters that may cross line boundaries. You don’t need to pin down every feature of each alternative; it’s sufficient at this point to sketch out a few of the most important methods.

假设您正在设计用于管理 GUI 文本编辑器文件文本的类。第一步是定义该类将呈现给编辑器其余部分的接口。与其选择想到的第一个想法,不如考虑几种可能性。一种选择是面向行的界面,该界面具有插入,修改和删除整行文本的操作。另一个选择是基于单个字符插入和删除的接口。第三种选择是面向字符串的接口,该接口可对可能跨越线边界的任意范围的字符进行操作。您无需确定每个替代方案的每个功能;在这一点上,勾勒出一些最重要的方法就足够了。

Try to pick approaches that are radically different from each other; you’ll learn more that way. Even if you are certain that there is only one reasonable approach, consider a second design anyway, no matter how bad you think it will be. It will be instructive to think about the weaknesses of that design and contrast them with the features of other designs.

尝试选择彼此根本不同的方法;这样您将学到更多。即使你确定只有一种合理的方法,无论如何也要考虑第二种设计,不管你认为它有多糟糕。考虑该设计的弱点并将它们与其他设计的特征进行对比将很有启发性。

After you have roughed out the designs for the alternatives, make a list of the pros and cons of each one. The most important consideration for an interface is ease of use for higher level software. In the example above, both the line-oriented interface and the character-oriented interface will require extra work in software that uses the text class. The line-oriented interface will require higher level software to split and join lines during partial-line and multi-line operations such as cutting and pasting the selection. The character-oriented interface will require loops to implement operations that modify more than a single character. It is also worth considering other factors:

在你粗略地设计出可供选择的方案后,列出每个方案的优缺点。对高级软件的易用性是接口最重要的考虑因素。在上面的示例中,面向行的接口和面向字符的接口都需要使用文本类的软件中做额外工作。面向行的接口将需要更高级别的软件来在部分行和多行操作(例如剪切和粘贴所选内容)期间拆分和合并行。面向字符的接口将需要循环来实现修改多个字符的操作。还值得考虑其他因素:

  • Does one alternative have a simpler interface than another? In the text example, all of the text interfaces are relatively simple.
  • Is one interface more general-purpose than another?
  • Does one interface enable a more efficient implementation than another? In the text example, the character-oriented approach is likely to be significantly slower than the others, because it requires a separate call into the text module for each character.

  • 一种选择是否具有比另一种有更简单的接口?在文本示例中,所有文本接口都相对简单。
  • 一个接口比另一个接口更通用吗?
  • 一个接口的实现是否比另一个接口的实现更有效率?在文本示例中,面向字符的方法可能比其他方法慢得多,因为它需要为每个字符单独调用文本模块。

Once you have compared alternative designs, you will be in a better position to identify the best design. The best choice may be one of the alternatives, or you may discover that you can combine features of multiple alternatives into a new design that is better than any of the original choices.

比较了备选设计之后,您将可以更好地确定最佳设计。最佳选择可能是这些选择之一,或者您可能会发现,您可以将多种选择的特性结合到一个新的设计中,这个新的设计比任何最初的选择都要好。

Sometimes none of the alternatives is particularly attractive; when this happens, see if you can come up with additional schemes. Use the problems you identified with the original alternatives to drive the new design(s). If you were designing the text class and considered only the line-oriented and character-oriented approaches, you might notice that each of the alternatives is awkward because it requires higher level software to perform additional text manipulations. That’s a red flag: if there’s going to be a text class, it should handle all of the text manipulation. In order to eliminate the additional text manipulations, the text interface needs to match more closely the operations happening in higher level software. These operations don’t always correspond to single characters or single lines. This line of reasoning should lead you to a range-oriented API for text, which eliminates the problem with the earlier designs.

有时所有的备选设计都没有特别的吸引力。发生这种情况时,看看是否可以提出其他方案。使用您在备选设计中发现的问题来推动新设计。如果您在设计文本类并且仅考虑面向行和面向字符的方法,则可能会注意到每个替代方案都比较笨拙,因为它需要更高级别的软件来执行其他文本操作。那是一个危险信号:如果要有一个文本类,它应该处理所有文本操作。为了消除其他文本操作,文本界面需要更紧密地匹配高级软件中发生的操作。这些操作并不总是对应于单个字符或一行。这种推理方式应该会让你找到一个面向范围的文本 API,它消除了早期设计的问题。

The design-it-twice principle can be applied at many levels in a system. For a module, you can use this approach first to pick the interface, as described above. Then you can apply it again when you are designing the implementation: for the text class, you might consider implementations such as a linked list of lines, fixed-size blocks of characters, or a “gap buffer.” The goals will be different for the implementation than for the interface: for the implementation, the most important things are simplicity and performance. It’s also useful to explore multiple designs at higher levels in the system, such as when choosing features for a user interface, or when decomposing a system into major modules. In each case, it’s easier to identify the best approach if you can compare a few alternatives.

设计两次原则可以在系统的许多级别上应用。对于模块,您可以首先使用此方法来选择接口,如上所述。然后,您可以在设计实现时再次应用它:对于文本类,您可以考虑实现这些实现,例如行的链接列表,固定大小的字符块或“间隙缓冲区”。实现的目标与接口的目标是不同的:对于实现,最重要的是简单性和性能。在系统的更高层次上探索多种设计也很有用,例如在为用户界面选择功能或将系统分解为主要模块时。在每种情况下,如果您可以比较几种选择,则更容易确定最佳方法。

Designing it twice does not need to take a lot of extra time. For a smaller module such as a class, you may not need more than an hour or two to consider alternatives. This is a small amount of time compared to the days or weeks you will spend implementing the class. The initial design experiments will probably result in a significantly better design, which will more than pay for the time spent designing it twice. For larger modules you’ll spend more time in the initial design explorations, but the implementation will also take longer, and the benefits of a better design will also be higher.

设计两次不需要花费很多额外的时间。对于较小的模块比如类,您可能不需要一两个小时去思考备选设计。与您将花费数天或数周时间来实现该类相比,这是很少的时间。最初的设计实验可能会导致明显更好的设计,这将比花两次设计时间所花的时间多。对于较大的模块,您将花费更多的时间进行初始设计探索,但是实现也将花费更长的时间,并且更好的设计所带来的好处也会更高。

I have noticed that the design-it-twice principle is sometimes hard for really smart people to embrace. When they are growing up, smart people discover that their first quick idea about any problem is sufficient for a good grade; there is no need to consider a second or third possibility. This makes it easy to develop bad work habits. However, as these people get older, they get promoted into environments with harder and harder problems. Eventually, everyone reaches a point where your first ideas are no longer good enough; if you want to get really great results, you have to consider a second possibility, or perhaps a third, no matter how smart you are. The design of large software systems falls in this category: no-one is good enough to get it right with their first try.

我已经注意到,真正聪明的人有时很难接受设计两次原则。当他们长大后,聪明的人会发现,他们对任何问题的第一个快速构想就足以取得良好的成绩。无需考虑第二种或第三种可能性。这使得容易养成不良的工作习惯。但是,随着这些人变老,他们将被提升到越来越困难的环境中。最终,每个人都达到了您的第一个想法不再足够好的境地。如果您想获得非常好的结果,那么无论您多么聪明,都必须考虑第二种可能性,或者第三种可能性。大型软件系统的设计属于此类:没有人足够优秀,能够第一次就做好。

Unfortunately, I often see smart people who insist on implementing the first idea that comes to mind, and this causes them to underperform their true potential (it also makes them frustrating to work with). Perhaps they subconsciously believe that “smart people get it right the first time,” so if they try multiple designs it would mean they are not smart after all. This is not the case. It isn’t that you aren’t smart; it’s that the problems are really hard! Furthermore, that’s a good thing: it’s much more fun to work on a difficult problem where you have to think carefully, rather than an easy problem where you don’t have to think at all.

不幸的是,我经常看到聪明的人坚持要实现第一个想到的想法,这会使他们无法发挥其真正的潜力(这也使他们沮丧地工作)。也许他们下意识地相信“聪明的人第一次就能做到”,因此,如果他们尝试多种设计,那将意味着他们并不聪明。不是这种情况。不是说你不聪明;而是问题真的很难解决!此外,这是一件好事:处理一个必须认真思考的难题比处理一个根本不需要思考的难题更有趣。

The design-it-twice approach not only improves your designs, but it also improves your design skills. The process of devising and comparing multiple approaches will teach you about the factors that make designs better or worse. Over time, this will make it easier for you to rule out bad designs and hone in on really great ones.

设计两次的方式不仅可以改善您的设计,而且可以提高您的设计能力。设计和比较多种方法的过程将教会你使设计变得更好或更差的因素。这将使你更容易排除不好的设计,并钻研真正伟大的设计。

',27)]))}const h=t(r,[["render",n],["__file","ch11.html.vue"]]),c=JSON.parse('{"path":"/ch11.html","title":"第 11 章 设计两次","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734160685000},"filePathRelative":"ch11.md"}');export{h as comp,c as data}; +import{_ as t,c as o,f as i,o as a}from"./app-BQdhfat9.js";const r={};function n(s,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 11 章 设计两次

Chapter 11 Design it Twice

Designing software is hard, so it’s unlikely that your first thoughts about how to structure a module or system will produce the best design. You’ll end up with a much better result if you consider multiple options for each major design decision: design it twice.

设计软件非常困难,因此您对如何构造模块或系统的初步思考不太可能会产生最佳的设计。如果为每个主要设计决策考虑多个选项,最终将获得更好的结果:设计两次。

Suppose you are designing the class that will manage the text of a file for a GUI text editor. The first step is to define the interface that the class will present to the rest of the editor; rather than picking the first idea that comes to mind, consider several possibilities. One choice is a line-oriented interface, with operations to insert, modify, and delete whole lines of text. Another option is an interface based on individual character insertions and deletions. A third choice is a string-oriented interface, which operates on arbitrary ranges of characters that may cross line boundaries. You don’t need to pin down every feature of each alternative; it’s sufficient at this point to sketch out a few of the most important methods.

假设您正在设计用于管理 GUI 文本编辑器文件文本的类。第一步是定义该类将呈现给编辑器其余部分的接口。与其选择想到的第一个想法,不如考虑几种可能性。一种选择是面向行的界面,该界面具有插入,修改和删除整行文本的操作。另一个选择是基于单个字符插入和删除的接口。第三种选择是面向字符串的接口,该接口可对可能跨越线边界的任意范围的字符进行操作。您无需确定每个替代方案的每个功能;在这一点上,勾勒出一些最重要的方法就足够了。

Try to pick approaches that are radically different from each other; you’ll learn more that way. Even if you are certain that there is only one reasonable approach, consider a second design anyway, no matter how bad you think it will be. It will be instructive to think about the weaknesses of that design and contrast them with the features of other designs.

尝试选择彼此根本不同的方法;这样您将学到更多。即使你确定只有一种合理的方法,无论如何也要考虑第二种设计,不管你认为它有多糟糕。考虑该设计的弱点并将它们与其他设计的特征进行对比将很有启发性。

After you have roughed out the designs for the alternatives, make a list of the pros and cons of each one. The most important consideration for an interface is ease of use for higher level software. In the example above, both the line-oriented interface and the character-oriented interface will require extra work in software that uses the text class. The line-oriented interface will require higher level software to split and join lines during partial-line and multi-line operations such as cutting and pasting the selection. The character-oriented interface will require loops to implement operations that modify more than a single character. It is also worth considering other factors:

在你粗略地设计出可供选择的方案后,列出每个方案的优缺点。对高级软件的易用性是接口最重要的考虑因素。在上面的示例中,面向行的接口和面向字符的接口都需要使用文本类的软件中做额外工作。面向行的接口将需要更高级别的软件来在部分行和多行操作(例如剪切和粘贴所选内容)期间拆分和合并行。面向字符的接口将需要循环来实现修改多个字符的操作。还值得考虑其他因素:

  • Does one alternative have a simpler interface than another? In the text example, all of the text interfaces are relatively simple.
  • Is one interface more general-purpose than another?
  • Does one interface enable a more efficient implementation than another? In the text example, the character-oriented approach is likely to be significantly slower than the others, because it requires a separate call into the text module for each character.

  • 一种选择是否具有比另一种有更简单的接口?在文本示例中,所有文本接口都相对简单。
  • 一个接口比另一个接口更通用吗?
  • 一个接口的实现是否比另一个接口的实现更有效率?在文本示例中,面向字符的方法可能比其他方法慢得多,因为它需要为每个字符单独调用文本模块。

Once you have compared alternative designs, you will be in a better position to identify the best design. The best choice may be one of the alternatives, or you may discover that you can combine features of multiple alternatives into a new design that is better than any of the original choices.

比较了备选设计之后,您将可以更好地确定最佳设计。最佳选择可能是这些选择之一,或者您可能会发现,您可以将多种选择的特性结合到一个新的设计中,这个新的设计比任何最初的选择都要好。

Sometimes none of the alternatives is particularly attractive; when this happens, see if you can come up with additional schemes. Use the problems you identified with the original alternatives to drive the new design(s). If you were designing the text class and considered only the line-oriented and character-oriented approaches, you might notice that each of the alternatives is awkward because it requires higher level software to perform additional text manipulations. That’s a red flag: if there’s going to be a text class, it should handle all of the text manipulation. In order to eliminate the additional text manipulations, the text interface needs to match more closely the operations happening in higher level software. These operations don’t always correspond to single characters or single lines. This line of reasoning should lead you to a range-oriented API for text, which eliminates the problem with the earlier designs.

有时所有的备选设计都没有特别的吸引力。发生这种情况时,看看是否可以提出其他方案。使用您在备选设计中发现的问题来推动新设计。如果您在设计文本类并且仅考虑面向行和面向字符的方法,则可能会注意到每个替代方案都比较笨拙,因为它需要更高级别的软件来执行其他文本操作。那是一个危险信号:如果要有一个文本类,它应该处理所有文本操作。为了消除其他文本操作,文本界面需要更紧密地匹配高级软件中发生的操作。这些操作并不总是对应于单个字符或一行。这种推理方式应该会让你找到一个面向范围的文本 API,它消除了早期设计的问题。

The design-it-twice principle can be applied at many levels in a system. For a module, you can use this approach first to pick the interface, as described above. Then you can apply it again when you are designing the implementation: for the text class, you might consider implementations such as a linked list of lines, fixed-size blocks of characters, or a “gap buffer.” The goals will be different for the implementation than for the interface: for the implementation, the most important things are simplicity and performance. It’s also useful to explore multiple designs at higher levels in the system, such as when choosing features for a user interface, or when decomposing a system into major modules. In each case, it’s easier to identify the best approach if you can compare a few alternatives.

设计两次原则可以在系统的许多级别上应用。对于模块,您可以首先使用此方法来选择接口,如上所述。然后,您可以在设计实现时再次应用它:对于文本类,您可以考虑实现这些实现,例如行的链接列表,固定大小的字符块或“间隙缓冲区”。实现的目标与接口的目标是不同的:对于实现,最重要的是简单性和性能。在系统的更高层次上探索多种设计也很有用,例如在为用户界面选择功能或将系统分解为主要模块时。在每种情况下,如果您可以比较几种选择,则更容易确定最佳方法。

Designing it twice does not need to take a lot of extra time. For a smaller module such as a class, you may not need more than an hour or two to consider alternatives. This is a small amount of time compared to the days or weeks you will spend implementing the class. The initial design experiments will probably result in a significantly better design, which will more than pay for the time spent designing it twice. For larger modules you’ll spend more time in the initial design explorations, but the implementation will also take longer, and the benefits of a better design will also be higher.

设计两次不需要花费很多额外的时间。对于较小的模块比如类,您可能不需要一两个小时去思考备选设计。与您将花费数天或数周时间来实现该类相比,这是很少的时间。最初的设计实验可能会导致明显更好的设计,这将比花两次设计时间所花的时间多。对于较大的模块,您将花费更多的时间进行初始设计探索,但是实现也将花费更长的时间,并且更好的设计所带来的好处也会更高。

I have noticed that the design-it-twice principle is sometimes hard for really smart people to embrace. When they are growing up, smart people discover that their first quick idea about any problem is sufficient for a good grade; there is no need to consider a second or third possibility. This makes it easy to develop bad work habits. However, as these people get older, they get promoted into environments with harder and harder problems. Eventually, everyone reaches a point where your first ideas are no longer good enough; if you want to get really great results, you have to consider a second possibility, or perhaps a third, no matter how smart you are. The design of large software systems falls in this category: no-one is good enough to get it right with their first try.

我已经注意到,真正聪明的人有时很难接受设计两次原则。当他们长大后,聪明的人会发现,他们对任何问题的第一个快速构想就足以取得良好的成绩。无需考虑第二种或第三种可能性。这使得容易养成不良的工作习惯。但是,随着这些人变老,他们将被提升到越来越困难的环境中。最终,每个人都达到了您的第一个想法不再足够好的境地。如果您想获得非常好的结果,那么无论您多么聪明,都必须考虑第二种可能性,或者第三种可能性。大型软件系统的设计属于此类:没有人足够优秀,能够第一次就做好。

Unfortunately, I often see smart people who insist on implementing the first idea that comes to mind, and this causes them to underperform their true potential (it also makes them frustrating to work with). Perhaps they subconsciously believe that “smart people get it right the first time,” so if they try multiple designs it would mean they are not smart after all. This is not the case. It isn’t that you aren’t smart; it’s that the problems are really hard! Furthermore, that’s a good thing: it’s much more fun to work on a difficult problem where you have to think carefully, rather than an easy problem where you don’t have to think at all.

不幸的是,我经常看到聪明的人坚持要实现第一个想到的想法,这会使他们无法发挥其真正的潜力(这也使他们沮丧地工作)。也许他们下意识地相信“聪明的人第一次就能做到”,因此,如果他们尝试多种设计,那将意味着他们并不聪明。不是这种情况。不是说你不聪明;而是问题真的很难解决!此外,这是一件好事:处理一个必须认真思考的难题比处理一个根本不需要思考的难题更有趣。

The design-it-twice approach not only improves your designs, but it also improves your design skills. The process of devising and comparing multiple approaches will teach you about the factors that make designs better or worse. Over time, this will make it easier for you to rule out bad designs and hone in on really great ones.

设计两次的方式不仅可以改善您的设计,而且可以提高您的设计能力。设计和比较多种方法的过程将教会你使设计变得更好或更差的因素。这将使你更容易排除不好的设计,并钻研真正伟大的设计。

',27)]))}const h=t(r,[["render",n],["__file","ch11.html.vue"]]),c=JSON.parse('{"path":"/ch11.html","title":"第 11 章 设计两次","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734161172000},"filePathRelative":"ch11.md"}');export{h as comp,c as data}; diff --git a/assets/ch12.html-DHfEHiJg.js b/assets/ch12.html-DoWOdQUQ.js similarity index 99% rename from assets/ch12.html-DHfEHiJg.js rename to assets/ch12.html-DoWOdQUQ.js index 2ac9dcc3..6a4fa5c8 100644 --- a/assets/ch12.html-DHfEHiJg.js +++ b/assets/ch12.html-DoWOdQUQ.js @@ -1 +1 @@ -import{_ as t,c as o,f as i,o as a}from"./app-CvqtBB8Z.js";const n={};function s(r,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 12 章 为什么要写注释?有四个理由

Chapter 12 Why Write Comments? The Four Excuses

In-code documentation plays a crucial role in software design. Comments are essential to help developers understand a system and work efficiently, but the role of comments goes beyond this. Documentation also plays an important role in abstraction; without comments, you can’t hide complexity. Finally, the process of writing comments, if done correctly, will actually improve a system’s design. Conversely, a good software design loses much of its value if it is poorly documented.

代码内文档在软件设计中起着至关重要的作用。注释对于帮助开发人员理解系统和有效工作至关重要,但是注释的作用不止于此。文档在抽象中也起着重要作用。没有注释,您就无法隐藏复杂性。最后,编写注释的过程(如果正确完成)实际上会改善系统的设计。相反,如果没有很好的文档记录,那么好的软件设计会失去很多价值。

Unfortunately, this view is not universally shared. A significant fraction of production code contains essentially no comments. Many developers think that comments are a waste of time; others see the value in comments, but somehow never get around to writing them. Fortunately, many development teams recognize the value of documentation, and it feels like the prevalence of these teams is gradually increasing. However, even in teams that encourage documentation, comments are often viewed as drudge work and many developers don’t understand how to write them, so the resulting documentation is often mediocre. Inadequate documentation creates a huge and unnecessary drag on software development.

不幸的是,这种观点并未得到普遍认同。生产代码的很大一部分基本上不包含任何注释。许多开发人员认为注释是浪费时间。其他人则看到了注释中的价值,但不知何故从不动手编写它们。幸运的是,许多开发团队认识到了文档的价值,并且感觉这样的团队越来越普及了。但是,即使在鼓励文档的团队中,注释也经常被视为繁琐的工作,而且许多开发人员也不了解如何编写注释,因此生成的文档通常是平庸的。文档不足会给软件开发带来巨大且不必要的拖累。

In this chapter I will discuss the excuses developers use to avoid writing comments, and the reasons why comments really do matter. Chapter 13 will then describe how to write good comments and the next few chapters after that will discuss related issues such as choosing variable names and how to use documentation to improve a system’s design. I hope these chapters will convince you of three things: good comments can make a big difference in the overall quality of software; it isn’t hard to write good comments; and (this may be hard to believe) writing comments can actually be fun.

在本章中,我将讨论开发人员用来避免写注释的借口,以及注释真正重要的原因。然后,第 13 章将描述如何编写好的注释,其后的几章将讨论相关问题,例如选择变量名以及如何使用文档来改进系统的设计。我希望这些章节能使您相信三件事:好的注释可以对软件的整体质量产生很大的影响;写好注释并不难;并且(可能很难相信)写注释实际上很有趣。

When developers don’t write comments, they usually justify their behavior with one or more of the following excuses:

当开发人员不写注释时,他们通常会以以下一种或多种借口为自己的行为辩护:

  • “Good code is self-documenting.”
  • “I don’t have time to write comments.”
  • “Comments get out of date and become misleading.”
  • “The comments I have seen are all worthless; why bother?” In the sections below I will address each of these excuses in turn.

  • “好的代码是自解释的。”
  • “我没有时间写注释。”
  • “注释过时,并会产生误导。”
  • “我所看到的注释都是毫无价值的;何必呢?” 在以下各节中,我将依次讨论这些借口。

12.1 Good code is self-documenting 好的代码是自解释的

Some people believe that if code is written well, it is so obvious that no comments are needed. This is a delicious myth, like a rumor that ice cream is good for your health: we’d really like to believe it! Unfortunately, it’s simply not true. To be sure, there are things you can do when writing code to reduce the need for comments, such as choosing good variable names (see Chapter 14). Nonetheless, there is still a significant amount of design information that can’t be represented in code. For example, only a small part of a class’s interface, such as the signatures of its methods, can be specified formally in the code. The informal aspects of an interface, such as a high-level description of what each method does or the meaning of its result, can only be described in comments. There are many other examples of things that can’t be described in the code, such as the rationale for a particular design decision, or the conditions under which it makes sense to call a particular method.

有人认为,如果代码编写得当,那么显而易见,不需要注释。这是一个美味的神话,就像谣言说冰淇淋对您的健康有益:我们真的很想相信!不幸的是,事实并非如此。可以肯定的是,在编写代码时可以做一些事情来减少对注释的需求,例如选择好的变量名(请参阅第 14 章)。尽管如此,仍有大量设计信息无法用代码表示。例如,只能在代码中正式指定类接口的一小部分,例如其方法的签名。接口的非正式方面,例如对每种方法的作用或其结果含义的高级描述,只能在注释中描述。还有许多代码中无法描述的东西,比如特定设计决策的基本原理,或者调用特定方法的条件。

Some developers argue that if others want to know what a method does, they should just read the code of the method: this will be more accurate than any comment. It’s possible that a reader could deduce the abstract interface of the method by reading its code, but it would be time-consuming and painful. In addition, if you write code with the expectation that users will read method implementations, you will try to make each method as short as possible, so that it’s easy to read. If the method does anything nontrivial, you will break it up into several smaller methods. This will result in a large number of shallow methods. Furthermore, it doesn’t really make the code easier to read: in order to understand the behavior of the top-level method, readers will probably need to understand the behaviors of the nested methods. For large systems it isn’t practical for users to read the code to learn the behavior.

一些开发人员认为,如果其他人想知道某个方法的作用,那么他们应该只阅读该方法的代码:这将比任何注释都更准确。读者可能会通过阅读其代码来推断该方法的抽象接口,但这既费时又痛苦。另外,如果在编写代码时期望用户会阅读方法实现,则将尝试使每个方法尽可能短,以便于阅读。如果该方法执行了一些重要操作,则将其分解为几个较小的方法。这将导致大量浅层方法。此外,它并没有真正使代码更易于阅读:为了理解顶层方法的行为,读者可能需要了解嵌套方法的行为。对于大型系统,用户通过阅读代码来学习行为是不切实际的。

Moreover, comments are fundamental to abstractions. Recall from Chapter 4 that the goal of abstractions is to hide complexity: an abstraction is a simplified view of an entity, which preserves essential information but omits details that can safely be ignored. If users must read the code of a method in order to use it, then there is no abstraction: all of the complexity of the method is exposed. Without comments, the only abstraction of a method is its declaration, which specifies its name and the names and types of its arguments and results. The declaration is missing too much essential information to provide a useful abstraction by itself. For example, a method to extract a substring might have two arguments, start and end, indicating the range of characters to extract. From the declaration alone, it isn’t possible to tell whether the extracted substring will include the character indicated by end, or what happens if start > end. Comments allow us to capture the additional information that callers need, thereby completing the simplified view while hiding implementation details. It’s also important that comments are written in a human language such as English; this makes them less precise than code, but it provides more expressive power, so we can create simple, intuitive descriptions. If you want to use abstractions to hide complexity, comments are essential.

此外,注释是抽象的基础。回顾第四章,抽象的目的是隐藏复杂性:抽象是实体的概览,它保留必要的信息,但忽略了可以放心地忽略的细节。如果用户必须阅读方法的代码才能使用它,则没有任何抽象可言:方法的所有复杂性都将暴露出来。没有注释,方法的唯一抽象就是其声明,该声明指定其名称以及其参数和结果的名称和类型。该声明缺少太多基本信息,无法单独提供有用的抽象。例如,提取子字符串的方法可能有两个参数,开始和结束,表示要提取的字符范围。仅凭声明,无法确定提取的子字符串是否将包含 end 指示的字符,或者如果 start > end 会发生什么。注释使我们能够捕获调用者所需的其他信息,从而在隐藏实现细节的同时完成简化的视图。用人类语言(例如英语)写注释也很重要;这使它们不如代码精确,但提供了更多的表达能力,因此我们可以创建简单直观的描述。如果要使用抽象来隐藏复杂性,则注释必不可少。

12.2 I don’t have time to write comments 我没有时间写注释

It’s tempting to prioritize comments lower than other development tasks. Given a choice between adding a new feature and documenting an existing feature, it seems logical to choose the new feature. However, software projects are almost always under time pressure, and there will always be things that seem higher priority than writing comments. Thus, if you allow documentation to be de-prioritized, you’ll end up with no documentation.

与其他开发任务相比,将注释的优先级降低是很诱人的。如果要在添加新功能和为现有功能写注释之间做出选择的话,选择新功能似乎合乎逻辑。但是,软件项目几乎总是处于时间压力之下,并且总会有比编写注释优先级更高的事情。因此,如果您允许取消文档的优先级,则最终将没有文档。

The counter-argument to this excuse is the investment mindset discussed on page 15. If you want a clean software structure, which will allow you to work efficiently over the long-term, then you must take some extra time up front in order to create that structure. Good comments make a huge difference in the maintainability of software, so the effort spent on them will pay for itself quickly. Furthermore, writing comments needn’t take a lot of time. Ask yourself how much of your development time you spend typing in code (as opposed to designing, compiling, testing, etc.), assuming you don’t include any comments; I doubt that the answer is more than 10%. Now suppose that you spend as much time typing comments as typing code; this should be a safe upper bound. With these assumptions, writing good comments won’t add more than about 10% to your development time. The benefits of having good documentation will quickly offset this cost.

反驳该借口的是第 15 页上讨论的投资心态。如果您想要一个干净的软件结构,可以长期有效地工作,那么您必须花一些额外的时间才能创建该结构。好的注释对软件的可维护性有很大的影响,因此花费在它们上面的精力将很快收回成本。此外,撰写注释不需要花费很多时间。询问自己,假设您不包含任何注释,那么您花费了多少开发时间来写代码(与设计,编译,测试等相比)。我怀疑答案是否超过 10%。现在假设您花在写注释上的时间与写代码所花费的时间一样多。这应该是一个安全的上限。基于这些假设,撰写好的注释不会增加您的开发时间约 10%。拥有良好文档的好处将迅速抵消这一成本。

Furthermore, many of the most important comments are those related to abstractions, such as the top-level documentation for classes and methods. Chapter 15 will argue that these comments should be written as part of the design process, and that the act of writing the documentation serves as an important design tool that improves the overall design. These comments pay for themselves immediately.

此外,许多最重要的注释是与抽象有关的,例如类和方法的顶级文档。第 15 章认为,这些评论应该作为设计过程的一部分来写,并且写文档的行为作为一个重要的设计工具来改进整体设计。这些评论马上就有回报了。

12.3 Comments get out of date and become misleading 注释过时并产生误导

Comments do sometimes get out of date, but this need not be a major problem in practice. Keeping documentation up-to-date does not require an enormous effort. Large changes to the documentation are only required if there have been large changes to the code, and the code changes will take more time than the documentation changes. Chapter 16 discusses how to organize documentation so that it is as easy as possible to keep it updated after code modifications (the key ideas are to avoid duplicated documentation and keep the documentation close to the corresponding code). Code reviews provide a great mechanism for detecting and fixing stale comments.

注释有时确实会过时,但这实践上并不是主要问题。使文档保持最新状态并不需要付出巨大的努力。仅当对代码进行了较大的更改时才需要对文档进行大的更改,并且代码更改将比文档的更改花费更多的时间。第 16 章讨论了如何组织文档,以便在修改代码后尽可能容易地对其进行更新(关键的思想是避免重复的文档,并保持文档接近相应的代码)。代码审查提供了一种检测和修复陈旧注释的强大机制。

12.4 All the comments I have seen are worthless 我所看到的所有注释都是毫无价值的

Of the four excuses, this is probably the one with the most merit. Every software developer has seen comments that provide no useful information, and most existing documentation is so-so at best. Fortunately, this problem is solvable; writing solid documentation is not hard, once you know how. The next chapters will lay out a framework for how to write good documentation and maintain it over time.

在这四个借口中,这可能是最有价值的借口。每个软件开发人员都看到过没有提供有用信息的注释,并且大多数现有文档充其量都是这样。幸运的是,这个问题是可以解决的。一旦你知道怎么做,写可靠的文档并不难。下一章将为如何编写良好的文档并随时间进行维护提供一个框架。

12.5 Benefits of well-written comments

Now that I have discussed (and, hopefully, debunked) the arguments against writing comments, let’s consider the benefits that you will get from good comments. The overall idea behind comments is to capture information that was in the mind of the designer but couldn’t be represented in the code. This information ranges from low-level details, such as a hardware quirk that motivates a particularly tricky piece of code, up to high-level concepts such as the rationale for a class. When other developers come along later to make modifications, the comments will allow them to work more quickly and accurately. Without documentation, future developers will have to rederive or guess at the developer’s original knowledge; this will take additional time, and there is a risk of bugs if the new developer misunderstands the original designer’s intentions. Comments are valuable even when the original designer is the one making the changes: if it has been more than a few weeks since you last worked in a piece of code, you will have forgotten many of the details of the original design.

既然我已经讨论了(并希望揭穿了这些)反对撰写注释的论点,让我们考虑一下从良好注释中将获得的好处。注释背后的总体思想是捕获设计者所想的但不能在代码中表示的信息。这些信息从低级详细信息(例如,激发特殊代码的硬件怪癖)到高级概念(例如,类的基本原理)。当其他开发人员稍后进行修改时,这些注释将使他们能够更快,更准确地工作。没有文档,未来的开发人员将不得不重新编写或猜测开发人员的原始知识。这将花费额外的时间,并且如果新开发者误解了原始设计者的意图,则存在错误的风险。即使是原作者在修改代码时注释也是有价值的:如果距离你最后一次在一段代码中工作已经有几个星期了,你会忘记最初设计的许多细节。

Chapter 2 described three ways in which complexity manifests itself in software systems:

第 2 章介绍了在软件系统中表现出复杂性的三种方式:

  • Change amplification: a seemingly simple change requires code modifications in many places.
  • Cognitive load: in order to make a change, the developer must accumulate a large amount of information.
  • Unknown unknowns: it is unclear what code needs to be modified, or what information must be considered in order to make those modifications.

  • 变更放大:看似简单的变更需要在许多地方进行代码修改。
  • 认知负荷:为了进行更改,开发人员必须积累大量信息。
  • 意料之外的或不可预见的情况:尚不清楚需要修改哪些代码,或必须考虑哪些信息才能进行这些修改。

Good documentation helps with the last two of these issues. Documentation can reduce cognitive load by providing developers with the information they need to make changes and by making it easy for developers to ignore information that is irrelevant. Without adequate documentation, developers may have to read large amounts of code to reconstruct what was in the designer’s mind. Documentation can also reduce the unknown unknowns by clarifying the structure of the system, so that it is clear what information and code is relevant for any given change.

好的文档可以帮助解决最后两个问题。通过为开发人员提供他们进行更改所需的信息,并使开发人员容易忽略不相关的信息,文档可以减轻认知负担。没有足够的文档,开发人员可能必须阅读大量代码才能重构设计人员的想法。文档还可以通过阐明系统的结构来减少意料之外的或不可预见的情况,从而可以清楚地了解与任何给定更改相关的信息和代码。

Chapter 2 pointed out that the primary causes of complexity are dependencies and obscurity. Good documentation can clarify dependencies, and it fills in gaps to eliminate obscurity.

第 2 章指出,导致复杂性的主要原因是依赖性和模糊性。好的文档可以阐明依赖关系,并且可以填补空白以消除模糊性。

The next few chapters will show you how to write good documentation. They will also discuss how to integrate documentation-writing into the design process so that it improves the design of your software.

接下来的几章将向您展示如何编写好的文档。他们还将讨论如何将文档编写集成到设计过程中,从而改善软件设计。

',47)]))}const h=t(n,[["render",s],["__file","ch12.html.vue"]]),c=JSON.parse('{"path":"/ch12.html","title":"第 12 章 为什么要写注释?有四个理由","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"12.1 Good code is self-documenting 好的代码是自解释的","slug":"_12-1-good-code-is-self-documenting-好的代码是自解释的","link":"#_12-1-good-code-is-self-documenting-好的代码是自解释的","children":[]},{"level":2,"title":"12.2 I don’t have time to write comments 我没有时间写注释","slug":"_12-2-i-don-t-have-time-to-write-comments-我没有时间写注释","link":"#_12-2-i-don-t-have-time-to-write-comments-我没有时间写注释","children":[]},{"level":2,"title":"12.3 Comments get out of date and become misleading 注释过时并产生误导","slug":"_12-3-comments-get-out-of-date-and-become-misleading-注释过时并产生误导","link":"#_12-3-comments-get-out-of-date-and-become-misleading-注释过时并产生误导","children":[]},{"level":2,"title":"12.4 All the comments I have seen are worthless 我所看到的所有注释都是毫无价值的","slug":"_12-4-all-the-comments-i-have-seen-are-worthless-我所看到的所有注释都是毫无价值的","link":"#_12-4-all-the-comments-i-have-seen-are-worthless-我所看到的所有注释都是毫无价值的","children":[]},{"level":2,"title":"12.5 Benefits of well-written comments","slug":"_12-5-benefits-of-well-written-comments","link":"#_12-5-benefits-of-well-written-comments","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch12.md"}');export{h as comp,c as data}; +import{_ as t,c as o,f as i,o as a}from"./app-BQdhfat9.js";const n={};function s(r,e){return a(),o("div",null,e[0]||(e[0]=[i('

第 12 章 为什么要写注释?有四个理由

Chapter 12 Why Write Comments? The Four Excuses

In-code documentation plays a crucial role in software design. Comments are essential to help developers understand a system and work efficiently, but the role of comments goes beyond this. Documentation also plays an important role in abstraction; without comments, you can’t hide complexity. Finally, the process of writing comments, if done correctly, will actually improve a system’s design. Conversely, a good software design loses much of its value if it is poorly documented.

代码内文档在软件设计中起着至关重要的作用。注释对于帮助开发人员理解系统和有效工作至关重要,但是注释的作用不止于此。文档在抽象中也起着重要作用。没有注释,您就无法隐藏复杂性。最后,编写注释的过程(如果正确完成)实际上会改善系统的设计。相反,如果没有很好的文档记录,那么好的软件设计会失去很多价值。

Unfortunately, this view is not universally shared. A significant fraction of production code contains essentially no comments. Many developers think that comments are a waste of time; others see the value in comments, but somehow never get around to writing them. Fortunately, many development teams recognize the value of documentation, and it feels like the prevalence of these teams is gradually increasing. However, even in teams that encourage documentation, comments are often viewed as drudge work and many developers don’t understand how to write them, so the resulting documentation is often mediocre. Inadequate documentation creates a huge and unnecessary drag on software development.

不幸的是,这种观点并未得到普遍认同。生产代码的很大一部分基本上不包含任何注释。许多开发人员认为注释是浪费时间。其他人则看到了注释中的价值,但不知何故从不动手编写它们。幸运的是,许多开发团队认识到了文档的价值,并且感觉这样的团队越来越普及了。但是,即使在鼓励文档的团队中,注释也经常被视为繁琐的工作,而且许多开发人员也不了解如何编写注释,因此生成的文档通常是平庸的。文档不足会给软件开发带来巨大且不必要的拖累。

In this chapter I will discuss the excuses developers use to avoid writing comments, and the reasons why comments really do matter. Chapter 13 will then describe how to write good comments and the next few chapters after that will discuss related issues such as choosing variable names and how to use documentation to improve a system’s design. I hope these chapters will convince you of three things: good comments can make a big difference in the overall quality of software; it isn’t hard to write good comments; and (this may be hard to believe) writing comments can actually be fun.

在本章中,我将讨论开发人员用来避免写注释的借口,以及注释真正重要的原因。然后,第 13 章将描述如何编写好的注释,其后的几章将讨论相关问题,例如选择变量名以及如何使用文档来改进系统的设计。我希望这些章节能使您相信三件事:好的注释可以对软件的整体质量产生很大的影响;写好注释并不难;并且(可能很难相信)写注释实际上很有趣。

When developers don’t write comments, they usually justify their behavior with one or more of the following excuses:

当开发人员不写注释时,他们通常会以以下一种或多种借口为自己的行为辩护:

  • “Good code is self-documenting.”
  • “I don’t have time to write comments.”
  • “Comments get out of date and become misleading.”
  • “The comments I have seen are all worthless; why bother?” In the sections below I will address each of these excuses in turn.

  • “好的代码是自解释的。”
  • “我没有时间写注释。”
  • “注释过时,并会产生误导。”
  • “我所看到的注释都是毫无价值的;何必呢?” 在以下各节中,我将依次讨论这些借口。

12.1 Good code is self-documenting 好的代码是自解释的

Some people believe that if code is written well, it is so obvious that no comments are needed. This is a delicious myth, like a rumor that ice cream is good for your health: we’d really like to believe it! Unfortunately, it’s simply not true. To be sure, there are things you can do when writing code to reduce the need for comments, such as choosing good variable names (see Chapter 14). Nonetheless, there is still a significant amount of design information that can’t be represented in code. For example, only a small part of a class’s interface, such as the signatures of its methods, can be specified formally in the code. The informal aspects of an interface, such as a high-level description of what each method does or the meaning of its result, can only be described in comments. There are many other examples of things that can’t be described in the code, such as the rationale for a particular design decision, or the conditions under which it makes sense to call a particular method.

有人认为,如果代码编写得当,那么显而易见,不需要注释。这是一个美味的神话,就像谣言说冰淇淋对您的健康有益:我们真的很想相信!不幸的是,事实并非如此。可以肯定的是,在编写代码时可以做一些事情来减少对注释的需求,例如选择好的变量名(请参阅第 14 章)。尽管如此,仍有大量设计信息无法用代码表示。例如,只能在代码中正式指定类接口的一小部分,例如其方法的签名。接口的非正式方面,例如对每种方法的作用或其结果含义的高级描述,只能在注释中描述。还有许多代码中无法描述的东西,比如特定设计决策的基本原理,或者调用特定方法的条件。

Some developers argue that if others want to know what a method does, they should just read the code of the method: this will be more accurate than any comment. It’s possible that a reader could deduce the abstract interface of the method by reading its code, but it would be time-consuming and painful. In addition, if you write code with the expectation that users will read method implementations, you will try to make each method as short as possible, so that it’s easy to read. If the method does anything nontrivial, you will break it up into several smaller methods. This will result in a large number of shallow methods. Furthermore, it doesn’t really make the code easier to read: in order to understand the behavior of the top-level method, readers will probably need to understand the behaviors of the nested methods. For large systems it isn’t practical for users to read the code to learn the behavior.

一些开发人员认为,如果其他人想知道某个方法的作用,那么他们应该只阅读该方法的代码:这将比任何注释都更准确。读者可能会通过阅读其代码来推断该方法的抽象接口,但这既费时又痛苦。另外,如果在编写代码时期望用户会阅读方法实现,则将尝试使每个方法尽可能短,以便于阅读。如果该方法执行了一些重要操作,则将其分解为几个较小的方法。这将导致大量浅层方法。此外,它并没有真正使代码更易于阅读:为了理解顶层方法的行为,读者可能需要了解嵌套方法的行为。对于大型系统,用户通过阅读代码来学习行为是不切实际的。

Moreover, comments are fundamental to abstractions. Recall from Chapter 4 that the goal of abstractions is to hide complexity: an abstraction is a simplified view of an entity, which preserves essential information but omits details that can safely be ignored. If users must read the code of a method in order to use it, then there is no abstraction: all of the complexity of the method is exposed. Without comments, the only abstraction of a method is its declaration, which specifies its name and the names and types of its arguments and results. The declaration is missing too much essential information to provide a useful abstraction by itself. For example, a method to extract a substring might have two arguments, start and end, indicating the range of characters to extract. From the declaration alone, it isn’t possible to tell whether the extracted substring will include the character indicated by end, or what happens if start > end. Comments allow us to capture the additional information that callers need, thereby completing the simplified view while hiding implementation details. It’s also important that comments are written in a human language such as English; this makes them less precise than code, but it provides more expressive power, so we can create simple, intuitive descriptions. If you want to use abstractions to hide complexity, comments are essential.

此外,注释是抽象的基础。回顾第四章,抽象的目的是隐藏复杂性:抽象是实体的概览,它保留必要的信息,但忽略了可以放心地忽略的细节。如果用户必须阅读方法的代码才能使用它,则没有任何抽象可言:方法的所有复杂性都将暴露出来。没有注释,方法的唯一抽象就是其声明,该声明指定其名称以及其参数和结果的名称和类型。该声明缺少太多基本信息,无法单独提供有用的抽象。例如,提取子字符串的方法可能有两个参数,开始和结束,表示要提取的字符范围。仅凭声明,无法确定提取的子字符串是否将包含 end 指示的字符,或者如果 start > end 会发生什么。注释使我们能够捕获调用者所需的其他信息,从而在隐藏实现细节的同时完成简化的视图。用人类语言(例如英语)写注释也很重要;这使它们不如代码精确,但提供了更多的表达能力,因此我们可以创建简单直观的描述。如果要使用抽象来隐藏复杂性,则注释必不可少。

12.2 I don’t have time to write comments 我没有时间写注释

It’s tempting to prioritize comments lower than other development tasks. Given a choice between adding a new feature and documenting an existing feature, it seems logical to choose the new feature. However, software projects are almost always under time pressure, and there will always be things that seem higher priority than writing comments. Thus, if you allow documentation to be de-prioritized, you’ll end up with no documentation.

与其他开发任务相比,将注释的优先级降低是很诱人的。如果要在添加新功能和为现有功能写注释之间做出选择的话,选择新功能似乎合乎逻辑。但是,软件项目几乎总是处于时间压力之下,并且总会有比编写注释优先级更高的事情。因此,如果您允许取消文档的优先级,则最终将没有文档。

The counter-argument to this excuse is the investment mindset discussed on page 15. If you want a clean software structure, which will allow you to work efficiently over the long-term, then you must take some extra time up front in order to create that structure. Good comments make a huge difference in the maintainability of software, so the effort spent on them will pay for itself quickly. Furthermore, writing comments needn’t take a lot of time. Ask yourself how much of your development time you spend typing in code (as opposed to designing, compiling, testing, etc.), assuming you don’t include any comments; I doubt that the answer is more than 10%. Now suppose that you spend as much time typing comments as typing code; this should be a safe upper bound. With these assumptions, writing good comments won’t add more than about 10% to your development time. The benefits of having good documentation will quickly offset this cost.

反驳该借口的是第 15 页上讨论的投资心态。如果您想要一个干净的软件结构,可以长期有效地工作,那么您必须花一些额外的时间才能创建该结构。好的注释对软件的可维护性有很大的影响,因此花费在它们上面的精力将很快收回成本。此外,撰写注释不需要花费很多时间。询问自己,假设您不包含任何注释,那么您花费了多少开发时间来写代码(与设计,编译,测试等相比)。我怀疑答案是否超过 10%。现在假设您花在写注释上的时间与写代码所花费的时间一样多。这应该是一个安全的上限。基于这些假设,撰写好的注释不会增加您的开发时间约 10%。拥有良好文档的好处将迅速抵消这一成本。

Furthermore, many of the most important comments are those related to abstractions, such as the top-level documentation for classes and methods. Chapter 15 will argue that these comments should be written as part of the design process, and that the act of writing the documentation serves as an important design tool that improves the overall design. These comments pay for themselves immediately.

此外,许多最重要的注释是与抽象有关的,例如类和方法的顶级文档。第 15 章认为,这些评论应该作为设计过程的一部分来写,并且写文档的行为作为一个重要的设计工具来改进整体设计。这些评论马上就有回报了。

12.3 Comments get out of date and become misleading 注释过时并产生误导

Comments do sometimes get out of date, but this need not be a major problem in practice. Keeping documentation up-to-date does not require an enormous effort. Large changes to the documentation are only required if there have been large changes to the code, and the code changes will take more time than the documentation changes. Chapter 16 discusses how to organize documentation so that it is as easy as possible to keep it updated after code modifications (the key ideas are to avoid duplicated documentation and keep the documentation close to the corresponding code). Code reviews provide a great mechanism for detecting and fixing stale comments.

注释有时确实会过时,但这实践上并不是主要问题。使文档保持最新状态并不需要付出巨大的努力。仅当对代码进行了较大的更改时才需要对文档进行大的更改,并且代码更改将比文档的更改花费更多的时间。第 16 章讨论了如何组织文档,以便在修改代码后尽可能容易地对其进行更新(关键的思想是避免重复的文档,并保持文档接近相应的代码)。代码审查提供了一种检测和修复陈旧注释的强大机制。

12.4 All the comments I have seen are worthless 我所看到的所有注释都是毫无价值的

Of the four excuses, this is probably the one with the most merit. Every software developer has seen comments that provide no useful information, and most existing documentation is so-so at best. Fortunately, this problem is solvable; writing solid documentation is not hard, once you know how. The next chapters will lay out a framework for how to write good documentation and maintain it over time.

在这四个借口中,这可能是最有价值的借口。每个软件开发人员都看到过没有提供有用信息的注释,并且大多数现有文档充其量都是这样。幸运的是,这个问题是可以解决的。一旦你知道怎么做,写可靠的文档并不难。下一章将为如何编写良好的文档并随时间进行维护提供一个框架。

12.5 Benefits of well-written comments

Now that I have discussed (and, hopefully, debunked) the arguments against writing comments, let’s consider the benefits that you will get from good comments. The overall idea behind comments is to capture information that was in the mind of the designer but couldn’t be represented in the code. This information ranges from low-level details, such as a hardware quirk that motivates a particularly tricky piece of code, up to high-level concepts such as the rationale for a class. When other developers come along later to make modifications, the comments will allow them to work more quickly and accurately. Without documentation, future developers will have to rederive or guess at the developer’s original knowledge; this will take additional time, and there is a risk of bugs if the new developer misunderstands the original designer’s intentions. Comments are valuable even when the original designer is the one making the changes: if it has been more than a few weeks since you last worked in a piece of code, you will have forgotten many of the details of the original design.

既然我已经讨论了(并希望揭穿了这些)反对撰写注释的论点,让我们考虑一下从良好注释中将获得的好处。注释背后的总体思想是捕获设计者所想的但不能在代码中表示的信息。这些信息从低级详细信息(例如,激发特殊代码的硬件怪癖)到高级概念(例如,类的基本原理)。当其他开发人员稍后进行修改时,这些注释将使他们能够更快,更准确地工作。没有文档,未来的开发人员将不得不重新编写或猜测开发人员的原始知识。这将花费额外的时间,并且如果新开发者误解了原始设计者的意图,则存在错误的风险。即使是原作者在修改代码时注释也是有价值的:如果距离你最后一次在一段代码中工作已经有几个星期了,你会忘记最初设计的许多细节。

Chapter 2 described three ways in which complexity manifests itself in software systems:

第 2 章介绍了在软件系统中表现出复杂性的三种方式:

  • Change amplification: a seemingly simple change requires code modifications in many places.
  • Cognitive load: in order to make a change, the developer must accumulate a large amount of information.
  • Unknown unknowns: it is unclear what code needs to be modified, or what information must be considered in order to make those modifications.

  • 变更放大:看似简单的变更需要在许多地方进行代码修改。
  • 认知负荷:为了进行更改,开发人员必须积累大量信息。
  • 意料之外的或不可预见的情况:尚不清楚需要修改哪些代码,或必须考虑哪些信息才能进行这些修改。

Good documentation helps with the last two of these issues. Documentation can reduce cognitive load by providing developers with the information they need to make changes and by making it easy for developers to ignore information that is irrelevant. Without adequate documentation, developers may have to read large amounts of code to reconstruct what was in the designer’s mind. Documentation can also reduce the unknown unknowns by clarifying the structure of the system, so that it is clear what information and code is relevant for any given change.

好的文档可以帮助解决最后两个问题。通过为开发人员提供他们进行更改所需的信息,并使开发人员容易忽略不相关的信息,文档可以减轻认知负担。没有足够的文档,开发人员可能必须阅读大量代码才能重构设计人员的想法。文档还可以通过阐明系统的结构来减少意料之外的或不可预见的情况,从而可以清楚地了解与任何给定更改相关的信息和代码。

Chapter 2 pointed out that the primary causes of complexity are dependencies and obscurity. Good documentation can clarify dependencies, and it fills in gaps to eliminate obscurity.

第 2 章指出,导致复杂性的主要原因是依赖性和模糊性。好的文档可以阐明依赖关系,并且可以填补空白以消除模糊性。

The next few chapters will show you how to write good documentation. They will also discuss how to integrate documentation-writing into the design process so that it improves the design of your software.

接下来的几章将向您展示如何编写好的文档。他们还将讨论如何将文档编写集成到设计过程中,从而改善软件设计。

',47)]))}const h=t(n,[["render",s],["__file","ch12.html.vue"]]),c=JSON.parse('{"path":"/ch12.html","title":"第 12 章 为什么要写注释?有四个理由","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"12.1 Good code is self-documenting 好的代码是自解释的","slug":"_12-1-good-code-is-self-documenting-好的代码是自解释的","link":"#_12-1-good-code-is-self-documenting-好的代码是自解释的","children":[]},{"level":2,"title":"12.2 I don’t have time to write comments 我没有时间写注释","slug":"_12-2-i-don-t-have-time-to-write-comments-我没有时间写注释","link":"#_12-2-i-don-t-have-time-to-write-comments-我没有时间写注释","children":[]},{"level":2,"title":"12.3 Comments get out of date and become misleading 注释过时并产生误导","slug":"_12-3-comments-get-out-of-date-and-become-misleading-注释过时并产生误导","link":"#_12-3-comments-get-out-of-date-and-become-misleading-注释过时并产生误导","children":[]},{"level":2,"title":"12.4 All the comments I have seen are worthless 我所看到的所有注释都是毫无价值的","slug":"_12-4-all-the-comments-i-have-seen-are-worthless-我所看到的所有注释都是毫无价值的","link":"#_12-4-all-the-comments-i-have-seen-are-worthless-我所看到的所有注释都是毫无价值的","children":[]},{"level":2,"title":"12.5 Benefits of well-written comments","slug":"_12-5-benefits-of-well-written-comments","link":"#_12-5-benefits-of-well-written-comments","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch12.md"}');export{h as comp,c as data}; diff --git a/assets/ch13.html-Ca7OIfBM.js b/assets/ch13.html-BAyTk7fJ.js similarity index 99% rename from assets/ch13.html-Ca7OIfBM.js rename to assets/ch13.html-BAyTk7fJ.js index 8f524354..335d13b8 100644 --- a/assets/ch13.html-Ca7OIfBM.js +++ b/assets/ch13.html-BAyTk7fJ.js @@ -1,4 +1,4 @@ -import{_ as n,c as s,f as a,o as t}from"./app-CvqtBB8Z.js";const o={};function i(l,e){return t(),s("div",null,e[0]||(e[0]=[a(`

第 13 章 注释应该描述代码中不明显的内容

Chapter 13 Comments Should Describe Things that Aren’t Obvious from the Code

The reason for writing comments is that statements in a programming language can’t capture all of the important information that was in the mind of the developer when the code was written. Comments record this information so that developers who come along later can easily understand and modify the code. The guiding principle for comments is that comments should describe things that aren’t obvious from the code.

编写注释的原因是,使用编程语言编写的语句无法捕获编写代码时开发人员想到的所有重要信息。注释记录了这些信息,以便后来的开发人员可以轻松地理解和修改代码。注释的指导原则是,注释应描述代码中不明显的内容

There are many things that aren’t obvious from the code. Sometimes it’s low-level details that aren’t obvious. For example, when a pair of indices describe a range, it isn’t obvious whether the elements given by the indices are inside the range or out. Sometimes it’s not clear why code is needed, or why it was implemented in a particular way. Sometimes there are rules the developer followed, such as “always invoke a before b.” You might be able to guess at a rule by looking at all of the code, but this is painful and error-prone; a comment can make the rule explicit and clear.

从代码来看,有许多事情并不明显。有时,底层细节并不明显。例如,当一对索引描述一个范围时,由索引给出的元素是在范围之内还是之外并不明显。有时不清楚为什么需要代码,或者为什么要以特定方式实现代码。有时,开发人员遵循一些规则,例如“总是在 b 之前调用 a”。您可能可以通过查看所有代码来猜测规则,但这很痛苦且容易出错。注释可以使规则清晰明了。

One of the most important reasons for comments is abstractions, which include a lot of information that isn’t obvious from the code. The idea of an abstraction is to provide a simple way of thinking about something, but code is so detailed that it can be hard to see the abstraction just from reading the code. Comments can provide a simpler, higher-level view (“after this method is invoked, network traffic will be limited to maxBandwidth bytes per second”). Even if this information can be deduced by reading the code, we don’t want to force users of a module to do that: reading the code is time-consuming and forces them to consider a lot of information that isn’t needed to use the module. Developers should be able to understand the abstraction provided by a module without reading any code other than its externally visible declarations. The only way to do this is by supplementing the declarations with comments.

注释的最重要原因之一是抽象,其中包括许多从代码中看不到的信息。抽象的思想是提供一种思考问题的简单方法,但是代码是如此详细,以至于仅通过阅读代码就很难看到抽象。注释可以提供一个更简单,更高级的预览(“调用此方法后,网络流量将被限制为每秒 maxBandwidth 字节”)。即使可以通过阅读代码推断出此信息,我们也不想强迫模块用户这样做:阅读代码很耗时,并且迫使他们考虑很多使用该模块不需要的信息。开发人员应该能够理解模块提供的抽象,而无需阅读其外部可见声明以外的任何代码。唯一的方法是用注释来补充声明。

This chapter discusses what information needs to be described in comments and how to write good comments. As you will see, good comments typically explain things at a different level of detail than the code, which is more detailed in some situations and less detailed (more abstract) in others.

本章讨论需要在注释中描述哪些信息以及如何编写良好的注释。就像您将看到的那样,好的注释通常以与代码不同的详细程度来解释事物,在某些情况下,注释会更详细,而在某些情况下,较不详细(更抽象)。

13.1 Pick conventions 选择约定

The first step in writing comments is to decide on conventions for commenting, such as what you will comment and the format you will use for comments. If you are programming in a language for which there exists a document compilation tool, such as Javadoc for Java, Doxygen for C++, or godoc for Go!, follow the conventions of the tools. None of these conventions is perfect, but the tools provide enough benefits to make up for that. If you are programming in an environment where there are no existing conventions to follow, try to adopt the conventions from some other language or project that is similar; this will make it easier for other developers to understand and adhere to your conventions.

编写注释的第一步是确定注释的约定,例如您要注释的内容和注释的格式。如果您正在使用存在文档编译工具的语言进行编程,例如 Java 的 Javadoc, C++ 的 Doxygen 或 Go!的 godoc,请遵循工具的约定。这些约定都不是完美的,但是这些工具可提供足够的好处来弥补这一缺点。如果在没有现有约定可遵循的环境中进行编程,请尝试从其他类似的语言或项目中采用这些约定;这将使其他开发人员更容易理解和遵守您的约定。

Conventions serve two purposes. First, they ensure consistency, which makes comments easier to read and understand. Second, they help to ensure that you actually write comments. If you don’t have a clear idea what you are going to comment and how, it’s easy to end up writing no comments at all.

约定有两个目的。首先,它们确保一致性,这使得注释更易于阅读和理解。其次,它们有助于确保您实际编写评论。如果您不清楚要注释的内容以及写注释的方式,那么很容易最终根本不写注释。

Most comments fall into one of the following categories:

大多数注释属于以下类别之一:

Interface: a comment block that immediately precedes the declaration of a module such as a class, data structure, function, or method. The comment describe’s the module’s interface. For a class, the comment describes the overall abstraction provided by the class. For a method or function, the comment describes its overall behavior, its arguments and return value, if any, any side effects or exceptions that it generates, and any other requirements the caller must satisfy before invoking the method.

接口:在模块声明(例如类,数据结构,函数或方法)之前的注释块。该注释描述模块的接口。对于一个类,注释描述了该类提供的整体抽象。对于方法或函数,注释描述其整体行为,其参数和返回值(如果有),其生成的任何副作用或异常,以及调用者在调用该方法之前必须满足的任何其他要求。

Data structure member: a comment next to the declaration of a field in a data structure, such as an instance variable or static variable for a class.

数据结构成员:数据结构中字段声明旁边的注释,例如类的实例变量或静态变量。

Implementation comment: a comment inside the code of a method or function, which describes how the code works internally.

实现注释:方法或函数代码内部的注释,它描述代码在内部的工作方式。

Cross-module comment: a comment describing dependencies that cross module boundaries.

跨模块注释:描述跨模块边界的依赖项的注释。

The most important comments are those in the first two categories. Every class should have an interface comment, every class variable should have a comment, and every method should have an interface comment. Occasionally, the declaration for a variable or method is so obvious that there is nothing useful to add in a comment (getters and setters sometimes fall in this category), but this is rare; it is easier to comment everything rather than spend energy worrying about whether a comment is needed. Implementation comments are often unnecessary (see Section 13.6 below). Cross-module comments are the most rare of all and they are problematic to write, but when they are needed they are quite important; Section 13.7 discusses them in more detail.

最重要的注释是前两个类别中的注释。每个类都应有一个接口注释,每个类变量应有一个注释,每个方法都应有一个接口注释。有时,变量或方法的声明是如此明显,以至于在注释中没有添加任何有用的东西(getter 和 setter 有时都属于此类),但这很少见。注释所有内容要比花精力担心是否需要注释要容易得多。具体实现的注释通常是不必要的(请参阅下面的 13.6 节)。跨模块注释是最罕见的,而且编写起来很成问题,但是当需要它们时,它们就很重要。第 13.7 节将更详细地讨论它们。

13.2 Don’t repeat the code 不要重复代码

Unfortunately, many comments are not particularly helpful. The most common reason is that the comments repeat the code: all of the information in the comment can easily be deduced from the code next to the comment. Here is a code sample that appeared in a recent research paper:

不幸的是,许多注释并不是特别有用。最常见的原因是注释重复了代码:可以轻松地从注释旁边的代码中推断出注释中的所有信息。这是最近研究论文中出现的代码示例:

ptr_copy = get_copy(obj)            # Get pointer copy
+import{_ as n,c as s,f as a,o as t}from"./app-BQdhfat9.js";const o={};function i(l,e){return t(),s("div",null,e[0]||(e[0]=[a(`

第 13 章 注释应该描述代码中不明显的内容

Chapter 13 Comments Should Describe Things that Aren’t Obvious from the Code

The reason for writing comments is that statements in a programming language can’t capture all of the important information that was in the mind of the developer when the code was written. Comments record this information so that developers who come along later can easily understand and modify the code. The guiding principle for comments is that comments should describe things that aren’t obvious from the code.

编写注释的原因是,使用编程语言编写的语句无法捕获编写代码时开发人员想到的所有重要信息。注释记录了这些信息,以便后来的开发人员可以轻松地理解和修改代码。注释的指导原则是,注释应描述代码中不明显的内容

There are many things that aren’t obvious from the code. Sometimes it’s low-level details that aren’t obvious. For example, when a pair of indices describe a range, it isn’t obvious whether the elements given by the indices are inside the range or out. Sometimes it’s not clear why code is needed, or why it was implemented in a particular way. Sometimes there are rules the developer followed, such as “always invoke a before b.” You might be able to guess at a rule by looking at all of the code, but this is painful and error-prone; a comment can make the rule explicit and clear.

从代码来看,有许多事情并不明显。有时,底层细节并不明显。例如,当一对索引描述一个范围时,由索引给出的元素是在范围之内还是之外并不明显。有时不清楚为什么需要代码,或者为什么要以特定方式实现代码。有时,开发人员遵循一些规则,例如“总是在 b 之前调用 a”。您可能可以通过查看所有代码来猜测规则,但这很痛苦且容易出错。注释可以使规则清晰明了。

One of the most important reasons for comments is abstractions, which include a lot of information that isn’t obvious from the code. The idea of an abstraction is to provide a simple way of thinking about something, but code is so detailed that it can be hard to see the abstraction just from reading the code. Comments can provide a simpler, higher-level view (“after this method is invoked, network traffic will be limited to maxBandwidth bytes per second”). Even if this information can be deduced by reading the code, we don’t want to force users of a module to do that: reading the code is time-consuming and forces them to consider a lot of information that isn’t needed to use the module. Developers should be able to understand the abstraction provided by a module without reading any code other than its externally visible declarations. The only way to do this is by supplementing the declarations with comments.

注释的最重要原因之一是抽象,其中包括许多从代码中看不到的信息。抽象的思想是提供一种思考问题的简单方法,但是代码是如此详细,以至于仅通过阅读代码就很难看到抽象。注释可以提供一个更简单,更高级的预览(“调用此方法后,网络流量将被限制为每秒 maxBandwidth 字节”)。即使可以通过阅读代码推断出此信息,我们也不想强迫模块用户这样做:阅读代码很耗时,并且迫使他们考虑很多使用该模块不需要的信息。开发人员应该能够理解模块提供的抽象,而无需阅读其外部可见声明以外的任何代码。唯一的方法是用注释来补充声明。

This chapter discusses what information needs to be described in comments and how to write good comments. As you will see, good comments typically explain things at a different level of detail than the code, which is more detailed in some situations and less detailed (more abstract) in others.

本章讨论需要在注释中描述哪些信息以及如何编写良好的注释。就像您将看到的那样,好的注释通常以与代码不同的详细程度来解释事物,在某些情况下,注释会更详细,而在某些情况下,较不详细(更抽象)。

13.1 Pick conventions 选择约定

The first step in writing comments is to decide on conventions for commenting, such as what you will comment and the format you will use for comments. If you are programming in a language for which there exists a document compilation tool, such as Javadoc for Java, Doxygen for C++, or godoc for Go!, follow the conventions of the tools. None of these conventions is perfect, but the tools provide enough benefits to make up for that. If you are programming in an environment where there are no existing conventions to follow, try to adopt the conventions from some other language or project that is similar; this will make it easier for other developers to understand and adhere to your conventions.

编写注释的第一步是确定注释的约定,例如您要注释的内容和注释的格式。如果您正在使用存在文档编译工具的语言进行编程,例如 Java 的 Javadoc, C++ 的 Doxygen 或 Go!的 godoc,请遵循工具的约定。这些约定都不是完美的,但是这些工具可提供足够的好处来弥补这一缺点。如果在没有现有约定可遵循的环境中进行编程,请尝试从其他类似的语言或项目中采用这些约定;这将使其他开发人员更容易理解和遵守您的约定。

Conventions serve two purposes. First, they ensure consistency, which makes comments easier to read and understand. Second, they help to ensure that you actually write comments. If you don’t have a clear idea what you are going to comment and how, it’s easy to end up writing no comments at all.

约定有两个目的。首先,它们确保一致性,这使得注释更易于阅读和理解。其次,它们有助于确保您实际编写评论。如果您不清楚要注释的内容以及写注释的方式,那么很容易最终根本不写注释。

Most comments fall into one of the following categories:

大多数注释属于以下类别之一:

Interface: a comment block that immediately precedes the declaration of a module such as a class, data structure, function, or method. The comment describe’s the module’s interface. For a class, the comment describes the overall abstraction provided by the class. For a method or function, the comment describes its overall behavior, its arguments and return value, if any, any side effects or exceptions that it generates, and any other requirements the caller must satisfy before invoking the method.

接口:在模块声明(例如类,数据结构,函数或方法)之前的注释块。该注释描述模块的接口。对于一个类,注释描述了该类提供的整体抽象。对于方法或函数,注释描述其整体行为,其参数和返回值(如果有),其生成的任何副作用或异常,以及调用者在调用该方法之前必须满足的任何其他要求。

Data structure member: a comment next to the declaration of a field in a data structure, such as an instance variable or static variable for a class.

数据结构成员:数据结构中字段声明旁边的注释,例如类的实例变量或静态变量。

Implementation comment: a comment inside the code of a method or function, which describes how the code works internally.

实现注释:方法或函数代码内部的注释,它描述代码在内部的工作方式。

Cross-module comment: a comment describing dependencies that cross module boundaries.

跨模块注释:描述跨模块边界的依赖项的注释。

The most important comments are those in the first two categories. Every class should have an interface comment, every class variable should have a comment, and every method should have an interface comment. Occasionally, the declaration for a variable or method is so obvious that there is nothing useful to add in a comment (getters and setters sometimes fall in this category), but this is rare; it is easier to comment everything rather than spend energy worrying about whether a comment is needed. Implementation comments are often unnecessary (see Section 13.6 below). Cross-module comments are the most rare of all and they are problematic to write, but when they are needed they are quite important; Section 13.7 discusses them in more detail.

最重要的注释是前两个类别中的注释。每个类都应有一个接口注释,每个类变量应有一个注释,每个方法都应有一个接口注释。有时,变量或方法的声明是如此明显,以至于在注释中没有添加任何有用的东西(getter 和 setter 有时都属于此类),但这很少见。注释所有内容要比花精力担心是否需要注释要容易得多。具体实现的注释通常是不必要的(请参阅下面的 13.6 节)。跨模块注释是最罕见的,而且编写起来很成问题,但是当需要它们时,它们就很重要。第 13.7 节将更详细地讨论它们。

13.2 Don’t repeat the code 不要重复代码

Unfortunately, many comments are not particularly helpful. The most common reason is that the comments repeat the code: all of the information in the comment can easily be deduced from the code next to the comment. Here is a code sample that appeared in a recent research paper:

不幸的是,许多注释并不是特别有用。最常见的原因是注释重复了代码:可以轻松地从注释旁边的代码中推断出注释中的所有信息。这是最近研究论文中出现的代码示例:

ptr_copy = get_copy(obj)            # Get pointer copy
 if is_unlocked(ptr_copy):           # Is obj free?
     return obj                      # return current obj
 if is_copy(ptr_copy):               # Already a copy?
@@ -248,4 +248,4 @@ import{_ as n,c as s,f as a,o as t}from"./app-CvqtBB8Z.js";const o={};function i
 > RAMCloud uses two techniques to neutralize zombies. First,
 ...
 

Then, in any piece of code that relates to one of these issues there is a short comment referring to the designNotes file:

然后,在与这些问题之一相关的任何代码段中,都有一条简短的注释引用了 designNotes 文件:

// See "Zombies" in designNotes.
-

With this approach, there is only a single copy of the documentation and it is relatively easy for developers to find it when they need it. However, this has the disadvantage that the documentation is not near any of the pieces of code that depend on it, so it may be difficult to keep up-to-date as the system evolves.

使用这种方法,文档只有一个副本,因此开发人员在需要时可以相对容易地找到它。但是,这样做的缺点是,文档离它依赖的任何代码段都不近,因此随着系统的发展,可能难以保持最新。

13.8 Conclusion 结论

The goal of comments is to ensure that the structure and behavior of the system is obvious to readers, so they can quickly find the information they need and make modifications to the system with confidence that they will work. Some of this information can be represented in the code in a way that will already be obvious to readers, but there is a significant amount of information that can’t easily be deduced from the code. Comments fill in this information.

注释的目的是确保系统的结构和行为对读者来说是显而易见的,因此他们可以快速找到所需的信息,并有信心对其进行修改,以对系统进行修改。这些信息中的某些信息可以以对读者来说显而易见的方式表示在代码中,但是有大量信息无法从代码中轻易推导出。注释将补充此信息。

When following the rule that comments should describe things that aren’t obvious from the code, “obvious” is from the perspective of someone reading your code for the first time (not you). When writing comments, try to put yourself in the mindset of the reader and ask yourself what are the key things he or she will need to know. If your code is undergoing review and a reviewer tells you that something is not obvious, don’t argue with them; if a reader thinks it’s not obvious, then it’s not obvious. Instead of arguing, try to understand what they found confusing and see if you can clarify that, either with better comments or better code.

当遵循注释应描述代码中不明显的内容的规则时,“明显”是从第一次读取您的代码的人(不是您)的角度出发。在撰写注释时,请尝试使自己进入读者的心态,并问自己他或她需要知道哪些关键事项。如果您的代码正在接受审核,并且审核者告诉您某些不明显的内容,请不要与他们争论。如果读者认为它不明显,那么它就不明显。不用争论,而是尝试了解他们发现的令人困惑的地方,并查看是否可以通过更好的注释或更好的代码来澄清它们。

13.9 Answers to questions from Section 13.5 回答第 13.5 节中的问题

Does a developer need to know each of the following pieces of information in order to use the IndexLookup class?

开发人员是否需要了解以下每条信息才能使用 IndexLookup 类?

  1. The format of messages that the IndexLookup class sends to the servers holding indexes and objects. No: this is an implementation detail that should be hidden within the class.
  2. The comparison function used to determine whether a particular object falls in the desired range (is comparison done using integers, floating-point numbers, or strings?). Yes: users of the class need to know this information.
  3. The data structure used to store indexes on servers. No: this information should be encapsulated on the servers; not even the implementation of IndexLookup should need to know this.
  4. Whether or not IndexLookup issues multiple requests to different servers concurrently. Possibly: if IndexLookup uses special techniques to improve performance, then the documentation should provide some high-level information about this, since users may care about performance.
  5. The mechanism for handling server crashes. No: RAMCloud recovers automatically from server crashes, so crashes are not visible to application-level software; thus, there is no need to mention crashes in the interface documentation for IndexLookup. If crashes were reflected up to applications, then the interface documentation would need to describe how they manifest themselves (but not the details of how crash recovery works).

  1. IndexLookup 类发送给包含索引和对象的服务器的消息格式。否:这是应隐藏在类中的实现细节。
  2. 用于确定特定对象是否在所需范围内的比较功能(使用整数,浮点数或字符串进行比较吗?)。是:该类的用户需要了解此信息。
  3. 用于在服务器上存储索引的数据结构。否:此信息应封装在服务器上;甚至 IndexLookup 的实现都不需要知道这一点。
  4. IndexLookup 是否同时向多个服务器发出多个请求。可能:如果 IndexLookup 使用特殊技术来提高性能,则文档应提供有关此问题的一些高级信息,因为用户可能会在意性能。
  5. 处理服务器崩溃的机制。否:RAMCloud 可从服务器崩溃中自动恢复,因此崩溃对于应用程序级软件不可见;因此,在 IndexLookup 的接口文档中无需提及崩溃。如果崩溃反映到应用程序中,则接口文档将需要描述它们如何表现出来(而不是崩溃恢复如何工作的详细信息)。
`,201)]))}const r=n(o,[["render",i],["__file","ch13.html.vue"]]),p=JSON.parse('{"path":"/ch13.html","title":"第 13 章 注释应该描述代码中不明显的内容","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"13.1 Pick conventions 选择约定","slug":"_13-1-pick-conventions-选择约定","link":"#_13-1-pick-conventions-选择约定","children":[]},{"level":2,"title":"13.2 Don’t repeat the code 不要重复代码","slug":"_13-2-don-t-repeat-the-code-不要重复代码","link":"#_13-2-don-t-repeat-the-code-不要重复代码","children":[]},{"level":2,"title":"13.3 Lower-level comments add precision 低级注释可提高精度","slug":"_13-3-lower-level-comments-add-precision-低级注释可提高精度","link":"#_13-3-lower-level-comments-add-precision-低级注释可提高精度","children":[]},{"level":2,"title":"13.4 Higher-level comments enhance intuition 高级注释可增强直觉","slug":"_13-4-higher-level-comments-enhance-intuition-高级注释可增强直觉","link":"#_13-4-higher-level-comments-enhance-intuition-高级注释可增强直觉","children":[]},{"level":2,"title":"13.5 Interface documentation 接口文档","slug":"_13-5-interface-documentation-接口文档","link":"#_13-5-interface-documentation-接口文档","children":[]},{"level":2,"title":"13.6 Implementation comments: what and why, not how 实现注释:什么以及为什么,而不是如何","slug":"_13-6-implementation-comments-what-and-why-not-how-实现注释-什么以及为什么-而不是如何","link":"#_13-6-implementation-comments-what-and-why-not-how-实现注释-什么以及为什么-而不是如何","children":[]},{"level":2,"title":"13.7 Cross-module design decisions 跨模块设计决策","slug":"_13-7-cross-module-design-decisions-跨模块设计决策","link":"#_13-7-cross-module-design-decisions-跨模块设计决策","children":[]},{"level":2,"title":"13.8 Conclusion 结论","slug":"_13-8-conclusion-结论","link":"#_13-8-conclusion-结论","children":[]},{"level":2,"title":"13.9 Answers to questions from Section 13.5 回答第 13.5 节中的问题","slug":"_13-9-answers-to-questions-from-section-13-5-回答第-13-5-节中的问题","link":"#_13-9-answers-to-questions-from-section-13-5-回答第-13-5-节中的问题","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch13.md"}');export{r as comp,p as data}; +

With this approach, there is only a single copy of the documentation and it is relatively easy for developers to find it when they need it. However, this has the disadvantage that the documentation is not near any of the pieces of code that depend on it, so it may be difficult to keep up-to-date as the system evolves.

使用这种方法,文档只有一个副本,因此开发人员在需要时可以相对容易地找到它。但是,这样做的缺点是,文档离它依赖的任何代码段都不近,因此随着系统的发展,可能难以保持最新。

13.8 Conclusion 结论

The goal of comments is to ensure that the structure and behavior of the system is obvious to readers, so they can quickly find the information they need and make modifications to the system with confidence that they will work. Some of this information can be represented in the code in a way that will already be obvious to readers, but there is a significant amount of information that can’t easily be deduced from the code. Comments fill in this information.

注释的目的是确保系统的结构和行为对读者来说是显而易见的,因此他们可以快速找到所需的信息,并有信心对其进行修改,以对系统进行修改。这些信息中的某些信息可以以对读者来说显而易见的方式表示在代码中,但是有大量信息无法从代码中轻易推导出。注释将补充此信息。

When following the rule that comments should describe things that aren’t obvious from the code, “obvious” is from the perspective of someone reading your code for the first time (not you). When writing comments, try to put yourself in the mindset of the reader and ask yourself what are the key things he or she will need to know. If your code is undergoing review and a reviewer tells you that something is not obvious, don’t argue with them; if a reader thinks it’s not obvious, then it’s not obvious. Instead of arguing, try to understand what they found confusing and see if you can clarify that, either with better comments or better code.

当遵循注释应描述代码中不明显的内容的规则时,“明显”是从第一次读取您的代码的人(不是您)的角度出发。在撰写注释时,请尝试使自己进入读者的心态,并问自己他或她需要知道哪些关键事项。如果您的代码正在接受审核,并且审核者告诉您某些不明显的内容,请不要与他们争论。如果读者认为它不明显,那么它就不明显。不用争论,而是尝试了解他们发现的令人困惑的地方,并查看是否可以通过更好的注释或更好的代码来澄清它们。

13.9 Answers to questions from Section 13.5 回答第 13.5 节中的问题

Does a developer need to know each of the following pieces of information in order to use the IndexLookup class?

开发人员是否需要了解以下每条信息才能使用 IndexLookup 类?

  1. The format of messages that the IndexLookup class sends to the servers holding indexes and objects. No: this is an implementation detail that should be hidden within the class.
  2. The comparison function used to determine whether a particular object falls in the desired range (is comparison done using integers, floating-point numbers, or strings?). Yes: users of the class need to know this information.
  3. The data structure used to store indexes on servers. No: this information should be encapsulated on the servers; not even the implementation of IndexLookup should need to know this.
  4. Whether or not IndexLookup issues multiple requests to different servers concurrently. Possibly: if IndexLookup uses special techniques to improve performance, then the documentation should provide some high-level information about this, since users may care about performance.
  5. The mechanism for handling server crashes. No: RAMCloud recovers automatically from server crashes, so crashes are not visible to application-level software; thus, there is no need to mention crashes in the interface documentation for IndexLookup. If crashes were reflected up to applications, then the interface documentation would need to describe how they manifest themselves (but not the details of how crash recovery works).

  1. IndexLookup 类发送给包含索引和对象的服务器的消息格式。否:这是应隐藏在类中的实现细节。
  2. 用于确定特定对象是否在所需范围内的比较功能(使用整数,浮点数或字符串进行比较吗?)。是:该类的用户需要了解此信息。
  3. 用于在服务器上存储索引的数据结构。否:此信息应封装在服务器上;甚至 IndexLookup 的实现都不需要知道这一点。
  4. IndexLookup 是否同时向多个服务器发出多个请求。可能:如果 IndexLookup 使用特殊技术来提高性能,则文档应提供有关此问题的一些高级信息,因为用户可能会在意性能。
  5. 处理服务器崩溃的机制。否:RAMCloud 可从服务器崩溃中自动恢复,因此崩溃对于应用程序级软件不可见;因此,在 IndexLookup 的接口文档中无需提及崩溃。如果崩溃反映到应用程序中,则接口文档将需要描述它们如何表现出来(而不是崩溃恢复如何工作的详细信息)。
`,201)]))}const r=n(o,[["render",i],["__file","ch13.html.vue"]]),p=JSON.parse('{"path":"/ch13.html","title":"第 13 章 注释应该描述代码中不明显的内容","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"13.1 Pick conventions 选择约定","slug":"_13-1-pick-conventions-选择约定","link":"#_13-1-pick-conventions-选择约定","children":[]},{"level":2,"title":"13.2 Don’t repeat the code 不要重复代码","slug":"_13-2-don-t-repeat-the-code-不要重复代码","link":"#_13-2-don-t-repeat-the-code-不要重复代码","children":[]},{"level":2,"title":"13.3 Lower-level comments add precision 低级注释可提高精度","slug":"_13-3-lower-level-comments-add-precision-低级注释可提高精度","link":"#_13-3-lower-level-comments-add-precision-低级注释可提高精度","children":[]},{"level":2,"title":"13.4 Higher-level comments enhance intuition 高级注释可增强直觉","slug":"_13-4-higher-level-comments-enhance-intuition-高级注释可增强直觉","link":"#_13-4-higher-level-comments-enhance-intuition-高级注释可增强直觉","children":[]},{"level":2,"title":"13.5 Interface documentation 接口文档","slug":"_13-5-interface-documentation-接口文档","link":"#_13-5-interface-documentation-接口文档","children":[]},{"level":2,"title":"13.6 Implementation comments: what and why, not how 实现注释:什么以及为什么,而不是如何","slug":"_13-6-implementation-comments-what-and-why-not-how-实现注释-什么以及为什么-而不是如何","link":"#_13-6-implementation-comments-what-and-why-not-how-实现注释-什么以及为什么-而不是如何","children":[]},{"level":2,"title":"13.7 Cross-module design decisions 跨模块设计决策","slug":"_13-7-cross-module-design-decisions-跨模块设计决策","link":"#_13-7-cross-module-design-decisions-跨模块设计决策","children":[]},{"level":2,"title":"13.8 Conclusion 结论","slug":"_13-8-conclusion-结论","link":"#_13-8-conclusion-结论","children":[]},{"level":2,"title":"13.9 Answers to questions from Section 13.5 回答第 13.5 节中的问题","slug":"_13-9-answers-to-questions-from-section-13-5-回答第-13-5-节中的问题","link":"#_13-9-answers-to-questions-from-section-13-5-回答第-13-5-节中的问题","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch13.md"}');export{r as comp,p as data}; diff --git a/assets/ch14.html-BOfqUa3t.js b/assets/ch14.html-D75FiwUn.js similarity index 99% rename from assets/ch14.html-BOfqUa3t.js rename to assets/ch14.html-D75FiwUn.js index f782636a..cc4bce2e 100644 --- a/assets/ch14.html-BOfqUa3t.js +++ b/assets/ch14.html-D75FiwUn.js @@ -1,4 +1,4 @@ -import{_ as n,c as a,f as s,o as t}from"./app-CvqtBB8Z.js";const o={};function i(l,e){return t(),a("div",null,e[0]||(e[0]=[s(`

第 14 章 选择的名字

Chapter 14 Choosing Names

Selecting names for variables, methods, and other entities is one of the most underrated aspects of software design. Good names are a form of documentation: they make code easier to understand. They reduce the need for other documentation and make it easier to detect errors. Conversely, poor name choices increase the complexity of code and create ambiguities and misunderstandings that can result in bugs. Name choice is an example of the principle that complexity is incremental. Choosing a mediocre name for a particular variable, as opposed to the best possible name, probably won’t have much impact on the overall complexity of a system. However, software systems have thousands of variables; choosing good names for all of these will have a significant impact on complexity and manageability.

为变量,方法和其他实体选择名称是软件设计中被低估的方面之一。良好的名字是一种文档形式:它们使代码更易于理解。它们减少了对其他文档的需求,并使检测错误更加容易。相反,名称选择不当会增加代码的复杂性,并造成可能导致错误的歧义和误解。名称选择是复杂度是递增的原理的一个示例。为特定变量选择一个平庸的名称,而不是最好的名称,这可能不会对系统的整体复杂性产生太大影响。但是,软件系统具有数千个变量。为所有这些选择好名字将对复杂性和可管理性产生重大影响。

14.1 Example: bad names cause bugs 示例:名称错误会导致错误

Sometimes even a single poorly named variable can have severe consequences. The most challenging bug I ever fixed came about because of a poor name choice. In the late 1980’s and early 1990’s my graduate students and I created a distributed operating system called Sprite. At some point we noticed that files would occasionally lose data: one of the data blocks suddenly became all zeroes, even though the file had not been modified by a user. The problem didn’t happen very often, so it was exceptionally difficult to track down. A few of the graduate students tried to find the bug, but they were unable to make progress and eventually gave up. However, I consider any unsolved bug to be an intolerable personal insult, so I decided to track it down.

有时,即使是一个名称不正确的变量也会产生严重的后果。我曾经修复过的最具挑战性的错误是由于名称选择不当造成的。在 1980 年代末和 1990 年代初,我的研究生和我创建了一个名为 Sprite 的分布式操作系统。在某个时候,我们注意到文件偶尔会丢失数据:即使用户未修改文件,数据块之一突然变为全零。该问题并不经常发生,因此很难追踪。一些研究生试图找到该错误,但他们未能取得进展,最终放弃了。但是,我认为任何未解决的错误都是无法忍受的个人侮辱,因此我决定对其进行跟踪。

It took six months, but I eventually found and fixed the bug. The problem was actually quite simple (as are most bugs, once you figure them out). The file system code used the variable name block for two different purposes. In some situations, block referred to a physical block number on disk; in other situations, block referred to a logical block number within a file. Unfortunately, at one point in the code there was a block variable containing a logical block number, but it was accidentally used in a context where a physical block number was needed; as a result, an unrelated block on disk got overwritten with zeroes.

花了六个月的时间,但我最终找到并修复了该错误。这个问题实际上很简单(就像大多数错误一样,一旦找出它们)。文件系统代码将变量名 “block” 用于两个不同的目的。在某些情况下,“block” 是指磁盘上的物理块号。在其他情况下,“block” 是指文件中的逻辑块号。不幸的是,在代码的某处有一个包含逻辑块号的块变量,但是在需要物理块号的情况下意外地使用了它。结果,磁盘上无关的块被重置为零了。

While tracking down the bug, several people, including myself, read over the faulty code, but we never noticed the problem. When we saw the variable block used as a physical block number, we reflexively assumed that it really held a physical block number. It took a long process of instrumentation, which eventually showed that the corruption must be happening in a particular statement, before I was able to get past the mental block created by the name and check to see exactly where its value came from. If different variable names had been used for the different kinds of blocks, such as fileBlock and diskBlock, it’s unlikely that the error would have happened; the programmer would have known that fileBlock couldn’t be used in that situation.

在跟踪该错误时,包括我自己在内的几个人阅读了错误的代码,但我们从未注意到问题所在。当我们看到变量 “block” 用作物理块号时,我们本能地假设它确实拥有物理块号。经过很长时间的检测,最终表明损坏必须发生在特定的语句中,然后我才能越过该名称所创建的思维障碍,并检查它的值究竟来自何处。如果对不同种类的块(例如 fileBlock 和 diskBlock)使用不同的变量名,则错误不太可能发生;程序员会知道在那种情况下不能使用 fileBlock。

Unfortunately, most developers don’t spend much time thinking about names. They tend to use the first name that comes to mind, as long as it’s reasonably close to matching the thing it names. For example, block is a pretty close match for both a physical block on disk and a logical block within a file; it’s certainly not a horrible name. Even so, it resulted in a huge expenditure of time to track down a subtle bug. Thus, you shouldn’t settle for names that are just “reasonably close”. Take a bit of extra time to choose great names, which are precise, unambiguous, and intuitive. The extra attention will pay for itself quickly, and over time you’ll learn to choose good names quickly.

不幸的是,大多数开发人员没有花太多时间在思考名字上面。他们倾向于使用想到的第一个名字,只要它与命名的事件合理接近即可。例如,块与磁盘上的物理块和文件内的逻辑块非常接近;这肯定不是一个可怕的名字。即使如此,它还是导致花费了大量时间来追踪一个细微的错误。因此,您不应该只选择“合理接近”的名称。花一些额外的时间来选择准确,明确且直观的好名字。额外的注意力将很快收回成本,随着时间的流逝,您将学会快速选择好名字。

14.2 Create an image 创建图像

When choosing a name, the goal is to create an image in the mind of the reader about the nature of the thing being named. A good name conveys a lot of information about what the underlying entity is, and, just as important, what it is not. When considering a particular name, ask yourself: “If someone sees this name in isolation, without seeing its declaration, its documentation, or any code that uses the name, how closely will they be able to guess what the name refers to? Is there some other name that will paint a clearer picture?” Of course, there is a limit to how much information you can put in a single name; names become unwieldy if they contain more than two or three words. Thus, the challenge is to find just a few words that capture the most important aspects of the entity.

选择名称时,目标是在读者的脑海中创建一幅关于被命名事物的性质的图像。一个好名字传达了很多有关底层实体是什么,以及同样重要的是,不是什么的信息。在考虑特定名称时,请问自己:“如果有人孤立地看到该名称,而没有看到其声明,文档或使用该名称的任何代码,他们将能够猜到该名称指的是什么?还有其他名称可以使画面更清晰吗?” 当然,一个名字可以输入多少信息是有限制的。如果名称包含两个或三个以上的单词,则会变得笨拙。因此,面临的挑战是仅找到捕获实体最重要方面的几个单词。

Names are a form of abstraction: they provide a simplified way of thinking about a more complex underlying entity. Like other forms of abstraction, the best names are those that focus attention on what is most important about the underlying entity while omitting details that are less important.

名称是一种抽象形式:名称提供了一种简化的方式来考虑更复杂的基础实体。像其他形式的抽象一样,最好的名字是那些将注意力集中在对底层实体最重要的东西上,而忽略那些次要的细节。

14.3 Names should be precise 名称应准确

Good names have two properties: precision and consistency. Let’s start with precision. The most common problem with names is that they are too generic or vague; as a result, it’s hard for readers to tell what the name refers to; the reader may assume that the name refers to something different from reality, as in the block bug above. Consider the following method declaration:

良好名称具有两个属性:精度和一致性。让我们从精度开始。名称最常见的问题是名称太笼统或含糊不清。结果,读者很难说出这个名字指的是什么。读者可能会认为该名称所指的是与现实不符的事物,如上面的 “block” bug 所示。考虑以下方法声明:

/**
+import{_ as n,c as a,f as s,o as t}from"./app-BQdhfat9.js";const o={};function i(l,e){return t(),a("div",null,e[0]||(e[0]=[s(`

第 14 章 选择的名字

Chapter 14 Choosing Names

Selecting names for variables, methods, and other entities is one of the most underrated aspects of software design. Good names are a form of documentation: they make code easier to understand. They reduce the need for other documentation and make it easier to detect errors. Conversely, poor name choices increase the complexity of code and create ambiguities and misunderstandings that can result in bugs. Name choice is an example of the principle that complexity is incremental. Choosing a mediocre name for a particular variable, as opposed to the best possible name, probably won’t have much impact on the overall complexity of a system. However, software systems have thousands of variables; choosing good names for all of these will have a significant impact on complexity and manageability.

为变量,方法和其他实体选择名称是软件设计中被低估的方面之一。良好的名字是一种文档形式:它们使代码更易于理解。它们减少了对其他文档的需求,并使检测错误更加容易。相反,名称选择不当会增加代码的复杂性,并造成可能导致错误的歧义和误解。名称选择是复杂度是递增的原理的一个示例。为特定变量选择一个平庸的名称,而不是最好的名称,这可能不会对系统的整体复杂性产生太大影响。但是,软件系统具有数千个变量。为所有这些选择好名字将对复杂性和可管理性产生重大影响。

14.1 Example: bad names cause bugs 示例:名称错误会导致错误

Sometimes even a single poorly named variable can have severe consequences. The most challenging bug I ever fixed came about because of a poor name choice. In the late 1980’s and early 1990’s my graduate students and I created a distributed operating system called Sprite. At some point we noticed that files would occasionally lose data: one of the data blocks suddenly became all zeroes, even though the file had not been modified by a user. The problem didn’t happen very often, so it was exceptionally difficult to track down. A few of the graduate students tried to find the bug, but they were unable to make progress and eventually gave up. However, I consider any unsolved bug to be an intolerable personal insult, so I decided to track it down.

有时,即使是一个名称不正确的变量也会产生严重的后果。我曾经修复过的最具挑战性的错误是由于名称选择不当造成的。在 1980 年代末和 1990 年代初,我的研究生和我创建了一个名为 Sprite 的分布式操作系统。在某个时候,我们注意到文件偶尔会丢失数据:即使用户未修改文件,数据块之一突然变为全零。该问题并不经常发生,因此很难追踪。一些研究生试图找到该错误,但他们未能取得进展,最终放弃了。但是,我认为任何未解决的错误都是无法忍受的个人侮辱,因此我决定对其进行跟踪。

It took six months, but I eventually found and fixed the bug. The problem was actually quite simple (as are most bugs, once you figure them out). The file system code used the variable name block for two different purposes. In some situations, block referred to a physical block number on disk; in other situations, block referred to a logical block number within a file. Unfortunately, at one point in the code there was a block variable containing a logical block number, but it was accidentally used in a context where a physical block number was needed; as a result, an unrelated block on disk got overwritten with zeroes.

花了六个月的时间,但我最终找到并修复了该错误。这个问题实际上很简单(就像大多数错误一样,一旦找出它们)。文件系统代码将变量名 “block” 用于两个不同的目的。在某些情况下,“block” 是指磁盘上的物理块号。在其他情况下,“block” 是指文件中的逻辑块号。不幸的是,在代码的某处有一个包含逻辑块号的块变量,但是在需要物理块号的情况下意外地使用了它。结果,磁盘上无关的块被重置为零了。

While tracking down the bug, several people, including myself, read over the faulty code, but we never noticed the problem. When we saw the variable block used as a physical block number, we reflexively assumed that it really held a physical block number. It took a long process of instrumentation, which eventually showed that the corruption must be happening in a particular statement, before I was able to get past the mental block created by the name and check to see exactly where its value came from. If different variable names had been used for the different kinds of blocks, such as fileBlock and diskBlock, it’s unlikely that the error would have happened; the programmer would have known that fileBlock couldn’t be used in that situation.

在跟踪该错误时,包括我自己在内的几个人阅读了错误的代码,但我们从未注意到问题所在。当我们看到变量 “block” 用作物理块号时,我们本能地假设它确实拥有物理块号。经过很长时间的检测,最终表明损坏必须发生在特定的语句中,然后我才能越过该名称所创建的思维障碍,并检查它的值究竟来自何处。如果对不同种类的块(例如 fileBlock 和 diskBlock)使用不同的变量名,则错误不太可能发生;程序员会知道在那种情况下不能使用 fileBlock。

Unfortunately, most developers don’t spend much time thinking about names. They tend to use the first name that comes to mind, as long as it’s reasonably close to matching the thing it names. For example, block is a pretty close match for both a physical block on disk and a logical block within a file; it’s certainly not a horrible name. Even so, it resulted in a huge expenditure of time to track down a subtle bug. Thus, you shouldn’t settle for names that are just “reasonably close”. Take a bit of extra time to choose great names, which are precise, unambiguous, and intuitive. The extra attention will pay for itself quickly, and over time you’ll learn to choose good names quickly.

不幸的是,大多数开发人员没有花太多时间在思考名字上面。他们倾向于使用想到的第一个名字,只要它与命名的事件合理接近即可。例如,块与磁盘上的物理块和文件内的逻辑块非常接近;这肯定不是一个可怕的名字。即使如此,它还是导致花费了大量时间来追踪一个细微的错误。因此,您不应该只选择“合理接近”的名称。花一些额外的时间来选择准确,明确且直观的好名字。额外的注意力将很快收回成本,随着时间的流逝,您将学会快速选择好名字。

14.2 Create an image 创建图像

When choosing a name, the goal is to create an image in the mind of the reader about the nature of the thing being named. A good name conveys a lot of information about what the underlying entity is, and, just as important, what it is not. When considering a particular name, ask yourself: “If someone sees this name in isolation, without seeing its declaration, its documentation, or any code that uses the name, how closely will they be able to guess what the name refers to? Is there some other name that will paint a clearer picture?” Of course, there is a limit to how much information you can put in a single name; names become unwieldy if they contain more than two or three words. Thus, the challenge is to find just a few words that capture the most important aspects of the entity.

选择名称时,目标是在读者的脑海中创建一幅关于被命名事物的性质的图像。一个好名字传达了很多有关底层实体是什么,以及同样重要的是,不是什么的信息。在考虑特定名称时,请问自己:“如果有人孤立地看到该名称,而没有看到其声明,文档或使用该名称的任何代码,他们将能够猜到该名称指的是什么?还有其他名称可以使画面更清晰吗?” 当然,一个名字可以输入多少信息是有限制的。如果名称包含两个或三个以上的单词,则会变得笨拙。因此,面临的挑战是仅找到捕获实体最重要方面的几个单词。

Names are a form of abstraction: they provide a simplified way of thinking about a more complex underlying entity. Like other forms of abstraction, the best names are those that focus attention on what is most important about the underlying entity while omitting details that are less important.

名称是一种抽象形式:名称提供了一种简化的方式来考虑更复杂的基础实体。像其他形式的抽象一样,最好的名字是那些将注意力集中在对底层实体最重要的东西上,而忽略那些次要的细节。

14.3 Names should be precise 名称应准确

Good names have two properties: precision and consistency. Let’s start with precision. The most common problem with names is that they are too generic or vague; as a result, it’s hard for readers to tell what the name refers to; the reader may assume that the name refers to something different from reality, as in the block bug above. Consider the following method declaration:

良好名称具有两个属性:精度和一致性。让我们从精度开始。名称最常见的问题是名称太笼统或含糊不清。结果,读者很难说出这个名字指的是什么。读者可能会认为该名称所指的是与现实不符的事物,如上面的 “block” bug 所示。考虑以下方法声明:

/**
  * Returns the total number of indexlets this object is managing.
  */
 int IndexletManager::getCount() {...}
@@ -40,4 +40,4 @@ import{_ as n,c as a,f as s,o as t}from"./app-CvqtBB8Z.js";const o={};function i
     }
     return count
 }
-

Personally, I don’t find the second version any more difficult to read than the first. If anything, the name count gives a slightly better clue to the behavior of the variable than n. With the first version I ended up reading through the code trying to figure out what n means, whereas I didn’t feel that need with the second version. But, if n is used consistently throughout the system to refer to counts (and nothing else), then the short name will probably be clear to other developers.

就个人而言,我不觉得第二版比第一版更难读。如果有的话,与 n 相比,名称计数为变量的行为提供了更好的线索。在第一个版本中,我最终通读了代码,试图弄清楚 n 的含义,而第二个版本中我并没有这种需要。但是,如果在整个系统中一致地使用 n 来引用计数(而没有其他内容),那么其他开发人员可能会清楚知道该短名称。

The Go culture encourages the use of the same short name for multiple different things: ch for character or channel, d for data, difference, or distance, and so on. To me, ambiguous names like these are likely to result in confusion and error, just as in the block example.

Go 文化鼓励在多个不同的事物上使用相同的短名称:ch 用于字符或通道,d 用于数据,差异或距离,等等。对我来说,像这样的模棱两可的名称很可能导致混乱和错误,就像在示例中一样。

Overall, I would argue that readability must be determined by readers, not writers. If you write code with short variable names and the people who read it find it easy to understand, then that’s fine. If you start getting complaints that your code is cryptic, then you should consider using longer names (a Web search for “go language short names” will identify several such complaints). Similarly, if I start getting complaints that long variable names make my code harder to read, then I’ll consider using shorter ones.

总的来说,我认为可读性必须由读者而不是作家来决定。如果您使用简短的变量名编写代码,并且阅读该代码的人很容易理解,那么很好。如果您开始抱怨代码很含糊,那么您应该考虑使用更长的名称(在网络上搜索“ go language short name”(使用语言简称)会发现一些这样的抱怨)。同样,如果我开始抱怨长变量名使我的代码难以阅读,那么我会考虑使用较短的变量名。

Gerrand makes one comment that I agree with: “The greater the distance between a name’s declaration and its uses, the longer the name should be.” The earlier discussion about using loop variables named i and j is an example of this rule.

Gerrand 发表一个我同意的评论:“名称声明与使用之间的距离越大,名称就应该越长。” 前面有关使用名为 i 和 j 的循环变量的讨论是此规则的示例。

14.6 Conclusion 结论

Well chosen names help to make code more obvious; when someone encounters the variable for the first time, their first guess about its behavior, made without much thought, will be correct. Choosing good names is an example of the investment mindset discussed in Chapter 3: if you take a little extra time up front to select good names, it will be easier for you to work on the code in the future. In addition, you will be less likely to introduce bugs. Developing a skill for naming is also an investment. When you first decide to stop settling for mediocre names, you may find it frustrating and time-consuming to come up with good names. However, as you get more experience you’ll find that it becomes easier; eventually, you’ll get to the point where it takes almost no extra time to choose good names, so you will get the benefits almost for free.

精心选择的名称有助于使代码更明显。当某人第一次遇到该变量时,他们对行为的第一次猜测是正确的。选择好名字是第 3 章讨论的投资思维方式的一个示例:如果您花一些额外的时间来选择好名字,那么将来您将更容易处理代码。此外,您不太可能引入错误。培养命名技巧也是一项投资。当您第一次决定不再满足于平庸的名字时,您会发现想出好名字的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现它变得更加容易。最终,您将几乎不需要花费额外的时间来选择好名字,因此您几乎可以免费获得好处。

1 https://talks.golang.org/2014/names.slide#1

`,80)]))}const c=n(o,[["render",i],["__file","ch14.html.vue"]]),p=JSON.parse('{"path":"/ch14.html","title":"第 14 章 选择的名字","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"14.1 Example: bad names cause bugs 示例:名称错误会导致错误","slug":"_14-1-example-bad-names-cause-bugs-示例-名称错误会导致错误","link":"#_14-1-example-bad-names-cause-bugs-示例-名称错误会导致错误","children":[]},{"level":2,"title":"14.2 Create an image 创建图像","slug":"_14-2-create-an-image-创建图像","link":"#_14-2-create-an-image-创建图像","children":[]},{"level":2,"title":"14.3 Names should be precise 名称应准确","slug":"_14-3-names-should-be-precise-名称应准确","link":"#_14-3-names-should-be-precise-名称应准确","children":[]},{"level":2,"title":"14.4 Use names consistently 一致使用名称(命名要确保一致性)","slug":"_14-4-use-names-consistently-一致使用名称-命名要确保一致性","link":"#_14-4-use-names-consistently-一致使用名称-命名要确保一致性","children":[]},{"level":2,"title":"14.5 A different opinion: Go style guide 不同的意见:Go 样式指南","slug":"_14-5-a-different-opinion-go-style-guide-不同的意见-go-样式指南","link":"#_14-5-a-different-opinion-go-style-guide-不同的意见-go-样式指南","children":[]},{"level":2,"title":"14.6 Conclusion 结论","slug":"_14-6-conclusion-结论","link":"#_14-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch14.md"}');export{c as comp,p as data}; +

Personally, I don’t find the second version any more difficult to read than the first. If anything, the name count gives a slightly better clue to the behavior of the variable than n. With the first version I ended up reading through the code trying to figure out what n means, whereas I didn’t feel that need with the second version. But, if n is used consistently throughout the system to refer to counts (and nothing else), then the short name will probably be clear to other developers.

就个人而言,我不觉得第二版比第一版更难读。如果有的话,与 n 相比,名称计数为变量的行为提供了更好的线索。在第一个版本中,我最终通读了代码,试图弄清楚 n 的含义,而第二个版本中我并没有这种需要。但是,如果在整个系统中一致地使用 n 来引用计数(而没有其他内容),那么其他开发人员可能会清楚知道该短名称。

The Go culture encourages the use of the same short name for multiple different things: ch for character or channel, d for data, difference, or distance, and so on. To me, ambiguous names like these are likely to result in confusion and error, just as in the block example.

Go 文化鼓励在多个不同的事物上使用相同的短名称:ch 用于字符或通道,d 用于数据,差异或距离,等等。对我来说,像这样的模棱两可的名称很可能导致混乱和错误,就像在示例中一样。

Overall, I would argue that readability must be determined by readers, not writers. If you write code with short variable names and the people who read it find it easy to understand, then that’s fine. If you start getting complaints that your code is cryptic, then you should consider using longer names (a Web search for “go language short names” will identify several such complaints). Similarly, if I start getting complaints that long variable names make my code harder to read, then I’ll consider using shorter ones.

总的来说,我认为可读性必须由读者而不是作家来决定。如果您使用简短的变量名编写代码,并且阅读该代码的人很容易理解,那么很好。如果您开始抱怨代码很含糊,那么您应该考虑使用更长的名称(在网络上搜索“ go language short name”(使用语言简称)会发现一些这样的抱怨)。同样,如果我开始抱怨长变量名使我的代码难以阅读,那么我会考虑使用较短的变量名。

Gerrand makes one comment that I agree with: “The greater the distance between a name’s declaration and its uses, the longer the name should be.” The earlier discussion about using loop variables named i and j is an example of this rule.

Gerrand 发表一个我同意的评论:“名称声明与使用之间的距离越大,名称就应该越长。” 前面有关使用名为 i 和 j 的循环变量的讨论是此规则的示例。

14.6 Conclusion 结论

Well chosen names help to make code more obvious; when someone encounters the variable for the first time, their first guess about its behavior, made without much thought, will be correct. Choosing good names is an example of the investment mindset discussed in Chapter 3: if you take a little extra time up front to select good names, it will be easier for you to work on the code in the future. In addition, you will be less likely to introduce bugs. Developing a skill for naming is also an investment. When you first decide to stop settling for mediocre names, you may find it frustrating and time-consuming to come up with good names. However, as you get more experience you’ll find that it becomes easier; eventually, you’ll get to the point where it takes almost no extra time to choose good names, so you will get the benefits almost for free.

精心选择的名称有助于使代码更明显。当某人第一次遇到该变量时,他们对行为的第一次猜测是正确的。选择好名字是第 3 章讨论的投资思维方式的一个示例:如果您花一些额外的时间来选择好名字,那么将来您将更容易处理代码。此外,您不太可能引入错误。培养命名技巧也是一项投资。当您第一次决定不再满足于平庸的名字时,您会发现想出好名字的过程既令人沮丧又耗时。但是,随着您获得更多的经验,您会发现它变得更加容易。最终,您将几乎不需要花费额外的时间来选择好名字,因此您几乎可以免费获得好处。

1 https://talks.golang.org/2014/names.slide#1

`,80)]))}const c=n(o,[["render",i],["__file","ch14.html.vue"]]),p=JSON.parse('{"path":"/ch14.html","title":"第 14 章 选择的名字","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"14.1 Example: bad names cause bugs 示例:名称错误会导致错误","slug":"_14-1-example-bad-names-cause-bugs-示例-名称错误会导致错误","link":"#_14-1-example-bad-names-cause-bugs-示例-名称错误会导致错误","children":[]},{"level":2,"title":"14.2 Create an image 创建图像","slug":"_14-2-create-an-image-创建图像","link":"#_14-2-create-an-image-创建图像","children":[]},{"level":2,"title":"14.3 Names should be precise 名称应准确","slug":"_14-3-names-should-be-precise-名称应准确","link":"#_14-3-names-should-be-precise-名称应准确","children":[]},{"level":2,"title":"14.4 Use names consistently 一致使用名称(命名要确保一致性)","slug":"_14-4-use-names-consistently-一致使用名称-命名要确保一致性","link":"#_14-4-use-names-consistently-一致使用名称-命名要确保一致性","children":[]},{"level":2,"title":"14.5 A different opinion: Go style guide 不同的意见:Go 样式指南","slug":"_14-5-a-different-opinion-go-style-guide-不同的意见-go-样式指南","link":"#_14-5-a-different-opinion-go-style-guide-不同的意见-go-样式指南","children":[]},{"level":2,"title":"14.6 Conclusion 结论","slug":"_14-6-conclusion-结论","link":"#_14-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch14.md"}');export{c as comp,p as data}; diff --git a/assets/ch15.html-4sanLber.js b/assets/ch15.html-BK0-CmCt.js similarity index 99% rename from assets/ch15.html-4sanLber.js rename to assets/ch15.html-BK0-CmCt.js index 859b61f0..6164daec 100644 --- a/assets/ch15.html-4sanLber.js +++ b/assets/ch15.html-BK0-CmCt.js @@ -1 +1 @@ -import{_ as t,c as o,f as i,o as n}from"./app-CvqtBB8Z.js";const a={};function s(r,e){return n(),o("div",null,e[0]||(e[0]=[i('

第 15 章 先写注释

Chapter 15 Write The Comments First(Use Comments As Part Of The Design Process)

Many developers put off writing documentation until the end of the development process, after coding and unit testing are complete. This is one of the surest ways to produce poor quality documentation. The best time to write comments is at the beginning of the process, as you write the code. Writing the comments first makes documentation part of the design process. Not only does this produce better documentation, but it also produces better designs and it makes the process of writing documentation more enjoyable.

许多开发人员推迟编写文档,直到开发过程结束,编码和单元测试完成之后。这是产生质量差的文档的最可靠方法之一。编写注释的最佳时间是在过程开始时。首先编写注释使文档成为设计过程的一部分。这不仅可以产生更好的文档,还可以产生更好的设计,并使编写文档的过程更加愉快。

15.1 Delayed comments are bad comments 迟到的注释不是好注释

Almost every developer I have ever met puts off writing comments. When asked why they don’t write documentation earlier, they say that the code is still changing. If they write documentation early, they say, they’ll have to rewrite it when the code changes; better to wait until the code stabilizes. However, I suspect that there is also another reason, which is that they view documentation as drudge work; thus, they put it off as long as possible.

我见过的几乎每个开发人员都会推迟编写注释。当被问及为什么不更早编写文档时,他们说代码仍在更改。他们说,如果他们尽早编写文档,则必须在代码更改时重新编写文档。最好等到代码稳定下来。但是,我怀疑还有另一个原因,那就是他们将文档视为苦差事。因此,他们尽可能地推迟了。

Unfortunately, this approach has several negative consequences. First, delaying documentation often means that it never gets written at all. Once you start delaying, it’s easy to delay a bit more; after all, the code will be even more stable in a few more weeks. By the time the code has inarguably stabilized, there is a lot of it, which means the task of writing documentation has become huge and even less attractive. There’s never a convenient time to stop for a few days and fill in all of the missing comments, and it’s easy to rationalize that the best thing for the project is to move on and fix bugs or write the next new feature. This will create even more undocumented code.

不幸的是,这种方法有几个负面影响。首先,延迟文档通常意味着根本无法编写文档。一旦开始延迟,就容易再延迟一些。毕竟,代码将在几周后变得更加稳定。到了代码毫无疑问地稳定下来的时候,代码已经很多了,这意味着编写文档的任务变得越来越庞大,甚至没有了吸引力。从来没有一个合适的时间可以停下来几天并填写所有缺失的注释,并且很容易合理化项目最好的事情是继续前进并修复错误或编写下一个新功能。这将导致更多没有注释的代码。

Even if you do have the self-discipline to go back and write the comments (and don’t fool yourself: you probably don’t), the comments won’t be very good. By this time in the process, you have checked out mentally. In your mind, this piece of code is done; you are eager to move on to your next project. You know that writing comments is the right thing to do, but it’s no fun. You just want to get through it as quickly as possible. Thus, you make a quick pass over the code, adding just enough comments to look respectable. By now, it’s been a while since you designed the code, so your memories of the design process are becoming fuzzy. You look at the code as you are writing the comments, so the comments repeat the code. Even if you try to reconstruct the design ideas that aren’t obvious from the code, there will be things you don’t remember. Thus, the comments are missing some of the most important things they should describe.

即使您有自律性回去写注释(不要欺骗您自己:您可能没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您设计代码已经有一段时间了,所以您对设计过程的记忆变得模糊了。您查看代码完成注释,因此注释重复了代码(comments repeat the code)。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释遗漏了一些他们应该描述的最重要的事情。

15.2 Write the comments first 首先写注释

I use a different approach to writing comments, where I write the comments at the very beginning:

我使用一种不同的方法来编写注释,在开始时就写注释:

  • For a new class, I start by writing the class interface comment.
  • Next, I write interface comments and signatures for the most important public methods, but I leave the method bodies empty.
  • I iterate a bit over these comments until the basic structure feels about right.
  • At this point I write declarations and comments for the most important class instance variables in the class.
  • Finally, I fill in the bodies of the methods, adding implementation comments as needed.
  • While writing method bodies, I usually discover the need for additional methods and instance variables. For each new method I write the interface comment before the body of the method; for instance variables I fill in the comment at the same time that I write the variable declaration.

  • 对于新类,我首先编写类接口注释。
  • 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。
  • 我对这些注释进行了迭代,直到基本结构感觉正确为止。
  • 此时我为类中最重要的类实例变量编写了声明和注释。
  • 最后,我填写方法的主体,并根据需要添加实现注释。
  • 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。对于每个变量,我在编写其声明的同时填写了注释。

When the code is done, the comments are also done. There is never a backlog of unwritten comments.

代码完成后,注释也将完成。从来没有积压的书面注释。

The comments-first approach has three benefits. First, it produces better comments. If you write the comments as you are designing the class, the key design issues will be fresh in your mind, so it’s easy to record them. It’s better to write the interface comment for each method before its body, so you can focus on the method’s abstraction and interface without being distracted by its implementation. During the coding and testing process you will notice and fix problems with the comments. As a result, the comments improve over the course of development.

注释优先的方法具有三个好处。首先,它会产生更好的注释。如果您在设计类时写注释,那么关键的设计问题将在您的脑海中浮现,因此很容易记录下来。最好在每个方法的主体之前编写接口注释,这样您就可以专注于方法的抽象和接口,而不会因其实现而分心。在编码和测试过程中,您会注意到并修复注释中的问题。结果,注释在开发过程中得到了改善。

15.3 Comments are a design tool 注释是一种设计工具

The second, and most important, benefit of writing the comments at the beginning is that it improves the system design. Comments provide the only way to fully capture abstractions, and good abstractions are fundamental to good system design. If you write comments describing the abstractions at the beginning, you can review and tune them before writing implementation code. To write a good comment, you must identify the essence of a variable or piece of code: what are the most important aspects of this thing? It’s important to do this early in the design process; otherwise you are just hacking code.

在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在破解代码(hacking code 未找到合适的翻译)。

Comments serve as a canary in the coal mine of complexity. If a method or variable requires a long comment, it is a red flag that you don’t have a good abstraction. Remember from Chapter 4 that classes should be deep: the best classes have very simple interfaces yet implement powerful functions. The best way to judge the complexity of an interface is from the comments that describe it. If the interface comment for a method provides all the information needed to use the method and is also short and simple, that indicates that the method has a simple interface. Conversely, if there’s no way to describe a method completely without a long and complicated comment, then the method has a complex interface. You can compare a method’s interface comment with the implementation to get a sense of how deep the method is: if the interface comment must describe all the major features of the implementation, then the method is shallow. The same idea applies to variables: if it takes a long comment to fully describe a variable, it’s a red flag that suggests you may not have chosen the right variable decomposition. Overall, the act of writing comments allows you to evaluate your design decisions early, so you can discover and fix problems.

注释是复杂煤矿中的金丝雀。如果方法或变量需要较长的注释,则它是一个危险信号,表明您没有很好的抽象。请记住,在第 4 章中,类应该很深:最好的类具有非常简单的接口,但可以实现强大的功能。判断接口复杂性的最佳方法是从描述接口的注释中进行。如果某个方法的接口注释提供了使用该方法所需的所有信息,并且又简短又简单,则表明该方法具有简单的接口。相反,如果没有冗长而复杂的注释无法完全描述一个方法,则该方法具有复杂的接口。您可以将方法的接口注释与实现进行比较,以了解该方法的深度:如果接口注释必须描述实现的所有主要功能,则该方法很浅。同样的想法也适用于变量:如果要花很长的时间来完整描述一个变量,那是一个危险信号,表明您可能没有选择正确的变量分解。总体而言,编写注释的行为使您可以及早评估设计决策,以便发现并解决问题。

img Red Flag: Hard to Describe img

The comment that describes a method or variable should be simple and yet complete. If you find it difficult to write such a comment, that’s an indicator that there may be a problem with the design of the thing you are describing.

描述方法或变量的注释应该简单而完整。如果您发现很难写这样的注释,则表明您所描述的内容的设计可能存在问题。

Of course, comments are only a good indicator of complexity if they are complete and clear. If you write a method interface comment that doesn’t provide all the information needed to invoke the method, or one that is so cryptic that it’s hard to understand, then that comment doesn’t provide a good measure of the method’s depth.

当然,如果注释完整而清晰,那么它们仅是复杂性的良好指标。如果编写的方法接口注释未提供调用该方法所需的全部信息,或者编写的注释太过晦涩难懂,那么则该注释不能很好地衡量该方法的深度。

15.4 Early comments are fun comments 早期注释很有趣

The third and final benefit of writing comments early is that it makes comment-writing more fun. For me, one of the most enjoyable parts of programming is the early design phase for a new class, where I’m fleshing out the abstractions and structure for the class. Most of my comments are written during this phase, and the comments are how I record and test the quality of my design decisions. I’m looking for the design that can be expressed completely and clearly in the fewest words. The simpler the comments, the better I feel about my design, so finding simple comments is a source of pride. If you are programming strategically, where your main goal is a great design rather than just writing code that works, then writing comments should be fun, since that’s how you identify the best designs.

尽早编写注释的第三个也是最后一个好处是,它使编写注释更加有趣。对我来说,编程中最有趣的部分之一是新类的早期设计阶段,我在这个阶段充实类的抽象和结构。我的大部分注释都是在此阶段编写的,这些注释是我记录和测试设计决策质量的方式。我正在寻找可以用最少的词来完整而清晰地表达的设计。注释越简单,我对设计的感觉就越好,因此找到简单的注释是一种自豪感。如果您是策略性编程,而您的主要目标是一个出色的设计,而不仅仅是编写有效的代码,那么编写注释应该很有趣,因为这是您确定最佳设计的方式。

15.5 Are early comments expensive? 早期注释是否昂贵?

Now let’s revisit the argument for delaying comments, which is that it avoids the cost of reworking the comments as the code evolves. A simple back-of-the-envelope calculation will show that this doesn’t save much. First, estimate the total fraction of development time that you spend typing in code and comments together, including time to revise code and comments; it’s unlikely that this will be more than about 10% of all development time. Even if half of your total code lines are comments, writing comments probably doesn’t account for more than about 5% of your total development time. Delaying the comments until the end will save only a fraction of this, which isn’t very much.

现在,让我们重新审视延迟注释的论点,它避免了在代码演变时重新处理注释的成本。一个简单的粗略计算会表明这并没有节省多少。首先,估算您一起键入代码和注释所花费的开发时间的总和,包括修改代码和注释的时间;这不太可能超过所有开发时间的 10%。即使您的全部代码行中有一半是注释,编写注释也可能不会占开发总时间的 5%以上。将注释延迟到最后只会节省其中的一小部分,这不是很多。

Writing the comments first will mean that the abstractions will be more stable before you start writing code. This will probably save time during coding. In contrast, if you write the code first, the abstractions will probably evolve as you code, which will require more code revisions than the comments-first approach. When you consider all of these factors, it’s possible that it might be faster overall to write the comments first.

首先编写注释将意味着在开始编写代码之前,抽象将更加稳定。这可能会节省编码时间。相反,如果您首先编写代码,则抽象可能会随代码的发展而变化,与注释优先方法相比,将需要更多的代码修订。当您考虑所有这些因素时,首先编写注释可能总体上更快。

15.6 Conclusion 结论

If you haven’t ever tried writing the comments first, give it a try. Stick with it long enough to get used to it. Then think about how it affects the quality of your comments, the quality of your design, and your overall enjoyment of software development. After you have tried this for a while, let me know whether your experience matches mine, and why or why not.

如果您从未尝试过先编写注释,请尝试一下。坚持足够长的时间来习惯它。然后考虑它如何影响您的注释质量,设计质量以及软件开发的整体乐趣。在尝试了一段时间之后,让我知道您的经历是否与我的相符,以及为什么或为什么不这样。

',42)]))}const h=t(a,[["render",s],["__file","ch15.html.vue"]]),c=JSON.parse('{"path":"/ch15.html","title":"第 15 章 先写注释","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"15.1 Delayed comments are bad comments 迟到的注释不是好注释","slug":"_15-1-delayed-comments-are-bad-comments-迟到的注释不是好注释","link":"#_15-1-delayed-comments-are-bad-comments-迟到的注释不是好注释","children":[]},{"level":2,"title":"15.2 Write the comments first 首先写注释","slug":"_15-2-write-the-comments-first-首先写注释","link":"#_15-2-write-the-comments-first-首先写注释","children":[]},{"level":2,"title":"15.3 Comments are a design tool 注释是一种设计工具","slug":"_15-3-comments-are-a-design-tool-注释是一种设计工具","link":"#_15-3-comments-are-a-design-tool-注释是一种设计工具","children":[]},{"level":2,"title":"15.4 Early comments are fun comments 早期注释很有趣","slug":"_15-4-early-comments-are-fun-comments-早期注释很有趣","link":"#_15-4-early-comments-are-fun-comments-早期注释很有趣","children":[]},{"level":2,"title":"15.5 Are early comments expensive? 早期注释是否昂贵?","slug":"_15-5-are-early-comments-expensive-早期注释是否昂贵","link":"#_15-5-are-early-comments-expensive-早期注释是否昂贵","children":[]},{"level":2,"title":"15.6 Conclusion 结论","slug":"_15-6-conclusion-结论","link":"#_15-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch15.md"}');export{h as comp,c as data}; +import{_ as t,c as o,f as i,o as n}from"./app-BQdhfat9.js";const a={};function s(r,e){return n(),o("div",null,e[0]||(e[0]=[i('

第 15 章 先写注释

Chapter 15 Write The Comments First(Use Comments As Part Of The Design Process)

Many developers put off writing documentation until the end of the development process, after coding and unit testing are complete. This is one of the surest ways to produce poor quality documentation. The best time to write comments is at the beginning of the process, as you write the code. Writing the comments first makes documentation part of the design process. Not only does this produce better documentation, but it also produces better designs and it makes the process of writing documentation more enjoyable.

许多开发人员推迟编写文档,直到开发过程结束,编码和单元测试完成之后。这是产生质量差的文档的最可靠方法之一。编写注释的最佳时间是在过程开始时。首先编写注释使文档成为设计过程的一部分。这不仅可以产生更好的文档,还可以产生更好的设计,并使编写文档的过程更加愉快。

15.1 Delayed comments are bad comments 迟到的注释不是好注释

Almost every developer I have ever met puts off writing comments. When asked why they don’t write documentation earlier, they say that the code is still changing. If they write documentation early, they say, they’ll have to rewrite it when the code changes; better to wait until the code stabilizes. However, I suspect that there is also another reason, which is that they view documentation as drudge work; thus, they put it off as long as possible.

我见过的几乎每个开发人员都会推迟编写注释。当被问及为什么不更早编写文档时,他们说代码仍在更改。他们说,如果他们尽早编写文档,则必须在代码更改时重新编写文档。最好等到代码稳定下来。但是,我怀疑还有另一个原因,那就是他们将文档视为苦差事。因此,他们尽可能地推迟了。

Unfortunately, this approach has several negative consequences. First, delaying documentation often means that it never gets written at all. Once you start delaying, it’s easy to delay a bit more; after all, the code will be even more stable in a few more weeks. By the time the code has inarguably stabilized, there is a lot of it, which means the task of writing documentation has become huge and even less attractive. There’s never a convenient time to stop for a few days and fill in all of the missing comments, and it’s easy to rationalize that the best thing for the project is to move on and fix bugs or write the next new feature. This will create even more undocumented code.

不幸的是,这种方法有几个负面影响。首先,延迟文档通常意味着根本无法编写文档。一旦开始延迟,就容易再延迟一些。毕竟,代码将在几周后变得更加稳定。到了代码毫无疑问地稳定下来的时候,代码已经很多了,这意味着编写文档的任务变得越来越庞大,甚至没有了吸引力。从来没有一个合适的时间可以停下来几天并填写所有缺失的注释,并且很容易合理化项目最好的事情是继续前进并修复错误或编写下一个新功能。这将导致更多没有注释的代码。

Even if you do have the self-discipline to go back and write the comments (and don’t fool yourself: you probably don’t), the comments won’t be very good. By this time in the process, you have checked out mentally. In your mind, this piece of code is done; you are eager to move on to your next project. You know that writing comments is the right thing to do, but it’s no fun. You just want to get through it as quickly as possible. Thus, you make a quick pass over the code, adding just enough comments to look respectable. By now, it’s been a while since you designed the code, so your memories of the design process are becoming fuzzy. You look at the code as you are writing the comments, so the comments repeat the code. Even if you try to reconstruct the design ideas that aren’t obvious from the code, there will be things you don’t remember. Thus, the comments are missing some of the most important things they should describe.

即使您有自律性回去写注释(不要欺骗您自己:您可能没有),注释也不会很好。在这个过程的这个时候,你已经在精神上离开了。在你的脑海中,这段代码已经完成了;你急于开始下一个项目。你知道写注释是正确的事情,但它没有乐趣。你只想尽快度过难关。因此,您快速地浏览代码,添加足够的注释以使其看起来令人满意。到目前为止,您设计代码已经有一段时间了,所以您对设计过程的记忆变得模糊了。您查看代码完成注释,因此注释重复了代码(comments repeat the code)。即使您试图重构代码中不明显的设计思想,也会有您不记得的事情。因此,这些注释遗漏了一些他们应该描述的最重要的事情。

15.2 Write the comments first 首先写注释

I use a different approach to writing comments, where I write the comments at the very beginning:

我使用一种不同的方法来编写注释,在开始时就写注释:

  • For a new class, I start by writing the class interface comment.
  • Next, I write interface comments and signatures for the most important public methods, but I leave the method bodies empty.
  • I iterate a bit over these comments until the basic structure feels about right.
  • At this point I write declarations and comments for the most important class instance variables in the class.
  • Finally, I fill in the bodies of the methods, adding implementation comments as needed.
  • While writing method bodies, I usually discover the need for additional methods and instance variables. For each new method I write the interface comment before the body of the method; for instance variables I fill in the comment at the same time that I write the variable declaration.

  • 对于新类,我首先编写类接口注释。
  • 接下来,我为最重要的公共方法编写接口注释和签名,但将方法主体保留为空。
  • 我对这些注释进行了迭代,直到基本结构感觉正确为止。
  • 此时我为类中最重要的类实例变量编写了声明和注释。
  • 最后,我填写方法的主体,并根据需要添加实现注释。
  • 在编写方法主体时,我通常会发现需要其他方法和实例变量。对于每个新方法,我在方法主体之前编写接口注释。对于每个变量,我在编写其声明的同时填写了注释。

When the code is done, the comments are also done. There is never a backlog of unwritten comments.

代码完成后,注释也将完成。从来没有积压的书面注释。

The comments-first approach has three benefits. First, it produces better comments. If you write the comments as you are designing the class, the key design issues will be fresh in your mind, so it’s easy to record them. It’s better to write the interface comment for each method before its body, so you can focus on the method’s abstraction and interface without being distracted by its implementation. During the coding and testing process you will notice and fix problems with the comments. As a result, the comments improve over the course of development.

注释优先的方法具有三个好处。首先,它会产生更好的注释。如果您在设计类时写注释,那么关键的设计问题将在您的脑海中浮现,因此很容易记录下来。最好在每个方法的主体之前编写接口注释,这样您就可以专注于方法的抽象和接口,而不会因其实现而分心。在编码和测试过程中,您会注意到并修复注释中的问题。结果,注释在开发过程中得到了改善。

15.3 Comments are a design tool 注释是一种设计工具

The second, and most important, benefit of writing the comments at the beginning is that it improves the system design. Comments provide the only way to fully capture abstractions, and good abstractions are fundamental to good system design. If you write comments describing the abstractions at the beginning, you can review and tune them before writing implementation code. To write a good comment, you must identify the essence of a variable or piece of code: what are the most important aspects of this thing? It’s important to do this early in the design process; otherwise you are just hacking code.

在开始时编写注释的第二个也是最重要的好处是可以改善系统设计。注释提供了完全捕获抽象的唯一方法,好的抽象是好的系统设计的基础。如果您在一开始就写了描述抽象的注释,则可以在编写实现代码之前对其进行检查和调整。要写一个好的注释,您必须确定一个变量或一段代码的本质:这件事最重要的方面是什么?在设计过程的早期进行此操作很重要;否则,您只是在破解代码(hacking code 未找到合适的翻译)。

Comments serve as a canary in the coal mine of complexity. If a method or variable requires a long comment, it is a red flag that you don’t have a good abstraction. Remember from Chapter 4 that classes should be deep: the best classes have very simple interfaces yet implement powerful functions. The best way to judge the complexity of an interface is from the comments that describe it. If the interface comment for a method provides all the information needed to use the method and is also short and simple, that indicates that the method has a simple interface. Conversely, if there’s no way to describe a method completely without a long and complicated comment, then the method has a complex interface. You can compare a method’s interface comment with the implementation to get a sense of how deep the method is: if the interface comment must describe all the major features of the implementation, then the method is shallow. The same idea applies to variables: if it takes a long comment to fully describe a variable, it’s a red flag that suggests you may not have chosen the right variable decomposition. Overall, the act of writing comments allows you to evaluate your design decisions early, so you can discover and fix problems.

注释是复杂煤矿中的金丝雀。如果方法或变量需要较长的注释,则它是一个危险信号,表明您没有很好的抽象。请记住,在第 4 章中,类应该很深:最好的类具有非常简单的接口,但可以实现强大的功能。判断接口复杂性的最佳方法是从描述接口的注释中进行。如果某个方法的接口注释提供了使用该方法所需的所有信息,并且又简短又简单,则表明该方法具有简单的接口。相反,如果没有冗长而复杂的注释无法完全描述一个方法,则该方法具有复杂的接口。您可以将方法的接口注释与实现进行比较,以了解该方法的深度:如果接口注释必须描述实现的所有主要功能,则该方法很浅。同样的想法也适用于变量:如果要花很长的时间来完整描述一个变量,那是一个危险信号,表明您可能没有选择正确的变量分解。总体而言,编写注释的行为使您可以及早评估设计决策,以便发现并解决问题。

img Red Flag: Hard to Describe img

The comment that describes a method or variable should be simple and yet complete. If you find it difficult to write such a comment, that’s an indicator that there may be a problem with the design of the thing you are describing.

描述方法或变量的注释应该简单而完整。如果您发现很难写这样的注释,则表明您所描述的内容的设计可能存在问题。

Of course, comments are only a good indicator of complexity if they are complete and clear. If you write a method interface comment that doesn’t provide all the information needed to invoke the method, or one that is so cryptic that it’s hard to understand, then that comment doesn’t provide a good measure of the method’s depth.

当然,如果注释完整而清晰,那么它们仅是复杂性的良好指标。如果编写的方法接口注释未提供调用该方法所需的全部信息,或者编写的注释太过晦涩难懂,那么则该注释不能很好地衡量该方法的深度。

15.4 Early comments are fun comments 早期注释很有趣

The third and final benefit of writing comments early is that it makes comment-writing more fun. For me, one of the most enjoyable parts of programming is the early design phase for a new class, where I’m fleshing out the abstractions and structure for the class. Most of my comments are written during this phase, and the comments are how I record and test the quality of my design decisions. I’m looking for the design that can be expressed completely and clearly in the fewest words. The simpler the comments, the better I feel about my design, so finding simple comments is a source of pride. If you are programming strategically, where your main goal is a great design rather than just writing code that works, then writing comments should be fun, since that’s how you identify the best designs.

尽早编写注释的第三个也是最后一个好处是,它使编写注释更加有趣。对我来说,编程中最有趣的部分之一是新类的早期设计阶段,我在这个阶段充实类的抽象和结构。我的大部分注释都是在此阶段编写的,这些注释是我记录和测试设计决策质量的方式。我正在寻找可以用最少的词来完整而清晰地表达的设计。注释越简单,我对设计的感觉就越好,因此找到简单的注释是一种自豪感。如果您是策略性编程,而您的主要目标是一个出色的设计,而不仅仅是编写有效的代码,那么编写注释应该很有趣,因为这是您确定最佳设计的方式。

15.5 Are early comments expensive? 早期注释是否昂贵?

Now let’s revisit the argument for delaying comments, which is that it avoids the cost of reworking the comments as the code evolves. A simple back-of-the-envelope calculation will show that this doesn’t save much. First, estimate the total fraction of development time that you spend typing in code and comments together, including time to revise code and comments; it’s unlikely that this will be more than about 10% of all development time. Even if half of your total code lines are comments, writing comments probably doesn’t account for more than about 5% of your total development time. Delaying the comments until the end will save only a fraction of this, which isn’t very much.

现在,让我们重新审视延迟注释的论点,它避免了在代码演变时重新处理注释的成本。一个简单的粗略计算会表明这并没有节省多少。首先,估算您一起键入代码和注释所花费的开发时间的总和,包括修改代码和注释的时间;这不太可能超过所有开发时间的 10%。即使您的全部代码行中有一半是注释,编写注释也可能不会占开发总时间的 5%以上。将注释延迟到最后只会节省其中的一小部分,这不是很多。

Writing the comments first will mean that the abstractions will be more stable before you start writing code. This will probably save time during coding. In contrast, if you write the code first, the abstractions will probably evolve as you code, which will require more code revisions than the comments-first approach. When you consider all of these factors, it’s possible that it might be faster overall to write the comments first.

首先编写注释将意味着在开始编写代码之前,抽象将更加稳定。这可能会节省编码时间。相反,如果您首先编写代码,则抽象可能会随代码的发展而变化,与注释优先方法相比,将需要更多的代码修订。当您考虑所有这些因素时,首先编写注释可能总体上更快。

15.6 Conclusion 结论

If you haven’t ever tried writing the comments first, give it a try. Stick with it long enough to get used to it. Then think about how it affects the quality of your comments, the quality of your design, and your overall enjoyment of software development. After you have tried this for a while, let me know whether your experience matches mine, and why or why not.

如果您从未尝试过先编写注释,请尝试一下。坚持足够长的时间来习惯它。然后考虑它如何影响您的注释质量,设计质量以及软件开发的整体乐趣。在尝试了一段时间之后,让我知道您的经历是否与我的相符,以及为什么或为什么不这样。

',42)]))}const h=t(a,[["render",s],["__file","ch15.html.vue"]]),c=JSON.parse('{"path":"/ch15.html","title":"第 15 章 先写注释","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"15.1 Delayed comments are bad comments 迟到的注释不是好注释","slug":"_15-1-delayed-comments-are-bad-comments-迟到的注释不是好注释","link":"#_15-1-delayed-comments-are-bad-comments-迟到的注释不是好注释","children":[]},{"level":2,"title":"15.2 Write the comments first 首先写注释","slug":"_15-2-write-the-comments-first-首先写注释","link":"#_15-2-write-the-comments-first-首先写注释","children":[]},{"level":2,"title":"15.3 Comments are a design tool 注释是一种设计工具","slug":"_15-3-comments-are-a-design-tool-注释是一种设计工具","link":"#_15-3-comments-are-a-design-tool-注释是一种设计工具","children":[]},{"level":2,"title":"15.4 Early comments are fun comments 早期注释很有趣","slug":"_15-4-early-comments-are-fun-comments-早期注释很有趣","link":"#_15-4-early-comments-are-fun-comments-早期注释很有趣","children":[]},{"level":2,"title":"15.5 Are early comments expensive? 早期注释是否昂贵?","slug":"_15-5-are-early-comments-expensive-早期注释是否昂贵","link":"#_15-5-are-early-comments-expensive-早期注释是否昂贵","children":[]},{"level":2,"title":"15.6 Conclusion 结论","slug":"_15-6-conclusion-结论","link":"#_15-6-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch15.md"}');export{h as comp,c as data}; diff --git a/assets/ch16.html-CYy_oqbT.js b/assets/ch16.html-DwaSHeqm.js similarity index 99% rename from assets/ch16.html-CYy_oqbT.js rename to assets/ch16.html-DwaSHeqm.js index ac7093b4..8e5dcb18 100644 --- a/assets/ch16.html-CYy_oqbT.js +++ b/assets/ch16.html-DwaSHeqm.js @@ -1,6 +1,6 @@ -import{_ as t,c as o,f as a,o as n}from"./app-CvqtBB8Z.js";const i={};function s(h,e){return n(),o("div",null,e[0]||(e[0]=[a(`

第 16 章 修改现有的代码

Chapter 16 Modifying Existing Code

Chapter 1 described how software development is iterative and incremental. A large software system develops through a series of evolutionary stages, where each stage adds new capabilities and modifies existing modules. This means that a system’s design is constantly evolving. It isn’t possible to conceive the right design for a system at the outset; the design of a mature system is determined more by changes made during the system’s evolution than by any initial conception. Previous chapters described how to squeeze out complexity during the initial design and implementation; this chapter discusses how to keep complexity from creeping in as the system evolves.

第 1 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。不可能从一开始就为系统构思出正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而蔓延。

16.1 Stay strategic 保持战略性

Chapter 3 introduced the distinction between tactical programming and strategic programming: in tactical programming, the primary goal is to get something working quickly, even if that results in additional complexity; in strategic programming, the most important goal is to produce a great system design. The tactical approach very quickly leads to a messy system design. If you want to have a system that is easy to maintain and enhance, then “working” isn’t a high enough standard; you have to prioritize design and think strategically. This idea also applies when you are modifying existing code.

第 3 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么能“工作”并不是一个足够高的标准。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。

Unfortunately, when developers go into existing code to make changes such as bug fixes or new features, they don’t usually think strategically. A typical mindset is “what is the smallest possible change I can make that does what I need?” Sometimes developers justify this because they are not comfortable with the code being modified; they worry that larger changes carry a greater risk of introducing new bugs. However, this results in tactical programming. Each one of these minimal changes introduces a few special cases, dependencies, or other forms of complexity. As a result, the system design gets just a bit worse, and the problems accumulate with each step in the system’s evolution.

不幸的是,当开发人员进入现有代码以进行更改(例如错误修复或新功能)时,他们通常不会从战略角度进行思考。一个典型的心态是“我能做出我需要做的最小的改变是什么?” 有时开发人员认为这是合理的,因为他们对修改的代码不放心。他们担心较大的更改会带来更大的风险,会引入新的错误。然而,这导致了战术编程。每一个最小的变化都会引入一些特殊情况,依赖性或其他形式的复杂性。结果,系统设计变得更糟,并且问题随着系统演进的每一步骤而累积。

If you want to maintain a clean design for a system, you must take a strategic approach when modifying existing code. Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind. To achieve this goal, you must resist the temptation to make a quick fix. Instead, think about whether the current system design is still the best one, in light of the desired change. If not, refactor the system so that you end up with the best possible design. With this approach, the system design improves with every modification.

如果要保持系统的简洁设计,则在修改现有代码时必须采取战略性方法。理想情况下,当您完成每次更改时,系统将具有如果你在一开始设计时就考虑这一变化而具有的结构。为了实现此目标,您必须抵制诱惑以快速解决问题。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会改善系统设计。

This is also an example of the investment mindset introduced on page 15: if you invest a little extra time to refactor and improve the system design, you’ll end up with a cleaner system. This will speed up development, and you will recoup the effort that you invested in the refactoring. Even if your particular change doesn’t require refactoring, you should still be on the lookout for design imperfections that you can fix while you’re in the code. Whenever you modify any code, try to find a way to improve the system design at least a little bit in the process. If you’re not making the design better, you are probably making it worse.

这也是第 15 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计变得更好,则可能会使它变得更糟。

As discussed in Chapter 3, an investment mindset sometimes conflicts with the realities of commercial software development. If refactoring the system “the right way” would take three months but a quick and dirty fix would take only two hours, you may have to take the quick and dirty approach, particularly if you are working against a tight deadline. Or, if refactoring the system would create incompatibilities that affect many other people and teams, then the refactoring may not be practical.

如第 3 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成不兼容,从而影响许多其他人员和团队,则重构可能不切实际。

Nonetheless, you should resist these compromises as much as possible. Ask yourself “Is this the best I can possibly do to create a clean system design, given my current constraints?” Perhaps there’s an alternative approach that would be almost as clean as the 3-month refactoring but could be done in a couple of days? Or, if you can’t afford to do a large refactoring now, get your boss to allocate time for you to come back to it after the current deadline. Every development organization should plan to spend a small fraction of its total effort on cleanup and refactoring; this work will pay for itself over the long run.

尽管如此,您应尽可能抵制这些妥协。问问自己:“考虑到我目前的限制,这是否是我能做的最好的工作来创建一个干净的系统设计?” 也许有一种替代方法几乎可以像 3 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在没有能力做大规模的重构,请让您的老板为您分配时间,让您在当前截止日期之后再来做。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将为自己带来回报。

16.2 Maintaining comments: keep the comments near the code 维护注释:将注释保留在代码附近

When you change existing code, there’s a good chance that the changes will invalidate some of the existing comments. It’s easy to forget to update comments when you modify code, which results in comments that are no longer accurate. Inaccurate comments are frustrating to readers, and if there are very many of them, readers begin to distrust all of the comments. Fortunately, with a little discipline and a couple of guiding rules, it’s possible to keep comments up-to-date without a huge effort. This section and the following ones put forth some specific techniques.

当您更改现有代码时,更改很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果有很多这样的注释,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不费吹灰之力使注释保持更新。本节及随后的部分提出了一些具体的技巧。

The best way to ensure that comments get updated is to position them close to the code they describe, so developers will see them when they change the code. The farther a comment is from its associated code, the less likely it is that it will be updated properly. For example, the best place for a method’s interface comment is in the code file, right next to the body of the method. Any changes to the method will involve this code, so the developer is likely to see the interface comments and update them if needed.

确保注释更新的最佳方法是将注释放置在它们描述的代码附近,以便开发人员在更改代码时可以看到它们。注释离其关联的代码越远,正确更新的可能性就越小。例如,方法接口注释的最佳位置是在代码文件中,紧靠该方法主体的位置。对方法的任何更改都将涉及此代码,因此开发人员很可能会看到接口注释,并在需要时进行更新。

An alternative for languages like C and C++ that have separate code and header files, is to place the interface comments next to the method’s declaration in the .h file. However, this is a long way from the code; developers won’t see those comments when modifying the method’s body, and it takes additional work to open a different file and find the interface comments to update them. Some might argue that interface comments should go in header files so that users can learn how to use an abstraction without having to look at the code file. However, users should not need to read either code or header files; they should get their information from documentation compiled by tools such as Doxygen or Javadoc. In addition, many IDEs will extract and present documentation to users, such as by displaying a method’s documentation when the method’s name is typed. Given tools such as these, the documentation should be located in the place that is most convenient for developers working on the code.

对于 C 和 C++ 等具有单独的代码和头文件的语言,一种替代方法是将接口注释放在.h 文件中方法声明的旁边。但是,这距离代码还有很长的路要走。开发人员在修改方法的主体时将看不到这些注释,因此需要打开其他文件并查找接口注释来更新它们,这需要额外的工作。有人可能会争辩说接口注释应该放在头文件中,以便用户可以学习如何使用一个抽象概念,而不需要查看代码。然而,用户不应该阅读代码或头文件;他们应该从由 Doxygen 或 Javadoc 等工具编译的文档中获取信息。此外,许多 IDE 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。鉴于这样的工具,文档应位于对开发人员进行代码开发最方便的位置。

When writing implementation comments, don’t put all the comments for an entire method at the top of the method. Spread them out, pushing each comment down to the narrowest scope that includes all of the code referred to by the comment. For example, if a method has three major phases, don’t write one comment at the top of the method that describes all of the phases in detail. Instead, write a separate comment for each phase and position that comment just above the first line of code in that phase. On the other hand, it can also be helpful to have a comment at the top of a method’s implementation that describes the overall strategy, like this:

在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。把他们分解开来,将每个注释向下写到最合适范围,即包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助,例如:

//  We proceed in three phases:
+import{_ as t,c as o,f as a,o as n}from"./app-BQdhfat9.js";const i={};function s(h,e){return n(),o("div",null,e[0]||(e[0]=[a(`

第 16 章 修改现有的代码

Chapter 16 Modifying Existing Code

Chapter 1 described how software development is iterative and incremental. A large software system develops through a series of evolutionary stages, where each stage adds new capabilities and modifies existing modules. This means that a system’s design is constantly evolving. It isn’t possible to conceive the right design for a system at the outset; the design of a mature system is determined more by changes made during the system’s evolution than by any initial conception. Previous chapters described how to squeeze out complexity during the initial design and implementation; this chapter discusses how to keep complexity from creeping in as the system evolves.

第 1 章介绍了软件开发是如何迭代和增量的。大型软件系统是通过一系列演化阶段开发的,其中每个阶段都添加了新功能并修改了现有模块。这意味着系统的设计在不断发展。不可能从一开始就为系统构思出正确的设计。一个成熟的系统的设计更多地取决于系统演化过程中所做的更改,而不是任何初始概念。前面的章节描述了如何在初始设计和实现过程中降低复杂性。本章讨论如何防止随着系统的发展而蔓延。

16.1 Stay strategic 保持战略性

Chapter 3 introduced the distinction between tactical programming and strategic programming: in tactical programming, the primary goal is to get something working quickly, even if that results in additional complexity; in strategic programming, the most important goal is to produce a great system design. The tactical approach very quickly leads to a messy system design. If you want to have a system that is easy to maintain and enhance, then “working” isn’t a high enough standard; you have to prioritize design and think strategically. This idea also applies when you are modifying existing code.

第 3 章介绍了战术编程和战略编程之间的区别:在战术编程中,主要目标是使某些事物快速工作,即使这会导致额外的复杂性;在战略编程中,最重要的目标是进行出色的系统设计。战术方法很快导致系统设计混乱。如果您想要一个易于维护和增强的系统,那么能“工作”并不是一个足够高的标准。您必须优先考虑设计并进行战略思考。当您修改现有代码时,此想法也适用。

Unfortunately, when developers go into existing code to make changes such as bug fixes or new features, they don’t usually think strategically. A typical mindset is “what is the smallest possible change I can make that does what I need?” Sometimes developers justify this because they are not comfortable with the code being modified; they worry that larger changes carry a greater risk of introducing new bugs. However, this results in tactical programming. Each one of these minimal changes introduces a few special cases, dependencies, or other forms of complexity. As a result, the system design gets just a bit worse, and the problems accumulate with each step in the system’s evolution.

不幸的是,当开发人员进入现有代码以进行更改(例如错误修复或新功能)时,他们通常不会从战略角度进行思考。一个典型的心态是“我能做出我需要做的最小的改变是什么?” 有时开发人员认为这是合理的,因为他们对修改的代码不放心。他们担心较大的更改会带来更大的风险,会引入新的错误。然而,这导致了战术编程。每一个最小的变化都会引入一些特殊情况,依赖性或其他形式的复杂性。结果,系统设计变得更糟,并且问题随着系统演进的每一步骤而累积。

If you want to maintain a clean design for a system, you must take a strategic approach when modifying existing code. Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind. To achieve this goal, you must resist the temptation to make a quick fix. Instead, think about whether the current system design is still the best one, in light of the desired change. If not, refactor the system so that you end up with the best possible design. With this approach, the system design improves with every modification.

如果要保持系统的简洁设计,则在修改现有代码时必须采取战略性方法。理想情况下,当您完成每次更改时,系统将具有如果你在一开始设计时就考虑这一变化而具有的结构。为了实现此目标,您必须抵制诱惑以快速解决问题。相反,请根据所需的更改来考虑当前的系统设计是否仍然是最佳的。如果不是,请重构系统,以便最终获得最佳设计。通过这种方法,每次修改都会改善系统设计。

This is also an example of the investment mindset introduced on page 15: if you invest a little extra time to refactor and improve the system design, you’ll end up with a cleaner system. This will speed up development, and you will recoup the effort that you invested in the refactoring. Even if your particular change doesn’t require refactoring, you should still be on the lookout for design imperfections that you can fix while you’re in the code. Whenever you modify any code, try to find a way to improve the system design at least a little bit in the process. If you’re not making the design better, you are probably making it worse.

这也是第 15 页介绍的投资心态的一个示例:如果您花费一些额外的时间来重构和改善系统设计,您将得到一个更干净的系统。这将加快开发速度,您将收回在重构方面投入的精力。即使您的特定更改不需要重构,您仍然应该注意在代码中可以修复的设计缺陷。每当您修改任何代码时,都尝试在该过程中至少找到一点方法来改进系统设计。如果您没有使设计变得更好,则可能会使它变得更糟。

As discussed in Chapter 3, an investment mindset sometimes conflicts with the realities of commercial software development. If refactoring the system “the right way” would take three months but a quick and dirty fix would take only two hours, you may have to take the quick and dirty approach, particularly if you are working against a tight deadline. Or, if refactoring the system would create incompatibilities that affect many other people and teams, then the refactoring may not be practical.

如第 3 章所述,投资心态有时与商业软件开发的现实相冲突。如果“正确的方式”重构系统需要三个月,而快速且肮脏的修复仅需两个小时,则您可能必须采取快速而肮脏的方法,尤其是在紧迫的期限内工作时。或者,如果重构系统会造成不兼容,从而影响许多其他人员和团队,则重构可能不切实际。

Nonetheless, you should resist these compromises as much as possible. Ask yourself “Is this the best I can possibly do to create a clean system design, given my current constraints?” Perhaps there’s an alternative approach that would be almost as clean as the 3-month refactoring but could be done in a couple of days? Or, if you can’t afford to do a large refactoring now, get your boss to allocate time for you to come back to it after the current deadline. Every development organization should plan to spend a small fraction of its total effort on cleanup and refactoring; this work will pay for itself over the long run.

尽管如此,您应尽可能抵制这些妥协。问问自己:“考虑到我目前的限制,这是否是我能做的最好的工作来创建一个干净的系统设计?” 也许有一种替代方法几乎可以像 3 个月的重构一样干净,但是可以在几天内完成?或者,如果您现在没有能力做大规模的重构,请让您的老板为您分配时间,让您在当前截止日期之后再来做。每个开发组织都应计划将其全部工作的一小部分用于清理和重构;从长远来看,这项工作将为自己带来回报。

16.2 Maintaining comments: keep the comments near the code 维护注释:将注释保留在代码附近

When you change existing code, there’s a good chance that the changes will invalidate some of the existing comments. It’s easy to forget to update comments when you modify code, which results in comments that are no longer accurate. Inaccurate comments are frustrating to readers, and if there are very many of them, readers begin to distrust all of the comments. Fortunately, with a little discipline and a couple of guiding rules, it’s possible to keep comments up-to-date without a huge effort. This section and the following ones put forth some specific techniques.

当您更改现有代码时,更改很有可能会使某些现有注释无效。修改代码时,很容易忘记更新注释,从而导致注释不再准确。不准确的注释使读者感到沮丧,如果有很多这样的注释,读者就会开始不信任所有注释。幸运的是,只要有一点纪律和一些指导规则,就可以在不费吹灰之力使注释保持更新。本节及随后的部分提出了一些具体的技巧。

The best way to ensure that comments get updated is to position them close to the code they describe, so developers will see them when they change the code. The farther a comment is from its associated code, the less likely it is that it will be updated properly. For example, the best place for a method’s interface comment is in the code file, right next to the body of the method. Any changes to the method will involve this code, so the developer is likely to see the interface comments and update them if needed.

确保注释更新的最佳方法是将注释放置在它们描述的代码附近,以便开发人员在更改代码时可以看到它们。注释离其关联的代码越远,正确更新的可能性就越小。例如,方法接口注释的最佳位置是在代码文件中,紧靠该方法主体的位置。对方法的任何更改都将涉及此代码,因此开发人员很可能会看到接口注释,并在需要时进行更新。

An alternative for languages like C and C++ that have separate code and header files, is to place the interface comments next to the method’s declaration in the .h file. However, this is a long way from the code; developers won’t see those comments when modifying the method’s body, and it takes additional work to open a different file and find the interface comments to update them. Some might argue that interface comments should go in header files so that users can learn how to use an abstraction without having to look at the code file. However, users should not need to read either code or header files; they should get their information from documentation compiled by tools such as Doxygen or Javadoc. In addition, many IDEs will extract and present documentation to users, such as by displaying a method’s documentation when the method’s name is typed. Given tools such as these, the documentation should be located in the place that is most convenient for developers working on the code.

对于 C 和 C++ 等具有单独的代码和头文件的语言,一种替代方法是将接口注释放在.h 文件中方法声明的旁边。但是,这距离代码还有很长的路要走。开发人员在修改方法的主体时将看不到这些注释,因此需要打开其他文件并查找接口注释来更新它们,这需要额外的工作。有人可能会争辩说接口注释应该放在头文件中,以便用户可以学习如何使用一个抽象概念,而不需要查看代码。然而,用户不应该阅读代码或头文件;他们应该从由 Doxygen 或 Javadoc 等工具编译的文档中获取信息。此外,许多 IDE 都会提取文档并将其呈现给用户,例如在键入方法名称时显示方法的文档。鉴于这样的工具,文档应位于对开发人员进行代码开发最方便的位置。

When writing implementation comments, don’t put all the comments for an entire method at the top of the method. Spread them out, pushing each comment down to the narrowest scope that includes all of the code referred to by the comment. For example, if a method has three major phases, don’t write one comment at the top of the method that describes all of the phases in detail. Instead, write a separate comment for each phase and position that comment just above the first line of code in that phase. On the other hand, it can also be helpful to have a comment at the top of a method’s implementation that describes the overall strategy, like this:

在编写实现注释时,不要将整个方法的所有注释放在方法的顶部。把他们分解开来,将每个注释向下写到最合适范围,即包括该注释所引用的所有代码。例如,如果一种方法具有三个主要阶段,则不要在方法的顶部写一个详细描述所有阶段的注释。而是为每个阶段编写一个单独的注释,并将该注释放置在该阶段的第一行代码的正上方。另一方面,在描述总体策略的方法实现的顶部添加注释也可能会有所帮助,例如:

//  We proceed in three phases:
 //  Phase 1: Find feasible candidates
 //  Phase 2: Assign each candidate a score
 //  Phase 3: Choose the best, and remove it
 

Additional details can be documented just above the code for each phase.

每个阶段的代码上方都可以记录其他详细信息。

In general, the farther a comment is from the code it describes, the more abstract it should be (this reduces the likelihood that the comment will be invalidated by code changes).

通常,注释离描述的代码越远,注释应该越抽象(这减少了注释因代码更改而无效的可能性)。

16.3 Comments belong in the code, not the commit log 注释属于代码,而不是提交日志

A common mistake when modifying code is to put detailed information about the change in the commit message for the source code repository, but then not to document it in the code. Although commit messages can be browsed in the future by scanning the repository’s log, a developer who needs the information is unlikely to think of scanning the repository log. Even if they do scan the log, it will be tedious to find the right log message.

修改代码时,常见的错误是将有关更改的详细信息放入源代码存储库的提交消息中,而不是将其记录在代码中。尽管将来可以通过扫描存储库的日志来浏览提交消息,但是需要该信息的开发人员不太可能考虑扫描存储库的日志。即使他们确实扫描了日志,也很难找到正确的日志也会很乏味。

When writing a commit message, ask yourself whether developers will need to use that information in the future. If so, then document this information in the code. An example is a commit message describing a subtle problem that motivated a code change. If this isn’t documented in the code, then a developer might come along later and undo the change without realizing that they have re-created a bug. If you want to include a copy of this information in the commit message as well, that’s fine, but the most important thing is to get it in the code. This illustrates the principle of placing documentation in the place where developers are most likely to see it; the commit log is rarely that place.

在编写提交消息时,请问自己将来开发人员是否需要使用该信息。如果是这样,则在代码中记录此信息。一个示例是提交消息,描述了导致代码更改的细微问题。如果代码中未对此进行记录,那么一个开发人员可能会稍后撤消这个更改,而没有意识到他们已经重新创建了错误。如果您也想在提交消息中包含此信息的副本,那也可以,但是最重要的是把他放在代码中。这说明了将文档放置在开发人员最有可能看到它的地方的原则;尽量少放在提交日志中。

16.4 Maintaining comments: avoid duplication 维护注释:避免重复

The second technique for keeping comments up to date is to avoid duplication. If documentation is duplicated, it is more difficult for developers to find and update all of the relevant copies. Instead, try to document each design decision exactly once. If there are multiple places in the code that are affected by a particular decision, don’t repeat the documentation at each of these points. Instead, find the most obvious single place to put the documentation. For example, suppose there is tricky behavior related to a variable, which affects several different places where the variable is used. You can document that behavior in the comment next to the variable’s declaration. This is a natural place that developers are likely to check if they’re having trouble understanding code that uses the variable.

保持注释最新的第二种技术是避免重复。如果文档重复,那么开发人员将很难找到并更新所有相关副本。因此尽量将每个设计决策精确的记录一次。如果代码中有多个地方受某个特定决定的影响,请不要在所有这些地方重复注释。所以找到放置注释最明显的位置。例如,假设存在与变量相关的棘手行为,这会影响使用变量的几个不同地方。您可以在变量声明旁边的注释中记录该行为。如果开发人员在理解使用该变量的代码时遇到麻烦,他们自然会在这里进行检查。

If there is no “obvious” single place to put a particular piece of documentation where developers will find it, create a designNotes file as described in Section 13.7. Or, pick the best of the available places and put the documentation there. In addition, add short comments in the other places that refer to the central location: “See the comment in xyz for an explanation of the code below.” If the reference becomes obsolete because the master comment was moved or deleted, this inconsistency will be self-evident because developers won’t find the comment at the indicated place; they can use revision control history to find out what happened to the comment and then update the reference. In contrast, if the documentation is duplicated and some of the copies don’t get updated, there will be no indication to developers that they are using stale information.

如果没有一个“明显的”地方来将特定的文档放在开发人员可以找到的地方,那么创建一个 designNotes 文件,如第 13.7 节所述。或者,在现有的地方中选择一个最好的地方,把文档放在那里。此外,在引用中心位置的其他地方添加简短的注释:“查看 xyz 中的注释以了解下面代码的解释。“如果引用因为主注释被移动或删除而使引用变得过时,这种不一致性将是不言而喻的,因为开发人员将无法在指定的位置找到注释;他们可以使用修订控制历史记录来查找注释发生了什么,然后更新引用。相反,如果文档是重复的,而一些副本没有得到更新,那么开发人员就不会知道他们使用的是陈旧的信息。

Don’t redocument one module’s design decisions in another module. For example, don’t put comments before a method call that explain what happens in the called method. If readers want to know, they should look at the interface comments for the method. Good development tools will usually provide this information automatically, for example, by displaying the interface comments for a method if you select the method’s name or hover the mouse over it. Try to make it easy for developers to find appropriate documentation, but don’t do it by repeating the documentation.

不要在另一个模块中记录一个模块的设计决策。例如,不要在方法调用前添加注释,以解释被调用方法中发生的情况。如果读者想知道,他们应该查看该方法的接口注释。好的开发工具通常会自动提供此信息,例如,如果您选择了方法的名称或将鼠标悬停在该方法的名称上,则将显示该方法的接口注释。尽量让开发人员容易找到合适的文档,但是不要通过重复文档来做到这一点。

If information is already documented someplace outside your program, don’t repeat the documentation inside the program; just reference the external documentation. For example, if you write a class that implements the HTTP protocol, there’s no need for you to describe the HTTP protocol inside your code. There are already numerous sources for this documentation on the Web; just add a short comment to your code with a URL for one of these sources. Another example is features that are already documented in a user manual. Suppose you are writing a program that implements a collection of commands, with one method responsible for implementing each command. If there is a user manual that describes those commands, there’s no need to duplicate this information in the code. Instead, include a short note like the following in the interface comment for each command method:

如果信息已经在程序之外的某个地方记录了,不要在程序内部重复记录;只需参考外部文档。例如,如果您编写一个实现 HTTP 协议的类,那么就不需要在代码中描述 HTTP 协议。在网上已经有很多关于这个文档的来源;只需在您的代码中添加一个简短的注释,并为其中一个源添加一个 URL。另一个例子是已经在用户手册中记录的特性。假设您正在编写一个实现命令集合的程序,其中有一个负责实现每个命令的方法。如果有描述这些命令的用户手册,就不需要在代码中重复这些信息。相反,在每个命令方法的接口注释中包含如下简短说明:

// Implements the Foo command; see the user manual for details.
-

It’s important that readers can easily find all the documentation needed to understand your code, but that doesn’t mean you have to write all of that documentation.

读者可以轻松找到理解代码所需的所有文档,这一点很重要,但这并不意味着您必须编写所有这些文档。

16.5 Maintaining comments: check the diffs 维护注释:检查差异

One good way to make sure documentation stays up to date is to take a few minutes before committing a change to your revision control system to scan over all the changes for that commit; make sure that each change is properly reflected in the documentation. These pre-commit scans will also detect several other problems, such as accidentally leaving debugging code in the system or failing to fix TODO items.

确保文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或未完成的 TODO 项目。

16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护

One final thought on maintaining documentation: comments are easier to maintain if they are higher-level and more abstract than the code. These comments do not reflect the details of the code, so they will not be affected by minor code changes; only changes in overall behavior will affect these comments. Of course, as discussed in Chapter 13, some comments do need to be detailed and precise. But in general, the comments that are most useful (they don’t simply repeat the code) are also easiest to maintain.

关于维护文档的最后一个想法:如果注释比代码更高级,更抽象,则注释更易于维护。这些注释不反映代码的详细信息,因此它们不会受到代码更改的影响;只有整体行为的变化才会影响这些评论。当然,正如第 13 章所讨论的那样,某些注释的确需要详细和精确。但总的来说,最有用的注释(它们不只是重复代码)也最容易维护。

`,54)]))}const r=t(i,[["render",s],["__file","ch16.html.vue"]]),l=JSON.parse('{"path":"/ch16.html","title":"第 16 章 修改现有的代码","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"16.1 Stay strategic 保持战略性","slug":"_16-1-stay-strategic-保持战略性","link":"#_16-1-stay-strategic-保持战略性","children":[]},{"level":2,"title":"16.2 Maintaining comments: keep the comments near the code 维护注释:将注释保留在代码附近","slug":"_16-2-maintaining-comments-keep-the-comments-near-the-code-维护注释-将注释保留在代码附近","link":"#_16-2-maintaining-comments-keep-the-comments-near-the-code-维护注释-将注释保留在代码附近","children":[]},{"level":2,"title":"16.3 Comments belong in the code, not the commit log 注释属于代码,而不是提交日志","slug":"_16-3-comments-belong-in-the-code-not-the-commit-log-注释属于代码-而不是提交日志","link":"#_16-3-comments-belong-in-the-code-not-the-commit-log-注释属于代码-而不是提交日志","children":[]},{"level":2,"title":"16.4 Maintaining comments: avoid duplication 维护注释:避免重复","slug":"_16-4-maintaining-comments-avoid-duplication-维护注释-避免重复","link":"#_16-4-maintaining-comments-avoid-duplication-维护注释-避免重复","children":[]},{"level":2,"title":"16.5 Maintaining comments: check the diffs 维护注释:检查差异","slug":"_16-5-maintaining-comments-check-the-diffs-维护注释-检查差异","link":"#_16-5-maintaining-comments-check-the-diffs-维护注释-检查差异","children":[]},{"level":2,"title":"16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护","slug":"_16-6-higher-level-comments-are-easier-to-maintain-更高级的注释更易于维护","link":"#_16-6-higher-level-comments-are-easier-to-maintain-更高级的注释更易于维护","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch16.md"}');export{r as comp,l as data}; +

It’s important that readers can easily find all the documentation needed to understand your code, but that doesn’t mean you have to write all of that documentation.

读者可以轻松找到理解代码所需的所有文档,这一点很重要,但这并不意味着您必须编写所有这些文档。

16.5 Maintaining comments: check the diffs 维护注释:检查差异

One good way to make sure documentation stays up to date is to take a few minutes before committing a change to your revision control system to scan over all the changes for that commit; make sure that each change is properly reflected in the documentation. These pre-commit scans will also detect several other problems, such as accidentally leaving debugging code in the system or failing to fix TODO items.

确保文档保持最新状态的一种好方法是,在将更改提交到修订控制系统之前需要花费几分钟,以扫描该提交的所有更改。确保文档中正确反映了每个更改。这些预先提交的扫描还将检测其他一些问题,例如意外地将调试代码留在系统中或未完成的 TODO 项目。

16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护

One final thought on maintaining documentation: comments are easier to maintain if they are higher-level and more abstract than the code. These comments do not reflect the details of the code, so they will not be affected by minor code changes; only changes in overall behavior will affect these comments. Of course, as discussed in Chapter 13, some comments do need to be detailed and precise. But in general, the comments that are most useful (they don’t simply repeat the code) are also easiest to maintain.

关于维护文档的最后一个想法:如果注释比代码更高级,更抽象,则注释更易于维护。这些注释不反映代码的详细信息,因此它们不会受到代码更改的影响;只有整体行为的变化才会影响这些评论。当然,正如第 13 章所讨论的那样,某些注释的确需要详细和精确。但总的来说,最有用的注释(它们不只是重复代码)也最容易维护。

`,54)]))}const r=t(i,[["render",s],["__file","ch16.html.vue"]]),l=JSON.parse('{"path":"/ch16.html","title":"第 16 章 修改现有的代码","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"16.1 Stay strategic 保持战略性","slug":"_16-1-stay-strategic-保持战略性","link":"#_16-1-stay-strategic-保持战略性","children":[]},{"level":2,"title":"16.2 Maintaining comments: keep the comments near the code 维护注释:将注释保留在代码附近","slug":"_16-2-maintaining-comments-keep-the-comments-near-the-code-维护注释-将注释保留在代码附近","link":"#_16-2-maintaining-comments-keep-the-comments-near-the-code-维护注释-将注释保留在代码附近","children":[]},{"level":2,"title":"16.3 Comments belong in the code, not the commit log 注释属于代码,而不是提交日志","slug":"_16-3-comments-belong-in-the-code-not-the-commit-log-注释属于代码-而不是提交日志","link":"#_16-3-comments-belong-in-the-code-not-the-commit-log-注释属于代码-而不是提交日志","children":[]},{"level":2,"title":"16.4 Maintaining comments: avoid duplication 维护注释:避免重复","slug":"_16-4-maintaining-comments-avoid-duplication-维护注释-避免重复","link":"#_16-4-maintaining-comments-avoid-duplication-维护注释-避免重复","children":[]},{"level":2,"title":"16.5 Maintaining comments: check the diffs 维护注释:检查差异","slug":"_16-5-maintaining-comments-check-the-diffs-维护注释-检查差异","link":"#_16-5-maintaining-comments-check-the-diffs-维护注释-检查差异","children":[]},{"level":2,"title":"16.6 Higher-level comments are easier to maintain 更高级的注释更易于维护","slug":"_16-6-higher-level-comments-are-easier-to-maintain-更高级的注释更易于维护","link":"#_16-6-higher-level-comments-are-easier-to-maintain-更高级的注释更易于维护","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch16.md"}');export{r as comp,l as data}; diff --git a/assets/ch17.html-CJ9pGCip.js b/assets/ch17.html-Bs7DbX5Q.js similarity index 99% rename from assets/ch17.html-CJ9pGCip.js rename to assets/ch17.html-Bs7DbX5Q.js index de3d375c..fb728054 100644 --- a/assets/ch17.html-CJ9pGCip.js +++ b/assets/ch17.html-Bs7DbX5Q.js @@ -1 +1 @@ -import{_ as t,c as o,f as n,o as a}from"./app-CvqtBB8Z.js";const i={};function s(r,e){return a(),o("div",null,e[0]||(e[0]=[n('

第 17 章 一致性

Chapter 17 Consistency

Consistency is a powerful tool for reducing the complexity of a system and making its behavior more obvious. If a system is consistent, it means that similar things are done in similar ways, and dissimilar things are done in different ways. Consistency creates cognitive leverage: once you have learned how something is done in one place, you can use that knowledge to immediately understand other places that use the same approach. If a system is not implemented in a consistent fashion, developers must learn about each situation separately. This will take more time.

一致性是一个强大的工具,可以降低系统复杂性并使其行为更明显。如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。一致性创造了认知杠杆:一旦您了解了某个地方的工作方式,就可以使用该知识立即了解其他使用相同方法的地方。如果一个系统的没有以一致的方式实施,则开发人员必须分别了解每种情况。这将花费更多时间。

Consistency reduces mistakes. If a system is not consistent, two situations may appear the same when in fact they are different. A developer may see a pattern that looks familiar and make incorrect assumptions based on previous encounters with that pattern. On the other hand, if the system is consistent, assumptions made based on familiar-looking situations will be safe. Consistency allows developers to work more quickly with fewer mistakes.

一致性减少了错误。如果系统不一致,两种情况可能看起来是一样的,但实际上它们是不同的。开发人员可能会看到一个看起来很熟悉的模式,并根据以前遇到的模式做出错误的假设。另一方面,如果系统是一致的,则基于看起来很熟悉的情况所做的假设就会很安全。一致性允许开发人员更快速的工作,并减少错误。

17.1 Examples of consistency 一致性示例

Consistency can be applied at many levels in a system; here are a few examples.

一致性可以应用于系统中的许多层面。这里有一些例子。

Names. Chapter 14 has already discussed the benefits of using names in a consistent way.

名字。第 14 章已经讨论了以一致的方式使用名称的好处。

Coding style. It is common nowadays for development organizations to have style guides that restrict program structure beyond the rules enforced by compilers. Modern style guides address a range of issues, such as indentation, curly-brace placement, order of declarations, naming, commenting, and restrictions on language features considered dangerous. Style guidelines make code easier to read and can reduce some kinds of errors.

编码风格。如今,开发组织通常会制定风格指南,将程序结构限制在编译器所强制执行的规则之外。现代风格指南解决了一系列问题,例如缩进,大括号放置,声明顺序,命名,注释以及对认为危险的语言功能的限制。风格指南使代码更易于阅读,并且可以减少某些类型的错误。

Interfaces. An interface with multiple implementations is another example of consistency. Once you understand one implementation of the interface, any other implementation becomes easier to understand because you already know the features it will have to provide.

接口。具有多个实现的接口是一致性的另一个示例。一旦了解了接口的一种实现,其他任何实现都将变得更易于理解,因为您已经知道它将必须提供的功能。

Design patterns. Design patterns are generally-accepted solutions to certain common problems, such as the model-view-controller approach to user interface design. If you can use an existing design pattern to solve the problem, the implementation will proceed more quickly, it is more likely to work, and your code will be more obvious to readers. Design patterns are discussed in more detail in Section 19.5.

设计模式。设计模式是某些常见问题的普遍接受的解决方案,例如用于用户界面设计的模型-视图-控制器方法。如果您可以使用现有的设计模式来解决问题,则实现会更快地进行,更有可能奏效,并且您的代码对读者来说也会更明显。设计模式将在 19.5 节中详细讨论。

Invariants. An invariant is a property of a variable or structure that is always true. For example, a data structure storing lines of text might enforce an invariant that each line is terminated by a newline character. Invariants reduce the number of special cases that must be considered in code and make it easier to reason about the code’s behavior.

不变量。不变量是一个变量或结构的属性,它总是为真的。例如,存储文本行的数据结构可能会强制要求每行以换行符终止。不变量减少了代码中必须考虑的特殊情况的数量,并且更容易推断代码的行为。

17.2 Ensuring consistency 确保一致性

Consistency is hard to maintain, especially when many people work on a project over a long time. People in one group may not know about conventions established in another group. Newcomers don’t know the rules, so they unintentionally violate the conventions and create new conventions that conflict with existing ones. Here are a few tips for establishing and maintaining consistency:

一致性很难保持,尤其是当许多人长时间从事一个项目时。一个小组的人可能不了解另一小组中建立的约定。新来的人不了解约定,因此他们无意间违反了约定,并创建了与现有约定冲突的新约定。以下是建立和保持一致性的一些技巧:

Document. Create a document that lists the most important overall conventions, such as coding style guidelines. Place the document in a spot where developers are likely to see it, such as a conspicuous place on the project Wiki. Encourage new people joining the group to read the document, and encourage existing people to review it every once in a while. Several style guides from various organizations have been published on the Web; consider starting with one of these.

文档。创建一个文档,列出最重要的总体约定,例如编码风格指南。将文档放置在开发人员可能会看到的位置,例如项目 Wiki 上的显眼位置。鼓励新成员加入小组阅读文档,并鼓励现有人员不时的回顾一下。一些来自不同组织的风格指南已经在网上发布;考虑从其中之一开始。

For conventions that are more localized, such as invariants, find an appropriate spot in the code to document them. If you don’t write the conventions down, it’s unlikely that other people will follow them.

对于那些更加本地化的约定,例如不变量,请在代码中找到合适的位置进行记录。如果您不把这些约定写下来,那么其他人不太可能会遵循它们。

Enforce. Even with good documentation, it’s hard for developers to remember all of the conventions. The best way to enforce conventions is to write a tool that checks for violations, and make sure that code cannot be committed to the repository unless it passes the checker. Automated checkers work particularly well for low-level syntactic conventions.

执行。即使有好的文档,开发人员也很难记住所有约定。执行约定的最佳方法是编写一个检查违规的工具,并确保代码在通过检查器之前不能提交到存储库。自动检查器对于低级别的语法约定特别有用。

One of my recent projects had problems with line termination characters. Some developers worked on Unix, where lines are terminated by newlines; others worked on Windows, where lines are normally terminated by a carriage-return followed by a newline. If a developer on one system made a small edit to a file previously edited on the other system, the editor would sometimes replace all of the line terminators with ones appropriate for that system. This gave the appearance that every line of the file had been modified, which made it hard to track the meaningful changes. We established a convention that files should contain newlines only, but it was hard to ensure that every tool used by every developer followed the convention. Every time a new developer joined the project, we would experience a rash of line termination problems while that developer adjusted to the convention.

我最近的一个项目有行终止字符的问题。一些开发人员在 Unix 上工作,行被换行终止;其他的工作在 Windows 上,行通常由一个 carriage-return 后跟一个换行符来结束。如果一个系统上的开发人员对先前在另一个系统上编辑过的文件进行了小的编辑,那么编辑器有时会将所有行终止符替换为适合该系统的行终止符。这给人的感觉是文件的每一行都被修改了,这使人很难追踪有意义的变化。我们建立了一个约定,即文件应该只包含换行,但是很难确保每个开发人员使用的每个工具都遵循这个约定。每当一个新的开发人员加入这个项目,我们就会遇到大量的行终止问题,而该开发者才会适应这个约定。

We eventually solved this problem by writing a short script that was executed automatically before changes are committed to the source code repository. The script checks all of the files that have been modified and aborts the commit if any of them contain carriage returns. The script can also be run manually to repair damaged files by replacing carriage-return/newline sequences with newlines. This instantly eliminated the problems, and it also helped train new developers.

我们最终解决了这个问题,通过编写了一个简短的脚本,该脚本在更改提交到源代码存储库之前自动执行。该脚本检查所有已修改的文件,如果其中任何一个包含回车符,则中止提交。该脚本也可以手动运行,通过用换行符替换回车/换行符序来修复损坏的文件。这一下子就消除了问题,并且还有助于培训新的开发人员。

Code reviews provide another opportunity for enforcing conventions and for educating new developers about the conventions. The more nit-picky that code reviewers are, the more quickly everyone on the team will learn the conventions, and the cleaner the code will be.

代码审查为实施约定和向新开发者提供有关约定的教育提供了另一个机会。代码审阅者越挑剔,团队中的每个人学习约定的速度就越快,并且代码越清晰。

When in Rome ... The most important convention of all is that every developer should follow the old adage “When in Rome, do as the Romans do.” When working in a new file, look around to see how the existing code is structured. Are all public variables and methods declared before private ones? Are the methods in alphabetical order? Do variables use “camel case,” as in firstServerName, or “snake case,” as in first_server_name? When you see anything that looks like it might possibly be a convention, follow it. When making a design decision, ask yourself if it’s likely that a similar decision was made elsewhere in the project; if so, find an existing example and use the same approach in your new code.

在罗马时……最重要的约定是每个开发人员都应遵循古老的格言“在罗马时,就像罗马人一样。” 在处理新文件时,请环顾四周以了解现有代码的结构。是否在私有变量和方法之前声明了所有公共变量和方法?方法是否按字母顺序排列?变量是像 firstServerName 中那样使用“camel case”,还是像 first_server_name 中那样使用“snake case”?当您看到任何看起来可能是约定的内容时,请遵循该约定。在做出设计决策时,请问自己是否有可能在项目的其他地方做出了类似的决策;如果是这样,请找到一个现有示例,并在新代码中使用相同的方法。

Don’t change existing conventions. Resist the urge to “improve” on existing conventions. Having a “better idea” is not a sufficient excuse to introduce inconsistencies. Your new idea may indeed be better, but the value of consistency over inconsistency is almost always greater than the value of one approach over another. Before introducing inconsistent behavior, ask yourself two questions. First, do you have significant new information justifying your approach that wasn’t available when the old convention was established? Second, is the new approach so much better that it is worth taking the time to update all of the old uses? If your organization agrees that the answers to both questions are “yes,” then go ahead and make the upgrade; when you are done, there should be no sign of the old convention. However, you still run the risk that other developers will not know about the new convention, so they may reintroduce the old approach in the future. Overall, reconsidering established conventions is rarely a good use of developer time.

不要改变现有约定。抵制“改善”现有约定的冲动。拥有一个“更好的主意”并不是引入不一致的充分借口。您的新想法可能确实更好,但是一致性胜于不一致的价值几乎总是大于一种方法胜过另一种方法的价值。在引入不一致的行为之前,请问自己两个问题。首先,您是否拥有大量的新信息来证明您的方法在建立旧约定时是不可用的?其次,新方法是否值得花时间更新所有旧用法?如果您的组织同意对两个问题的回答均为“是”,那么就去进行更新;当您完成后,应该没有旧约定的迹象。然而,您仍然面临着其他开发人员不了解新约定的风险,因此他们将来可能会重新引入旧方法。总体而言,重新考虑已建立的约定很少会很好能很好的利用开发人员时间。

17.3 Taking it too far 走得太远

Consistency means not only that similar things should be done in similar ways, but that dissimilar things should be done in different ways. If you become overzealous about consistency and try to force dissimilar things into the same approach, such as by using the same variable name for things that are really different or using an existing design pattern for a task that doesn’t fit the pattern, you’ll create complexity and confusion. Consistency only provides benefits when developers have confidence that “if it looks like an x, it really is an x.”

一致性不仅意味着相似的事情应该以相似的方式完成,而且不同的事情也应该以不同的方式完成。如果您对一致性过于热衷,并试图将不同的事物强制采用相同的方法,例如对确实不同的事物使用相同的变量名,或者对不适合该模式的任务使用现有的设计模式,那么会造成复杂性和混乱。一致性只有在开发人员确信“如果看起来像 x 时,它确实是 x”时才会带来好处。

17.4 Conclusion 结论

Consistency is another example of the investment mindset. It will take a bit of extra work to ensure consistency: work to decide on conventions, work to create automated checkers, work to look for similar situations to mimic in new code, and work in code reviews to educate the team. The return on this investment is that your code will be more obvious. Developers will be able to understand the code’s behavior more quickly and accurately, and this will allow them to work faster, with fewer bugs.

一致性是投资心态的另一个例子。确保一致性的工作将需要一些额外的工作:确定约定,创建自动检查程序,寻找类似情况以在新代码中模仿,并在代码审查中教育团队成员。这项投资的回报是您的代码将更加明显。开发人员将能够更快,更准确地了解代码的行为,这将使他们能够更快地工作,并减少错误。

',44)]))}const c=t(i,[["render",s],["__file","ch17.html.vue"]]),h=JSON.parse('{"path":"/ch17.html","title":"第 17 章 一致性","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"17.1 Examples of consistency 一致性示例","slug":"_17-1-examples-of-consistency-一致性示例","link":"#_17-1-examples-of-consistency-一致性示例","children":[]},{"level":2,"title":"17.2 Ensuring consistency 确保一致性","slug":"_17-2-ensuring-consistency-确保一致性","link":"#_17-2-ensuring-consistency-确保一致性","children":[]},{"level":2,"title":"17.3 Taking it too far 走得太远","slug":"_17-3-taking-it-too-far-走得太远","link":"#_17-3-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"17.4 Conclusion 结论","slug":"_17-4-conclusion-结论","link":"#_17-4-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch17.md"}');export{c as comp,h as data}; +import{_ as t,c as o,f as n,o as a}from"./app-BQdhfat9.js";const i={};function s(r,e){return a(),o("div",null,e[0]||(e[0]=[n('

第 17 章 一致性

Chapter 17 Consistency

Consistency is a powerful tool for reducing the complexity of a system and making its behavior more obvious. If a system is consistent, it means that similar things are done in similar ways, and dissimilar things are done in different ways. Consistency creates cognitive leverage: once you have learned how something is done in one place, you can use that knowledge to immediately understand other places that use the same approach. If a system is not implemented in a consistent fashion, developers must learn about each situation separately. This will take more time.

一致性是一个强大的工具,可以降低系统复杂性并使其行为更明显。如果系统是一致的,则意味着相似的事情以相似的方式完成,而不同的事情则以不同的方式完成。一致性创造了认知杠杆:一旦您了解了某个地方的工作方式,就可以使用该知识立即了解其他使用相同方法的地方。如果一个系统的没有以一致的方式实施,则开发人员必须分别了解每种情况。这将花费更多时间。

Consistency reduces mistakes. If a system is not consistent, two situations may appear the same when in fact they are different. A developer may see a pattern that looks familiar and make incorrect assumptions based on previous encounters with that pattern. On the other hand, if the system is consistent, assumptions made based on familiar-looking situations will be safe. Consistency allows developers to work more quickly with fewer mistakes.

一致性减少了错误。如果系统不一致,两种情况可能看起来是一样的,但实际上它们是不同的。开发人员可能会看到一个看起来很熟悉的模式,并根据以前遇到的模式做出错误的假设。另一方面,如果系统是一致的,则基于看起来很熟悉的情况所做的假设就会很安全。一致性允许开发人员更快速的工作,并减少错误。

17.1 Examples of consistency 一致性示例

Consistency can be applied at many levels in a system; here are a few examples.

一致性可以应用于系统中的许多层面。这里有一些例子。

Names. Chapter 14 has already discussed the benefits of using names in a consistent way.

名字。第 14 章已经讨论了以一致的方式使用名称的好处。

Coding style. It is common nowadays for development organizations to have style guides that restrict program structure beyond the rules enforced by compilers. Modern style guides address a range of issues, such as indentation, curly-brace placement, order of declarations, naming, commenting, and restrictions on language features considered dangerous. Style guidelines make code easier to read and can reduce some kinds of errors.

编码风格。如今,开发组织通常会制定风格指南,将程序结构限制在编译器所强制执行的规则之外。现代风格指南解决了一系列问题,例如缩进,大括号放置,声明顺序,命名,注释以及对认为危险的语言功能的限制。风格指南使代码更易于阅读,并且可以减少某些类型的错误。

Interfaces. An interface with multiple implementations is another example of consistency. Once you understand one implementation of the interface, any other implementation becomes easier to understand because you already know the features it will have to provide.

接口。具有多个实现的接口是一致性的另一个示例。一旦了解了接口的一种实现,其他任何实现都将变得更易于理解,因为您已经知道它将必须提供的功能。

Design patterns. Design patterns are generally-accepted solutions to certain common problems, such as the model-view-controller approach to user interface design. If you can use an existing design pattern to solve the problem, the implementation will proceed more quickly, it is more likely to work, and your code will be more obvious to readers. Design patterns are discussed in more detail in Section 19.5.

设计模式。设计模式是某些常见问题的普遍接受的解决方案,例如用于用户界面设计的模型-视图-控制器方法。如果您可以使用现有的设计模式来解决问题,则实现会更快地进行,更有可能奏效,并且您的代码对读者来说也会更明显。设计模式将在 19.5 节中详细讨论。

Invariants. An invariant is a property of a variable or structure that is always true. For example, a data structure storing lines of text might enforce an invariant that each line is terminated by a newline character. Invariants reduce the number of special cases that must be considered in code and make it easier to reason about the code’s behavior.

不变量。不变量是一个变量或结构的属性,它总是为真的。例如,存储文本行的数据结构可能会强制要求每行以换行符终止。不变量减少了代码中必须考虑的特殊情况的数量,并且更容易推断代码的行为。

17.2 Ensuring consistency 确保一致性

Consistency is hard to maintain, especially when many people work on a project over a long time. People in one group may not know about conventions established in another group. Newcomers don’t know the rules, so they unintentionally violate the conventions and create new conventions that conflict with existing ones. Here are a few tips for establishing and maintaining consistency:

一致性很难保持,尤其是当许多人长时间从事一个项目时。一个小组的人可能不了解另一小组中建立的约定。新来的人不了解约定,因此他们无意间违反了约定,并创建了与现有约定冲突的新约定。以下是建立和保持一致性的一些技巧:

Document. Create a document that lists the most important overall conventions, such as coding style guidelines. Place the document in a spot where developers are likely to see it, such as a conspicuous place on the project Wiki. Encourage new people joining the group to read the document, and encourage existing people to review it every once in a while. Several style guides from various organizations have been published on the Web; consider starting with one of these.

文档。创建一个文档,列出最重要的总体约定,例如编码风格指南。将文档放置在开发人员可能会看到的位置,例如项目 Wiki 上的显眼位置。鼓励新成员加入小组阅读文档,并鼓励现有人员不时的回顾一下。一些来自不同组织的风格指南已经在网上发布;考虑从其中之一开始。

For conventions that are more localized, such as invariants, find an appropriate spot in the code to document them. If you don’t write the conventions down, it’s unlikely that other people will follow them.

对于那些更加本地化的约定,例如不变量,请在代码中找到合适的位置进行记录。如果您不把这些约定写下来,那么其他人不太可能会遵循它们。

Enforce. Even with good documentation, it’s hard for developers to remember all of the conventions. The best way to enforce conventions is to write a tool that checks for violations, and make sure that code cannot be committed to the repository unless it passes the checker. Automated checkers work particularly well for low-level syntactic conventions.

执行。即使有好的文档,开发人员也很难记住所有约定。执行约定的最佳方法是编写一个检查违规的工具,并确保代码在通过检查器之前不能提交到存储库。自动检查器对于低级别的语法约定特别有用。

One of my recent projects had problems with line termination characters. Some developers worked on Unix, where lines are terminated by newlines; others worked on Windows, where lines are normally terminated by a carriage-return followed by a newline. If a developer on one system made a small edit to a file previously edited on the other system, the editor would sometimes replace all of the line terminators with ones appropriate for that system. This gave the appearance that every line of the file had been modified, which made it hard to track the meaningful changes. We established a convention that files should contain newlines only, but it was hard to ensure that every tool used by every developer followed the convention. Every time a new developer joined the project, we would experience a rash of line termination problems while that developer adjusted to the convention.

我最近的一个项目有行终止字符的问题。一些开发人员在 Unix 上工作,行被换行终止;其他的工作在 Windows 上,行通常由一个 carriage-return 后跟一个换行符来结束。如果一个系统上的开发人员对先前在另一个系统上编辑过的文件进行了小的编辑,那么编辑器有时会将所有行终止符替换为适合该系统的行终止符。这给人的感觉是文件的每一行都被修改了,这使人很难追踪有意义的变化。我们建立了一个约定,即文件应该只包含换行,但是很难确保每个开发人员使用的每个工具都遵循这个约定。每当一个新的开发人员加入这个项目,我们就会遇到大量的行终止问题,而该开发者才会适应这个约定。

We eventually solved this problem by writing a short script that was executed automatically before changes are committed to the source code repository. The script checks all of the files that have been modified and aborts the commit if any of them contain carriage returns. The script can also be run manually to repair damaged files by replacing carriage-return/newline sequences with newlines. This instantly eliminated the problems, and it also helped train new developers.

我们最终解决了这个问题,通过编写了一个简短的脚本,该脚本在更改提交到源代码存储库之前自动执行。该脚本检查所有已修改的文件,如果其中任何一个包含回车符,则中止提交。该脚本也可以手动运行,通过用换行符替换回车/换行符序来修复损坏的文件。这一下子就消除了问题,并且还有助于培训新的开发人员。

Code reviews provide another opportunity for enforcing conventions and for educating new developers about the conventions. The more nit-picky that code reviewers are, the more quickly everyone on the team will learn the conventions, and the cleaner the code will be.

代码审查为实施约定和向新开发者提供有关约定的教育提供了另一个机会。代码审阅者越挑剔,团队中的每个人学习约定的速度就越快,并且代码越清晰。

When in Rome ... The most important convention of all is that every developer should follow the old adage “When in Rome, do as the Romans do.” When working in a new file, look around to see how the existing code is structured. Are all public variables and methods declared before private ones? Are the methods in alphabetical order? Do variables use “camel case,” as in firstServerName, or “snake case,” as in first_server_name? When you see anything that looks like it might possibly be a convention, follow it. When making a design decision, ask yourself if it’s likely that a similar decision was made elsewhere in the project; if so, find an existing example and use the same approach in your new code.

在罗马时……最重要的约定是每个开发人员都应遵循古老的格言“在罗马时,就像罗马人一样。” 在处理新文件时,请环顾四周以了解现有代码的结构。是否在私有变量和方法之前声明了所有公共变量和方法?方法是否按字母顺序排列?变量是像 firstServerName 中那样使用“camel case”,还是像 first_server_name 中那样使用“snake case”?当您看到任何看起来可能是约定的内容时,请遵循该约定。在做出设计决策时,请问自己是否有可能在项目的其他地方做出了类似的决策;如果是这样,请找到一个现有示例,并在新代码中使用相同的方法。

Don’t change existing conventions. Resist the urge to “improve” on existing conventions. Having a “better idea” is not a sufficient excuse to introduce inconsistencies. Your new idea may indeed be better, but the value of consistency over inconsistency is almost always greater than the value of one approach over another. Before introducing inconsistent behavior, ask yourself two questions. First, do you have significant new information justifying your approach that wasn’t available when the old convention was established? Second, is the new approach so much better that it is worth taking the time to update all of the old uses? If your organization agrees that the answers to both questions are “yes,” then go ahead and make the upgrade; when you are done, there should be no sign of the old convention. However, you still run the risk that other developers will not know about the new convention, so they may reintroduce the old approach in the future. Overall, reconsidering established conventions is rarely a good use of developer time.

不要改变现有约定。抵制“改善”现有约定的冲动。拥有一个“更好的主意”并不是引入不一致的充分借口。您的新想法可能确实更好,但是一致性胜于不一致的价值几乎总是大于一种方法胜过另一种方法的价值。在引入不一致的行为之前,请问自己两个问题。首先,您是否拥有大量的新信息来证明您的方法在建立旧约定时是不可用的?其次,新方法是否值得花时间更新所有旧用法?如果您的组织同意对两个问题的回答均为“是”,那么就去进行更新;当您完成后,应该没有旧约定的迹象。然而,您仍然面临着其他开发人员不了解新约定的风险,因此他们将来可能会重新引入旧方法。总体而言,重新考虑已建立的约定很少会很好能很好的利用开发人员时间。

17.3 Taking it too far 走得太远

Consistency means not only that similar things should be done in similar ways, but that dissimilar things should be done in different ways. If you become overzealous about consistency and try to force dissimilar things into the same approach, such as by using the same variable name for things that are really different or using an existing design pattern for a task that doesn’t fit the pattern, you’ll create complexity and confusion. Consistency only provides benefits when developers have confidence that “if it looks like an x, it really is an x.”

一致性不仅意味着相似的事情应该以相似的方式完成,而且不同的事情也应该以不同的方式完成。如果您对一致性过于热衷,并试图将不同的事物强制采用相同的方法,例如对确实不同的事物使用相同的变量名,或者对不适合该模式的任务使用现有的设计模式,那么会造成复杂性和混乱。一致性只有在开发人员确信“如果看起来像 x 时,它确实是 x”时才会带来好处。

17.4 Conclusion 结论

Consistency is another example of the investment mindset. It will take a bit of extra work to ensure consistency: work to decide on conventions, work to create automated checkers, work to look for similar situations to mimic in new code, and work in code reviews to educate the team. The return on this investment is that your code will be more obvious. Developers will be able to understand the code’s behavior more quickly and accurately, and this will allow them to work faster, with fewer bugs.

一致性是投资心态的另一个例子。确保一致性的工作将需要一些额外的工作:确定约定,创建自动检查程序,寻找类似情况以在新代码中模仿,并在代码审查中教育团队成员。这项投资的回报是您的代码将更加明显。开发人员将能够更快,更准确地了解代码的行为,这将使他们能够更快地工作,并减少错误。

',44)]))}const c=t(i,[["render",s],["__file","ch17.html.vue"]]),h=JSON.parse('{"path":"/ch17.html","title":"第 17 章 一致性","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"17.1 Examples of consistency 一致性示例","slug":"_17-1-examples-of-consistency-一致性示例","link":"#_17-1-examples-of-consistency-一致性示例","children":[]},{"level":2,"title":"17.2 Ensuring consistency 确保一致性","slug":"_17-2-ensuring-consistency-确保一致性","link":"#_17-2-ensuring-consistency-确保一致性","children":[]},{"level":2,"title":"17.3 Taking it too far 走得太远","slug":"_17-3-taking-it-too-far-走得太远","link":"#_17-3-taking-it-too-far-走得太远","children":[]},{"level":2,"title":"17.4 Conclusion 结论","slug":"_17-4-conclusion-结论","link":"#_17-4-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch17.md"}');export{c as comp,h as data}; diff --git a/assets/ch18.html-Cq8r6WwA.js b/assets/ch18.html-N4aSI0ya.js similarity index 99% rename from assets/ch18.html-Cq8r6WwA.js rename to assets/ch18.html-N4aSI0ya.js index 276e4f28..58260cd5 100644 --- a/assets/ch18.html-Cq8r6WwA.js +++ b/assets/ch18.html-N4aSI0ya.js @@ -1,4 +1,4 @@ -import{_ as s,c as e,f as a,o as t}from"./app-CvqtBB8Z.js";const o={};function i(p,n){return t(),e("div",null,n[0]||(n[0]=[a(`

第 18 章 代码应该是显而易见的

Chapter 18 Code Should be Obvious

Obscurity is one of the two main causes of complexity described in Section 2.3. Obscurity occurs when important information about a system is not obvious to new developers. The solution to the obscurity problem is to write code in a way that makes it obvious; this chapter discusses some of the factors that make code more or less obvious.

晦涩难懂是第 2.3 节中描述的造成复杂性的两个主要原因之一。当有关系统的重要信息对于新开发人员而言并不明显时,就会发生模糊(当和一个系统相关的一些重要信息对于新的开发人员不那么容易理解,那就是模糊性)。解决晦涩问题的方法是以显而易见的方式编写代码。本章讨论了一些使代码更明显或不明显的因素。

If code is obvious, it means that someone can read the code quickly, without much thought, and their first guesses about the behavior or meaning of the code will be correct. If code is obvious, a reader doesn’t need to spend much time or effort to gather all the information they need to work with the code. If code is not obvious, then a reader must expend a lot of time and energy to understand it. Not only does this reduce their efficiency, but it also increases the likelihood of misunderstanding and bugs. Obvious code needs fewer comments than nonobvious code.

如果代码是显而易见的,则意味着某人可以不加思索地快速阅读该代码,无需多想,他们对代码的行为或含义的初步猜测将是正确的。如果代码是显而易见的,那么读者就不需要花费太多时间或精力来收集他们使用代码所需的所有信息。如果代码不明显,那么读者必须花费大量时间和精力来理解它。这不仅会降低他们的效率,而且还增加了误解和错误的可能性。显而易见的代码比不明显的代码需要更少的注释。

“Obvious” is in the mind of the reader: it’s easier to notice that someone else’s code is nonobvious than to see problems with your own code. Thus, the best way to determine the obviousness of code is through code reviews. If someone reading your code says it’s not obvious, then it’s not obvious, no matter how clear it may seem to you. By trying to understand what made the code nonobvious, you will learn how to write better code in the future.

读者的想法是“显而易见”(易读性是由读者来判断的):注意到别人的代码不明显比发现自己的代码有问题要容易得多(相对来说关注到别人代码中的难理解比注意到自己的代码要容易的多)。因此,确定代码是否显而易见的最佳方法是通过代码审查。如果阅读您代码的人说它并不明显,那么它就不明显,无论它对您来说是多么清晰。通过尝试理解什么使代码变得不明显,您将学习如何在未来写出更好的代码。

18.1 Things that make code more obvious 使代码更明显的事情

Two of the most important techniques for making code obvious have already been discussed in previous chapters. The first is choosing good names (Chapter 14). Precise and meaningful names clarify the behavior of the code and reduce the need for documentation. If a name is vague or ambiguous, then readers will have read through the code in order to deduce the meaning of the named entity; this is time-consuming and error-prone. The second technique is consistency (Chapter 17). If similar things are always done in similar ways, then readers can recognize patterns they have seen before and immediately draw (safe) conclusions without analyzing the code in detail.

在前面的章节中已经讨论了使代码显而易见的两种最重要的技术。首先是选择好名字(第 14 章)。精确而有意义的名称可以阐明代码的行为,并减少对文档的需求。如果名字含糊不清,那么读者将不得不通读代码,以推论命名实体的含义;这既费时又容易出错。第二种技术是一致性(第 17 章)。如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。

Here are a few other general-purpose techniques for making code more obvious:

以下是使代码更明显的其他一些通用技术:

Judicious use of white space. The way code is formatted can impact how easy it is to understand. Consider the following parameter documentation, in which whitespace has been squeezed out:

明智地使用空白。代码的格式化方式会影响其理解的容易程度。考虑以下参数文档,其中空格已被压缩:

/**
+import{_ as s,c as e,f as a,o as t}from"./app-BQdhfat9.js";const o={};function i(p,n){return t(),e("div",null,n[0]||(n[0]=[a(`

第 18 章 代码应该是显而易见的

Chapter 18 Code Should be Obvious

Obscurity is one of the two main causes of complexity described in Section 2.3. Obscurity occurs when important information about a system is not obvious to new developers. The solution to the obscurity problem is to write code in a way that makes it obvious; this chapter discusses some of the factors that make code more or less obvious.

晦涩难懂是第 2.3 节中描述的造成复杂性的两个主要原因之一。当有关系统的重要信息对于新开发人员而言并不明显时,就会发生模糊(当和一个系统相关的一些重要信息对于新的开发人员不那么容易理解,那就是模糊性)。解决晦涩问题的方法是以显而易见的方式编写代码。本章讨论了一些使代码更明显或不明显的因素。

If code is obvious, it means that someone can read the code quickly, without much thought, and their first guesses about the behavior or meaning of the code will be correct. If code is obvious, a reader doesn’t need to spend much time or effort to gather all the information they need to work with the code. If code is not obvious, then a reader must expend a lot of time and energy to understand it. Not only does this reduce their efficiency, but it also increases the likelihood of misunderstanding and bugs. Obvious code needs fewer comments than nonobvious code.

如果代码是显而易见的,则意味着某人可以不加思索地快速阅读该代码,无需多想,他们对代码的行为或含义的初步猜测将是正确的。如果代码是显而易见的,那么读者就不需要花费太多时间或精力来收集他们使用代码所需的所有信息。如果代码不明显,那么读者必须花费大量时间和精力来理解它。这不仅会降低他们的效率,而且还增加了误解和错误的可能性。显而易见的代码比不明显的代码需要更少的注释。

“Obvious” is in the mind of the reader: it’s easier to notice that someone else’s code is nonobvious than to see problems with your own code. Thus, the best way to determine the obviousness of code is through code reviews. If someone reading your code says it’s not obvious, then it’s not obvious, no matter how clear it may seem to you. By trying to understand what made the code nonobvious, you will learn how to write better code in the future.

读者的想法是“显而易见”(易读性是由读者来判断的):注意到别人的代码不明显比发现自己的代码有问题要容易得多(相对来说关注到别人代码中的难理解比注意到自己的代码要容易的多)。因此,确定代码是否显而易见的最佳方法是通过代码审查。如果阅读您代码的人说它并不明显,那么它就不明显,无论它对您来说是多么清晰。通过尝试理解什么使代码变得不明显,您将学习如何在未来写出更好的代码。

18.1 Things that make code more obvious 使代码更明显的事情

Two of the most important techniques for making code obvious have already been discussed in previous chapters. The first is choosing good names (Chapter 14). Precise and meaningful names clarify the behavior of the code and reduce the need for documentation. If a name is vague or ambiguous, then readers will have read through the code in order to deduce the meaning of the named entity; this is time-consuming and error-prone. The second technique is consistency (Chapter 17). If similar things are always done in similar ways, then readers can recognize patterns they have seen before and immediately draw (safe) conclusions without analyzing the code in detail.

在前面的章节中已经讨论了使代码显而易见的两种最重要的技术。首先是选择好名字(第 14 章)。精确而有意义的名称可以阐明代码的行为,并减少对文档的需求。如果名字含糊不清,那么读者将不得不通读代码,以推论命名实体的含义;这既费时又容易出错。第二种技术是一致性(第 17 章)。如果总是以相似的方式完成相似的事情,那么读者可以识别出他们以前所见过的模式,并立即得出(安全)结论,而无需详细分析代码。

Here are a few other general-purpose techniques for making code more obvious:

以下是使代码更明显的其他一些通用技术:

Judicious use of white space. The way code is formatted can impact how easy it is to understand. Consider the following parameter documentation, in which whitespace has been squeezed out:

明智地使用空白。代码的格式化方式会影响其理解的容易程度。考虑以下参数文档,其中空格已被压缩:

/**
  *  ...
  *  @param numThreads The number of threads that this manager should
  *  spin up in order to manage ongoing connections. The MessageManager
@@ -67,4 +67,4 @@ import{_ as s,c as e,f as a,o as t}from"./app-CvqtBB8Z.js";const o={};function i
     ...
     new RaftClient(myAddress, serverAddresses);
 }
-

Most applications exit when their main programs return, so readers are likely to assume that will happen here. However, that is not the case. The constructor for RaftClient creates additional threads, which continue to operate even though the application’s main thread finishes. This behavior should be documented in the interface comment for the RaftClient constructor, but the behavior is nonobvious enough that it’s worth putting a short comment at the end of main as well. The comment should indicate that the application will continue executing in other threads. Code is most obvious if it conforms to the conventions that readers will be expecting; if it doesn’t, then it’s important to document the behavior so readers aren’t confused.

大多数应用程序在其主程序返回时退出,因此读者可能会认为这将在此处发生。然而,事实并非如此。RaftClient 的构造函数创建了额外的线程,即使应用程序的主线程结束了,该线程仍在继续运行。应该在 RaftClient 构造函数的接口注释中记录此行为,但是该行为不够明显,因此值得在 main 末尾添加简短注释。该注释应指示该应用程序将继续在其他线程中执行。如果代码符合读者期望的惯例,那么它是最明显的。如果没有,那么记录该行为很重要,这样读者就不会感到困惑。

18.3 Conclusion 结论

Another way of thinking about obviousness is in terms of information. If code is nonobvious, that usually means there is important information about the code that the reader does not have: in the RaftClient example, the reader might not know that the RaftClient constructor created new threads; in the Pair example, the reader might not know that result.getKey() returns the number of the current term.

关于显而易见性的另一种思考方式是信息。如果代码不是显而易见的,则通常意味着存在有关读者所不具备的代码的重要信息:在 RaftClient 示例中,读者可能不知道 RaftClient 构造函数创建了新线程;在“配对”示例中,读者可能不知道 result.getKey()返回当前项的编号。

To make code obvious, you must ensure that readers always have the information they need to understand it. You can do this in three ways. The best way is to reduce the amount of information that is needed, using design techniques such as abstraction and eliminating special cases. Second, you can take advantage of information that readers have already acquired in other contexts (for example, by following conventions and conforming to expectations) so readers don’t have to learn new information for your code. Third, you can present the important information to them in the code, using techniques such as good names and strategic comments.

为了使代码清晰可见,您必须确保读者始终拥有理解它们所需的信息。您可以通过三种方式执行此操作。最好的方法是使用抽象等设计技术并消除特殊情况,以减少所需的信息量。其次,您可以利用读者在其他情况下已经获得的信息(例如,通过遵循约定并符合期望),这样读者不必为您的代码学习新的信息。第三,您可以使用诸如好名和战略注释之类的技术在代码中向他们提供重要信息。

`,66)]))}const c=s(o,[["render",i],["__file","ch18.html.vue"]]),r=JSON.parse('{"path":"/ch18.html","title":"第 18 章 代码应该是显而易见的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"18.1 Things that make code more obvious 使代码更明显的事情","slug":"_18-1-things-that-make-code-more-obvious-使代码更明显的事情","link":"#_18-1-things-that-make-code-more-obvious-使代码更明显的事情","children":[]},{"level":2,"title":"18.2 Things that make code less obvious 使代码不那么明显的事情","slug":"_18-2-things-that-make-code-less-obvious-使代码不那么明显的事情","link":"#_18-2-things-that-make-code-less-obvious-使代码不那么明显的事情","children":[]},{"level":2,"title":"18.3 Conclusion 结论","slug":"_18-3-conclusion-结论","link":"#_18-3-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch18.md"}');export{c as comp,r as data}; +

Most applications exit when their main programs return, so readers are likely to assume that will happen here. However, that is not the case. The constructor for RaftClient creates additional threads, which continue to operate even though the application’s main thread finishes. This behavior should be documented in the interface comment for the RaftClient constructor, but the behavior is nonobvious enough that it’s worth putting a short comment at the end of main as well. The comment should indicate that the application will continue executing in other threads. Code is most obvious if it conforms to the conventions that readers will be expecting; if it doesn’t, then it’s important to document the behavior so readers aren’t confused.

大多数应用程序在其主程序返回时退出,因此读者可能会认为这将在此处发生。然而,事实并非如此。RaftClient 的构造函数创建了额外的线程,即使应用程序的主线程结束了,该线程仍在继续运行。应该在 RaftClient 构造函数的接口注释中记录此行为,但是该行为不够明显,因此值得在 main 末尾添加简短注释。该注释应指示该应用程序将继续在其他线程中执行。如果代码符合读者期望的惯例,那么它是最明显的。如果没有,那么记录该行为很重要,这样读者就不会感到困惑。

18.3 Conclusion 结论

Another way of thinking about obviousness is in terms of information. If code is nonobvious, that usually means there is important information about the code that the reader does not have: in the RaftClient example, the reader might not know that the RaftClient constructor created new threads; in the Pair example, the reader might not know that result.getKey() returns the number of the current term.

关于显而易见性的另一种思考方式是信息。如果代码不是显而易见的,则通常意味着存在有关读者所不具备的代码的重要信息:在 RaftClient 示例中,读者可能不知道 RaftClient 构造函数创建了新线程;在“配对”示例中,读者可能不知道 result.getKey()返回当前项的编号。

To make code obvious, you must ensure that readers always have the information they need to understand it. You can do this in three ways. The best way is to reduce the amount of information that is needed, using design techniques such as abstraction and eliminating special cases. Second, you can take advantage of information that readers have already acquired in other contexts (for example, by following conventions and conforming to expectations) so readers don’t have to learn new information for your code. Third, you can present the important information to them in the code, using techniques such as good names and strategic comments.

为了使代码清晰可见,您必须确保读者始终拥有理解它们所需的信息。您可以通过三种方式执行此操作。最好的方法是使用抽象等设计技术并消除特殊情况,以减少所需的信息量。其次,您可以利用读者在其他情况下已经获得的信息(例如,通过遵循约定并符合期望),这样读者不必为您的代码学习新的信息。第三,您可以使用诸如好名和战略注释之类的技术在代码中向他们提供重要信息。

`,66)]))}const c=s(o,[["render",i],["__file","ch18.html.vue"]]),r=JSON.parse('{"path":"/ch18.html","title":"第 18 章 代码应该是显而易见的","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"18.1 Things that make code more obvious 使代码更明显的事情","slug":"_18-1-things-that-make-code-more-obvious-使代码更明显的事情","link":"#_18-1-things-that-make-code-more-obvious-使代码更明显的事情","children":[]},{"level":2,"title":"18.2 Things that make code less obvious 使代码不那么明显的事情","slug":"_18-2-things-that-make-code-less-obvious-使代码不那么明显的事情","link":"#_18-2-things-that-make-code-less-obvious-使代码不那么明显的事情","children":[]},{"level":2,"title":"18.3 Conclusion 结论","slug":"_18-3-conclusion-结论","link":"#_18-3-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch18.md"}');export{c as comp,r as data}; diff --git a/assets/ch19.html-_fJpQetx.js b/assets/ch19.html-5VdqwktA.js similarity index 99% rename from assets/ch19.html-_fJpQetx.js rename to assets/ch19.html-5VdqwktA.js index 635fa5fa..9f562c15 100644 --- a/assets/ch19.html-_fJpQetx.js +++ b/assets/ch19.html-5VdqwktA.js @@ -1 +1 @@ -import{_ as t,c as s,f as a,o}from"./app-CvqtBB8Z.js";const n={};function i(r,e){return o(),s("div",null,e[0]||(e[0]=[a('

第 19 章 软件发展趋势

Chapter 19 Software Trends

As a way of illustrating the principles discussed in this book, this chapter considers several trends and patterns that have become popular in software development over the last few decades. For each trend, I will describe how that trend relates to the principles in this book and use the principles to evaluate whether that trend provides leverage against software complexity.

为了说明本书中讨论的原理,本章考虑了过去几十年来在软件开发中流行的几种趋势和模式。对于每种趋势,我将描述该趋势与本书中的原理之间的关系,并使用这些原理来评估该趋势是否提供了针对软件复杂性的杠杆作用。

19.1 Object-oriented programming and inheritance 面向对象的编程和继承

Object-oriented programming is one of the most important new ideas in software development over the last 30–40 years. It introduced notions such as classes, inheritance, private methods, and instance variables. If used carefully, these mechanisms can help to produce better software designs. For example, private methods and variables can be used to ensure information hiding: no code outside a class can invoke private methods or access private variables, so there can’t be any external dependencies on them.

在过去的 30-40 年中,面向对象编程是软件开发中最重要的新思想之一。它引入了诸如类,继承,私有方法和实例变量之类的概念。如果谨慎使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可用于确保信息隐藏:类外部的任何代码都不能调用私有方法或访问私有变量,所以没有任何外部依赖。

One of the key elements of object-oriented programming is inheritance. Inheritance comes in two forms, which have different implications for software complexity. The first form of inheritance is interface inheritance, in which a parent class defines the signatures for one or more methods, but does not implement the methods. Each subclass must implement the signatures, but different subclasses can implement the same methods in different ways. For example, the interface might define methods for performing I/O; one subclass might implement the I/O operations for disk files, and another subclass might implement the same operations for network sockets.

面向对象编程的关键要素之一是继承。继承有两种形式,它们对软件复杂性有不同的影响。继承的第一种形式是接口继承,其中父类定义一个或多个方法的签名,但不实现这些方法。每个子类都必须实现签名,但是不同的子类可以以不同的方式实现相同的方法。例如,该接口可能定义用于执行 I/O 的方法。一个子类可能对磁盘文件实现 I/O 操作,而另一个子类可能对网络套接字实现相同的操作。

Interface inheritance provides leverage against complexity by reusing the same interface for multiple purposes. It allows knowledge acquired in solving one problem (such as how to use an I/O interface to read and write disk files) to be used to solve other problems (such as communicating over a network socket). Another way of thinking about this is in terms of depth: the more different implementations there are of an interface, the deeper the interface becomes. In order for an interface to have many implementations, it must capture the essential features of all the underlying implementations while steering clear of the details that differ between the implementations; this notion is at the heart of abstraction.

接口继承通过将同一接口用于多种用途,从而提供了对抗复杂性的杠杆作用。它使解决一个问题(例如如何使用 I/O 接口读取和写入磁盘文件)中获得的知识可以用于解决其他问题(例如通过网络套接字进行通信)。关于深度的另一种思考方式是:接口的不同实现越多,接口的深度就越大。为了让一个接口有很多实现,它必须拥有所有底层实现的基本特征,同时避免不同实现之间的差异。这个概念是抽象的核心所在。

The second form of inheritance is implementation inheritance. In this form, a parent class defines not only signatures for one or more methods, but also default implementations. Subclasses can choose to inherit the parent’s implementation of a method or override it by defining a new method with the same signature. Without implementation inheritance, the same method implementation might need to be duplicated in several subclasses, which would create dependencies between those subclasses (modifications would need to be duplicated in all copies of the method). Thus, implementation inheritance reduces the amount of code that needs to be modified as the system evolves; in other words, it reduces the change amplification problem described in Chapter 2.

继承的第二种形式是实现继承。以这种形式,父类不仅定义了一个或多个方法的签名,而且还定义了默认实现。子类可以选择继承方法的父类实现,也可以通过定义具有相同签名的新方法来覆盖它。如果没有实现继承,则可能需要在几个子类中复制相同的方法实现,这将在这些子类之间创建依赖关系(需要在方法的所有副本中复制修改)。因此,实现继承减少了随着系统的发展而需要修改的代码量。换句话说,它减少了第 2 章中描述的变化放大问题。

However, implementation inheritance creates dependencies between the parent class and each of its subclasses. Class instance variables in the parent class are often accessed by both the parent and child classes; this results in information leakage between the classes in the inheritance hierarchy and makes it hard to modify one class in the hierarchy without looking at the others. For example, a developer making changes to the parent class may need to examine all of the subclasses to ensure that the changes don’t break anything. Similarly, if a subclass overrides a method in the parent class, the developer of the subclass may need to examine the implementation in the parent. In the worst case, programmers will need complete knowledge of the entire class hierarchy underneath the parent class in order to make changes to any of the classes. Class hierarchies that use implementation inheritance extensively tend to have high complexity.

但是,实现继承会在父类及其每个子类之间创建依赖关系。父类中的类实例变量经常被父类和子类访问。这导致了继承层次中的类之间的信息泄漏,并且使修改层次中的一个类时很难不考虑其他类。例如,对父类进行修改的开发人员可能需要检查所有子类,以确保所做的修改不会破坏任何内容。同样,如果子类覆盖了父类中的方法,则子类的开发人员可能需要检查父类中的实现。在最坏的情况下,程序员将需要完全了解父类下的整个类层次结构,以便对任何类进行更改。广泛使用实现继承的类层次结构往往具有很高的复杂性。

Thus, implementation inheritance should be used with caution. Before using implementation inheritance, consider whether an approach based on composition can provide the same benefits. For instance, it may be possible to use small helper classes to implement the shared functionality. Rather than inheriting functions from a parent, the original classes can each build upon the features of the helper classes.

因此,应谨慎使用实现继承。在使用实现继承之前,请考虑基于组合的方法是否可以提供相同的好处。例如,可以使用小型辅助类来实现共享功能。与其从父类中继承功能,原始类可以各自建立在辅助类的功能之上。

If there is no viable alternative to implementation inheritance, try to separate the state managed by the parent class from that managed by subclasses. One way to do this is for certain instance variables to be managed entirely by methods in the parent class, with subclasses using them only in a read-only fashion or through other methods in the parent class. This applies the notion of information hiding within the class hierarchy to reduce dependencies.

如果没有实现继承的可行的替代方案,请尝试将父类管理的状态与子类管理的状态分开。一种方法是让某些实例变量完全由父类中的方法管理,子类仅以只读方式或通过父类中的其他方法使用它们。这适用于隐藏在类层次结构中的信息的概念,以减少依赖性。

Although the mechanisms provided by object-oriented programming can assist in implementing clean designs, they do not, by themselves, guarantee good design. For example, if classes are shallow, or have complex interfaces, or permit external access to their internal state, then they will still result in high complexity.

尽管面向对象编程提供的机制可有助于实现干净的设计,但是它们本身不能保证良好的设计。例如,如果类很浅,或者具有复杂的接口,或者允许外部访问其内部状态,那么它们仍将导致很高的复杂性。

19.2 Agile development 敏捷开发

Agile development is an approach to software development that emerged in the late 1990’s from a collection of ideas about how to make software development more lightweight, flexible, and incremental; it was formally defined during a meeting of practitioners in 2001. Agile development is mostly about the process of software development (organizing teams, managing schedules, the role of unit testing, interacting with customers, etc.) as opposed to software design. Nonetheless, it relates to some of the design principles in this book.

敏捷开发是 20 世纪 90 年代末出现的一种软件开发方法,是关于如何使软件开发更加轻量,灵活和渐进的一系列想法。它是在 2001 年的一次从业者会议上正式定义的。敏捷开发主要是关于软件开发的过程(组织团队,管理进度表,单元测试的角色,与客户交互等),而不是软件设计。但是,它与本书中的一些设计原则有关。

One of the most important elements of agile development is the notion that development should be incremental and iterative. In the agile approach, a software system is developed in a series of iterations, each of which adds and evaluates a few new features; each iteration includes design, test, and customer input. In general, this is similar to the incremental approach advocated here. As mentioned in Chapter 1, it isn’t possible to visualize a complex system well enough at the outset of a project to determine the best design. The best way to end up with a good design is to develop a system in increments, where each increment adds a few new abstractions and refactors existing abstractions based on experience. This is similar to the agile development approach.

敏捷开发中最重要的元素之一是开发应该是渐进的和迭代的概念。在敏捷方法中,软件系统是通过一系列迭代开发的,每个迭代都添加并评估了一些新功能。每个迭代都包括设计,测试和客户的意见。通常,这类似于这里提倡的增量方法。如第 1 章所述,在项目开始时就不可能对复杂的系统进行充分的可视化以决定最佳设计。最终获得良好设计的最佳方法是逐步开发一个系统,其中每个增量都会添加一些新的抽象,并根据经验重构现有的抽象。这类似于敏捷开发方法。

One of the risks of agile development is that it can lead to tactical programming. Agile development tends to focus developers on features, not abstractions, and it encourages developers to put off design decisions in order to produce working software as soon as possible. For example, some agile practitioners argue that you shouldn’t implement general-purpose mechanisms right away; implement a minimal special-purpose mechanism to start with, and refactor into something more generic later, once you know that it’s needed. Although these arguments make sense to a degree, they argue against an investment approach, and they encourage a more tactical style of programming. This can result in a rapid accumulation of complexity.

敏捷开发的风险之一是它可能导致战术编程。敏捷开发倾向于将开发人员的注意力集中在功能上,而不是抽象上,它鼓励开发人员推迟设计决策,以便尽快生产可以使用的软件。例如,一些敏捷的实践者认为,您不应该马上实现通用机制;应该先实现一个最小的特殊用途机制,然后在知道需要它时重构为更通用的东西。尽管这些论点在一定程度上是合理的,但它们反对投资方法,并鼓励采用更具战术性的编程风格。这会导致复杂性的快速累积。

Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features. It’s fine to put off all thoughts about a particular abstraction until it’s needed by a feature. Once you need the abstraction, invest the time to design it cleanly; follow the advice of Chapter 6 and make it somewhat general-purpose.

渐进式开发通常是一个好主意,但是软件开发的增量应该是抽象而不是功能。可以推迟对特定抽象的所有想法,直到功能需要它为止。一旦需要抽象,就要花一些时间进行简洁的设计。遵循第 6 章的建议并使其具有通用性。

19.3 Unit tests 单元测试

It used to be that developers rarely wrote tests. If tests were written at all, they were written by a separate QA team. However, one of the tenets of agile development is that testing should be tightly integrated with development, and programmers should write tests for their own code. This practice has now become widespread. Tests are typically divided into two kinds: unit tests and system tests. Unit tests are the ones most often written by developers. They are small and focused: each test usually validates a small section of code in a single method. Unit tests can be run in isolation, without setting up a production environment for the system. Unit tests are often run in conjunction with a test coverage tool to ensure that every line of code in the application is tested. Whenever developers write new code or modify existing code, they are responsible for updating the unit tests to maintain proper test coverage.

过去,开发人员很少编写测试。如果测试是由一个独立的 QA 团队编写的,那么它们就是由一个独立的 QA 团队编写的。然而,敏捷开发的原则之一是测试应该与开发紧密集成,程序员应该为他们自己的代码编写测试。这种做法现在已经很普遍了。测试通常分为两类:单元测试和系统测试。单元测试是开发人员最常编写的测试。它们很小,而且重点突出:每个测试通常在单个方法中验证一小段代码。单元测试可以独立运行,而不需要为系统设置生产环境。单元测试通常与测试覆盖工具一起运行,以确保应用程序中的每一行代码都经过了测试。每当开发人员编写新代码或修改现有代码时,他们都要负责更新单元测试以保持适当的测试覆盖率。

The second kind of test consists of system tests (sometimes called integration tests), which ensure that the different parts of an application all work together properly. They typically involve running the entire application in a production environment. System tests are more likely to be written by a separate QA or testing team.

第二种测试包括系统测试(有时称为集成测试),这些测试可确保应用程序的不同部分都能正常协同工作。它们通常涉及在生产环境中运行整个应用程序。系统测试更有可能由独立的质量检查或测试小组编写。

Tests, particularly unit tests, play an important role in software design because they facilitate refactoring. Without a test suite, it’s dangerous to make major structural changes to a system. There’s no easy way to find bugs, so it’s likely that bugs will go undetected until the new code is deployed, where they are much more expensive to find and fix. As a result, developers avoid refactoring in systems without good test suites; they try to minimize the number of code changes for each new feature or bug fix, which means that complexity accumulates and design mistakes don’t get corrected.

测试,尤其是单元测试,在软件设计中起着重要作用,因为它们有助于重构。没有测试套件,对系统进行重大结构更改很危险。没有简单的方法可以找到错误,因此在部署新代码之前,很可能将无法检测到错误,因为在新代码中查找和修复它们的成本要高得多。结果,开发人员避免在没有良好测试套件的系统中进行重构。他们尝试将每个新功能或错误修复的代码更改次数降至最低,这意味着复杂性会累积,而设计错误不会得到纠正。

With a good set of tests, developers can be more confident when refactoring because the test suite will find most bugs that are introduced. This encourages developers to make structural improvements to a system, which results in a better design. Unit tests are particularly valuable: they provide a higher degree of code coverage than system tests, so they are more likely to uncover any bugs.

有了一套很好的测试,开发人员可以在重构时更有信心,因为测试套件将发现大多数引入的 bug。这鼓励开发人员对系统进行结构改进,从而获得更好的设计。单元测试特别有价值:与系统测试相比,它们提供更高的代码覆盖率,因此它们更有可能发现任何 bug。

For example, during the development of the Tcl scripting language, we decided to improve performance by replacing Tcl’s interpreter with a byte-code compiler. This was a huge change that affected almost every part of the core Tcl engine. Fortunately, Tcl had an excellent unit test suite, which we ran on the new byte-code engine. The existing tests were so effective in uncovering bugs in the new engine that only a single bug turned up after the alpha release of the byte-code compiler.

例如,在开发 Tcl 脚本语言期间,我们决定通过用字节码编译器替换 Tcl 的解释器来提高性能。这是一个巨大的变化,几乎影响了核心 Tcl 引擎的每个部分。幸运的是,Tcl 有一个出色的单元测试套件,我们在新的字节码引擎上运行了该套件。现有测试在发现新引擎中的错误方面是如此有效,以至于在字节码编译器的 alpha 版本发布之后仅出现了一个错误。

19.4 Test-driven development 测试驱动的开发

Test-driven development is an approach to software development where programmers write unit tests before they write code. When creating a new class, the developer first writes unit tests for the class, based on its expected behavior. None of the tests pass, since there is no code for the class. Then the developer works through the tests one at a time, writing enough code for that test to pass. When all of the tests pass, the class is finished.

测试驱动开发是一种软件开发方法,程序员可以在编写代码之前先编写单元测试。创建新类时,开发人员首先根据其预期行为为该类编写单元测试。没有一个测试通过,因为该类没有代码。然后,开发人员一次完成一个测试,编写足够的代码以使该测试通过。所有测试通过后,这个类的功能就完成了。

Although I am a strong advocate of unit testing, I am not a fan of test-driven development. The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design. This is tactical programming pure and simple, with all of its disadvantages. Test-driven development is too incremental: at any point in time, it’s tempting to just hack in the next feature to make the next test pass. There’s no obvious time to do design, so it’s easy to end up with a mess.

尽管我是单元测试的坚决拥护者,但我并不热衷测试驱动的开发。测试驱动开发的问题在于,它将注意力集中在使特定功能正常工作上,而不是寻找最佳设计。这是纯粹的战术性编程,有其所有的弊端。测试驱动的开发过于增量:在任何时间点,很容易完成一个功能然后让测试通过。没有明显的时间来做设计,因此很容易搞的一团糟。

As mentioned in Section 19.2, the units of development should be abstractions, not features. Once you discover the need for an abstraction, don’t create the abstraction in pieces over time; design it all at once (or at least enough to provide a reasonably comprehensive set of core functions). This is more likely to produce a clean design whose pieces fit together well.

如第 19.2 节所述,开发单位应该是抽象的,而不是功能。一旦发现了对抽象的需求,就不要在一段时间内零散的创建抽象,而应该一次性的设计(或至少足以提供一组合理全面的核心功能)。这样更有可能产生干净的设计,使各个部分很好地契合在一起。

One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug. Then fix the bug and make sure that the unit test now passes. This is the best way to make sure you really have fixed the bug. If you fix the bug before writing the test, it’s possible that the new unit test doesn’t actually trigger the bug, in which case it won’t tell you whether you really fixed the problem.

有一个地方先编写测试是有意义的,那就是修复 bug 的时候。在修复错误之前,请编写由于该错误而失败的单元测试。然后修复该错误,并确保现在可以通过单元测试。这是确保您已真正修复该错误的最佳方法。如果您在编写测试之前就已修复了该错误,则新的单元测试很可能实际上不会触发该错误,在这种情况下,它不会告诉您是否真的修复了该问题。

19.5 Design patterns 设计模式

A design pattern is a commonly used approach for solving a particular kind of problem, such as an iterator or an observer. The notion of design patterns was popularized by the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, and design patterns are now widely used in object-oriented software development.

设计模式是解决特定类型问题(例如迭代器或观察器)的常用方法。设计模式的概念在 Gamma,Helm,Johnson 和 Vlissides 的《设计模式:可复用的面向对象软件的基础》一书中而普及,现在设计模式已广泛用于面向对象的软件开发中。

Design patterns represent an alternative to design: rather than designing a new mechanism from scratch, just apply a well-known design pattern. For the most part, this is good: design patterns arose because they solve common problems, and because they are generally agreed to provide clean solutions. If a design pattern works well in a particular situation, it will probably be hard for you to come up with a different approach that is better.

设计模式代表了设计的另一种选择:与其从头设计新机制,不如应用一种众所周知的设计模式。在大多数情况下,这是很好的:设计模式的出现是因为它们解决了常见的问题,并且因为它们被普遍认为提供干净的解决方案。如果设计模式在特定情况下运作良好,那么您可能很难想出另一种更好的方法。

The greatest risk with design patterns is over-application. Not every problem can be solved cleanly with an existing design pattern; don’t try to force a problem into a design pattern when a custom approach will be cleaner. Using design patterns doesn’t automatically improve a software system; it only does so if the design patterns fit. As with many ideas in software design, the notion that design patterns are good doesn’t necessarily mean that more design patterns are better.

设计模式的最大风险是过度使用。不是每个问题都可以用现有的设计模式来解决。当自定义方法更加简洁时,请勿尝试将问题强加到设计模式中。使用设计模式并不能自动改善软件系统。只有在设计模式合适的情况下才会如此。与软件设计中的许多想法一样,设计模式良好的概念并不一定意味着更多的设计模式会更好。

19.6 Getters and setters Getter 和 Setters

In the Java programming community, getter and setter methods are a popular design pattern. A getter and a setter are associated with an instance variable for a class. They have names like getFoo and setFoo, where Foo is the name of the variable. The getter method returns the current value of the variable, and the setter method modifies the value.

在 Java 编程社区中,getter 和 setter 方法是一种流行的设计模式。一个 getter 和一个 setter 与一个类的实例变量相关联。它们具有类似 getFoo 和 setFoo 的名称,其中 Foo 是变量的名称。getter 方法返回变量的当前值,setter 方法修改该值。

Getters and setters aren’t strictly necessary, since instance variables can be made public. The argument for getters and setters is that they allow additional functions to be performed while getting and setting, such as updating related values when a variable changes, notifying listeners of changes, or enforcing constraints on values. Even if these features aren’t needed initially, they can be added later without changing the interface.

由于实例变量可以公开,因此不一定必须使用 getter 和 setter 方法。getter 和 setter 的论点是,它们允许在获取和设置时执行额外功能,例如在变量更改时更新相关值,通知监听器的变化或对值实施约束。即使最初不需要这些功能,以后也可以在不更改界面的情况下添加它们。

Although it may make sense to use getters and setters if you must expose instance variables, it’s better not to expose instance variables in the first place. Exposed instance variables mean that part of the class’s implementation is visible externally, which violates the idea of information hiding and increases the complexity of the class’s interface. Getters and setters are shallow methods (typically only a single line), so they add clutter to the class’s interface without providing much functionality. It’s better to avoid getters and setters (or any exposure of implementation data) as much as possible.

如果必须公开实例变量,则可以使用 getter 和 setter 方法,但最好不要首先公开实例变量。暴露的实例变量意味着类的实现的一部分在外部是可见的,这违反了信息隐藏的思想,并增加了类接口的复杂性。Getter 和 Setter 是浅层方法(通常只有一行),因此它们在不提供太多功能的情况下使类的接口变得混乱。最好避免使用 getter 和 setter(或任何公开的实现数据)。

One of the risks of establishing a design pattern is that developers assume the pattern is good and try to use it as much as possible. This has led to overusage of getters and setters in Java.

建立设计模式的风险之一是开发人员认为该模式是好的,并试图尽可能多地使用它。这导致 Java 中的 getter 和 setter 的过度使用。

19.7 Conclusion 结论

Whenever you encounter a proposal for a new software development paradigm, challenge it from the standpoint of complexity: does the proposal really help to minimize complexity in large software systems? Many proposals sound good on the surface, but if you look more deeply you will see that some of them make complexity worse, not better.

每当您遇到有关新软件开发范例的提案时,就必须从复杂性的角度对其进行挑战:该提案确实有助于最大程度地降低大型软件系统的复杂性吗?许多建议表面上听起来不错,但是如果您深入研究,您会发现其中一些会使复杂性恶化,而不是更好。

',69)]))}const h=t(n,[["render",i],["__file","ch19.html.vue"]]),c=JSON.parse('{"path":"/ch19.html","title":"第 19 章 软件发展趋势","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"19.1 Object-oriented programming and inheritance 面向对象的编程和继承","slug":"_19-1-object-oriented-programming-and-inheritance-面向对象的编程和继承","link":"#_19-1-object-oriented-programming-and-inheritance-面向对象的编程和继承","children":[]},{"level":2,"title":"19.2 Agile development 敏捷开发","slug":"_19-2-agile-development-敏捷开发","link":"#_19-2-agile-development-敏捷开发","children":[]},{"level":2,"title":"19.3 Unit tests 单元测试","slug":"_19-3-unit-tests-单元测试","link":"#_19-3-unit-tests-单元测试","children":[]},{"level":2,"title":"19.4 Test-driven development 测试驱动的开发","slug":"_19-4-test-driven-development-测试驱动的开发","link":"#_19-4-test-driven-development-测试驱动的开发","children":[]},{"level":2,"title":"19.5 Design patterns 设计模式","slug":"_19-5-design-patterns-设计模式","link":"#_19-5-design-patterns-设计模式","children":[]},{"level":2,"title":"19.6 Getters and setters Getter 和 Setters","slug":"_19-6-getters-and-setters-getter-和-setters","link":"#_19-6-getters-and-setters-getter-和-setters","children":[]},{"level":2,"title":"19.7 Conclusion 结论","slug":"_19-7-conclusion-结论","link":"#_19-7-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch19.md"}');export{h as comp,c as data}; +import{_ as t,c as s,f as a,o}from"./app-BQdhfat9.js";const n={};function i(r,e){return o(),s("div",null,e[0]||(e[0]=[a('

第 19 章 软件发展趋势

Chapter 19 Software Trends

As a way of illustrating the principles discussed in this book, this chapter considers several trends and patterns that have become popular in software development over the last few decades. For each trend, I will describe how that trend relates to the principles in this book and use the principles to evaluate whether that trend provides leverage against software complexity.

为了说明本书中讨论的原理,本章考虑了过去几十年来在软件开发中流行的几种趋势和模式。对于每种趋势,我将描述该趋势与本书中的原理之间的关系,并使用这些原理来评估该趋势是否提供了针对软件复杂性的杠杆作用。

19.1 Object-oriented programming and inheritance 面向对象的编程和继承

Object-oriented programming is one of the most important new ideas in software development over the last 30–40 years. It introduced notions such as classes, inheritance, private methods, and instance variables. If used carefully, these mechanisms can help to produce better software designs. For example, private methods and variables can be used to ensure information hiding: no code outside a class can invoke private methods or access private variables, so there can’t be any external dependencies on them.

在过去的 30-40 年中,面向对象编程是软件开发中最重要的新思想之一。它引入了诸如类,继承,私有方法和实例变量之类的概念。如果谨慎使用,这些机制可以帮助产生更好的软件设计。例如,私有方法和变量可用于确保信息隐藏:类外部的任何代码都不能调用私有方法或访问私有变量,所以没有任何外部依赖。

One of the key elements of object-oriented programming is inheritance. Inheritance comes in two forms, which have different implications for software complexity. The first form of inheritance is interface inheritance, in which a parent class defines the signatures for one or more methods, but does not implement the methods. Each subclass must implement the signatures, but different subclasses can implement the same methods in different ways. For example, the interface might define methods for performing I/O; one subclass might implement the I/O operations for disk files, and another subclass might implement the same operations for network sockets.

面向对象编程的关键要素之一是继承。继承有两种形式,它们对软件复杂性有不同的影响。继承的第一种形式是接口继承,其中父类定义一个或多个方法的签名,但不实现这些方法。每个子类都必须实现签名,但是不同的子类可以以不同的方式实现相同的方法。例如,该接口可能定义用于执行 I/O 的方法。一个子类可能对磁盘文件实现 I/O 操作,而另一个子类可能对网络套接字实现相同的操作。

Interface inheritance provides leverage against complexity by reusing the same interface for multiple purposes. It allows knowledge acquired in solving one problem (such as how to use an I/O interface to read and write disk files) to be used to solve other problems (such as communicating over a network socket). Another way of thinking about this is in terms of depth: the more different implementations there are of an interface, the deeper the interface becomes. In order for an interface to have many implementations, it must capture the essential features of all the underlying implementations while steering clear of the details that differ between the implementations; this notion is at the heart of abstraction.

接口继承通过将同一接口用于多种用途,从而提供了对抗复杂性的杠杆作用。它使解决一个问题(例如如何使用 I/O 接口读取和写入磁盘文件)中获得的知识可以用于解决其他问题(例如通过网络套接字进行通信)。关于深度的另一种思考方式是:接口的不同实现越多,接口的深度就越大。为了让一个接口有很多实现,它必须拥有所有底层实现的基本特征,同时避免不同实现之间的差异。这个概念是抽象的核心所在。

The second form of inheritance is implementation inheritance. In this form, a parent class defines not only signatures for one or more methods, but also default implementations. Subclasses can choose to inherit the parent’s implementation of a method or override it by defining a new method with the same signature. Without implementation inheritance, the same method implementation might need to be duplicated in several subclasses, which would create dependencies between those subclasses (modifications would need to be duplicated in all copies of the method). Thus, implementation inheritance reduces the amount of code that needs to be modified as the system evolves; in other words, it reduces the change amplification problem described in Chapter 2.

继承的第二种形式是实现继承。以这种形式,父类不仅定义了一个或多个方法的签名,而且还定义了默认实现。子类可以选择继承方法的父类实现,也可以通过定义具有相同签名的新方法来覆盖它。如果没有实现继承,则可能需要在几个子类中复制相同的方法实现,这将在这些子类之间创建依赖关系(需要在方法的所有副本中复制修改)。因此,实现继承减少了随着系统的发展而需要修改的代码量。换句话说,它减少了第 2 章中描述的变化放大问题。

However, implementation inheritance creates dependencies between the parent class and each of its subclasses. Class instance variables in the parent class are often accessed by both the parent and child classes; this results in information leakage between the classes in the inheritance hierarchy and makes it hard to modify one class in the hierarchy without looking at the others. For example, a developer making changes to the parent class may need to examine all of the subclasses to ensure that the changes don’t break anything. Similarly, if a subclass overrides a method in the parent class, the developer of the subclass may need to examine the implementation in the parent. In the worst case, programmers will need complete knowledge of the entire class hierarchy underneath the parent class in order to make changes to any of the classes. Class hierarchies that use implementation inheritance extensively tend to have high complexity.

但是,实现继承会在父类及其每个子类之间创建依赖关系。父类中的类实例变量经常被父类和子类访问。这导致了继承层次中的类之间的信息泄漏,并且使修改层次中的一个类时很难不考虑其他类。例如,对父类进行修改的开发人员可能需要检查所有子类,以确保所做的修改不会破坏任何内容。同样,如果子类覆盖了父类中的方法,则子类的开发人员可能需要检查父类中的实现。在最坏的情况下,程序员将需要完全了解父类下的整个类层次结构,以便对任何类进行更改。广泛使用实现继承的类层次结构往往具有很高的复杂性。

Thus, implementation inheritance should be used with caution. Before using implementation inheritance, consider whether an approach based on composition can provide the same benefits. For instance, it may be possible to use small helper classes to implement the shared functionality. Rather than inheriting functions from a parent, the original classes can each build upon the features of the helper classes.

因此,应谨慎使用实现继承。在使用实现继承之前,请考虑基于组合的方法是否可以提供相同的好处。例如,可以使用小型辅助类来实现共享功能。与其从父类中继承功能,原始类可以各自建立在辅助类的功能之上。

If there is no viable alternative to implementation inheritance, try to separate the state managed by the parent class from that managed by subclasses. One way to do this is for certain instance variables to be managed entirely by methods in the parent class, with subclasses using them only in a read-only fashion or through other methods in the parent class. This applies the notion of information hiding within the class hierarchy to reduce dependencies.

如果没有实现继承的可行的替代方案,请尝试将父类管理的状态与子类管理的状态分开。一种方法是让某些实例变量完全由父类中的方法管理,子类仅以只读方式或通过父类中的其他方法使用它们。这适用于隐藏在类层次结构中的信息的概念,以减少依赖性。

Although the mechanisms provided by object-oriented programming can assist in implementing clean designs, they do not, by themselves, guarantee good design. For example, if classes are shallow, or have complex interfaces, or permit external access to their internal state, then they will still result in high complexity.

尽管面向对象编程提供的机制可有助于实现干净的设计,但是它们本身不能保证良好的设计。例如,如果类很浅,或者具有复杂的接口,或者允许外部访问其内部状态,那么它们仍将导致很高的复杂性。

19.2 Agile development 敏捷开发

Agile development is an approach to software development that emerged in the late 1990’s from a collection of ideas about how to make software development more lightweight, flexible, and incremental; it was formally defined during a meeting of practitioners in 2001. Agile development is mostly about the process of software development (organizing teams, managing schedules, the role of unit testing, interacting with customers, etc.) as opposed to software design. Nonetheless, it relates to some of the design principles in this book.

敏捷开发是 20 世纪 90 年代末出现的一种软件开发方法,是关于如何使软件开发更加轻量,灵活和渐进的一系列想法。它是在 2001 年的一次从业者会议上正式定义的。敏捷开发主要是关于软件开发的过程(组织团队,管理进度表,单元测试的角色,与客户交互等),而不是软件设计。但是,它与本书中的一些设计原则有关。

One of the most important elements of agile development is the notion that development should be incremental and iterative. In the agile approach, a software system is developed in a series of iterations, each of which adds and evaluates a few new features; each iteration includes design, test, and customer input. In general, this is similar to the incremental approach advocated here. As mentioned in Chapter 1, it isn’t possible to visualize a complex system well enough at the outset of a project to determine the best design. The best way to end up with a good design is to develop a system in increments, where each increment adds a few new abstractions and refactors existing abstractions based on experience. This is similar to the agile development approach.

敏捷开发中最重要的元素之一是开发应该是渐进的和迭代的概念。在敏捷方法中,软件系统是通过一系列迭代开发的,每个迭代都添加并评估了一些新功能。每个迭代都包括设计,测试和客户的意见。通常,这类似于这里提倡的增量方法。如第 1 章所述,在项目开始时就不可能对复杂的系统进行充分的可视化以决定最佳设计。最终获得良好设计的最佳方法是逐步开发一个系统,其中每个增量都会添加一些新的抽象,并根据经验重构现有的抽象。这类似于敏捷开发方法。

One of the risks of agile development is that it can lead to tactical programming. Agile development tends to focus developers on features, not abstractions, and it encourages developers to put off design decisions in order to produce working software as soon as possible. For example, some agile practitioners argue that you shouldn’t implement general-purpose mechanisms right away; implement a minimal special-purpose mechanism to start with, and refactor into something more generic later, once you know that it’s needed. Although these arguments make sense to a degree, they argue against an investment approach, and they encourage a more tactical style of programming. This can result in a rapid accumulation of complexity.

敏捷开发的风险之一是它可能导致战术编程。敏捷开发倾向于将开发人员的注意力集中在功能上,而不是抽象上,它鼓励开发人员推迟设计决策,以便尽快生产可以使用的软件。例如,一些敏捷的实践者认为,您不应该马上实现通用机制;应该先实现一个最小的特殊用途机制,然后在知道需要它时重构为更通用的东西。尽管这些论点在一定程度上是合理的,但它们反对投资方法,并鼓励采用更具战术性的编程风格。这会导致复杂性的快速累积。

Developing incrementally is generally a good idea, but the increments of development should be abstractions, not features. It’s fine to put off all thoughts about a particular abstraction until it’s needed by a feature. Once you need the abstraction, invest the time to design it cleanly; follow the advice of Chapter 6 and make it somewhat general-purpose.

渐进式开发通常是一个好主意,但是软件开发的增量应该是抽象而不是功能。可以推迟对特定抽象的所有想法,直到功能需要它为止。一旦需要抽象,就要花一些时间进行简洁的设计。遵循第 6 章的建议并使其具有通用性。

19.3 Unit tests 单元测试

It used to be that developers rarely wrote tests. If tests were written at all, they were written by a separate QA team. However, one of the tenets of agile development is that testing should be tightly integrated with development, and programmers should write tests for their own code. This practice has now become widespread. Tests are typically divided into two kinds: unit tests and system tests. Unit tests are the ones most often written by developers. They are small and focused: each test usually validates a small section of code in a single method. Unit tests can be run in isolation, without setting up a production environment for the system. Unit tests are often run in conjunction with a test coverage tool to ensure that every line of code in the application is tested. Whenever developers write new code or modify existing code, they are responsible for updating the unit tests to maintain proper test coverage.

过去,开发人员很少编写测试。如果测试是由一个独立的 QA 团队编写的,那么它们就是由一个独立的 QA 团队编写的。然而,敏捷开发的原则之一是测试应该与开发紧密集成,程序员应该为他们自己的代码编写测试。这种做法现在已经很普遍了。测试通常分为两类:单元测试和系统测试。单元测试是开发人员最常编写的测试。它们很小,而且重点突出:每个测试通常在单个方法中验证一小段代码。单元测试可以独立运行,而不需要为系统设置生产环境。单元测试通常与测试覆盖工具一起运行,以确保应用程序中的每一行代码都经过了测试。每当开发人员编写新代码或修改现有代码时,他们都要负责更新单元测试以保持适当的测试覆盖率。

The second kind of test consists of system tests (sometimes called integration tests), which ensure that the different parts of an application all work together properly. They typically involve running the entire application in a production environment. System tests are more likely to be written by a separate QA or testing team.

第二种测试包括系统测试(有时称为集成测试),这些测试可确保应用程序的不同部分都能正常协同工作。它们通常涉及在生产环境中运行整个应用程序。系统测试更有可能由独立的质量检查或测试小组编写。

Tests, particularly unit tests, play an important role in software design because they facilitate refactoring. Without a test suite, it’s dangerous to make major structural changes to a system. There’s no easy way to find bugs, so it’s likely that bugs will go undetected until the new code is deployed, where they are much more expensive to find and fix. As a result, developers avoid refactoring in systems without good test suites; they try to minimize the number of code changes for each new feature or bug fix, which means that complexity accumulates and design mistakes don’t get corrected.

测试,尤其是单元测试,在软件设计中起着重要作用,因为它们有助于重构。没有测试套件,对系统进行重大结构更改很危险。没有简单的方法可以找到错误,因此在部署新代码之前,很可能将无法检测到错误,因为在新代码中查找和修复它们的成本要高得多。结果,开发人员避免在没有良好测试套件的系统中进行重构。他们尝试将每个新功能或错误修复的代码更改次数降至最低,这意味着复杂性会累积,而设计错误不会得到纠正。

With a good set of tests, developers can be more confident when refactoring because the test suite will find most bugs that are introduced. This encourages developers to make structural improvements to a system, which results in a better design. Unit tests are particularly valuable: they provide a higher degree of code coverage than system tests, so they are more likely to uncover any bugs.

有了一套很好的测试,开发人员可以在重构时更有信心,因为测试套件将发现大多数引入的 bug。这鼓励开发人员对系统进行结构改进,从而获得更好的设计。单元测试特别有价值:与系统测试相比,它们提供更高的代码覆盖率,因此它们更有可能发现任何 bug。

For example, during the development of the Tcl scripting language, we decided to improve performance by replacing Tcl’s interpreter with a byte-code compiler. This was a huge change that affected almost every part of the core Tcl engine. Fortunately, Tcl had an excellent unit test suite, which we ran on the new byte-code engine. The existing tests were so effective in uncovering bugs in the new engine that only a single bug turned up after the alpha release of the byte-code compiler.

例如,在开发 Tcl 脚本语言期间,我们决定通过用字节码编译器替换 Tcl 的解释器来提高性能。这是一个巨大的变化,几乎影响了核心 Tcl 引擎的每个部分。幸运的是,Tcl 有一个出色的单元测试套件,我们在新的字节码引擎上运行了该套件。现有测试在发现新引擎中的错误方面是如此有效,以至于在字节码编译器的 alpha 版本发布之后仅出现了一个错误。

19.4 Test-driven development 测试驱动的开发

Test-driven development is an approach to software development where programmers write unit tests before they write code. When creating a new class, the developer first writes unit tests for the class, based on its expected behavior. None of the tests pass, since there is no code for the class. Then the developer works through the tests one at a time, writing enough code for that test to pass. When all of the tests pass, the class is finished.

测试驱动开发是一种软件开发方法,程序员可以在编写代码之前先编写单元测试。创建新类时,开发人员首先根据其预期行为为该类编写单元测试。没有一个测试通过,因为该类没有代码。然后,开发人员一次完成一个测试,编写足够的代码以使该测试通过。所有测试通过后,这个类的功能就完成了。

Although I am a strong advocate of unit testing, I am not a fan of test-driven development. The problem with test-driven development is that it focuses attention on getting specific features working, rather than finding the best design. This is tactical programming pure and simple, with all of its disadvantages. Test-driven development is too incremental: at any point in time, it’s tempting to just hack in the next feature to make the next test pass. There’s no obvious time to do design, so it’s easy to end up with a mess.

尽管我是单元测试的坚决拥护者,但我并不热衷测试驱动的开发。测试驱动开发的问题在于,它将注意力集中在使特定功能正常工作上,而不是寻找最佳设计。这是纯粹的战术性编程,有其所有的弊端。测试驱动的开发过于增量:在任何时间点,很容易完成一个功能然后让测试通过。没有明显的时间来做设计,因此很容易搞的一团糟。

As mentioned in Section 19.2, the units of development should be abstractions, not features. Once you discover the need for an abstraction, don’t create the abstraction in pieces over time; design it all at once (or at least enough to provide a reasonably comprehensive set of core functions). This is more likely to produce a clean design whose pieces fit together well.

如第 19.2 节所述,开发单位应该是抽象的,而不是功能。一旦发现了对抽象的需求,就不要在一段时间内零散的创建抽象,而应该一次性的设计(或至少足以提供一组合理全面的核心功能)。这样更有可能产生干净的设计,使各个部分很好地契合在一起。

One place where it makes sense to write the tests first is when fixing bugs. Before fixing a bug, write a unit test that fails because of the bug. Then fix the bug and make sure that the unit test now passes. This is the best way to make sure you really have fixed the bug. If you fix the bug before writing the test, it’s possible that the new unit test doesn’t actually trigger the bug, in which case it won’t tell you whether you really fixed the problem.

有一个地方先编写测试是有意义的,那就是修复 bug 的时候。在修复错误之前,请编写由于该错误而失败的单元测试。然后修复该错误,并确保现在可以通过单元测试。这是确保您已真正修复该错误的最佳方法。如果您在编写测试之前就已修复了该错误,则新的单元测试很可能实际上不会触发该错误,在这种情况下,它不会告诉您是否真的修复了该问题。

19.5 Design patterns 设计模式

A design pattern is a commonly used approach for solving a particular kind of problem, such as an iterator or an observer. The notion of design patterns was popularized by the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, and design patterns are now widely used in object-oriented software development.

设计模式是解决特定类型问题(例如迭代器或观察器)的常用方法。设计模式的概念在 Gamma,Helm,Johnson 和 Vlissides 的《设计模式:可复用的面向对象软件的基础》一书中而普及,现在设计模式已广泛用于面向对象的软件开发中。

Design patterns represent an alternative to design: rather than designing a new mechanism from scratch, just apply a well-known design pattern. For the most part, this is good: design patterns arose because they solve common problems, and because they are generally agreed to provide clean solutions. If a design pattern works well in a particular situation, it will probably be hard for you to come up with a different approach that is better.

设计模式代表了设计的另一种选择:与其从头设计新机制,不如应用一种众所周知的设计模式。在大多数情况下,这是很好的:设计模式的出现是因为它们解决了常见的问题,并且因为它们被普遍认为提供干净的解决方案。如果设计模式在特定情况下运作良好,那么您可能很难想出另一种更好的方法。

The greatest risk with design patterns is over-application. Not every problem can be solved cleanly with an existing design pattern; don’t try to force a problem into a design pattern when a custom approach will be cleaner. Using design patterns doesn’t automatically improve a software system; it only does so if the design patterns fit. As with many ideas in software design, the notion that design patterns are good doesn’t necessarily mean that more design patterns are better.

设计模式的最大风险是过度使用。不是每个问题都可以用现有的设计模式来解决。当自定义方法更加简洁时,请勿尝试将问题强加到设计模式中。使用设计模式并不能自动改善软件系统。只有在设计模式合适的情况下才会如此。与软件设计中的许多想法一样,设计模式良好的概念并不一定意味着更多的设计模式会更好。

19.6 Getters and setters Getter 和 Setters

In the Java programming community, getter and setter methods are a popular design pattern. A getter and a setter are associated with an instance variable for a class. They have names like getFoo and setFoo, where Foo is the name of the variable. The getter method returns the current value of the variable, and the setter method modifies the value.

在 Java 编程社区中,getter 和 setter 方法是一种流行的设计模式。一个 getter 和一个 setter 与一个类的实例变量相关联。它们具有类似 getFoo 和 setFoo 的名称,其中 Foo 是变量的名称。getter 方法返回变量的当前值,setter 方法修改该值。

Getters and setters aren’t strictly necessary, since instance variables can be made public. The argument for getters and setters is that they allow additional functions to be performed while getting and setting, such as updating related values when a variable changes, notifying listeners of changes, or enforcing constraints on values. Even if these features aren’t needed initially, they can be added later without changing the interface.

由于实例变量可以公开,因此不一定必须使用 getter 和 setter 方法。getter 和 setter 的论点是,它们允许在获取和设置时执行额外功能,例如在变量更改时更新相关值,通知监听器的变化或对值实施约束。即使最初不需要这些功能,以后也可以在不更改界面的情况下添加它们。

Although it may make sense to use getters and setters if you must expose instance variables, it’s better not to expose instance variables in the first place. Exposed instance variables mean that part of the class’s implementation is visible externally, which violates the idea of information hiding and increases the complexity of the class’s interface. Getters and setters are shallow methods (typically only a single line), so they add clutter to the class’s interface without providing much functionality. It’s better to avoid getters and setters (or any exposure of implementation data) as much as possible.

如果必须公开实例变量,则可以使用 getter 和 setter 方法,但最好不要首先公开实例变量。暴露的实例变量意味着类的实现的一部分在外部是可见的,这违反了信息隐藏的思想,并增加了类接口的复杂性。Getter 和 Setter 是浅层方法(通常只有一行),因此它们在不提供太多功能的情况下使类的接口变得混乱。最好避免使用 getter 和 setter(或任何公开的实现数据)。

One of the risks of establishing a design pattern is that developers assume the pattern is good and try to use it as much as possible. This has led to overusage of getters and setters in Java.

建立设计模式的风险之一是开发人员认为该模式是好的,并试图尽可能多地使用它。这导致 Java 中的 getter 和 setter 的过度使用。

19.7 Conclusion 结论

Whenever you encounter a proposal for a new software development paradigm, challenge it from the standpoint of complexity: does the proposal really help to minimize complexity in large software systems? Many proposals sound good on the surface, but if you look more deeply you will see that some of them make complexity worse, not better.

每当您遇到有关新软件开发范例的提案时,就必须从复杂性的角度对其进行挑战:该提案确实有助于最大程度地降低大型软件系统的复杂性吗?许多建议表面上听起来不错,但是如果您深入研究,您会发现其中一些会使复杂性恶化,而不是更好。

',69)]))}const h=t(n,[["render",i],["__file","ch19.html.vue"]]),c=JSON.parse('{"path":"/ch19.html","title":"第 19 章 软件发展趋势","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"19.1 Object-oriented programming and inheritance 面向对象的编程和继承","slug":"_19-1-object-oriented-programming-and-inheritance-面向对象的编程和继承","link":"#_19-1-object-oriented-programming-and-inheritance-面向对象的编程和继承","children":[]},{"level":2,"title":"19.2 Agile development 敏捷开发","slug":"_19-2-agile-development-敏捷开发","link":"#_19-2-agile-development-敏捷开发","children":[]},{"level":2,"title":"19.3 Unit tests 单元测试","slug":"_19-3-unit-tests-单元测试","link":"#_19-3-unit-tests-单元测试","children":[]},{"level":2,"title":"19.4 Test-driven development 测试驱动的开发","slug":"_19-4-test-driven-development-测试驱动的开发","link":"#_19-4-test-driven-development-测试驱动的开发","children":[]},{"level":2,"title":"19.5 Design patterns 设计模式","slug":"_19-5-design-patterns-设计模式","link":"#_19-5-design-patterns-设计模式","children":[]},{"level":2,"title":"19.6 Getters and setters Getter 和 Setters","slug":"_19-6-getters-and-setters-getter-和-setters","link":"#_19-6-getters-and-setters-getter-和-setters","children":[]},{"level":2,"title":"19.7 Conclusion 结论","slug":"_19-7-conclusion-结论","link":"#_19-7-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch19.md"}');export{h as comp,c as data}; diff --git a/assets/ch20.html-DDvZmVuW.js b/assets/ch20.html-Do36buxh.js similarity index 99% rename from assets/ch20.html-DDvZmVuW.js rename to assets/ch20.html-Do36buxh.js index 10c45190..523c3102 100644 --- a/assets/ch20.html-DDvZmVuW.js +++ b/assets/ch20.html-Do36buxh.js @@ -1 +1 @@ -import{_ as t,c as a,f as o,o as s}from"./app-CvqtBB8Z.js";const i="/A-Philosophy-of-Software-Design-zh/assets/00022-CAKLnDUD.jpeg",n="/A-Philosophy-of-Software-Design-zh/assets/00023-CcNUy9iq.gif",r="/A-Philosophy-of-Software-Design-zh/assets/00024-DLcgw9w_.gif",l={};function c(h,e){return s(),a("div",null,e[0]||(e[0]=[o('

第 20 章 设计性能

Chapter 20 Designing for Performance

Up until this point, the discussion of software design has focused on complexity; the goal has been to make software as simple and understandable as possible. But what if you are working on a system that needs to be fast? How should performance considerations affect the design process? This chapter discusses how to achieve high performance without sacrificing clean design. The most important idea is still simplicity: not only does simplicity improve a system’s design, but it usually makes systems faster.

到目前为止,关于软件设计的讨论都集中在复杂性上。目标是使软件尽可能简单易懂。但是,如果您需要让一个系统运行的更加高效,该怎么办?性能方面的考虑应如何影响设计过程?本章讨论如何在不牺牲简洁设计的情况下实现高性能。最重要的想法仍然是简单性:简单性不仅可以改善系统的设计,而且通常可以使系统更快。

20.1 How to think about performance 如何考虑性能

The first question to address is “how much should you worry about performance during the normal development process?” If you try to optimize every statement for maximum speed, it will slow down development and create a lot of unnecessary complexity. Furthermore, many of the “optimizations” won’t actually help performance. On the other hand, if you completely ignore performance issues, it’s easy to end up with a large number of significant inefficiencies spread throughout the code; the resulting system can easily be 5–10x slower than it needs to be. In this “death by a thousand cuts” scenario it’s hard to come back later and improve the performance, because there is no single improvement that will have much impact.

要解决的第一个问题是“在正常的开发过程中,您应该在多大程序上担心性能?” 如果您尝试优化每条语句以获得最大速度,则它将减慢开发速度并产生很多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果您完全忽略了性能问题,则很容易导致遍及整个代码中出现大量严重的低效问题。结果系统很容易比所需的速度慢 5–10 倍。在这种“千刀万剐”的情况下,以后很难再回来改进性能了,因为没有单一的改进会产生很大的影响。

The best approach is something between these extremes, where you use basic knowledge of performance to choose design alternatives that are “naturally efficient” yet also clean and simple. The key is to develop an awareness of which operations are fundamentally expensive. Here are a few examples of operations that are relatively expensive today:

最好的方法是介于这两种极端之间,在这种极端情况下,您可以利用性能的基本知识来选择“自然高效”但又干净简单的设计方案。关键是要意识到哪些操作从根本上来说是昂贵的。以下是一些今天相对昂贵的操作示例:

  • Network communication: even within a datacenter, a round-trip message exchange can take 10–50 µs, which is tens of thousands of instruction times. Wide-area round-trips can take 10–100 ms.
  • I/O to secondary storage: disk I/O operations typically take 5–10 ms, which is millions of instruction times. Flash storage takes 10–100 µs. New emerging nonvolatile memories may be as fast as 1 µs, but this is still around 2000 instruction times.
  • Dynamic memory allocation (malloc in C, new in C++ or Java) typically involves significant overhead for allocation, freeing, and garbage collection.
  • Cache misses: fetching data from DRAM into an on-chip processor cache takes a few hundred instruction times; in many programs, overall performance is determined as much by cache misses as by computational costs.

  • 网络通信:即使在数据中心内,往返消息交换也可能花费 10–50 µs,这是数以万计的指令时间。广域网往返可能需要 10 到 100 毫秒。
  • 二级存储的 I/O:磁盘 I/O 操作通常需要 5 到 10 毫秒,这是数百万条指令时间。闪存存储需要 10–100 µs。新出现的非易失性存储器的速度可能高达 1 µs,但这仍约为 2000 条指令时间。
  • 动态内存分配(C 语言中的 malloc, C++ 或 Java 中的新增功能)通常涉及分配,释放和垃圾回收的大量开销。
  • 缓存缺失:将数据从 DRAM 提取到片上处理器高速缓存中需要数百条指令时间;在许多程序中,整体性能取决于缓存未命中和计算成本。

The best way to learn which things are expensive is to run micro-benchmarks (small programs that measure the cost of a single operation in isolation). In the RAMCloud project, we created a simple program that provides a framework for microbenchmarks. It took a few days to create the framework, but the framework makes it possible to add new micro-benchmarks in five or ten minutes. This has allowed us to accumulate dozens of micro-benchmarks. We use these both to understand the performance of existing libraries used in RAMCloud, and also to measure the performance of new classes written for RAMCloud.

了解哪些东西最昂贵的最好方法是运行微基准测试(单独衡量单个操作成本的小程序)。在 RAMCloud 项目中,我们创建了一个提供微基准测试的框架简单的程序。创建该框架花了几天时间,但是该框架使在五到十分钟内添加新的微基准成为可能。这使我们积累了几十个微基准。我们既可以使用它们来了解 RAMCloud 中使用的现有库的性能,也可以衡量为 RAMCloud 编写的新类的性能。

Once you have a general sense for what is expensive and what is cheap, you can use that information to choose cheap operations whenever possible. In many cases, a more efficient approach will be just as simple as a slower approach. For example, when storing a large collection of objects that will be looked up using a key value, you could use either a hash table or an ordered map. Both are commonly available in library packages, and both are simple and clean to use. However, hash tables can easily be 5–10x faster. Thus, you should always use a hash table unless you need the ordering properties provided by the map.

一旦您对什么是昂贵和什么便宜有了大致的了解,就可以使用该信息尽可能地选择便宜的操作。在许多情况下,更有效的方法将与较慢的方法一样简单。例如,当存储将使用键值查找的大量对象时,可以使用哈希表或有序映射。两者都通常在库包中提供,并且都简单易用。但是,哈希表可以轻松地快 5-10 倍。因此,除非需要映射提供的排序属性,否则您应始终使用哈希表。

As another example, consider allocating an array of structures in a language such as C or C++. There are two ways you can do this. One way is for the array to hold pointers to structures, in which case you must first allocate space for the array, then allocate space for each individual structure. It is much more efficient to store the structures in the array itself, so you only allocate one large block for everything.

作为另一个示例,请考虑使用诸如 C 或 C++ 之类的语言分配结构数组。有两种方法可以执行此操作。一种方法是让数组保留指向结构的指针,在这种情况下,您必须首先为数组分配空间,然后为每个单独的结构分配空间。将结构存储在数组本身中效率要高得多,因此您只为所有内容分配一大块内存。

If the only way to improve efficiency is by adding complexity, then the choice is more difficult. If the more efficient design adds only a small amount of complexity, and if the complexity is hidden, so it doesn’t affect any interfaces, then it may be worthwhile (but beware: complexity is incremental). If the faster design adds a lot of implementation complexity, or if it results in more complicated interfaces, then it may be better to start off with the simpler approach and optimize later if performance turns out to be a problem. However, if you have clear evidence that performance will be important in a particular situation, then you might as well implement the faster approach immediately.

如果提高效率的唯一方法是增加复杂性,那么选择就更困难了。如果更高效的设计仅增加了少量复杂性,并且复杂性是隐藏的,那么它不影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现复杂性,或者导致更复杂的接口,那么最好是从更简单的方法开始,然后在性能出现问题时进行优化。但是,如果您有明确的证据表明性能在特定情况下很重要,那么您不妨立即实施更高效的方法。

In the RAMCloud project one of our overall goals was to provide the lowest possible latency for client machines accessing the storage system over a datacenter network. As a result, we decided to use special hardware for networking, which allowed RAMCloud to bypass the kernel and communicate directly with the network interface controller to send and receive packets. We made this decision even though it added complexity, because we knew from prior measurements that kernel-based networking would be too slow to meet our needs. In most of the rest of the RAMCloud system we were able to design for simplicity; getting this one big issue “right” made many other things easier.

在 RAMCloud 项目中,我们的总体目标之一是为通过数据中心网络访问存储系统的客户端机器提供尽可能低的延迟。结果,我们决定使用特殊的硬件进行联网,从而使 RAMCloud 绕过内核并直接与网络接口控制器进行通信以发送和接收数据包。尽管增加了复杂性,但我们还是做出了这个决定,因为我们从先前的测量中知道,基于内核的网络太慢了,无法满足我们的需求。在其余的 RAMCloud 系统中,我们能够进行简单设计。把这个大问题“解决”会让其他事情变得更加容易。

In general, simpler code tends to run faster than complex code. If you have defined away special cases and exceptions, then no code is needed to check for those cases and the system runs faster. Deep classes are more efficient than shallow ones, because they get more work done for each method call. Shallow classes result in more layer crossings, and each layer crossing adds overhead.

通常,简单的代码往往比复杂的代码运行得更快。如果您已经定义好了特殊情况和异常情况,那么就不需要代码来检查这些情况,系统就会运行速度更快。深层类比浅层类更有效,因为它们为每个方法调用完成了更多工作。浅类会导致更多的层交叉,并且每个层交叉都会增加开销。

20.2 Measure before modifying 修改前的度量

But suppose that your system is still too slow, even though you have designed it as described above. It’s tempting to rush off and start making performance tweaks, based on your intuitions about what is slow. Don’t do this! Programmers’ intuitions about performance are unreliable. This is true even for experienced developers. If you start making changes based on intuition, you’ll waste time on things that don’t actually improve performance, and you’ll probably make the system more complicated in the process.

但是假设您的系统仍然太慢,即使您已经按照上面描述的方式设计了它。根据您对什么是缓慢的直觉,很容易匆忙进行性能调整。不要这样做!程序员对性能的直觉是不可靠的。即使对于有经验的开发人员也是如此。如果您开始根据直觉进行修改,你会把时间浪费在实际上无法提高性能的事情上,并且在这个过程中可能会使系统变得更加复杂。

Before making any changes, measure the system’s existing behavior. This serves two purposes. First, the measurements will identify the places where performance tuning will have the biggest impact. It isn’t sufficient just to measure the top-level system performance. This may tell you that the system is too slow, but it won’t tell you why. You’ll need to measure deeper to identify in detail the factors that contribute to overall performance; the goal is to identify a small number of very specific places where the system is currently spending a lot of time, and where you have ideas for improvement. The second purpose of the measurements is to provide a baseline, so that you can re-measure performance after making your changes to ensure that performance actually improved. If the changes didn’t make a measurable difference in performance, then back them out (unless they made the system simpler). There’s no point in retaining complexity unless it provides a significant speedup.

进行任何更改之前,请测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调整将产生最大影响的地方。仅仅测量顶级系统性能是不够的。这可能会告诉您系统速度太慢,但不会告诉您原因。您需要进行更深入的衡量,以详细确定影响整体绩效的因素;目标是确定系统当前花费大量时间的少量非常具体的地方,以及您有改进想法的地方。测量的第二个目的是提供基线,以便您可以在进行更改后重新测量性能,以确保性能确实得到改善。如果更改并未在性能上产生可衡量的差异,则将它们撤销(除非它们使系统更简单)。保留复杂性是没有意义的,除非它提供了显著的速度提升。

20.3 Design around the critical path 围绕关键路径进行设计

At this point, let’s assume that you have carefully analyzed performance and have identified a piece of code that is slow enough to affect the overall system performance. The best way to improve its performance is with a “fundamental” change, such as introducing a cache, or using a different algorithmic approach (balanced tree vs. list, for instance). Our decision to bypass the kernel for network communication in RAMCloud is an example of a fundamental fix. If you can identify a fundamental fix, then you can implement it using the design techniques discussed in previous chapters.

在这一点上,假设您已经仔细分析了性能并确定了一段速度缓慢到足以影响整个系统的性能的代码。改善其性能的最佳方法是进行“根本性”更改,例如引入缓存,或使用其他算法方法(例如,平衡树与列表)。我们决定绕过内核进行 RAMCloud 中的网络通信的决定是一个根本性修正的示例。如果您能确定一个根本性的修正,则可以使用前面各章中讨论的设计技术来实施它。

Unfortunately, situations will sometimes arise where there isn’t a fundamental fix. This brings us to the core issue for this chapter, which is how to redesign an existing piece of code so that it runs faster. This should be your last resort, and it shouldn’t happen often, but there are cases where it can make a big difference. The key idea is to design the code around the critical path.

不幸的是,有时会出现一些没有根本解决办法的情况。这就把我们带到本章的核心问题,即如何重新设计现有代码,使其运行更快。这应该是您的不得已的方法,并且不应该经常发生,但是在某些情况下它可能会带来很大的不同。关键思想是围绕关键路径设计代码。

Start off by asking yourself what is the smallest amount of code that must be executed to carry out the desired task in the common case. Disregard any existing code structure. Imagine instead that you are writing a new method that implements just the critical path, which is the minimum amount of code that must be executed in the the most common case. The current code is probably cluttered with special cases; ignore them in this exercise. The current code might pass through several method calls on the critical path; imagine instead that you could put all the relevant code in a single method. The current code may also use a variety of variables and data structures; consider only the data needed for the critical path, and assume whatever data structure is most convenient for the critical path. For example, it may make sense to combine multiple variables into a single value. Assume that you could completely redesign the system in order to minimize the code that must be executed for the critical path. Let’s call this code “the ideal.”

首先,问自己在通常情况下执行所需任务必须执行的最少代码量是多少。忽略任何现有的代码结构。想象一下您正在编写一个仅实现关键路径的新方法,这是在最常见的情况下必须执行的最少代码量。当前的代码可能充满特殊情况。在此练习中,请忽略它们。当前的代码可能会在关键路径上通过多个方法调用。想象一下,您可以将所有相关代码放在一个方法中。当前代码还可以使用各种变量和数据结构。仅考虑关键路径所需的数据,并假定最适合关键路径的任何数据结构。例如,将多个变量合并为一个值可能很有意义。假设您可以完全重新设计系统,以最大程度地减少必须为关键路径执行的代码。我们把这段代码称为“理想”。

The ideal code probably clashes with your existing class structure, and it may not be practical, but it provides a good target: this represents the simplest and fastest that the code can ever be. The next step is to look for a new design that comes as close as possible to the ideal while still having a clean structure. You can apply all of the design ideas from previous chapters of this book, but with the additional constraint of keeping the ideal code (mostly) intact. You may have to add a bit of extra code to the ideal in order to allow clean abstractions; for example, if the code involves a hash table lookup, it’s OK to introduce an extra method call to a general-purpose hash table class. In my experience it’s almost always possible to find a design that is clean and simple, yet comes very close to the ideal.

理想的代码可能会与您现有的类结构冲突,并且可能不切实际,但它提供了一个很好的目标:这代表了代码可能是最简单和最快的。下一步是寻找一种新设计,使其尽可能接近理想状态,同时又要保持干净的结构。您可以应用本书前面各章中的所有设计思想,但要保持(大部分)理想代码的完整性。您可能需要在理想情况下添加一些额外的代码,以便实现干净的抽象。例如,如果代码涉及哈希表查找,引入一个额外的方法调用到一个通用哈希表类是可以的。根据我的经验,几乎总是能找到一种简洁明了但是但非常接近理想的设计。

One of the most important things that happens in this process is to remove special cases from the critical path. When code is slow, it’s often because it must handle a variety of situations, and the code gets structured to simplify the handling of all the different cases. Each special case adds a little bit of code to the critical path, in the form of extra conditional statements and/or method calls. Each of these additions makes the code a bit slower. When redesigning for performance, try to minimize the number of special cases you must check. Ideally, there will be a single if statement at the beginning, which detects all special cases with one test. In the normal case, only this one test will need to be made, after which the the critical path can be executed with no additional tests for special cases. If the initial test fails (which means a special case has occurred) the code can branch to a separate place off the critical path to handle it. Performance isn’t as important for special cases, so you can structure the special-case code for simplicity rather than performance.

在此过程中发生的最重要的事情之一是从关键路径中除去特殊情况。当代码运行缓慢时,通常是因为它必须处理各种情况,并且代码经过结构化以简化所有不同情况的处理。每个特殊情况都以额外的条件语句和/或方法调用的形式向关键路径添加了一些代码。这些添加中的每一个都会使代码变慢。重新设计性能时,请尝试减少必须检查的特殊情况的数量。理想情况下,开头应该有一个 if 语句,该语句可以通过一个测试检测所有特殊情况。在正常情况下,只需要进行一项测试,之后就可以执行关键路径,而对于特殊情况则无需进行其他测试。如果初始测试失败(这意味着发生了特殊情况),则代码可以分支到关键路径之外的单独位置以进行处理。对于特殊情况,性能并不是那么重要,因此您可以将特殊情况下的代码结构化,使之简单化而不是性能化。

20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区

Let’s consider an example, in which the Buffer class of the RAMCloud storage system was optimized to achieve a speedup of about 2x for the most common operations.

让我们考虑一个例子,在这个例子中,对 RAMCloud 存储系统的 Buffer 类进行了优化,在最常见的操作中实现了约 2 倍的速度提升。

RAMCloud uses Buffer objects to manage variable-length arrays of memory, such as request and response messages for remote procedure calls. Buffers are designed to reduce overheads from memory copying and dynamic storage allocation. A Buffer stores what appears to be a linear array of bytes, but for efficiency it allows the underlying storage to be divided into multiple discontiguous chunks of memory, as shown in Figure 20.1. A Buffer is created by appending chunks of data. Each chunk is either external or internal. If a chunk is external, its storage is owned by the caller; the Buffer keeps a reference to this storage. External chunks are typically used for large chunks in order to avoid memory copies. If a chunk is internal, the Buffer owns the storage for the chunk; data supplied by the caller is copied into the Buffer’s internal storage. Each Buffer contains a small built-in allocation, which is a block of memory available for storing internal chunks. If this space is exhausted, then the Buffer creates additional allocations, which must be freed when the Buffer is destroyed. Internal chunks are convenient for small chunks where the memory copying costs are negligible. Figure 20.1 shows a Buffer with 5 chunks: the first chunk is internal, the next two are external, and the final two chunks are internal.

RAMCloud 使用 Buffer 对象管理可变长度的内存数组,例如远程过程调用的请求和响应消息。缓冲区旨在减少内存复制和动态存储分配的开销。缓冲区存储的似乎是一个线性的字节数组,但为了提高效率,它允许将底层存储划分为多个不连续的内存块,如图 20.1 所示。缓冲区是通过附加数据块创建的。每个块要么是外部的,要么是内部的。如果块是外部的,则其存储空间由调用方拥有;Buffer 保留对此存储的引用。外部块通常用于大型块,以避免内存复制。如果块是内部的,则 Buffer 拥有该块的存储;调用者提供的数据将被复制到缓冲区的内部存储器中。每个 Buffer 包含一个小的内置分配,这是一个可用于存储内部块的内存块。如果此空间已用完,则 Buffer 需要额外分配内存,这些分配的内存必须在 Buffer 销毁时进行释放。内部块对于小块来说是很方便的,因为内存复制的成本可以忽略不计。图 20.1 显示了具有 5 个块的 Buffer:第一个块是内部的,接下来的两个块是外部的,最后两个块是内部的。

Figure 20.1: A Buffer object uses a collection of memory chunks to store what appears to be a linear array of bytes. Internal chunks are owned by the Buffer and freed when the Buffer is destroyed; external chunks are not owned by the Buffer.

图 20.1:Buffer 对象使用内存块的集合来存储看似线性字节数组。内部块为 Buffer 拥有,并在 Buffer 销毁时释放;外部块不为 Buffer 所有。

The Buffer class itself represents a “fundamental fix,” in that it eliminates expensive memory copies that would have been required without it. For example, when assembling a response message containing a short header and the contents of a large object in the RAMCloud storage system, RAMCloud uses a Buffer with two chunks. The first chunk is an internal one that contains the header; the second chunk is an external one that refers to the object contents in the RAMCloud storage system. The response can be collected in the Buffer without copying the large object.

Buffer 类本身代表“根本性的修补程序”,因为它消除了昂贵的内存拷贝,而如果没有它的话,就需要进行拷贝。例如,在 RAMCloud 存储系统中组装包含短标头和大对象内容的响应消息时,RAMCloud 使用带有两个块的 Buffer。第一个块是包含头的内部块;第二个块是一个外部块,它引用 RAMCloud 存储系统中的对象内容。可以在不复制大对象的情况下将响应收集到 Buffer 中。

Aside from the fundamental approach of allowing discontiguous chunks, we did not attempt to optimize the code of the Buffer class in the original implementation. Over time, however, we noticed Buffers being used in more and more situations; for example, at least four Buffers are created during the execution of each remote procedure call. Eventually, it became clear that speeding up the implementation of Buffer could have a noticeable impact on overall system performance. We decided to see if we could improve the performance of the Buffer class.

除了允许不连续块的基本方法外,在最初的实现中,我们并没有尝试优化 Buffer 类的代码。然而,随着时间的流逝,我们注意到 Buffer 越来越多地被使用。例如,在执行每个远程过程调用期间,至少会创建四个缓冲区。最终,我们发现,加速 Buffer 的实现可能会对整体系统性能产生显著影响。我们决定看看是否可以提高 Buffer 类的性能。

The most common operation for Buffer is to allocate space for a small amount of new data using an internal chunk. This happens, for example, when creating headers for request and response messages. We decided to use this operation as the critical path for optimization. In the simplest possible case, the space can be allocated by enlarging the last existing chunk in the Buffer. However, this is only possible if the last existing chunk is internal, and if there is enough space in its allocation to accommodate the new data. The ideal code would perform a single check to confirm that the simple approach is possible, then it would adjust the size of the existing chunk.

Buffer 最常见的操作是使用内部块为少量新数据分配空间。例如,在为请求和响应消息创建标头时,就会发生这种情况。我们决定使用将此操作作为优化的关键路径。在最简单的情况下,可以通过扩大 Buffer 中最后一个现有块来分配空间。但是,只有在最后一个现有块是内部块,并且在其分配中有足够的空间来容纳新数据时才有可能。理想的代码将执行一次检查,以确认简单方法是否可行的,然后将调整现有块的大小。

Figure 20.2 shows the original code for the critical path, which starts with the method Buffer::alloc. In the fastest possible case, Buffer::alloc calls Buffer:: allocateAppend, which calls Buffer::Allocation::allocateAppend. From a performance standpoint, this code has two problems. The first problem is that numerous special cases are checked individually:

图 20.2 显示了关键路径的原始代码,该代码以 Buffer::alloc 方法开头。在最快的情况下,Buffer::alloc 调用 Buffer::allocateAppend,后者调用 Buffer::Allocation::allocateAppend。从性能的角度来看,此代码有两个问题。第一个问题是要单独检查许多特殊情况:

  • Buffer::allocateAppend checks to see if the Buffer currently has any allocations.
  • The code checks twice to see if the current allocation has enough room for the new data: once in Buffer::Allocation::allocateAppend, and again when its return value is tested by Buffer::allocateAppend.
  • Buffer::alloc tests the return value from Buffer::allocAppend to confirm yet again that the allocation succeeded.

  • Buffer::allocateAppend 检查当前 Buffer 是否有足够的空间。
  • 代码检查两次以查看当前分配是否有足够的空间容纳新数据:一次在 Buffer::Allocation::allocateAppend 中,一次在其返回值由 Buffer::allocateAppend 测试时。
  • Buffer::alloc 测试 Buffer::allocAppend 的返回值,再次确认分配成功。

Furthermore, rather than trying to expand the last chunk directly, the code allocates new space without any consideration of the last chunk. Then Buffer::alloc checks to see if that space happens to be adjacent to the last chunk, in which case it merges the new space with the existing chunk. This results in additional checks. Overall, this code tests 6 distinct conditions in the critical path.

此外,该代码没有尝试直接扩展最后一个块,而是在不考虑最后一个块的情况下分配了新空间。然后,Buffer::alloc 检查该空间是否恰好与最后一块相邻,在这种情况下,它将新空间与现有块合并。这导致额外的检查。总体而言,此代码测试了关键路径中的 6 种不同条件。

The second problem with the original code is that it has too many layers, all of which are shallow. This is both a performance problem and a design problem. The critical path makes two additional method calls in addition to the original invocation of Buffer::alloc. Each method call takes additional time, and the result of each call must be checked by its caller, which results in more special cases to consider. Chapter 7 discussed how abstractions should normally change as you pass from one layer to another, but all three of the methods in Figure 20.2 have identical signatures and they provide essentially the same abstraction; this is a red flag. Buffer::allocateAppend is nearly a pass-though method; its only contribution is to create a new allocation if needed. The extra layers make the code both slower and more complicated.

原始代码的第二个问题是它的层数太多,而且都很浅。这既是性能问题,也是设计问题。除了对 Buffer::alloc 的原始调用之外,关键路径还进行了两个额外的方法调用。每个方法调用都需要额外的时间,并且每个调用的结果必须由其调用者检查,这导致需要考虑更多的特殊情况。第 7 章讨论了当您从一层传递到另一层时,抽象通常应该如何变化,但是图 20.2 中的所有三种方法都具有相同的签名,并且它们提供了基本相同的抽象。这是一个危险信号。Buffer::allocateAppend 几乎是一个传递方法;它的唯一作用是在需要时创建新的分配。额外的层使代码更慢,更复杂。

To fix these problems, we refactored the Buffer class so that its design is centered around the most performance-critical paths. We considered not just the allocation code above but several other commonly executed paths, such as retrieving the total number of bytes of data currently stored in a Buffer. For each of these critical paths, we tried to identify the smallest amount of code that must be executed in the common case. Then we designed the rest of the class around these critical paths. We also applied the design principles from this book to simplify the class in general. For example, we eliminated shallow layers and created deeper internal abstractions. The refactored class is 20% smaller than the original version (1476 lines of code, versus 1886 lines in the original).

为了解决这些问题,我们重构了 Buffer 类,使其设计围绕最关键性能的路径进行。我们不仅考虑了上面的分配代码,还考虑了其他几种通用的执行路径,例如检索当前存储在 Buffer 中的数据的字节总数。对于这些关键路径中的每一个,我们试图确定在通常情况下必须执行的最少代码量。然后,我们围绕这些关键路径设计了课程的其余部分。我们还应用了本书中的设计原则来简化整个类。例如,我们消除了浅层并创建了更深的内部抽象。重构后的类比原始版本小 20%(1476 行代码,而原始版本为 1886 行)。

Figure 20.2: The original code for allocating new space at the end of a Buffer, using an internal chunk.

图 20.2:使用内部块在 Buffer 的末尾分配新空间的原始代码。

Figure 20.3: The new code for allocating new space in an internal chunk of a Buffer.

图 20.3:用于在 Buffer 的内部块中分配新空间的新代码。

Figure 20.3 shows the new critical path for allocating internal space in a Buffer. The new code is not only faster, but it is also easier to read, since it avoids shallow abstractions. The entire path is handled in a single method, and it uses a single test to rule out all of the special cases. The new code introduces a new instance variable, extraAppendBytes, in order to simplify the critical path. This variable keeps track of how much unused space is available immediately after the last chunk in the Buffer. If there is no space available, or if the last chunk in the Buffer isn’t an internal chunk, or if the Buffer contains no chunks at all, then extraAppendBytes is zero. The code in Figure 20.3 represents the least possible amount of code to handle this common case.

图 20.3 显示了用于在 Buffer 中分配内部空间的新关键路径。新代码不仅更快,而且更容易阅读,因为它避免了浅层抽象。整个路径使用单一方法处理,它使用单一测试来排除所有特殊情况。新代码引入了新的实例变量 extraAppendBytes,以简化关键路径。该变量跟踪缓冲区中最后一个块之后立即有多少未使用空间可用。如果没有可用空间,或者 Buffer 中的最后一个块不是内部块,或者 Buffer 根本不包含任何块,则 extraAppendBytes 为零。图 20.3 中的代码表示处理这种常见情况的最少代码量。

Note: the update to totalLength could have been eliminated by recomputing the total Buffer length from the individual chunks whenever it is needed. However, this approach would be expensive for a large Buffer with many chunks, and fetching the total Buffer length is another common operation. Thus, we chose to add a small amount of extra overhead to alloc in order to ensure that the Buffer length is always immediately available.

注意:可以通过在需要重新计算各个块的总缓冲区长度来消除对 totalLength 的更新。但是,这种方法对于具有许多块的大型 Buffer 而言将是昂贵的,并且获取 Buffer 的总长度是另一种常见的操作。因此,我们选择向 alloc 添加少量额外开销,以确保 Buffer 长度始终立即可用。

The new code is about twice as fast as the old code: the total time to append a 1-byte string to a Buffer using internal storage dropped from 8.8 ns to 4.75 ns. Many other Buffer operations also speeded up because of the revisions. For example, the time to construct a new Buffer, append a small chunk in internal storage, and destroy the Buffer dropped from 24 ns to 12 ns.

新代码的速度约为旧代码的两倍:使用内部存储将 1 字节字符串附加到缓冲区的总时间从 8.8ns 降低到 4.75ns。许多其他 Buffer 操作也因为修改而加快了速度。例如,构建一个新的 Buffer,在内部存储中附加一小块,销毁 Buffer 的时间从 24ns 降至 12ns。

20.5 Conclusion 结论

The most important overall lesson from this chapter is that clean design and high performance are compatible. The Buffer class rewrite improved its performance by a factor of 2 while simplifying its design and reducing code size by 20%. Complicated code tends to be slow because it does extraneous or redundant work. On the other hand, if you write clean, simple code, your system will probably be fast enough that you don’t have to worry much about performance in the first place. In the few cases where you do need to optimize performance, the key is simplicity again: find the critical paths that are most important for performance and make them as simple as possible.

本章最重要的总体经验是,简洁的设计和高性能是兼容的。重写 Buffer 类可将其性能提高 2 倍,同时简化其设计并将代码大小减少 20%。复杂的代码通常会很慢,因为它会执行无关或冗余的工作。另一方面,如果您编写干净,简单的代码,则系统可能会足够快,您一开始就不必担心性能。在少数需要优化性能的情况下,关键还是简化:找到对性能最重要的关键路径,并使它们尽可能简单。

',80)]))}const d=t(l,[["render",c],["__file","ch20.html.vue"]]),f=JSON.parse('{"path":"/ch20.html","title":"第 20 章 设计性能","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"20.1 How to think about performance 如何考虑性能","slug":"_20-1-how-to-think-about-performance-如何考虑性能","link":"#_20-1-how-to-think-about-performance-如何考虑性能","children":[]},{"level":2,"title":"20.2 Measure before modifying 修改前的度量","slug":"_20-2-measure-before-modifying-修改前的度量","link":"#_20-2-measure-before-modifying-修改前的度量","children":[]},{"level":2,"title":"20.3 Design around the critical path 围绕关键路径进行设计","slug":"_20-3-design-around-the-critical-path-围绕关键路径进行设计","link":"#_20-3-design-around-the-critical-path-围绕关键路径进行设计","children":[]},{"level":2,"title":"20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区","slug":"_20-4-an-example-ramcloud-buffers-示例-ramcloud-缓冲区","link":"#_20-4-an-example-ramcloud-buffers-示例-ramcloud-缓冲区","children":[]},{"level":2,"title":"20.5 Conclusion 结论","slug":"_20-5-conclusion-结论","link":"#_20-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"ch20.md"}');export{d as comp,f as data}; +import{_ as t,c as a,f as o,o as s}from"./app-BQdhfat9.js";const i="/A-Philosophy-of-Software-Design-zh/assets/00022-CAKLnDUD.jpeg",n="/A-Philosophy-of-Software-Design-zh/assets/00023-CcNUy9iq.gif",r="/A-Philosophy-of-Software-Design-zh/assets/00024-DLcgw9w_.gif",l={};function c(h,e){return s(),a("div",null,e[0]||(e[0]=[o('

第 20 章 设计性能

Chapter 20 Designing for Performance

Up until this point, the discussion of software design has focused on complexity; the goal has been to make software as simple and understandable as possible. But what if you are working on a system that needs to be fast? How should performance considerations affect the design process? This chapter discusses how to achieve high performance without sacrificing clean design. The most important idea is still simplicity: not only does simplicity improve a system’s design, but it usually makes systems faster.

到目前为止,关于软件设计的讨论都集中在复杂性上。目标是使软件尽可能简单易懂。但是,如果您需要让一个系统运行的更加高效,该怎么办?性能方面的考虑应如何影响设计过程?本章讨论如何在不牺牲简洁设计的情况下实现高性能。最重要的想法仍然是简单性:简单性不仅可以改善系统的设计,而且通常可以使系统更快。

20.1 How to think about performance 如何考虑性能

The first question to address is “how much should you worry about performance during the normal development process?” If you try to optimize every statement for maximum speed, it will slow down development and create a lot of unnecessary complexity. Furthermore, many of the “optimizations” won’t actually help performance. On the other hand, if you completely ignore performance issues, it’s easy to end up with a large number of significant inefficiencies spread throughout the code; the resulting system can easily be 5–10x slower than it needs to be. In this “death by a thousand cuts” scenario it’s hard to come back later and improve the performance, because there is no single improvement that will have much impact.

要解决的第一个问题是“在正常的开发过程中,您应该在多大程序上担心性能?” 如果您尝试优化每条语句以获得最大速度,则它将减慢开发速度并产生很多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果您完全忽略了性能问题,则很容易导致遍及整个代码中出现大量严重的低效问题。结果系统很容易比所需的速度慢 5–10 倍。在这种“千刀万剐”的情况下,以后很难再回来改进性能了,因为没有单一的改进会产生很大的影响。

The best approach is something between these extremes, where you use basic knowledge of performance to choose design alternatives that are “naturally efficient” yet also clean and simple. The key is to develop an awareness of which operations are fundamentally expensive. Here are a few examples of operations that are relatively expensive today:

最好的方法是介于这两种极端之间,在这种极端情况下,您可以利用性能的基本知识来选择“自然高效”但又干净简单的设计方案。关键是要意识到哪些操作从根本上来说是昂贵的。以下是一些今天相对昂贵的操作示例:

  • Network communication: even within a datacenter, a round-trip message exchange can take 10–50 µs, which is tens of thousands of instruction times. Wide-area round-trips can take 10–100 ms.
  • I/O to secondary storage: disk I/O operations typically take 5–10 ms, which is millions of instruction times. Flash storage takes 10–100 µs. New emerging nonvolatile memories may be as fast as 1 µs, but this is still around 2000 instruction times.
  • Dynamic memory allocation (malloc in C, new in C++ or Java) typically involves significant overhead for allocation, freeing, and garbage collection.
  • Cache misses: fetching data from DRAM into an on-chip processor cache takes a few hundred instruction times; in many programs, overall performance is determined as much by cache misses as by computational costs.

  • 网络通信:即使在数据中心内,往返消息交换也可能花费 10–50 µs,这是数以万计的指令时间。广域网往返可能需要 10 到 100 毫秒。
  • 二级存储的 I/O:磁盘 I/O 操作通常需要 5 到 10 毫秒,这是数百万条指令时间。闪存存储需要 10–100 µs。新出现的非易失性存储器的速度可能高达 1 µs,但这仍约为 2000 条指令时间。
  • 动态内存分配(C 语言中的 malloc, C++ 或 Java 中的新增功能)通常涉及分配,释放和垃圾回收的大量开销。
  • 缓存缺失:将数据从 DRAM 提取到片上处理器高速缓存中需要数百条指令时间;在许多程序中,整体性能取决于缓存未命中和计算成本。

The best way to learn which things are expensive is to run micro-benchmarks (small programs that measure the cost of a single operation in isolation). In the RAMCloud project, we created a simple program that provides a framework for microbenchmarks. It took a few days to create the framework, but the framework makes it possible to add new micro-benchmarks in five or ten minutes. This has allowed us to accumulate dozens of micro-benchmarks. We use these both to understand the performance of existing libraries used in RAMCloud, and also to measure the performance of new classes written for RAMCloud.

了解哪些东西最昂贵的最好方法是运行微基准测试(单独衡量单个操作成本的小程序)。在 RAMCloud 项目中,我们创建了一个提供微基准测试的框架简单的程序。创建该框架花了几天时间,但是该框架使在五到十分钟内添加新的微基准成为可能。这使我们积累了几十个微基准。我们既可以使用它们来了解 RAMCloud 中使用的现有库的性能,也可以衡量为 RAMCloud 编写的新类的性能。

Once you have a general sense for what is expensive and what is cheap, you can use that information to choose cheap operations whenever possible. In many cases, a more efficient approach will be just as simple as a slower approach. For example, when storing a large collection of objects that will be looked up using a key value, you could use either a hash table or an ordered map. Both are commonly available in library packages, and both are simple and clean to use. However, hash tables can easily be 5–10x faster. Thus, you should always use a hash table unless you need the ordering properties provided by the map.

一旦您对什么是昂贵和什么便宜有了大致的了解,就可以使用该信息尽可能地选择便宜的操作。在许多情况下,更有效的方法将与较慢的方法一样简单。例如,当存储将使用键值查找的大量对象时,可以使用哈希表或有序映射。两者都通常在库包中提供,并且都简单易用。但是,哈希表可以轻松地快 5-10 倍。因此,除非需要映射提供的排序属性,否则您应始终使用哈希表。

As another example, consider allocating an array of structures in a language such as C or C++. There are two ways you can do this. One way is for the array to hold pointers to structures, in which case you must first allocate space for the array, then allocate space for each individual structure. It is much more efficient to store the structures in the array itself, so you only allocate one large block for everything.

作为另一个示例,请考虑使用诸如 C 或 C++ 之类的语言分配结构数组。有两种方法可以执行此操作。一种方法是让数组保留指向结构的指针,在这种情况下,您必须首先为数组分配空间,然后为每个单独的结构分配空间。将结构存储在数组本身中效率要高得多,因此您只为所有内容分配一大块内存。

If the only way to improve efficiency is by adding complexity, then the choice is more difficult. If the more efficient design adds only a small amount of complexity, and if the complexity is hidden, so it doesn’t affect any interfaces, then it may be worthwhile (but beware: complexity is incremental). If the faster design adds a lot of implementation complexity, or if it results in more complicated interfaces, then it may be better to start off with the simpler approach and optimize later if performance turns out to be a problem. However, if you have clear evidence that performance will be important in a particular situation, then you might as well implement the faster approach immediately.

如果提高效率的唯一方法是增加复杂性,那么选择就更困难了。如果更高效的设计仅增加了少量复杂性,并且复杂性是隐藏的,那么它不影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现复杂性,或者导致更复杂的接口,那么最好是从更简单的方法开始,然后在性能出现问题时进行优化。但是,如果您有明确的证据表明性能在特定情况下很重要,那么您不妨立即实施更高效的方法。

In the RAMCloud project one of our overall goals was to provide the lowest possible latency for client machines accessing the storage system over a datacenter network. As a result, we decided to use special hardware for networking, which allowed RAMCloud to bypass the kernel and communicate directly with the network interface controller to send and receive packets. We made this decision even though it added complexity, because we knew from prior measurements that kernel-based networking would be too slow to meet our needs. In most of the rest of the RAMCloud system we were able to design for simplicity; getting this one big issue “right” made many other things easier.

在 RAMCloud 项目中,我们的总体目标之一是为通过数据中心网络访问存储系统的客户端机器提供尽可能低的延迟。结果,我们决定使用特殊的硬件进行联网,从而使 RAMCloud 绕过内核并直接与网络接口控制器进行通信以发送和接收数据包。尽管增加了复杂性,但我们还是做出了这个决定,因为我们从先前的测量中知道,基于内核的网络太慢了,无法满足我们的需求。在其余的 RAMCloud 系统中,我们能够进行简单设计。把这个大问题“解决”会让其他事情变得更加容易。

In general, simpler code tends to run faster than complex code. If you have defined away special cases and exceptions, then no code is needed to check for those cases and the system runs faster. Deep classes are more efficient than shallow ones, because they get more work done for each method call. Shallow classes result in more layer crossings, and each layer crossing adds overhead.

通常,简单的代码往往比复杂的代码运行得更快。如果您已经定义好了特殊情况和异常情况,那么就不需要代码来检查这些情况,系统就会运行速度更快。深层类比浅层类更有效,因为它们为每个方法调用完成了更多工作。浅类会导致更多的层交叉,并且每个层交叉都会增加开销。

20.2 Measure before modifying 修改前的度量

But suppose that your system is still too slow, even though you have designed it as described above. It’s tempting to rush off and start making performance tweaks, based on your intuitions about what is slow. Don’t do this! Programmers’ intuitions about performance are unreliable. This is true even for experienced developers. If you start making changes based on intuition, you’ll waste time on things that don’t actually improve performance, and you’ll probably make the system more complicated in the process.

但是假设您的系统仍然太慢,即使您已经按照上面描述的方式设计了它。根据您对什么是缓慢的直觉,很容易匆忙进行性能调整。不要这样做!程序员对性能的直觉是不可靠的。即使对于有经验的开发人员也是如此。如果您开始根据直觉进行修改,你会把时间浪费在实际上无法提高性能的事情上,并且在这个过程中可能会使系统变得更加复杂。

Before making any changes, measure the system’s existing behavior. This serves two purposes. First, the measurements will identify the places where performance tuning will have the biggest impact. It isn’t sufficient just to measure the top-level system performance. This may tell you that the system is too slow, but it won’t tell you why. You’ll need to measure deeper to identify in detail the factors that contribute to overall performance; the goal is to identify a small number of very specific places where the system is currently spending a lot of time, and where you have ideas for improvement. The second purpose of the measurements is to provide a baseline, so that you can re-measure performance after making your changes to ensure that performance actually improved. If the changes didn’t make a measurable difference in performance, then back them out (unless they made the system simpler). There’s no point in retaining complexity unless it provides a significant speedup.

进行任何更改之前,请测量系统的现有行为。这有两个目的。首先,这些测量将确定性能调整将产生最大影响的地方。仅仅测量顶级系统性能是不够的。这可能会告诉您系统速度太慢,但不会告诉您原因。您需要进行更深入的衡量,以详细确定影响整体绩效的因素;目标是确定系统当前花费大量时间的少量非常具体的地方,以及您有改进想法的地方。测量的第二个目的是提供基线,以便您可以在进行更改后重新测量性能,以确保性能确实得到改善。如果更改并未在性能上产生可衡量的差异,则将它们撤销(除非它们使系统更简单)。保留复杂性是没有意义的,除非它提供了显著的速度提升。

20.3 Design around the critical path 围绕关键路径进行设计

At this point, let’s assume that you have carefully analyzed performance and have identified a piece of code that is slow enough to affect the overall system performance. The best way to improve its performance is with a “fundamental” change, such as introducing a cache, or using a different algorithmic approach (balanced tree vs. list, for instance). Our decision to bypass the kernel for network communication in RAMCloud is an example of a fundamental fix. If you can identify a fundamental fix, then you can implement it using the design techniques discussed in previous chapters.

在这一点上,假设您已经仔细分析了性能并确定了一段速度缓慢到足以影响整个系统的性能的代码。改善其性能的最佳方法是进行“根本性”更改,例如引入缓存,或使用其他算法方法(例如,平衡树与列表)。我们决定绕过内核进行 RAMCloud 中的网络通信的决定是一个根本性修正的示例。如果您能确定一个根本性的修正,则可以使用前面各章中讨论的设计技术来实施它。

Unfortunately, situations will sometimes arise where there isn’t a fundamental fix. This brings us to the core issue for this chapter, which is how to redesign an existing piece of code so that it runs faster. This should be your last resort, and it shouldn’t happen often, but there are cases where it can make a big difference. The key idea is to design the code around the critical path.

不幸的是,有时会出现一些没有根本解决办法的情况。这就把我们带到本章的核心问题,即如何重新设计现有代码,使其运行更快。这应该是您的不得已的方法,并且不应该经常发生,但是在某些情况下它可能会带来很大的不同。关键思想是围绕关键路径设计代码。

Start off by asking yourself what is the smallest amount of code that must be executed to carry out the desired task in the common case. Disregard any existing code structure. Imagine instead that you are writing a new method that implements just the critical path, which is the minimum amount of code that must be executed in the the most common case. The current code is probably cluttered with special cases; ignore them in this exercise. The current code might pass through several method calls on the critical path; imagine instead that you could put all the relevant code in a single method. The current code may also use a variety of variables and data structures; consider only the data needed for the critical path, and assume whatever data structure is most convenient for the critical path. For example, it may make sense to combine multiple variables into a single value. Assume that you could completely redesign the system in order to minimize the code that must be executed for the critical path. Let’s call this code “the ideal.”

首先,问自己在通常情况下执行所需任务必须执行的最少代码量是多少。忽略任何现有的代码结构。想象一下您正在编写一个仅实现关键路径的新方法,这是在最常见的情况下必须执行的最少代码量。当前的代码可能充满特殊情况。在此练习中,请忽略它们。当前的代码可能会在关键路径上通过多个方法调用。想象一下,您可以将所有相关代码放在一个方法中。当前代码还可以使用各种变量和数据结构。仅考虑关键路径所需的数据,并假定最适合关键路径的任何数据结构。例如,将多个变量合并为一个值可能很有意义。假设您可以完全重新设计系统,以最大程度地减少必须为关键路径执行的代码。我们把这段代码称为“理想”。

The ideal code probably clashes with your existing class structure, and it may not be practical, but it provides a good target: this represents the simplest and fastest that the code can ever be. The next step is to look for a new design that comes as close as possible to the ideal while still having a clean structure. You can apply all of the design ideas from previous chapters of this book, but with the additional constraint of keeping the ideal code (mostly) intact. You may have to add a bit of extra code to the ideal in order to allow clean abstractions; for example, if the code involves a hash table lookup, it’s OK to introduce an extra method call to a general-purpose hash table class. In my experience it’s almost always possible to find a design that is clean and simple, yet comes very close to the ideal.

理想的代码可能会与您现有的类结构冲突,并且可能不切实际,但它提供了一个很好的目标:这代表了代码可能是最简单和最快的。下一步是寻找一种新设计,使其尽可能接近理想状态,同时又要保持干净的结构。您可以应用本书前面各章中的所有设计思想,但要保持(大部分)理想代码的完整性。您可能需要在理想情况下添加一些额外的代码,以便实现干净的抽象。例如,如果代码涉及哈希表查找,引入一个额外的方法调用到一个通用哈希表类是可以的。根据我的经验,几乎总是能找到一种简洁明了但是但非常接近理想的设计。

One of the most important things that happens in this process is to remove special cases from the critical path. When code is slow, it’s often because it must handle a variety of situations, and the code gets structured to simplify the handling of all the different cases. Each special case adds a little bit of code to the critical path, in the form of extra conditional statements and/or method calls. Each of these additions makes the code a bit slower. When redesigning for performance, try to minimize the number of special cases you must check. Ideally, there will be a single if statement at the beginning, which detects all special cases with one test. In the normal case, only this one test will need to be made, after which the the critical path can be executed with no additional tests for special cases. If the initial test fails (which means a special case has occurred) the code can branch to a separate place off the critical path to handle it. Performance isn’t as important for special cases, so you can structure the special-case code for simplicity rather than performance.

在此过程中发生的最重要的事情之一是从关键路径中除去特殊情况。当代码运行缓慢时,通常是因为它必须处理各种情况,并且代码经过结构化以简化所有不同情况的处理。每个特殊情况都以额外的条件语句和/或方法调用的形式向关键路径添加了一些代码。这些添加中的每一个都会使代码变慢。重新设计性能时,请尝试减少必须检查的特殊情况的数量。理想情况下,开头应该有一个 if 语句,该语句可以通过一个测试检测所有特殊情况。在正常情况下,只需要进行一项测试,之后就可以执行关键路径,而对于特殊情况则无需进行其他测试。如果初始测试失败(这意味着发生了特殊情况),则代码可以分支到关键路径之外的单独位置以进行处理。对于特殊情况,性能并不是那么重要,因此您可以将特殊情况下的代码结构化,使之简单化而不是性能化。

20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区

Let’s consider an example, in which the Buffer class of the RAMCloud storage system was optimized to achieve a speedup of about 2x for the most common operations.

让我们考虑一个例子,在这个例子中,对 RAMCloud 存储系统的 Buffer 类进行了优化,在最常见的操作中实现了约 2 倍的速度提升。

RAMCloud uses Buffer objects to manage variable-length arrays of memory, such as request and response messages for remote procedure calls. Buffers are designed to reduce overheads from memory copying and dynamic storage allocation. A Buffer stores what appears to be a linear array of bytes, but for efficiency it allows the underlying storage to be divided into multiple discontiguous chunks of memory, as shown in Figure 20.1. A Buffer is created by appending chunks of data. Each chunk is either external or internal. If a chunk is external, its storage is owned by the caller; the Buffer keeps a reference to this storage. External chunks are typically used for large chunks in order to avoid memory copies. If a chunk is internal, the Buffer owns the storage for the chunk; data supplied by the caller is copied into the Buffer’s internal storage. Each Buffer contains a small built-in allocation, which is a block of memory available for storing internal chunks. If this space is exhausted, then the Buffer creates additional allocations, which must be freed when the Buffer is destroyed. Internal chunks are convenient for small chunks where the memory copying costs are negligible. Figure 20.1 shows a Buffer with 5 chunks: the first chunk is internal, the next two are external, and the final two chunks are internal.

RAMCloud 使用 Buffer 对象管理可变长度的内存数组,例如远程过程调用的请求和响应消息。缓冲区旨在减少内存复制和动态存储分配的开销。缓冲区存储的似乎是一个线性的字节数组,但为了提高效率,它允许将底层存储划分为多个不连续的内存块,如图 20.1 所示。缓冲区是通过附加数据块创建的。每个块要么是外部的,要么是内部的。如果块是外部的,则其存储空间由调用方拥有;Buffer 保留对此存储的引用。外部块通常用于大型块,以避免内存复制。如果块是内部的,则 Buffer 拥有该块的存储;调用者提供的数据将被复制到缓冲区的内部存储器中。每个 Buffer 包含一个小的内置分配,这是一个可用于存储内部块的内存块。如果此空间已用完,则 Buffer 需要额外分配内存,这些分配的内存必须在 Buffer 销毁时进行释放。内部块对于小块来说是很方便的,因为内存复制的成本可以忽略不计。图 20.1 显示了具有 5 个块的 Buffer:第一个块是内部的,接下来的两个块是外部的,最后两个块是内部的。

Figure 20.1: A Buffer object uses a collection of memory chunks to store what appears to be a linear array of bytes. Internal chunks are owned by the Buffer and freed when the Buffer is destroyed; external chunks are not owned by the Buffer.

图 20.1:Buffer 对象使用内存块的集合来存储看似线性字节数组。内部块为 Buffer 拥有,并在 Buffer 销毁时释放;外部块不为 Buffer 所有。

The Buffer class itself represents a “fundamental fix,” in that it eliminates expensive memory copies that would have been required without it. For example, when assembling a response message containing a short header and the contents of a large object in the RAMCloud storage system, RAMCloud uses a Buffer with two chunks. The first chunk is an internal one that contains the header; the second chunk is an external one that refers to the object contents in the RAMCloud storage system. The response can be collected in the Buffer without copying the large object.

Buffer 类本身代表“根本性的修补程序”,因为它消除了昂贵的内存拷贝,而如果没有它的话,就需要进行拷贝。例如,在 RAMCloud 存储系统中组装包含短标头和大对象内容的响应消息时,RAMCloud 使用带有两个块的 Buffer。第一个块是包含头的内部块;第二个块是一个外部块,它引用 RAMCloud 存储系统中的对象内容。可以在不复制大对象的情况下将响应收集到 Buffer 中。

Aside from the fundamental approach of allowing discontiguous chunks, we did not attempt to optimize the code of the Buffer class in the original implementation. Over time, however, we noticed Buffers being used in more and more situations; for example, at least four Buffers are created during the execution of each remote procedure call. Eventually, it became clear that speeding up the implementation of Buffer could have a noticeable impact on overall system performance. We decided to see if we could improve the performance of the Buffer class.

除了允许不连续块的基本方法外,在最初的实现中,我们并没有尝试优化 Buffer 类的代码。然而,随着时间的流逝,我们注意到 Buffer 越来越多地被使用。例如,在执行每个远程过程调用期间,至少会创建四个缓冲区。最终,我们发现,加速 Buffer 的实现可能会对整体系统性能产生显著影响。我们决定看看是否可以提高 Buffer 类的性能。

The most common operation for Buffer is to allocate space for a small amount of new data using an internal chunk. This happens, for example, when creating headers for request and response messages. We decided to use this operation as the critical path for optimization. In the simplest possible case, the space can be allocated by enlarging the last existing chunk in the Buffer. However, this is only possible if the last existing chunk is internal, and if there is enough space in its allocation to accommodate the new data. The ideal code would perform a single check to confirm that the simple approach is possible, then it would adjust the size of the existing chunk.

Buffer 最常见的操作是使用内部块为少量新数据分配空间。例如,在为请求和响应消息创建标头时,就会发生这种情况。我们决定使用将此操作作为优化的关键路径。在最简单的情况下,可以通过扩大 Buffer 中最后一个现有块来分配空间。但是,只有在最后一个现有块是内部块,并且在其分配中有足够的空间来容纳新数据时才有可能。理想的代码将执行一次检查,以确认简单方法是否可行的,然后将调整现有块的大小。

Figure 20.2 shows the original code for the critical path, which starts with the method Buffer::alloc. In the fastest possible case, Buffer::alloc calls Buffer:: allocateAppend, which calls Buffer::Allocation::allocateAppend. From a performance standpoint, this code has two problems. The first problem is that numerous special cases are checked individually:

图 20.2 显示了关键路径的原始代码,该代码以 Buffer::alloc 方法开头。在最快的情况下,Buffer::alloc 调用 Buffer::allocateAppend,后者调用 Buffer::Allocation::allocateAppend。从性能的角度来看,此代码有两个问题。第一个问题是要单独检查许多特殊情况:

  • Buffer::allocateAppend checks to see if the Buffer currently has any allocations.
  • The code checks twice to see if the current allocation has enough room for the new data: once in Buffer::Allocation::allocateAppend, and again when its return value is tested by Buffer::allocateAppend.
  • Buffer::alloc tests the return value from Buffer::allocAppend to confirm yet again that the allocation succeeded.

  • Buffer::allocateAppend 检查当前 Buffer 是否有足够的空间。
  • 代码检查两次以查看当前分配是否有足够的空间容纳新数据:一次在 Buffer::Allocation::allocateAppend 中,一次在其返回值由 Buffer::allocateAppend 测试时。
  • Buffer::alloc 测试 Buffer::allocAppend 的返回值,再次确认分配成功。

Furthermore, rather than trying to expand the last chunk directly, the code allocates new space without any consideration of the last chunk. Then Buffer::alloc checks to see if that space happens to be adjacent to the last chunk, in which case it merges the new space with the existing chunk. This results in additional checks. Overall, this code tests 6 distinct conditions in the critical path.

此外,该代码没有尝试直接扩展最后一个块,而是在不考虑最后一个块的情况下分配了新空间。然后,Buffer::alloc 检查该空间是否恰好与最后一块相邻,在这种情况下,它将新空间与现有块合并。这导致额外的检查。总体而言,此代码测试了关键路径中的 6 种不同条件。

The second problem with the original code is that it has too many layers, all of which are shallow. This is both a performance problem and a design problem. The critical path makes two additional method calls in addition to the original invocation of Buffer::alloc. Each method call takes additional time, and the result of each call must be checked by its caller, which results in more special cases to consider. Chapter 7 discussed how abstractions should normally change as you pass from one layer to another, but all three of the methods in Figure 20.2 have identical signatures and they provide essentially the same abstraction; this is a red flag. Buffer::allocateAppend is nearly a pass-though method; its only contribution is to create a new allocation if needed. The extra layers make the code both slower and more complicated.

原始代码的第二个问题是它的层数太多,而且都很浅。这既是性能问题,也是设计问题。除了对 Buffer::alloc 的原始调用之外,关键路径还进行了两个额外的方法调用。每个方法调用都需要额外的时间,并且每个调用的结果必须由其调用者检查,这导致需要考虑更多的特殊情况。第 7 章讨论了当您从一层传递到另一层时,抽象通常应该如何变化,但是图 20.2 中的所有三种方法都具有相同的签名,并且它们提供了基本相同的抽象。这是一个危险信号。Buffer::allocateAppend 几乎是一个传递方法;它的唯一作用是在需要时创建新的分配。额外的层使代码更慢,更复杂。

To fix these problems, we refactored the Buffer class so that its design is centered around the most performance-critical paths. We considered not just the allocation code above but several other commonly executed paths, such as retrieving the total number of bytes of data currently stored in a Buffer. For each of these critical paths, we tried to identify the smallest amount of code that must be executed in the common case. Then we designed the rest of the class around these critical paths. We also applied the design principles from this book to simplify the class in general. For example, we eliminated shallow layers and created deeper internal abstractions. The refactored class is 20% smaller than the original version (1476 lines of code, versus 1886 lines in the original).

为了解决这些问题,我们重构了 Buffer 类,使其设计围绕最关键性能的路径进行。我们不仅考虑了上面的分配代码,还考虑了其他几种通用的执行路径,例如检索当前存储在 Buffer 中的数据的字节总数。对于这些关键路径中的每一个,我们试图确定在通常情况下必须执行的最少代码量。然后,我们围绕这些关键路径设计了课程的其余部分。我们还应用了本书中的设计原则来简化整个类。例如,我们消除了浅层并创建了更深的内部抽象。重构后的类比原始版本小 20%(1476 行代码,而原始版本为 1886 行)。

Figure 20.2: The original code for allocating new space at the end of a Buffer, using an internal chunk.

图 20.2:使用内部块在 Buffer 的末尾分配新空间的原始代码。

Figure 20.3: The new code for allocating new space in an internal chunk of a Buffer.

图 20.3:用于在 Buffer 的内部块中分配新空间的新代码。

Figure 20.3 shows the new critical path for allocating internal space in a Buffer. The new code is not only faster, but it is also easier to read, since it avoids shallow abstractions. The entire path is handled in a single method, and it uses a single test to rule out all of the special cases. The new code introduces a new instance variable, extraAppendBytes, in order to simplify the critical path. This variable keeps track of how much unused space is available immediately after the last chunk in the Buffer. If there is no space available, or if the last chunk in the Buffer isn’t an internal chunk, or if the Buffer contains no chunks at all, then extraAppendBytes is zero. The code in Figure 20.3 represents the least possible amount of code to handle this common case.

图 20.3 显示了用于在 Buffer 中分配内部空间的新关键路径。新代码不仅更快,而且更容易阅读,因为它避免了浅层抽象。整个路径使用单一方法处理,它使用单一测试来排除所有特殊情况。新代码引入了新的实例变量 extraAppendBytes,以简化关键路径。该变量跟踪缓冲区中最后一个块之后立即有多少未使用空间可用。如果没有可用空间,或者 Buffer 中的最后一个块不是内部块,或者 Buffer 根本不包含任何块,则 extraAppendBytes 为零。图 20.3 中的代码表示处理这种常见情况的最少代码量。

Note: the update to totalLength could have been eliminated by recomputing the total Buffer length from the individual chunks whenever it is needed. However, this approach would be expensive for a large Buffer with many chunks, and fetching the total Buffer length is another common operation. Thus, we chose to add a small amount of extra overhead to alloc in order to ensure that the Buffer length is always immediately available.

注意:可以通过在需要重新计算各个块的总缓冲区长度来消除对 totalLength 的更新。但是,这种方法对于具有许多块的大型 Buffer 而言将是昂贵的,并且获取 Buffer 的总长度是另一种常见的操作。因此,我们选择向 alloc 添加少量额外开销,以确保 Buffer 长度始终立即可用。

The new code is about twice as fast as the old code: the total time to append a 1-byte string to a Buffer using internal storage dropped from 8.8 ns to 4.75 ns. Many other Buffer operations also speeded up because of the revisions. For example, the time to construct a new Buffer, append a small chunk in internal storage, and destroy the Buffer dropped from 24 ns to 12 ns.

新代码的速度约为旧代码的两倍:使用内部存储将 1 字节字符串附加到缓冲区的总时间从 8.8ns 降低到 4.75ns。许多其他 Buffer 操作也因为修改而加快了速度。例如,构建一个新的 Buffer,在内部存储中附加一小块,销毁 Buffer 的时间从 24ns 降至 12ns。

20.5 Conclusion 结论

The most important overall lesson from this chapter is that clean design and high performance are compatible. The Buffer class rewrite improved its performance by a factor of 2 while simplifying its design and reducing code size by 20%. Complicated code tends to be slow because it does extraneous or redundant work. On the other hand, if you write clean, simple code, your system will probably be fast enough that you don’t have to worry much about performance in the first place. In the few cases where you do need to optimize performance, the key is simplicity again: find the critical paths that are most important for performance and make them as simple as possible.

本章最重要的总体经验是,简洁的设计和高性能是兼容的。重写 Buffer 类可将其性能提高 2 倍,同时简化其设计并将代码大小减少 20%。复杂的代码通常会很慢,因为它会执行无关或冗余的工作。另一方面,如果您编写干净,简单的代码,则系统可能会足够快,您一开始就不必担心性能。在少数需要优化性能的情况下,关键还是简化:找到对性能最重要的关键路径,并使它们尽可能简单。

',80)]))}const d=t(l,[["render",c],["__file","ch20.html.vue"]]),f=JSON.parse('{"path":"/ch20.html","title":"第 20 章 设计性能","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"20.1 How to think about performance 如何考虑性能","slug":"_20-1-how-to-think-about-performance-如何考虑性能","link":"#_20-1-how-to-think-about-performance-如何考虑性能","children":[]},{"level":2,"title":"20.2 Measure before modifying 修改前的度量","slug":"_20-2-measure-before-modifying-修改前的度量","link":"#_20-2-measure-before-modifying-修改前的度量","children":[]},{"level":2,"title":"20.3 Design around the critical path 围绕关键路径进行设计","slug":"_20-3-design-around-the-critical-path-围绕关键路径进行设计","link":"#_20-3-design-around-the-critical-path-围绕关键路径进行设计","children":[]},{"level":2,"title":"20.4 An example: RAMCloud Buffers 示例:RAMCloud 缓冲区","slug":"_20-4-an-example-ramcloud-buffers-示例-ramcloud-缓冲区","link":"#_20-4-an-example-ramcloud-buffers-示例-ramcloud-缓冲区","children":[]},{"level":2,"title":"20.5 Conclusion 结论","slug":"_20-5-conclusion-结论","link":"#_20-5-conclusion-结论","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"ch20.md"}');export{d as comp,f as data}; diff --git a/assets/ch21.html-Q8Jm88-g.js b/assets/ch21.html-Cc6N4a4h.js similarity index 97% rename from assets/ch21.html-Q8Jm88-g.js rename to assets/ch21.html-Cc6N4a4h.js index 78e63635..4ee7b6b5 100644 --- a/assets/ch21.html-Q8Jm88-g.js +++ b/assets/ch21.html-Cc6N4a4h.js @@ -1 +1 @@ -import{_ as o,c as t,f as i,o as s}from"./app-CvqtBB8Z.js";const n={};function a(r,e){return s(),t("div",null,e[0]||(e[0]=[i('

第 21 章 结论

Chapter 21 Conclusion

This book is about one thing: complexity. Dealing with complexity is the most important challenge in software design. It is what makes systems hard to build and maintain, and it often makes them slow as well. Over the course of the book I have tried to describe the root causes that lead to complexity, such as dependencies and obscurity. I have discussed red flags that can help you identify unnecessary complexity, such as information leakage, unneeded error conditions, or names that are too generic. I have presented some general ideas you can use to create simpler software systems, such as striving for classes that are deep and generic, defining errors out of existence, and separating interface documentation from implementation documentation. And, finally, I have discussed the investment mindset needed to produce simple designs.

这本书只针对一件事:复杂性。处理复杂性是软件设计中最重要的挑战。这也是为什么系统难以构建和维护的原因,而且复杂的系统通常运行也很缓慢。在本书中,我试图描述导致复杂性的根本原因,例如依赖性和模糊性。我已经讨论了可以帮助您识别不必要的复杂性的危险信号,例如信息泄漏,不必要的错误情况或名称过于笼统。我提出了一些通用的思想,可以用来创建更简单的软件系统,例如,努力创建更深和更通用的类,定义不存在的错误以及将接口文档与实现文档分离。最后,我讨论了产生简单设计所需的投资思路。

The downside of all these suggestions is that they create extra work in the early stages of a project. Furthermore, if you aren’t used to thinking about design issues, then you will slow down even more while you learn good design techniques. If the only thing that matters to you is making your current code work as soon as possible, then thinking about design will seem like drudge work that is getting in the way of your real goal.

所有这些建议的缺点是它们会在项目的早期阶段需要额外的工作量。此外,如果您不习惯于思考设计问题,那么当您学习良好的设计技巧时,您的速度会比较慢。如果对您而言唯一重要的事情让您当前的代码尽快运行,那么思考设计就好像是件苦差事,而这实际上妨碍了您实现真正的目标。

On the other hand, if good design is an important goal for you, then the ideas in this book should make programming more fun. Design is a fascinating puzzle: how can a particular problem be solved with the simplest possible structure? It’s fun to explore different approaches, and it’s a great feeling to discover a solution that is both simple and powerful. A clean, simple, and obvious design is a beautiful thing.

另一方面,如果良好的设计对您来说是重要的目标,那么本书中的思想会让编程变得更有趣。设计是一个令人着迷的难题:如何用最简单的结构解决特定问题?探索不同的方法很有趣,找到一种既简单又强大的解决方案是一种很棒的感觉。干净,简单和明显的设计是一件美丽的事情。

Furthermore, the investments you make in good design will pay off quickly. The modules you defined carefully at the beginning of a project will save you time later as you reuse them over and over. The clear documentation that you wrote six months ago will save you time when you return to the code to add a new feature. The time you spent honing your design skills will also pay for itself: as your skills and experience grow, you will find that you can produce good designs more and more quickly. Good design doesn’t really take much longer than quick-and-dirty design, once you know how.

此外,您对优质设计的投资将很快获得回报。在项目开始时仔细定义的模块,在您一遍又一遍地重复使用它们时,会节省您的时间。您六个月前编写的清晰文档将为您节省返回代码添加新功能的时间。花在磨练设计技能上的时间也将有所回报:随着技能和经验的增长,您会发现您可以越来越快地做出好的设计。一旦您掌握了方法,好的设计实际上并不会比草率的设计花费更多的时间。

The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code. If you improve your design skills, not only will you produce higher quality software more quickly, but the software development process will be more enjoyable.

成为一名优秀设计师的回报是,您可以将大部分时间花在设计阶段,这很有趣。糟糕的设计师花费大量时间在复杂而脆弱的代码中寻找错误。如果您提高了设计技能,不仅可以更快地生产出更高质量的软件,而且软件开发过程也将也会更加愉快。

',12)]))}const d=o(n,[["render",a],["__file","ch21.html.vue"]]),u=JSON.parse('{"path":"/ch21.html","title":"第 21 章 结论","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734160685000},"filePathRelative":"ch21.md"}');export{d as comp,u as data}; +import{_ as o,c as t,f as i,o as s}from"./app-BQdhfat9.js";const n={};function a(r,e){return s(),t("div",null,e[0]||(e[0]=[i('

第 21 章 结论

Chapter 21 Conclusion

This book is about one thing: complexity. Dealing with complexity is the most important challenge in software design. It is what makes systems hard to build and maintain, and it often makes them slow as well. Over the course of the book I have tried to describe the root causes that lead to complexity, such as dependencies and obscurity. I have discussed red flags that can help you identify unnecessary complexity, such as information leakage, unneeded error conditions, or names that are too generic. I have presented some general ideas you can use to create simpler software systems, such as striving for classes that are deep and generic, defining errors out of existence, and separating interface documentation from implementation documentation. And, finally, I have discussed the investment mindset needed to produce simple designs.

这本书只针对一件事:复杂性。处理复杂性是软件设计中最重要的挑战。这也是为什么系统难以构建和维护的原因,而且复杂的系统通常运行也很缓慢。在本书中,我试图描述导致复杂性的根本原因,例如依赖性和模糊性。我已经讨论了可以帮助您识别不必要的复杂性的危险信号,例如信息泄漏,不必要的错误情况或名称过于笼统。我提出了一些通用的思想,可以用来创建更简单的软件系统,例如,努力创建更深和更通用的类,定义不存在的错误以及将接口文档与实现文档分离。最后,我讨论了产生简单设计所需的投资思路。

The downside of all these suggestions is that they create extra work in the early stages of a project. Furthermore, if you aren’t used to thinking about design issues, then you will slow down even more while you learn good design techniques. If the only thing that matters to you is making your current code work as soon as possible, then thinking about design will seem like drudge work that is getting in the way of your real goal.

所有这些建议的缺点是它们会在项目的早期阶段需要额外的工作量。此外,如果您不习惯于思考设计问题,那么当您学习良好的设计技巧时,您的速度会比较慢。如果对您而言唯一重要的事情让您当前的代码尽快运行,那么思考设计就好像是件苦差事,而这实际上妨碍了您实现真正的目标。

On the other hand, if good design is an important goal for you, then the ideas in this book should make programming more fun. Design is a fascinating puzzle: how can a particular problem be solved with the simplest possible structure? It’s fun to explore different approaches, and it’s a great feeling to discover a solution that is both simple and powerful. A clean, simple, and obvious design is a beautiful thing.

另一方面,如果良好的设计对您来说是重要的目标,那么本书中的思想会让编程变得更有趣。设计是一个令人着迷的难题:如何用最简单的结构解决特定问题?探索不同的方法很有趣,找到一种既简单又强大的解决方案是一种很棒的感觉。干净,简单和明显的设计是一件美丽的事情。

Furthermore, the investments you make in good design will pay off quickly. The modules you defined carefully at the beginning of a project will save you time later as you reuse them over and over. The clear documentation that you wrote six months ago will save you time when you return to the code to add a new feature. The time you spent honing your design skills will also pay for itself: as your skills and experience grow, you will find that you can produce good designs more and more quickly. Good design doesn’t really take much longer than quick-and-dirty design, once you know how.

此外,您对优质设计的投资将很快获得回报。在项目开始时仔细定义的模块,在您一遍又一遍地重复使用它们时,会节省您的时间。您六个月前编写的清晰文档将为您节省返回代码添加新功能的时间。花在磨练设计技能上的时间也将有所回报:随着技能和经验的增长,您会发现您可以越来越快地做出好的设计。一旦您掌握了方法,好的设计实际上并不会比草率的设计花费更多的时间。

The reward for being a good designer is that you get to spend a larger fraction of your time in the design phase, which is fun. Poor designers spend most of their time chasing bugs in complicated and brittle code. If you improve your design skills, not only will you produce higher quality software more quickly, but the software development process will be more enjoyable.

成为一名优秀设计师的回报是,您可以将大部分时间花在设计阶段,这很有趣。糟糕的设计师花费大量时间在复杂而脆弱的代码中寻找错误。如果您提高了设计技能,不仅可以更快地生产出更高质量的软件,而且软件开发过程也将也会更加愉快。

',12)]))}const d=o(n,[["render",a],["__file","ch21.html.vue"]]),u=JSON.parse('{"path":"/ch21.html","title":"第 21 章 结论","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734161172000},"filePathRelative":"ch21.md"}');export{d as comp,u as data}; diff --git a/assets/index.html-C2UbQzrl.js b/assets/index.html-_5zP8eA1.js similarity index 96% rename from assets/index.html-C2UbQzrl.js rename to assets/index.html-_5zP8eA1.js index 30d52e30..85234ee5 100644 --- a/assets/index.html-C2UbQzrl.js +++ b/assets/index.html-_5zP8eA1.js @@ -1 +1 @@ -import{_ as d,c as f,a as t,b as u,d as n,r as m,o as s,e as i}from"./app-CvqtBB8Z.js";const e="/A-Philosophy-of-Software-Design-zh/assets/cover-BbonuOFD.jpeg",r={};function p(h,l){const o=m("RouteLink");return s(),f("div",null,[l[23]||(l[23]=t("h1",{id:"目录",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#目录"},[t("span",null,"目录")])],-1)),l[24]||(l[24]=t("img",{src:e,style:{width:"28%"}},null,-1)),l[25]||(l[25]=t("p",null,"斯坦福教授、Tcl 语言发明者 John Ousterhout 的著作《A Philosophy of Software Design》,自出版以来,好评如潮。按照 IT 图书出版的惯例,如果冠名为“实践”,书中内容关注的是某项技术的细节和技巧;冠名为“艺术”,内容可能是记录一件优秀作品的设计过程和经验;而冠名为“哲学”,则是一些通用的原则和方法论,这些原则方法论串起来,能够形成一个体系。正如”知行合一”、“世界是由原子构成的”、“我思故我在”,这些耳熟能详的句子能够一定程度上代表背后的人物和思想。用一句话概括《A Philosophy of Software Design》,软件设计的核心在于降低复杂性。",-1)),t("ul",null,[t("li",null,[u(o,{to:"/preface.html"},{default:n(()=>l[0]||(l[0]=[i("前言")])),_:1})]),t("li",null,[u(o,{to:"/ch01.html"},{default:n(()=>l[1]||(l[1]=[i("第 1 章 介绍")])),_:1})]),t("li",null,[u(o,{to:"/ch02.html"},{default:n(()=>l[2]||(l[2]=[i("第 2 章 复杂性的本质")])),_:1})]),t("li",null,[u(o,{to:"/ch03.html"},{default:n(()=>l[3]||(l[3]=[i("第 3 章 工作代码是不够的")])),_:1})]),t("li",null,[u(o,{to:"/ch04.html"},{default:n(()=>l[4]||(l[4]=[i("第 4 章 模块应该是深的")])),_:1})]),t("li",null,[u(o,{to:"/ch05.html"},{default:n(()=>l[5]||(l[5]=[i("第 5 章 信息隐藏(和泄漏)")])),_:1})]),t("li",null,[u(o,{to:"/ch06.html"},{default:n(()=>l[6]||(l[6]=[i("第 6 章 通用模块更深入")])),_:1})]),t("li",null,[u(o,{to:"/ch07.html"},{default:n(()=>l[7]||(l[7]=[i("第 7 章 不同的层,不同的抽象")])),_:1})]),t("li",null,[u(o,{to:"/ch08.html"},{default:n(()=>l[8]||(l[8]=[i("第 8 章 降低复杂性")])),_:1})]),t("li",null,[u(o,{to:"/ch09.html"},{default:n(()=>l[9]||(l[9]=[i("第 9 章 在一起更好还是分开更好?")])),_:1})]),t("li",null,[u(o,{to:"/ch10.html"},{default:n(()=>l[10]||(l[10]=[i("第 10 章 定义不存在的错误")])),_:1})]),t("li",null,[u(o,{to:"/ch11.html"},{default:n(()=>l[11]||(l[11]=[i("第 11 章 设计它两次")])),_:1})]),t("li",null,[u(o,{to:"/ch12.html"},{default:n(()=>l[12]||(l[12]=[i("第 12 章 为什么要写注释?有四个理由")])),_:1})]),t("li",null,[u(o,{to:"/ch13.html"},{default:n(()=>l[13]||(l[13]=[i("第 13 章 注释应该描述代码中不明显的内容")])),_:1})]),t("li",null,[u(o,{to:"/ch14.html"},{default:n(()=>l[14]||(l[14]=[i("第 14 章 选择的名字")])),_:1})]),t("li",null,[u(o,{to:"/ch15.html"},{default:n(()=>l[15]||(l[15]=[i("第 15 章 先写注释")])),_:1})]),t("li",null,[u(o,{to:"/ch16.html"},{default:n(()=>l[16]||(l[16]=[i("第 16 章 修改现有的代码")])),_:1})]),t("li",null,[u(o,{to:"/ch17.html"},{default:n(()=>l[17]||(l[17]=[i("第 17 章 一致性")])),_:1})]),t("li",null,[u(o,{to:"/ch18.html"},{default:n(()=>l[18]||(l[18]=[i("第 18 章 代码应该是显而易见的")])),_:1})]),t("li",null,[u(o,{to:"/ch19.html"},{default:n(()=>l[19]||(l[19]=[i("第 19 章 软件发展趋势")])),_:1})]),t("li",null,[u(o,{to:"/ch20.html"},{default:n(()=>l[20]||(l[20]=[i("第 20 章 设计性能")])),_:1})]),t("li",null,[u(o,{to:"/ch21.html"},{default:n(()=>l[21]||(l[21]=[i("第 21 章 结论")])),_:1})]),t("li",null,[u(o,{to:"/summary.html"},{default:n(()=>l[22]||(l[22]=[i("总结")])),_:1})])])])}const x=d(r,[["render",p],["__file","index.html.vue"]]),g=JSON.parse('{"path":"/","title":"目录","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734160685000},"filePathRelative":"README.md"}');export{x as comp,g as data}; +import{_ as d,c as f,a as t,b as u,d as n,r as m,o as s,e as i}from"./app-BQdhfat9.js";const e="/A-Philosophy-of-Software-Design-zh/assets/cover-BbonuOFD.jpeg",r={};function p(h,l){const o=m("RouteLink");return s(),f("div",null,[l[23]||(l[23]=t("h1",{id:"目录",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#目录"},[t("span",null,"目录")])],-1)),l[24]||(l[24]=t("img",{src:e,style:{width:"28%"}},null,-1)),l[25]||(l[25]=t("p",null,"斯坦福教授、Tcl 语言发明者 John Ousterhout 的著作《A Philosophy of Software Design》,自出版以来,好评如潮。按照 IT 图书出版的惯例,如果冠名为“实践”,书中内容关注的是某项技术的细节和技巧;冠名为“艺术”,内容可能是记录一件优秀作品的设计过程和经验;而冠名为“哲学”,则是一些通用的原则和方法论,这些原则方法论串起来,能够形成一个体系。正如”知行合一”、“世界是由原子构成的”、“我思故我在”,这些耳熟能详的句子能够一定程度上代表背后的人物和思想。用一句话概括《A Philosophy of Software Design》,软件设计的核心在于降低复杂性。",-1)),t("ul",null,[t("li",null,[u(o,{to:"/preface.html"},{default:n(()=>l[0]||(l[0]=[i("前言")])),_:1})]),t("li",null,[u(o,{to:"/ch01.html"},{default:n(()=>l[1]||(l[1]=[i("第 1 章 介绍")])),_:1})]),t("li",null,[u(o,{to:"/ch02.html"},{default:n(()=>l[2]||(l[2]=[i("第 2 章 复杂性的本质")])),_:1})]),t("li",null,[u(o,{to:"/ch03.html"},{default:n(()=>l[3]||(l[3]=[i("第 3 章 工作代码是不够的")])),_:1})]),t("li",null,[u(o,{to:"/ch04.html"},{default:n(()=>l[4]||(l[4]=[i("第 4 章 模块应该是深的")])),_:1})]),t("li",null,[u(o,{to:"/ch05.html"},{default:n(()=>l[5]||(l[5]=[i("第 5 章 信息隐藏(和泄漏)")])),_:1})]),t("li",null,[u(o,{to:"/ch06.html"},{default:n(()=>l[6]||(l[6]=[i("第 6 章 通用模块更深入")])),_:1})]),t("li",null,[u(o,{to:"/ch07.html"},{default:n(()=>l[7]||(l[7]=[i("第 7 章 不同的层,不同的抽象")])),_:1})]),t("li",null,[u(o,{to:"/ch08.html"},{default:n(()=>l[8]||(l[8]=[i("第 8 章 降低复杂性")])),_:1})]),t("li",null,[u(o,{to:"/ch09.html"},{default:n(()=>l[9]||(l[9]=[i("第 9 章 在一起更好还是分开更好?")])),_:1})]),t("li",null,[u(o,{to:"/ch10.html"},{default:n(()=>l[10]||(l[10]=[i("第 10 章 定义不存在的错误")])),_:1})]),t("li",null,[u(o,{to:"/ch11.html"},{default:n(()=>l[11]||(l[11]=[i("第 11 章 设计它两次")])),_:1})]),t("li",null,[u(o,{to:"/ch12.html"},{default:n(()=>l[12]||(l[12]=[i("第 12 章 为什么要写注释?有四个理由")])),_:1})]),t("li",null,[u(o,{to:"/ch13.html"},{default:n(()=>l[13]||(l[13]=[i("第 13 章 注释应该描述代码中不明显的内容")])),_:1})]),t("li",null,[u(o,{to:"/ch14.html"},{default:n(()=>l[14]||(l[14]=[i("第 14 章 选择的名字")])),_:1})]),t("li",null,[u(o,{to:"/ch15.html"},{default:n(()=>l[15]||(l[15]=[i("第 15 章 先写注释")])),_:1})]),t("li",null,[u(o,{to:"/ch16.html"},{default:n(()=>l[16]||(l[16]=[i("第 16 章 修改现有的代码")])),_:1})]),t("li",null,[u(o,{to:"/ch17.html"},{default:n(()=>l[17]||(l[17]=[i("第 17 章 一致性")])),_:1})]),t("li",null,[u(o,{to:"/ch18.html"},{default:n(()=>l[18]||(l[18]=[i("第 18 章 代码应该是显而易见的")])),_:1})]),t("li",null,[u(o,{to:"/ch19.html"},{default:n(()=>l[19]||(l[19]=[i("第 19 章 软件发展趋势")])),_:1})]),t("li",null,[u(o,{to:"/ch20.html"},{default:n(()=>l[20]||(l[20]=[i("第 20 章 设计性能")])),_:1})]),t("li",null,[u(o,{to:"/ch21.html"},{default:n(()=>l[21]||(l[21]=[i("第 21 章 结论")])),_:1})]),t("li",null,[u(o,{to:"/summary.html"},{default:n(()=>l[22]||(l[22]=[i("总结")])),_:1})])])])}const x=d(r,[["render",p],["__file","index.html.vue"]]),g=JSON.parse('{"path":"/","title":"目录","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734161172000},"filePathRelative":"README.md"}');export{x as comp,g as data}; diff --git a/assets/preface.html-CFq2nus8.js b/assets/preface.html-DOreJfjA.js similarity index 99% rename from assets/preface.html-CFq2nus8.js rename to assets/preface.html-DOreJfjA.js index 77a78102..2b43dacf 100644 --- a/assets/preface.html-CFq2nus8.js +++ b/assets/preface.html-DOreJfjA.js @@ -1 +1 @@ -import{_ as t,c as o,f as a,o as s}from"./app-CvqtBB8Z.js";const i={};function n(r,e){return s(),o("div",null,e[0]||(e[0]=[a('

前言

Preface

People have been writing programs for electronic computers for more than 80 years, but there has been surprisingly little conversation about how to design those programs or what good programs should look like. There has been considerable discussion about software development processes such as agile development and about development tools such as debuggers, version control systems, and test coverage tools. There has also been extensive analysis of programming techniques such as object-oriented programming and functional programming, and of design patterns and algorithms. All of these discussions have been valuable, but the core problem of software design is still largely untouched. David Parnas’ classic paper “On the Criteria to be used in Decomposing Systems into Modules” appeared in 1971, but the state of the art in software design has not progressed much beyond that paper in the ensuing 45 years.

80 多年来,人们一直在为电子计算机编写程序,但令人惊讶的是,关于如何设计这些程序或什么是好的程序应该是什么样子的讨论却很少。关于软件开发过程(如敏捷开发)和开发工具(如调试器、版本控制系统和测试覆盖工具),已经有了相当多的讨论。还广泛分析了编程技术,如面向对象编程和函数式编程,以及设计模式和算法。所有这些讨论都是有价值的,但是软件设计的核心问题在很大程度上仍然没有触及。David Parnas 的经典论文“关于将系统分解成模块的标准”发表于 1971 年,但是在随后的 45 年里,软件设计的技术水平并没有超过这篇论文。

The most fundamental problem in computer science is problem decomposition: how to take a complex problem and divide it up into pieces that can be solved independently. Problem decomposition is the central design task that programmers face every day, and yet, other than the work described here, I have not been able to identify a single class in any university where problem decomposition is a central topic. We teach for loops and object-oriented programming, but not software design.

计算机科学中最基本的问题是问题分解:如何处理复杂的问题并将其分解为可以独立解决的部分。问题分解是程序员每天都要面对的中心设计任务,但是,除了这里描述的工作之外,我还没有在任何一所大学里找到一门以问题分解为中心的课程。我们讲授循环和面向对象的程序设计,而不是软件设计。

In addition, there is a huge variation in quality and productivity among programmers, but we have made little attempt to understand what makes the best programmers so much better or to teach those skills in our classes. I have talked with several people I consider to be great programmers, but most of them had difficulty articulating specific techniques that give them their advantage. Many people assume that software design skill is an innate talent that cannot be taught. However, there is quite a bit of scientific evidence that outstanding performance in many fields is related more to high-quality practice than innate ability (see, for example, Talent is Overrated by Geoff Colvin).

此外,程序员之间在质量和生产率上存在巨大差异,但是我们几乎没有尝试去了解什么使最好的程序员变得更好,或者在我们的课堂上教授这些技能。我曾与几位我认为是优秀的程序员的人进行过交谈,但是他们中的大多数人都难以阐明赋予他们优势的特定技术。许多人认为软件设计技能是天生的天赋,无法教授。但是,有相当多的科学证据表明,许多领域的杰出表现更多地与高质量的实践有关,而不是与先天能力有关(例如,参见 Geoff Colvin 的《人才被高估》)。

For many years these issues have perplexed and frustrated me. I have wondered whether software design can be taught, and I have hypothesized that design skill is what separates great programmers from average ones. I finally decided that the only way to answer these questions was to attempt to teach a course on software design. The result is CS 190 at Stanford University. In this class I put forth a set of principles of software design. Students then work through a series of projects to assimilate and practice the principles. The class is taught in a fashion similar to a traditional English writing class. In an English class, students use an iterative process where they write a draft, get feedback, and then rewrite to make improvements. In CS 190, students develop a substantial piece of software from scratch. We then go through extensive code reviews to identify design problems, and students revise their projects to fix the problems. This allows students to see how their code can be improved by applying design principles.

多年来,这些问题使我感到困惑和沮丧。我想知道是否可以教授软件设计,并且我假设设计技巧是区分优秀程序员和普通程序员的原因。我最终决定,回答这些问题的唯一方法是尝试教授软件设计课程。结果是斯坦福大学的 CS 190。在这一节课中,我提出了一套软件设计原则。然后,学生将通过一系列项目来吸收和实践这些原理。该课程的授课方式类似于传统的英语写作课。在英语课堂上,学生使用迭代过程,在其中编写草稿,获取反馈,然后重写以进行改进。在 CS 190 中,学生从头开始开发大量软件。然后,我们将进行大量的代码审查以识别设计问题,然后学生修订其项目以解决问题。这使学生可以了解如何通过应用设计原理来改进其代码。

I have now taught the software design class three times, and this book is based on the design principles that emerged from the class. The principles are fairly high level and border on the philosophical (“Define errors out of existence”), so it is hard for students to understand the ideas in the abstract. Students learn best by writing code, making mistakes, and then seeing how their mistakes and the subsequent fixes relate to the principles.

现在,我已经教过 3 次软件设计课程,并且本书是基于该课程中出现的设计原理编写的。这些原则是相当高的水平,并且是哲学上的边界(“定义错误不再存在”),因此学生很难以抽象的方式理解这些思想。通过编写代码,犯错误,然后查看他们的错误以及后续的修正与这些原则之间的关系,学生将学得最好。

At this point you may well be wondering: what makes me think I know all the answers about software design? To be honest, I don’t. There were no classes on software design when I learned to program, and I never had a mentor to teach me design principles. At the time I learned to program, code reviews were virtually nonexistent. My ideas about software design come from personal experience writing and reading code. Over my career I have written about 250,000 lines of code in a variety of languages. I’ve worked on teams that created three operating systems from scratch, multiple file and storage systems, infrastructure tools such as debuggers, build systems, and GUI toolkits, a scripting language, and interactive editors for text, drawings, presentations, and integrated circuits. Along the way I’ve experienced firsthand the problems of large systems and experimented with various design techniques. In addition, I’ve read a considerable amount of code written by other people, which has exposed me to a variety of approaches, both good and bad.

在这一点上,您可能会想知道:是什么让我认为我知道有关软件设计的所有答案?老实说,我没有。当我学会编程时,没有关于软件设计的课程,而且我从来没有导师来教我设计原理。在我学习编程时,几乎没有代码审查。我对软件设计的想法来自于编写和阅读代码的个人经验。在我的职业生涯中,我已经用多种语言编写了大约 250,000 行代码。我曾在团队中工作过,这些团队从零开始创建了三个操作系统,多个文件和存储系统,基础结构工具(例如调试器,构建系统和 GUI 工具包),脚本语言以及用于文本,图形,演示文稿和集成电路的交互式编辑器。一路上,我亲身经历了大型系统的问题,并尝试了各种设计技术。另外,我已经阅读了很多其他人编写的代码,这使我接触到了很多方法,无论是好是坏。

Out of all of this experience, I’ve tried to extract common threads, both about mistakes to avoid and techniques to use. This book is a reflection of my experiences: every problem described here is one that I have experienced personally, and every suggested technique is one that I have used successfully in my own coding.

从所有这些经验中,我尝试提取通用线程,包括有关避免的错误和使用的技巧。本书反映了我的经验:这里描述的每个问题都是我亲身经历的,每种建议的技术都是我在自己的编码中成功使用的一种技术。

I don’t expect this book to be the final word on software design; I’m sure there are valuable techniques that I’ve missed, and some of my suggestions may turn out to be bad ideas in the long run. However, I hope that the book will start a conversation about software design. Compare the ideas in this book with your own experiences and decide for yourself whether the approaches described here really do reduce software complexity. This book is an opinion piece, so some readers will disagree with some of my suggestions. If you do disagree, try to understand why. I’m interested in hearing about things that work for you, things that don’t work, and any other ideas you may have about software design. I hope that the ensuing conversations will improve our collective understanding of software design. I will incorporate what I learn in future editions of this book.

我不希望这本书成为软件设计的定论。我敢肯定,我错过了一些有价值的技术,从长远来看,我的一些建议可能会变成坏主意。但是,我希望本书能开始有关软件设计的对话。将本书中的想法与您自己的经验进行比较,并自己决定此处介绍的方法是否确实降低了软件复杂性。这本书是一个观点,所以有些读者会不同意我的一些建议。如果您不同意,请尝试理解原因。我有兴趣了解对您有用的东西,不起作用的东西以及您可能对软件设计有任何其他想法。我希望随后的对话将增进我们对软件设计的集体理解。

The best way to communicate with me about the book is to send email to the following address:

与我交流有关这本书的最好方法是将电子邮件发送到以下地址:

software-design-book@googlegroups.com

I’m interested in hearing specific feedback about the book, such as bugs or suggestions for improvement, as well as general thoughts and experiences related to software design. I’m particularly interested in compelling examples that I can use in future editions of the book. The best examples illustrate an important design principle and are simple enough to explain in a paragraph or two. If you would like to see what other people are saying on the email address and participate in discussions, you can join the Google Group software-design-book.

我有兴趣听取有关本书的特定反馈,例如错误或改进建议,以及与软件设计相关的一般思想和经验。我对可以在本书未来版本中使用的引人注目的示例特别感兴趣。最好的示例说明了重要的设计原理,并且足够简单,可以在一两个段落中进行解释。如果您想在电子邮件地址上看到其他人在说什么并参与讨论,可以加入 Google Group software-design-book

If for some reason the software-design-book Google Group should disappear in the future, search on the Web for my home page; it will contain updated instructions for how to communicate about the book. Please don’t send book-related email to my personal email address.

如果出于某种原因该 software-design-book Google Group 将来会消失,请在 Web 上搜索我的主页;它将包含有关如何与这本书进行交流的更新说明。请不要将与图书相关的电子邮件发送到我的个人电子邮件地址。

I recommend that you take the suggestions in this book with a grain of salt. The overall goal is to reduce complexity; this is more important than any particular principle or idea you read here. If you try an idea from this book and find that it doesn’t actually reduce complexity, then don’t feel obligated to keep using it (but, do let me know about your experience; I’d like to get feedback on what works and what doesn’t).

我建议您使用本书建议时持保留态度。总体目标是降低复杂性;这比您在此处阅读的任何特定原理或想法更为重要。如果您尝试从本书中获得一个想法并发现它实际上并没有降低复杂性,那么您就不必继续使用它(但是,请让我知道您的经验;我想获得有关有效方法的反馈意见而不是)。

Many people have offered criticisms or made suggestions that improved the quality of the book. The following people offered helpful comments on various drafts of the book: Jeff Dean, Sanjay Ghemawat, John Hartman, Brian Kernighan, James Koppel, Amy Ousterhout, Kay Ousterhout, Rob Pike, Partha Ranganathan, Keith Schwartz, and Alex Snaps. Christos Kozyrakis suggested the terms “deep” and “shallow” for classes and interfaces, replacing previous terms “thick” and “thin”, which were somewhat ambiguous. I am indebted to the students in CS 190; the process of reading their code and discussing it with them has helped to crystallize my thoughts about design.

许多人提出了批评或提出建议,以提高本书的质量。以下人员对本书的各种草稿提供了有用的意见:杰夫·迪恩,桑杰·格玛瓦特,约翰·哈特曼,布莱恩·科尼根,詹姆斯·科佩尔,艾米·奥斯特豪特,凯·奥斯特豪特,罗伯·派克,帕塔·朗格纳森,基思·施瓦茨和亚历克斯·斯内普斯。Christos Kozyrakis 为类和接口建议了术语“深层”和“浅层”,代替了之前有点模糊的术语“厚”和“薄”。我很感激 CS 190 中的学生;阅读他们的代码并与他们讨论的过程有助于明确我对设计的想法。

',29)]))}const h=t(i,[["render",n],["__file","preface.html.vue"]]),l=JSON.parse('{"path":"/preface.html","title":"前言","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734160685000},"filePathRelative":"preface.md"}');export{h as comp,l as data}; +import{_ as t,c as o,f as a,o as s}from"./app-BQdhfat9.js";const i={};function n(r,e){return s(),o("div",null,e[0]||(e[0]=[a('

前言

Preface

People have been writing programs for electronic computers for more than 80 years, but there has been surprisingly little conversation about how to design those programs or what good programs should look like. There has been considerable discussion about software development processes such as agile development and about development tools such as debuggers, version control systems, and test coverage tools. There has also been extensive analysis of programming techniques such as object-oriented programming and functional programming, and of design patterns and algorithms. All of these discussions have been valuable, but the core problem of software design is still largely untouched. David Parnas’ classic paper “On the Criteria to be used in Decomposing Systems into Modules” appeared in 1971, but the state of the art in software design has not progressed much beyond that paper in the ensuing 45 years.

80 多年来,人们一直在为电子计算机编写程序,但令人惊讶的是,关于如何设计这些程序或什么是好的程序应该是什么样子的讨论却很少。关于软件开发过程(如敏捷开发)和开发工具(如调试器、版本控制系统和测试覆盖工具),已经有了相当多的讨论。还广泛分析了编程技术,如面向对象编程和函数式编程,以及设计模式和算法。所有这些讨论都是有价值的,但是软件设计的核心问题在很大程度上仍然没有触及。David Parnas 的经典论文“关于将系统分解成模块的标准”发表于 1971 年,但是在随后的 45 年里,软件设计的技术水平并没有超过这篇论文。

The most fundamental problem in computer science is problem decomposition: how to take a complex problem and divide it up into pieces that can be solved independently. Problem decomposition is the central design task that programmers face every day, and yet, other than the work described here, I have not been able to identify a single class in any university where problem decomposition is a central topic. We teach for loops and object-oriented programming, but not software design.

计算机科学中最基本的问题是问题分解:如何处理复杂的问题并将其分解为可以独立解决的部分。问题分解是程序员每天都要面对的中心设计任务,但是,除了这里描述的工作之外,我还没有在任何一所大学里找到一门以问题分解为中心的课程。我们讲授循环和面向对象的程序设计,而不是软件设计。

In addition, there is a huge variation in quality and productivity among programmers, but we have made little attempt to understand what makes the best programmers so much better or to teach those skills in our classes. I have talked with several people I consider to be great programmers, but most of them had difficulty articulating specific techniques that give them their advantage. Many people assume that software design skill is an innate talent that cannot be taught. However, there is quite a bit of scientific evidence that outstanding performance in many fields is related more to high-quality practice than innate ability (see, for example, Talent is Overrated by Geoff Colvin).

此外,程序员之间在质量和生产率上存在巨大差异,但是我们几乎没有尝试去了解什么使最好的程序员变得更好,或者在我们的课堂上教授这些技能。我曾与几位我认为是优秀的程序员的人进行过交谈,但是他们中的大多数人都难以阐明赋予他们优势的特定技术。许多人认为软件设计技能是天生的天赋,无法教授。但是,有相当多的科学证据表明,许多领域的杰出表现更多地与高质量的实践有关,而不是与先天能力有关(例如,参见 Geoff Colvin 的《人才被高估》)。

For many years these issues have perplexed and frustrated me. I have wondered whether software design can be taught, and I have hypothesized that design skill is what separates great programmers from average ones. I finally decided that the only way to answer these questions was to attempt to teach a course on software design. The result is CS 190 at Stanford University. In this class I put forth a set of principles of software design. Students then work through a series of projects to assimilate and practice the principles. The class is taught in a fashion similar to a traditional English writing class. In an English class, students use an iterative process where they write a draft, get feedback, and then rewrite to make improvements. In CS 190, students develop a substantial piece of software from scratch. We then go through extensive code reviews to identify design problems, and students revise their projects to fix the problems. This allows students to see how their code can be improved by applying design principles.

多年来,这些问题使我感到困惑和沮丧。我想知道是否可以教授软件设计,并且我假设设计技巧是区分优秀程序员和普通程序员的原因。我最终决定,回答这些问题的唯一方法是尝试教授软件设计课程。结果是斯坦福大学的 CS 190。在这一节课中,我提出了一套软件设计原则。然后,学生将通过一系列项目来吸收和实践这些原理。该课程的授课方式类似于传统的英语写作课。在英语课堂上,学生使用迭代过程,在其中编写草稿,获取反馈,然后重写以进行改进。在 CS 190 中,学生从头开始开发大量软件。然后,我们将进行大量的代码审查以识别设计问题,然后学生修订其项目以解决问题。这使学生可以了解如何通过应用设计原理来改进其代码。

I have now taught the software design class three times, and this book is based on the design principles that emerged from the class. The principles are fairly high level and border on the philosophical (“Define errors out of existence”), so it is hard for students to understand the ideas in the abstract. Students learn best by writing code, making mistakes, and then seeing how their mistakes and the subsequent fixes relate to the principles.

现在,我已经教过 3 次软件设计课程,并且本书是基于该课程中出现的设计原理编写的。这些原则是相当高的水平,并且是哲学上的边界(“定义错误不再存在”),因此学生很难以抽象的方式理解这些思想。通过编写代码,犯错误,然后查看他们的错误以及后续的修正与这些原则之间的关系,学生将学得最好。

At this point you may well be wondering: what makes me think I know all the answers about software design? To be honest, I don’t. There were no classes on software design when I learned to program, and I never had a mentor to teach me design principles. At the time I learned to program, code reviews were virtually nonexistent. My ideas about software design come from personal experience writing and reading code. Over my career I have written about 250,000 lines of code in a variety of languages. I’ve worked on teams that created three operating systems from scratch, multiple file and storage systems, infrastructure tools such as debuggers, build systems, and GUI toolkits, a scripting language, and interactive editors for text, drawings, presentations, and integrated circuits. Along the way I’ve experienced firsthand the problems of large systems and experimented with various design techniques. In addition, I’ve read a considerable amount of code written by other people, which has exposed me to a variety of approaches, both good and bad.

在这一点上,您可能会想知道:是什么让我认为我知道有关软件设计的所有答案?老实说,我没有。当我学会编程时,没有关于软件设计的课程,而且我从来没有导师来教我设计原理。在我学习编程时,几乎没有代码审查。我对软件设计的想法来自于编写和阅读代码的个人经验。在我的职业生涯中,我已经用多种语言编写了大约 250,000 行代码。我曾在团队中工作过,这些团队从零开始创建了三个操作系统,多个文件和存储系统,基础结构工具(例如调试器,构建系统和 GUI 工具包),脚本语言以及用于文本,图形,演示文稿和集成电路的交互式编辑器。一路上,我亲身经历了大型系统的问题,并尝试了各种设计技术。另外,我已经阅读了很多其他人编写的代码,这使我接触到了很多方法,无论是好是坏。

Out of all of this experience, I’ve tried to extract common threads, both about mistakes to avoid and techniques to use. This book is a reflection of my experiences: every problem described here is one that I have experienced personally, and every suggested technique is one that I have used successfully in my own coding.

从所有这些经验中,我尝试提取通用线程,包括有关避免的错误和使用的技巧。本书反映了我的经验:这里描述的每个问题都是我亲身经历的,每种建议的技术都是我在自己的编码中成功使用的一种技术。

I don’t expect this book to be the final word on software design; I’m sure there are valuable techniques that I’ve missed, and some of my suggestions may turn out to be bad ideas in the long run. However, I hope that the book will start a conversation about software design. Compare the ideas in this book with your own experiences and decide for yourself whether the approaches described here really do reduce software complexity. This book is an opinion piece, so some readers will disagree with some of my suggestions. If you do disagree, try to understand why. I’m interested in hearing about things that work for you, things that don’t work, and any other ideas you may have about software design. I hope that the ensuing conversations will improve our collective understanding of software design. I will incorporate what I learn in future editions of this book.

我不希望这本书成为软件设计的定论。我敢肯定,我错过了一些有价值的技术,从长远来看,我的一些建议可能会变成坏主意。但是,我希望本书能开始有关软件设计的对话。将本书中的想法与您自己的经验进行比较,并自己决定此处介绍的方法是否确实降低了软件复杂性。这本书是一个观点,所以有些读者会不同意我的一些建议。如果您不同意,请尝试理解原因。我有兴趣了解对您有用的东西,不起作用的东西以及您可能对软件设计有任何其他想法。我希望随后的对话将增进我们对软件设计的集体理解。

The best way to communicate with me about the book is to send email to the following address:

与我交流有关这本书的最好方法是将电子邮件发送到以下地址:

software-design-book@googlegroups.com

I’m interested in hearing specific feedback about the book, such as bugs or suggestions for improvement, as well as general thoughts and experiences related to software design. I’m particularly interested in compelling examples that I can use in future editions of the book. The best examples illustrate an important design principle and are simple enough to explain in a paragraph or two. If you would like to see what other people are saying on the email address and participate in discussions, you can join the Google Group software-design-book.

我有兴趣听取有关本书的特定反馈,例如错误或改进建议,以及与软件设计相关的一般思想和经验。我对可以在本书未来版本中使用的引人注目的示例特别感兴趣。最好的示例说明了重要的设计原理,并且足够简单,可以在一两个段落中进行解释。如果您想在电子邮件地址上看到其他人在说什么并参与讨论,可以加入 Google Group software-design-book

If for some reason the software-design-book Google Group should disappear in the future, search on the Web for my home page; it will contain updated instructions for how to communicate about the book. Please don’t send book-related email to my personal email address.

如果出于某种原因该 software-design-book Google Group 将来会消失,请在 Web 上搜索我的主页;它将包含有关如何与这本书进行交流的更新说明。请不要将与图书相关的电子邮件发送到我的个人电子邮件地址。

I recommend that you take the suggestions in this book with a grain of salt. The overall goal is to reduce complexity; this is more important than any particular principle or idea you read here. If you try an idea from this book and find that it doesn’t actually reduce complexity, then don’t feel obligated to keep using it (but, do let me know about your experience; I’d like to get feedback on what works and what doesn’t).

我建议您使用本书建议时持保留态度。总体目标是降低复杂性;这比您在此处阅读的任何特定原理或想法更为重要。如果您尝试从本书中获得一个想法并发现它实际上并没有降低复杂性,那么您就不必继续使用它(但是,请让我知道您的经验;我想获得有关有效方法的反馈意见而不是)。

Many people have offered criticisms or made suggestions that improved the quality of the book. The following people offered helpful comments on various drafts of the book: Jeff Dean, Sanjay Ghemawat, John Hartman, Brian Kernighan, James Koppel, Amy Ousterhout, Kay Ousterhout, Rob Pike, Partha Ranganathan, Keith Schwartz, and Alex Snaps. Christos Kozyrakis suggested the terms “deep” and “shallow” for classes and interfaces, replacing previous terms “thick” and “thin”, which were somewhat ambiguous. I am indebted to the students in CS 190; the process of reading their code and discussing it with them has helped to crystallize my thoughts about design.

许多人提出了批评或提出建议,以提高本书的质量。以下人员对本书的各种草稿提供了有用的意见:杰夫·迪恩,桑杰·格玛瓦特,约翰·哈特曼,布莱恩·科尼根,詹姆斯·科佩尔,艾米·奥斯特豪特,凯·奥斯特豪特,罗伯·派克,帕塔·朗格纳森,基思·施瓦茨和亚历克斯·斯内普斯。Christos Kozyrakis 为类和接口建议了术语“深层”和“浅层”,代替了之前有点模糊的术语“厚”和“薄”。我很感激 CS 190 中的学生;阅读他们的代码并与他们讨论的过程有助于明确我对设计的想法。

',29)]))}const h=t(i,[["render",n],["__file","preface.html.vue"]]),l=JSON.parse('{"path":"/preface.html","title":"前言","lang":"zh-CN","frontmatter":{},"headers":[],"git":{"updatedTime":1734161172000},"filePathRelative":"preface.md"}');export{h as comp,l as data}; diff --git a/assets/setupDevtools-7MC2TMWH-DY3SRJV5.js b/assets/setupDevtools-7MC2TMWH-Cw84T5yU.js similarity index 93% rename from assets/setupDevtools-7MC2TMWH-DY3SRJV5.js rename to assets/setupDevtools-7MC2TMWH-Cw84T5yU.js index e02454fb..76dd7139 100644 --- a/assets/setupDevtools-7MC2TMWH-DY3SRJV5.js +++ b/assets/setupDevtools-7MC2TMWH-Cw84T5yU.js @@ -1 +1 @@ -import{s as T,w as E}from"./app-CvqtBB8Z.js";var l="org.vuejs.vuepress",v="VuePress",I=v,r=l,N=v,i="client-data",a="Client Data",g=(p,n)=>{T({app:p,id:l,label:v,packageName:"@vuepress/client",homepage:"https://vuepress.vuejs.org",logo:"https://vuepress.vuejs.org/images/hero.png",componentStateTypes:[I]},t=>{const c=Object.entries(n),u=Object.keys(n),d=Object.values(n);t.on.inspectComponent(e=>{e.instanceData.state.push(...c.map(([s,o])=>({type:I,editable:!1,key:s,value:o.value})))}),t.addInspector({id:r,label:N,icon:"article"}),t.on.getInspectorTree(e=>{e.inspectorId===r&&(e.rootNodes=[{id:i,label:a,children:u.map(s=>({id:s,label:s}))}])}),t.on.getInspectorState(e=>{e.inspectorId===r&&(e.nodeId===i&&(e.state={[a]:c.map(([s,o])=>({key:s,value:o.value}))}),u.includes(e.nodeId)&&(e.state={[a]:[{key:e.nodeId,value:n[e.nodeId].value}]}))}),E(d,()=>{t.notifyComponentUpdate(),t.sendInspectorState(r)})})};export{g as setupDevtools}; +import{s as T,w as E}from"./app-BQdhfat9.js";var l="org.vuejs.vuepress",v="VuePress",I=v,r=l,N=v,i="client-data",a="Client Data",g=(p,n)=>{T({app:p,id:l,label:v,packageName:"@vuepress/client",homepage:"https://vuepress.vuejs.org",logo:"https://vuepress.vuejs.org/images/hero.png",componentStateTypes:[I]},t=>{const c=Object.entries(n),u=Object.keys(n),d=Object.values(n);t.on.inspectComponent(e=>{e.instanceData.state.push(...c.map(([s,o])=>({type:I,editable:!1,key:s,value:o.value})))}),t.addInspector({id:r,label:N,icon:"article"}),t.on.getInspectorTree(e=>{e.inspectorId===r&&(e.rootNodes=[{id:i,label:a,children:u.map(s=>({id:s,label:s}))}])}),t.on.getInspectorState(e=>{e.inspectorId===r&&(e.nodeId===i&&(e.state={[a]:c.map(([s,o])=>({key:s,value:o.value}))}),u.includes(e.nodeId)&&(e.state={[a]:[{key:e.nodeId,value:n[e.nodeId].value}]}))}),E(d,()=>{t.notifyComponentUpdate(),t.sendInspectorState(r)})})};export{g as setupDevtools}; diff --git a/assets/summary.html-eSWz3qhW.js b/assets/summary.html-UdUuxpqv.js similarity index 98% rename from assets/summary.html-eSWz3qhW.js rename to assets/summary.html-UdUuxpqv.js index 5c2ec2d3..6f2c44ab 100644 --- a/assets/summary.html-eSWz3qhW.js +++ b/assets/summary.html-UdUuxpqv.js @@ -1 +1 @@ -import{_ as r,c as a,f as n,a as l,e as t,b as o,d as s,r as d,o as u}from"./app-CvqtBB8Z.js";const m={};function p(f,e){const i=d("RouteLink");return u(),a("div",null,[e[47]||(e[47]=n('

总结

设计原则小结

Here are the most important software design principles discussed in this book:

这是本书中讨论的最重要的软件设计原则:

  1. Complexity is incremental: you have to sweat the small stuff (see p. 11).
  2. Working code isn’t enough (see p. 14).
  3. Make continual small investments to improve system design (see p. 15).
  4. Modules should be deep (see p. 22)
  5. Interfaces should be designed to make the most common usage as simple as possible (see p. 27).
  6. It’s more important for a module to have a simple interface than a simple implementation (see pp. 55, 71).
  7. General-purpose modules are deeper (see p. 39).
  8. Separate general-purpose and special-purpose code (see p. 62).
  9. Different layers should have different abstractions (see p. 45).
  10. Pull complexity downward (see p. 55).
  11. Define errors (and special cases) out of existence (see p. 79).
  12. Design it twice (see p. 91).
  13. Comments should describe things that are not obvious from the code (see p. 101).
  14. Software should be designed for ease of reading, not ease of writing (see p. 149).
  15. The increments of software development should be abstractions, not features (see p. 154).
',5)),l("ol",null,[l("li",null,[e[1]||(e[1]=t("复杂性是逐步增加的:您必须努力处理小事情(请参阅 ")),o(i,{to:"/ch02.html"},{default:s(()=>e[0]||(e[0]=[t("2.4")])),_:1}),e[2]||(e[2]=t(")。"))]),l("li",null,[e[4]||(e[4]=t("能跑起来的的代码是不够的(请参阅 ")),o(i,{to:"/ch03.html"},{default:s(()=>e[3]||(e[3]=[t("3.2")])),_:1}),e[5]||(e[5]=t(")。"))]),l("li",null,[e[7]||(e[7]=t("持续进行少量投资以改善系统设计(请参阅 ")),o(i,{to:"/ch03.html"},{default:s(()=>e[6]||(e[6]=[t("3.3")])),_:1}),e[8]||(e[8]=t(")。"))]),l("li",null,[e[10]||(e[10]=t("模块应较深(请参见 ")),o(i,{to:"/ch04.html"},{default:s(()=>e[9]||(e[9]=[t("4.4")])),_:1}),e[11]||(e[11]=t(")"))]),l("li",null,[e[13]||(e[13]=t("接口的设计应尽可能简化最常见的用法(请参阅 ")),o(i,{to:"/ch04.html"},{default:s(()=>e[12]||(e[12]=[t("4.7")])),_:1}),e[14]||(e[14]=t(")。"))]),l("li",null,[e[17]||(e[17]=t("一个模块具有一个简单的接口比一个简单的实现更重要(请参阅 ")),o(i,{to:"/ch08.html"},{default:s(()=>e[15]||(e[15]=[t("第八章")])),_:1}),e[18]||(e[18]=t(", ")),o(i,{to:"/ch09.html"},{default:s(()=>e[16]||(e[16]=[t("9.8")])),_:1}),e[19]||(e[19]=t(")。"))]),l("li",null,[e[21]||(e[21]=t("通用模块更深入(请参阅 ")),o(i,{to:"/ch06.html"},{default:s(()=>e[20]||(e[20]=[t("第六章")])),_:1}),e[22]||(e[22]=t(")。"))]),l("li",null,[e[24]||(e[24]=t("通用和专用代码分开(请参见 ")),o(i,{to:"/ch09.html"},{default:s(()=>e[23]||(e[23]=[t("9.4")])),_:1}),e[25]||(e[25]=t(")。"))]),l("li",null,[e[27]||(e[27]=t("不同的层应具有不同的抽象(请参见 ")),o(i,{to:"/ch07.html"},{default:s(()=>e[26]||(e[26]=[t("第七章")])),_:1}),e[28]||(e[28]=t(")。"))]),l("li",null,[e[30]||(e[30]=t("降低复杂度(请参阅 ")),o(i,{to:"/ch08.html"},{default:s(()=>e[29]||(e[29]=[t("第八章")])),_:1}),e[31]||(e[31]=t(")。"))]),l("li",null,[e[33]||(e[33]=t("定义不存在的错误(和特殊情况)(请参阅 ")),o(i,{to:"/ch10.html"},{default:s(()=>e[32]||(e[32]=[t("10.3")])),_:1}),e[34]||(e[34]=t(")。"))]),l("li",null,[e[36]||(e[36]=t("设计两次(请参阅 ")),o(i,{to:"/ch11.html"},{default:s(()=>e[35]||(e[35]=[t("第十一章")])),_:1}),e[37]||(e[37]=t(")。"))]),l("li",null,[e[39]||(e[39]=t("注释应描述代码中不明显的内容(请参见 ")),o(i,{to:"/ch13.html"},{default:s(()=>e[38]||(e[38]=[t("第十三章")])),_:1}),e[40]||(e[40]=t(")。"))]),l("li",null,[e[42]||(e[42]=t("软件的设计应易于阅读而不是易于编写(请参见 ")),o(i,{to:"/ch18.html"},{default:s(()=>e[41]||(e[41]=[t("18.2")])),_:1}),e[43]||(e[43]=t("))。"))]),l("li",null,[e[45]||(e[45]=t("软件开发的增量应该是抽象而不是功能(请参见 ")),o(i,{to:"/ch19.html"},{default:s(()=>e[44]||(e[44]=[t("19.2")])),_:1}),e[46]||(e[46]=t(")。"))])]),e[48]||(e[48]=n('

危险信号小结

Here are a few of of the most important red flags discussed in this book. The presence of any of these symptoms in a system suggests that there is a problem with the system’s design:

这是本书中讨论的一些最重要的危险信号。系统中任何这些症状的存在表明系统的设计存在问题:

  • Shallow Module: the interface for a class or method isn’t much simpler than its implementation (see pp. 25, 110).
  • Information Leakage: a design decision is reflected in multiple modules (see p. 31).
  • Temporal Decomposition: the code structure is based on the order in which operations are executed, not on information hiding (see p. 32).
  • Overexposure: An API forces callers to be aware of rarely used features in order to use commonly used features (see p. 36).
  • Pass-Through Method: a method does almost nothing except pass its arguments to another method with a similar signature (see p. 46).
  • Repetition: a nontrivial piece of code is repeated over and over (see p. 62).
  • Special-General Mixture: special-purpose code is not cleanly separated from general purpose code (see p. 65).
  • Conjoined Methods: two methods have so many dependencies that its hard to understand the implementation of one without understanding the implementation of the other (see p. 72).
  • Comment Repeats Code: all of the information in a comment is immediately obvious from the code next to the comment (see p. 104).
  • Implementation Documentation Contaminates Interface: an interface comment describes implementation details not needed by users of the thing being documented (see p. 114).
  • Vague Name: the name of a variable or method is so imprecise that it doesn’t convey much useful information (see p. 123).
  • Hard to Pick Name: it is difficult to come up with a precise and intuitive name for an entity (see p. 125).
  • Hard to Describe: in order to be complete, the documentation for a variable or method must be long. (see p. 131).
  • Nonobvious Code: the behavior or meaning of a piece of code cannot be understood easily. (see p. 148).
  • 浅模块:类或方法的接口并不比其实现简单得多(请参见第 25、110 页)。
  • 信息泄漏:设计决策反映在多个模块中(请参阅第 31 页)。
  • 时间分解:代码结构基于执行操作的顺序,而不是信息隐藏(请参见第 32 页)。
  • 过度暴露:API 强制调用者注意很少使用的功能,以便使用常用功能(请参阅第 36 页)。
  • Pass-Through Method:一种方法几乎不执行任何操作,只是将其参数传递给具有相似签名的另一种方法(请参见第 46 页)。
  • 重复:一遍又一遍的重复代码(请参见第 62 页)。
  • 特殊通用混合物:特殊用途代码未与通用代码完全分开(请参阅第 65 页)。
  • 联合方法:两种方法之间的依赖性很大,以至于很难理解一种方法的实现而又不理解另一种方法的实现(请参阅第 72 页)。
  • 注释重复代码:注释旁边的代码会立即显示注释中的所有信息(请参阅第 104 页)。
  • 实施文档污染了界面:界面注释描述了所记录事物的用户不需要的实施细节(请参阅第 114 页)。
  • 含糊不清的名称:变量或方法的名称过于精确,以至于它不能传达很多有用的信息(请参阅第 123 页)。
  • 难以选择的名称:很难为实体提供准确而直观的名称(请参见第 125 页)。
  • 难以描述:为了完整起见,变量或方法的文档必须很长。(请参阅第 131 页)。
  • 非显而易见的代码:一段代码的行为或含义不容易理解。(请参阅第 148 页)。

关于作者

John Ousterhout is the Bosack Lerner Professor of Computer Science at Stanford University. He is the creator of the Tcl scripting language and is also well known for his work in distributed operating systems and storage systems. Ousterhout received a BS degree in Physics from Yale University and a PhD in Computer Science from Carnegie Mellon University. He is a member of the National Academy of Engineering and has received numerous awards, including the ACM Software System Award, the ACM Grace Murray Hopper Award, the National Science Foundation Presidential Young Investigator Award, and the U.C. Berkeley Distinguished Teaching Award.

John Ousterhout 是斯坦福大学的 Bosack Lerner 计算机科学教授。他是 Tcl 脚本语言的创建者,并且以在分布式操作系统和存储系统中的工作而闻名。Ousterhout 在耶鲁大学获得了物理学学士学位,并在卡内基梅隆大学获得了计算机科学博士学位。他是美国国家工程院院士,并获得了无数奖项,包括 ACM 软件系统奖,ACM Grace Murray Hopper 奖,美国国家科学基金会总统年轻研究者奖和 UC Berkeley 杰出教学奖。

',8))])}const g=r(m,[["render",p],["__file","summary.html.vue"]]),b=JSON.parse('{"path":"/summary.html","title":"总结","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"设计原则小结","slug":"设计原则小结","link":"#设计原则小结","children":[]},{"level":2,"title":"危险信号小结","slug":"危险信号小结","link":"#危险信号小结","children":[]},{"level":2,"title":"关于作者","slug":"关于作者","link":"#关于作者","children":[]}],"git":{"updatedTime":1734160685000},"filePathRelative":"summary.md"}');export{g as comp,b as data}; +import{_ as r,c as a,f as n,a as l,e as t,b as o,d as s,r as d,o as u}from"./app-BQdhfat9.js";const m={};function p(f,e){const i=d("RouteLink");return u(),a("div",null,[e[47]||(e[47]=n('

总结

设计原则小结

Here are the most important software design principles discussed in this book:

这是本书中讨论的最重要的软件设计原则:

  1. Complexity is incremental: you have to sweat the small stuff (see p. 11).
  2. Working code isn’t enough (see p. 14).
  3. Make continual small investments to improve system design (see p. 15).
  4. Modules should be deep (see p. 22)
  5. Interfaces should be designed to make the most common usage as simple as possible (see p. 27).
  6. It’s more important for a module to have a simple interface than a simple implementation (see pp. 55, 71).
  7. General-purpose modules are deeper (see p. 39).
  8. Separate general-purpose and special-purpose code (see p. 62).
  9. Different layers should have different abstractions (see p. 45).
  10. Pull complexity downward (see p. 55).
  11. Define errors (and special cases) out of existence (see p. 79).
  12. Design it twice (see p. 91).
  13. Comments should describe things that are not obvious from the code (see p. 101).
  14. Software should be designed for ease of reading, not ease of writing (see p. 149).
  15. The increments of software development should be abstractions, not features (see p. 154).
',5)),l("ol",null,[l("li",null,[e[1]||(e[1]=t("复杂性是逐步增加的:您必须努力处理小事情(请参阅 ")),o(i,{to:"/ch02.html"},{default:s(()=>e[0]||(e[0]=[t("2.4")])),_:1}),e[2]||(e[2]=t(")。"))]),l("li",null,[e[4]||(e[4]=t("能跑起来的的代码是不够的(请参阅 ")),o(i,{to:"/ch03.html"},{default:s(()=>e[3]||(e[3]=[t("3.2")])),_:1}),e[5]||(e[5]=t(")。"))]),l("li",null,[e[7]||(e[7]=t("持续进行少量投资以改善系统设计(请参阅 ")),o(i,{to:"/ch03.html"},{default:s(()=>e[6]||(e[6]=[t("3.3")])),_:1}),e[8]||(e[8]=t(")。"))]),l("li",null,[e[10]||(e[10]=t("模块应较深(请参见 ")),o(i,{to:"/ch04.html"},{default:s(()=>e[9]||(e[9]=[t("4.4")])),_:1}),e[11]||(e[11]=t(")"))]),l("li",null,[e[13]||(e[13]=t("接口的设计应尽可能简化最常见的用法(请参阅 ")),o(i,{to:"/ch04.html"},{default:s(()=>e[12]||(e[12]=[t("4.7")])),_:1}),e[14]||(e[14]=t(")。"))]),l("li",null,[e[17]||(e[17]=t("一个模块具有一个简单的接口比一个简单的实现更重要(请参阅 ")),o(i,{to:"/ch08.html"},{default:s(()=>e[15]||(e[15]=[t("第八章")])),_:1}),e[18]||(e[18]=t(", ")),o(i,{to:"/ch09.html"},{default:s(()=>e[16]||(e[16]=[t("9.8")])),_:1}),e[19]||(e[19]=t(")。"))]),l("li",null,[e[21]||(e[21]=t("通用模块更深入(请参阅 ")),o(i,{to:"/ch06.html"},{default:s(()=>e[20]||(e[20]=[t("第六章")])),_:1}),e[22]||(e[22]=t(")。"))]),l("li",null,[e[24]||(e[24]=t("通用和专用代码分开(请参见 ")),o(i,{to:"/ch09.html"},{default:s(()=>e[23]||(e[23]=[t("9.4")])),_:1}),e[25]||(e[25]=t(")。"))]),l("li",null,[e[27]||(e[27]=t("不同的层应具有不同的抽象(请参见 ")),o(i,{to:"/ch07.html"},{default:s(()=>e[26]||(e[26]=[t("第七章")])),_:1}),e[28]||(e[28]=t(")。"))]),l("li",null,[e[30]||(e[30]=t("降低复杂度(请参阅 ")),o(i,{to:"/ch08.html"},{default:s(()=>e[29]||(e[29]=[t("第八章")])),_:1}),e[31]||(e[31]=t(")。"))]),l("li",null,[e[33]||(e[33]=t("定义不存在的错误(和特殊情况)(请参阅 ")),o(i,{to:"/ch10.html"},{default:s(()=>e[32]||(e[32]=[t("10.3")])),_:1}),e[34]||(e[34]=t(")。"))]),l("li",null,[e[36]||(e[36]=t("设计两次(请参阅 ")),o(i,{to:"/ch11.html"},{default:s(()=>e[35]||(e[35]=[t("第十一章")])),_:1}),e[37]||(e[37]=t(")。"))]),l("li",null,[e[39]||(e[39]=t("注释应描述代码中不明显的内容(请参见 ")),o(i,{to:"/ch13.html"},{default:s(()=>e[38]||(e[38]=[t("第十三章")])),_:1}),e[40]||(e[40]=t(")。"))]),l("li",null,[e[42]||(e[42]=t("软件的设计应易于阅读而不是易于编写(请参见 ")),o(i,{to:"/ch18.html"},{default:s(()=>e[41]||(e[41]=[t("18.2")])),_:1}),e[43]||(e[43]=t("))。"))]),l("li",null,[e[45]||(e[45]=t("软件开发的增量应该是抽象而不是功能(请参见 ")),o(i,{to:"/ch19.html"},{default:s(()=>e[44]||(e[44]=[t("19.2")])),_:1}),e[46]||(e[46]=t(")。"))])]),e[48]||(e[48]=n('

危险信号小结

Here are a few of of the most important red flags discussed in this book. The presence of any of these symptoms in a system suggests that there is a problem with the system’s design:

这是本书中讨论的一些最重要的危险信号。系统中任何这些症状的存在表明系统的设计存在问题:

  • Shallow Module: the interface for a class or method isn’t much simpler than its implementation (see pp. 25, 110).
  • Information Leakage: a design decision is reflected in multiple modules (see p. 31).
  • Temporal Decomposition: the code structure is based on the order in which operations are executed, not on information hiding (see p. 32).
  • Overexposure: An API forces callers to be aware of rarely used features in order to use commonly used features (see p. 36).
  • Pass-Through Method: a method does almost nothing except pass its arguments to another method with a similar signature (see p. 46).
  • Repetition: a nontrivial piece of code is repeated over and over (see p. 62).
  • Special-General Mixture: special-purpose code is not cleanly separated from general purpose code (see p. 65).
  • Conjoined Methods: two methods have so many dependencies that its hard to understand the implementation of one without understanding the implementation of the other (see p. 72).
  • Comment Repeats Code: all of the information in a comment is immediately obvious from the code next to the comment (see p. 104).
  • Implementation Documentation Contaminates Interface: an interface comment describes implementation details not needed by users of the thing being documented (see p. 114).
  • Vague Name: the name of a variable or method is so imprecise that it doesn’t convey much useful information (see p. 123).
  • Hard to Pick Name: it is difficult to come up with a precise and intuitive name for an entity (see p. 125).
  • Hard to Describe: in order to be complete, the documentation for a variable or method must be long. (see p. 131).
  • Nonobvious Code: the behavior or meaning of a piece of code cannot be understood easily. (see p. 148).
  • 浅模块:类或方法的接口并不比其实现简单得多(请参见第 25、110 页)。
  • 信息泄漏:设计决策反映在多个模块中(请参阅第 31 页)。
  • 时间分解:代码结构基于执行操作的顺序,而不是信息隐藏(请参见第 32 页)。
  • 过度暴露:API 强制调用者注意很少使用的功能,以便使用常用功能(请参阅第 36 页)。
  • Pass-Through Method:一种方法几乎不执行任何操作,只是将其参数传递给具有相似签名的另一种方法(请参见第 46 页)。
  • 重复:一遍又一遍的重复代码(请参见第 62 页)。
  • 特殊通用混合物:特殊用途代码未与通用代码完全分开(请参阅第 65 页)。
  • 联合方法:两种方法之间的依赖性很大,以至于很难理解一种方法的实现而又不理解另一种方法的实现(请参阅第 72 页)。
  • 注释重复代码:注释旁边的代码会立即显示注释中的所有信息(请参阅第 104 页)。
  • 实施文档污染了界面:界面注释描述了所记录事物的用户不需要的实施细节(请参阅第 114 页)。
  • 含糊不清的名称:变量或方法的名称过于精确,以至于它不能传达很多有用的信息(请参阅第 123 页)。
  • 难以选择的名称:很难为实体提供准确而直观的名称(请参见第 125 页)。
  • 难以描述:为了完整起见,变量或方法的文档必须很长。(请参阅第 131 页)。
  • 非显而易见的代码:一段代码的行为或含义不容易理解。(请参阅第 148 页)。

关于作者

John Ousterhout is the Bosack Lerner Professor of Computer Science at Stanford University. He is the creator of the Tcl scripting language and is also well known for his work in distributed operating systems and storage systems. Ousterhout received a BS degree in Physics from Yale University and a PhD in Computer Science from Carnegie Mellon University. He is a member of the National Academy of Engineering and has received numerous awards, including the ACM Software System Award, the ACM Grace Murray Hopper Award, the National Science Foundation Presidential Young Investigator Award, and the U.C. Berkeley Distinguished Teaching Award.

John Ousterhout 是斯坦福大学的 Bosack Lerner 计算机科学教授。他是 Tcl 脚本语言的创建者,并且以在分布式操作系统和存储系统中的工作而闻名。Ousterhout 在耶鲁大学获得了物理学学士学位,并在卡内基梅隆大学获得了计算机科学博士学位。他是美国国家工程院院士,并获得了无数奖项,包括 ACM 软件系统奖,ACM Grace Murray Hopper 奖,美国国家科学基金会总统年轻研究者奖和 UC Berkeley 杰出教学奖。

',8))])}const g=r(m,[["render",p],["__file","summary.html.vue"]]),b=JSON.parse('{"path":"/summary.html","title":"总结","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"设计原则小结","slug":"设计原则小结","link":"#设计原则小结","children":[]},{"level":2,"title":"危险信号小结","slug":"危险信号小结","link":"#危险信号小结","children":[]},{"level":2,"title":"关于作者","slug":"关于作者","link":"#关于作者","children":[]}],"git":{"updatedTime":1734161172000},"filePathRelative":"summary.md"}');export{g as comp,b as data}; diff --git a/ch01.html b/ch01.html index e1a14dcc..ac939951 100644 --- a/ch01.html +++ b/ch01.html @@ -33,11 +33,11 @@ 第 1 章 介绍 | 《软件设计的哲学》 - - + +
- + diff --git a/ch02.html b/ch02.html index 410d6c27..1c702955 100644 --- a/ch02.html +++ b/ch02.html @@ -33,11 +33,11 @@ 第 2 章 复杂性的本质 | 《软件设计的哲学》 - - + +
- + diff --git a/ch03.html b/ch03.html index 9d2b296b..1e0ce97d 100644 --- a/ch03.html +++ b/ch03.html @@ -33,11 +33,11 @@ 第 3 章 工作代码是不够的 | 《软件设计的哲学》 - - + +
- + diff --git a/ch04.html b/ch04.html index f4298d88..7dfd1516 100644 --- a/ch04.html +++ b/ch04.html @@ -33,8 +33,8 @@ 第 4 章 模块应该是深的 | 《软件设计的哲学》 - - + +
- + diff --git a/ch05.html b/ch05.html index 8851ea5d..350334aa 100644 --- a/ch05.html +++ b/ch05.html @@ -33,8 +33,8 @@ 第 5 章 信息隐藏(和泄漏) | 《软件设计的哲学》 - - + +
- + diff --git a/ch06.html b/ch06.html index 7f04c0d0..46899542 100644 --- a/ch06.html +++ b/ch06.html @@ -33,8 +33,8 @@ 第 6 章 通用模块更深入 | 《软件设计的哲学》 - - + +
- + diff --git a/ch07.html b/ch07.html index 33984c0c..095e834b 100644 --- a/ch07.html +++ b/ch07.html @@ -33,8 +33,8 @@ 第 7 章 不同的层,不同的抽象 | 《软件设计的哲学》 - - + +
- + diff --git a/ch08.html b/ch08.html index c329ae40..8dbd4af3 100644 --- a/ch08.html +++ b/ch08.html @@ -33,11 +33,11 @@ 第 8 章 降低复杂性 | 《软件设计的哲学》 - - + +
- + diff --git a/ch09.html b/ch09.html index da4590a6..8e9716b1 100644 --- a/ch09.html +++ b/ch09.html @@ -33,8 +33,8 @@ 第 9 章 在一起更好还是分开更好? | 《软件设计的哲学》 - - + +
- + diff --git a/ch10.html b/ch10.html index 222ec767..1399c658 100644 --- a/ch10.html +++ b/ch10.html @@ -33,8 +33,8 @@ 第 10 章 通过定义规避错误 | 《软件设计的哲学》 - - + +
- + diff --git a/ch11.html b/ch11.html index 81b98844..764893ac 100644 --- a/ch11.html +++ b/ch11.html @@ -33,11 +33,11 @@ 第 11 章 设计两次 | 《软件设计的哲学》 - - + +
- + diff --git a/ch12.html b/ch12.html index 49097f32..0030cf0d 100644 --- a/ch12.html +++ b/ch12.html @@ -33,11 +33,11 @@ 第 12 章 为什么要写注释?有四个理由 | 《软件设计的哲学》 - - + +
- + diff --git a/ch13.html b/ch13.html index 46ab5d16..341bda8a 100644 --- a/ch13.html +++ b/ch13.html @@ -33,8 +33,8 @@ 第 13 章 注释应该描述代码中不明显的内容 | 《软件设计的哲学》 - - + +
- + diff --git a/ch14.html b/ch14.html index 57c36850..a1bcfe19 100644 --- a/ch14.html +++ b/ch14.html @@ -33,8 +33,8 @@ 第 14 章 选择的名字 | 《软件设计的哲学》 - - + +
- + diff --git a/ch15.html b/ch15.html index b755f155..dd5ff3b4 100644 --- a/ch15.html +++ b/ch15.html @@ -33,11 +33,11 @@ 第 15 章 先写注释 | 《软件设计的哲学》 - - + +
- + diff --git a/ch16.html b/ch16.html index 0d5fd740..47fbd190 100644 --- a/ch16.html +++ b/ch16.html @@ -33,8 +33,8 @@ 第 16 章 修改现有的代码 | 《软件设计的哲学》 - - + +
- + diff --git a/ch17.html b/ch17.html index af2657df..85d6dd4d 100644 --- a/ch17.html +++ b/ch17.html @@ -33,11 +33,11 @@ 第 17 章 一致性 | 《软件设计的哲学》 - - + +
- + diff --git a/ch18.html b/ch18.html index cda0287a..042c08df 100644 --- a/ch18.html +++ b/ch18.html @@ -33,8 +33,8 @@ 第 18 章 代码应该是显而易见的 | 《软件设计的哲学》 - - + +
- + diff --git a/ch19.html b/ch19.html index ca9a0553..cf19393f 100644 --- a/ch19.html +++ b/ch19.html @@ -33,11 +33,11 @@ 第 19 章 软件发展趋势 | 《软件设计的哲学》 - - + +
- + diff --git a/ch20.html b/ch20.html index 9d838511..073572c3 100644 --- a/ch20.html +++ b/ch20.html @@ -33,11 +33,11 @@ 第 20 章 设计性能 | 《软件设计的哲学》 - - + +
- + diff --git a/ch21.html b/ch21.html index 392c50d2..70e121c1 100644 --- a/ch21.html +++ b/ch21.html @@ -33,11 +33,11 @@ 第 21 章 结论 | 《软件设计的哲学》 - - + +
- + diff --git a/index.html b/index.html index 7c8cfa13..8f2a7a1d 100644 --- a/index.html +++ b/index.html @@ -33,11 +33,11 @@ 目录 | 《软件设计的哲学》 - - + +
- + diff --git a/preface.html b/preface.html index 1e3f5744..d345b840 100644 --- a/preface.html +++ b/preface.html @@ -33,11 +33,11 @@ 前言 | 《软件设计的哲学》 - - + +
- + diff --git a/summary.html b/summary.html index 056cd3b0..a8a24e85 100644 --- a/summary.html +++ b/summary.html @@ -33,11 +33,11 @@ 总结 | 《软件设计的哲学》 - - + +
- +