최근 다른 사람이 진행한 프로젝트를 이어받으면서 이전의 환상이 깨졌습니다. 제가 처음 일을 시작할 때는 PHP로 프로젝트를 하고 있었는데, PHP 자체의 웹 프레임워크는 일반적으로 단순히 MVC만 구분하고, 조금 더 번거롭다면 라이브러리나 헬퍼가 여러 개 있을 것이기 때문입니다. 이러한 레이어링에는 장점과 단점이 거의 없습니다. 물론 현대 프레임워크는 일반적으로 네임스페이스를 지원하며, 다른 언어로부터 학습하여 자신만의 내부 프레임워크를 구축할 수도 있습니다. 여기서는 이에 대해 이야기하지 말자.
mvc의 장점은 당연히 단순하다는 것입니다. 새로운 사람이 이전에 관련 작업을 수행했는지 여부에 관계없이 각 계층의 책임이 무엇인지 간략하게 설명하면 즉시 작업을 시작할 수 있습니다. 단점도 매우 명확합니다. 너무 단순하기 때문에 코드의 복잡성이 어느 정도 쌓이면 제어하기 어려워집니다. 동시에 경험이 부족한 프로그래머가 유지 관리하기 어려운 코드를 작성하기 쉽습니다.
그래서 이 분야에서는 Java 프레임워크가 더 좋다는 말을 듣고, 학과의 Java 프로젝트에 대해 약간의 환상을 품게 되었습니다. 하지만 막상 맡아보니 이상과 현실의 괴리가 크다는 걸 깨달았다. 그럼 먼저 주니어 프로그래머들이 저지르기 쉬운 일반적인 실수를 요약해 보겠습니다. 언젠가 팀을 이끌게 된다면, 다른 사람들과 인터뷰할 때 사람들을 구별하는 방법으로 이러한 질문을 사용할 수도 있습니다. 프로젝트를 진행할 때 생각하는 사람과 생각하지 않는 사람 사이에는 여전히 큰 차이가 있습니다.
이 문제는 실제로 꽤 흔한 문제입니다. 전문적인 배경을 갖고 오랫동안 일한 사람이라도 글을 작성할 때 var a/b/가 잔뜩 나타날 수 있습니다. c와 같은 프로그램. 프로그램 자체가 짧고 간결하다면, 아니면 그냥 순수한 알고리즘 문제라면 큰 문제는 아닌 것 같습니다(어쨌든 oj ac 이후에는 자신의 코드를 절대 보지 않을 수도 있습니다. 생산 환경에서는 문제가 큽니다. 우선, 일부 변수의 수명 주기는 가로 화면의 수명을 초과할 수 있습니다. 예를 들어, 내 13에 따르면 모든 사람이 Dell의 가로 또는 세로 화면을 감당할 수 있는 것은 아닙니다. -inch 노트북. 위에서 판단하면 vim은 전체 화면 모드에서 33줄만 표시할 수 있으므로(물론 시력이 좋지 않고 글꼴도 상대적으로 큽니다) 첫 번째 줄에 변수 var a 가 선언되어 있으면 읽어보세요. 33행까지. 제외... 이게 뭔지 기억이 안나는 것 같아요
변수를 선언하려면 productName, productPrice, userInfo, orderHistory 등 변수 이름을 의미있게 만드세요. 이름에는 의미가 있지만 이름이 해당 의미와 일치하는지 확인해야 하며 최적화 이름으로 명명된 변수에 다양한 값을 반복적으로 저장하지 마십시오. . 하지만 실제로는 userInfo 정보가 잔뜩 출력됩니다.
내부 비즈니스 로직의 변수 이름 지정이 아직 완료되어야 하고, 의미도 명확해야 합니다. 예를 들어 Java Spring 프레임워크의 서비스 이름 지정은 다음과 같이 좀 더 간단해야 합니다.
KnowledgeService kService; OrderService oService; UserMapper uMapper;
서비스와 매퍼는 일반적으로 전체 클래스 내에서 고유한 개체만 가지므로 여기서는 이해하지 못하더라도 , 전체 학급의 책임자로 이동하면 모두 찾을 수 있습니다. 이러한 개체는 매우 자주 사용되므로 짧게 입력하는 것이 좋습니다(물론 그냥 게으르게 하고 싶을 뿐입니다). 이름을 전부 입력하면 괜찮을 것 같습니다. 변수 이름 지정 외에도 함수 이름 지정도 매우 중요합니다. 예를 들어 이전 회사에서는 누군가 dealData 또는 handlerData 함수를 호출했을 것입니다. formatData로 대체되나요?
这个问题在哪里都看得到,最简单的例如各种订单的status跳转。你会发现各种updateStatus(1)之类的神奇代码。如果恰巧数据库的表定义里又没有这个status的定义,那必然会变成维护人员的噩梦。解决方法很简单,在配置文件中集中维护这些特殊变量。
再进一步的话,应该引入各种方便的配置系统。例如java里常用的disconf,可以在不对系统上下线的情况下在配置系统里看到配置的key和value,并且可以即时地进行修改和配置下发。如果是其它语言没有类似系统的话,也可以借助现在流行的etcd或者zookeeper来自己开发,不会太复杂。
如果你发现自己的系统里充斥着各种不明所以的数值的话,那么就是时候考虑引入配置进行管理了。(其实从一开始就应该做这些事情)
有了配置系统以后,其实还有个麻烦的问题。就是所有配置都放在一个文件里。例如之前做的系统,把所有key和对应的sql都放在一起,然后导致配置文件变得很大很难看。想改个简单的sql连找都找不到在哪里。
改进方法很简单,第一是对配置文件进行业务逻辑分类,例如Order相关的配置项就放在Order的下面。如果配置内容还是很多,可以进一步进行粒度的细分,例如OrderHistory, OrderType之类的。其实一般的系统也不会这么复杂。
对配置进行了分类,但有时候也有可能配置粒度实在太细,导致文件很多,找起来还是很不方便。
这怎么办呢?
第一是粒度划分要掌握好度,第二是相关的配置最好有对应的comment可以拿来进行简单的检索。第三么,那可能就是将系统的模块进行拆分了。不同模块管理自己的配置文件。眼不见心不烦。
划分之后的模块是使用json http还是用rpc通信,看自己的业务量和延迟要求来定吧。
这也是常见的问题。业务开发在堆代码的阶段特别容易引起这种问题。例如上一家公司的createOrder逻辑,一开始只是80行的小函数。但后来陆续加入了商品内容校验,用户对商品的合法性校验(主要是指用户是否是跨越了购买区域啊,或者购买了本没有资格购买的vip商品),价格校验(是否优惠,优惠了多少,前端的价格有没有问题之类的)等等。然后就让事情变得越来越难以收拾,80行的开头最终变成了1000行的巨无霸。
这个又怎么解决呢?
首先是要根据功能进行简单的小函数块划分。
func ABCDE() { A B C D E } => func main() { A() B() C() D() E() } 再具体一些 func createOrder(userInfo UserInfo, products []Product) { checkUserValid(userInfo); checkProducts(products); checkUserAndProducts(userInfo, products); var order = insertOrder(userInfo, products); var createFlag = createOrderDetail(order, products); return createFlag; }
看起来就清晰多了,如果本身有上千行,进行划分之后每个函数也不会太长。
当然了,也有人会说,我的api逻辑怎么可能这么简单。有很多其它系统依赖于我当前系统的数据,我创建了订单以后要对他们的接口进行回调啊。这部分代码怎么都节省不下来吧?
其实还是可以的。
这一“点”其实就是消息队列的存在意义了。在后面的滥用回调一节中会做详述。
滥用回调其实挺常见的,特别是在现在这个公司。很多时候有数据依赖的项目会彼此之间搞一堆错综复杂的回调接口。这样在初期开发的时候因为调用是同步操作,所以看起来项目很简单,而且同步调用随时打日志,出了问题也容易排查。
但依赖项多了以后就会变成维护和重构的噩梦。
举个实际场景例子:
这里有一个用户的评论系统,评论系统会对服务你的商家进行一些tag勾选和内容填空。 对于评论系统本身,只需要简单记录被打tag和被评论的对象到mysql即可。 但后面有信用系统、用户画像系统、客服系统依赖于这些评论的数据,所以需要把这些评论tag和内容同步给其它的几个系统,或者甚至是跨部门的系统。
目前是怎么做的呢?通过回调。
回调逻辑在一个接口里超过五个之后,程序员就不知道自己的代码是干什么的了。。在现在互联网公司每年离职率这么高的情况下还会导致pm和rd都离职了以后,后来的新人根本就不敢碰这些回调的问题。本来很简单的接口,50行实现完成了自己的业务逻辑,但是几百行都是在回调别人的系统。
这个问题想要解决起来也不难,其实根本就不是该解决,这种问题在一开始就应该极力避免。可以使用消息队列来避免这个问题。我们可以回想一下人们常说的消息队列可以用来解耦。所谓解耦,其实指的就是上面这种场景。在你的系统里发生了一个事件,其它系统对这个事件的数据有依赖,那么就让他去订阅你的系统里产生的消息,这条消息只要放在队列里即可。你喜欢用kafka还是其它的消息队列其实都是可以的。
这样,变化是下面这样的:
event A happend then { call sys A1(); call sys A2(); call sys A3(); call sys A4(); call sys A5(); call sys A6(); call sys A7(); call sys A8(); ... } => event A happend then { push msg to msg queue } A1~AN subscribe topic A in msg queue
复杂性被分散去了各自的系统中。看起来是不是明朗了很多。
当然,这种方案也不是没有问题。一旦引入消息队列,那么整个系统对消息队列本身的可靠性就有一定的考验。其次,原来的同步回调变成了基于消息队列的异步分布式系统。说到这种系统,大多数人肯定会想到让人头痛的分布式事务。而一般分布式事务还就只是数据库领域说的比较多,涉及到消息队列的分布式事务相对则会再复杂一些。所幸的是现在也有一些解决这种问题的思路,虽然资料不多,感兴趣的读者可以搜索一下saga pattern。实际上即使是saga pattern也没有完美解决这种问题。
实际项目里出错的概率其实挺低的,如果push失败打失败日志报警,并及时解决的话其实大多数的系统也没有这么大的成本。只要在失败时准备好恢复的预案即可。例如一些两边系统进行时间区间内数据同步的工具。要保证一定程度的幂等。
这个在啥语言的项目里都比较多。
比如最近接触的java项目,分了entity/mapper/provider/service/controller几层。
其实是很科学的分层,但实际上在service这一层的使用上因为缺乏使用约束和规范,乱用的结果就是返回结果很混乱。例如有些函数是简单地返回了查询到的对象/对象数组,但另一些可能是直接把最终呈现给用户的business status code都进行了返回。
这样的话后来的维护者在接到需求的时候,难以根据直觉判断组合哪些小函数就能实现新功能。只能一个一个地去看你的具体函数实现。
这里似乎又体现出了设计模式和规范作为程序员共同语言的重要性,哈哈。
这个错误。。其实很简单,就是常说的可重入不可重入的概念,当年刚毕业的时候我也被很多人问到,但那个时候确实没法理解。不过其实很简单,如果你的函数在被调用时不能保证返回结果的正确性,例如使用了全局变量且没有加锁,或者使用了静态局部变量,那么这个函数就是不可重入的。但如果你的环境是php的话,这里又有一些微妙的不同。因为php本身是单线程运行,所以所谓的不可重入就只是你丫别用全局变量来存储函数计算的结果啊。。。
换到其它语言的话,其实就是尽量还是写可重入的函数,即使性能上稍微有些问题,但对于大多数的Web程序而言,你所谓的性能问题都是扯jb蛋。
这里为了不扯jb蛋,我们举个例子:
在这个部门的XX系统里有这么一段php代码: global $validUserList = array(); public static getValidUserList($users) { global $validUserList; for($users as $user) { $validUserList[] = $user; } }
代码的作者很自以为聪明地使用了一个全局的validUserList的变量。然后在getValidUserList里对该global变量进行修改。
但是关键问题是。。没有在每次调用的时候进行初始化,导致了这个函数根本就只能被调用一次的问题。如果先后调用多次,那么一定会得到匪夷所思的结果。
更关键的问题是,这样的代码竟然出现在线上运行的业务系统里(你们真的不是一个外包团队吗
这种问题改起来很简单,犯错的人只能说也比较弱智了。。
public static getValidUserList($users) { $validUserList = array(); for($users as $user) { $validUserList[] = $user; } return $validUserList; }
不要觉得每次都声明新的数组会导致性能问题。你的web程序不在乎那一点性能。
这一点可能是不太好界定的一点,用设计模式本身就是为了把重复的工作进行一次性化。但问题很多时候不在于你用了什么设计模式,而在于写这段代码的人是谁。比如有人用的明明是策略模式,但你在他的代码的字里行间都看不出来这是策略模式,只看到什么getOm(其实是莫名其妙的getObjectModel的缩写),再通过反射去找到一个写死了名字的xxxxfunction,再直接通过字符串进行函数调用(php才可以这样),读得人云里雾里。而且待你把他的代码全部扫过一遍之后才发现,虽然用了策略模式,但这段代码只有一种策略,其功能只是把数据库里的一个表的一个字段修改为一个固定的状态值。
实际上只需要三行代码就可以解决的问题。
实际上这位程序员写了多少代码呢?
1000行。。。
你是用代码量来衡量工作量的公司的员工吗?
比较典型的场景,现在大多数的web程序都可以分为列表页和详情页。。。说批量,其实主要说的就是列表页的问题。
例如一个商品列表页,里面有五十种商品,每个商品有一个分类,在商品表里只存储了分类的id,而在展示的时候需要把分类的路径和分类的名字都展示出来。
func getProductList(xxxx) { var products []Product for product := range products { categoryName := getCategoryName(product.getCategoryId()) product.setCategoryName() } }
这个问题看起来似乎问题不大?问题大了去了。你想想本来查询两次就可以解决的问题,50种商品,你就需要至少查询51次数据库。像php/py这样的语言,如果没有数据库连接池,那每次查询都需要去和数据库建立连接,这个问题就更加的严重,本来几毫秒的接口变成了几秒或者几十秒。
即使是有连接池可以复用连接的语言,你查询50次和2次的性能会一样么?
公司里有人会把存储树形结构的表设计成这样:
Field | Type | Null | Key | Default | Extra | +------------------+--------------+------+-----+---------------------+-----------------------------+ | id | int(10) | NO | PRI | NULL | auto_increment | | name | varchar(64) | NO | | | | | parent_id | int(10) | NO | | 0 | | | status | tinyint(3) | NO | | 0 | | | createtime | datetime | NO | | 0000-00-00 00:00:00 | | | modifytime | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | | category_id_path | varchar(256) | NO | | 0 | |
先看表结构,有没有什么问题?对,没有存储level信息,这种情况下如果我要找三/四级分类的所有分类id,那么就变成了一件非常麻烦不可能的事情。何必呢。
除了level之外,看起来是不是没什么问题了,那我们来看看表里存储的具体数据:
id: 3459 name: fasdfasf parent_id: 0 status: 0 createtime: 2016-10-20 17:11:12 modifytime: 2016-10-20 17:11:13 category_id_path: 1,345,2359 1 row in set (0.01 sec)
发现什么问题了么。。
对,category_id_path
这个字段,设计者很聪明地存储了一个分类从根到该category的完整路径,但是却犯了个错误。先卖个关子,在这样的表里,如果你要查询345结点(含)的所有子结点要怎么做呢?
select * from category where
category_id_path
like '1,345%'
这样显然是有问题的。如果一个结点的path是1,3456
,那么不幸的,你的查询也会把这个结点选出来。
那么我是不是可以:
select * from category where
category_id_path
like '1,345,'
看起来似乎是没问题了,那么上面要求的1,345这个结点怎么办呢。。只能
select * from category where
category_id_path
like '1,345,' orcategory_id_path
= '1,345'
了。倒是问题不大,不过为什么不在path的两边再加一个逗号呢?好处留给读者去思考吧。
从例子开始吧:
func createUser(user UserInfo) { if user.Age <= 0 { inValid = true; } else { if user.Name == "" { inValid = true } else { if user.History.Length == 0 { } else { //500行 //500行 //500行 //500行 //500行 //500行 //500行 } } } return inValid; }
把逻辑正常的业务流程放在一个巨大的else里,可能是很多人的爱好。这种情况下如果你在前面的if else有十个,那你可能翻到后面连else里面是什么东西都看不到了(超出了ide的80字的红线)。
改起来很简单:
func createUser(user UserInfo) { if user.Age <= 0 { inValid = true; return inValid } if user.Name == "" { inValid = true return inValid } if user.History.Length == 0 { inValid = true; return inValid; } //500行 return inValid; }
这样可以让你的代码神清气爽。
还是我们的线上系统,dao层有这么个mapper:
public interface CategoryMapper xxxx { @Select("select * from category where name = #{name}") public List<Category> findByName(@Param("name") String name); @Select("select * from category where id = #{id}") public List<Category> findById(@Param("id") Long id); @Select("select * from category where parentId = #{parentId}") public List<Category> findByParentId(@Param("parentId") Long parentId); @Select("select * from category where status = #{status}") public List<Category> findByStatus(@Param("status") Integer status); //以下略 }
在php的系统里也有类似的东西,model层一张表写了几十个函数,因为我们的表经常会有几十个字段嘛。
写出了这些代码的员工都是我们部门的优秀员工哦~
现在做开发应该尽量避免这种重复劳动。现在不管哪种语言,一般都会有开源的sql builder工具可以直接拿来用。这些sql builder工具一般也会有统一的查询接口,支持传入table name/order by/limit/where的map(java)/array(php)。
这种东西即使你不用开源的,自己开发其实也花不了太久。我们前一家公司的工具就是我们的同事自己完成的,也只有两百行左右。
另外还可以考虑一些代码生成器,dao层如果不是做事务、表关联查询的话,那很多时候几乎不用自己去写这方面的代码了。
如果你不这样做~那么在java里会变成你的dao层噩梦。无数的重复工作的工作量啊。。不过或许老板喜欢能狂怼代码的员工呢~
呵呵。
工作流/订单状态流之类的系统都会有状态流转,其实和编译原理里的状态机意思差不多。不同的状态之间跳转应该有一个基本的先置条件,从某一个固定的状态,满足某一些条件,然后跳转到下一个状态中。但实际的业务系统,却让人发现很多系统在做状态变更的时候,根本不考虑前置状态。比如下面的代码:
update xxx set status = yyy where id = zzz;
像这样,根本不判断前置状态的sql实在是太多了。那么实际运转起来的系统一定不会是你想象的状态流,出现了莫名其妙的跳转你会欲哭无泪。
为了解决这种问题很多系统把校验扔到前端去完成,风险很大。也就是因为这里做的都是内部系统,没有人搞破坏,所以程序员才不重视这些问题。如果你的订单/工单状态流和你公司的奖励、损失密切相关,那我觉得你不会觉得这些不是问题。
解决起来很简单,和经常被提到的高并发时的乐观锁概念差不多,留给读者去思考吧。
这个问题其实说起来挺复杂的。。多线程程序里很难查的大多是这种问题,所以现在一般做非性能要求很高的系统都会尽量避免掉多线程并发。或者在该并发的时候再并发执行特定的任务,比如java里用future或者ExecutorService之类的来进行数据库并发查询,以减少串行执行任务的等待时间。
不过现在golang成为了搅局者,并发可能会成为未来的常态,所幸的是,golang提供了race检测工具,可以让你方便的在有race的时候程序主动崩溃(233333)。这样好歹是比C的黑暗时代进步太多了。
但在做开发的时候还是应该有这样的概念,如果有全局的map之类的变量,在访问的时候要考虑加锁。加了锁还要评估性能,性能太差还得考虑是不是要参考一些concurrentMap的实现。点点点。
是个蛮复杂的话题。
这个错常由php转其它语言的程序员来犯。我们php程序员open的东西从来不close(误。
如果记性不好的话,像数据库连接池,redis连接池之类的资源都会很快被耗尽。然后你就懂了。。
之前有这么一个程序员,在做了一个要3s才要返回的接口之后说这是贵司php版本5.3的原因,你要是让我换php 7肯定能快十倍。
然后被秒打脸。就是前面说的没有做好批量查询的问题。
이것은 때때로 사실입니다. . 태도 문제.
대부분의 경우 언어가 특정 인터페이스의 성능에 미치는 영향은 그리 크지 않으므로 다른 사람에게 말하기 전에 로그를 사용하여 프로그램의 각 단계에 소요된 시간을 기록해 두시기 바랍니다.
똑똑한 프로그래머가 되세요~
사실 저도 주니어 프로그래머입니다.
위 내용은 주니어 프로그래머가 저지르는 일반적인 실수 목록의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!