腾讯 Code Review 规范出炉!你还敢乱写代码?

if key.DomainID == uint64(access.SPECIAL_FOLDER_DOMAIN_ID) &&

len(info.AccessInfos) > model.FilePrivilegeMaxFolderNum {

return errors.Errorf(errors.PrivilegeFolderLimit,

“folder owner num %d larger than limit %d”,

len(info.AccessInfos), model.FilePrivilegeMaxFolderNum)

}

if len(info.AccessInfos) > model.FilePrivilegeMaxNum {

return errors.Errorf(errors.PrivilegeUserLimit,

“file owner num %d larger than limit %d”,

len(info.AccessInfos), model.FilePrivilegeMaxNum)

}

}

pbDataSt := infoToData(info, &key)

var updateBuf []byte

if updateBuf, err = proto.Marshal(pbDataSt); err != nil {

return errors.Wrapf(err, errors.MarshalPBError,

“FilePrivilegeStore.Update Marshal data error, key[%v]”, key)

}

if err = s.setCKV(generateKey(&key), updateBuf); err != nil {

return errors.Wrapf(err, errors.Code(err),

“FilePrivilegeStore.Update setCKV error, key[%v]”, key)

}

return nil

}

现在看,这个代码挺好的,长度没超过 80 行,逻辑比价清晰。但是当 isMerge 这里判断逻辑,如果加入更多的逻辑,把局部行数撑到 50 行以上,这个函数,味道就坏了。出现两个问题:

1)函数内代码不在一个逻辑层次上,阅读代码,本来在阅读着顶层逻辑,突然就掉入了长达 50 行的 isMerge 的逻辑处理细节,还没看完,读者已经忘了前面的代码讲了什么,需要来回看,挑战自己大脑的 cache 尺寸。

2)代码有问题后,再新加代码的同学,是改还是不改前人写好的代码呢?出 bug 谁来背?这是一个灵魂拷问。

过早的优化


这个大家听了很多了,这里不赘述。

对合理性没有苛求


‘两种写法都 ok,你随便挑一种吧’,‘我这样也没什么吧’,这是我经常听到的话。

// Get 获取IP

func (i *IPGetter) Get(cardName string) string {

i.l.RLock()

ip, found := i.m[cardName]

i.l.RUnlock()

if found {

return ip

}

i.l.Lock()

var err error

ip, err = getNetIP(cardName)

if err == nil {

i.m[cardName] = ip

}

i.l.Unlock()

return ip

}

i.l.Unlock()可以放在当前的位置,也可以放在 i.l.Lock()下面,做成 defer。两种在最初构造的时候,好像都行。这个时候,很多同学态度就变得不坚决。实际上,这里必须是 defer 的。

i.l.Lock()

defer i.l.Unlock()

var err error

ip, err = getNetIP(cardName)

if err != nil {

return “127.0.0.1”

}

i.m[cardName] = ip

return ip

这样的修改,是极有可能发生的,它还是要变成 defer,那,为什么不一开始就是 defer,进入最合理的状态?不一开始就进入最合理的状态,在后续协作中,其他同学很可能犯错!

总是面向对象/总喜欢封装


我是软件工程科班出身。学的第一门编程语言是 c++。教材是这本 。当时自己读完教材,初入程序设计之门,对于里面讲的’封装’,惊为天人,多么美妙的设计啊,面向对象,多么智慧的设计啊。但是,这些年来,我看到了大牛’云风’对于’毕业生使用 mysql api 就喜欢搞个 class 封装再用’的嘲讽;看到了各种莫名其妙的 class 定义;体会到了经常要去看一个莫名其妙的继承树,必须要把整个继承树整体读明白才能确认一个细小的逻辑分支;多次体会到了我需要辛苦地压抑住自己的抵触情绪,去细度一个自作聪明的被封装的代码,确认我的 bug。除了 UI 类场景,我认为少用继承、多用组合。

template

class CSuperAction : public CSuperActionBase {

public:

typedef _PKG_TYPE pkg_type;

typedef CSuperAction<pkg_type> this_type;

}

这是 sspp 的代码。CSuperAction 和 CSuperActionBase,一会儿 super,一会儿 base,Super 和 SuperBase 是在怎样的两个抽象层次上,不通读代码,没人能读明白。我想确认任何细节,都要把多个层次的代码都通读了,有什么封装性可言?

好,你说是作者没有把 class name 取得好。那,问题是,你能取得好么?一个刚入职的 T1.2 的同学能把 class name、class 树设计得好么?即使是对简单的业务模型,也需要无数次’坏’的对象抽象实践,才能培养出一个具有合格的 class 抽象能力的同学,这对于大型却松散的团队协作,不是破坏性的?已经有了一套继承树,想要添加功能就只能在这个继承树里添加,以前的继承树不再适合新的需求,这个继承树上所有的 class,以及使用它们的地方,你都去改?不,是个正常人都会放弃,开始堆屎山。

封装,就是我可以不关心实现。但是,做一个稳定的系统,每一层设计都可能出问题。abi,总有合适的用法和不合适的用法,真的存在我们能完全不关心封装的部分是怎么实现的?不,你不能。bug 和性能问题,常常就出现在,你用了错误的用法去使用一个封装好的函数。即使是 android、ios 的 api,golang、java 现成的 api,我们常常都要去探究实现,才能把 api 用好。

那,我们是不是该一上来,就做一个透明性很强的函数,才更为合理?使用者想知道细节,进来吧,我的实现很易读,你看看就明白,使用时不会迷路!对于逻辑复杂的函数,我们还要强调函数内部工作方式’可以让读者在大脑里想象呈现完整过程’的可现性,让使用者轻松读懂,有把握,使用时,不迷路!

根本没有设计


这个最可怕,所有需求,上手就是一顿撸,'设计是什么东西?我一个文件 5w 行,一个函数 5k 行,干不完需求?'从第一行代码开始,就是无设计的,随意地踩着满地的泥坑,对于旁人的眼光没有感觉,一个人独舞,产出的代码,完成了需求,毁灭了接手自己代码的人。这个就不举例了,每个同学应该都能在自己的项目类发现这种代码。

必须形而上的思考

========

常常,同学们听演讲,公开课,就喜欢听一些细枝末节的’干活’。这没有问题。但是,你干了几年活,学习了多少干货知识点?构建起自己的技术思考’面’,进入立体的’工程思维’,把技术细节和系统要满足的需求在思考上连接起来了么?当听一个需求的时候,你能思考到自己的 code package 该怎么组织,函数该怎么组织了么?

那,技术点要怎么和需求连接起来呢?答案很简单,你需要在时间里总结,总结出一些明确的原则、思维过程。思考怎么去总结,特别像是在思考哲学问题。从一些琐碎的细节中,由具体情况上升到一些原则、公理。同时,大家在接受原则时,不应该是接受和记住原则本身,而应该是结构原则,让这个原则在自己这里重新推理一遍,自己完全掌握这个原则的适用范围。

再进一步具体地说,对于工程最佳实践的形而上的思考过程,就是:

把工程实践中遇到的问题,从问题类型和解法类型,两个角度去归类,总结出一些有限适用的原则,就从点到了面。把诸多总结出的原则,组合应用到自己的项目代码中,就是把多个面结合起来构建了一套立体的最佳实践的方案。当你这套方案能适应 30w+行代码的项目,超过 30 人的项目,你就架构师入门了!当你这个项目,是多端,多语言,代码量超过 300w 行,参与人数超过 300 人,代码质量依然很高,代码依然在高效地自我迭代,每天消除掉过时的代码,填充高质量的替换旧代码和新生的代码。

恭喜你,你已经是一个很高级的架构师了!再进一步,你对某个业务模型有独到或者全面的理解,构建了一套行业第一的解决方案,结合刚才高质量实现的能力,实现了这么一个项目。没啥好说的,你已经是专家工程师了。级别再高,我就不了解了,不在这里讨论。

那么,我们要重头开始积累思考和总结?不,有一本书叫做《unix 编程艺术》,我在不同的时期分别读了 3 遍,等一会,我讲一些里面提到的,我觉得在腾讯尤其值得拿出来说的原则。这些原则,正好就能作为 code review 时大家判定代码质量的准绳。但,在那之前,我得讲一下另外一个很重要的话题,模型设计。

model 设计

========

没读过 oauth2.0 RFC,就去设计第三方授权登陆的人,终归还要再发明一个撇脚的 oauth。

2012 年我刚毕业,我和一个去了广州联通公司的华南理工毕业生聊天。当时他说他工作很不开心,因为工作里不经常写代码,而且认为自己有 ACM 竞赛金牌级的算法熟练度+对 CPP 代码的熟悉,写下一个个指针操作内存,什么程序写不出来,什么事情做不好。当时我觉得,挺有道理,编程工具在手,我什么事情做不了?

现在,我会告诉他,复杂如 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 博士的学术能力?我无法回答。唯一回答就是,进入某个领域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通识能力,去进一步思考。至于怎么建立好的通识思考能力,可能得去常青藤读个书吧:)或者,就在工程实践中思考和锻炼自己的这个能力!

同时,基于 model 设计的代码,能更好地适应产品经理不断变更的需求。比如说,一个 calendar(日历)应用,简单来想,不要太简单!以’userid_date’为 key 记录一个用户的每日安排不就完成了么?只往前走一步,设计了一个任务,上限分发给 100w 个人,创建这么一个任务,是往 100w 个人下面添加一条记录?你得改掉之前的设计,换 db。再往前走一步,要拉出某个用户和某个人一起要参与的所有事务,是把两个人的所有任务来做 join?好像还行。如果是和 100 个人一起参与的所有任务呢?100 个人的任务来 join?不现实了吧。

好,你引入一个群组 id,那么,你最开始的’userid_date’为 key 的设计,是不是又要修改和做数据迁移了?经常来一个需求,你就得把系统推翻重来,或者根本就只能拒绝用户的需求,这样的战斗力,还好意思叫自己工程师?你一开始就应该思考自己面对的业务领域,思考自己的日历应用可能的模型边界,把可能要做的能力都拿进来思考,构建一个 model,设计一套通用的 store 层接口,基于通用接口的逻辑代码。当产品不断发展,就是不停往模型里填内容,而不是推翻重来。

这,思考模型边界,构建模型细节,就是两个很重要的能力,也是绝大多数腾讯产品经理不具备的能力,你得具备,对整个团队都是极其有益的。你面对产品经理时,就听取他们出于对用户体验负责思考出的需求点,到你自己这里,用一个完整的模型去涵盖这些零碎的点。

model 设计,是形而上思考中的一个方面,一个特别重要的方面。接下来,我们来抄袭抄袭 unix 操作系统构建的实践为我们提出的前人实践经验和’公理’总结。在自己的 coding/code review 中,站在巨人的肩膀上去思考。不重复地发现经典力学,而是往相对论挺进。

UNIX 设计哲学

=========

不懂 Unix 的人注定最终还要重复发明一个撇脚的 Unix。–Henry Spenncer, 1987.11

下面这一段话太经典,我必须要摘抄一遍(自《UNIX 编程艺术》):“工程和设计的每个分支都有自己的技术文化。在大多数工程领域中,就一个专业人员的素养组成来说,有些不成文的行业素养具有与标准手册及教科书同等重要的地位(并且随着专业人员经验的日积月累,这些经验常常会比书本更重要)。资深工程师们在工作中会积累大量的隐性知识,他们用类似禅宗’教外别传’的方式,通过言传身教传授给后辈。软件工程算是此规则的一个例外:技术变革如此之快,软件环境日新月异,软件技术文化暂如朝露。

然而,例外之中也有例外。确有极少数软件技术被证明经久耐用,足以演进为强势的技术文化、有鲜明特色的艺术和世代相传的设计哲学。“

接下来,我用我的理解,讲解一下几个我们常常做不到的原则。

Keep It Simple Stuped!


KISS 原则,大家应该是如雷贯耳了。但是,你真的在遵守?什么是 Simple?简单?golang 语言主要设计者之一的 Rob Pike 说’大道至简’,这个’简’和简单是一个意思么?

首先,简单不是面对一个问题,我们印入眼帘第一映像的解法为简单。我说一句,感受一下。"把一个事情做出来容易,把事情用最简单有效的方法做出来,是一个很难的事情。"比如,做一个三方授权,oauth2.0 很简单,所有概念和细节都是紧凑、完备、易用的。

你觉得要设计到 oauth2.0 这个效果很容易么?要做到简单,就要对自己处理的问题有全面的了解,然后需要不断积累思考,才能做到从各个角度和层级去认识这个问题,打磨出一个通俗、紧凑、完备的设计,就像 ios 的交互设计。简单不是容易做到的,需要大家在不断的时间和 code review 过程中去积累思考,pk 中触发思考,交流中总结思考,才能做得愈发地好,接近’大道至简’。

两张经典的模型图,简单又全面,感受一下,没看懂,可以立即自行 google 学习一下:RBAC:

logging:

原则 3 组合原则: 设计时考虑拼接组合


关于 OOP,关于继承,我前面已经说过了。那我们怎么组织自己的模块?对,用组合的方式来达到。linux 操作系统离我们这么近,它是怎么架构起来的?往小里说,我们一个串联一个业务请求的数据集合,如果使用 BaseSession,XXXSession inherit BaseSession 的设计,其实,这个继承树,很难适应层出不穷的变化。但是如果使用组合,就可以拆解出 UserSignature 等等各种可能需要的部件,在需要的时候组合使用,不断添加新的部件而没有对老的继承树的记忆这个心智负担。

使用组合,其实就是要让你明确清楚自己现在所拥有的是哪个部件。如果部件过于多,其实完成组合最终成品这个步骤,就会有较高的心智负担,每个部件展开来,琳琅满目,眼花缭乱。比如 QT 这个通用 UI 框架,看它的Class 列表,有 1000 多个。如果不用继承树把它组织起来,平铺展开,组合出一个页面,将会变得心智负担高到无法承受。OOP 在’需要无数元素同时展现出来’这种复杂度极高的场景,有效的控制了复杂度 。'那么,古尔丹,代价是什么呢?'代价就是,一开始做出这个自上而下的设计,牵一发而动全身,每次调整都变得异常困难。

实际项目中,各种职业级别不同的同学一起协作修改一个 server 的代码,就会出现,职级低的同学改哪里都改不对,根本没能力进行修改,高级别的同学能修改对,也不愿意大规模修改,整个项目变得愈发不合理。对整个继承树没有完全认识的同学都没有资格进行任何一个对继承树有调整的修改,协作变得寸步难行。代码的修改,都变成了依赖一个高级架构师高强度监控继承体系的变化,低级别同学们束手束脚的结果。组合,就很好的解决了这个问题,把问题不断细分,每个同学都可以很好地攻克自己需要攻克的点,实现一个 package。产品逻辑代码,只需要去组合各个 package,就能达到效果。

这是 golang 标准库里 http request 的定义,它就是 Http 请求所有特性集合出来的结果。其中通用/异变/多种实现的部分,通过 duck interface 抽象,比如 Body io.ReadCloser。你想知道哪些细节,就从组合成 request 的部件入手,要修改,只需要修改对应部件。[这段代码后,对比.NET 的 HTTP 基于 OOP 的抽象]

// 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, p 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.
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习

已经将知识体系整理好(源码,笔记,PPT,学习视频)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
[外链图片转存中…(img-KbQDuzn7-1712508498849)]

[外链图片转存中…(img-aDYcLD95-1712508498849)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我还为大家准备了一套体系化的架构师学习资料包以及BAT面试资料,供大家参考及学习

已经将知识体系整理好(源码,笔记,PPT,学习视频)

[外链图片转存中…(img-EeUBNfB6-1712508498849)]

[外链图片转存中…(img-dTeiKM7X-1712508498850)]

[外链图片转存中…(img-bsUFieTj-1712508498850)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值