谚语曰: 'Talk Is Cheap, Show Me The Code'。知易行难,知行合一难。嘴里要讲出来总是轻松,把别人讲过的话记住,组织一下语言,再讲出来,很容易。绝知此事要躬行。设计理念你可能道听途说了一些,以为自己掌握了,但是你会做么?有能力去思考、改进自己当前的实践方式和实践中的代码细节么?不客气地说,很多人仅仅是知道并且认同了某个设计理念,进而产生了一种虚假的安心感---自己的技术并不差。但是,他根本没有去实践这些设计理念,甚至根本实践不了这些设计理念,从结果来说,他懂不懂这些道理/理念,有什么差别?变成了自欺欺人。
我是软件工程科班出身。学的第一门编程语言是 c++。教材是这本 。当时自己读完教材,初入程序设计之门,对于里面讲的'封装',惊为天人,多么美妙的设计啊,面向对象,多么智慧的设计啊。但是,这些年来,我看到了大牛'云风'对于'毕业生使用 mysql api 就喜欢搞个 class 封装再用'的嘲讽;看到了各种莫名其妙的 class 定义;体会到了经常要去看一个莫名其妙的继承树,必须要把整个继承树整体读明白才能确认一个细小的逻辑分支;多次体会到了我需要辛苦地压抑住自己的抵触情绪,去细度一个自作聪明的被封装的代码,确认我的 bug。除了 UI 类场景,我认为少用继承、多用组合。
好,你说是作者没有把 class name 取得好。那,问题是,你能取得好么?一个刚入职的 T1.2 的同学能把 class name、class 树设计得好么?即使是对简单的业务模型,也需要无数次'坏'的对象抽象实践,才能培养出一个具有合格的 class 抽象能力的同学,这对于大型却松散的团队协作,不是破坏性的?已经有了一套继承树,想要添加功能就只能在这个继承树里添加,以前的继承树不再适合新的需求,这个继承树上所有的 class,以及使用它们的地方,你都去改?不,是个正常人都会放弃,开始堆屎山。
封装,就是我可以不关心实现。但是,做一个稳定的系统,每一层设计都可能出问题。abi,总有合适的用法和不合适的用法,真的存在我们能完全不关心封装的部分是怎么实现的?不,你不能。bug 和性能问题,常常就出现在,你用了错误的用法去使用一个封装好的函数。即使是 android、ios 的 api,golang、java 现成的 api,我们常常都要去探究实现,才能把 api 用好。
现在,我会告诉他,复杂如 linux 操作系统、Chromium 引擎、windows office,你做不了。原因是,他根本没进入软件工程的工程世界。不是会搬砖就能修出港珠澳大桥。但是,这么回答并不好,举证用的论据离我们太遥远了。见微知著。我现在会回答,你做不了,简单如一个权限系统,你知道怎么做么?堆积一堆逻辑层次一维展开的 if else?简单如一个共享文件管理,你知道怎么做么?堆积一堆逻辑层次一维展开的 ife lse?你联通有上万台服务器,你要怎么写一个管理平台?堆积一堆逻辑层次一维展开的 ife lse?
上来就是干,能实现上面提到的三个看似简单的需求?想一想,亚马逊、阿里云折腾了多少年,最后才找到了容器+Kubernetes 的大杀器。这里,需要谷歌多少年在 BORG 系统上的实践,提出了优秀的服务编排领域模型。权限领域,有 RBAC、DAC、MAC 等等模型,到了业务,又会有细节的不同。如 Domain Driven Design 说的,没有良好的领域思考和模型抽象,逻辑复杂度就是 n^2 指数级的,你得写多少 ifelse,得思考多少可能的 if 路径,来 cover 所有的不合符预期的情况。你必须要有 Domain 思考探索、model 拆解/抽象/构建的能力。
有人问过我,要怎么有效地获得这个能力?这个问题我没能回答,就像是在问我,怎么才能获得 MIT 博士的学术能力?我无法回答。唯一回答就是,进入某个领域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通识能力,去进一步思考。至于怎么建立好的通识思考能力,可能得去常青藤读个书吧:)或者,就在工程实践中思考和锻炼自己的这个能力!
实际项目中,各种职业级别不同的同学一起协作修改一个 server 的代码,就会出现,职级低的同学改哪里都改不对,根本没能力进行修改,高级别的同学能修改对,也不愿意大规模修改,整个项目变得愈发不合理。对整个继承树没有完全认识的同学都没有资格进行任何一个对继承树有调整的修改,协作变得寸步难行。代码的修改,都变成了依赖一个高级架构师高强度监控继承体系的变化,低级别同学们束手束脚的结果。组合,就很好的解决了这个问题,把问题不断细分,每个同学都可以很好地攻克自己需要攻克的点,实现一个 package。产品逻辑代码,只需要去组合各个 package,就能达到效果。
// A Request represents an HTTP request received by a server // or to be sent by a client. //
// The field semantics differ slightly between client and server // usage. In addition to the notes on the fields below, see the // documentation for Request.Write and RoundTripper. type Request struct { // Method specifies the HTTP method (GET, POST, PUT, etc.). // For client requests, an empty string means GET. // // Go's HTTP client does not support sending a request with // the CONNECT method. See the documentation on Transport for // details. Method string
// URL specifies either the URI being requested (for server // requests) or the URL to access (for client requests). // // For server requests, the URL is parsed from the URI // supplied on the Request-Line as stored in RequestURI. For // most requests, fields other than Path and RawQuery will be // empty. (See RFC 7230, Section 5.3) // // For client requests, the URL's Host specifies the server to // connect to, while the Request's Host field optionally // specifies the Host header value to send in the HTTP // request. URL *url.URL
// The protocol version for incoming server requests. // // For client requests, these fields are ignored. The HTTP // client code always uses either HTTP/1.1 or HTTP/2. // See the docs on Transport for details. Proto string // "HTTP/1.0" ProtoMajor int// 1 ProtoMinor int// 0
// Header contains the request header fields either received // by the server or to be sent by the client. // // If a server received a request with header lines, // // Host: example.com // accept-encoding: gzip, deflate // Accept-Language: en-us // fOO: Bar // foo: two // // then // // Header = map[string][]string{ // "Accept-Encoding": {"gzip, deflate"}, // "Accept-Language": {"en-us"}, // "Foo": {"Bar", "two"}, // } // // For incoming requests, the Host header is promoted to the // Request.Host field and removed from the Header map. // // HTTP defines that header names are case-insensitive. The // request parser implements this by using CanonicalHeaderKey, // making the first character and any characters following a // hyphen uppercase and the rest lowercase. // // For client requests, certain headers such as Content-Length // and Connection are automatically written when needed and // values in Header may be ignored. See the documentation // for the Request.Write method. Header Header
// Body is the request's body. // // For client requests, a nil body means the request has no // body, such as a GET request. The HTTP Client's Transport // is responsible for calling the Close method. // // For server requests, the Request Body is always non-nil // but will return EOF immediately when no body is present. // The Server will close the request body. The ServeHTTP // Handler does not need to. Body io.ReadCloser
// GetBody defines an optional func to return a new copy of // Body. It is used for client requests when a redirect requires // reading the body more than once. Use of GetBody still // requires setting Body. // // For server requests, it is unused. GetBody func()(io.ReadCloser, error)
// ContentLength records the length of the associated content. // The value -1 indicates that the length is unknown. // Values >= 0 indicate that the given number of bytes may // be read from Body. // // For client requests, a value of 0 with a non-nil Body is // also treated as unknown. ContentLength int64
// TransferEncoding lists the transfer encodings from outermost to // innermost. An empty list denotes the "identity" encoding. // TransferEncoding can usually be ignored; chunked encoding is // automatically added and removed as necessary when sending and // receiving requests. TransferEncoding []string
// Close indicates whether to close the connection after
// replying to this request (for servers) or after sending this // request and reading its response (for clients). // // For server requests, the HTTP server handles this automatically // and this field is not needed by Handlers. // // For client requests, setting this field prevents re-use of // TCP connections between requests to the same hosts, as if // Transport.DisableKeepAlives were set. Close bool
// For server requests, Host specifies the host on which the // URL is sought. For HTTP/1 (per RFC 7230, section 5.4), this // is either the value of the "Host" header or the host name // given in the URL itself. For HTTP/2, it is the value of the // ":authority" pseudo-header field. // It may be of the form "host:port". For international domain // names, Host may be in Punycode or Unicode form. Use // golang.org/x/net/idna to convert it to either format if // needed. // To prevent DNS rebinding attacks, server Handlers should // validate that the Host header has a value for which the // Handler considers itself authoritative. The included // ServeMux supports patterns registered to particular host // names and thus protects its registered Handlers. // // For client requests, Host optionally overrides the Host // header to send. If empty, the Request.Write method uses // the value of URL.Host. Host may contain an international // domain name. Host string
// Form contains the parsed form data, including both the URL // field's query parameters and the PATCH, POST, or PUT form data. // This field is only available after ParseForm is called. // The HTTP client ignores Form and uses Body instead. Form url.Values
// PostForm contains the parsed form data from PATCH, POST // or PUT body parameters. // // This field is only available after ParseForm is called. // The HTTP client ignores PostForm and uses Body instead. PostForm url.Values
// MultipartForm is the parsed multipart form, including file uploads. // This field is only available after ParseMultipartForm is called. // The HTTP client ignores MultipartForm and uses Body instead. MultipartForm *multipart.Form
// Trailer specifies additional headers that are sent after the request // body. // // For server requests, the Trailer map initially contains only the // trailer keys, with nil values. (The client declares which trailers it // will later send.) While the handler is reading from Body, it must // not reference Trailer. After reading from Body returns EOF, Trailer // can be read again and will contain non-nil values, if they were sent // by the client. // // For client requests, Trailer must be initialized to a map containing // the trailer keys to later send. The values may be nil or their final // values. The ContentLength must be 0 or -1, to send a chunked request. // After the HTTP request is sent the map values can be updated while // the request body is read. Once the body returns EOF, the caller must // not mutate Trailer. // // Few HTTP clients, servers, or proxies support HTTP trailers. Trailer Header
// RemoteAddr allows HTTP servers and other software to record // the network address that sent the request, usually for // logging. This field is not filled in by ReadRequest and // has no defined format. The HTTP server in this package // sets RemoteAddr to an "IP:port" address before invoking a // handler. // This field is ignored by the HTTP client. RemoteAddr string
// RequestURI is the unmodified request-target of the // Request-Line (RFC 7230, Section 3.1.1) as sent by the client // to a server. Usually the URL field should be used instead. // It is an error to set this field in an HTTP client request. RequestURI string
// TLS allows HTTP servers and other software to record // information about the TLS connection on which the request // was received. This field is not filled in by ReadRequest. // The HTTP server in this package sets the field for // TLS-enabled connections before invoking a handler; // otherwise it leaves the field nil. // This field is ignored by the HTTP client. TLS *tls.ConnectionState
// Cancel is an optional channel whose closure indicates that the client // request should be regarded as canceled. Not all implementations of // RoundTripper may support Cancel. // // For server requests, this field is not applicable. // // Deprecated: Set the Request's context with NewRequestWithContext // instead. If a Request's Cancel field and context are both // set, it is undefined whether Cancel is respected. Cancel {}
// Response is the redirect response which caused this request // to be created. This field is only populated during client // redirects. Response *Response
// ctx is either the client or server context. It should only // be modified via copying the whole Request using WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers of the same request. ctx context.Context }
看看.NET 里对于 web 服务的抽象,仅仅看到末端,不去看完整个继承树的完整图景,我根本无法知道我关心的某个细节在什么位置。进而,我要往整个 http 服务体系里修改任何功能,都无法抛开对整体完整设计的理解和熟悉,还极容易没有知觉地破坏者整体的设计。
说到组合,还有一个关系很紧密的词,叫插件化。大家都用 vscode 用得很开心,它比 visual studio 成功在哪里?如果 vscode 通过添加一堆插件达到 visual studio 具备的能力,那么它将变成另一个和 visual studio 差不多的东西,叫做 vs studio 吧。大家应该发现问题了,我们很多时候其实并不需要 visual studio 的大多数功能,而且希望灵活定制化一些比较小众的能力,用一些小众的插件。甚至,我们希望选择不同实现的同类型插件。这就是组合的力量,各种不同的组合,它简单,却又满足了各种需求,灵活多变,要实现一个插件,不需要事先掌握一个庞大的体系。体现在代码上,也是一样的道理。至少后端开发领域,组合,比 OOP,'香'很多。
我们要写好程序,减少 bug,就要增强自己对代码的控制力。你始终做到,理解自己调用的函数/复用的代码大概是怎么实现的。不然,你可能就会在单线程状态机的 server 里调用有 IO 阻塞的函数,让自己的 server 吞吐量直接掉到底。进而,为了保证大家能对自己代码能做到有控制力,所有人写的函数,就必须具备很高的透明性。而不是写一些看了一阵看不明白的函数/代码,结果被迫使用你代码的人,直接放弃了对掌控力的追取,甚至放弃复用你的代码,另起炉灶,走向了'制造重复代码'的深渊。