初级码农常犯错误

前言

很多问题是能力还是态度,还是经验,我认为每个人的成长都要犯很多错误,但是是不以为然,还是想尽办法找到解决方案进而改进,在信息爆炸的如今,对于很多问题的原因,我个人的观点是热情和态度。

命名啰嗦,不规范

禁止这类写法,因为在一个func里跟到最后我们可能完全不知道a是个什么东西,又得跳回看。

var a = 1

类型名要表达的意思要和实际大致一样,减少维护心智负担。

userId
userInfo
orderHistory

函数命名更要讲究英文和中文的准确对应,比如下面这两种,想要表达的意思是处理数据,但是handle和deal似乎在英文中和数据不是很搭配,而且一般来讲,函数对数据无非是做一个中间处理,不会产生什么终态效应。所以可以改为formatData()

func handleData() {}
func dealData() {}

魔法数字

万恶之源,现代项目应该从一开始就着手配置相关开发, config driven development

updateState(1)
updateState(stateConfig.UP)

配置分类

配置做到了,但是还有一个比较头疼的问题,配置爆炸,把所有的配置信息都放在一个conf文件,或者常量类...解决方法就是分业务逻辑,比如订单类的就放到订单下,用户类就...

go项目的话其实不推荐使用传统的MVC架构,所以根据业务逻辑层来进行配置切分更加方便。

分类的粒度

如果配置切分过于细化,反而会影响后期维护,这点主要是难以检索等问题,个人感觉倒不是细化本身是错的。

解决方案:

  • 从一开始掌握好粒度要求,比如就细分到某个业务逻辑层,不在某个业务逻辑层下再细分用户,订单...这个看项目和团队的设计。
  • 相关的配置写好comment和doc,这样检索很方便,一个配置文件名在清晰也可能会导致别人语义的理解错误还有不同模块可能细化程度不一会导致重复。(但是comment这东西在代码中就尽量要注意不要造成注释灾难,个人看老外的项目注释比较多,但是看一些软件设计相关的书籍,其实并不推荐过多的使用comment,因为本能的comment使用会导致自己并不会对代码有较高的要求,而是寄希望于comment来理解。)
  • 模块切分,同时配置互不影响。

函数写太长

最近看的《软件设计哲学》中倒是对这个观点并不完全赞同,其主要原因是很多人把小函数当成了一个死记硬背的东西,不管什么只要超过了n行就拆出一个小函数,这其实无形之中反而增加了维护成本,同时也打破了一个时间顺序。

所以拆分要注意不要将状态分离出去,时序不要改变。

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;
}

滥用回调,增加复杂性

回调在业务场景很常见,比如增加一条数据回调一个log,回调一个状态更新...其实完全可以同步来代替,回调反而在直观时序上不那么明确。

实际例子

这里有一个用户的评论系统,评论系统会对服务你的商家进行一些tag勾选和内容填空。

对于评论系统本身,只需要简单记录被打tag和被评论的对象到mysql即可。

但后面有信用系统、用户画像系统、客服系统依赖于这些评论的数据,所以需要把这些评论tag和内容同步给其它的几个系统,或者甚至是跨部门的系统。

一个提交,回调函数n个,关键是回调函数根本不是直接的逻辑,自己根本不知道写了这段逻辑产生的真正影响,所以出了问题可能又得查到别人那里,如果对方离职又是一个难题。

解决方案:时序解耦,消息队列。

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

这里就是将问题转移出去,即使出了问题,你的函数的最终作用是发送消息给消息队列,这样你只需要确认你的问题就ok了,不像之前那样n个回调都耦合在一起。再进一步将push打上log,这样监控也更方便了,当然分布式的一致性会不如之前,而且不仅要做到预警还要做好恢复方案,出了问题可快速恢复。

分层但是分层没有明确的界线

比如在某一层里,有些只是在内存操作,比如返回一个result array,但是有些直接返回一个status state给用户,直接持久化...

公共接口本身只能用一次

换个说法就是可重入不可重入。

比如你的func内引用了一个全局静态变量,而且没有加锁,那这就属于不可重入,因为几个线程一起调用不久乱了。

所以不要吝惜你的锁!尽量拒绝使用全局变量!

性能问题?你的业务真的需要考虑吗?又要搬出“过早优化是万恶之源”

设计模式滥用

设计模式并非银弹,如果做Java开发,其实现代framework做的已经蛮好了。

查询数据库不做批量

这个根据id查没问题,但是如果商品是n+个,那就意味着执行一次这个func要进行n+次数据库连接,即使有连接池,这似乎也不合理,这种要做到批量查询。

func getProductList(xxxx) {
   var products []Product
   for product := range products {
      categoryName := getCategoryName(product.getCategoryId())
      product.setCategoryName()
   }
}

if else嵌套

这个在写golang不是很常见,因为写久了就会有一种错误及时处理的概念。

同一张数据库表的查询,每换一种查询方式就写一个函数

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);

    //以下略
}

解决方案:使用sql builder等类似开源工具

工作流update不考虑修改前的state

其实就是状态机,一种状态要满足一些条件才能转移到另一种状态,但是转移前我们要确定前置状态,尤其是现在的程序大多数都是多线程环境下运行。

很常见的一种update sql,其实这问题可大了,不合理的处理方案是在其他地方对某个前置状态再查一次,的确这样可以避免前端上的错误反馈,但是问题是已经持久化了,难道还要包在一个事务里?

update xxx set status = yyy where id = zzz;

解决方案:比如我们在上面这种update时多加一个,解决方案很多,大致就是“订单流状态机乐观锁”这种关键字搜索就好了。

where status = ?

open资源不关闭

套接字,文件描述符,连接池~~~

golang就好多了,记住defer close()

抱怨!

抱怨前要有证据,如果你说是因为自身之外的原因导致的bug或者性能问题,请拿出证据,否则只能给人一种甩锅,态度问题。

结语

希望早日能成为一个合格的程序员,曹大是我的学习榜样。

学习自曹大blog

https://xargin.com/rookie-programmer-faults/

原文地址:https://www.cnblogs.com/CherryTab/p/14106008.html