首頁 > 常見問題 > 主體

初級程式設計師常犯錯一覽

發布: 2023-08-08 16:27:03
轉載
1485 人瀏覽過

最近接手其他人做的項目,導致之前的一些幻想破滅了。因為剛工作的時候做專案是php,而php本身的web框架一般只簡單區分mvc,稍微麻煩一些的會多個library或helper之類的。這樣分層很少有優點同時也有缺點。當然了,現代的框架一般支援namespace,你也完全可以藉鏡其它語言來做自己的內部框架。這裡先不說這個。

mvc的優點自然是簡單,無論一個新人有沒有做過相關的工作,你只要跟他簡單說明每一層的職責是什麼,馬上就可以開始工作。缺點也非常明顯,因為太簡單,所以程式碼在累積到一定量以後會變得難以控制複雜度。同時容易讓沒什麼經驗的程式設計師把程式碼寫得難以維護。

所以之前聽說java的框架這方面做得好一些,然後對部門的java專案抱持一些幻想。不過實際接手後,發現理想和現實真是有比較巨大的鴻溝。所以先來總結共通的初級程式設計師比較容易犯的錯誤吧。如果哪天自己帶團隊了,面試別人也可以拿這些題目作為區分人的一種界線。做專案的時候有思考的人和不思考的人還是會有不小的差別的。

命名太囉嗦,或不規範

#這個問題其實挺普遍的,即使是科班出身的人,工作了很多年的人,寫程式也可能冒出一堆var a/b/c之類的變數。如果你的程式本身短小精悍,或者只是純粹的演算法題,這樣似乎問題不大(反正oj ac了以後你可能也就再也不會看自己的程式碼。但這樣的程式碼放在生產環境中,問題就很大了。首先有些變數本身的生命週期可能會超過一個橫著放的螢幕。這裡逗逼一下,不是誰都能買得起Dell的可橫可豎的螢幕好嗎。例如按照我的13寸筆記本來看,vim的全螢幕模式下只能顯示33行(當然了,我視力不好,字體比較大)。所以如果在第一行聲明了一個變數var a。那麼順著讀下去,在33行之外。。我覺得我肯定不記得這是什麼東西了。

如果你想要聲明一個變量,一定要讓這個變量的名字本身俱有含義,比如productName,productPrice,userId,userInfo, orderHistory。但保證名字有意義的同時,一定要保證名字和其含義對應得上。並且不要把一個命名好的變量以優化的名義反復存儲各種不同的值。例如我們這裡的代碼就曾經出現過看起來是userIds,但實際上打印出來卻是一堆userInfo的信息的事情。著實讓人惱火。

業務內部邏輯的變量命名還是講究單詞打全,意義明了等等。如果是框架本身的一些內容命名的話。例如java的spring框架裡的service命名,反而應該稍微簡潔一些。例如:

KnowledgeService kService;
OrderService oService;
UserMapper uMapper;
登入後複製

因為在整個class內部,service、mapper一般只會有唯一的對象,所以這裡就算看不明白了去整個類的頭部也能找得到。因為這些對象使用頻率很高,所以簡短一些對打字有好處(其實你是想偷懶吧)。當然了,如果你喜歡把名字打全,那我覺得也沒什麼不可。

變數命名之外,函數命名也很重要

#例如上一家公司就有人會把函數叫dealData或handleData,怎麼看怎麼彆扭。。換成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,' or category_id_path = '1,345'

了。倒是问题不大,不过为什么不在path的两边再加一个逗号呢?好处留给读者去思考吧。

if/else嵌套层次

从例子开始吧:

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不判断修改前的状态

工作流/订单状态流之类的系统都会有状态流转,其实和编译原理里的状态机意思差不多。不同的状态之间跳转应该有一个基本的先置条件,从某一个固定的状态,满足某一些条件,然后跳转到下一个状态中。但实际的业务系统,却让人发现很多系统在做状态变更的时候,根本不考虑前置状态。比如下面的代码:

update xxx set status = yyy where id = zzz;
登入後複製

像这样,根本不判断前置状态的sql实在是太多了。那么实际运转起来的系统一定不会是你想象的状态流,出现了莫名其妙的跳转你会欲哭无泪。

为了解决这种问题很多系统把校验扔到前端去完成,风险很大。也就是因为这里做的都是内部系统,没有人搞破坏,所以程序员才不重视这些问题。如果你的订单/工单状态流和你公司的奖励、损失密切相关,那我觉得你不会觉得这些不是问题。

解决起来很简单,和经常被提到的高并发时的乐观锁概念差不多,留给读者去思考吧。

非单线程系统不考虑线程安全问题

这个问题其实说起来挺复杂的。。多线程程序里很难查的大多是这种问题,所以现在一般做非性能要求很高的系统都会尽量避免掉多线程并发。或者在该并发的时候再并发执行特定的任务,比如java里用future或者ExecutorService之类的来进行数据库并发查询,以减少串行执行任务的等待时间。

不过现在golang成为了搅局者,并发可能会成为未来的常态,所幸的是,golang提供了race检测工具,可以让你方便的在有race的时候程序主动崩溃(233333)。这样好歹是比C的黑暗时代进步太多了。

但在做开发的时候还是应该有这样的概念,如果有全局的map之类的变量,在访问的时候要考虑加锁。加了锁还要评估性能,性能太差还得考虑是不是要参考一些concurrentMap的实现。点点点。

是个蛮复杂的话题。

open的资源不关闭,造成句柄泄露

这个错常由php转其它语言的程序员来犯。我们php程序员open的东西从来不close(误。

如果记性不好的话,像数据库连接池,redis连接池之类的资源都会很快被耗尽。然后你就懂了。。

抱怨接口性能是语言问题

之前有这么一个程序员,在做了一个要3s才要返回的接口之后说这是贵司php版本5.3的原因,你要是让我换php 7肯定能快十倍。

然后被秒打脸。就是前面说的没有做好批量查询的问题。

這事情有時候確實是。 。態度問題。

大多數情況下語言對特定介面的效能影響不會有那麼大,所以在你向別人這麼說之前,請先簡單用日誌來記錄你的程式每一步所花費的時間為好。

做一個聰明的程式設計師~

嗯,其實我也是初級程式設計師。

#

以上是初級程式設計師常犯錯一覽的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:Golang菜鸟
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板