注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

纯C文件推理Llama 2

web
这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的...
继续阅读 »

这段项目可以让你通过PyTorch从头开始训练Llama 2 LLM架构模型,然后将权重保存到一个原始二进制文件中,再将其加载到一个仅有500行的简单C文件(run.c)中,该文件推断模型,目前仅支持fp32。在作者的云Linux开发平台上,一个维度为288的6层6头模型(约15M个参数)推断速度约为每秒100个令牌;在M1 MacBook Air上推断速度也差不多。作者有些惊喜地发现,采用这种简单方法,可以以高度交互的速度运行相当大的模型(几千万个参数)。




参考文献:
https://github.com/karpathy/llama2.c

作者:阿升
来源:mdnice.com/writing/6f98f171b14e4050bf627afe59ccb82a

收起阅读 »

也许跟大家不太一样,我是这么用TypeScript来写前端的

web
一、当前一些写前端的骚操作 先罗列一下见到过的一些写法吧:) 1. interface(或Type)一把梭 掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样: type User = { ...
继续阅读 »

一、当前一些写前端的骚操作


先罗列一下见到过的一些写法吧:)


1. interface(或Type)一把梭


掘金上很多文章,一提到 TypeScript,那不得先用 interface 或者 type 来声明个数据结构吗?像这样:


type User = {
nickname: string
avatar?: string
age: number
}

interface User {
nickname: string
avatar?: string
age: number
}

然后其他方法限制下入参类型,搞定,我掌握了 TypeScript 了,工资不得给我涨3000???



这里说明一下, 我司 不允许 直接使用 interface type 来定义非装饰器参数和配置性参数之外其他 任何数据类型



2. 类型体操整花活


要么把属性整成只读了,要么猪狗类型联合了,要么猪尾巴搞丢了,要么牛真的会吹牛逼了。


类型体操确实玩出了很多花活。 昨天说过了:TypeScript最好玩的就是类型体操, 也恰好是最不应该出现的东西


3. hook 的无限神话


不知道什么时候开始,hook 越来越流行。 听说不会写 hook 的前端程序员,已经算不上高阶程序员了, 不 use 点啥都展示不出牛逼的水平。


4. axios 拦截大法好


随便搜索一下 axios 的文章, 没有 拦截器 这个关键词的文章都算不上 axios 的高端用法了。


二、 我们一些不太一样的前端骚操作


昨天的文章有提到一些关于在前端使用 装饰器 来实现一些基于配置的需求实现, 今天其实想重点聊一聊如何在前端优雅的面向对象。


写过 JavaSpringBootJPA 等代码的后端程序员应该非常熟悉的一些概念:



  • 抽象: 万物都可抽象成相关的类和对象

  • 面向对象: 继承、封装、多态等特性的面向对象设计思维

  • 切面: 没有什么是切一刀解决不了的,如果一刀不行, 那就多来几刀。

  • 注解: 没有什么常量是不能使用注解来配置的, 也没有什么注解是切面想切还能躲得掉的

  • 反射: 没有什么是暴力拿取会失败的, 即使失败也没有异常是丢不出来的

  • 实体: 没有什么是不能抽象到实体上的, 万物皆唯一。

  • 很多: 还有很多,以上描述比较主观和随意。


于是我们开始把后端思维往前端来一个个的转移:)


1. 抽象和面向对象


与后端的交互数据对象、 请求的API接口都给抽象到具体的类上去,于是有了:



  • Service API请求类


abstract class AbstractService{
// 实现一个抽象属性 让子类们实现
abstract baseUrl!: string

// 再实现一些通用的 如增删改查之类的网络请求
// save()

// getDetail()

// deleteById()

// select()

// page()

// disabled()

// ......
}


  • Entity 数据实体基类


abstract class AbstractBaseEntityextends AbstractService> {
abstract service!: AbstractService

// 任何数据都是唯一的 ID
id!: number

// 再来实现一些数据实体的更新和删除方法
save(){
await service.save(this.toJson())
Notify.success("新增成功")
}

delete(){
service.deleteById(this.id)
Notify.success("删除成功")
}

async validate(scene: EntityScene):Promise<void>{
return new Promise((resolve,reject)=>{
// 多场景的话 可以Switch
if(...){
Notify.error("XXX校验失败")
reject();
}
resove();
})
}
// ......
}



  • 子类的实现:)


class UserEntity extends AbstractUserEntity<UserService>{
service = new UserService()

nickname!: string
age!: number
avatar?: string

// 用户是否成年人
isAdult(): boolean{
return this.age >= 18
}

async validate(scene: EntityScene): Promise<void> {
return new Promise((resove,reject)=>{
if(!this.isAdult()){
Notify.error("用户未成年, 请确认年龄")
reject();
}
await super.validate(scene)
})
}

}


  • View 视图调用


<template>
<el-input v-model="user.nickname"/>
<el-button @click="onUserSave()">创建用户el-button>
template>
<script setup lang="ts">
const user = ref(new UserEntity())
async function onUserSave(){
await user.validate(EntityScene.SAVE);
await user.save()
}
script>

2. 装饰器/切面/反射


装饰器部分的话,昨天的文章有提到一些了,今天主要所说反射和切面部分。


TypeScript 中, 其实装饰器本身就可以理解为一个切面了, 这里与 Java 中还是有很多不同的, 但概念和思维上是基本一致的。


反射 ReflectTypeScript 中比较坑的一个存在, 目前主要是依赖 reflect-metadata 这个第三方库来实现, 将一些元数据存储到 metadata 中, 在需要使用的时候通过反射的方式来获取。 可以参考这篇文章:TypeScript 中的元数据以及 reflect-metadata 实现原理分析


在实际使用中, 我们早前用的是 class-transformer 这个库, 之前我对这个库的评价应该是非常高的: “如果没有 class-transformer 这个库, TypeScript 狗都不写。”


确实很棒的一个库,但是在后来,我们写了个通用的内部框架, 为了适配 微信小程序端 以及 uniapp 端, 再加上有一些特殊的业务功能以及 class-transfromer 的写法和命名方式我个人不太喜欢的种种原因, 我们放弃了这个库, 但我们仿照了它的思想重新实现了一个内部使用的库,做了一些功能的阉割和新特性的添加。


核心功能的一些说明




  • 通过反射进行数据转换



    如将后端API返回的数据按照前端的数据结构强制进行转换, 当后端数据返回乱七八糟的时候,保证前端数据在使用中不会出现任何问题, 如下 demo



    class UserEntity {
    @Type(String) phone!: string;
    @Type(RoleEntity) roleInfo!: RoleEntity:
    @Type(DeptEntity) @List @Default([]) deptInfoList!: DeptEntity[]
    @Type(Boolean) @Default(false) isDisabled!: boolean
    }



  • 通过反射进行配置的存储和读取



    这个在昨天的文章中有讲到一部分, 比如配置表单、表格、搜索框、权限 等





3. 再次强调面向对象


为了整个前端项目的工程化、结构化、高度抽象化,这里不得不再次强调面向对象的设计:)




  • 这是个拼爹的社会



    一些通用的功能,一旦有复用的可能, 都可以考虑和尝试让其父类进行实现, 如需要子类传入一些特性参数时, 可以使用抽象方法或抽象属性(这可是Java中没有的)来传入父类实现过程中需要的特性参数。





  • 合理的抽象分层



    将一些特性按照不同的抽象概念进行组合与抽离,实现每个类的功能都是尽可能不耦合,实现类的单一职责。如存在多继承, 在考虑实现类的实现成本前提下,可考虑抽象到接口 interface 中。





  • 还有很多,有空再一一列举




4. 严格但又有趣的 tsdoc


我们先来看一些注释的截图吧:)








一些详细的注释、弃用的方法、选填的参数、传入参数后可能影响或依赖的其他参数,在注释里写好玩的 emoji或者图片,甚至是 直接在注释里写调用 demo, 让调用方可以很轻松愉快的对接调用, 玩归玩, 确实对整体项目的质量有很大的帮助。


三、 写在最后


中午跟同事吃饭聊了聊现在国内大前端的一个状态, 当时聊到一个关键词 舒适区, 还有前端整个技术栈过于灵活的一些优缺点, 几个大老爷们都发出了一些感慨, 如果前端能够更标准化一些, 像 Java 一样, 说不定前端还能上升几个高度。


我们基于今天文章里的一些设计写了一些DEMO,但目前不太方便直接开源,如果有兴趣,可以私聊我获取代码链接。


是的, 我还是那个 Java 仔, 是, 也不仅仅是。

作者:Hamm
来源:juejin.cn/post/7259562014417813564

收起阅读 »

请自信的大声告诉面试官forEach跳不出循环

web
如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!! foreach 跳不出循环 为什么呢? 先看看foreach大体实现。 Array.prototype.customForEach = function (fn...
继续阅读 »

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!


foreach 跳不出循环


为什么呢?


先看看foreach大体实现。


Array.prototype.customForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i, this)
}
}

list.customForEach((item, i, list) => {
console.log(item, i, list)
})

let list = [1,2,3,4,5]

list.forEach((item,index,list)=>{
console.log(item,index,list)
})

list.customForEach((item,index,list)=>{
console.log(item,index,list)
})




两个输出的结果是一样的没啥问题,这就是foreach的大体实现,既然都知道了它的实现,那么对它为什么跳不出循环♻️应该都知道了,再不清楚的话,再看一下下面的例子。



function demo(){
return 'demo'
}

function demo2(){
demo()
return 'demo2'
}

demo()


在demo2函数里面调用demo函数,demo函数的return能阻止demo2函数下面的执行吗?很明显不行啊,demo函数里的return跟demo2函数一点关系都没有。现在你再回头看看foreach的实现,就明白它跳不出循环一清二楚了。


有点同学说不是可以通过抛出错误跳出循环吗?是的。看看下面例子。



let list = [1,2,3,4,5]

try {
list.forEach((item, index, list) => {
if (index === 2) {
throw new Error('demo')
}
console.log(item)
})
} catch (e) {
// console.log(e)
}




结果是我们想要,但是你看代码,哪个正常人会这样写代码?是非foreach不用吗?还是其他的循环关键字不配呢。


end


有反驳在评论区,show me your code !!!!!!!!!


作者:啥也不懂的前端
来源:juejin.cn/post/7259595485090906149
收起阅读 »

这次被 foreach 坑惨了,再也不敢乱用了...

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)<...
继续阅读 »

近日,项目中有一个耗时较长的Job存在CPU占用过高的问题,经排查发现,主要时间消耗在往MyBatis中批量插入数据。mapper configuration是用foreach循环做的,差不多是这样。(由于项目保密,以下代码均为自己手写的demo代码)

<insert id="batchInsert" parameterType="java.util.List">  
insert into USER (id, name) values
<foreach collection="list" item="model" index="index" separator=",">
(#{model.id}, #{model.name})
</foreach>
</insert>

这个方法提升批量插入速度的原理是,将传统的:

INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");  
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");
INSERT INTO `table1` (`field1`, `field2`) VALUES ("data1", "data2");

转化为:

INSERT INTO `table1` (`field1`, `field2`)   
VALUES ("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2"),
("data1", "data2");

在MySql Docs中也提到过这个trick,如果要优化插入速度时,可以将许多小型操作组合到一个大型操作中。理想情况下,这样可以在单个连接中一次性发送许多新行的数据,并将所有索引更新和一致性检查延迟到最后才进行。

乍看上去这个foreach没有问题,但是经过项目实践发现,当表的列数较多(20+),以及一次性插入的行数较多(5000+)时,整个插入的耗时十分漫长,达到了14分钟,这是不能忍的。在资料中也提到了一句话:

Of course don't combine ALL of them, if the amount is HUGE. Say you have 1000 rows you need to insert, then don't do it one at a time. You shouldn't equally try to have all 1000 rows in a single query. Instead break it into smaller sizes.

它强调,当插入数量很多时,不能一次性全放在一条语句里。可是为什么不能放在同一条语句里呢?这条语句为什么会耗时这么久呢?我查阅了资料发现:

Insert inside Mybatis foreach is not batch, this is a single (could become giant) SQL statement and that brings drawbacks:

some database such as Oracle here does not support.

in relevant cases: there will be a large number of records to insert and the database configured limit (by default around 2000 parameters per statement) will be hit, and eventually possibly DB stack error if the statement itself become too large.

Iteration over the collection must not be done in the mybatis XML. Just execute a simple Insertstatement in a Java Foreach loop. The most important thing is the session Executor type.

SqlSession session = sessionFactory.openSession(ExecutorType.BATCH);
for (Model model : list) {
session.insert("insertStatement", model);
}
session.flushStatements();

Unlike default ExecutorType.SIMPLE, the statement will be prepared once and executed for each record to insert.

从资料中可知,默认执行器类型为Simple,会为每个语句创建一个新的预处理语句,也就是创建一个PreparedStatement对象。

在我们的项目中,会不停地使用批量插入这个方法,而因为MyBatis对于含有的语句,无法采用缓存,那么在每次调用方法时,都会重新解析sql语句。

Internally, it still generates the same single insert statement with many placeholders as the JDBC code above.

MyBatis has an ability to cache PreparedStatement, but this statement cannot be cached because it containselement and the statement varies depending on the parameters. As a result, MyBatis has to 1) evaluate the foreach part and 2) parse the statement string to build parameter mapping [1] on every execution of this statement. And these steps are relatively costly process when the statement string is big and contains many placeholders.

[1] simply put, it is a mapping between placeholders and the parameters.

从上述资料可知,耗时就耗在,由于我foreach后有5000+个values,所以这个PreparedStatement特别长,包含了很多占位符,对于占位符和参数的映射尤其耗时。并且,查阅相关资料可知,values的增长与所需的解析时间,是呈指数型增长的。

图片

所以,如果非要使用 foreach 的方式来进行批量插入的话,可以考虑减少一条 insert 语句中 values 的个数,最好能达到上面曲线的最底部的值,使速度最快。一般按经验来说,一次性插20~50行数量是比较合适的,时间消耗也能接受。

重点来了。上面讲的是,如果非要用的方式来插入,可以提升性能的方式。而实际上,MyBatis文档中写批量插入的时候,是推荐使用另外一种方法。(可以看
http://www.mybatis.org/mybatis-dyn… Insert Support 标题里的内容)

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);  
try {
SimpleTableMapper mapper = session.getMapper(SimpleTableMapper.class);
List<SimpleTableRecord> records = getRecordsToInsert(); // not shown

BatchInsert<SimpleTableRecord> batchInsert = insert(records)
.into(simpleTable)
.map(id).toProperty("id")
.map(firstName).toProperty("firstName")
.map(lastName).toProperty("lastName")
.map(birthDate).toProperty("birthDate")
.map(employed).toProperty("employed")
.map(occupation).toProperty("occupation")
.build()
.render(RenderingStrategy.MYBATIS3);

batchInsert.insertStatements().stream().forEach(mapper::insert);

session.commit();
} finally {
session.close();
}

即基本思想是将 MyBatis session 的 executor type 设为 Batch ,然后多次执行插入语句。就类似于JDBC的下面语句一样。

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=UTF-8&useServerPrepStmts=false&rewriteBatchedStatements=true","root","root");  
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement(
"insert into tb_user (name) values(?)");
for (int i = 0; i < stuNum; i++) {
ps.setString(1,name);
ps.addBatch();
}
ps.executeBatch();
connection.commit();
connection.close();

经过试验,使用了 ExecutorType.BATCH 的插入方式,性能显著提升,不到 2s 便能全部插入完成。

总结一下

如果MyBatis需要进行批量插入,推荐使用 ExecutorType.BATCH 的插入方式,如果非要使用  的插入的话,需要将每次插入的记录控制在 20~50 左右。


作者:Java小虫
链接:https://juejin.cn/post/7220611580193964093
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

项目开发过程中,成员提离职,怎么办?

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。通常...
继续阅读 »

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。

项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。

通常情况下,一个员工向上级提出离职,那意味着他已经下决心走了,你留得住人,留不住心。而且这段时间,最好别派太多活,他只想早点交接完早点离开。

我们试着从环境、问题本身、问题主体三个方面来思考解决方案。

环境

  • 从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。
  • 提前安排更多的资源来做项目,预防资源不足的情况发生。比如整体预留了20%的开发时间做缓冲,或者整体安排的工作量比规划的多20%。

问题本身

从问题本身思考,员工离职导致的问题是资源不够用。

  • 新增资源,能不能快速找到替代离职员工的人?或者我们能不能使用外包方式完成需求?跟团队商量增加一些工作时间或提高工作效率?
  • 减少需求,少做一些不是很重要的需求,把离职员工的需求分给其他人。

这2个解决方案其实都有一个前提,那就是离职人员的代码是遵循编码规范的,这样接手的人才看得懂。否则,需要增加的资源会比原来规划的多很多。这种问题不能靠员工自觉,而应该要有一套制度来规范编码。

问题的主体

我们不一定能解决问题,但可以解决让问题发生的人。这样问题就不存在了。比如,既然问题出现在张三面前,那就想办法搞定张三,让他愿意按计划把项目完成。如果公司里没人能搞定这个事,这里还有另一个思路,就是想想谁能解决这个问题,找那个能解决问题的人。

从环境、问题本身、问题的主体三个维度来分析,我们得到了好几个解决方案。我们接着分析哪种方案更靠谱。

解决方案分析

方案一,从环境角度分析,让问题不发生。这种成本是最小的。但如果问题已经发生,那这个方案就没用了。

方案二,在项目规划的时候,提前安排更多资源。这招好是好,但前提是你公司有那么多资源。大部分公司都是资源不足。

方案三,新增资源,这个招人不会那么快,就算招进来了,一时半会还发挥不出多大的价值。请外包的话,其实跟招人一样,一时半会还发挥不出多大的价值,成本还更高,也不适合。至于跟团队成员商量提高工作效率或者大家加个班赶上进度,这也是一个解决方案。不过前提是团队还有精力承担这些工作。

方案四,减少需求。这个成本最小,对大部分公司其实也适用。关键是需求管理要做好,对需求的优先级有共识。

方案五,解决让问题发生的人。这个如果不是有大的积怨,也是一个比较好的方案。对整个项目来说,成本也不会很大,项目时间和质量都有保证。

项目管理里有一个生命周期概念,越是在早期发生问题,成本越小。越到后期成本越大。所以,如果让我选,我会选择方案一。但如果已经发生,那只能在四和五里选一个。

实战经验

离职是一场危机管理

让问题不发生,那么解决之道就是不让员工离职。尤其是不让核心骨干员工提离职。离职就是一场危机管理。

这里的本质的是人才是资产,我们在市场上看到很多案例,很多企业的倒闭并不是因为经营问题,而是管理层的大批量流失,资本市场也不看好管理层流失的企业。了解这点,你就能理解为什么人才是资产了。所以对企业来说,核心员工离职不亚于一场危机。

下面分享一个危机管理矩阵,这样有助于我们对危机进行分类。

横轴是一件事情发生之后,危害性有多大,我们分为大、中、小。纵轴就是这件事发生的概率,也可以分为大、中、小。然后就形成了九种不同的类型。

我自己的理解是,有精力的话,上图红色区域是需要重点关注的。如果精力有限,就关注最右边那三种离职后,危害性特别大的员工(不管概率发生的大小)。要知道给企业造成大影响的往往是那些发生概率小的,因为概率大的,你肯定有预防动作,而那些你认为不会离职的员工,突然一天找到你提离职,你连什么准备都没,这种伤害是最大的。

理论上所有岗位都应该准备好”接班人“计划,但实际上很多公司没办法做到。在一些小公司是一个萝卜一个坑,这个岗位人员离职,还得现招。这不合理,但这就是现状。

公司如何管理危机?

好,回到公司身上,公司如何管理危机?

第一,稳住关键性员工,让员工利益和公司利益进行深入绑定。

那些创造利润最大的前10~20%的员工,就应该获得50%甚至更高的收益。当然除了金钱上的激励外,还要有精神上的激励,给他目标,让他有成就感等等。

第二,有意识地培养关键岗位的接班人或者助理。

比如通过激励鼓励他们带新人、轮岗等等

第三,人员的危机管理是动态变化的,要时不时地明确团队各成员的位置。

比如大公司每年都会做人才盘点。

第四,当危机真的出现后,要有应对方案。

也就是把危机控制在可承受的范围内。比如,项目管理中的planB方案,真遇到资源不够,时间不够的情况下,我们能不能放弃一些不重要的需求?亦或者能不能先用相对简单但可用的方案?

离职管理的核心是:降低离职发生的概率和降低离职造成危害的大小。

离职沟通

如果事情已经发生了,管理者应该先通过离职沟通,释放自己的善意。我会按照如下情况跟离职员工沟通

第一,先做离职沟通,了解对方为什么离职?还有没有留下来的可能,作为管理者有什么能帮他做的?

第二,确定走的话,确认下对方期望的离职时间,然后根据公司情况,协商一个双方都能接受的离职时间点。不要因为没有交接人,就不给明确时间。

第三,征求对方意见,是否需要公布离职。然后一起商量这段时间的工作安排。比如,你会坦诚告知会减少工作量,但哪些工作是需要他继续支持的。希望他能一如既往地高效完成工作。

第四,如果还没有交接人到岗,最好在一周内安排人员到岗,可以考虑内部换岗,内招、猎聘等手段尽快让人员到岗。

第五,如果已经到离职时间,但还没有交接人,作为公司管理者,你就是最好的交接人。在正式交接工作之前,要理清楚需要哪些相关的资料,做好文档分类。如果实在对离职员工的工作不了解,可以让离职人员写一封日常工作的总结。

如果做完这些,离职员工还是消极怠工。作为管理者能做得就比较有限,可以尝试以下几个方法

1、再进行一次沟通。表明现在公司的情况,希望他给予支持。

2、看看自己能给予对方哪些帮助,先把这些落实好。比如写推荐信。另外有些公司入职的时候会做背景调查,这也是你能够帮助到他的。

3、如果你有权利,可以跟离职员工商量是否可以以兼职的方式来完成后续工作。这种方式对大家都好,他可以早点离职,你也不用担心因为时间仓促招错人。

如果做完以上这些还不行,那么就考虑减少一些需求,用更简单的方案先用着,后期做迭代。至于说让团队加班加点赶进度,这个要根据项目实际情况来定。

总结:今天给大家分享了一个简单分析问题的方法。然后重点聊了一下项目成员突然要离职,项目负责人有哪些应对方案。如果你看完有收获,欢迎留言讨论。


作者:石云升
链接:https://juejin.cn/post/7147319129542770702
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员提高效率的办法

最重要的-利用好工具 🔧工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;http://www.dooocs.com/chatgpt/REA…1. 早上不要开会 📅每个人一天是 24 小时,时间是均等的,但是时...
继续阅读 »

最重要的-利用好工具 🔧

工欲善其事必先利其器,利用好的知识外脑来帮助自己,学会使用AI大模型 比如Chagpt等;http://www.dooocs.com/chatgpt/REA…

1. 早上不要开会 📅

每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?

因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。

2. 不要使用番茄钟 🍅

有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。

有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。

好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。

3. 休息时间不要玩手机 📱

大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。

那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:

  • 闭目养神 😪
  • 听音乐 🎶
  • 在办公室走动走动 🏃‍♂️
  • 和同事聊会天 💑
  • 扭扭脖子活动活动 💁‍♂️
  • 冥想 or 正念 🧘

4. 不要在工位上吃午饭 🥣

大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:

  • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

    • 日光浴:外出的时候晒太阳可以促进血清素的分泌
    • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌
  • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

  • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。

5. 睡午觉 😴

现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:

  • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。
  • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街
  • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等

睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。

6. 下午上班前运动一下 🚴

下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:

  • 1️⃣ 深蹲
  • 2️⃣ 俯卧撑
  • 3️⃣ 胯下击掌
  • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)

7. 2 分钟解决和 30 秒决断 🖖

⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成,2 分钟解决就是一个很好的辅助决策的办法。

💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。

8. 不要加班,充足睡眠 💤

作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2 、3 点。

压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。

想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。

9. 睡前 2 小时 🛌

  1. 睡前两小时不能做的事情:

    • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了
    • 🥃 喝酒
    • ⛹️ 剧烈运动
    • 💦 洗澡水过高
    • 🎮 视觉娱乐(打游戏,看电影等)
    • 📺 闪亮的东西(看手机,看电脑,看电视)
    • 💡 在灯光过于明亮的地方
  2. 适合做的事情

    • 📖 读书
    • 🎶 听音乐
    • 🎨 非视觉娱乐
    • 🧘‍♂️ 使身体放松的轻微运动

10. 周末不用刻意补觉 🚫

很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。

其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。

我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。

参考

以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑;


作者:dooocs
链接:https://juejin.cn/post/7255189463747543095
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如何诊断Java 应用线程泄漏

大家经常听到内存泄漏, 那么线程泄漏是指什么呢?线程泄漏是指 JVM 里面的线程越来越多, 而这些新创建的线程在初期被使用之后, 再也不被使用了, 然而也没有被销毁. 通常是由于错误的代码导致的这类问题.一般通过监控 Java 应用的线程数量的相关指标, 都能...
继续阅读 »

大家经常听到内存泄漏, 那么线程泄漏是指什么呢?

线程泄漏是指 JVM 里面的线程越来越多, 而这些新创建的线程在初期被使用之后, 再也不被使用了, 然而也没有被销毁. 通常是由于错误的代码导致的这类问题.

一般通过监控 Java 应用的线程数量的相关指标, 都能发现这种问题. 如果没有很好的对这些指标的监控措施, 或者没有设置报警信息, 可能要到等到线程耗尽操作系统内存导致OOM才能暴露出来.

最常见的例子

在生产环境中, 见过很多次类似下面例子:

public void handleRequest(List<String> requestPayload) {
if (requestPayload.size() > 0) {
ExecutorService executor = Executors.newFixedThreadPool(2);

for (String str : requestPayload) {
final String s = str;
executor.submit(new Runnable() {
@Override
public void run() {
// print 模拟做很多事情
System.out.println(s);
}
});
}
}
// do some other things
}

这段代码在处理一个业务请求, 业务请求中包含很多小的任务, 于是想到使用线程池去处理每个小任务, 于是创建了一个 ExecutorService, 接着去处理小任务去了.

错误及改正

看到这段代码, 大家会觉的不可能啊, 怎么会有人这么使用线程池呢? 线程池不是这么用的啊? 一脸问号. 可是现实情况是: 总有新手写出这样的代码.

有的新手被指出这个问题之后, 就去查文档, 发现 ExecutorService 有 shutdown() 和 shutdownNow() 方法啊, 于是就在 for 循环后边加了 executor.shutdown(). 当然, 这会解决线程泄漏的问题. 但却不是线程池正确的用法, 因为这样虽然避免了线程泄漏, 却还是每次都要创建线程池, 创建新线程, 并没有提升性能.

正确的使用方法是做一个全局的线程池, 而不是一个局部变量的线程池, 然后在应用退出前通过 hook 的方式 shutdown 线程池.

然而, 我们是在知道这段代码位置的前提下, 很快就修好了. 如果你有一个复杂的 Java 应用, 它的线程不断的增加, 我们怎么才能找到导致线程泄漏的代码块呢?

情景再现

通常情况下, 我们会有每个应用的线程数量的指标, 如果某个应用的线程数量启动后, 不管分配的 CPU 个数, 一直保持上升趋势, 那么就危险了. 这个时候, 我们就会去查看线程的 Thread dump, 去查看到底哪些线程在持续的增加, 为什么这些线程会不断创建, 创建新线程的代码在哪?

找到出问题的代码

在 Thread dump 里面, 都有线程创建的顺序, 还有线程的名字. 如果新创建的线程都有一个自己定义的名字, 那么就很容易的找到创建的地方了, 我们可以根据这些名字去查找出问题的代码.

根据线程名去搜代码

比如下面创建的线程的方式, 就给了每个线程统一的名字:

Thread t = new Thread(new Runnable() {
@Override
public void run() {
}
}, "ProcessingTaskThread");
t.setDaemon(true);
t.start();

如果这些线程启动之前不设置名字, 系统都会分配一个统一的名字, 比如thread-npool-m-thread-n, 这个时候通过名字就很难去找到出错的代码.

根据线程处理的业务逻辑去查代码

大多数时候, 这些线程在 Thread dump 里都表现为没有任何事情可做, 但有些时候, 你可以能发现这些新创建的线程还在处理某些业务逻辑, 这时候, 根据这些业务逻辑的代码向上查找创建线程的代码, 也不失为一种策略.

比如下面的线程栈里可以看出这个线程池在处理我们的业务逻辑代码 AsyncPropertyChangeSupport.run, 然后根据这个关键信息, 我们就可以查找出到底那个地方创建了这个线程:

"pool-2-thread-4" #159 prio=5 os_prio=0 cpu=7.99ms elapsed=354359.32s tid=0x00007f559c6c9000 nid=0x6eb in Object.wait()  [0x00007f55a010a000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@11.0.18/Native Method)
- waiting on <0x00000007c5320a88> (a java.lang.ProcessImpl)
at java.lang.Object.wait(java.base@11.0.18/Object.java:328)
... 省略 ...
at com.tianxiaohui.JvmConfigBean.propertyChange(JvmConfigBean.java:180)
at com.tianxiaohui.AsyncPropertyChangeSupport.run(AsyncPropertyChangeSupport.java:346)
at java.util.concurrent.Executors$RunnableAdapter.call(java.base@11.0.18/Executors.java:515)
at java.util.concurrent.FutureTask.run(java.base@11.0.18/FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.18/ThreadPoolExecutor.java:1128)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.18/ThreadPoolExecutor.java:628)
at java.lang.Thread.run(java.base@11.0.18/Thread.java:829)

使用 btrace 查找创建线程的代码

在上面2种比较容易的方法已经失效的时候, 还有一种一定能查找到问题代码的方式, 就是使用 btrace 注入拦截代码: 拦截创建新线程的地方, 然后打印当时的线程栈.

我们稍微改下官方的拦截启动新线程的例子, 加入打印当前栈信息:

import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.Self;

import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace
public class ThreadStart {
@OnMethod(
clazz = "java.lang.Thread",
method = "start"
)
public static void onnewThread(@Self Thread t) {
D.probe("jthreadstart", Threads.name(t));
println("starting " + Threads.name(t));
println(jstackStr());
}
}

然后执行 btrace 注入, 一旦有新线程被创建, 我们就能找到创建新线程的代码, 当然, 我们可能拦截到不是我们想要的线程创建栈, 所以要区分, 哪些才是我们希望找到的, 有时候, 上面的代码中可以加一个判断, 比如线程名字是不是符合我们要找的模式.

$ ./bin/btrace 1036 ThreadStart.java
Attaching BTrace to PID: 1036
starting HandshakeCompletedNotify-Thread
java.base/java.lang.Thread.start(Thread.java)
java.base/sun.security.ssl.TransportContext.finishHandshake(TransportContext.java:632)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.onConsumeFinished(Finished.java:558)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.consume(Finished.java:525)
java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)

上面的代码, 就抓住了一个新创建的线程的地方, 只不过这个可能不是我们想要的.

除了线程会泄漏之外, 线程组(ThreadGroup) 也有可能泄漏, 导致内存被用光, 感兴趣的可以查看生产环境出现的一个真实的问题: 为啥 java.lang.ThreadGroup 把内存干爆了

总结

针对线程泄漏的问题, 诊断的过程还算简单, 基本过程如下:

  1. 先确定是哪些线程在持续不断的增加;
  2. 然后再找出创建这些线程的错误代码;
    1. 根据线程名字去搜错误代码位置;
    2. 根据线程处理的业务逻辑代码去查找错误代码位置;
    3. 使用 btrace 拦截创建新线程的代码位置

作者:int0x03
链接:https://juejin.cn/post/7219695336422621221
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从零开始搭建个人网站

伴随着颈椎疼痛的困扰,此时的我不敢轻易扭动脖子,宛如一只梗着脖子的傻猫。好在个人博客网站基本搭建完毕,尽管下一个任务紧迫,但它没有任何盈利点。现在,只需简单总结一下,就能稍微松口气了。个人博客网站功能设计炫酷展示:独特页面展示博客列表,包括标题和发布日期,可通...
继续阅读 »

伴随着颈椎疼痛的困扰,此时的我不敢轻易扭动脖子,宛如一只梗着脖子的傻猫。好在个人博客网站基本搭建完毕,尽管下一个任务紧迫,但它没有任何盈利点。现在,只需简单总结一下,就能稍微松口气了。

个人博客网站功能设计

炫酷展示:独特页面展示博客列表,包括标题和发布日期,可通过点击链接阅读完整内容。

分类与标签:文章分类和标签,助力读者按主题或关键词快速筛选和浏览感兴趣的内容。

神奇搜索:强大而快捷的模糊搜索功能,让读者迅速找到心仪文章,关键词轻松搜。

探秘联系:Github的联系方式隐藏在神秘角落,前往解锁更多关于开发者的信息。

响应式布局:兼容各设备,个人主页灵活展示在桌面、平板和手机,畅享极致用户体验。

数据剖析:揭示访问统计与分析报告,文章点击、来源追踪,读者行为一览无余。数据助力个人优化内容、领略读者趣味。

主题风格:提供浅色模式和暗黑模式,页面可一键换装或跟随系统。

为什么是Next.js和vercel?

首先是这样最简单,从基友的劳动成果中扒拉了整个技术架构。在此基础上,我又做了一些页面的功能的扩展,比如全文模糊搜索、继续阅读、文章目录等。【叉腰】

服务器渲染(SSR)可以给博客带来超棒的性能和超棒的SEO优化。它会让页面加载速度变得超快,还能让搜索引擎更好地抓取和索引博客内容。(但现在还不能搜到我的文章ε=(´ο`*))))

Vercel作为部署平台。它简单易用,超快速而且靠谱。最重要的是,它能跟Next.js完美结合,让我轻轻松松地将我的应用程序部署到全球分布式网络上。它还提供自动化的CI/CD流程,让我可以专注于撰写博客内容,而不用花费太多时间和精力在繁琐的部署和服务器配置上。

赞美基友!

UI设计的灵感来源

页面布局

首页的布局模仿nextjs框架的样本页,它本身拥有响应式布局。

首页的内容并不需要太多,所以保留了样本页的单屏设计,页面不需要滚动就能展示完整的内容。页面分为三个模块:顶部为面包屑按钮区,中间区域面积最大,突出博主猫奴本色,底部则包含了三个菜单,分别作为专栏文章和其他项目的链接入口。

对于专栏文章列表页的布局,首先需要考虑两个主要模块:专栏标题和描述,文章列表的标题和发布时间。在PC端,按2:3比例左右分栏,移动端按2:8比例上下分栏。顶部和首页类似有漂亮的手绘按钮点缀页面,还有一只正在认真工作的猫猫作为背景,增添趣味。

文章内容页的布局设计采用主流的设计,基本上在每个教程文档都能看见这样的布局方式。布局是类似的,但怎样才能做得好看点呢?如何用颜色+透明度、字号+行高的奇妙组合创造美的魔术?对于个人网站,这很大程度依赖于个人的审美喜好,以及最重要的、很容易被个人开发者忽略的一点:考虑读者的阅读体验。

关于颜色

浅色模式的颜色来源于我的=月白色的瓷碗,和瓷碗里的黄米汤圆。降低一点饱和度,调成目前主流的莫兰迪色系,给人一种温柔的感觉。我喜欢性格温和的人,希望自己也变得温柔一些。

深色模式中,很多网站都呈现出超棒的视觉效果,我个人最喜欢tailwind的配色。这种模式下的背景傻猫露出了其暗黑的一面,似乎正在谋划消灭人类,重建一个有吃不完的小鱼干和虾虾的新世界。

图标和图片

小图标采用阿里iconfont的手绘风格系列,给页面增添了一抹生动与活泼。

背景图由网图和两个手绘Icon图标巧妙拼合而成,似乎在讲述一个工作猫的故事。在图中,你可以看到一只憨憨的小猫专注地盯着电脑屏幕,右爪按住鼠标,而鼠标线的末端却连接着一个毛线团,给人一种不太聪明的印象。

问题和解决方案

  1. 浅色和深色模式转换的解决方案,哪个最方便快捷?

项目使用的组件库mantine提供了MantineProvider组件,可用于更改主题。

import { MantineProvider } from '@mantine/core';

function Demo() {
return (
<MantineProvider theme={{ fontFamily: 'Open Sans' }} withGlobalStyles>
<App />
</MantineProvider>
);
}

theme属性可用来传递其它任意风格属性。withGlobalStyles 这个属性可以增加几个样式,其中一个就是深色模式的颜色和浅色模式的颜色。

但我并没有使用这个方案,原因有三:

杀鸡用牛刀既视感,theme提供了很多选项默认值,但除了颜色,其它用不上。
主题色色彩不够多,如果想要更多颜色,需要修改它的css变量。而tailwind的颜色选择范围更多,添加透明度也很方便。
相比于在js中修改样式,我更喜欢在css中完成同样的功能。

比如,使用tailwind,仅需在某个className加上前缀 dark:,比如dark:bg-slate-900, 表示bg-slate-900在深色模式下生效。具体如何使用可以参考tailwind官网,这里不做赘述。

  1. 老大难useEffect。

当需要使用useEffect监听事件时,有时候只想在组件 mounted 前执行某些操作,而不希望随着状态的变化而不停地更新。对于那个状态,我会创建一个ref值,就像是一块“不会变形的金属”。这样,我可以在useEffect中使用这个ref值作为判断依据,而不会受到状态的干扰。

然而,还有一种情况无法绕开。由Context上下文提供的value值 和 updateValue方法,updateValue需要在某个子组件的useEffect中监听事件而触发。

  1. Tailwind SVG 样式未生效

tailwind支持处理SVG图标的样式,比如你可以这样写一个svg的颜色:

<svg class="fill-blue-500 ..."> <!-- ... --> </svg>

<svg class="stroke-blue-500 ..."> <!-- ... --> </svg>

可是,对于我下载的那些手绘图标,上述方法却不起作用,包括stroke属性也无效。我猜测可能与图标的path路径有关。脑海中浮现出一个解决方案:使用全局provider来传递主题色作为参数,并将其赋值给fill属性。但是,我实在太懒了,不想采用那么繁琐的方式,也不想依赖市面上已有的状态管理方案。

fill属性接受的值是颜色,同样可以使用currentColor,它会继承最近祖先元素的颜色,类似于inherit的效果。当fill属性不存在或为空字符串时,默认会被填充为黑色。然而,这些手绘图标似乎并不理会tailwind针对svg的className属性,反而继承了更上一级的颜色。既然如此,那解决方案就是在引用这些图标的组件中指定颜色。

  1. sticky不粘了。 通常,我们知道,英文文档的中文翻译总有一些令人困惑的小问题。但这一次,是mdn的中文文档略胜一筹。例如,在stick相关文档中:

This value always creates a new stacking context. Note that a sticky element "sticks" to its nearest ancestor that has a "scrolling mechanism" (created when overflow is hiddenscrollauto, or overlay), even if that ancestor isn't the nearest actually scrolling ancestor.

注意,一个 sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的 overflow 是 hiddenscrollauto 或 overlay 时),即便这个祖先不是最近的真实可滚动祖先。

中文文档明确多了这一句话:

这有效地抑制了任何“sticky”行为(详情见 Github issue on W3C CSSWG)。

如果不看这句话,中英文文档都会让人以为是要在overflow有这些值的时候才生效。然而事实却完全相反。在我去掉藏在 body 中的overflow-x:hidden后, sticky终于能按预期触发了。

  1. 模糊查询算法和匹配文本标记

前文提到,贴心而又温柔的笔者为网站提供了搜索功能。搜索功能的界面和常见开发文档的Spotlight看起来基本上一样,不同的是细节的模糊查询功能。

一个具备模糊查询的Spotlight看起来像这样:

(好吧,截图的时候又发现自己漏了个细节,搜索文章内容节选的前后省略号。烦)

关于模糊查询算法,可以看这里探秘Fuse.js:模糊查询算法的学习笔记,这里不做赘述。

而匹配文本颜色标记,这个功能需要自己来实现,大致需求是,找出最佳匹配的文章内容节选展示在Spotlight的搜索结果列表里。解析md文档,去掉那些不想要的md文档标记符号,再循环找出最佳匹配区间作为节选内容,节选内容不超过30个字。

以上。


作者:Kiera
链接:https://juejin.cn/post/7247024731442479163
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

什么是设计思维?

你有没有想过事情是否可以采取不同的方式, 但又不确定如何做?那么,不要爱上解决方案,而是爱上问题!这是一篇深入探讨设计思维如何做到这一点的文章。在快节奏的行动和反应的世界中,高质量的结果是我们所有人的目标。但是你怎样才能做得更好呢?无论是产品、服务还是流程,我...
继续阅读 »

你有没有想过事情是否可以采取不同的方式, 但又不确定如何做?那么,不要爱上解决方案,而是爱上问题!这是一篇深入探讨设计思维如何做到这一点的文章。

在快节奏的行动和反应的世界中,高质量的结果是我们所有人的目标。但是你怎样才能做得更好呢?无论是产品、服务还是流程,我们的目标都是让事情变得更好。它让我们想知道我们将如何设计一个漫长而公平的世界以达到一定程度的卓越。

当我们谈论让“事情变得更好”时,我们不能否认看板系统在丰田实施 TQM(全面质量管理)中的作用。如果TQM为制造业做到了这一点,那么设计思维就有可能为创新带来以人为本的解决方案。

设计思维采用以人为本的方法,以便在我们跳入所有可能的解决方案之前了解如何处理问题。我们可以看到它在各个地方的使用。嗡嗡声无处不在,从社会部门到政策制定,从医疗保健到商业。那么什么是设计思维呢?

设计思维的意义 

设计思维促进以创造性的方式解决复杂问题,这种方式优先考虑人类的需求,并着重于寻找技术上可行的创造性解决方案。

虽然很难用几个词来定义“设计思维”,但我宁愿将设计思维视为一种哲学或一种思维方式,以解决难以用传统和标准方法解决的复杂问题解决问题的实践。设计思维采取的途径是提供以下解决方案:可行的、可行的和可取的。

一般来说,问题的解决方案有时会被传统的解决方法所忽视,而有些方法是高度理性和分析性的,而另一些则是情绪化的。设计思维可能只是增加人类问题的理性、情感和功能需求的第三种方式。设计思维不仅限于建筑产品;任何促进创新的新举措都可以利用解决问题的设计思维原则。

起源 

虽然没有确凿的证据表明设计思维的起源,但设计思维作为一种思维方式可以追溯到John E. Arnold,他是斯坦福大学“创意工程”研究的先驱,机械工程教授。他是最早撰写有关设计思维的少数人之一,并将设计思维的种子视为一种运动。他的讲座激发了更多的想象力和创新性。他的问题解决理论通过将问题的个人、科学和实践方面联系起来,着重于人类需求。

他强调了像艺术家一样处理问题并将人类作为想要构建的解决方案的基石的重要性。“创意问题”没有一个正确答案。

“工程师可以承担艺术家的某些方面,并通过美化或改善产品或机器的外观,或者通过对市场以及人们想要或不想要的东西的种类具有更敏锐的敏感性来尝试改善或增加产品或机器的适销性'想要。— 约翰·E·阿诺德*

设计思维过程 

在对设计思维进行情境化和应用的努力中,它通常被认为是一个过程,该过程可以指导价值观来控制如何处理问题。这个过程可能不一定是线性的或顺序的,而是一个对特定问题或用例有意义的循环。

设计思维过程

EMPATHIZE - 移情 

为人类设计可能很棘手。有时,需求或愿望未被发现,可能无法完全反映真正的问题。传统的市场研究过程是根据事实进行的,而设计思维方法是通过同理心来解决问题。同理心试图理解潜在需求并转化环境的当前现实。这有助于解决方案设计人员了解可以解释问题的人员、他们的行为和上下文,从而构建更好的解决方案。

因此,从问题中获得灵感的第一步是了解设计的对象以及他们寻求解决方案的动机。这对于企业了解可用的机会空间尤其有用。就像苹果所做的那样。虽然 MP3 播放器已经成为一种东西,但iPod 改变了人们消费音乐的方式。当索尼凭借随身听和 CD 统治消费电子市场时,苹果公司凭借 iPod彻底改变了音乐世界。Apple 对人们随身携带盒式磁带的问题深表同情,而 iPod 改变了游戏规则!

为了识别和理解问题的脉搏并结合这一步的背景,收集信息是关键。这是与问题空间中的人交谈以了解他们关心什么以及他们目前如何处理问题的地方。用户访谈和他们的反馈可以帮助了解他们的情况。

DEFINE - 定义 

这是形成问题的最重要步骤之一。

在设计思维中,构思不当的问题陈述为构建“问题”而非“问题”的解决方案铺平了道路。

例如,与某人谈论一个问题。记录下观察结果,并以合理的解决方案来解决该问题。用户对解决方案可能会解决该问题感到兴奋。但这里真正发生的是你和那个人一直在讨论他们许多其他问题中的一个问题。因此,他们是否决定采用你的解决方案取决于此人对你承诺解决的问题的重视程度。

在这里,定义问题的脉搏变得非常重要。 通常,事后看来,焦点会落在你试图解决的问题上,而不是这个人可能实际遇到的许多其他问题。

因此,定义问题对引导如何着手构建以人为本的解决方案大有帮助。

IDEATE - 构思 

在此阶段,观察结果会找到归宿,加以综合以创造改变的机会。集思广益来定义和重新定义潜在的解决方案,以创建解决问题的竞争想法。理想情况下,这一步可以帮助找到问题的核心。

如前所述,这不会是一个线性过程,可能经常会发现自己在共同努力挑战想法或问题本身时会回到之前的步骤。因此,可以挑选出好的想法来实施。

PROTOTYPE - 原型 

下一阶段是原型,可以通过创建最终解决方案的模型来验证想法。解决方案采用有形的形式来展示原型实施阶段的证据。它还展示了在构思阶段未说明的想法的限制和局限性。

Uber 是成功构建出色原型的经典公司之一。Uber 在最初发布时专注于解决“找出租车”的核心问题。该产品的第一个测试版是一个非常简约的应用程序,所有订单都是手动管理的,CEO 可以联系司机预订行程。而且它没有付费功能。目标是测试和验证叫车问题,这是该应用程序的核心优势。最终,当他们了解了目标市场和痛点后,他们开始改进其他功能。

TEST - 测试 

然后将最终原型与目标群体进行测试,并反复进行以适应学习方式。验证在这里采用最终形式,并且可能再次要求重新访问之前的一些步骤以大规模实施该计划。

行动中的设计思维 - 案例研究 

  1. 社会部门的设计思维 

疟疾是非洲最令人不安的问题之一,也是5 岁以下儿童死亡的前 5 大原因之一。这是一个本质上非常复杂的社会问题,分发蚊帐的设计思维方法有助于有效地对抗这种疾病。世界卫生组织报告称,埃塞俄比亚和卢旺达等国家的死亡率下降了 50%-60% ,加纳下降了 34%。

结果发现,蚊帐的设计对加纳的一些人没有吸引力。一组研究人员确定了一个潜在的解决方案来解决让人们使用网络的问题。他们提出了一种以人为本的蚊帐设计,为长期解决这一社会问题铺平了道路。

以人为本的蚊帐设计方法。( 大预览 

  1. 爱彼迎 

设计思维在改造一家几近失败的公司——Airbnb 方面也发挥了巨大的作用。他们的业务正在瘫痪,当他们遇到问题时,他们发现广告中的图片效果不佳。展出的照片是用劣质手机拍摄的。当看到网站上的照片时,想租房的人觉得他们没有看到他们实际支付的是什么。创始人一意识到这一点,就租了一台相机,为客户的财产拍下好照片。创始人之一杰比亚 (Gebbia)继续解释设计学校的经历如何帮助他们重塑自我,更好地为客户服务。

  1. 网飞 

另一个家喻户晓的品牌 Netflix 已经取得了长足的进步,设计思维在他们做出的决策中发挥了重要作用。Netflix 推出了直接送货上门的 DVD 租赁服务,而其他竞争对手则让人们开车穿过商店挑选电影。

后来当有线电视开始提供点播电影时,Netflix 了解了客户的痛点,开始按月提供在线流媒体服务,而无需为每张 DVD 付费。他们建立了一个订阅模式的在线目录,让客户在家中舒适地观看他们最喜欢的电影的便利性使他们感到高兴。

当选择要观看的内容比实际观看要花费更长的时间时,Netflix 想出了“预览”来帮助人们选择要观看的内容。听起来很简单,但 Netflix推荐系统背后的想法有助于减少人们花在决定观看内容上的时间。虽然变化是不可避免的,但 Netflix 不断通过设计思维方法重塑自己,以发现以最终用户为中心的创新解决方案。

从租用 DVD 到在线流媒体,Netflix 一直致力于了解最终用户,以以人为本的方式设计解决方案。

不仅如此。清单不胜枚举。

最后的想法 

设计思维一直在不断发展。人们一直在为使其在各个领域的使用情境化更有用做出贡献。根据问题的大小和复杂性,应用设计思维框架并着手创建以人为本的解决方案具有不同的形式。

考虑到它的灵活性,设计思维可以帮助熟悉并适应歧义。该方法可以在各种规模上动态地进行游戏,使其成为一项有价值的追求。

从想要解决的任何问题开始尝试,并在下面的评论中告诉我它是如何工作的。如果你愿意,我很乐意倾听并提供帮助。

相关资源 


作者:aeasy
链接:https://juejin.cn/post/7248453787215626301
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

技术人创业是怎么被自己短板KO的

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没...
继续阅读 »

这几天搜索我的聊天记录,无意中看到了一个两年前的聊天消息。那是和我讨论出海业务的独立开发网友,后来又去创业的业界精英。顺手一搜,当初他在我的一个开发者群也是高度活跃的人群之一,还经常私下给我提建议,人能力很强,做出海矩阵方向的项目做的很拿手。但是在后来忽然就没消息了,虽然人还留在群里。

我好奇点开了他的朋友圈,才知道他已经不做独立开发了,而且也(暂时)不在 IT 圈里玩了,去帮亲戚家的服装批发业务打打下手,说是下手,应该也是二当家级别了,钱不少,也相对安稳。朋友圈的画风以前是IT行业动态,出海资讯现在是销售文案和二维码。

和他私下聊了几句,他跟我说他现在过的也还好,人生路还长着呢,谈起了自己在现在这行做事情的经历,碎碎念说了不少有趣的事情,最后还和我感慨说:“转行后感觉脑子灵活了很多”,我说那你写程序的时候脑子不灵活吗,他发了个尴尬而不失礼貌的表情,“我以前技术搞多了,有时候死脑筋。”

这种话我没少听过,但是从一个认识(虽然是网友)而且大跨度转行的朋友这里说出来,就显得特别有说服力。尤其了解了他的经历后,想写篇文章唠叨下关于程序员短板的问题,还有这种短板不去补强,会怎么一步步让路越走越窄的。

现在离职(或者被离职)的程序员越来越多了,程序员群体,尤其是客户端程序员这个群体,只要能力过得去,都有全栈化和业务全面化的潜力。尤其是客户端程序员,就算是在公司上班时,业余时间写写个人项目,发到网上,每个月赚个四到五位数的副业收入也是可以的。

再加上在公司里遇到的各种各样的窝囊事,受了无数次“煞笔领导”的窝囊气,这会让一些程序员产生一种想法,我要不是业余时间不够,不然全职做个项目不就起飞了?

知道缺陷在哪儿,才能扬长避短,所以我想复盘一下,程序员创业,在主观问题上存在哪些短板。(因为说的是总体情况,也请别对号入座)

第一,认死理。

和代码,协议,文档打交道多了,不管自己情愿不情愿,人多多少少就有很强的“契约概念”,代码的世界条理清晰,因果分明,1就是1,0就是0,在这样的世界里呆多了,你要说思维方式不被改变,那是不可能的 --- 而且总的来说,这种塑造其实是好事情。要不然也不会有那么多家长想孩子从小学编程了。(当然了,家长只是想孩子学编程,不是做程序员。)

常年埋头程序的结果,很容易让技术人对于社会上很多问题的复杂性本质认识不到位,恐惧,轻视,或者视而不见,总之,喜欢用自己常年打磨的逻辑能力做一个推理,然后下一个简单的结论。用毛爷爷的话说,是犯了形而上的毛病。

例如,在处理iOS产品上架合规性一类问题时,这种毛病暴露的就特别明显。

比如说相信一个功能别的产品也是这么做的,也能通过审核,那自己照着做也能通过。但是他忽略了这种判断背后的条件是,你的账号和别的账号在苹果眼里分量也许不同的,而苹果是不会把这件事写在文档上的。

如果只是说一说不要紧,最怕的是“倔”,要不怎么说是“认死理”呢。

第二,喜欢拿技术套市场。

这个怎么理解呢,就是有追求的技术人喜欢研究一些很强的技术,但是研究出来后怎么用,也就是落实到具体的应用场景,就很缺点想象力了。

举个身边有意思的例子,有个技术朋友花了三年时间业余时间断断续续的写,用 OpenGL 写了一套动画效果很棒的 UI 引擎,可以套一个 View 进去后定制各种酷炫的动画效果。做出来后也不知道用来干嘛好,后来认识了一个创业老板,老板一看你这个效果真不错啊,你这引擎多少钱我买了,朋友也没什么概念,说那要不五万卖你。老板直接钱就打过去了。后来老板拿给手下的程序员维护,用这套东西做了好几个“小而美”定位的效率工具,简单配置下就有酷炫的按钮动画效果,配合高级的视觉设计逼格拉满,收入怎么样我没问,但是苹果在好几个国家都上过推荐。

可能有人要说,那这个程序员哥哥没有UI帮忙啊,对,是这个理,但是最根本的问题是,做小而美工具这条路线,他想都没想到,连意识都意识不到的赚钱机会,怎么可能把握呢?有没有UI帮忙那是实现层的门槛而已。

第三,不擅长合作。

为什么很多创业赚到小钱(马化腾,李彦宏这些赚大钱就不说了,对我们大部分人没有参考价值)而且稳定活下来的都是跑商务,做营销出身的老板。

他们会搞钱。

他们会搞钱,是因为他们会搞定人,投资人,合伙人,还有各种七七八八的资源渠道。

大部分人,在创业路上直接卡死在这条路线上了。

投资人需要跑,合作渠道需要拉,包括当地的税务减免优惠,创业公司激励奖金,都需要和各种人打交道才能拿下来。

那我出海总行了吧,出海就不用那么麻烦了吧。不好意思,出海的合作优势也是领先的,找海外的自媒体渠道合作,给产品提曝光。坚持给苹果写推荐信,让自家产品多上推荐。你要擅长做这些,就不说比同行强一大截,起码做出好产品后创业活下来的希望要高出不少,还有很多信息差方法论,需要进圈子才知道。

--- 

我说的这些,不是贬损也不是中伤,说白了,任何职业都有自己的短板,也就是我们说的职业病,本来也不是什么大不了的事情。只是我们在大公司拧螺丝的时候,被保护的太好了。

只是创业会让一个人的短处不断放大,那是因为你必须为自己的选择负责了,没人帮你擦屁股了背锅了。所以短板才显得那么刺眼。

最后说一下,不是说有短板就会失败,谁没点短处呢。写出来只是让自己和朋友有更好的自我认知,明白自己的长处在哪,短处在哪。

最后补一个,左耳朵耗子的事情告诉我们,程序员真的要保养身子,拼到最后其实还是拼身体,活下来才有输出。


作者:风海铜锣
链接:https://juejin.cn/post/7238443713873199159
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

工作 7 年的老程序员,现在怎么样了

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人...
继续阅读 »

犹记得高中班主任说:“大家努力考上大学,大学没人管,到时候随便玩”。我估计很多老师都这么说过。

我考上大学(2010年)之前也是这么过的。第一年哥哥给买了个一台华硕笔记本电脑。那个年代买华硕的应该不少,我周边就好几个。有了电脑之后,室友就拉着我一起 cs,四个人组队玩,那会觉得很嗨,上头。

后来看室友在玩魔兽世界,那会不知道是什么游戏,就感觉很好玩,再后来就入坑了。还记得刚开始玩,完全不会,玩个防骑,但是打副本排DPS,结果还被人教育,教育之后还不听(因为别的职业不会玩),就经常被 T 出组。之后,上课天天看游戏攻略和玩法,或者干脆看小说。前两年就这么过去了

1 跟风考研

大三开始,觉得这么混下去不行了。在豆瓣上找了一些书,平时不上课的时候找个自习室学习。那会家里打电话说有哪个亲戚家的孩子考研了,那是我第一次知道“考研”这个词。那会在上宏微观经济学的课,刚好在豆瓣上看到一本手《牛奶面包经济学》,就在自习室里看。刚好有个同院系的同学在里面准备考研,在找小伙伴一起战斗(毕竟,考研是一场长跑,没有同行者,会很艰难)。我一合计,就加入了他们的小团队。从此成为“中国合伙人”(刚好四个人)中的一员。

我那会也不知道毕业了之后能去哪些公司,能找哪些岗位,对于社会完全不了解,对于考研也是完全不了解。小团队中的三个人都是考金融学,我在网上查,知道了学硕和专硕的区别,也知道专硕学费贵。我家里没钱,大学时期的生活费都是自己去沃尔玛、麦当劳、发传单挣得,大学四年,我在沃尔玛工作超过了 2 年、麦当劳半年,食堂倒盘子半年,中途还去发过传单,暑假还去实习。没钱,他们考金融学专硕,那我就靠经济学学硕吧,学硕学费便宜。

从此开始了考研之路。

2 三次考研

大三的时候,报名不是那么严格,混进去报了名,那会还没开始看书,算是体验了一把考研流程;

还记得那次政治考了 48 分,基本都过了很多学校的单科线,那会就感觉政治最好考(最后发现,还是太年轻)。

大四毕业那年,把所有考研科目的参数书都过了 2 遍,最后上考场,最后成绩也就刚过国家线。

毕业了,也不知道干啥,就听小伙伴的准备再考一次,之前和小伙伴一起来了北京,租了个阳台,又开始准备考研。结果依然是刚过国家线。这一年也多亏了一起来北京的几个同学资助我,否则可能都抗不过考试就饿死街头了。

总结这几次考研经历,失败的最大原因是,我根本不知道考研是为了什么。只是不知道如果工作的话,找什么工作。刚好别人提供了这样一个逃避工作的路,我麻木的跟着走而已。这也是为什么后面两次准备的过程中,一有空就看小说的原因。

但是,现在来看,我会感谢那会没有考上,不然就错过了现在喜欢的技术工作。因为如果真的考上了经济学研究生,我毕业之后也不知道能干啥,而且金融行业的工作我也不喜欢,性格上也不合适,几个小伙伴都是考的金融,去的券商,还是比较了解的。

3 入坑 JAVA 培训

考完之后,大概估了分,知道自己大概率上不了就开始找工作了。那会在前程无忧上各种投简历。开始看到一个做外汇的小公司,因为我在本科在一个工作室做过外汇交易相关的工作,还用程序写了一段量化交易的小程序。

所以去培训了几天,跟我哥借了几千块钱,注册了一个账号,开始买卖外汇。同时在网上找其他工作。

后面看介绍去西二旗的一家公司面试,说我的技术不行,他们提供 Java 培训(以前的套路),没钱可以贷款。

我自己也清楚本科一行 Java 代码没写过,直接工作也找不到工作。就贷款培训了,那会还提供住宿,跟学校宿舍似的,上下铺。

4 三年新手&非全研究生

培训四个月之后,开始找工作。那会 Java 还没这么卷,而且自己还有个 211 学历,一般公司的面试还是不少的。但是因为培训的时候学习不够刻苦(也是没有基础)。最后进了一个小公司,面试要 8000,最后给了 7000。这也是我给自己的最底线工资,少于这个工资就离开北京了,这一年是 2015 年。

这家公司是给政府单位做内部系统的,包括中石油、气象局等。我被分配其中一个组做气象相关系统。第二年末的时候,组内的活对我来说已经没什么难度了,就偷偷在外面找工作,H3C 面试前 3 面都通过了,结果最后大领导面气场不符,没通过。最后被另外一家公司的面试官劝退了。然后公司团建的时候,大领导也极力挽留我,最后没走成。

这次经历的经验教训有 2 个,第 1 个是没有拿到 offer 之前,尽量不要被领导知道。第 2 个是,只要领导知道你要离职,就一定要离职。这次就是年终团建的时候,被领导留下来了。但是第二年以各种理由不给工资。

之前自己就一直在想出路,但是小公司,技术成长有限,看书也对工作没有太大作用,没有太大成长。之后了解到研究生改革,有高中同学考了人大非全。自己也就开始准备非全的考试。最后拿到录取通知书,就开始准备离职了。PS:考研准备

在这家公司马上满 3 年重新签合同的时候,偷偷面试了几家,拿到了 2 个还不错的 offer。第二天就跟直属领导提离职了。这次不管直属领导以及大领导如何劝说,还是果断离职了。

这个公司有两个收获。一个是,了解了一般 Java Web 项目的全流程,掌握了基本开发技能,了解了一些大数据开发技术,如Hadoop套件。另外一个是,通过准备考研的过程,也整理出了一套开发过程中应该先思而后行。只有先整理出

5 五年开发经历

第二家公司是一家央企控股上市公司,市场规模中等。主要给政府提供集成项目。到这家公司第二年就开始带小团队做项目,但是工资很低,可能跟公司性质有关。还好公司有宿舍,有食堂。能省下一些钱。

到这家公司的时候,非全刚好开始上课,还好我们 5 点半就下班,所以我天天卡点下班,大领导天天给开发经理说让我加班。但是第一学期要上课,领导对我不爽,也只能这样了。

后来公司来了一个奇葩的产品经理,但是大领导很挺他,大领导下面有 60 号人,研发、产品、测试都有。需求天天改,还不写在文档上。研发都开发完了,后面发现有问题,要改回去,产品还问,谁让这么改的。

是否按照文档开发,也是大领导说的算,最后你按照文档开发也不对,因为他们更新不及时;不按照文档开发也不对,写了你不用。

最后,研发和产品出差,只能同时去一波人,要是同时去用户现场,会打架。最后没干出成绩,产品和大领导一起被干走了。

后面我们整体调整了部门,部门领导是研发出身。干了几个月之后,领导也比较认可我的能力,让我带团队做一个中型项目,下面大概有 10 号人,包括前后端和算法。也被提升为开发经理。

最后因为工资、工作距离(老婆怀孕,离家太远)以及工作内容等原因,跳槽到了下一家互联网公司。

6 入行互联网

凭借着 5 年的工作经历,还算可以的技术广度(毕竟之前啥都干),985 学校的非全研究生学历,以及还过得去的技术能力。找到了一家知名度还可以的互联网公司做商城开发。

这个部门是公司新成立的部门,领导是有好几家一线互联网经验的老程序员,技术过硬,管理能力强,会做人。组内成员都年轻有干劲。本打算在公司大干一场,涨涨技术深度(之前都是传统企业,技术深度不够,但是广度可以)。

结果因为政策调整,整个部门被裁,只剩下直属领导以及领导的领导。这一年是 2020 年。这个时候,我在这个公司还不到 1 年。

7 再前行

拿着上家公司的大礼包,马上开始改简历,投简历,面试,毕竟还有房贷要还(找了个好老婆,她们家出了大头,付了首付),马上还有娃要养,一天也不敢歇息。

经过一个半月的面试,虽然挂的多,通过的少。最终还是拿了 3 个不错的offer,一个滴滴(滴滴面经)、一个XXX网络(最终入职,薪资跟滴滴基本差不多,技术在市场上认可度也还不错。)以及一个建信金科的offer。

因为大厂部门也和上家公司一样,是新组建的部门,心有余悸。然后也还年轻,不想去银行躺平,也怕银行也不靠谱,毕竟现在都是银行科技公司,干几年被裁,更没有出路。最终入职XXX网络。

8 寒冬

入职XXX网络之后,开始接入公司的各种技术组件,以及看到比较成熟的需求提出、评估、开发、测试、发布规范。也看到公司各个业务中心、支撑中心的访问量,感叹还是大公司好,流程规范,流量大且有挑战性。

正要开心的进入节奏,还没转正呢(3 个月转正),组内一个刚转正的同事被裁,瞬间慌得一批。

刚半年呢,听说组内又有 4 个裁员指标,已经开始准备简历了。幸运的是,这次逃过一劫。

现在已经 1 年多了,在这样一个裁员消息满天飞的年代,还有一份不错的工作,很幸运、也很忐忑,也在慢慢寻找自己未来的路,共勉~

9 总结

整体来看,我对自己的现状还算满意,从一个高中每个月 300 块钱生活费家里都拿不出来;高考志愿填报,填学校看心情想去哪,填专业看专业名字的的村里娃,走到现在在北京有个不错的工作,组建了幸福的家庭,买了个不大不小的房子的城里娃。不管怎么样,也算给自己立足打下了基础,留在了这个有更多机会的城市;也给后代一个更高的起点。

但是,我也知道,现在的状态并不稳固,互联网工作随时可能会丢,家庭成员的一场大病可能就会导致整个家庭回到解放前。

所以,主业上,我的规划就是,尽力提升自己的技术能力和管理能力,争取能在中型公司当上管理层,延迟自己的下岗年龄;副业上,提升自己的写作能力,尝试各种不同的主题,尝试给各个自媒体投稿,增加副业收入。

希望自己永远少年,不要下岗~


作者:六七十三
链接:https://juejin.cn/post/7173506418506072101
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-Widget重装上阵

如果要在Android系统中找一个一直存在,但一直被人忽略,而且有十分好用的功能,那么Widget,一定算一个。这个从Android 1.x就已经存在的功能,经历了近10年的迭代,在遭到无数无视和白眼之后,又重新回到了大家的视线之内,当然,也有可能是App内部...
继续阅读 »

如果要在Android系统中找一个一直存在,但一直被人忽略,而且有十分好用的功能,那么Widget,一定算一个。这个从Android 1.x就已经存在的功能,经历了近10年的迭代,在遭到无数无视和白眼之后,又重新回到了大家的视线之内,当然,也有可能是App内部已经没东西好卷了,所以大家又把目光放到了App之外,但不管怎样,Widget在Android 12之后,都开始焕发一新,官网镇楼,让我们重新来了解下这个最熟悉的陌生人。

developer.android.com/develop/ui/…

Widget使用的是RemoteView,这与Notification的使用如出一辙,RemoteView是继承自Parcelable的组件,可以跨进程使用。在Widget中,通过AppWidgetProvider来管理Widget的行为,通过RemoteView来对Widget进行布局,通过AppWidgetManager来对Widget进行刷新。基本的使用方式,我们可以通过一套模板代码来实现,在Android Studio中,直接New Widget即可。这样Android Studio就可以自动为你生成一个Widget的模板代码,详细代码我们就不贴了,我们来分析下代码的组成。

首先,每个Widget都包含一个AppWidgetProvider。这是Widget的逻辑管理类,它继承自BroadcastReceiver,然后,我们需要在清单中注册这个Receiver,并在meta-data中指定它的配置文件,它的配置文件是一个xml,这里描述的是添加Widget时展示的一些信息。

从这些地方来看,其实Widget的使用还是比较简单的,所以本文也不准备来讲解这些基础知识,下面我们针对开发中会遇到的一些实际需求来进行分析。

appwidget-provider配置文件

这个xml文件虽然简单,但还是有些有意思的东西的。

尺寸

在这里我们可以为Widget配置尺寸信息,通过maxResizeWidth、maxResizeHeight和minWidth、minHeight,我们可以大致将Widget的尺寸控制在MxN的格子内,这也是Widget在桌面上的展示方式,它并不是通过指定的宽高来展示的,而是桌面所占据的格子数。

官方设计文档中,对格子数和尺寸的转换标准,有一个表格,如下所示。

我们在设计的时候,也应该尽量遵循这个尺寸约束,避免在桌面上展示异常。在Android12之后,描述文件中,还增加了targetCellWidth和targetCellHeight两个参数,他们可以直接指定Widget所占据的格子数,这样更加方便,但由于它仅支持Android12+,所以,通常这些属性会一起设置。

有意思的是这个尺寸标准并不适用于所有的设备,因为ROM的碎片化问题,各个厂商的桌面都不一样,所以。。。只能参考参考。

updatePeriodMillis

这个参数用于指定Widget的被动刷新频率,它由系统控制,所以具有很强的不定性,而且它也不能随意设置,官网上对这个属性的限制如下所示。

updatePeriodMillis只支持设置30分钟以上的间隔,即1800000milliseconds,这也是为了保证后台能耗,即使你设置了小于30分钟的updatePeriodMillis,它也不会生效。

对于Widget来说,updatePeriodMillis控制的是系统被动刷新Widget的频率,如果当前App是活着的,那么随时可以通过广播来修改Widget。

而且这个值很有可能因为不同ROM而不同,所以,这是一个不怎么稳定的刷新机制。

其它

除了上面我们提到的一些属性,还有一些需要留意的。

  • resizeMode:拉伸的方向,可以设置为horizontal|vertical,表示两边都可以拉伸。
  • widgetCategory:对于现在的App来说,只能设置为home_screen了,5.0之前可以设置为锁屏,现在基本已经不用了。
  • widgetFeatures:这是Android12之后新加的属性,设置为reconfigurable之后,就可以直接调整Widget的尺寸,而不用像之前那样先删除旧的Widget再添加新的Widget了。

配置表

这个配置文件的主要作用,就是在添加Widget时,展示一个简要的描述信息,所以,一个App中是可以存在多个描述xml文件的,而且有几个描述文件,添加时,就会展示几个Widget的缩略图,通常我们会创建几个不同尺寸的Widget,例如2x2、4x2、4x1等,并创建多个xml面试文件,从而让用户可以选择添加哪一个Widget。

不过在Android12之后,设置一个Widget,通过拉动来改变尺寸,就可以动态改变Widget的不同展示效果了,但这仅限于Android12+,所以需要权衡使用利弊。

configure

通过configure属性可以配置添加Widget时的Configure Activity,这个在创建默认的Widget项目时就已经可以选择创建了,所以不多讲了,实际上就是一个简单的Activity,你可以配置一些参数,写入SP,然后在Widget中进行读取,从而实现自定义配置。

应用内唤起Widget的添加页面

大部分时候,我们都是通过在桌面上长按的方式来添加Widget,但是在Android API 26之后,系统提供了一直新的方式来在应用内唤起——requestPinAppWidget。

文档如下。

developer.android.com/reference/a…

代码如下所示。

fun requestToPinWidget(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val appWidgetManager: AppWidgetManager? = getSystemService(context, AppWidgetManager::class.java)
appWidgetManager?.let {
val myProvider = ComponentName(context, NewAppWidget::class.java)
if (appWidgetManager.isRequestPinAppWidgetSupported) {
val pinnedWidgetCallbackIntent = Intent(context, MainGroupActivity::class.java)
val successCallback: PendingIntent = PendingIntent.getBroadcast(context, 0,
pinnedWidgetCallbackIntent, PendingIntent.FLAG_UPDATE_CURRENT)
appWidgetManager.requestPinAppWidget(myProvider, null, successCallback)
}
}
}
}

通过这种方式,就可以直接唤起Widget的添加入口,从而避免用户手动在桌面中进行添加。

应用内主动更新Widget

前面我们提到了,当App活着的时候,可以主动来更新Widget,而且有两种方式可以实现,一种是通过广播ACTION_APPWIDGET_UPDATE,触发Widget的update回调,从而进行更新,代码如下所示。

val manager = AppWidgetManager.getInstance(this)
val ids = manager.getAppWidgetIds(ComponentName(this, XXXWidget::class.java))
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(updateIntent)

这种方式的本质就是发送更新的广播,除此之外,还可以使用AppWidgetManager来直接对Widget进行更新,代码如下。

val remoteViews = RemoteViews(context.packageName, R.layout.xxx)
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, XXXWidgetProvider::class.java)
appWidgetManager.updateAppWidget(componentName, remoteViews)

这种方式就是通过AppWidgetManager来对指定的Widget进行修改,使用新的RemoteViews来更新当前Widget。

这两种方式一种是主动替换,一种是被动刷新,具体的使用场景可以根据业务的不同来使用不同的方式。

应用外被动更新Widget

产品现在重新开始重视Widget的一个重要原因,实际上就是App内部卷不动了,Widget可以在不打开App的情况下,对App进行引流,所以,应用外的Widget更新,就是一个很重要的组成部分,Widget需要展示用户感兴趣的内容,才能触发用户的点击。

前面我们提到了通过设置updatePeriodMillis来进行Widget的更新,但是这种方式存在一些使用限制,如果你需要完全自主的控制Widget的刷新,那么可以使用AlarmManager或者WorkManager,类似的代码如下所示。

private fun scheduleUpdates(context: Context) {
val activeWidgetIds = getActiveWidgetIds(context)
if (activeWidgetIds.isNotEmpty()) {
val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
val pendingIntent = getUpdatePendingIntent(context)
context.alarmManager.set(
AlarmManager.RTC_WAKEUP,
nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
pendingIntent
)
}
}

当然,这种方式也同样会受到ROM的限制,所以说,不管是WorkManager还是AlarmManager,或者是updatePeriodMillis,都不是稳定可靠的,随它去吧,强扭的瓜不甜。

一般来说,使用updatePeriodMillis就够了,Widget的目的是为了引流,对内容的实时性其实并不是要求的那么严格,updatePeriodMillis在大部分场景下,都是够用的。

多布局动态适配

由于在Android12之后,用户可以在单个Widget上进行修改,从而修改Widget当前的配置,所以,用户在拖动修改Widget的尺寸时,就需要动态去调整Widget的布局,以自动适应不同的尺寸。我们可以通过下面的方式,来进行修改。

internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, widgetData: AppWidgetData) {
val views41 = RemoteViews(context.packageName, R.layout.new_app_widget41).also { updateView(it, context, appWidgetId, widgetData) }
val views42 = RemoteViews(context.packageName, R.layout.new_app_widget42).also { updateView(it, context, appWidgetId, widgetData) }
val views21 = RemoteViews(context.packageName, R.layout.new_app_widget21).also { updateView(it, context, appWidgetId, widgetData) }
val viewMapping: Map<SizeF, RemoteViews> = mapOf(
SizeF(180f, 110f) to views21,
SizeF(270f, 110f) to views41,
SizeF(270f, 280f) to views42
)
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
}

private fun updateView(remoteViews: RemoteViews, context: Context, appWidgetId: Int, widgetData: AppWidgetData) {
remoteViews.setTextViewText(R.id.xxx, widgetData.xxx)
}

它的核心就是RemoteViews(viewMapping),通过这个就可以动态适配当前用户选择的尺寸。

那么如果是Android12之前呢?

我们需要重写onAppWidgetOptionsChanged回调来获取当前Widget的宽高,从而修改不同的布局,模板代码如下所示。

override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
   super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
   val options = appWidgetManager.getAppWidgetOptions(appWidgetId)

   val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
   val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)

   val rows: Int = getWidgetCellsM(minHeight)
   val columns: Int = getWidgetCellsN(minWidth)
   updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
}

fun getWidgetCellsN(size: Int): Int {
var n = 2
while (73 * n - 16 < size) {
++n
}
return n - 1
}

fun getWidgetCellsM(size: Int): Int {
var m = 2
while (118 * m - 16 < size) {
++m
}
return m - 1
}

其中的计算公式,n x m:(73n-16)x(118m-16)就是文档中提到的算法。

但是这种方案有一个致命的问题,那就是不同的ROM的计算方式完全不一样,有可能在Vivo上一个格子的高度只有80,但是在Pixel中,一个格子就是100,所以,在不同的设备上显示的n x m不一样,也是很正常的事。

也正是因为这样的问题,如果不是只在Android 12+的设备上使用,那么通常都是固定好Widget的大小,避免使用动态布局,这也是没办法的权衡之举。

RemoteViews行为

RemoteViews不像普通的View,所以我们不能像写普通布局的方式一样来操纵View,但RemoteViews提供了一些set方法来帮助我们对RemoteViews中的View进行修改,例如下面的代码。

remoteViews.setTextViewText(R.id.title, widgetData.xxx)

再比如点击后刷新Widget,实际上就是创建一个PendingIntent。

val intentUpdate = Intent(context, XXXAppWidget::class.java).also {
it.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
it.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
}
val pendingUpdate = PendingIntent.getBroadcast(
context, appWidgetId, intentUpdate,
PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.btn, pendingUpdate)

原理

RemoteViews通常用在通知和Widget中,分别通过NotificationManager和AppWidgetManager来进行管理,它们则是通过Binder来和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信,所以,RemoteViews实际上是运行在SystemServer中的,我们在修改RemoteViews时,就需要进行跨进程通信了,而RemoteViews封装了一系列跨进程通信的方法,简化了我们的调用,这也是为什么RemoteViews不支持全部的View方法的原因,RemoteViews抽象了一系列的set方法,并将它们抽象为统一的Action接口,这样就可以提供跨进程通信的效率,同时精简核心的功能。

如何进行后台请求

Widget在后台进行更新时,通常会请求网络,然后根据返回数据来修改Widget的数据展示。

AppWidgetProvider本质是广播,所以它拥有和广播一致的生命周期,ROM通常会定制广播的生命周期时间,例如设置为5s、7s,如果超过这个时间,那么就会产生ANR或者其它异常。

所以,我们一般不会把网络请求直接写在AppWidgetProvider中,一个比较好的方式,就是通过Service来进行更新。

首先我们创建一个Service,用来进行后台请求。

class AppWidgetRequestService : Service() {

override fun onBind(intent: Intent): IBinder? {
return null
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val appWidgetManager = AppWidgetManager.getInstance(this)
val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
if (allWidgetIds != null) {
for (appWidgetId in allWidgetIds) {
BackgroundRequest.getWidgetData {
NewAppWidget.updateAppWidget(this, appWidgetManager, appWidgetId, AppWidgetData(book1Cover = it))
}
}
}
return super.onStartCommand(intent, flags, startId)
}
}

在onStartCommand中,我们创建一个协程,来进行真正的网络请求。

object BackgroundRequest : CoroutineScope by MainScope() {
fun getWidgetData(onSuccess: (result: String) -> Unit) {
launch(Dispatchers.IO) {
val response = RetrofitClient.getXXXApi().getXXXX()
if (response.isSuccess) {
onSuccess(response.data.toString())
}
}
}
}

所以,在AppWidgetProvider的update里面,就需要进行下修改,将原有逻辑改为对Service的启动。

class NewAppWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val intent = Intent(context.applicationContext, AppWidgetRequestService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}

动画?

有必要这么卷吗,Widget里面还要加动画。由于RemoteViews里面不能实现正常的View动画,所以,Widget里面的动画基本都是通过类似「帧动画」的方式来实现的,即将动画抽成一帧一帧的图,然后通过Animator来进行切换,从而实现动画效果,群友给出了一篇比较好的实践,大家可以参考参考,我就不卷了。

juejin.cn/post/704862…

Widget的使用场景主要还是以实用功能为主,只有让用户觉得有用,才能锦上添花给App带来更多的活跃,否则只能是鸡肋。


作者:xuyisheng
链接:https://juejin.cn/post/7140445483964563470
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

请自信的大声告诉面试官forEach跳不出循环

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!foreach 跳不出循环为什么呢?先看看foreach大体实现。Array.prototype.customForEach = function (fn) { ...
继续阅读 »

如果面试官,或者有人问你foreach怎么跳出循环,请你大声的告诉ta,跳不出!!!!!!!!!!

foreach 跳不出循环

为什么呢?

先看看foreach大体实现。

Array.prototype.customForEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i, this)
}
}

list.customForEach((item, i, list) => {
console.log(item, i, list)
})

let list = [1,2,3,4,5]

list.forEach((item,index,list)=>{
console.log(item,index,list)
})

list.customForEach((item,index,list)=>{
console.log(item,index,list)
})

两个输出的结果是一样的没啥问题,这就是foreach的大体实现,既然都知道了它的实现,那么对它为什么跳不出循环♻️应该都知道了,再不清楚的话,再看一下下面的例子。


function demo(){
return 'demo'
}

function demo2(){
demo()
return 'demo2'
}

demo()

在demo2函数里面调用demo函数,demo函数的return能阻止demo2函数下面的执行吗?很明显不行啊,demo函数里的return跟demo2函数一点关系都没有。现在你再回头看看foreach的实现,就明白它跳不出循环一清二楚了。

有点同学说不是可以通过抛出错误跳出循环吗?是的。看看下面例子。


let list = [1,2,3,4,5]

try {
list.forEach((item, index, list) => {
if (index === 2) {
throw new Error('demo')
}
console.log(item)
})
} catch (e) {
// console.log(e)
}

结果是我们想要,但是你看代码,哪个正常人会这样写代码?是非foreach不用吗?还是其他的循环关键字不配呢。

end

有反驳在评论区,show me your code !!!!!!!!!


作者:啥也不懂的前端
链接:https://juejin.cn/post/7259595485090906149
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

环信EaseCallKit 使用指南及AppServer相关配置

简介:环信EaseCallKit 是结合声网音视频结合开发的音视频 UI 库,实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。环信官网EaseCallKit 使用文档: http://do...
继续阅读 »

简介:

环信EaseCallKit 是结合声网音视频结合开发的音视频 UI 库,实现了一对一语音和视频通话以及多人音视频通话的功能。基于 EaseCallKit 可以快速实现通用音视频功能。

在官网使用指南里面介绍了EaseCallKit 快速集成。如果项目中只使用了音视频,根据EaseCallKit 步骤集成即可。
下面介绍的是如何在环信官网Demo 中修改配置,跑Demo 音视频 和AppServer

前提条件:

需要分别创建 环信应用及 声网应用
环信Demo中音视频的介绍,以V4.0.3 Demo 为例
快速跑通环信IM Android Demo 参考之前文章(见链接:https://www.imgeek.net/article/825363562

环信IM Demo体验:https://www.easemob.com/download/demo

Android 项目快速集成音视频:

远程依赖不可以对代码进行修改,集成前可以先体验Demo是否可以满足业务需求,如果需要对页面进行修改,可以把ease-call-kit 以module 的形式导进项目中,对其进行修改
远程依赖:implementation 'io.hyphenate:ease-call-kit:4.0.3'
本地依赖源码链接:https://github.com/easemob/easecallkitui-android

本地Module的导入教程

1、从链接中下载源码并解压,下面是解压完的目录,需要导入的是红圈中的文件:

2、直接拖进项目里面

也可以通过Android Studio 导进去

3、在 build.gradle (App级别下)添加 api project(':ease-call-kit')

4、在settings.gradle下添加api project(':ease-call-kit')

添加完以后编译项目即可

Android 项目中的相关配置:

关于EaseCallKit 初始化在DemoHelper类下,见下图。

注:AgoraAppId这里是从声网平台注册的,在项目中需要切换,不可以直接上线使用


在Demo中 DemoHelper 类下,可以看到两个请求接口如下图
第一个接口:获取声网token ,这里用于音视频通话
第二个接口:根channelName和声网uId获取频道内所有人的UserId

注:这里的两个接口对应的appServer中的接口,a1.easemob.com 是环信域名,搭建appServer 以后需要将这里换成自己的域名


在环信Demo中,DemoHelper类中可以看到Demo是通过AppServer获取到token,这里获取到的token用于音视频

注:如果在测试中遇到音视频无法正常拨打接通,可以使用断点判断这里请求是否成功,环信Demo 中默认的token 请求接口不可以直接用于项目正常上线



代码简介:

获取到token和uid以后通过回调的形式传到音视频页面



下面是加入音视频的api 调用

注:在测试或者集成中如果遇到问题,可以按上面的步骤断点查看,是否有报错

Demo 中音视频点击代码如下

Java服务端项目中的配置:

appserver中需要修改的配置:
orgName:环信appkey 中#前面的部分
appName:环信appkey中#后面的部分
agoraAppId 和agoraCert 从声网console 下获取

修改完配置正常运行项目
Uid是服务端生成的随机数

扩展阅读:
手把手教你从零开始集成声网音视频功能(iOS版):https://www.imgeek.net/article/825364398

收起阅读 »

北宋 赵普:成事法则

前言 老规矩,唠唠嗑~ 1、历史才是真正值得我们去学习、去研究的 我们处于21世纪,相对于比以前更加先进,经济也更加发达了,但是你也会发现信息碎片化了,然后我们每天接收各种各样的海量的数据,真正对我们成长没有一丁点的好处。 我觉得原因有几个:第一还是信息碎片...
继续阅读 »

8e0dc884457ed5fb795606f4a251441e.jpg


前言




老规矩,唠唠嗑~


1、历史才是真正值得我们去学习、去研究的


我们处于21世纪,相对于比以前更加先进,经济也更加发达了,但是你也会发现信息碎片化了,然后我们每天接收各种各样的海量的数据,真正对我们成长没有一丁点的好处。


我觉得原因有几个:第一还是信息碎片化,东一块西一块,你无法吃透某个领域,形成自己的知识体系;第二你接收到的信息大部分是别人想让你知道的,你被人当“枪”使了,第三个知识是别人嚼过一次的半成品,我们习惯性的搬运,没有经过自己的思考,那么这些信息也是没有多大价值的。


比如抖音有个“不提倡苦难让我们成长的一种说法”,我的思考是这样的:苦难是环境+个人因果里面一种情况,就像一瓶水,里面有水、塑料瓶、包装构造成的一种东西。我们学过王阳明在龙场悟道,曾国藩在经历贬官、人生挫折之后,换了个人。里面最重要一点,是人在挫折面前存活了下来,然后是做事方式、思考方式变了,这才是最为关键的。


如果你没有变化,苦难还是那个苦难,不提倡感恩苦难还是那么一句话,没有一点营养。经过我们这番思考之后,如何改变做事方式,如何改变思考方式才是我们要提高的,才是有营养的东西!



我们虽然处于一个现代化的时代,但是做事方式、思考方式甚至智慧,不一定超越古人。



所以冯唐老师:“历史是一块磨刀石,在不同人眼中,增长智慧”


2、成长需要老师


在我人生成长路上,我都会选某些优秀的人作为榜样去学习,这使得我成长飞快。最重要的原因是,人容易以自己常用的方法来处理事情,那么就会固守踏步不前,正是因为有这些优秀的老师,我们才能一直前进学习。


我有这么一个亲戚,我老叔,他儿子都很厉害,厅级,我老叔为人处事比较强,家教里面有这么一段话:就是跟别人有矛盾不要到处讲,甚至反其道帮助别人。当然啦,这个一般人达不到的境界,我们来看看历史上韩信,当他贫穷的时候受到了胯下之辱,从别人胯下爬过去,当他功成名就的时候没有去记仇,所以有这等气概之人一般都会有所作为。


当然我们今天想聊的另一个角色:北宋 赵普,就不是这样有气概的一个人,我们不能以一个很高的标准去要求其他人这样做,气概会分为好几等,曾国藩讲看一个人未来的成就,就看他有多少气概。


我们今天这位是北宋 赵普,我是在《百家讲坛》王立群老师讲述北宋历史中认识的,三次为相,最吸引我的点是做事犀利,眼光同样犀利,这正是我们成事所追求的。


北宋-赵普 生平




他辅佐了两代皇帝,三次拜相,跟历史上很著名的事件都有关联,陈桥兵变,黄袍加身,将赵匡胤扶上了皇帝,然后在治理方针上也有独到的见解,先治理南方再北方,整顿内政,做事相当犀利,当然缺点也是有的,这里我们略过,因为我们主要学习里面的成事法则。


有人讲他没有什么业绩,或者重大的贡献,我们来看看他做了哪些事情


他所做突出事件


1、陈桥兵变,黄袍加身 关键参与者


赵普不仅是宰相,还是谋士。960年,在赵光义和赵普等人的策划下,太祖在陈桥驿发动兵变,随即成功登基,赵普因拥立之功升任右谏议大夫。


我们跟现实对比,就像你能否帮你老板提升一个level,甚至好几个level,做的东西能够提升整个团队影响力,产出可以影响其他团队的价值的事情,这个非常有难度的。


2、治理方针


赵匡胤经常登门找赵普,有一次问他你的治理想法是什么,他说先南方后北方,南方相对比较肥沃,资源比较多,其次北方有契丹,让另一个敌对势力去抵挡,这样不会消耗精力,可以看出他在平时是有研究的,不会在这种突发的检查中慌乱。


杯酒释兵权,也是在他的劝说下,赵匡胤实施了,对于稳定的局势是有帮助的。


3、二次拜相给出有价值的东西


太宗赵光义即位的时候,位置不太符合常规,赵普给出当年的“金匮之盟”,让太宗的位置合理了,然后让他儿子继位也搞定了,赵普给了很有价值的东西。


4、治理内政


打掉了“五人帮”,还有另一个“大师集团”,这是非常厉害的两件事,比如说让你打掉你们公司某些团体哈哈哈,可想而知很难的,第一个没有背景,第二个很容易翻车,特别是“大师集团”,那是太宗赵光义专门去保的,最后还是被赵普办成铁案,翻不了盘,在我眼中看到了做事的犀利。


5、为老板解围


前几任老板打契丹,都无功而返,很多人站出来指责,最后是赵普承担了所有,然后在晚年也极力推荐人才,标准是人靠谱,不耍聪明。


成事法则




1、有眼界、有经验


我们做一件事的时候,需要去研究,别人有没有比较好的方案,而不是瞎折腾,比如apm方案、全链路灰度,其实业界早有成熟的方案让我们来借鉴,这样我们再结合自己团队的特点特殊化,让整个技术方案更加稳健。


其次是不会慌乱,如果你对问题平时有研究,就不会自乱阵脚,看到一个段子:泰森面对重拳也会晕过去,正是因为实力够强,练习够多,才能轻松应对。


2、干对事、干好事


赵普虽然说没有做出什么杰出贡献,所以他每次对这个岗位都很有动力,给他一个空间他能干出很大的事业出来,但是你仔细去看他干过的事情,非常细致,也很有价值。


比如说我们日常工作中也有一些重要的工作,也有一些修修补补的工作,当然那些重要的工作价值会更大,不一定是你能拿到的,你懂的,我们尽可能眼光要犀利点,比如说将一个功能做成一个系统,这样它的价值是辐射性向其他项目、其他团队产出价值。


做事计划周密,让我想起一个朋友跟我讲他们架构师推ddd,最后没推成被辞退。当我们在做计划前,需要调研,明白ddd解决的问题是什么,团队推不下去的原因可能是什么,团队的特点是怎样的,我们的计划应该怎么调整,这是非常关键,最后跟主要干系人沟通,让他们参与进来,而不是自己yy一套不适合方案然后直接推,你都没有沟通过别人的意见,还有没有获得别人的支持,所以在推的过程会很多问题。


说直白点,ddd是为了解决系统庞大之后错综复杂的关系,提高扩展能力,同时你也要意识到业务团队的特点,业绩为大,时间紧迫,不可能全套搬迁到应用到,只能针对性的改造!


3、职场角色扮演


小兵角色,就是博主这个角色啦,哈哈。我认为很重要的一点:主动性,只有你主动参与事件的一员,你才能改变点什么,做出点什么贡献,其次整个团队、项目也是在这种主动性基础推进更快,更稳健。因为规则只能保证基本的流程,而且它也需要优化的,也没有创造性的能力。第二个:有眼力,能够发现有价值的闪光点,第三个:做事计划周密,第四个;行动力,跟第一个有所重叠。


领导的角色,因为我这方面经验比较少,所以只能大概聊聊。第一个有主见,有原则,你对某些问题有研究,有自己的判断,赵普正是因为这个被百官推崇,不会说你老板一会改一个需求,你的团队跟着变,这样做不好事情的,团队劳累,所以需要一个比较强势的领导。第二个,有背景、有支持度,在公司做事还是在其他地方做事都一样,需要点人脉、资源,不然你的方案比较难推进的,没有主要干系人的支持,你怎么获得大多数人的支持呢对吧。第三个:同样是有眼光,这个不分等级,就跟我们高考一样,只不过题型变了,解题思路不变的,什么事情价值更大一些。


最后引用孙子一段话“知己知彼,百战不殆”,这句话非常简单,但是你真正去做会发现很多的细节,所以我们上面总结很多的要点,关键还是在事上去练我们这种成事的技能。


作者:大鸡腿同学
来源:juejin.cn/post/7259372449833254969
收起阅读 »

10年程序员,想对新人说什么?

前言 最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。 这篇文章根据我多年的工作经验,给新人总结了25条建议,希望对你会有...
继续阅读 »

前言


最近知乎上,有一位大佬邀请我回答下面这个问题,看到这个问题我百感交集,感触颇多。图片在我是新人时,如果有前辈能够指导方向一下,分享一些踩坑经历,或许会让我少走很多弯路,节省更多的学习的成本。


这篇文章根据我多年的工作经验,给新人总结了25条建议,希望对你会有所帮助。


1.写好注释


很多小伙伴不愿意给代码写注释,主要有以下两个原因:



  1. 开发时间太短了,没时间写注释。

  2. 《重构》那本书说代码即注释。


我在开发的前面几年也不喜欢写注释,觉得这是一件很酷的事情。


但后来发现,有些两年之前的代码,业务逻辑都忘了,有些代码自己都看不懂。特别是有部分非常复杂的逻辑和算法,需要重新花很多时间才能看明白,可以说自己把自己坑了。


没有注释的代码,不便于维护。


因此强烈建议大家给代码写注释。


但注释也不是越多越好,注释多了增加了代码的复杂度,增加了维护成本,给自己增加工作量。


我们要写好注释,但不能太啰嗦,要给关键或者核心的代码增加注释。我们可以写某个方法是做什么的,主要步骤是什么,给算法写个demo示例等。


这样以后过了很长时间,再去看这段代码的时候,也会比较容易上手。


2.多写单元测试


我看过身边很多大佬写代码有个好习惯,比如新写了某个Util工具类,他们会同时在test目录下,给该工具类编写一些单元测试代码。


很多小伙伴觉得写单元测试是浪费时间,没有这个必要。


假如你想重构某个工具类,但由于这个工具类有很多逻辑,要把这些逻辑重新测试一遍,要花费不少时间。


于是,你产生了放弃重构的想法。


但如果你之前给该工具类编写了完整的单元测试,重构完成之后,重新执行一下之前的单元测试,就知道重构的结果是否满足预期,这样能够减少很多的测试时间。


多写单元测试对开发来说,是一个非常好的习惯,有助于提升代码质量。


即使因为当初开发时间比较紧,没时间写单元测试,也建议在后面空闲的时间内,把单元测试补上。


3.主动重构自己的烂代码


好的代码不是一下子就能写成的,需要不断地重构,修复发现的bug。


不知道你有没有这种体会,看自己1年之前写的代码,简直不忍直视。


这说明你对业务或者技术的理解,比之前更深入了,认知水平有一定的提升。


如果有机会,建议你主动重构一下自己的烂代码。把重复的代码,抽取成公共方法。有些参数名称,或者方法名称当时没有取好的,可以及时修改一下。对于逻辑不清晰的代码,重新梳理一下业务逻辑。看看代码中能不能引入一些设计模式,让代码变得更优雅等等。


通过代码重构的过程,以自我为驱动,能够不断提升我们编写代码的水平。


4.代码review很重要


有些公司在系统上线之前,会组织一次代码评审,一起review一下这个迭代要上线的一些代码。


通过相互的代码review,可以发现一些代码的漏洞,不好的写法,发现自己写代码的坏毛病,让自己能够快速提升。


当然如果你们公司没有建立代码的相互review机制,也没关系。


可以后面可以多自己review自己的代码。


5.多用explain查看执行计划


我们在写完查询SQL语句之后,有个好习惯是用explain关键字查看一下该SQL语句有没有走索引


对于数据量比较大的表,走了索引和没有走索引,SQL语句的执行时间可能会相差上百倍。


我之前亲身经历过这种差距。


因此建议大家多用explain查看SQL语句的执行计划。


关于explain关键字的用法,如果你想进一步了解,可以看看我的另外一篇文章《explain | 索引优化的这把绝世好剑,你真的会用吗?》,里面有详细的介绍。


6.上线前整理checklist


在系统上线之前,一定要整理上线的清单,即我们说的:checklist


系统上线有可能是一件很复杂的事情,涉及的东西可能会比较多。


假如服务A依赖服务B,服务B又依赖服务C。这样的话,服务发版的顺序是:CBA,如果顺序不对,可能会出现问题。


有时候新功能上线时,需要提前执行sql脚本初始化数据,否则新功能有问题。


要先配置定时任务。


上线之前,要在apollo中增加一些配置。


上线完成之后,需要增加相应的菜单,给指定用户或者角色分配权限。


等等。


系统上线,整个过程中,可能会涉及多方面的事情,我们需要将这些事情记录到checklist当中,避免踩坑。


7.写好接口文档


接口文档对接口提供者,和接口调用者来说,都非常重要。


如果你没有接口文档,别人咋知道你接口的地址是什么,接口参数是什么,请求方式时什么,接口多个参数分别代码什么含义,返回值有哪些字段等等。


他们不知道,必定会多次问你,无形当中,增加了很多沟通的成本。


如果你的接口文档写的不好,写得别人看不懂,接口文档有很多错误,比如:输入参数的枚举值,跟实际情况不一样。


这样不光把自己坑了,也会把别人坑惨。


因此,写接口文档一定要写好,尽量不要马马虎虎应付差事。


如果对写接口文档比较感兴趣,可以看看我的另一篇文章《瞧瞧别人家的API接口,那叫一个优雅》,里面有详细的介绍。


8.接口要提前评估请求量


我们在设计接口的时候,要跟业务方或者产品经理确认一下请求量。


假如你的接口只能承受100qps,但实际上产生了1000qps。


这样你的接口,很有可能会承受不住这么大的压力,而直接挂掉。


我们需要对接口做压力测试,预估接口的请求量,需要部署多少个服务器节点。


压力测试的话,可以用jmeter、loadRunner等工具。


此外,还需要对接口做限流,防止别人恶意调用你的接口,导致服务器压力过大。


限流的话,可以基于用户id、ip地址、接口地址等多个维度同时做限制。


可以在nginx层,或者网关层做限流。


9.接口要做幂等性设计


我们在设计接口时,一定要考虑并发调用的情况。


比如:用户在前端页面,非常快的点击了两次保存按钮,这样就会在极短的时间内调用你两次接口。


如果不做幂等性设计,在数据库中可能会产生两条重复的数据。


还有一种情况时,业务方调用你这边的接口,该接口发生了超时,它有自动重试机制,也可能会让你这边产生重复的数据。


因此,在做接口设计时,要做幂等设计。


当然幂等设计的方案有很多,感兴趣的小伙伴可以看看我的另一篇文章《高并发下如何保证接口的幂等性?》。


如果接口并发量不太大,推荐大家使用在表中加唯一索引的方案,更加简单。


10.接口参数有调整一定要慎重


有时候我们提供的接口,需要调整参数。


比如:新增加了一个参数,或者参数类型从int改成String,或者参数名称有status改成auditStatus,参数由单个id改成批量的idList等等。


建议涉及到接口参数修改一定要慎重。


修改接口参数之前,一定要先评估调用端和影响范围,不要自己偷偷修改。如果出问题了,调用方后面肯定要骂娘。


我们在做接口参数调整时,要做一些兼容性的考虑。


其实删除参数和修改参数名称是一个问题,都会导致那个参数接收不到数据。


因此,尽量避免删除参数和修改参数名。


对于修改参数名称的情况,我们可以增加一个新参数,来接收数据,老的数据还是保留,代码中做兼容处理。


11.调用第三方接口要加失败重试


我们在调用第三方接口时,由于存在远程调用,可能会出现接口超时的问题。


如果接口超时了,你不知道是执行成功,还是执行失败了。


这时你可以增加自动重试机制


接口超时会抛一个connection_timeout或者read_timeout的异常,你可以捕获这个异常,用一个while循环自动重试3次。


这样就能尽可能减少调用第三方接口失败的情况。


当然调用第三方接口还有很多其他的坑,感兴趣的小伙伴可以看看我的另一篇文章《我调用第三方接口遇到的13大坑》,里面有详细的介绍。


12.处理线上数据前,要先备份数据


有时候,线上数据出现了问题,我们需要修复数据,但涉及的数据有点多。


这时建议在处理线上数据前,一定要先备份数据


备份数据非常简单,可以执行以下sql:


create table order_2022121819 like `order`;
insert into order_2022121819 select * from `order`;

数据备份之后,万一后面哪天数据处理错了,我们可以直接从备份表中还原数据,防止悲剧的产生。


13.不要轻易删除线上字段


不要轻易删除线上字段,至少我们公司是这样规定的。


如果你删除了某个线上字段,但是该字段引用的代码没有删除干净,可能会导致代码出现异常。


假设开发人员已经把程序改成不使用删除字段了,接下来如何部署呢?


如果先把程序部署好了,还没来得及删除数据库相关表字段。


当有insert请求时,由于数据库中该字段是必填的,会报必填字段不能为空的异常。


如果先把数据库中相关表字段删了,程序还没来得及发。这时所有涉及该删除字段的增删改查,都会报字段不存在的异常。


所以,线上环境字段不要轻易删除。


14.要合理设置字段类型和长度


我们在设计表的时候,要给相关字段设置合理的字段类型和长度。


如果字段类型和长度不够,有些数据可能会保存失败。


如果字段类型和长度太大了,又会浪费存储空间。


我们在工作中,要根据实际情况而定。


以下原则可以参考一下:



  • 尽可能选择占用存储空间小的字段类型,在满足正常业务需求的情况下,从小到大,往上选。

  • 如果字符串长度固定,或者差别不大,可以选择char类型。如果字符串长度差别较大,可以选择varchar类型。

  • 是否字段,可以选择bit类型。

  • 枚举字段,可以选择tinyint类型。

  • 主键字段,可以选择bigint类型。

  • 金额字段,可以选择decimal类型。

  • 时间字段,可以选择timestamp或datetime类型。


15.避免一次性查询太多数据


我们在设计接口,或者调用别人接口的时候,都要避免一次性查询太多数据。


一次性查询太多的数据,可能会导致查询耗时很长,更加严重的情况会导致系统出现OOM的问题。


我们之前调用第三方,查询一天的指标数据,该接口经常出现超时问题。


在做excel导出时,如果一次性查询出所有的数据,导出到excel文件中,可能会导致系统出现OOM问题。


因此我们的接口要做分页设计


如果是调用第三方的接口批量查询接口,尽量分批调用,不要一次性根据id集合查询所有数据。


如果调用第三方批量查询接口,对性能有一定的要求,我们可以分批之后,用多线程调用接口,最后汇总返回数据。


16.多线程不一定比单线程快


很多小伙伴有一个误解,认为使用了多线程一定比使用单线程快。


其实要看使用场景。


如果你的业务逻辑是一个耗时的操作,比如:远程调用接口,或者磁盘IO操作,这种使用多线程比单线程要快一些。


但如果你的业务逻辑非常简单,在一个循环中打印数据,这时候,使用单线程可能会更快一些。


因为使用多线程,会引入额外的消耗,比如:创建新线程的耗时,抢占CPU资源时线程上下文需要不断切换,这个切换过程是有一定的时间损耗的。


因此,多线程不一定比单线程快。我们要根据实际业务场景,决定是使用单线程,还是使用多线程。


17.注意事务问题


很多时候,我们的代码为了保证数据库多张表保存数据的完整性和一致性,需要使用@Transactional注解的声明式事务,或者使用TransactionTemplate的编程式事务。


加入事务之后,如果A,B,C三张表同时保存数据,要么一起成功,要么一起失败。


不会出现数据保存一半的情况,比如:表A保存成功了,但表B和C保存失败了。


这种情况数据会直接回滚,A,B,C三张表的数据都会同时保存失败。


如果使用@Transactional注解的声明式事务,可能会出现事务失效的问题,感兴趣的小伙伴可以看看我的另一篇文章《聊聊spring事务失效的12种场景,太坑了》。


建议优先使用TransactionTemplate的编程式事务的方式创建事务。


此外,引入事务还会带来大事务问题,可能会导致接口超时,或者出现数据库死锁的问题。


因此,我们需要优化代码,尽量避免大事务的问题,因为它有许多危害。关于大事务问题,感兴趣的小伙伴,可以看看我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,里面有详情介绍。


18.小数容易丢失精度


不知道你在使用小数时,有没有踩过坑,一些运算导致小数丢失了精度。


如果你在项目中使用了float或者double类型的数据,用他们参与计算,极可能会出现精度丢失问题。


使用Double时可能会有这种场景:


double amount1 = 0.02;
double amount2 = 0.03;
System.out.println(amount2 - amount1);

正常情况下预计amount2 - amount1应该等于0.01


但是执行结果,却为:


0.009999999999999998

实际结果小于预计结果。


Double类型的两个参数相减会转换成二进制,因为Double有效位数为16位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


因此,在做小数运算时,更推荐大家使用BigDecimal,避免精度的丢失。


但如果在使用BigDecimal时,使用不当,也会丢失精度。



BigDecimal amount1 = new BigDecimal(0.02);
BigDecimal amount2 = new BigDecimal(0.03);
System.out.println(amount2.subtract(amount1));

这个例子中定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。


结果:


0.0099999999999999984734433411404097569175064563751220703125

使用BigDecimal的构造函数创建BigDecimal,也会导致精度丢失。


如果如何避免精度丢失呢?


BigDecimal amount1 = BigDecimal.valueOf(0.02);
BigDecimal amount2 = BigDecimal.valueOf(0.03);
System.out.println(amount2.subtract(amount1));

使用BigDecimal.valueOf方法初始化BigDecimal类型参数,能保证精度不丢失。


19.优先使用批量操作


有些小伙伴可能写过这样的代码,在一个for循环中,一个个调用远程接口,或者执行数据库的update操作。


其实,这样是比较消耗性能的。


我们尽可能将在一个循环中多次的单个操作,改成一次的批量操作,这样会将代码的性能提升不少。


例如:


for(User user : userList) {
   userMapper.update(user);
}

改成:


userMapper.updateForBatch(userList);

20.synchronized其实用的不多


我们在面试中当中,经常会被面试官问到synchronized加锁的考题。


说实话,synchronized的锁升级过程,还是有点复杂的。


但在实际工作中,使用synchronized加锁的机会不多。


synchronized更适合于单机环境,可以保证一个服务器节点上,多个线程访问公共资源时,只有一个线程能够拿到那把锁,其他的线程都需要等待。


但实际上我们的系统,大部分是处于分布式环境当中的。


为了保证服务的稳定性,我们一般会把系统部署到两个以上的服务器节点上。


后面哪一天有个服务器节点挂了,系统也能在另外一个服务器节点上正常运行。


当然也能会出现,一个服务器节点扛不住用户请求压力,也挂掉的情况。


这种情况,应该提前部署3个服务节点。


此外,即使只有一个服务器节点,但如果你有api和job两个服务,都会修改某张表的数据。


这时使用synchronized加锁也会有问题。


因此,在工作中更多的是使用分布式锁


目前比较主流的分布式锁有:



  1. 数据库悲观锁。

  2. 基于时间戳或者版本号的乐观锁。

  3. 使用redis的分布式锁。

  4. 使用zookeeper的分布式锁。


其实这些方案都有一些使用场景。


目前使用更多的是redis分布式锁。


当然使用redis分布式锁也很容易踩坑,感兴趣的小伙伴可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》,里面有详细介绍。


21.异步思想很重要


不知道你有没有做过接口的性能优化,其中有一个非常重要的优化手段是:异步


如果我们的某个保存数据的API接口中的业务逻辑非常复杂,经常出现超时问题。


现在让你优化该怎么优化呢?


先从索引,sql语句优化。


这些优化之后,效果不太明显。


这时该怎么办呢?


这就可以使用异步思想来优化了。


如果该接口的实时性要求不高,我们可以用一张表保存用户数据,然后使用job或者mq,这种异步的方式,读取该表的数据,做业务逻辑处理。


如果该接口对实效性要求有点高,我们可以梳理一下接口的业务逻辑,看看哪些是核心逻辑,哪些是非核心逻辑。


对于核心逻辑,可以在接口中同步执行。


对于非核心逻辑,可以使用job或者mq这种异步的方式处理。


22.Git提交代码要有好习惯


有些小伙伴,不太习惯在Git上提交代码。


非常勤劳的使用idea,写了一天的代码,最后下班前,准备提交代码的时候,电脑突然死机了。


会让你欲哭无泪。


用Git提交代码有个好习惯是:多次提交。


避免一次性提交太多代码的情况。


这样可以减少代码丢失的风险。


更重要的是,如果多个人协同开发,别人能够尽早获取你最新的代码,可以尽可能减少代码的冲突。


假如你开发一天的代码准备去提交的时候,发现你的部分代码,别人也改过了,产生了大量的冲突。


解决冲突这个过程是很痛苦的。


如果你能够多次提交代码,可能会及时获取别人最新的代码,减少代码冲突的发生。因为每次push代码之前,Git会先检查一下,代码有没有更新,如果有更新,需要你先pull一下最新的代码。


此外,使用Git提交代码的时候,一定要写好注释,提交的代码实现了什么功能,或者修复了什么bug。


如果有条件的话,每次提交时在注释中可以带上jira任务的id,这样后面方便统计工作量。


23.善用开源的工具类


我们一定要多熟悉一下开源的工具类,真的可以帮我们提升开发效率,避免在工作中重复造轮子。


目前业界使用比较多的工具包有:apache的common,google的guava和国内几个大佬些hutool。


比如将一个大集合的数据,按每500条数据,分成多个小集合。


这个需求如果要你自己实现,需要巴拉巴拉写一堆代码。


但如果使用google的guava包,可以非常轻松的使用:


List<Integer> list = Lists.newArrayList(12345);
List<List<Integer>> partitionList = Lists.partition(list, 2);
System.out.println(partitionList);

如果你对更多的第三方工具类比较感兴趣,可以看看我的另一篇文章《吐血推荐17个提升开发效率的“轮子”》。


24.培养写技术博客的好习惯


我们在学习新知识点的时候,学完了之后,非常容易忘记。


往往学到后面,把前面的忘记了。


回头温习前面的,又把后面的忘记了。


因此,建议大家培养做笔记的习惯。


我们可以通过写技术博客的方式,来记笔记,不仅可以给学到的知识点加深印象,还能锻炼自己的表达能力。


此外,工作中遇到的一些问题,以及解决方案,都可以沉淀到技术博客中。


一方面是为了避免下次犯相同的错误。


另一方面也可以帮助别人少走弯路。


而且,在面试中如果你的简历中写了技术博客地址,是有一定的加分的。


因此建议大家培养些技术博客的习惯。


25.多阅读优秀源码


建议大家利用空闲时间,多阅读JDK、Spring、Mybatis的源码。


通过阅读源码,可以真正的了解某个技术的底层原理是什么,这些开源项目有哪些好的设计思想,有哪些巧妙的编码技巧,使用了哪些优秀的设计模式,可能会出现什么问题等等。


当然阅读源码是一个很枯燥的过程。


有时候我们会发现,有些源码代码量很多,继承关系很复杂,使用了很多设计模式,一眼根本看不明白。


对于这类不太容易读懂的源码,我们不要一口吃一个胖子。


要先找一个切入点,不断深入,由点及面的阅读。


我们可以通过debug的方式阅读源码。


在阅读的过程中,可以通过idea工具,自动生成类的继承关系,辅助我们更好的理解代码逻辑。


我们可以一边读源码,一边画流程图,可以更好的加深印象。


当然还有很多建议,由于篇幅有限,后面有机会再跟大家分享。


当然还有很多建议,由于篇幅有限,后面有机会再跟大家分享。


最后欢迎大家加入苏三的知识星球【Java突击队】,一起学习。


星球中有很多独家的干货内容,比如:Java后端学习路线,分享实战项目,源码分析,百万级系统设计,系统上线的一些坑,MQ专题,真实面试题,每天都会回答大家提出的问题。


星球目前开通了6个优质专栏:技术选型、系统设计、Spring源码解读、痛点问题、高频面试题 和 性能优化。


作者:苏三说技术
来源:juejin.cn/post/7259341632700235832
收起阅读 »

有些程序员表面老实,背地里不知道玩得有多花

作者:CODING来源:juejin.cn/post/7259258539164205115


















作者:CODING

来源:juejin.cn/post/7259258539164205115

该怎么放弃你,我的内卷

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也...
继续阅读 »

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也都清楚,最后给我打了个 B-。呵呵,扣工资 20%,变成所谓的绩效工资,下次考核看情况发放。


很多兄弟看到这可能会替我打抱不平,狗资本家,快去发起劳动仲裁。可是他们马上又下上另一剂猛药,那就是不停的 PUA 你,告诉你现在有家庭,要多努力,要一心扑在工作上,放心,下次一定给你打回来。


我承认,他们这些话术我都看过,基本相当于明牌。可依然我还是被影响到了,情绪十分低落,也没心思去劳动局跟他们 PK。对未来的预期瞬间变得很悲观,人要是一悲观了,真的干什么也提不起兴趣。我一门心思都扑在以后能干什么上,其它的啥也不想管,疯狂的在国内外门户上刷信息,希望能找到一条“赚钱之路”。我研究了 Web3、AI 绘画、搞自媒体、网赚攻略(什么视频搬运、抄书、小说转漫画等等),基本上信息流推给我的,我都研究了一遍。这些玩意越研究越让人焦虑,因为那些标题都起的特别的有煽动性,动不动就日入几万,而我发的那些,浏览量都破不了百。于是我就想研究更多的路子去赚钱,老实说,东南亚那边的情况,我也了解过一些。


后面我对家人的态度也越来越坏,经常不耐烦,看着我小孩我经常叹气,我想这他妈可能就是中年危机提前爆发了,总之那段时间人会越来越焦虑。


后来还是我一兄弟,邀请我一家人去平潭自驾游,我们其实也没玩几天,属于特种兵式旅游,两天两晚(晚上熬夜开车去)。回来之后心情就好多了,也没那么焦虑了。其实本来也没什么,君子不立于危墙之下,这里不行那就走。找不到就先干自己的项目(我有开源项目)。我其实对干这行还是蛮有兴趣的,应该持续坚持的干下去,半途而废干别的是下策。


回想下我那时候焦虑的经历,我以前根本看都不看那种赚钱文章的,因为我知道这些大部分是在卖课,可为什么那时候我着了魔一样呢?其实很大部分与网络有关系,你着急干什么,你就愿意看点什么,你看点什么,网络就给你推什么。这种消极循环人一旦深陷其中,光凭自己是很难走出来的。其实这种时候应该主动去接收一些积极乐观的情绪,有助于自己调整心态,网络给不了你,只有身边人能给你。


更深一步的想,所谓内卷是不是也是通过网络在传播着,深刻的影响到每一个人。所谓的“智能推荐算法”,真的智能吗?大家想看的一定就是适合每个人的吗?你不停的点击去看的信息,真的能帮助到你吗?网络是我们的工具,还是我们是网络的工具?


我想我们真的应该停下来,想想我们到底在多大程度上需要抖音、需要 BiliBili、需要知乎,也许它们真的没这么重要。


人生在世,我们到底应该追逐什么?或者说,追逐什么其实不重要,重要的是我们去追逐的过程。在这个过程中,没有内卷,没有与别人的竞争,只有对自我的审视和成长。


换句话说,我有多久没有好好了解自己了,那些独属于自己的东西,永远不会背叛的资源。我们常说的:能力、人脉、技术、视野。其实除此之外还有很多很多,我刷视频看到的有趣视频点的赞,我 Chrome 里收藏的网页,我百度网盘里躺着的分享资料等等等等,还有最重要的一项,就是我的身体和组成我身体的每一个部分:大脑、心脏、肺...... 有多久没有关注和了解它们了?在这个内卷的时代,每个人都在比拼都在竞争,都怕落于人后,都想快点挣更多的钱,这些,时常让我们忽视了对我们最重要的东西。


有趣的是,每个平台都在疯狂的更新自己的算法,期望能更精准的描述一个人,给人打上各种各样的标签。但在这场竞赛中,没有平台能竞争的过你自己,在这个世界上,只有自己更了解自己。所以我真的感觉它们在做无用功,浪费资源,最好的平台,不是给打各种标签,而是引导每个人发现自己的标签是什么。


这里我想分享给各位几个我思考的点,以供探讨。


原则一:相比与到处去找信息差,更重要的是建立自己的“资源池”


我那时候不停的刷信息,不停的找信息,本质上,我是在幻想着找到一个信息差,从而获利。这也是网上铺天盖地的文章所推崇的,所谓在风口上猪都能飞。但它们总是在掩盖一个逻辑错误,那就是找到信息差和获利之间的因果关系。实际上,找到信息差只是获利的条件之一,你有多大的能力利用这个信息差,这个信息差的时效性,方方面面的因素都会互相交织和影响。


更进一步的想,信息差就像风一样,它存在于冷热空气的交换之时,它存在于各行各业、每时每刻。让我们去追逐风,这现实吗?


我们更应该静下来,好好数数自己手头的东西,整理自己的大脑。找到自己“资源池”有哪些资源,哪些可以为我们所用,哪些可以继续扩充。思路可以打开一点,任何在当前时刻属于你的东西,都是你“资源池”的一部分。


原则二:出卖自己时间和体力的不做


这个不做,不是指不去做,而是指不长期的做。一般入门一个行业或者技术,肯定要付出时间和体力的。但你要说十年如一日的付出相同的东西,那所谓“35 岁”危机就只能找到你了。这点其实各行各业都一样,只是互联网行业处在发声的前沿罢了。


包括所谓网赚、搬运都是一个道理,毫无技术含量的事做几年就好。要时常审视自己现在在干什么,手头有哪些资源,未来的目标是什么。这跟程序运行是一个道理,运行了一段时间,停下来让自己 GC 一下。不然很容易 StackOverflow。


原则三:自己抓住的资源,千万不要轻易放手


如果不经常审视自己的“资源池”,给所有资源估估价值,就很容易被人带坑里。


原先我就做过一个项目,这是个跨部门项目,我那个领导一直告诉我说这个项目没前途、没卵用,绩效也给我打的不好,问我还要不要继续做。我说那就算了吧,做的我都不想做了。


我一放弃,马上就有新人接手,连交接也不用做,代码直接拿走,吃相可见一斑。


也就是从这里我才理解到,我其实没有了解自己,没了解过我手里的项目,被人潜移默化的影响了。影响一个人的思想真的不难,不停的重复就好了。所以还是那句话,多把自己手里的“资源池”拿出来晒一晒,整理一下。


其实 996 也是一样,拿出了你最重要的资源---身体,到底换来了什么,值得好好评估一下。


原则四:做自己喜欢的赛道,更要积累自己的资源


这几个月的经历给我的最大感觉是,这世界上真的有太多太多的行业,也有很多人赚到了钱(至少网络上宣传他们赚了钱)。网络能让这些信息病毒式的传播,导致很多人错觉的以为自己照着做也能挣到钱。但他们忽视的是,网络能把世界各地的人汇聚起来,让信息流通。其实也提供了一个更大的平台,在这个平台里,只有更卷的人才能挣到钱。


有时候真的应该抛开网络。比如,你会写代码,这是你“资源池”里的一项技能,你把这个技能公开到网络售卖。只有两种情况,要么你非常的卷,打拼出一番事业;要么你根本竞争不过别人,这是普遍情况,这世界那么大,比你优秀的人有太多太多了。


但是抛开网络,回到你身边的小小社交圈子,你的技能可能就没那么普遍了。可能你会说,那我做程序员,我身边朋友认识的大部分也是做程序员啊。那么可以这么想,假如你会做菜,你身边的程序员朋友都会做菜吗?假如你会画画,你身边的程序员朋友都会画画吗?人和人总有差异点,你觉得找不到优势,那是因为你尚未建立自己的“资源库”。


先认识自己,再让身边的人认识自己,当他们会给你打标签时,他们就成了你“资源库”中的一员,这就是人脉。这才是是独属于你自己的标签,而不是抖音、B 站为你打的冷冰冷的标签。


总结


以上我感悟的四个原则,我称之为“资源池思维”,一个比较程序员化的名词。


这篇文章发完后,我后续可能就继续更新一下具体的技术文章了,继续深耕技术。


最后,推荐看到最后的各位看一部冷门电影:《神迹》,讲述的是医生维维安托马斯的故事。看完可以来一起交流交流感悟。



本文仅发于掘金平台,禁止未经作者同意转载、复制。


作者:FengY_HYY
来源:juejin.cn/post/7259210874447151163

收起阅读 »

独立开发前100天真正重要的事

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功...
继续阅读 »

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功(没有走的很远),所以只能分享独立开发前100天的经验。


先说一下我认为独立开发起步阶段面临的主要困难:


第一:没有公司的孤独感。如果是一个人全职开发就更寂寞了。即使有一两个合作伙伴,但是大概率也是异地,因此也算是网友性质的社交。人说到底是群居的,所以需要找到一种社交平衡。我想可能这也是很多独立开发白天要在外面地方待着的原因,也许一个人一直在家待着有点闷。


第二:无法建立产品的健康开发节奏。以前在公司的时候自己是流程里的一环,只关心自己分工的完成情况。做了独立开发以后,所有事情都需要自己决策。太多自由的结果就是没了方向。什么都想做,好像什么都可以做,又感觉什么都不好做。


第三:没有收入。产品从开始建造到有足够健康收入中间有一段过程。这还有一个前提要是一个真正有用户价值的产品。如果你起步的时候自己没有一个优势产品方向,又没有个人社区号召力,就算你的产品是好的,也需要一段时间(可长可短)才能获得有效收入。一上线就火的概率太小了。高强度投入一件事情,如果长期没有收入,家人会有很多质疑,可能最后自己也很怀疑自己。


我把这三点结合起来,编一个故事大家可能比较有画面了:



一个人做独立开发已经半年多了,产品设计都是自己做,也没有什么人可以讨论,不知道下一步该做什么。目前每天只有零星的新增。做了这么久,总共只有两三千的收入。看来做产品还得会营销**,打算最近开始学习一下运营**。最近也打算做一下AI产品,感觉这个赛道很火。老婆说如果不行就早点回去上班好了,总不能一直这样。家人们,你们说我应该坚持吗。




家人们会说你的产品很棒,会说你做的比他们强,会说下次一定,会说你未来会成功。但是家人们不会为你掏一分钱。



也许我们不知道如何成功,但是我们可以知道什么是失败。你知道的失败方式越多,你成功的概率就越大。总的来说,产品的成功就两个要点:有用户价值,能赚钱。注意,这两点是或的关系,不是且的关系。一个产品可以能赚钱,但是没有用。一个产品也可以有用,但是不赚钱。失败就是你做的产品:既没用,又不赚钱


基于前面提到的三个困难,我得出的前100天最重要的事是:找到一个可行的产品迭代方向。和团队经过磨合,互相能有有效、信任的协作。找到一百个种子用户。你越早解决这三个困难,你越快走上轨道


确认产品方向


如果你真的做过产品,你就知道最终正确的产品路径不是通过脑中的某刻灵光乍现得到的。所以不是那种大脑飞速运算解题的方式。这里有两件事情需要确认:大的产品方向,产品的路径。


比如阿里巴巴,马云不是一开始就做的淘宝网。他只是觉得互联网普及以后,电子商务会有需求。最开始做的是黄页,并不是淘宝网。但是他没有在第一个项目失败以后,去做门户网站。产品路径的例子是特斯拉。特斯拉很早就确定了先出高性能的跑车,高性能轿车(model s),有了前面的技术积累以后,最后通过推出平价的轿车赢得市场(model 3)。特斯拉在 model 3 大规模量产前都是亏损的。


所以最重要的是确认产品方向。这个方向要结合自身的情况进行设定,就是我在前面帖子里提到的要是你想做的,能做的。也许想达到的产品方向有很多工作量,这个时候就要有同步的产品路径。比如小米手机的创业,他们一开始就想造手机。但是直接启动手机的制造市场、技术都有很大的困难。于是他们先通过做 MIUI 入局。


这里面首先要有个大的方向判断,对于独立开发来说,我觉得张宁在《创作者》里提到的两个维度的方向挺有意思:大众、小众;高频、低频。这里面两两结合各有什么特点我这里就不展开了,大家可以自行体会。


但是可以明确的是,独立开发者做不了又大众又高频的应用。大众又高频,就不可能小而美。大众又高频,最后赢家除了产品能力,要有运营优势,要有资源优势。独立开发者通常没有运营优势和资源优势。另外一点,如果是小众低频,就一定要高忠诚,高付费转化。可以往大众低频或者小众高频的方向多想想


产品方向选择还有一个建议就是要有秘密。成功的业务后面一定有秘密。秘密也回答了一个问题:如果这个需求真的存在,为什么用户选择了你的产品。


最初级的秘密就是信息差,你知道别人不知道,所以你可以,更早做,可以更低的成本,更高效,有更高的获客率。


更高级的秘密就是大家都能看到,但是大家知道了,但是大家不信(脑中想到了拼多多的砍一刀)。


最高级的秘密就是所有人都知道,但是他们做不到。


总结起来,你应该找到一个你有优势的细分方向。信息优势,洞察优势也是优势。


没有一发即中的银弹,最平凡的方式想很多方向,用最低成本进行最快速的验证。在反馈中渐渐明晰产品路径。如果你三个月不管反馈闷头做,只做出了一个产品方向。你失败的概率是很大的。所以我看到很多产品1.0 的时候就做会员,做社区,做跨平台我是很不理解的。其实这些功能在早期性价比很低。


我的方式是脑海中有10个想法,挑出3个想法做初步设计,选出一个或者两个想法做产品验证。可能是原型,数据是模拟的,没有设计,如果产品真的解决了痛点的话,用户会愿意用,然后他会给你反馈他想要更好的体验,他愿意付钱得到这些改进。这里的效率优势是,你能在更短的时间验证产品方向是不是对的。总比走了3个月才发现是一条死胡同要好。


开发者很容易因为想到一个想法很兴奋,觉得这个很有用,就闷头做了一个月。有可能的问题是,这个想法虽然是个痛点,但是这个痛点频次很低,场景很少,所以虽然有用,但是没人会愿意买单。所以尽量跳出自己的思维,从用户的角度来进行验证是很必要的。


团队协作


独立开发的开发方式和传统公司不同。需要建立一个全新的工作流程。在初期大家都是空白,所以需要通过产品迭代中,形成高效的开发默契。大家松散做东西,工作习惯,工作职责都需要有共识才行。


比如我合作的设计师早期喜欢一次做一大板块的整体设计,大概一周的工作量。初期我觉得我们对产品有激情,大家都应该有自由的发挥空间。但是做了一周的设计图和产品脑海中的产品行进方向不一致怎么办。在工作时间上,我合作的设计师因为目前还是兼职,他只能在下班后设计。然而我全职只在6点前工作。这又是一个要协调的地方。


如果你是一个产品,需要协调研发和设计,三个人协调就又更复杂了。要找到一个大家都舒服,高效的协作方式。


100个种子用户


独立开发最核心的一环就是找到一个健康的商业模式。产品方向和团队协作的目标都是为了未来可以达成一个健康的商业模式。我觉得太多独立开发者上来就把目标(野心)定的太高。一口吃不成胖子。独立开发早期的商业目标只有一个:尽快达成团队最低维持标准。一鸟在手胜过二鸟在林。不要在团队只有几个人的时候用几十个人的方式管理。


初期就要估算出产品(团队)能够持续运转的最低收入。这个成本越低,团队就越容易跑起来。当收入足够覆盖团队的成本后,你的心态就会得到极大的自由,可以尝试很多奇奇怪怪有趣的想法。所以早期不要想有多高的天花板,如何建立壁垒,就关心如何达成产品的及格生命线。谁会想做一个注定失败的产品呢。


早期在没有运营优势的情况下,最重要的指标就是用户满意度了。用户满意度,就暗示了这个产品有没有解决切实的用户问题,用户愿不愿意为你宣传。其实很多人都搞错了重点,在产品没有让100个种子用户满意前,新增的流量是没有意义的。因为再多的用户都会流失。竹篮打水一场空。如果你把产品的用户目标定在100个种子用户,你也就没了运营压力,可以关注在如何打造正确的产品上。在产品基本盘没有问题后,再思考后面的才有意义。


总结


总结起来三点就是:做什么(产品方向),怎么做(团队协作),为谁做(验证用户)。以上就是我全职独立开发3个多月以来肤浅的经验分享,希望对你有帮助。



PS:目前我的 App 是:打工人小组件(只在 AppStore),有兴趣的欢迎下载体验。


作者:没故事的卓同学
来源:juejin.cn/post/7259210748801663031

收起阅读 »

你的代码不堪一击!太烂了!

前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
继续阅读 »

前言


小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


一、变量解构一解就报错


优化前


const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



所以当 dataundefinednull 时候,上述代码就会报错。


优化后


const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值


估计有些同学,看到上小节的代码,感觉还可以再优化一下。


再优化一下


const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



所以当 props.datanull,那么 const { name, age } = null 就会报错!


三、数组的方法只能用真数组调用


优化前:


const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


优化后:


const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象


优化前:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


二次优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用


优化前:


const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


优化后:


const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获


优化前:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。


优化后:


import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


二次优化后:


import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse


优化前:


const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


优化后:


const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据


优化前:


const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


优化后:


import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作


优化前:


const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


优化后:


const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御


优化前:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


优化后:


const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续


以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误

作者:红尘炼心
来源:juejin.cn/post/7259007674520158268
,真的可以考虑转行。

收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀

作者:三分恶
来源:juejin.cn/post/7259036373579350077
一下光哥带来的快乐。

收起阅读 »

应届毕业生关于五险一金你知道多少?很多人找工作都吃了亏

“明明说好月薪1w,结果最后到手才7k” 最近,不少上了工作岗位的小伙伴跟作者吐槽 为什么面试的时候说好的薪资跟与实际到手的差别这么大呢? 所以五险一金究竟有什么作用? 薪资都是如何被它扣掉的? 很多同学在面试或者刚刚入职时 怕HR觉得自己问题多 就不敢多问...
继续阅读 »

“明明说好月薪1w,结果最后到手才7k”


最近,不少上了工作岗位的小伙伴跟作者吐槽


为什么面试的时候说好的薪资跟与实际到手的差别这么大呢?


所以五险一金究竟有什么作用?


薪资都是如何被它扣掉的?


img


很多同学在面试或者刚刚入职时


怕HR觉得自己问题多


就不敢多问什么


但是,这和你的薪资福利息息相关


如果没问清,你一年可能要少拿上万元!


跟着作者一起来看看五险一金的具体细则吧。


1. 什么是五险一金?


五险是指养老保险、医疗保险、失业保险、工伤保险和生育保险,这五险可以统称为社会保险,也就是我们常说的“社保”。而“一金”则是住房公积金。


在这五险中,个人需要承担养老保险、医疗保险和失业保险这三项的缴费,而单位则需要为员工交齐五险的全部费用。


对于应届生来说,五险一金的重要性可能并不清楚,但它与日后的购房、生育、医疗和退休等方面息息相关。


img


举个例子,如果你打算在上海或北京买房,那你一定要连续缴纳5年社保才可以。


2. 五险一金的具体内容:


养老保险:


养老保险是为了解决劳动者在达到国家规定的解除劳动义务的劳动年龄界限,或因年老丧失劳动能力退出劳动岗位后的基本生活而建立的一种社会保险制度。个人缴纳养老保险累计满15年后才能领取退休金。养老保险的缴纳标准各地有所不同,以2018年北京市为例,单位缴费比例为19%,个人缴费比例为8%。也就是说,如果你的月入为10000元,你每个月需要交800元的养老保险金。


医疗保险:


医疗保险是为了补偿劳动者因疾病风险造成的经济损失而建立的一项社会保险制度。个人需要缴纳医疗保险费用,以获取医疗方面的一定报销和救助。各地医疗保险的缴纳标准也有所不同,以2018年北京市为例,单位缴费比例为10%,个人缴费比例为2%。也就是说,如果你在北京月入10000元,你每个月需要交203元的医疗保险金。


失业保险:


失业保险是为了对因失业而暂时中断生活来源的劳动者提供物质帮助,以保障其基本生活。单位和个人共同按照缴费基数进行缴纳。以2018年北京市为例,单位缴费比例为1%,个人缴费比例为0.2%。失业保险的领取条件包括按规定参加失业保险,所在单位和本人已按照规定履行缴费义务满1年,非因本人意愿中断就业等情况。


工伤保险:


工伤保险是为在工作期间或上下班途中因意外受伤的劳动者提供报销的一项社会保险制度。工伤保险费由公司全额缴纳。工伤保险的认定比较复杂,一旦发生意外,建议第一时间报警或联系公司存留证据,同时需要在一个月内办理工伤鉴定。


生育保险:


生育保险是针对怀孕生育的劳动者,缴纳一定时间后可以享受产假,并在产假期间领取生育津贴。单位全额缴纳生育保险费用。产假期间的生育津贴额度为本人或妻子生育当月本单位人均缴费工资除以30(天)再乘以产假/陪产假天数。


注意:男性职工也是有生育保险的哦~如果你的妻子没有工作,是可以使用你的生育保险的;如果你的工作性质决定了你不能休14天的陪产假(陪产假天数各地有差异),你在正常拿工资的同时是可以申请生育津贴的。


所以,不要再说,我是男生,为什么还要缴生育保险。


产假期间的生育津贴额度:


本人或妻子生育当月本单位人平缴费工资÷30(天)×产假/陪产假天数。


值得注意的是,大家通常理解的产假是有工资的,这个工资其实是生育津贴,是你在休完产假后国家支付给你的,并不是单位支付给。根据法律规定,单位也没有必要支付给你。个别福利较好的单位,才会同时支付生育津贴+产假工资。


【缴纳标准】


按照保险基数进行缴纳,由公司全额缴纳。以2018年北京市为例,缴纳比例为0.8%。


关于“五险一金”的缴纳费用,一张图更清楚


以2018年北京市为例,如果月薪一万,那么个人所要缴纳的五险一金为2223元。具体计算方法如下图所示,


img


住房公积金:


住房公积金是一项强制性的储蓄制度,员工每月交纳公积金,单位也会给员工交纳相同金额的公积金,存入员工的公积金账户。这笔钱可以用于购房、还房贷、自己盖房或租房等住房相关支出。不同地区的公积金缴纳比例和政策也不尽相同。


关于公积金,你需要注意以下问题:


不买房子,住房公积金可以取出来吗?


公积金大家最关心的,就是能不能不买房子可以把这笔钱取出来?基本上现在都是可以取出来的,主要看当地的满足条件。


公积金存的越多购房贷款就越多吗?


是的。连续缴满半年就可以公积金贷款,能贷款多少也跟你的公积金余额和缴存比例有很大的关系。但是无论你薪资多高,公积金的缴存比例不得超过 12%。


试用期不缴五险一金?


《中华人民共和国劳动合同法》、《住房公积金管理条例》清楚表明,确定劳动关系后,用人单位就要为职工缴纳社保和公积金。


企业不缴五险一金或者强迫你签订不缴社保的协议都是违反劳动法的!


如果你遇到这样的公司,可以考虑拿起法律武器保护自己的权益,收集好相关证据(合同、工资条、考勤记录等)去当地的人力资源和社会保障局申请劳动仲裁


五险一金中断怎么办?


如果你找到了新工作:那么养老保险、医疗保险、失业保险、工伤保险、生育保险五险都可以转移到新公司。


如果你辞职了,没有新的接收公司,也都可以找代缴公司给你代缴上述保险。


但是注意!医疗保险要及时补缴,因为**一断医保就会停,当连续中断时间超过三个月,你的连续缴费年限就会清零!**会影响到以后生大病的门诊报销比例及各项额度。


住房公积金中断后可以补缴,但自己补缴不了。只能在找到新公司后,填写好补缴材料给公司的人事部门,再让公司的人去办理补缴手续。


那么你能贷多少?


第一次办理。如果夫妻二人共同贷款,贷款最高限额 70 万,如果只有单人贷款,最高限额 45 万。


第二次办理。如果夫妻二人共同贷款,贷款最高限额 50 万,如果只有单人贷款,最高限额 30 万


应届生找工作的例子:


假设小明是一名应届大学毕业生,他找到了一家公司,并在面试时商定了月薪1万。然而,当他拿到第一个月的工资时,发现实际到手只有7000元。为什么会出现这种情况呢?


经过了解,小明发现差额是因为公司依法为他缴纳了五险一金,而这部分费用被从他的薪资中扣除了。具体来说,他需要缴纳养老保险、医疗保险和失业保险,而单位为他缴纳了五险的全部费用。


养老保险缴纳比例为8%,医疗保险缴纳比例为2%,失业保险缴纳比例为0.2%,这些费用按照小明的月薪进行扣除,导致他实际到手的薪资较少。


尽管初时看起来差额较大,但五险一金对员工未来的福利和保障有着重要作用。养老保险可以为他的退休生活提供保障,医疗保险可以在生病时获得一定程度的报销和救助,失业保险可以在意外失业时提供一定的经济支持。


因此,尽管五险一金会让员工的实际到手薪资较少,但从长远来看,这些社会保险和公积金将为员工的未来提供重要的帮助和保障。所以,在面试或入职时,了解清楚五险一金的具体细则是非常重要的,而不仅仅关注月薪本身。


总之,五险一金是员工福利的重要组成部分,也是雇主合法义务,而对于应届生来说,了解这些内容对于未来的职业生涯和生活规划至关重要。在求职过程中,应该了解和询问相关的薪资和福利细则,确保自己的权益得到保障。



本文由博客一文多发平台 OpenWrite 发布!


作者:不败顽童
来源:juejin.cn/post/7258207459357933605

收起阅读 »

new 一个对象时,js 做了什么?

js
前言在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。new 的作用我们先通过例子来了解 ...
继续阅读 »

前言

在 JavaScript 中, 通过 new 操作符可以创建一个实例对象,而这个实例对象继承了原对象的属性和方法。因此,new 存在的意义在于它实现了 JavaScript 中的继承,而不仅仅是实例化了一个对象。

new 的作用

我们先通过例子来了解 new 的作用,示例如下:

function Person(name) {
this.name = name
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const t = new Person('小明')
console.log(t.name) // 小明
t.sayName() // 小明

从上面的例子中我们可以得出以下结论:

  • new 通过构造函数 Person 创建出来的实例对象可以访问到构造函数中的属性。

  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来。

构造函数 Person 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?

function Person(name) {
this.name = name
return 1
}
const t = new Person('小明')
console.log(t.name) // 小明

在上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。我们又可以得出一个结论:

构造函数如果返回原始值,那么这个返回值毫无意义。

我们再来试试返回对象会发生什么:

function Person(name) {
this.name = name
return {age: 23}
}
const t = new Person('小明')
console.log(t) // { age: 23 }
console.log(t.name) // undefined

通过上面这个例子我们可以发现,当返回值为对象时,这个返回值就会被正常的返回出去。我们再次得出了一个结论:

构造函数如果返回值为对象,那么这个返回值会被正常使用。

总结:这两个例子告诉我们,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。

实现 new

首先我们要清楚,在使用 new 操作符时,js 做了哪些事情:

  1. js 在内部创建了一个对象
  2. 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来
  3. 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  4. 返回原始值需要忽略,返回对象需要正常处理

知道了步骤后,我们就可以着手来实现 new 的功能了:

function _new(fn, ...args) {
const newObj = Object.create(fn.prototype);
const value = fn.apply(newObj, args);
return value instanceof Object ? value : newObj;
}

测试示例如下:

function Person(name) {
this.name = name;
}
Person.prototype.sayName = function () {
console.log(this.name);
};

const t = _new(Person, "小明");
console.log(t.name); // 小明
t.sayName(); // 小明

以上就是关于 JavaScript 中 new 操作符的作用,以及如何来实现一个 new 操作符。


作者:codinglin
链接:https://juejin.cn/post/7222274630395379771
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

手写一个Promise

Promise背景JavaScript这种单线程事件循环模型,异步行为是为了优化因计算量大而时间长的操作。在JavaScript中我们可以见到很多异步行为,比如计时器、ui渲染、请求数据等等。Promise的主要功能,是为异步代码提供了清晰的抽象,支持优雅地定...
继续阅读 »

Promise

背景

JavaScript这种单线程事件循环模型,异步行为是为了优化因计算量大而时间长的操作。在JavaScript中我们可以见到很多异步行为,比如计时器、ui渲染、请求数据等等。

Promise的主要功能,是为异步代码提供了清晰的抽象,支持优雅地定义和组织异步逻辑。可以用Promise表示异步执行的代码块,也可以用Promise表示异步计算的值。

Promise现在主流的翻译为“期约”,在英文里,promise还有承诺的意思,既然是承诺,那就是一种约定,这恰好就符合异步情境的需求:异步的代码不在当前的代码块中调用,而是由外部调用。既然如此,为了获取到异步代码执行的状态,或是为了拿到执行结果,就需要制定一定的规范去获取和维护,Promise A+就是对此指定的规范,Promise类型就是对Promise A+规范的实现。

过去在JavaScript中处理异步,通常会使用一层层的回调嵌套,没有一个规范、清晰的处理逻辑,造成的结果就是阅读困难、调试困难,可维护性差。

Promise A+规范设计的一套逻辑,Promise提供统一的API,可以使我们更有条理的去处理异步操作。

首先,将某个异步任务相关的代码包裹在一个代码块里,也就是Promise执行器函数的函数体中;比如下面的代码:

let p1 = new Promise((resolve, reject) => { // 执行器函数
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});

同时,针对这段异步代码的执行状态和执行结果,Promise实例内部会进行维护;

此外,Promise类型内部维护一个resolve和reject函数,用于维护状态的更新,以及调用处理程序将异步执行结果传递给用户进行后续处理,这些处理程序由用户自己定义。

Promise类型实现了Thenable接口,用户可以通过Promise的实例方法then来新增处理程序

当用Promise指代异步执行的代码块时,他涉及异步代码执行的三种状态:进行中等待结果的pending、成功执行fulfilled(一般也用resolved)、执行失败或出现异常rejected。当一个Promise实例被初始化时,其对应的异步代码块就进入进行中的状态,也就是说pending是初始状态。

当代码块执行完毕或者出现异常,将得到最终的一个确定状态,resolved或者rejected,和执行结果,并且不能被再次更新。

Promise的基本使用

let p = new Promise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
});

简易版Promise

针对Promise的基本使用,可以实现一个简易版的Promise

首先是状态常量的维护,以便于开发和后期维护:

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

然后定义我们自己的MyPromise,维护Promise实例对象的属性

function MyPromise(fn) {
const that = this;
   that.state = PENDING;
   that.value = null;
   that.resolvedCallbacks = [];
   that.rejectedCallbacks = [];
}
  • 首先是state,表示异步代码块执行的状态,初始状态为pending
  • value变量用于维护异步代码执行的结果
  • resolvedCallbacks用于维护部分的处理程序,处理成功执行的结果
  • rejectedCallbacks用于维护另一部分的处理程序,处理的是执行失败的结果

内部使用常量that是因为,代码可能会异步执行,这用于获取正确的this。

接下来定义resolve和reject函数,添加在MyPromise函数体内部

function resolve(value) {
   if (that.state === PENDING) {
       that.state = RESOLVED;
       that.value = value;
       that.resolvedCallbacks.forEach(cb => cb(value));
  }
}

function reject(reason) {
   if (that.state === PENDING) {
       that.state = REJECTED;
       that.value = reason;
       that.rejectedCallbacks.forEach(cb => cb(reason));
  }
}
  • 首先这两个函数都得判断当前状态是否为pending,因为状态落定后不允许再次修改
  • 如果判断为pending,就更新为对应状态,并且将异步执行结果维护到Promise实例的value属性上
  • 最后遍历处理程序,并传入异步结果挨个执行

当然传递给Promise的执行器函数fn也得执行

try {
   fn(resolve, reject);
} catch (e) {
   reject(e);
}

执行器函数接收两个函数类型的参数,实际传入的就是前面定义的resolve和reject。另外,执行函数的过程中可能会抛出异常,需要捕获并执行reject函数。

最后实现较为复杂的then函数

MyPromise.prototype.then = function (onResolved, onRejected) {
   const that = this;
   onResolved = typeof onResolved === 'function' ? onResolved: v => v;
   onRejected = typeof onRejected === 'function'
       ? onRejected
      : r => {
           throw r;
      };
   if (that.state === PENDING) {
       that.resolvedCallbacks.push(onResolved);
       that.rejectedCallbacks.push(onRejected);
  }
   if (that.state === RESOLVED) {
       onResolved(that.value);
  }
   if (that.state === REJECTED) {
       onRejected(that.value);
  }
}
  • 首先判断两个参数是否为函数类型,因为这两个参数是可选参数。
  • 当参数不是函数类型时,就创建一个函数赋值给对应的参数,实现透传
  • 然后是状态的判断,当Promise的状态是等待结果pending时,就会将处理程序维护到Promise实例内部的处理程序的数组中,resolvedCallbacks和rejectedCallbacks,如果不是pending,就去执行对应状态的处理程序。

至此就实现了一个简易版本的MyPromise,可以进行测试:

let p = new MyPromise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
});

进阶版Promise

根据promise的使用经验,我们知道promise解析异步结果是一个微任务,并且promise的原型方法then会返回一个promise类型的值,这些简易版中都没有实现,为了使我们的MyPromise更符合Promise A+的规范,我们需要对简易版进行改造。

首先是resolvereject函数,这两个函数中的代码会被推入微任务的队列中等待执行

  function resolve(value) {
       if (value instanceof MyPromise) {
           return value.then(resolve, reject);
      }

       // 调用queueMicrotask,将代码插入微任务的队列
       queueMicrotask(() => {
           if (that.state === PENDING) {
               that.state = RESOLVED;
               that.value = value;
               that.resolvedCallbacks.forEach(cb => cb(value));
          }
      })
  }

   function reject(reason) {
       queueMicrotask(() => {
           if (that.state === PENDING) {
               that.state = REJECTED;
               that.value = reason;
               that.rejectedCallbacks.forEach(cb => cb(reason));
          }
      });
  }
  • 对于resolve函数,我们首先需要判断传入的值是否为Promise类型,如果是,则要得到x最终的异步执行结果再继续执行resolve和reject
  • 此处使用queueMicrotask方法将代码推入微任务队列

接下来继续改造then函数中的代码

  • 首先新增一个变量promise2用于返回,因为每个then函数都需要返回一个新的Promise对象,该变量就用于保存新的返回对象

    let promise2; // then方法必须返回一个promise
  • 然后先改造pending状态的逻辑

    if (that.state === PENDING) {
       return promise2 = new MyPromise((resolve, reject) => {
           that.resolvedCallbacks.push(() => {
               try {
                   const x = onResolved(that.value); // 执行原promise的成功处理程序,如果未定义就透传
                   // 如果正常得到一个解决值x,即onResolved的返回值,就解决新的promise2,即调用resolutionProcedure函数,这是对[[Resolve]](promise, x)的实现
                   // 将新创建的promise2,处理程序返回结果x,以及与promise2关联的resolve和reject函数作为参数传递给 这个函数
                   resolutionProcedure(promise2, x, resolve, reject);
              } catch(r) { // 如果onResolved程序执行过程中抛出异常,promise2就被标记为失败,执行reject
                   reject(r);
              }
          });
           that.rejectedCallbacks.push(() => {
               try {
                   const x = onRejected(that.value); // 执行原promise的失败处理程序,如果未定义就抛出异常
                   resolutionProcedure(promise2, x, resolve, reject); // 解决新的promise2
              } catch(r) {
                   reject(r);
              }
          });
      })
    }

    整体来看下:

    • 首先创建新的Promise实例,传入执行器函数
    • 大致逻辑还是和之前一样,往回调数组中push处理程序,只是除了onResolved函数之外,还做了一些额外操作
    • 首先在onResolved和onRejected函数调用的时候包裹了一层try/catch用于处理异常,如果出现异常,promise2就被标记为失败,执行其关联的reject函数
    • 如果onResolved和onRejected正常执行,就调用resolutionProcedure函数去解决promise2
  • 继续改造resolved状态的逻辑

    if (that.state === RESOLVED) {
       return promise2 = new MyPromise((resolve, reject) => {
           queueMicrotask(() => {
               try {
                   const x = onResolved(that.value);
                   resolutionProcedure(promise2, x, resolve, reject);
              } catch (r) {
                   reject(r);
              }
          });
      })
    }
    • 这段代码和pending的逻辑基本一致,不同之处在于,这里直接将处理程序插入微任务队列,而不是push进回调数组
    • rejected状态的逻辑基本也类似

最后就是实现上述代码中所调用的resolutionProcedure函数,用于解决promise2

function resolutionProcedure(promise2, x, resolve, reject) {}
  • 首先规范规定了x不能与promise2相等,否则会发生循环引用的问题

    if (promise2 === x) { // 如果x和promise2相等,以 TypeError 为拒因 拒绝执行 promise2
       return reject(new TypeError('Error'));
    }
  • 接着判断x的类型是否为promise

    if (x instanceof MyPromise) { // 如果x为Promise类型,则使 promise2 接受 x 的状态
       x.then(function (value) {
           // 等到x状态落定后,再去解决promise2,也就是递归调用resolutionProcedure这个函数
           resolutionProcedure(promise2, value, resolve, reject);
      }, reject/*如果x落定为拒绝状态,就用同样的拒因拒绝promise2*/);
    }
  • 处理x的类型不是promise的情况

    首先创建一个变量called用于标识是否调用过函数

    let called = false;
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // 如果x为对象或函数类型
       try {
           let then = x.then; // 取出x上的then属性
           if (typeof then === 'function') { // 判断then的类型是否为函数,进行调用
            // 根据规范可知,在then调用时,要将this指向x,所以这里使用call对then函数进行调用
               // then接收两个函数类型的参数,第一个参数叫做resolvePromise,第二个参数叫做rejectPromise
               // 如果resolvePromise被执行,则去解决promise2,如果rejectPromise被调用,则promise2被认为失败,会调用其关联的reject函数
               then.call(
                   x, // 将this指向x
                   y => { // 第一个参数叫做resolvePromise
                       if (called) return;
                       called = true;
                       resolutionProcedure(promise2, y, resolve, reject);
                  },
                   r => { // 第二个参数叫做rejectPromise
                       if (called) return;
                       called = true;
                       reject(r);
                  }
              )
          } else { // 如果then不是函数,就将x传递给resolve,执行promise2的resolve函数
               resolve(x);
          }
      } catch (e) { // 如果上述代码抛出异常,则认为promise2失败,执行其关联的reject函数
           if (called) return;
           called = true;
           reject(e);
      }
    } else { // 如果x不是对象或函数,就将x传递给promise2关联的resolve并执行
       resolve(x);
    }

至此resolutionProcedure函数就完成了,最终会执行promise2关联的resolve或者reject函数。之所以说关联,是因为这两个函数中有对实例的引用。

到这为止,进阶版的promise就基本完成了,可以来试用一下:

let p = new MyPromise((resolve, reject) => {
   const add = (a, b) => a + b;
   resolve(add(1, 2));
   console.log(add(3, 4));
});
p.then(res => {
   console.log(res);
   return res;
}).then(res => {
   console.log(res);
});
p.then(res => {
   return {
       name: 'x',
       then: function (resolvePromise, rejectPromise) {
           resolvePromise(this.name + res);
      }
  }
}).then(res => {
   console.log(res);
})

作者:beckyyy
链接:https://juejin.cn/post/7258675185547919416
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

前端同事最讨厌的后端行为,看看你中了没有

前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,...
继续阅读 »

前端吐槽:后端从不自测接口,等到前后端联调时,这个接口获取不到,那个接口提交不了,把前端当成自己的接口测试员,耽误前端的开发进度。

听到这个吐槽,仿佛看到曾经羞愧的自己。这个毛病以前我也有啊,有些接口,尤其是大表单提交接口,表单特别大,字段很多,有时候就偷懒了,直接编译过了,就发到测试环境了。前端同时联调的时候一调接口,异常了。

好在后来改了,毕竟让人发现自己接口写的有问题,也是一件丢脸的事儿。

但是我还真见过后端的同学,写完接口一个都不测,直接发测试环境的。

我就碰到过厉害的,编译都不过,就直接提代码。以前,有个新来的同事,分了任务就默默的干着,啥也不问,然后他做的功能测试就各种发现问题。说过之后,就改一下,但是基本上还是不测试,本想再给他机会的,所以后来他每次提代码,我都review一下。直到有一天,我发现忍不了了,他把一段全局配置给注释了,然后把代码提了,我过去问他是不是本地调试,忘了取消注释了。他的回答直接让我震惊了,他说:不是的,是因为不注释那段代码,我本地跑步起来,所以肯定是那段代码有问题,所以就注释了。

然后,当晚,他就离职了。

解决方式

对于这种大表单类似的问题,应该怎么处理呢?

好像没有别的方法,只能克服自己的懒惰,为自己写的代码负责。就想着,万一接口有问题,别人可能会怀疑你水平不行,你水平不行,就是你不行啊,程序员怎么能不行呢。

你可以找那么在线 Java Bean转 JSON的功能,直接帮你生成请求参数,或者现在更可以借助 ChatGPT ,帮你生成请求参数,而且生成的参数可能比你自己瞎填的看上去更合理。

或者,如果是小团队,不拘一格的话,可以让前端的同事把代码提了,你本地跑着自测一下,让前端同事先做别的功能,穿插进行也可以。

前端吐槽:后端修改了字段或返回结构不通知前端

这个就有点不讲武德了。

正常情况下,返回结构和字段都是事先约定好的,一般都是先写接口,做一些 Mock 数据,然后再实现真实的逻辑。

除了约定好返回字段和结构外,还包括接口地址、请求方法、头信息等等,而且一个项目都会有项目接口规范,同一类接口的返回字段可能有很多相同的部分。

后端如果改接口,必须要及时通知前端,这其实应该是正常的开发流程。后端改了接口,不告诉前端,到时候测试出问题了,一般都会先找前端,这不相当于让前端背锅了吗,确实不地道啊。

后端的同学们,谨记啊。

前端吐槽:为了获取一个信息,要先调用好几个接口,可能参数还是相同的

假设在一个详情页面,以前端的角度就是,我获取详情信息,就调用详情接口好了,为什么调用详情接口之前,要调用3、4个其他的接口,你详情里需要啥参数,我直接给你传过去不就好了吗。

在后端看来可能是这样的,我这几个接口之前就写好了,前端拿过去就能用,只不过就是多调几次罢了,没什么大不了的吧。

有些时候,可能确实是必须这么做的,比如页面内容太多,有的部分查询逻辑复杂,比较耗时,这时候需要异步加载,这样搞确实比较好。

但是更多时候其实就是后端犯懒了,不想再写个新接口。除了涉及到性能的问题,大多数逻辑都应该在后端处理,能用一个接口处理完,就不应该让前端多调用第二个接口。

有前端的朋友曾经问过我,他说,他们现在做的系统中有些接口是根据用户身份来展示数据的,但是前端调用登录接口登录系统后,在调用其他接口的时候,除了在 Header 中加入 token 外,还有传很多关于用户信息的很多参数,这样做是不是不合理的。

这肯定不合理,token 本来就是根据用户身份产生的,后端拿到 token 就能获取用户信息,这是常识问题,让前端在接口中再传一遍,既不合理也不安全。

类似的问题还有,比如后端接口返回一堆数据,然后有的部分有用、有的部分没有,有的部分还涉及到逻辑,不借助文档根本就看不明白怎么用,这其实并不合理。

接口应该尽量只包含有用的部分,并且尽可能结构清晰,配合简单的字段说明就能让人明白是怎么回事,是最好的效果。

如果前后端都感觉形势不对了,后端一个接口处理性能跟不上了,前端处理又太麻烦了。这时候就要向上看了,产品设计上可能需要改一改了。

后端的同学可以学一点前端,前端的同学也可以学一点后端,当你都懂一些的时候,就能两方面考虑了,这样做出来的东西可能会更好用一点。总之,前后端相互理解,毕竟都是为了生活嘛。


作者:古时的风筝
链接:https://juejin.cn/post/7254927062425829413
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你的代码不堪一击!太烂了!

前言小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致...
继续阅读 »

前言

小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。

刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。

类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”

等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。

一、变量解构一解就报错

优化前

const App = (props) => {
const { data } = props;
const { name, age } = data
}

如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined 、null无法转为对象,所以对它们进行解构赋值时都会报错。

所以当 data 为 undefined 、null 时候,上述代码就会报错。

优化后

const App = (props) => {
const { data } = props || {};
const { name, age } = data || {};
}

二、不靠谱的默认值

估计有些同学,看到上小节的代码,感觉还可以再优化一下。

再优化一下

const App = (props = {}) => {
const { data = {} } = props;
const { name, age } = data ;
}

我看了摇摇头,只能说你对ES6默认值的掌握不扎实。

ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。

所以当 props.data 为 null,那么 const { name, age } = null 就会报错!

三、数组的方法只能用真数组调用

优化前:

const App = (props) => {
const { data } = props || {};
const nameList = (data || []).map(item => item.name);
}

那么问题来了,当 data 为 123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。

数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。

优化后:

const App = (props) => {
const { data } = props || {};
let nameList = [];
if (Array.isArray(data)) {
nameList = data.map(item => item.name);
}
}

四、数组中每项不一定都是对象

优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
}
}

一旦 data 数组中某项值是 undefined 或 null,那么 item.name 必定报错,可能又白屏了。

优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
}
}

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。

二次优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
}

五、对象的方法谁能调用

优化前:

const App = (props) => {
const { data } = props || {};
const nameList = Object.keys(data || {});
}

只要变量能被转成对象,就可以使用对象的方法,但是 undefined 和 null 无法转换成对象。对其使用对象方法时就会报错。

优化后:

const _toString = Object.prototype.toString;
const isPlainObject = (obj) => {
return _toString.call(obj) === '[object Object]';
}
const App = (props) => {
const { data } = props || {};
const nameList = [];
if (isPlainObject(data)) {
nameList = Object.keys(data);
}
}

六、async/await 错误捕获

优化前:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const res = await queryData();
setLoading(false);
}
}

如果 queryData() 执行报错,那是不是页面一直在转圈圈。

优化后:

import React, { useState } from 'react';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
try {
const res = await queryData();
setLoading(false);
} catch (error) {
setLoading(false);
}
}
}

如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。

二次优化后:

import React, { useState } from 'react';
import to from 'await-to-js';

const App = () => {
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const [err, res] = await to(queryData());
setLoading(false);
}
}

七、不是什么都能用来JSON.parse

优化前:

const App = (props) => {
const { data } = props || {};
const dataObj = JSON.parse(data);
}

JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。

优化后:

const App = (props) => {
const { data } = props || {};
let dataObj = {};
try {
dataObj = JSON.parse(data);
} catch (error) {
console.error('data不是一个有效的JSON字符串')
}
}

八、被修改的引用类型数据

优化前:

const App = (props) => {
const { data } = props || {};
if (Array.isArray(data)) {
data.forEach(item => {
if (item) item.age = 12;
})
}
}

如果谁用 App 这个函数后,他会搞不懂为啥 data 中 age 的值为啥一直为 12,在他的代码中找不到任何修改 data中 age 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。

优化后:

import cloneDeep from 'lodash.clonedeep';

const App = (props) => {
const { data } = props || {};
const dataCopy = cloneDeep(data);
if (Array.isArray(dataCopy)) {
dataCopy.forEach(item => {
if (item) item.age = 12;
})
}
}

九、并发异步执行赋值操作

优化前:

const App = (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
data.forEach(item => {
const { id = '' } = item || {};
getUrl(id).then(res => {
if (res) urlList.push(res);
});
});
console.log(urlList);
}
}

上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。

所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。

优化后:

const App = async (props) => {
const { data } = props || {};
let urlList = [];
if (Array.isArray(data)) {
const jobs = data.map(async item => {
const { id = '' } = item || {};
const res = await getUrl(id);
if (res) urlList.push(res);
return res;
});
await Promise.all(jobs);
console.log(urlList);
}
}

十、过度防御

优化前:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList?.join(',');
}

infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。

优化后:

const App = (props) => {
const { data } = props || {};
let infoList = [];
if (Array.isArray(data)) {
infoList = data.map(item => {
const { name, age } = item || {};
return `我的名字是${name},今年${age}岁了`;
});
}
const info = infoList.join(',');
}

后续

以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


作者:红尘炼心
链接:https://juejin.cn/post/7259007674520158268
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

多个AAR打包成一个AAR

AAR
1. 背景介绍公司日常开发基于自建的Maven服务器,不对外开放,公司内开发的SDK都传到私服,经过这么多年的迭代已经有上百个包,前段时间有其他公司需要依赖内部某个SDK,而这个SDK有依赖了公司好多SDK,但是公司内网权限无法对外开放,所以无法使用Maven...
继续阅读 »

1. 背景介绍

公司日常开发基于自建的Maven服务器,不对外开放,公司内开发的SDK都传到私服,经过这么多年的迭代已经有上百个包,前段时间有其他公司需要依赖内部某个SDK,而这个SDK有依赖了公司好多SDK,但是公司内网权限无法对外开放,所以无法使用Maven方式对外提供依赖,如果基于AAR方式,对外提供十几个AAR不仅不友好,而且内部也不好维护迭代。

2. 解决思路及办法

市面上有一套开源的合并AAR的方案,合并AAR主要的步骤:

  • AndroidManifest合并
  • Classes合并
  • Jar合并
  • Res合并
  • Assets合并
  • Jni合并
  • R.txt合并
  • R.class合并
  • DataBinding合并
  • Proguard合并
  • Kotlin module合并

这些都有对应Gradle task,具体方案可以看对应源码:adwiv/android-fat-aar目前已不再维护,gradle不支持高版本,kezong/fat-aar-android虽然也不在维护,但是已经适配了AGP 3.0 - 7.1.0,Gradle 4.9 - 7.3。

3. 遇到问题

3.1 资源冲突

如果library和module中含有同名的资源(比如 string/app_name),编译将会报duplication resources的相关错误,有两种方法可以解决这个问题:

  • 将library以及module中的资源都加一个前缀来避免资源冲突(不是所有历史版本的SDK都遵循这个规范);
  • gradle.properties中添加android.disableResourceValidation=true可以忽略资源冲突的编译错误,程序会采用第一个找到的同名资源作为实际资源(资源覆盖可能会导致某些错误)

3.2 动态库冲突

在application中动态库冲突可以使用pickFirst指定第一个,但是这个无法适用于library中。

关于packagingOptions常见的设置项有exclude、pickFirst、doNotStrip、merge。

1. exclude,过滤掉某些文件或者目录不添加到APK中,作用于APK,不能过滤aar和jar中的内容。

比如:

packagingOptions {
exclude 'META-INF/**'
exclude 'lib/arm64-v8a/libopus.so'
}

2. pickFirst,匹配到多个相同文件,只提取第一个。只作用于APK,不能过滤aar和jar中的文件。

比如:

 packagingOptions {
pickFirst "lib/armeabi-v7a/libopus.so"
pickFirst "lib/armeabi-v7a/libopus.so"
}

3. doNotStrip,可以设置某些动态库不被优化压缩。

比如:

 packagingOptions{
doNotStrip "*/armeabi/*.so"
doNotStrip "*/armeabi-v7a/*.so"
}

4. merge,将匹配的文件都添加到APK中,和pickFirst有些相反,会合并所有文件。

比如:

packagingOptions {
merge '**/LICENSE.txt'
merge '**/NOTICE.txt'
}

最后针对包含冲突动态库的SDK,单独对外依赖,在application中pickfirst,暂时没有特别好的方法。

3.3 外部依赖库

SDK中有些依赖的是外部公共仓库,比如OKHTTP等,如果都合并到同一的AAR,会导致外部依赖不够灵活,我们的思路是合并的时候不合并外部SDK,只打包公司内部SDK,并打印外部依赖的SDK,提供给外部手动依赖:

  1. 先定义内部SDK规则方法:
static boolean isInnerDep(RenderableDependency dep) {
return (dep.name.contains("com.xxx")
|| dep.name.contains("com.xxxxx")
|| dep.name.contains("com.xxxxxxx")
|| dep.name.contains("com.xxxxxxxx"))
}
  1. 定义三个集合:
//所有的内部库依赖
Map<String, String> allInnerDeps = new HashMap<>()
//所有的非内部依赖:公共平台库
Map<String, String> allCommonDeps = new HashMap<>()
//库的类型,jar 或者 aar,依赖方式不同
Map<String, String> depType = new HashMap<>()
  1. 分析依赖,放到不同集合打印、合并:

void collectDependencies(Map<String, String> commonDependencies, Map<String, String> innerDependencies, RenderableDependency result) {
String depName = result.name.substring(0, result.name.lastIndexOf(":"))
// println "denName = " + depName
String version = result.name.substring(result.name.lastIndexOf(":") + 1, result.name.length())

if (result.getChildren() != null && result.getChildren().size() > 0) {
if (isInnerDep(result) && !isExcludeDep(result)) {
tryToAdd(innerDependencies, depName, version)
result.getChildren().each {
res ->
collectDependencies(commonDependencies, innerDependencies, res)
}
} else {
tryToAdd(commonDependencies, depName, version)
}
} else {
if (isInnerDep(result) && !isExcludeDep(result)) {
tryToAdd(innerDependencies, depName, version)
} else {
tryToAdd(commonDependencies, depName, version)
}
}
}

configurations.findAll { conf ->
return conf.name == "implementation" || conf.name == "api"
}.each {
conf ->
// println "--------------"+conf.name
def copyConf = conf.copy()
copyConf.setCanBeResolved(true)
copyConf.each {
file ->
String s = file.name.substring(0, file.name.lastIndexOf("."))
String key
if (s.contains("-SNAPSHOT")) {
String t = (s.substring(0, s.lastIndexOf("-SNAPSHOT")))
key = t.substring(0, t.lastIndexOf("-"))
} else {
key = s.substring(0, s.lastIndexOf("-"))
}
String value = file.name.substring(file.name.lastIndexOf("."), file.name.length())
depType.put(key, value)
}
ResolutionResult result = copyConf.getIncoming().getResolutionResult()
RenderableDependency depRoot = new RenderableModuleResult(result.getRoot())
depRoot.getChildren().each {
d ->
collectDependencies(allCommonDeps, allInnerDeps, d)
}

}
println("==================内部依赖====================")

allInnerDeps.each {
dep ->
println dep.key + ":" + dep.value

dependencies {
String key = dep.key.substring(dep.key.lastIndexOf(":") + 1, dep.key.length())
String type = depType.get(key)
if (type == ".aar") {
embed(dep.key + ":" + dep.value + "@aar")
} else {
embed(dep.key + ":" + dep.value)
}
}
}

println "=====================正确使用 sdk,需要添加如下依赖========================"
allCommonDeps.each {
dep ->
println "api " + """ + dep.key + ":" + dep.value + """
}

3.4 对外提供多个业务SDK

我们提供一个同一AAR后,另一个业务也要对外提供SDK,这样有公共依赖的就会有冲突问题,如果都合并成一个,某一方改动,势必会引起另一方回归测试,最后抽取公共的sdk合并成一个aar,各自业务合并各自的AAR。

4. 参考资料

使用fat-aar编译打包多个aar库 - 简书

fat-aar实践及原理分享 - 简书

github.com/kezong/fat-…

GitHub - adwiv/android-fat-aar: Gradle script that allows you to merge and embed dependencies in generted aar file

5. 总结

本文介绍了Android对外输出AAR和不依赖maven,通过合并多个AAR的方式减少依赖方成本,并介绍了实际使用过程中遇到的问题和解决方案。


作者:轻口味
链接:https://juejin.cn/post/7250377976134615097
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

那些隐藏在项目中的kotlin小知识,在座各位...

写kotlin越来越久,很多代码虽然能看懂,并且能去改,但是不知道他用了啥,里面的原理是什么,举个例子?大家一起学习一下吧内联函数顾名思义,但是在项目中我遇到得很少,他比较适用于一些包装方法的写法,比如下面这个inline fun measureTimeMil...
继续阅读 »

写kotlin越来越久,很多代码虽然能看懂,并且能去改,但是不知道他用了啥,里面的原理是什么,举个例子?大家一起学习一下吧

内联函数

顾名思义,但是在项目中我遇到得很少,他比较适用于一些包装方法的写法,比如下面这个

inline fun measureTimeMillis(block: () -> Unit): Long {
val startTime = System.currentTimeMillis()
block()
return System.currentTimeMillis() - startTime
}

val time = measureTimeMillis {
// code to be measured here
}
println("Time taken: $time ms")

这样的函数,以后如果想要测一段代码的运行时间,只需要将measureTimeMillis包着他就行

类型别名

一个很神奇的东西,允许为现有类型定义新名称

data class Person(val name: String, val age: Int)
typealias People = List<Person>

val people: People = listOf(
Person("Alice", 25),
Person("Bob", 30),
Person("Charlie", 35)
)

fun findOlderThan(people: People, age: Int): People {
return people.filter { it.age > age }
}

fun main() {
val olderPeople = findOlderThan(people, 30)
println(olderPeople)
}

其中People就是一个别名,如果使用typealias替代直接定义list,项目中就会少很多后缀为list的列表,少了类似于personlist这种变量,在搜索,全局替换,修改时也会更加直观看到person和people的区分场景

typealias可以被大量使用在list, map乃至于函数中,因为这些命名可能会比较长,替换后可以提高可读性

高阶函数

一个一开始很难理解,理解后又真香的函数,我愿称理解的那一刻为程序员进阶闪耀时,当一个老程序员回首往事时,他不会因为虚度年华而悔恨,但是一定会因为不懂高阶函数而羞耻

尤其是在项目中发现这种函数,又看不懂时,是万万不敢问同事的,所以,请现在就了解清楚吧

fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}

fun main() {
val sum = calculate(10, 5) { x, y -> x + y }
println("Sum is $sum")

val difference = calculate(10, 5) { x, y -> x - y }
println("Difference is $difference")
}

可以看到,calculate其实并没有做什么,只是执行了传入进来的operation,这,就是高阶函数,所谓领导也是如此,优秀的下属,往往将方案随着问题传入进来,领导只要批示一下执行operation即可

配合上lambda原则,最后一个参数可以提出到括号外面,也就是讲operation提出到外面的{}中,交给调用方自己执行,就形成了这样的写法

    val sum = calculate(10, 5) { x, y -> x + y }

理解这一点后,一下子就清晰了很多,calculate看起来什么都没做,他却成为了世界上功能最强大,最灵活,bug最少的计算两个数运算结果的函数

深入

了解上面分析,已经足够我们在kotlin项目中进阶了,现在,我们来看下高阶函数反编译后的java代码

public final class TestKt {
public static final int calculate(int x, int y, @NotNull Function2 operation) {
Intrinsics.checkNotNullParameter(operation, "operation");
return ((Number)operation.invoke(x, y)).intValue();
}

public static final void main() {
int sum = calculate(10, 5, (Function2)null.INSTANCE);
String var1 = "Sum is " + sum;
System.out.println(var1);
int difference = calculate(10, 5, (Function2)null.INSTANCE);
String var2 = "Difference is " + difference;
System.out.println(var2);
}

// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}

虽然java的实现太不优雅,但是我们可以看出,高阶函数,本质上传入的函数是一个名为Function2的对象,

public interface Function2<in P1, in P2, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2): R
}

他是kotlin包自带的函数,看起来可以用来在反编译中替换匿名lambda表达式,将其逻辑移动到自身的invoke中,然后生成一个Function2对象,这样实现kotlin反编译为java时的lambda替换

这也是高阶函数得以实现的根本原因


作者:小松漫步
链接:https://juejin.cn/post/7256985832313094204
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin语法和 Gson 碰撞产生的空指针问题

1. 背景Gson 作为 json 解析最有名的库,我们也在多处使用或借鉴其实现。但是 json解析本就存在很多问题,并且这些问题轻则导致数据丢失,重则直接崩溃,我们应该对他引起重视。在项目更新kotlin之后,更由于g...
继续阅读 »

1. 背景

  • Gson 作为 json 解析最有名的库,我们也在多处使用或借鉴其实现。但是 json解析本就存在很多问题,并且这些问题轻则导致数据丢失,重则直接崩溃,我们应该对他引起重视。在项目更新kotlin之后,更由于gson库是基于java设计的,进而引出了我们今天遇到的问题。

2. 问题

  • 当通过 kotlin 调用 Gson.fromJson(“json”, Class<T>) 解析 json,并且对象通过 kotlin 创建时,有可能在非空的字段解析出 null,例如使用下列 json 和 class 进行解析。
data class LowGsonData(
@SerializedName("name") var name: String,
@SerializedName("age") var age: Int,
@SerializedName("address") var address: String
)

data class LowGsonData(
@SerializedName("name") var name: String = "",
@SerializedName("age") var age?: Int = 0,
@SerializedName("address") var address: String = ""
)

class TestGson {
@Test
fun test() {
val json = "{\"name\":\"cong\",\"age\":11}"
// val json2 = "{\"name\":,\"age\":11}"
val testData = Gson().fromJson(json, LowGsonData::class.java)
println("testData: name = ${testData.name} age = ${testData.age} address = ${testData.address}")
}
}
  • 在我们使用上述两个LowGsonData对json进行解析时,我们关注一下 testData.address 会被解析为什么?

test_address_null.png

address 不是非空的吗?为什么这里address是空?这样在业务代码很容易因为kotlin的空安全检测,导致空指针问题!

3. 寻找原因

1.把kotlin data转为java

  • 因为 kotlin 最终都是转化成 java 字节码运行在虚拟机上的,所以我们先把这个类转为 java 代码方便我们看清这个对象的本质
public final class TestGsonData {
@SerializedName("name")
@NotNull
private String name;
@SerializedName("age")
private int age;
@SerializedName("address")
@NotNull
private String address;

public TestGsonData(@NotNull String name, int age, @NotNull String address) {
Intrinsics.checkNotNullParameter(name, "name");
Intrinsics.checkNotNullParameter(address, "address");
super();
this.name = name;
this.age = age;
this.address = address;
}
}
  • 看着好像没啥问题,调用这个构造函数依然能保证数据非空。那我们就需要继续分析gson是怎么构造出对象的?

2.分析Gson是如何构造对象的

  • Gson 的逻辑,一般都是根据读取到的类型,然后找对应的 TypeAdapter 处理,本例为普通自定义对象,所以会最终走到 ReflectiveTypeAdapterFactory.create 返回相应的 TypeAdapter。其中包含构造对象的方法 3 个:

(1)newDefaultConstructor :我们大部分对象都是通过这个地方创建的,获取无参的构造函数,如果能够找到,则通过 newInstance反射的方式构建对象。

private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
try {
final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
return new ObjectConstructor<T>() {
@SuppressWarnings("unchecked") // T is the same raw type as is requested
@Override public T construct() {
Object[] args = null;
return (T) constructor.newInstance(args);

// 省略了一些异常处理
};
} catch (NoSuchMethodException e) {
return null;
}
}

(2)newDefaultImplementationConstructor:都是一些集合类相关对象的逻辑。

(3)newUnsafeAllocator:通过 sun.misc.Unsafe 构造了一个对象,是用来访问 hidden API,以及获取一定的操作内存的能力。

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
return new UnsafeAllocator() {
@Override
@SuppressWarnings("unchecked")
public <T> T newInstance(Class<T> c) throws Exception {
assertInstantiable(c);
return (T) allocateInstance.invoke(unsafe, c);
}
};
} catch (Exception ignored) {
}

// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream

}
  • 现在我们已经知道了,当这个对象没有无参构造函数时,第一个方法不成立,最终会通过 unSafe 方式构建对象。虽然 gson 自身的设计,通过三种方式来保证对象创建成功很棒,但是这恰好在Unsafe构造中绕过了 kotlin 的空安全检查。

  • 所以 Unsafe 为啥没能符合空安全呢?

    因为 UnSafe 是直接获取内存中的值, String 对象在没有赋值时正好是 null,并且 json 里没有对应值,最后将不会覆盖他。

  • 好的真相大白了,那有什么改进方法吗?有,尽量满足第一个条件。

  • kotlin 的 data calss 只要有一个属性没有给初始值就不会生成无参构造方法。所以要想保证 gson 解析场景的非空性,我们应该给所有非可空属性附初始值。或者一开始就设置可空,并在业务代码中判空。

data class FullGsonData(
@SerializedName("name") var name: String = "",
@SerializedName("age") var age: Int = 0,
@SerializedName("address") var address: String = ""
)
  • 但是全都这么写吗?毕竟有些对象在业务中需要构造方法传入一些必传的值。那我就比较贪心,我既要又要还要。

  • 我的想法: 在聊天 elem 的场景,结合业务,封装一个工厂供业务构造对象。并在 data class 中继续保持非空构造。

  • 有没有其他好的想法?

    • 通过 kotlin 插件规避

4. 如何规避该问题:

经过调研我认为比较好的方式有:

1.引入noarg和allopen自动生成无参构造函数。

2.尝试对现有项目中使用的json解析库进行升级改造

如moshi,同时适配属性缺失、属性异常等在生产中可能会遇到的问题。

5. 参考:


作者:学不明白的聪
链接:https://juejin.cn/post/7258129183330779194
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

编写整洁代码的技巧

背景前菜什么样的代码是整洁的?衡量代码质量的唯一标准,是别人阅读你代码时的感受。所谓整洁代码,即可读性高、易于理解的代码。不整洁的代码,阅读体验是这样的:乱(组织乱、职责乱、名称乱起)逻辑不清晰(if-else太多)绕弯子(简单的事写的很复杂)看不懂(只有写的...
继续阅读 »

背景

前菜

什么样的代码是整洁的?

衡量代码质量的唯一标准,是别人阅读你代码时的感受。所谓整洁代码,即可读性高、易于理解的代码。

不整洁的代码,阅读体验是这样的:

  1. 乱(组织乱、职责乱、名称乱起)
  2. 逻辑不清晰(if-else太多)
  3. 绕弯子(简单的事写的很复杂)
  4. 看不懂(只有写的人能理解)
  5. 难修改(耦合严重,各种写死)

整洁的代码,阅读体验是这样的:

  1. 清晰(是什么,做了什么,一眼看得出来)
  2. 简单(职责少,代码少,逻辑少)
  3. 干净(没有多余的逻辑)
  4. 好拓展(依赖的比较少,修改不会影响很多)

为什么需要编写整洁的代码?

  1. 保持代码整洁是程序员专业性的重要体现。 写软件就像是盖房子,很难想象一个地板不平、门窗关不严实的房子能称为一个大师制作。代码整洁可以体现一个人的专业水平和追求专业性的态度。

  2. 读代码的时间远远大于写代码。 根据《整洁代码之道》作者在书中小数据量统计,读代码与写代码的时间比可能达到10:1,实际项目中虽然达不到这个比例,但是需要阅读其他同学代码的场景并不少见。让代码容易阅读和理解,可以优化阅读代码的时间成本和沟通成本。

  3. 不整洁的代码带来诸多坏处。

    1. 每一笔不整洁的代码都是一笔技术债,迟早需要偿还,且随着时间的推移,偿还成本可能会越来越大。
    2. 烂代码难以理解,不敢改动,容易按住葫芦浮起瓢,修完一个bug又引入了另一个bug。
    3. 阅读不好的代码,会让人心情烦躁,充满负能量,是一种精神折磨。
    4. 容易引起破窗效应。当代码开始有一些bad smell,因为破窗效应,可能会导致代码越来越烂,不断积累形成“屎山”。

让代码变得整洁

命名

名副其实

在新建变量、函数或类的时候,给一个语义化的命名,不要因为害怕花时间取名字就先随手写一个想着以后再改(个人经验以后大概率是不会再改,或者想改的时候忘记要改哪里了)。如果名称需要注释来补充,那就不算是名副其实。

避免误导

起名字时,避免别人将这个名字误读成其他的含义。有以下几条准则可以使用:

  • 避免使用和本意相悖的词。

e.g.表达一组账号的变量:

  • 如果不是一个List类型,不要使用accountList。
  • 建议使用accountGroup。
  • 避免有歧义的命名。

e.g. 表达过滤后剩下的数据

  • 不要使用filteredUsers,filter具有二义性,不清楚到底是被过滤的,还是过滤后剩下的。
  • 建议使用removedUsers、remainedUsers来分别表示被过滤的和过滤后剩下的。
  • 避免使用外形相似度高的名称。

e.g.简单和单选:

  • 不要使用simple和single,外形相似,容易混淆。
  • 建议使用easy和single。
  • 避免使用不常见的缩写。

避免使用没有区分性的命名

  • 避免使用一些很宽泛的词: 比如Product类和ProductInfo或者ProductData类名称虽然不同,但其实意思是一样的。应该使用更有区分性的命名。再比如,getSize可能返回一个数据结构的长度、所占空间等,改成getLength或getMemoryBytes则更合适一些。
  • 避免tmp之类的名字: 除非真的是显而易见且无关紧要的变量,否则不要使用tmpXxx来命名。
  • 修改 IDE 自动生成的 变量名  IDE自动生成变量名字,有些时候是没有语义的,为了易于理解,在生成代码后,顺便修改变量名字。
/// BAD: element表示的语义是啥?需要结合前面的selectedOptions来推断element的语义
List<String> get selectedKeys {
return selectedOptions.map((element) => element.key).toList();
}

/// GOOD: 阅读代码即可知道获取的是已选选项的key
List<String> get selectedKeys {
return selectedOptions.map((option) => option.key).toList();
}

给变量名带上重要的细节

  • 表示度量的 变量名 带上单位: 如果变量是一个度量(长度、字节数),最好在名字中带上它的单位。比如:startMs、delaySecs、sizeMb等。
  • 附带其他属性: 比如未处理的变量前面加上raw。

不使用魔法数字

遇到常量时,避免直接将魔法数字编写到代码中。这种方式有诸多坏处:

  • 没有语义,需要思考这个魔法数字代表什么意思。进而导致这个代码只有写的人敢改。
  • 如果该魔法数出现多次,之后修改时需要覆盖到每个使用之处,一旦有一处没改,就会有风险。
  • 不便于搜索。

建议改为表达意图的常量,或使用枚举。

/// BAD: 需要耗费注意力寻找2的意义
if (status == 2) {
retry();
}

/// GOOD: 改为表达意图的命名变量
const int timeOut = 2;
if (status == timeOut) {
retry();
}

避免拼写错误

AndroidStudio有自带的拼写检查,平时在写代码的时候可以注意一下拼写错误提示。

注意变量名的长度

变量名不能太长,也不能太短。 太长的名字读起来太费劲,太短的名字读不懂是什么意思。那变量名长度到底多少最合适呢?这个问题没有定论,但是在决策变量名长度时,有一些准则可以使用:

  • 在小的作用域里可以使用短的名字: 作用域小的标识符不用带上太多信息。
  • 丢掉没用的词: 有时候名字中的某些单词可以拿掉且不会损失任何信息。例如:convertToString可以替换为toString。
  • 使用常见的缩写降低变量长度: 例如,pre代替previous、eval代替evaluation、doc代替document、tmp代替temporary、str代替string。

e.g. 在方法里,使用tempMap命名,只需要理解它是用于临时存储,最后作为返回值;但是如果tempMap是在一个类中,那么看到这个变量可能就会比较费解了。

static Map<String, dynamic> toMap(List<Pair> valuePairs) {
Map<String, dynamic> tempMap = {};
for (final pair in valuePairs) {
tempMap[pair.first] = pair.second;
}
return tempMap;
}

附:一些常用 命名规范 

变量

删除没有价值的临时变量

当某个临时变量满足以下条件时,可以删除这个临时变量:

  • 没有拆分任何复杂的表达式。
  • 没有做更多的澄清,即表达式本身就已经比较容易理解了。
  • 只用过一次,并没有压缩任何冗余代码。
/// BAD: 使用临时变量now
final now = datetime.datetime.now();
rootMessage.lastVisitTime = now;

/// GOOD: 去除临时变量now
rootMessage.lastVisitTime = datetime.datetime.now();

缩小变量的作用域

  1. 谨慎使用全局变量。 因为很难跟踪这些全局变量在哪里以及如何使用他们,并且过多的全局变量可能会导致与局部变量命名冲突,进而使得代码会意外地改变全局变量的值。所以在定义全局变量时,问自己一个问题,它一定要被定义成全局变量吗?

  2. 让你的变量对尽可能少的代码可见。 因为这样有效地减少了读者需要同时考虑的变量个数,如果能把所有的变量作用于都减半,则意味着同时需要思考的变量个数平均来说是原来的一半。比如:

    1. 当类中的成员变量太多时,可以将大的类拆分成小的类,让某些变量成为小类中的私有变量。
    2. 定义类的成员变量或方法时,如果不希望外界使用,将它定义成私有的。
  3. 把定义下移。 把变量的定义放在紧贴着它使用的地方。不要在函数或语句块的顶端直接放上所有需要使用的变量的定义,这会让读者在还没开始阅读代码的时候强迫考虑这几个变量的意义,并在接下来的阅读中,不断地索引是哪个变量。

函数

  1. 避免长函数

  • 函数要短小!函数要短小!函数要短小!(重要的事情说三遍)
  • 每个函数只做一件事。

如果发现一个函数太长,一般都是一个函数里干了太多事情,可以使用Extract Method(提取函数) 重构技巧,将函数拆分成若干个子功能,放到若干个子函数中,并给每个子函数一个语义化的命名(必要时可以添加注释)。这样既提高了函数的可读性,同时短小、单一功能的函数也方便复用。

避免太重的分支逻辑

if-else语句、switch语句、try-catch语句中,如果某个分支过于复杂,可以将该分支的内容提炼成独立的函数。这样不但能保持函数短小,而且因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。

/// BAD: if-else中语句多且繁杂
if (date.before(summerStart) || date.after(summerEnd)) {
charge = quantity * winterRate + winterServiceCharge;
} else {
charge = quantity * summerRate;
}

/// GOOD: 分别提炼函数
if (notSummer(date)) {
charge = winterCharge(quantity);
} else {
charge = summerCharge(quantity);
}

使用具有语义化的描述性名称给函数命名

  1. 函数名称应具有描述性,别害怕长的名称。长而具有描述性的名称,要比短而令人费解的名称好。
  2. 别害怕花时间取名字。

降低参数个数

  1. 参数个数越少,理解起来越容易。同时也意味着单测需要覆盖的参数组合少,有利于写单测。

  2. 当输入参数中有bool值时,建议使用Dart中的命名参数。

       /// BAD: bool类型取值只有true和false,无法理解在这个场景下取值的意义,必须得点到方法的声明里
    search(true);

    /// GOOD: 通过命名函数可以了解到取值的意义
    search(forceSearch : true);
  3. 当输入参数过多时,建议将其中一些参数封装成类。不然后续每每增加一个参数,就得修改函数的声明。

       /// BAD: 函数参数中放置多个离散的数据项
    void initUser({
    required String key,
    required String name,
    required int age,
    required String sex,
    }) {
    ...
    }

    /// GOOD: 将紧密相连的数据项聚合到一个类中
    class UserInfo {
    String key;
    String name;
    String sex;
    int age;
    }

    void initStore({required UserInfo user}) {
    ...
    }

分隔指令和查询

函数要么做什么事,要么回答什么事,二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致逻辑混乱。

注释

真正好的注释只有一种,那就是通过其他方式不写注释。

  1. 如果你发现自己需要写注释,再想想看能否用更清晰的代码来表达。
  2. 为什么要贬低注释的价值?注释存在的时间越久,就离所描述的代码越远,变得越来越错误,因为程序员不能坚持维护注释。
  3. 这个目标也并非铁律,项目中经常会存在一些千奇百怪背景的代码,指望全部靠代码表达是不可能的。

坏的注释

  1. 臃肿的、不清楚的、令人费解的注释。 如果不确定注释写的是否合适,让你旁边的同学看下能不能看懂。
  2. 简单的代码,复杂的注释。 阅读注释比代码本身更费时。
  3. 有误导的注释。 随着业务迭代,在改代码的时候并没有更改对应的注释,导致代码逻辑与注释不匹配,引起误解,这对其他开发人员是致命的。
  4. 显而易见的东西,没必要写注释。
  5. 注释不需要的代码。 不需要的代码不要通过注释的方式保存,直接删掉就好。不然别人可能也不敢删除这段代码,会觉得放在那里是有原因的。
  6. 注释不应该用于粉饰不好的设计。 比如给函数或变量随便取了个名字,然后花一大段注释来解释。应该想办法取个更易懂的名字。

好的注释

以下场景值得加上注释:

  1. 代码中含有复杂的业务逻辑,或需要一定的上下文才能理解的代码。 如果希望阅读者对代码背景或代码设计有个全局的了解,可以附上相关的文档链接。
  2. 用输入输出举例,来说明特别的情况。 相较于大段注释来说,一个精心挑选的输入、输出例子更有效。
  3. 将一些晦涩的参数和返回值翻译成可读的东西。
assertTrue(a.compareTo(a) == 0);  // a == a
assertTrue(a.compareTo(b) != 0); // a != b
  1. 代码中的警告、强调,避免其他人调用代码时踩坑。 在写注释的时候问问自己:“这段代码有什么出人意料的地方?会不会被误用?”预料到其他人使用你的代码时可能会遇到的问题,再针对问题写注释。
  2. 对代码的想法。 TODO(待办)、FIXME(有问题的代码)、HACK(对一个问题不得不采用的比较粗糙的解决方案)或一些自定义的注释(REFACTOR、WARNING)。
  3. 在文件、类的级别上,使用“全局观”的注释来解释所有的部分是如何工作的。 用注释来总结代码块,使读者不至于迷失在细节中。
  4. 代码注释应该仅回答代码不能回答的问题。 例如,方法注释应当应该写的是“为什么存在这个方法” 和 “方法做了什么”,而不是“方法是如何实现的”。如果方法注释过于关注方法“如何”工作,那么随着代码的不断变化,它很快就会过时。当开发人员依赖过时的注释来理解方法的工作原理时,这可能会导致混淆和错误。

格式

限制单个文件的代码行数

上图统计了Java中一些知名项目的每个文件的代码行数。可以看到都是由很多行数比较小的文件构成,没有超过500行的单独文件,大多数都少于200行。

小文件通常比大文件更加容易理解。 虽然这不是一个硬性规定,但一般一个文件不应该超过200行,且上限为500行。

dart中,可以通过part字段,对长文件进行拆分。

  1. 限制代码的长度

眼睛在阅读高而窄的文本时会更舒服,这正是报纸文章看起来是这样的原因:避免编写太长的代码行是一个很好的做法。另外,短行代码在使用三栏方式解冲突的时候,不需要横向滚动,更容易发现冲突的内容。

/// BAD: 参数放在一行展示
Future navigateToXxxPage({required BuildContext context, required Map<String, dynamic> queryParams, Object? arguments,});

/// GOOD: 每个参数一行展示,更清晰
Future navigateToXxxPage({
required BuildContext context,
required Map<String, dynamic> queryParams,
Object? arguments,
});

合理使用代码中的空行

源代码中的空行可以很好的区分不同的概念。反之,内容相关的代码不应该空行,应该紧贴在一起。

变量、函数声明

  1. 变量的声明应尽可能接近它使用的地方。 类的成员变量的声明应该出现在类的顶部。局部使用的变量应该声明在它使用之处附近。
  2. Dart函数中参数的声明,required标记的参数尽量归在一起。
  3. 如果有一堆变量要声明(类的成员变量、函数的参数),可以从重要的到不重要的进行排序。
  4. 如果一个函数调用另一个函数,它们应该在垂直上靠近,并且如果可能的话,调用者应该在被调用者之上。 在一般情况下,我们希望函数调用依赖关系指向向下的方向。也就是说,一个被调用的函数应该在一个执行调用的函数下面。像在看报纸一样,我们期待最重要的概念最先出现,低层次的细节出现在最后。

简化控制流、表达式

如果代码中没有条件判断、循环或者任何其他的控制流语句,那么它的可读性会很好。而跳转和分支等部分则会很快地让代码变得混乱。

调整条件语句中参数的顺序

比较的左值为变量,右值为常量。这种方式更符合自然语言的顺序。

/// BAD: 
if (10 <= length)

/// GOOD:
if (length >= 10)

调整if-else语句块的顺序

在写if-else语句的时候:

  • 首先处理正逻辑而不是负逻辑的情况。例如,用if(debug)而不是if(!debug)
  • 先处理掉简单的情况。这种方式可能还会使得if和else在屏幕之内都可见。
  • 先处理有趣的或者是可疑的情况。

合并相同返回值

当有一系列的条件测试返回同样的结果时,可以将这些测试合并成一个条件表达式,并将这个条件表达式提炼成一个独立函数。/// BAD: 多个条件分开写,但是返回了同一个值。

int test() {
final bool case1 = ...;
final bool case2 = ...;
final bool case3 = ...;
if (case1) {
return 0;
}
if (case2) {
return 0;
}
if (case3) {
return 0;
}
return 1;
}

/// GOOD:将统一返回值对应的条件合并。
int test() {
if (shouldReturnZero()) {
return 0;
}
return 1;
}

bool shouldReturnZero() {
final bool case1 = ...;
final bool case2 = ...;
final bool case3 = ...;
return case1 || case2 || case3;
}

不要追求降低代码行数而写出难理解的表达式

三目运算符可以写出紧凑的代码,但是不要为了将所有代码都挤到一行里而使用三目运算符。三目运算符应该是从两个简单的值中做选择,如果逻辑复杂,使用if-else更好。

/// BAD: 
return exponent >= 0 ? mantissa * (1 << exponent): mantissa/(1<<-exponent);

/// GOOD:
if (exponent >= 0) {
return mantissa * (1 << exponent);
} else {
return mantissa / (1 << -exponent);
}

避免嵌套过深

条件表达式通常有两种表现形式:

  • 所有分支都属于正常行为(使用if-else形式)。
  • 只有一种是正常行为,其他都是不常见的情况(if-if-if...-正常情况)。

嵌套过多深使代码更难读取和跟踪,可以尽量将代码转为以上两种标准的if形式。

/// BAD: if-else嵌套太深,难以理解逻辑。
void test() {
if (case1) {
return a;
} else {
if (case2) {
return b;
} else {
if (case3) {
return c;
} else {
return d;
}
}
}
}

/// GOOD: 先处理非正常情况,直接退出,再处理正常情况,降低理解成本
void test() {
if (case1) return a;
if (case2) return b;
if (case3) return c;
return d;
}

不要使用if-else代替switch

一般使用switch的场景,都是某个变量有可枚举的取值,比如枚举类型,不要使用if-else来代替枚举值的判断:

enum State {success, failed, loading}

/// BAD: 对于现在的流程是没问题,但是万一新增了一个State,忘记修改这里,就会出现风险;
/// 况且switch本身就适合在这种场景下使用。
void fun() {
if (state == State.success) {
// do something when success
} else if (state == State.failed) {
// do something when failed
} else {
// do something when loading
}
}

/// GOOD: 当State新增了一个枚举值时,这里会报错,必须修改这里才能编译通过
void fun () {
switch (state) {
case State.success:
// do something when success
break;
case State.failed:
// do something when failed
break;
case State.loading:
// do something when loading
break;
}
}

让表达式更易读

日常写代码时,最常见的一个现象就是if语句的条件中,包含了大量的与或非表达式,如果表达式的逻辑简单还好,一旦表达式开始嵌套或多个与或非并列,那么对于理解代码的人来说将是一个灾难。遇到这种情况,可以使用以下的技巧,逐步优化代码:

  1. 提取解释变量。 引入额外的变量,来表示一个小一点的子表达式。

    /// BAD: 阅读代码的人需要理解line.split(":")[0].trim()代表什么,当没有注释时往往纯靠猜测
    if (line.split(":")[0].trim() == "root") {
    // xxx
    }
    /// GOOD: 快速理解line.split(":")[0].trim()的语义,便于理解if条件表达式
    final userName = line.split(":")[0].trim();
    if (userName == "root") {
    // xxx
    }

    /// BAD: 理解这个表达式需要花多久?
    if (line.split(":")[0].trim() == "root" || line.split(":")[1].trim() == "admin") {
    // xxx
    }
    /// GOOD: 还是这个更容易理解?
    final isRootUser = line.split(":")[0].trim() == "root";
    final isAdminUser = line.split(":")[1].trim() == "admin";
    if (isRootUser || isAdminUser) {
    // xxx
    }
  2. 使用总结变量。 当if语句的条件比较复杂时,将整个条件表达式使用一个总结变量代替。

    /// BAD: 阅读代码的人需要理解什么情况下能进入if语句,代表什么语义
    if (newSelect != null && preSelect != null && newSelect != preSelect) {
    // xxx
    }
    /// GOOD: 快速理解if语句的语义,如果关注细节,再看表达式的构成
    final selectionChanged = newSelect != null && preSelect != null && newSelect != preSelect;
    if (selectionChanged) {
    // xxx
    }
  3. 减少非逻辑嵌套。 对于一个bool表达式,有一下两种等价写法,大家可以自行判断哪个更加可读。

    /// BAD: 阅读代码的人需要理解什么情况下能进入if语句,代表什么语义
    if (!(fileExists && !isProtected)) {
    // xxx
    }
    /// GOOD: 快速理解if语句的语义,如果关注细节,再看表达式的构成
    if (!fileExists || isProtected) {
    // xxx
    }

类应该短小

与函数一样,在设计类时,首要规则就是尽可能短小。对于函数,评价的指标是代码行数;对于类,评价指标则为职责,即如果无法为某个类取个精准的名称,那就表明这个类太长了。

那么,如何让类保持短小呢?这里需要先介绍一条原则:

单一职责原则:即类或模块应有且只有一条加以修改的理由,即一个类只负责一项职责。

使用单一职责,将大类拆分为若干内聚性高的小类,即可实现类应该短小的规则。

  • 所谓内聚,即类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。通常而言,方法操作的变量越多,则内聚性越高。
  • 让代码能运行保持代码整洁,是截然不同的两项工作。大多数人往往把精力花在了前者,这是没问题的。问题在于,当代码能运行后,不是马上转向实现下一个功能,而是回头将臃肿的类切分成只有单一职责的去耦合单元。
  • 许多开发者可能会觉得使用单一职责会导致类的数量变多,但其实这种方式会让复杂系统中的检索和修改变得更加清晰简单。

为修改而组织

编写代码时,需要考虑以后的修改是否方便,降低修改代码的风险。

开放封闭原则 :类应当对扩展开放,对修改关闭。

单元测试

测试驱动开发(Test-Driven Development, TDD),要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。 这有助于编写简洁可用和高质量的代码,并加速开发过程。

虽然在日常开发中,我们并不是按照这种方式开发,但是这种思想对于提高代码能力大有裨益。也许你会好奇,这种测试先行的方法与测试后行的方法有什么区别?总结下来就是:

  • 假如你首先编写 测试用例 ,那么你将可以更早发现缺陷,同时也更容易修正它们。 当你聚焦在一个方法时,将会更容易发现这个方法的边界case,而我们代码中大部分的缺陷都是由于一些边界case疏漏导致的。
  • 在编写代码之前先编写 测试用例 ,能更早地把需求上的问题暴露出来。 在写业务代码前,先思考测试用例有哪些,一些边界问题自然就会浮出水面,迫使你去解决。
  • 首先编写 测试用例 ,将迫使你在开始写代码之前至少思考一下需求和设计,而这往往会催生更高质量的代码。 写测试,你就会站在代码用户的角度来思考,而不仅仅是一个单纯的实现者,因为你自己要使用它们,所以能设计一个更有用,更一致的接口。另外为了保证代码的可测性,也迫使你会将不同的逻辑解耦,降低测试需要的上下文。

保持测试整洁

  • 如果没有测试代码,程序员会不敢动他的业务代码。 这点在改表单、计算引擎逻辑时深有体会,经常按下葫芦浮起瓢。

  • 修改业务代码的同时,相应的测试代码也需要修改。 如果单测不能跑,那单测就毫无意义,写单测并不是为了应付,而是保证代码的正确性,所以不要因为懒得修改导致破窗效应。

  • 如何让测试代码整洁? 可读性!可读性!可读性!单元测试中的可读性比生产代码中更加重要。测试代码中,相同的代码应抽象在一起,遵循 Build-Operate-Check 原则,即每一个测试应该清晰的由以下这三个部分组成:

    • Build: 构建测试数据。
    • Operation: 操作测试数据。
    • Check: 检验操作是否得到期望结果。
  • 每个 测试用例 只做一件事。 不要写出超长的测试函数。如果想要测试3个功能,就是拆成3个测试用例。

整洁测试规则(F.I.R.S.T)

  • 快速(Fast) :测试代码需要执行得很快。测试运行慢→不想频繁运行测试代码→不能尽早发现生产代码的问题→代码腐坏。
  • 独立(Independent):测试代码不应该相互依赖,某个测试不应该成为下一个测试的设定条件。测试代码都应该可以独立运行,以及按任何顺序运行。当测试互相依赖时,会导致问题难以定位。
  • 可重复(Repeatable):测试代码应该在任何环境下都可以重复执行。
  • 自足验证(Self-Validating) :测试需要有一个bool类型的输出。不能通过看log判断测试是否通过,而应该通过断言。
  • 及时(Timely):测试代码需要及时更新,在编写业务代码之前先写测试代码。如果程序员先写业务代码,很有可能造成写测试代码不方便的问题。

作者:陌上疏影凉
链接:https://juejin.cn/post/7258445326913683517
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

做项目,聊聊敏捷开发

我对敏捷开发是源于10多年前看了一本关于迭代开发的书,从而对迭代开发有了一些兴趣。从那时开始有了迭代开发的概念。随着项目经验的增加迭代的重要性也越发觉得明显。随后进入了提倡敏捷开发的公司,被迫式的接触了许多“敏捷开发”,随着项目经历越来越多,慢慢的就开始有了更...
继续阅读 »

我对敏捷开发是源于10多年前看了一本关于迭代开发的书,从而对迭代开发有了一些兴趣。从那时开始有了迭代开发的概念。随着项目经验的增加迭代的重要性也越发觉得明显。随后进入了提倡敏捷开发的公司,被迫式的接触了许多“敏捷开发”,随着项目经历越来越多,慢慢的就开始有了更新的认识和想法。

但是在接触敏捷开发这个体系之前,自己有机会做一个项目,那个时候我开始将自己认为更有利于项目的管理工作做了一些应用,那个阶段我的主要做法是:

1、项目中开始划分更短的制品交互周期,而不是以前那样等待产品开发完毕后发布各种测试版本。
2、更充分与市场人员交流,在市场人员进行需求交底时,让更多的甚至全体成员参与会议,了解产品的原始业务及需求。并且在过程中有问题也及时的解答及沟通。
3、加强沟通力度,开发测试都在一起每天都会开个小会,通报每日的工作成果,将自己的问题说出来。
4、不同以往的发布频率,测试从项目开始便要切入到产品生产过程,而不是等到最后所有功能都完成后。从而大大减少变动对计划的影响。

在做这些工作的时候我并不知道敏捷开发这个东西,直到在2010年进入一个公司非常提倡敏捷开发,已经有了迭代周期、backlog、站立会议、周例会等等,在这个团队中对开发过程有各种规章要求,完全是制度化的,这在我加入的初期非常的不适应。事实上回头想想,那种方式已经变的不敏捷了,完全是一种教条式的应用。

后来自己有机会回到了老东家,开始自己带团队,很巧老东家被收购后开始推广敏捷开发,只不过因为不是总部,所以这次没有范本,完全由我自己来组织及控制。很高兴这个小团队几个月下来,个人觉得比较成功,当然后面也得到了公司的认可。

下面就敏捷开发分享一些应该着重注意的点,解决这些问题我想对任何开发团队都会有很大的帮助。

需求在开发中的重要性

大量的开发过程告诉我,需求在软件开发过程中是极其重要的。传统的开发强调初期的需求调研及需要分析,这个过程对于一些正规的团队会产生大量的文档,而后交由开发展开产品生产。

然而,事实却不是想象这么简单,无数的例子说明了一点,仅仅在需求调研过程中了解到的需求是无法保证的。数不清的例子告诉我们,需求是会变的,变的原因很多。在极端的情况下,有些客户签字的需求在开发完后,有需要变更也很正常。

所以需求是影响软件开发的第一重要因素,需求来源于业务,我们开发的产品不就是因为这些业务才去做的吗?如何需求都无法把握好,还谈什么开发出好用的产品?

然而如何做好需求呢?我想首先要确立需求的地位,然后只有通过不断的沟通、尝试、反馈向真实需求迈进。

强调人与人的交流

不管怎么样开发过程中主要还是靠人的,而且软件开发是个复杂的团体工程,一个小些的产品也会涉及到各类人:客户、业务分析、管理人员、程序员、测试员等等。这么多人在一起做事情,有一方没有处理好结果肯定就会有问题。

有这样一个例子:客户提出了一个会员管理功能需求,需求人员了解后组织了解决方案,于是交付了开发实现。而经过二个月无尽的黑夜之后交付,需求一看有个模块做的有偏差,但是已经来不及修改了。交给客户看后,发现这不是他们要的会员管理功能相差较大,另外在功能开发的这一段时间,客户又有了新想法,要对原先需求做调整。

这种例子可能大家经常经历吧?

这种问题在敏捷开发方法中提出了解决方法,就是通过不断的交付可用的制品。看起来很抽象,其实很简单。同样是上面的例子:
Ø 客户提出会员管理功能需求
Ø 需求人员在了解需求后与开发负责人商量,确定一个快迭代的开发计划,每二周向客户演示一次,并将这个计划与客户确认
Ø 确认后需求人员向全体成员讲解需求背景故事
Ø 开发负责人组织并确定迭代计划内容,明确每个迭代提交的产品目标、开发任务安排、测试跟踪计划
Ø 每个迭代过程中都由需求及测试进行确认每个任务的实现结果是否跑偏
Ø 后面就是每二周向客户演示一次产品,并获得客户的反馈
Ø 根据客户的反馈调整下个迭代计划,并继续下一个迭代
Ø 直到产品交付

通过上面的步骤,就不至于在开发完成后才知道用户的真实想法,因为很多用户对软件开发是没有概念的,他只知道自己有某种需求,但最开始是没有一个完整的概念的。所以就要通过不断的让用户看到产品的模型,这个过程用户才会逐步的对产品产生概念。同样的在过程中客户的提出需求变更也是在一定的可控制范围之内,这样一来可以大大的减少软件返工的情况,自然就不会拖延计划了。

而这个过程中,需求已经完成了一个真正的过渡,不再是一头重的情况了。他让需求从客户那快速的反馈到开发团队中。同样的,在开发不断的交付制品时,需求也更加及时的了解到产品的进度,把握开发人员开发的功能是否符合需求。
当然这并不是一个标准做法,不同的团队可以有不同的处理方式。这里只是想强调需求需要更多的投入到开发过程中去,及时的与客户沟通交流,了解到客户的真实想法。

强调文档的作用

我觉得很多对敏捷开发的一个误解就是不需要文档,敏捷开发并未抛弃文档。只是更强调更有效的方式使用文档。在很多传统开发方法中,特别是很多很正规的开发团队对文档的要求非常苛刻。然而事实是文档不易管理,最痛苦的是不好维护,文档需要随着变化而变化,比如需求调整、技术架构升级、产品维护等等。如果要保证文档的一致性,太难了。特别是对于一些无法进行有效管理的开发团队就更加明显,经常是软件已经几个版本了,文档却是两年前的。

但敏捷真的不需要文档吗?我想不是的,如何把文档做到好维护我想才是最重要的。文档到底指的指的什么?什么样的算文档?

提出上面两个问题,我们先想想经常说的文档的作用是什么?不就是一个传播工具吗?可以用作记录、给他人看、用于以后查看。有很多方法可就解决了这个问题,比如wiki系统。维护一个wiki系统,可以随时写,随时维护,可以方便的查找。嗯,多方便。

另外一个问题就是什么样的工作需要形成文档呢?

记得在前一家公司,维护一个10多年的老系统修改一个公式计算的BUG,但是怎么也不知道这个复杂的公式是什么意思,问过了公司大部分的人也无人可解。这时想,如果当初有那么一份文档,谢天谢地。

像这种关键的内容有份文档还是很重要的,否则随着时间推移,谁也不能保证能记得住当时为什么会这么干。

记得多年前一次记笔记的经历,我看了一篇文章了解了DELPHI实现单实例模式的方法,这种方法很酷。于是整理成了笔记写在了wiki上,第二天就得到了回复,帮助到了别外产品开发组的同事。

嗯,文档就是这样他具有传播性,你不可能跑去跟所有人说出你的想法,但是文档却更容易达成。他也有传承性,有些文档也许10多年后又起了重要作用。

团队协作

1、减少对开发人员的干扰

曾经接手一个产品的开发,最初遇到一个很头痛的问题,原先写好的迭代计划,而且工作量也较大,大家都在忙着。即便在这样的状态下,客服人员却经常跑来找某个程序员A维护各种系统问题,程序员A在一次维护中竟然导致了系统数据出现大面积错误。程序员A心理上承受着巨大的压力,而每天的这些问题又不得不解决,加之新版本又有很重的开发任务无法完成,最终导致整个开发计划变更。

我无法再忍受,找到了需求及客服的负责人,沟通后发现这些问题很多都是重复性的,主要是因为原先系统的不足。于是回去组织人员做了几个后台临时功能,并交付给了客服人员,之后就没有再来找过这位程序员A。后续我又找到了客服负责人,要求不能直接找开发人员解决这类问题,并与负责人约定了处理过程。

这是个例子,在实际情况中还有很多这种事情,甚至有很多开发人员要直接面对客户。我想对于职能型团队来说,开发团队最好是减少这些方面的干忧。当然对于一个人包干的情况就不讨论了。

大部分的人都不是超人,在一个时间段内处理超出自己负荷的工作是很难做好保质保量的。所以对于开发管理人员一定要考虑到这点,尽量让开发人员有比较好的工作进度环境,通过外界的方式来解决一些开发团队的干扰。

成功的前端工程师很会善用工具,这些年低代码概念开始流行,像国外的Mendix,国内的JNPF,这种新型的开发方式,图形化的拖拉拽配置界面,并兼容了自定义的组件、代码扩展,确实在B端后台管理类网站建设中很大程度上的提升了效率。

开源地址:http://www.yinmaisoft.com/?from=jueji…

任何信息化项目都可以基于 JNPF 开发出 ERP、OA、CRM、EHR 等各类管理系统。

这边强烈建议试试它,你会发现不一样的惊喜!心情舒畅还是很重要的,记得有一次迭代总结时,有个程序员总结说:发现心情舒畅自己的工作效率很高。呵呵。我想你也有同感吧。

2、不要忽略测试人员在开发阶段的作用

曾经多少次在项目发布前加班到深夜2点的情景还历历在目,那种感觉即快乐又痛苦。由于和客户签定的合同的交付日期就要到了,产品却迟迟未集成完成,测试只能干等着上网聊QQ。就在下班前的一刻发布了,测试开始了紧张的测试,在屏幕闪动中,一个个的BUG提交,直到流程都无法都走不下去,测试无奈了。第二天就要发布,实施人员就等着制品第二天出差。只有不断的改,再发布,无尽的循环。直到大家都憔悴的看着老大,终于老大说:还剩下的这几个问题无关紧要,大家回去吧。

几个月的开发过去后在总结会上,只能抱怨测试资源不足,时间太短,需求更改太多,需求更改后测试不知道。无数的问题一次一次的出现在同样的总结会议上。

上面的这个例子很多人应该经历过,真的测试只有最后一刻才能体现价值吗?我想不是的。

在后面的项目中我总结了这个问题的,针对每个开发任务要求进行测试验证。而测试如何验证呢?他需要知道这个开发任务的需求是如何,提前做好测试计划及测试用例,在接到开发制品后测试并提交BUG,这个工作是可以开发过程中就能不断的进行的。保证每一个任务的质量,可以大大减少后期集成的错误量。

另外根据敏捷开发的思想,测试团队在开发过程中也需要加强与开发团队的交流,甚至有必要组成虚拟团队,位置调整到一起,这样可以及时快速的交流,参加开发团队的站立会议同样可以及时了解到开发的实际情况及进度,反过来把握测试计划及测试内容。

特别是测试从另一个角度来审视需求,这样也可以一定程度上发现或者改善需求上的不足。

3、发挥团队人员的潜力

敏捷开发比较提倡开发任务由开发自己评估并认领工作任务,这样可以激发开发的潜在动力。

之前在做一个新产品时,需要使用java,而我们团队是使用C#的,面临转型问题。而有一位同事很感兴趣,于是我就让他负责前期的框架探索与搭建。结果就是这位小伙工作效率很高,我最初给他的目标全部都完成了。最有意思的是后面产品开始研发时,这位小伙已经成为了团队的大牛,大家有问题都找他解决。也正是因为这个过程,这位小伙被全面激活,也在大家面前展示了能力。甚至在小伙离职时也被领导给予大幅涨薪来挽留。只不过谁又能想象到这位小伙进入我团队之前是因为被定为裁员的目标而调剂过来的呢!

所以充分发挥好每个人员的特点,让人能够在自己感兴趣的工作中,效果会很多。减少指派方式的任务的分配,充分发挥个人的主动性,这个团队精神面貌也会好很多。

4、管理者不要离团队太远

作为团队的Leader要参与到团队的工作中去,比如一个开发主管一定要写写代码,参与架构等对项目有关的事情,而不是在那里分分任务。这样团队成员才会觉得这个Leader很亲近感。

特别是有些开发主管在带队后离团队越来越远,有时对于开发进度不如意时就说:“这么个简单功能怎么会搞了这么久?”,其实每天都在加班的同事心里想着:“有本事你来?”,即使这个小组长有这个能力,但对于团队来说也不是一件好事,因为大家都抱有怨恨之心,还谈什么好好工作呢?这个小组长就是失职的。所以这种情况下应该主动去了解进度滞后的原因,并且自己要加入到解决问题的工作中去,而不是在边上抱怨别人。

5、小组织不要搞太多的官

中国几千年的文化,官本位一直影响着我们,大家都想坐在那指挥,自己啥事也不用干,想想都惬意。在我们这个行业是不是发现也很类似?大家都想着干几年当个小组长,然后升个部门经理,当上CTO迎娶白富美。

团队的管理基本是事与人的管理,非常的伤脑和心。如果一个组织内,特别是小组织内“官”太多,协调就会非常的难,大家就会经常性的扯皮。

结束

与敏捷开发结缘也有几年,从开始的抵触到后面的认可经历了许多,这个过程并不是一蹴而就的,需要花时间花精力,特别是要去实践、总结。

还有我觉得是不能太教条,很多事情都要有怀疑的心,然后去实践总结,找到合适自己团队的方式方法。

注:此文章为原创,欢迎转载,请在文章页面明显位置给出此文链接! 若您觉得这篇文章还不错请点击下右下角的推荐,非常感谢!


作者:雾岛听风来
链接:https://juejin.cn/post/7249205118532763709
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

如果写劣质代码是犯罪,那我该判无期

导读程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的...
继续阅读 »

导读

程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的不整洁代码行为,并提出针对性优化建议。继续阅读~

目录

1 代码风格和可读性

2 注释

3 错误处理和异常处理

4 代码复用和模块化

5 硬编码

6 测试和调试

7 性能优化

8 代码安全性

9 版本控制和协作

10 总结

01、代码风格和可读性

  • 错误习惯
不一致的命名规则:使用多种命名规则,如 camelCase、snake_case 和 PascalCase 等。过长的函数和方法:编写过长的函数和方法,导致代码难以阅读和理解。 过长的行:编写超过50字符的代码行,导致代码难以阅读。

1.1 变量命名不规范

在编程中,变量命名是非常重要的,良好的变量命名能够提高代码的可读性和可维护性。不规范的命名会增加理解难度,以下是一个不规范命名的例子:

int a, b, c; // 不具有描述性的变量名
float f; // 不清楚变量表示的含义

这样的变量命名不仅会降低代码的可读性,还可能会导致变量混淆,增加代码维护的难度。正确的做法应该使用有意义的名称来命名变量。例如:

int num1, num2, result; // 具有描述性的变量名
float price; // 清晰明了的变量名

1.2 长函数和复杂逻辑

长函数和复杂逻辑是另一个常见的错误和坏习惯。长函数难以理解和维护,而复杂逻辑可能导致错误和难以调试。以下是一个长函数和复杂逻辑的案例:

def count_grade(score):
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'

if grade == 'A' or grade == 'B':
result = 'Pass'
else:
result = 'Fail'
return result

在这个例子中,函数 count_grade 包含了较长的逻辑和多个嵌套的条件语句,使得代码难以理解和维护。正确的做法是将逻辑拆分为多个小函数,每个函数只负责一个简单的任务,例如:

def count_grade(score):
grade = get_grade(score)
result = pass_or_fail(grade)
return result
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def pass_or_fail(grade):
if grade == 'A' or grade == 'B':
return 'Pass'
else:
return 'Fail'

通过拆分函数,我们使得代码更加可读和可维护。

1.3 过长的行

代码行过长,会导致代码难以阅读和理解,增加了维护和调试的难度。例如:

def f(x):
if x>0:return 'positive' elif x<0:return 'negative'else:return 'zero'

这段代码的问题在于,它没有正确地使用空格和换行,使得代码看起来混乱,难以阅读。正确的方法是,我们应该遵循一定的代码规范和风格,使得代码清晰、易读。下面是按照 PEP 8规范改写的代码

def check_number(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'

这段代码使用了正确的空格和换行,使得代码清晰、易读。

02、注释

  • 错误习惯
缺少注释:没有为代码编写注释,导致其他人难以理解代码的功能和逻辑。 过时的注释:未及时更新注释,使注释与实际代码不一致。 错误注释:注释上并不规范,常常使用一些不合理的注释。
  • 错误的注释

注释是非常重要的,良好的注释可以提高代码的可读性和可维护性。以下是一个不规范的例子:

int num1, num2; // 定义两个变量

上述代码中,注释并没有提供有用的信息,反而增加了代码的复杂度。

03、错误处理和异常处理

  • 错误的习惯
忽略错误:未对可能出现的错误进行处理。 过度使用异常处理:滥用 try...except 结构,导致代码逻辑混乱。 捕获过于宽泛的异常:捕获过于宽泛的异常,如 except Exception,导致难以定位问题。

3.1 忽略错误

我们往往会遇到各种错误和异常。如果我们忽视了错误处理,那么当错误发生时,程序可能会崩溃,或者出现不可预知的行为。例如:

def divide(x, y):
return x / y

这段代码的问题在于,当 y 为0时,它会抛出 ZeroDivisionError 异常,但是这段代码没有处理这个异常。下面是改进的代码:

def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return 'Cannot divide by zero!'

3.2 过度使用异常处理

我们可能会使用异常处理来替代条件判断,这是不合适的。异常处理应该用于处理异常情况,而不是正常的控制流程。例如:

def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = float('inf')
return result

在这个示例中,我们使用异常处理来处理除以零的情况。正确做法:

def divide(a, b):
if b == 0:
result = float('inf')
else:
result = a / b
return result

在这个示例中,我们使用条件判断来处理除以零的情况,而不是使用异常处理。

3.3 捕获过于宽泛的异常

捕获过于宽泛的异常可能导致程序崩溃或隐藏潜在的问题。以下是一个案例:

try {
// 执行一些可能抛出异常的代码
} catch (Exception e) {
// 捕获所有异常,并忽略错误}

在这个例子中,异常被捕获后,没有进行任何处理或记录,导致程序无法正确处理异常情况。正确的做法是根据具体情况,选择合适的异常处理方式,例如:

try {
// 执行一些可能抛出异常的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常
logger.error("File not found", e);
} catch (IOException e) {
// 处理IO异常
logger.error("IO error", e);
} catch (Exception e) {
// 处理其他异常
logger.error("Unexpected error", e);}

通过合理的异常处理,我们可以更好地处理异常情况,增加程序的稳定性和可靠性。

04、错误处理和异常处理

  • 错误的习惯
缺乏复用性:代码冗余,维护困难,增加 bug 出现的可能性。 缺乏模块化:代码耦合度高,难以重构和测试。

4.1 缺乏复用性

代码重复是一种非常常见的错误。当我们需要实现某个功能时,可能会复制粘贴之前的代码来实现,这样可能会导致代码重复,增加代码维护的难度。例如:

   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume_of_cuboid(length, width, height):
return length * width * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

上述代码中,计算逻辑存在重复,这样的代码重复会影响代码的可维护性。为了避免代码重复,我们可以将相同的代码复用,封装成一个函数或者方法。例如:

   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume(length, width, height):
return calculate_area_of_rectangle(length, width) * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

这样,我们就可以避免代码重复,提高代码的可维护性。

4.2 缺乏模块化

缺乏模块化是一种常见的错误,这样容易造成冗余,降低代码的可维护性,例如:

   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑
```

此例中,User 和 Order 类都包含了保存和发送邮件的逻辑,导致代码重复,耦合度高。我们可以通过将发送邮件的逻辑提取为一个独立的类,例如:

   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

class EmailSender:
def send_email(self, content):
# 发送邮件的逻辑

通过把发送邮件单独提取出来,实现了模块化。现在 User 和 Order 类只负责自己的核心功能,而发送邮件的逻辑由 EmailSender 类负责。这样一来,代码更加清晰,耦合度降低,易于重构和测试。

05、硬编码

  • 错误的习惯
常量:设置固定常量,导致维护困难。 全局变量:过度使用全局变量,导致程序的状态难以跟踪。

5.1 常量

在编程中,我们经常需要使用一些常量,如数字、字符串等。然而,直接在代码中硬编码这些常量是一个不好的习惯,因为它们可能会在未来发生变化,导致维护困难。例如:

def calculate_score(score):
if (score > 60) {
// do something}

这里的60就是一个硬编码的常量,导致后续维护困难,正确的做法应该使用常量或者枚举来表示。例如:

PASS_SCORE = 60;
def calculate_score(score):
if (score > PASS_SCORE) {
// do something }

这样,我们就可以避免硬编码,提高代码的可维护性。

5.2 全局变量

过度使用全局变量在全局范围内都可以访问和修改。因此,过度使用全局变量可能会导致程序的状态难以跟踪,增加了程序出错的可能性。例如:

counter = 0
def increment():
global counter
counter += 1

这段代码的问题在于,它使用了全局变量 counter,使得程序的状态难以跟踪。我们应该尽量减少全局变量的使用,而是使用函数参数和返回值来传递数据。例如:

def increment(counter):
return counter + 1

这段代码没有使用全局变量,而是使用函数参数和返回值来传递数据,使得程序的状态更易于跟踪。

06、测试和调试

  • 错误的习惯
单元测试:不进行单元测试会导致无法及时发现和修复代码中的错误,增加代码的不稳定性和可维护性。 边界测试:不进行边界测试可能导致代码在边界情况下出现错误或异常。 代码的可测试性:有些情况依赖于当前条件,使测试变得很难。

6.1 单元测试

单元测试是验证代码中最小可测试单元的方法,下面是不添加单元测试的案例:

def add_number(a, b):
return a + b

在这个示例中,我们没有进行单元测试来验证函数 add_number 的正确性。正确示例:

import unittest

def add_number(a, b):
return a + b

class TestAdd(unittest.TestCase):
def add_number(self):
self.assertEqual(add(2, 3), 5)

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行单元测试,确保函数 add 的正确性。

6.2 边界测试

边界测试是针对输入的边界条件进行测试,以验证代码在边界情况下的行为下面是错误示例:

def is_even(n):
return n % 2 == 0

在这个示例中,我们没有进行边界测试来验证函数 is_even 在边界情况下的行为。正确示例:

import unittest

def is_even(n):
return n % 2 == 0

class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(3))

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行边界测试,验证函数 is_even 在边界情况下的行为。

6.3 可测试性

代码的可测试性我们需要编写测试来验证代码的正确性。如果我们忽视了代码的可测试性,那么编写测试将会变得困难,甚至无法编写测试。例如:

def get_current_time():
return datetime.datetime.now()

这段代码的问题在于,它依赖于当前的时间,这使得我们无法编写确定性的测试。我们应该尽量减少代码的依赖,使得代码更易于测试。例如:

def get_time(now):
return now

这段代码不再依赖于当前的时间,而是通过参数传入时间,这使得我们可以编写确定性的测试。

07、性能优化

  • 错误的习惯
过度优化:过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。 合适的数据结构:选择合适的数据结构可以提高代码的性能。

7.1 过度优化

我们往往会试图优化代码,使其运行得更快。然而,过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。例如:

def sum(numbers):
return functools.reduce(operator.add, numbers)

这段代码的问题在于,它使用了 functools.reduce 和 operator.add 来计算列表的和,虽然这样做可以提高一点点性能,但是这使得代码难以理解。我们应该在保持代码清晰和易读的前提下,进行适度的优化。例如:

def sum(numbers):
return sum(numbers)

这段代码使用了内置的 sum 函数来计算列表的和,虽然它可能比上面的代码慢一点,但是它更清晰、易读。

7.2 没有使用合适的数据结构

选择合适的数据结构可以提高代码的性能。使用不合适的数据结构可能导致代码执行缓慢或占用过多的内存。例如:

def find_duplicate(numbers):
duplicates = []
for i in range(len(numbers)):
if numbers[i] in numbers[i+1:]:
duplicates.append(numbers[i])
return duplicates

在这个示例中,我们使用了列表来查找重复元素,但这种方法的时间复杂度较高。我们可以使用集合来查找元素。例如:

def find_duplicate(numbers):
duplicates = set()
seen = set()
for num in numbers:
if num in seen:
duplicates.add(num)
else:
seen.add(num)
return list(duplicates)

我们使用了集合来查找重复元素,这种方法的时间复杂度较低。

08、代码安全性

  • 错误的习惯
输入验证:不正确的输入验证可能导致安全漏洞,如 SQL 注入、跨站脚本攻击等。 密码存储:不正确的密码存储可能导致用户密码泄露。 权限控制:不正确的权限控制可能导致未经授权的用户访问敏感信息或执行特权操作。

8.1 输入验证

没有对用户输入进行充分验证和过滤可能导致恶意用户执行恶意代码或获取敏感信息。例如:

import sqlite3
def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们没有对用户输入的 username 参数进行验证和过滤,可能导致 SQL 注入攻击。正确示例:

import sqlite3

def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们使用参数化查询来过滤用户输入,避免了 SQL 注入攻击。

8.2 不正确的密码存储

将明文密码存储在数据库或文件中,或使用不安全的哈希算法存储密码都是不安全的做法。错误示例:

import hashlib

def store_password(password):
hashed_password = hashlib.md5(password.encode()).hexdigest()
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了不安全的哈希算法 MD5 来存储密码。正确示例:

import hashlib
import bcrypt

def store_password(password):
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了更安全的哈希算法 bcrypt 来存储密码。

8.3 不正确的权限控制

没有正确验证用户的身份和权限可能导致安全漏洞。错误示例:

def delete_user(user_id):
if current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们只检查了当前用户是否为管理员,但没有进行足够的身份验证和权限验证。正确示例:

def delete_user(user_id):
if current_user.is_authenticated and current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们不仅检查了当前用户是否为管理员,还检查了当前用户是否已经通过身份验证。

09、版本控制和协作

  • 错误的习惯
版本提交信息:不合理的版本提交信息会造成开发人员难以理解和追踪代码的变化。 忽略版本控制和备份:没有备份代码和版本控制的文件可能导致丢失代码、难以追溯错误来源和无法回滚等问题。

9.1 版本提交信息

不合理的版本提交信息可能导致代码丢失、开发人员难以理解等问题。错误示例:

git commit -m "Fixed a bug"

在这个例子中,提交信息没有提供足够的上下文和详细信息,导致其他开发人员难以理解和追踪代码的变化。正确的做法是提供有意义的提交信息,例如:

$ git commit -m "Fixed a bug in calculate function, which caused grade calculation for scores below 60"

通过提供有意义的提交信息,我们可以更好地追踪代码的变化,帮助其他开发人员理解和维护代码。

9.2 忽略版本控制和备份

忽略使用版本控制工具进行代码管理和备份是一个常见的错误。错误示例:

$ mv important_code.py important_code_backup.py
$ rm important_code.py

在这个示例中,开发者没有使用版本控制工具,只是简单地对文件进行重命名和删除,没有进行适当的备份和记录。正确示例:

$ git clone project.git
$ cp important_code.py important_code_backup.py
$ git add .
$ git commit -m "Created backup of important code"
$ git push origin master
$ rm important_code.py

在这个示例中,开发者使用了版本控制工具进行代码管理,并在删除之前创建了备份,确保了代码的安全性和可追溯性。

10、总结

好的代码应该如同一首好文,让人爱不释手。优雅的代码,不仅是功能完善,更要做好每一个细节。

最后,引用韩磊老师在《代码整洁之道》写到的一句话送给大家:

细节之中自有天地,整洁成就卓越代码。

以上是本文全部内容,欢迎分享。

-End-


作者:腾讯云开发者
链接:https://juejin.cn/post/7257894053902565433
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

初学矩阵

web
前言 矩阵是人类的瑰宝,矩阵里数字与数字通过关系组在一起。正如大道无形,用不同的视角去解读数的关系,它就有不同的作用。大道至简,难的是解读道的心。(作者发癫中...) 让我们放开的自己的心,不要限制它的解读,(san +++) 下面进行简单的描述。 矩阵 (M...
继续阅读 »

前言


矩阵是人类的瑰宝,矩阵里数字与数字通过关系组在一起。正如大道无形,用不同的视角去解读数的关系,它就有不同的作用。大道至简,难的是解读道的心。(作者发癫中...)


让我们放开的自己的心,不要限制它的解读,(san +++)


下面进行简单的描述。


矩阵 (Matrix)


定义


矩阵由 m 行 n 列 组成的方队,即为 m * n 的矩阵。 其组成的元素可以为实数,虚数。


好了开写。


我这边定义一个枚举类型,因为我想通过矩阵计算对象某个属性。
但是实现实在是有点生草。最开始的时候准备封装个矩阵类,然后通过它进行计算。
但是现实中我那微不足道的 OOP 水平撑不下去了,在矩阵的灵活扩展想法败北了┭┮﹏┭┮。
最后是个四不像的实现。


// 定义矩阵类型
type IMatrix<T = number> = T[][];

class Matrix {
// 获取矩阵常用的坐标集合操作
static getRow<T = number>(matrix: IMatrix<T>, rowIndex: number) {
return matrix?.[rowIndex]
};
static getCol<T = number>(matrix: IMatrix<T>, colIndex: number) {
return matrix.map(row => row[colIndex])
};
static getMatrixLen<T = number>(matrix: IMatrix<T>) {
return {
rowLen: matrix.length,
colLen: Math.max(...matrix.map(row => row.length)),
}
};
}

上面就是获取矩阵常用简化。


这里关于使用类静态方法的考虑是因为我觉得相对比较直观,虽然现在有 esm 现代模块化方案,可以基于文件即模块,但是考虑到更直观的抽象关系,我选择这种方式。第一调用的时候,不会 esm import 那样少了直观的从属关系,esm 的文件即模块确实很方便,但引用代码如果没有插件的话,需要追踪对应的文件模块的话,只能从函数名语义入手。


在项目中有时候会碰到大杂烩语义的文件,比如一个 util文件 会承当各种逻辑封装而失去模块的意义,用类作为一个抽象空间是一种相对方法。


同型矩阵/单位矩阵


同型矩阵就是矩阵之间的行数列数均相同,则视矩阵之间的关系为同型矩阵。符合同型矩阵是一些计算逻辑的前置判断。


单位矩阵是矩阵主对角线之间的数为 1 ,其余为 0 。其实就是行列坐标相同的点就是 1 ,其余就是 0。


interface IGetMatrixValue<T = number> {
(matrixItem: T) : number;
}
//...
static isHomotypeMatrix<T = number>(matrix1: IMatrix<T>, matrix2: IMatrix<T>): boolean {
if (matrix1.length !== matrix2.length) {
return false;
}
if (this.getMatrixLen(matrix1) !== this.getMatrixLen(matrix2)) {
return false;
}

return true;
};

static isUnitMatrix<T = number>(matrix: IMatrix<T>, getMatrixVal?: IGetMatrixValue<T>) {
const handleGetMatrixValue = getMatrixVal || getDefaultMatrixItem;

for (let i = 0; i < matrix.length; i++) {
const row = matrix[i];
for (let j = 0; j < row.length; j++) {
const isSameIdx = i === j;
const val = handleGetMatrixValue(matrix[i][j] as any);

if (isSameIdx && val !== 1) {
return false;
}

if (!isSameIdx && val !== 0) {
return false;
}
}
}

return true;
};


当时写到这里,我感觉脑子乱乱的,可能是经常熬夜吧。
第二个获取单元行数,是不是有点不一样的。其实从这里,我才意识这里正因为我的定义函数因为它太灵活,导致我这边要处理更多的边际逻辑。(当时大脑宕机中...😐)


我希望我的代码可以使用对象运算,但是如何标准的写出可扩展的函数,或许是我要去学习的。 Lodash 源码获取是个不错的选择,但是总有一些事情,让我没有机会。


同型矩阵加/减


同型矩阵同个行列位置的进行加减运算,所以我写道一般,还是抽了一个计算同个位置逻辑的函数,并把其它情况交给调用者自己扩展运算吧。


interface ICustMatrixAWithB<T> {
(matrixAItem: T, matrixBItem: T) : T;
}

static computeHomotypeMatrix<T = number>(matrix1: IMatrix<T>, matrix2: IMatrix<T>, custom: ICustMatrixAWithB<T>): IMatrix<T> {
if (!this.isHomotypeMatrix(matrix1, matrix2)) {
throw new Error('该矩阵非二维数组');
}
const { rowLen, colLen } = this.getMatrixLen(matrix1)

const nextMatrix: T[][] = [];

for (let i = 0; i < rowLen; i++) {
nextMatrix[i] = [];
for (let j = 0; j < colLen; j++) {
nextMatrix[i][j] = custom(matrix1[i]?.[j], matrix2[i]?.[j]);
}
}
return nextMatrix;
};

static addHomotypeMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
return this.computeHomotypeMatrix(matrix1, matrix2, (num1, num2) => num1 + num2);
};

static subHomotypeMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
return this.computeHomotypeMatrix(matrix1, matrix2, (num1, num2) => num1 - num2);
};

写到这里也说我最纠结的,因为我的封装设计出现了问题,最后一层胶水层没法解决,只能交由调用者使用 computeHomotypeMatrix 去实现自己的加减逻辑。


但我不知道要如何去解决,如果你有办法请留言教教我吧。


这里的逻辑,也让我想起了若川大佬的 vant 组件源码共读中的计算逻辑。只是想不起具体细节,时间真的是改变一切,生物的宿命真是难以跨域。


矩阵乘法 矩阵相乘/矩阵标量


矩阵和标量的乘积,标量指的是一个数,数和矩阵相乘等于数与矩阵的每个元素相乘


矩阵相乘则是比较奇怪,我不太了解原理。是这么一个公式,矩阵1 m * n , 矩阵2 n * p , 当前矩阵的列数等于后矩阵的行数的时候的才可以进行相乘,可以得到这么一个新矩阵 m * p。矩阵的每一项等于 当前行数的前矩阵的列数的项 * 当前的列数的后矩阵的行数的项,两个数组之间的每一元素相乘后累加成项的值


//...
static mapMatralItem<T>(matrix: IMatrix<T>, map: (matrix: T) => T) {

const nextMatrix: IMatrix<T> = [];

for (let i = 0; i < matrix.length; i++) {
const row = matrix[i] || [];
nextMatrix[i] = [];

for (let j = 0; j < row.length; j++) {
const item = row[j];
nextMatrix[i][j] = map(item);
}
}

return nextMatrix;
};
// 数乘
static multipleItemMatrix(matrix: IMatrix<number>, multiple: number) {
return this.mapMatralItem(matrix, (item) => item * multiple);
};
// 矩阵相乘
static multiplyMatrix(matrix1: IMatrix<number>, matrix2: IMatrix<number>) {
const { colLen, rowLen: nexRowLen } = this.getMatrixLen(matrix1);
const { rowLen, colLen: nexColLen } = this.getMatrixLen(matrix2);

if (colLen !== rowLen) {
/** A m * n * B n * p = C m *p */
throw new Error('矩阵乘法必须,前矩阵列数等于后矩阵行数');
}

const nextMatrix: IMatrix = [];

for (let i = 0; i < nexRowLen; i++) {
nextMatrix[i] = [];
const curCol = this.getCol(matrix1, i);
for (let j = 0; j < nexColLen; j++) {
const curRow = this.getRow(matrix2, j);
const len = Math.max(curCol.length, curRow.length);
const computed = Array.from({
length: len,
}, (_, idx) => {
const curColVal = curCol[idx] || 0;
const curRowVal = curRow[idx] || 0;
return curColVal * curRowVal;
});

nextMatrix[i][j] = computed.reduce((acc, cur) => acc + cur, 0);
}
}

return nextMatrix;
};

mapMatralItem 函数封装,它不提供具体的逻辑,只是提供矩阵每个元素的类似数组 map 的能力,但也没有比数组好,因为它再设计的时候少了更多的环境参数。


矩阵转秩


矩阵转秩代表矩阵的行列坐标相互交换,前面有提到的对角线坐标在转秩后仍然还在同一个位置,其它都是 0 交换后也变,所以也可以得出单位矩阵的秩等于单位的结论。


// 矩阵转秩
static randConversionMatrix<T>(matrix: IMatrix<T>) {
const { rowLen, colLen } = this.getMatrixLen(matrix);
const nextMatrix:IMatrix<T> = [];
for (let i = 0; i < colLen; i++) {
nextMatrix[i] = [];
for (let j = 0; j < rowLen; j++) {
nextMatrix[i][j] = matrix[j][i];
}
};

return nextMatrix;
};

矩阵 共轭


这些不懂,暂时跳过,当初不好好学习,上个好的学校 ┭┮﹏┭┮。


矩阵快速幂运算


快速幂是什么,其实就是减少指数,转为同等的底数,来减少计算次数。
看了好久才懂,太菜了。
比方说,一个 2 ** 8 ,我们通过二分指数的方式把底数扩大 (2 ** 2 ) ** 4 -> (4 ** 2) * 2 最后就变小,指数越大效率越高


/**
* 快速幂运算
* @param a 底数
* @param pow 阶乘
*/

const multiQuick = (a: number, pow: number) => {
let curPow = pow, result = 1;

while(curPow) {
if (curPow === 0) {
result *= 1;
} else if (curPow === 1) {
result *= a;
curPow = 0;
} else if (curPow % 2 === 1) {
result *= result * a;
curPow -= 1;
} else if (curPow % 2 === 0) {
result *= result;
curPow >>= 1;
}
};

return result;
};

static multilpyQuiickMatrix(matrix: IMatrix<number>, pow: number) {
return this.mapMatralItem(matrix, (num) => multiQuick(num, pow))
};

这里有小知识点二进制也算复习了,很多东西学了忘,学了忘,只有真正意识到它的价值,才能接纳它。


这里也可以看得出来,虽然我封装得 map 函数不够好,但是确实确实简化很多过程表述。只是从一个数据向另一个数据迁移。


结语


本次文章记录也到到此为止,感谢人生中让我成长的一切,感谢每一个让我快乐的人。


最近行情真的不好,有时候在想,我除了编程还有技能吗?可惜好像没有发现,我学历低,没有大厂背景,真的失业了可能就很难找到工作。


最近也是高考结束了期间,有一批学子成为了准大学生。记得当年报考,我最后还是说服父母说了报考移动应用开发专业。那时候的梦想真的是想学习编程的来开发游戏,然而现在感觉工作了,那股热情却萎了。


人生且叹且前

作者:孤独之舟
来源:juejin.cn/post/7258191640564334653
行,缘分渐行渐无书。

收起阅读 »

我的又一个神奇的框架——Skins换肤框架

为什么会有换肤的需求 app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。 换肤是什么 换...
继续阅读 »

为什么会有换肤的需求


app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。


换肤是什么


换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。


Skins怎么使用


Skins就是一个解决这样一种换肤需求的框架。


// 添加以下代码到项目根目录下的build.gradle
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
// 添加以下代码到app模块的build.gradle
dependencies {
// skins依赖了dora框架,所以你也要implementation dora
implementation("com.github.dora4:dora:1.1.12")
implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。


<!-- 需要换肤的颜色 -->
<color name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>

将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。


private fun applySkin() {
val manager = PreferencesManager(this)
when (manager.getSkinType()) {
0 -> {
}
1 -> {
SkinManager.changeSkin("cyan")
}
2 -> {
SkinManager.changeSkin("orange")
}
3 -> {
SkinManager.changeSkin("black")
}
4 -> {
SkinManager.changeSkin("green")
}
5 -> {
SkinManager.changeSkin("red")
}
6 -> {
SkinManager.changeSkin("blue")
}
7 -> {
SkinManager.changeSkin("purple")
}
}
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。


val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")

这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。


override fun setImageDrawable(imageView: ImageView, resName: String) {
val drawable = getDrawable(resName) ?: return
imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
val drawable = getDrawable(resName) ?: return
view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
val color = getColor(resName)
view.setBackgroundColor(color)
}

框架原理解析


先看BaseSkinActivity的源码。


package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity<T : ViewDataBinding> : BaseActivity<T>(),
ISkinChangeListener, LayoutInflaterFactory {

private val constructorArgs = arrayOfNulls<Any>(2)

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
if (createViewMethod == null) {
val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
"createView", *createViewSignature)
createViewMethod = methodOnCreateView
}
var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
context, attrs) as View?
if (view == null) {
view = createViewFromTag(context, name, attrs)
}
val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
if (skinAttrList.isEmpty()) {
return view
}
injectSkin(view, skinAttrList)
return view
}

private fun injectSkin(view: View?, skinAttrList: MutableList<SkinAttr>) {
if (skinAttrList.isNotEmpty()) {
var skinViews = SkinManager.getSkinViews(this)
if (skinViews == null) {
skinViews = arrayListOf()
}
skinViews.add(SkinView(view, skinAttrList))
SkinManager.addSkinView(this, skinViews)
if (SkinManager.needChangeSkin()) {
SkinManager.apply(this)
}
}
}

private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
var name = viewName
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
constructorArgs[0] = context
constructorArgs[1] = attrs
if (-1 == name.indexOf('.')) {
// try the android.widget prefix first...
createView(context, name, "android.widget.")
} else {
createView(context, name, null)
}
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
} finally {
// Don't retain references on context.
constructorArgs[0] = null
constructorArgs[1] = null
}
}

@Throws(InflateException::class)
private fun createView(context: Context, name: String, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
val clazz = context.classLoader.loadClass(
if (prefix != null) prefix + name else name).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap[name] = constructor
}
constructor!!.isAccessible = true
constructor.newInstance(*constructorArgs)
} catch (e: Exception) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
LayoutInflaterCompat.setFactory(layoutInflater, this)
super.onCreate(savedInstanceState)
SkinManager.addListener(this)
}

override fun onDestroy() {
super.onDestroy()
SkinManager.removeListener(this)
}

override fun onSkinChanged(suffix: String) {
SkinManager.apply(this)
}

companion object {
val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
private val constructorMap: MutableMap<String, Constructor<out View>> = ArrayMap()
private var createViewMethod: Method? = null
val createViewSignature = arrayOf(View::class.java, String::class.java,
Context::class.java, AttributeSet::class.java)
}
}

我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。


package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

override fun attachBaseContext(base: Context) {
}

override fun onCreate(application: Application) {
SkinManager.init(application)
}

override fun onTerminate(application: Application) {
}
}

所以你无需手动配置<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。


    /**
* 从xml的属性集合中获取皮肤相关的属性。
*/

fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
val skinAttrs: MutableList<SkinAttr> = ArrayList()
var skinAttr: SkinAttr
for (i in 0 until attrs.attributeCount) {
val attrName = attrs.getAttributeName(i)
val attrValue = attrs.getAttributeValue(i)
val attrType = getSupportAttrType(attrName) ?: continue
if (attrValue.startsWith("@")) {
val ref = attrValue.substring(1)
if (TextUtils.isEqualTo(ref, "null")) {
// 跳过@null
continue
}
val id = ref.toInt()
// 获取资源id的实体名称
val entryName = context.resources.getResourceEntryName(id)
if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
skinAttr = SkinAttr(attrType, entryName)
skinAttrs.add(skinAttr)
}
}
}
return skinAttrs
}

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。


package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

/**
* 背景属性。
*/

BACKGROUND("background") {
override fun apply(view: View, resName: String) {
val drawable = loader.getDrawable(resName)
if (drawable != null) {
view.setBackgroundDrawable(drawable)
} else {
val color = loader.getColor(resName)
view.setBackgroundColor(color)
}
}
},

/**
* 字体颜色。
*/

TEXT_COLOR("textColor") {
override fun apply(view: View, resName: String) {
val colorStateList = loader.getColorStateList(resName) ?: return
(view as TextView).setTextColor(colorStateList)
}
},

/**
* 图片资源。
*/

SRC("src") {
override fun apply(view: View, resName: String) {
if (view is ImageView) {
val drawable = loader.getDrawable(resName) ?: return
view.setImageDrawable(drawable)
}
}
};

abstract fun apply(view: View, resName: String)

/**
* 获取资源管理器。
*/

val loader: SkinLoader
get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。


开源项目传送门


如果你要深入理解完整的换肤流程,请阅读skins的源代码,[github.com/dora4/dvi

ew…] 。

收起阅读 »

年中总结、再品苏轼、怀古望今、胡思漫谈

-- 迟到年终总结/虽迟但到 -- 现在是公元2023年7月22日,不知觉,2023年已过一半有余。 在整 1000 年前,1023 年,北宋宋仁宗开启“天圣”元年,仁宗在位四十二年,搜揽天下豪杰,不可胜数,其中就包括文坛 T1 阵容 —— 苏轼。 仁宗可以说...
继续阅读 »

-- 迟到年终总结/虽迟但到 --


现在是公元2023年7月22日,不知觉,2023年已过一半有余。


在整 1000 年前,1023 年,北宋宋仁宗开启“天圣”元年,仁宗在位四十二年,搜揽天下豪杰,不可胜数,其中就包括文坛 T1 阵容 —— 苏轼。


仁宗可以说是苏轼最大的伯乐,认苏轼有宰相之才,为他的诗词/才华直连拍手叫好。


而仁宗之后,苏轼便逐渐踏上了他的一生被贬之路。


怀古


step1


苏轼先是和王安石政见不一,自请至杭州做通判,苏堤春晓、三潭印月,欲把西湖比西子,淡妆浓抹总相宜。


现实中与人气场不合、难以互存,而又不得不共事的时候,确实是一件不幸之事。


王安石改革激进、求快,苏轼体恤民情,当宋神宗选择王安石的时候,苏轼就知道了:道不同不相谋。


从杭州再到湖州,远离庙堂、过足山水之瘾,有道是:“待君诗百首,来写浙西春”。


step2


苏杭山水之后,再是乌台诗案,被文字狱、坐牢 100 多天,被贬黄州;


在黄州的四年多时间,从“寂寞沙洲冷”转变为“一蓑烟雨任平生”,他天生是乐观的,能够快速的整理、修复情绪;


最后,黄州成就了苏轼,写出了千古流传的《赤壁赋》,“天地之间,物各有主”,不是我等能占有的,假若想要有无限的时光、享用无尽的自然美景,只得是把自己交给自然,“耳得之而为声,目遇之而成色,取之无禁,用之不竭”。


我认为,这种豁达,可以解忧,现代人的忙碌、焦虑、急切心态等。


事物兴衰、时间流转,不是我等能掌控的,占有欲再强,最后也是赤条条来去无牵挂。


不如就是“适之”,你我“共适”当下,便就是永久的享受了。


step3


虽然,不多久,苏轼又被高太后启用,但随着这位迷妹去世,哲宗启用新派、打击旧派,苏轼又被贬到惠州;“日啖荔枝三百颗,不辞长作岭南人”,即使离皇帝/权利/显贵越来越远,但并不妨碍他这样快活的心态;


step4


一贬再贬,晚年苏轼被贬到海南儋州,可谓是:天涯海角。有一首《西江月》:



世事一场大梦,人生几度秋凉?夜来风叶已鸣廊。看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光。把盏凄然北望。



有人分析说这是在儋州所作,有人分析说在黄州所作;


个人感觉,前者说法可能性更大,人生之短促,壮志之难酬,确实悲凉,难有青壮年的狂放、通达。


望今


p1


现在年轻人也是很艰难的,虽绝大部分人都没有苏轼这样有才华的诗词表达,但对于这种人生变迁的体味肯定是闷在心头,千滋百味、无法言说的。


时间转眼就没,就像李白所说:朝如青丝、暮成雪;


没有什么是永恒的,可能在一家公司日复一日、勤勤恳恳工作两、三年,转眼间,因为某一天来了一个擅长 PUA 的小领导,愤懑之下就裸辞;或者某一天,突然就不想起早八了,不想麻木/盲目了,毅然离开,像是重启、代谢更新,又像是一种惋惜,恨不得志;


当然没时间啦。


早八到晚八,或者早十到晚十,都一样,最普通的,基本工作时间8小时,还要提前准备、通勤;晚归后还要休息调整思路;就算是一点不加班,工作也要消耗掉一天二分之一的时间。正常的,还要有睡眠时间,7个、8个小时左右,剩下的,也就3、4小时;再除去内务整理、社交交友、家人长谈,还有几个属于自己的时间,可以用来思考:文学、艺术、哲学、宗教、政治、科学等等?


只要是随着这个时间流去走,不去挣扎的话,真的时间一晃而过。就像有人所说的,普通人能忙于物质生活都已经足够累了,还有几个能在此之外,建设/丰富精神世界?


这是给自己找理由吗?时间就像海绵里的水,挤一挤还是有的?怎么挤?不是人人都能凿壁借光、或闻鸡起舞,不然这故事也没什么好值得人敬佩的了。


有一说是:中国人把吃做到了极致,而欧洲人把闲做到了极致。


我辈当负大任,这也是时代的洪流所决定,不是个人的决定;长达三年的疫情的洪流,其实也就在今年才结束,相信还有许多人被影响、没走出来,但没谁会在乎,时间就是如水、如刀,不谈感情,继续往前,即使青丝变成白发。


2023 年,不一样的是什么,听到了许多,比如常谈的公众号已死、前端已死、B站收益已死、xxx已死,whatever,几乎没人会收集自己表皮更新而产生的死皮吧,落在满地,灰尘皆是,该怎样,还是怎样。


还是想到 2018 我说的一句话,每当年中,每当盛夏:“常言道:韶光易逝、寸暑难留、然其虚无、何以擒之”。夏天很好,夏夜更好,但没人能无穷尽的享用。或许,也只有想到,苏轼所说:共适此时,才得宝藏。


故:怎样都好。


p2


具体一点,2023 年上半年有什么不一样,自己敲定了人生大事之一,当然无非几件其一:出生、上学、考学升学、大学、就业、结婚、生子、循环往复,外乎还有买房、买车等等,就像大你十岁的人,聊天时,一定会问类似这些大事上的问题。


其次,工作转型,之前是前端,现在是项目经理;写文方向转型,之前写具体的前端技术文章,现在写 AIGC 的专栏;偶尔,用 AIGC 发一发想要翻译的潮流前线技术文章,但囿于自己时间、囿于自己执行力、囿于其它事情的权重取舍,所以现状就是这么个现状。


另外:发了很多知乎、但有起伏,马上7级;做了AIGC抖音号,获赞1k+;还有在同花顺论股,发声就是在生产观点、观点即内容,有冲突,也有价值,也会带来认知变化,以及有可能生产产生财富。


还有,改书一事,来回修订好几次,有种有心无力的感觉,给到的压力不小,但是怀疑自己究竟能否COVER;工作上事情很多,KPI 的压力也是直接到项目,薪酬结构调整、增量激励等等,没有人会在之前问你:你准备好了吗?你对这件事怎么看?要不等等你?


能留一个下午,去回看这些事,都是一种“偷窃”之举,得之所幸。


p3


再到,重点看看“起伏”这个事。红楼梦的经典在于此,“训有方、保不定日后作强梁;择膏梁,谁承望流落烟花巷”;


苏轼的几次被召、几次被贬也在于此,哀吾生之须臾、羡长江之无穷;


长安三万里高适、李白也如此,十年寒窗、十年还乡、十年扬州、十年边塞、十年朝堂、十年流亡;人生又几个十年,少小十年、老弱十年、睡梦十年又十年、乱哄哄,你方唱罢我登场。


说回股市,上半年关注很多,我是在想:财富的本质在于生产力,现在觉得也在于流动性;打工人是无产者,唯一能有产的就是,把微薄的薪水几成放到股市,同市场共振,让钱成为一种资产,成为一种员工,为自己所用。所以投资是开公司,我们打工,被当做资产来估价、人是一种资产、钱更应该是一种资产,怎样选对方向,是一直需要去专研的,虽然看不太明白,股市是最复杂的混沌系统,不能预测,但也只能尽全力在无序中找有序。


找规律、模仿、是我们一直在做的,将信息整合、同步各方,也是我们一直在做的,殊途同归、并无新鲜。


p4


再看以后,无复多言。做好工作,做好精神建设,做好“负熵”。


1、坚持输出、生产内容,输出倒逼输入,生产带来财富、流动带来财富;


2、做好身体锻炼,25 岁以后,身体代谢下降,真的是一个客观现实;如果每天手表的三个圆环都难合拢,身体只会走下坡路吧;


3、正能量/乐观,待人待事,在随波逐流和坚守原则之间权衡、适之。


最后用苏轼最经典之一的词总结:“回首向来萧瑟处,归去,也无风雨”。


所以,2023 年中,I`m fine,Thanks all,And you

作者:掘金安东尼
来源:juejin.cn/post/7258445326914076733
?

收起阅读 »

放假了,讲个真实的转岗故事

大家好啊,我是董董灿。 难得周中放了一天假,过了明天后天,又要过礼拜了,这周是真的幸福。 前天给自己立了个实战项目——《从零手写Resnet50》,这两天抽时间把Resnet50的模型,利用 torchvision 下载下来,并且写了个简单地 python 脚...
继续阅读 »

大家好啊,我是董董灿。


难得周中放了一天假,过了明天后天,又要过礼拜了,这周是真的幸福。


前天给自己立了个实战项目——《从零手写Resnet50》,这两天抽时间把Resnet50的模型,利用 torchvision 下载下来,并且写了个简单地 python 脚本,将参数保存成 txt 文件了。


方便后面验证算法进行整网推理。



最近考研结束了,有一些拟录取了新研一小朋友,私信我,希望给一些选专业方面的建议。


很多小伙伴是跨专业考研,对未来甚是迷茫。


这种情况,我有一个观点就是——


研一最宝贵的就是有大量的时间,现在喜欢做什么方向,就大胆去学、去做,在毕业前几年的时间,都可以不断试错,不断寻找最适合自己的职业道路。


 

今天不写技术了,讲一个真实的攻城狮转岗的故事,以下是正文——


回想硕士毕业,离开北京的那天早上,是阴天。


起了个大早,舍友们还在睡觉,我拎起已经收拾好的行李,慢慢的打开门,慢慢的关上门,从工大的宿舍走到工大西门坐地铁去南站,一路上,寥寥无几的行人。


从实验室到宿舍的路,走了3年,边上的校园,待了7年,离开前看了最后一眼,有点不舍。


人生处处是十字路口:北京上学,去青岛工作,是我毕业之后做的第一个抉择


机械专业的我,为了找到一份看起来还不错的工作,毅然决然地选择了换专业就业。


现在看来,感谢当初自己的选择,让自己做着自己喜欢的事——程序的世界是最简单的,它会按照你写的代码的自然法则来运行,一旦世界出了bug,你可以化身上帝,大手一挥,bug解除,世界归于平静。
没错,研究生毕业,我从机械跨专业去做了程序开发。


在青岛工作了一年,发现青岛的环境离我的预期相差甚远,机会少发展有限,熟悉北京节奏的我,又一次决然从青岛辞职,回北京闯荡。


和当初离开北京去青岛一样,没有一点犹豫,因为年轻,因为觉得,前方是一片大海,那里会有属于我的一个小岛。


于是刷题投简历面试,很幸运的是,进入了一家不错的公司,在那里,得到了长足的成长。


从初级攻城狮,到中高级攻城狮,从小白,到面试官,从只会做事写代码,到做方案讨论可行性,可以说,我经历了所有攻城狮都会经历的成长路径。


期间不断地学习,搞嵌入式,看电路图,学C++,学算法,学人工智能,搞AI芯片,一路走来,收获满满。我喜欢周星驰的《功夫》电影。电影中的星爷是一个万中无一的练武奇才,而我仅仅是平平无奇的一个开发者。


一个攻城狮,在无数个白天黑夜,游走于属于自己的小岛上,沉浸在代码的世界,一边开疆扩土(新特性开发),一边修路(优化)铺桥(bug修复),一边练着属于自己的如来神掌。


几年下来,让我变得和离开北京的那个清晨截然不同。


工作教会了我很多,除了技术,还收获了友谊,学到了职场的处事技巧。


现在的我,很庆幸,当初的两次选择,北京到青岛,青岛到北京——兜兜转转,逐渐看清了自己。


如果说选择比努力更重要,那么我觉得,只有选择了愿意为之努力的方向,选择才会变得重要。


现在的我,在一家创业公司工作,一边做着自己喜欢的事情,一边整理着自己几年来的收获,一边关注着行业的发展,一边重温星爷的电影。


有很多个瞬间,感觉自己又回到了研究生的宿舍:右边坐着龙哥,鼠标操作着盖伦,大喊着德玛西亚万岁;左边坐着畅哥,因为之前一直玩游戏,正在为明天的作业准备熬夜。


那时的我们,都做着自己喜欢的事情,对未来充满着憧憬。而现在的我们,也依然做着自己喜欢的事情,对未来充满的希望,相信舍友肯定也是这样。


兜兜转转,北京到青岛,青岛到北京,画了一个圈,从现在开始,坚持学习,坚持锻炼,坚持做自己喜欢的事。


"远行是为憧憬,归来仍是少年。"

作者:董董灿是个攻城狮
来源:juejin.cn/post/7218554163050758201

收起阅读 »

希尔排序,我真的领悟了

web
之前文章我们讲到过 冒泡排序、选择排序、插入排序 都是原地的,并且时间复杂度都为O(n^2) 的排序算法。那么今天我们来讲一下希尔排序,它的时间复杂度为O(n*logn)。那这个算法是怎么做到的呢?我们这回一次看个透。 首先再回顾一下 冒泡、选择、插入这3个排...
继续阅读 »

之前文章我们讲到过 冒泡排序选择排序插入排序 都是原地的,并且时间复杂度都为O(n^2) 的排序算法。那么今天我们来讲一下希尔排序,它的时间复杂度为O(n*logn)。那这个算法是怎么做到的呢?我们这回一次看个透。


首先再回顾一下 冒泡选择插入这3个排序。这三个排序都有一个共同的特点,就是每次比较都会得到当前的最大值或者最小值。有人会说这是屁话,但你细品,为什么很多人都会去刻意背10大排序算法,本质就是因为自己的思想被困住了(什么每轮比较得出最大值的就是冒泡,得出最小值的就是选择等等),假设你从没有接触过排序算法,我还真不相信你不会排序,最差的情况就是做不出原地呗,时间复杂度最差也是N^2,就像下面这样:


let data = [30, 20, 55, 10, 90];

for (let index = 0; index < data.length; index++){
for (let y = 0; y < data.length - index; y++){
if(data[y] > data[y+1]){
[ data[y], data[y+1] ] = [ data[y+1], data[y] ];
}
}
}


data的长度是5,就循环5次,每轮比较中都要得出当前轮次的最大值。那么在每一轮中,如何得出最大值呢?那就再来一次遍历。


上述思想我们会发现,它在时间复杂度上是突破不了 O(n^2) 的限制的。原因在于你是两两比较(一次只能在两个数中得到最大值,一次只能给两个数排序)


如何突破限制呢?那就一次比较多个,就像下面这样:


let data = [30, 20, 55, 10, 90];

// 1、我们对data数组进行拆分,拆分规则:步长为2的数据放到一个集合里。
// 2、根据上面的拆分规则,我们可以将data数组拆成2个子数组。分别是:[30, 55, 90]、[20, 10]
// 3、分别对这2个子数组进行排序,排序后的子数组分别是:[30, 55, 90]、[10, 20]
// 4、将上面的子数组合并为一个新的数组data1:[30, 10, 55, 20, 90]
// 5、修改拆分规则,对修改后的data1数组进行拆分,步长为1。
// 6、因为步长为1,所以相当于对data1数组进行整体排序。

那么如何用代码表示呢?请继续阅读。


第一步、确定步长


这里我们以不断均分,直到均分结果大于0为原则来确定步长:


let data = [30, 20, 55, 10, 90];

// 维护步长集合
let gapArr = [];

let temp = Math.floor(data / 2);

while(temp > 0){
gap.push(temp);
temp = Math.floor(temp / 2);
}


第二步、得到间隔相等的元素


这一步其实本质上就是将间隔相等的元素放在一起进行比较


这意味着我们不用分割数组,只要保证原地对间隔相同的元素进行排序即可。


let data = [30, 20, 55, 10, 90];

let gapArr = [2, 1];

for (let gapIndex = 0; gapIndex < gapArr.length; gapIndex++){
// 当前步长
let curGap = gapArr[gapIndex];
// 从当前索引项为步长的地方开始进行比较
for(let index = curGap; index < data.length; index++){
let curValue = data[index]; // 当前的值
let prevIndex = index - curGap; // 间隔为gap的前一项索引
while(prevIndex >= 0 && curValue < data[prevIndex]){
// 这里面的while就代表着gap相等的数据项之间的比较...
prevIndex = prevIndex - curGap;
}
}
}

第二层的for循环、最里面的while循环需要我们好好理解一下。


就以上面的数据为例,我们先不考虑数据交换的问题,只考虑上面的写法是如何把gap相等的元素联系到一块的,现在我们来推演一下:




从上图我们看到,第一次while循环因为不满足条件,导致没有被触发。紧接着index++,我们来推演一下这种状态下的数据:



继续index++,此时我们来推演一下这种状态下的数据:




经过我们一轮间隔(gap)的分析,我们发现这种 for + while 的方法能够满足我们对间隔相等的元素进行排序。因为我们通过这种方式可以获取到间隔相等的元素


此时,我们终于可以进入到了最后一步,那就是对间隔相等的元素进行排序


第三步、对间隔相等的元素进行排序


在上一步的基础上,我们来完成相应元素的排序。


// 其它代码都不变......
for(let index = curGap; index < data.length; index++){
let curValue = data[index]; // 当前的值
let prevIndex = index - curGap; // 间隔为gap的前一项索引
while(prevIndex >= 0 && curValue < data[prevIndex]){
// 新增代码 ++++++
data[prevIndex + curGap] = data[prevIndex];
prevIndex = prevIndex - curGap;
}
// 新增代码 ++++++
data[prevIndex + curGap] = curValue;
}

现在我们来对排序的过程进行一下数据推演:


注意,这里我们只演示 curGap === 2 && index === data.length - 1 && data === [30, 20, 55, 10, 9] 的情况。


读到这里大家可能会发现我们突然换了数据源,因为原先的数据源的最后一项正好是最大值,不方便看到数据比较的全貌,所以在这里我们将最后一项改为了最小值。




开启while循环如下:




紧接上图,第二次进入while循环如下:




第二次循环结束后,此时的prevIndex < 0,因为未能进入到第三次的while循环:




至此,我们完成了本轮的数据推演。


在本轮数据推演中,我们会发现它跟之前的两两相比,区别在于它一次可能会比较很多个元素,更具体的说就是,它的一次for循环里,可以比较多个元素对,并将这些元素对进行排序。


第四步、源码展示


function hillSort(arr){
let newData = Array.from(arr);
// 增量序列集合
let incrementSequenceArr = [];
// 数组总长度
let allLength = newData.length;
// 获取增量序列
while(incrementSequenceArr[incrementSequenceArr.length - 1] != 1){
let increTemp = Math.floor(allLength / 2);
incrementSequenceArr.push(increTemp);
allLength = increTemp;
}
for (let gapIndex = 0; gapIndex < incrementSequenceArr.length; gapIndex++){
// 遍历间隔
let gap = incrementSequenceArr[gapIndex]; // 获取当前gap
for (let currentIndex = gap; currentIndex < newData.length; currentIndex++){
let preIndex = currentIndex - gap; // 前一个gap对应的索引
let curValue = newData[currentIndex];
while(preIndex >= 0 && curValue < newData[preIndex]){
newData[preIndex + gap] = newData[preIndex];
preIndex = preIndex - gap;
}
newData[preIndex + gap] = curValue;
}
}
return newData;
}

最后


又到分别的时刻啦,在上述过程中如果有讲的不透彻的地方,欢迎小伙伴里评论留言,希望我说的对你有启发,我们下期再见啦~~

作者:小九九的爸爸
来源:juejin.cn/post/7258180488359018557

收起阅读 »

二维码基本原理

二维码技术始于20世纪80年代末,全球现有250多种二维码,其中常见技术标准有PDF417,QRCode,Code49Code16K,CodeOne等20余种。我们日常扫码以QR码居多。 从1997年到2012年,我国陆续发布了5个二维码国家标准:PDF41...
继续阅读 »


二维码技术始于20世纪80年代末,全球现有250多种二维码,其中常见技术标准有PDF417,QRCode,Code49Code16K,CodeOne等20余种。我们日常扫码以QR码居多。



从1997年到2012年,我国陆续发布了5个二维码国家标准:PDF417,QRCode(快速响应码),汉信码,GM码(网格矩阵码)和CM码(紧密矩阵码)。其中QRCode因为具有识读速度快、信息容量大、占用空间小、保密性强、可靠性高的优势,是目前使用最为广泛的一种二维码。QRCode 呈正方形,只有两种颜色,在4个角落的其中3个,印有像“回”字的的小正方图案。QR码是属于开放式的标准。




二维码的工作原理


二维码内的图案代表二进制代码,经过解释后可显示代码存储的数据。


二维码阅读器根据二维码外侧的三个较大方块来识别标准二维码。当识别出这三个形状后,就知道整个方块内包含的内容是一个二维码。


二维码阅读器随后将整个二维码分解到网格进行分析。它查看每个网格方块,并根据方块是黑色还是白色来为其分配一个值。然后将网格方块组合在一起,创建更大的图案。



二维码由哪些部分组成


①.静态区域 (Quiet zone)


这是二维码外侧的空白边框。如果没有这个边框,二维码阅读器会因为外界因素的干扰而无法确定二维码包含和不包含的内容。


②.寻像图案 (Finder pattern)


二维码在左下角、左上角和右上角包含三个黑色方块。这些方块告诉二维码阅读器它看到的是一个二维码,及二维码的外部边框在哪里。


③.校准图案 (Alignment pattern)


这是二维码右下角附近的某个位置包含的另一个较小方块,用于确保二维码在倾斜或有角度的情况下仍然可以阅读。


④.定位图案 (Timing pattern)


这是一条 L 形线,在寻像图案的三个方块之间。定位图案帮助阅读器识别整个二维码中的各个方块,使损坏的二维码仍有可能被阅读。


⑤.版本信息 (Version information)


这是二维码右上角寻像图案附近的一小块信息区域。它标识了正在阅读的二维码的版本(请参阅“二维码有哪四个版本?”)。


⑥.数据单元 (Data cell)


二维码的其余部分传达实际信息,即所包含的 URL、电话号码或消息。



二维码的特点


①.高密度编码,信息容量大:可容纳1850个大写字母或2710个数字或1108个字节,或500多个汉字,比普通条码信息容量约高几十倍。


②.编码范围广:可以把图片、声音、文字、签字、指纹等可以数字化的信息进行编码,用二维码表示出来;可以表示多种语言文字;可表示图像数据。


③.容错能力强,具有纠错功能:这使得二维条码因穿孔、污损等引起局部损坏时,照样可以正确识读,损毁面积达30%仍可恢复信息。


④.译码可靠性高:它比普通条码译码错误率百万分之二要低得多,误码率不超过千万分之一。


⑤.可引入加密措施:保密性、防伪性好。


⑥.成本低,易制作,持久耐用。



为什么要统一标准


①.如果二维码的数据格式不统一、印制精度、符号大小不符合要求,就容易导致信息乱码、无法识读。
②.如果特定二维码只能在特定客户端上扫描,导致用户扫描反复受挫,用户体验不好。


 
统一的二维码国家标准是解决这方面问题的最佳手段,以实现最佳的兼容性和用户体验。而不兼容国家标准的客户端由于用户体验差,自然被用户抛弃。



常用二维码对比




QR二维码读取


QR码从360°任一方向均可快速读取。其奥秘就在于QR码中的3处定位图案,可以帮助QR码不受背景样式的影响,实现快速稳定的读取。





QR码的基本结构



格式信息:表示改二维码的纠错级别,分为L、M、Q、H;


校正图形:规格确定,校正图形的数量和位置也就确定了;


数据和纠错码字:实际保存的二维码信息,和纠错码字(用于修正二维码损坏带来的错误)


位置探测图形、位置探测图形分隔符、定位图形:用于对二维码的定位,每个QR码位置都是固定存在的,只是大小有所差异;


版本信息:即二维码的规格,QR码符号共有40种规格的矩阵(一般为黑白色),从21x21(版本1),到177x177(版本40),每一版本符号比前一版本 每边增加4个模块。





QR码存储容量


格式容量
数字最多7089字符
字母最多4296字符
二进制数(8 bit)最多2953字节
日文汉字/片假名最多1817字符(采用Shift JIS)
中文汉字最多984字符(采用UTF-8)


QR码纠错能力


即使编码变脏或破损,也可自动恢复数据。这一“纠错能力”具备4个级别,用户可根据使用环境选择相应的级别。调高级别,纠错能力也相应提高,但由于数据量会随之增加,编码尺寸也也会变大。


纠错等级纠错水平
L7%字码修正
M15%字码修正
Q25%字码修正
H

30%字码修正







下图所示:相同内容的二维码,纠错等级不一样,矩阵的密度也不一样,容错率越高,密度越大




不是所有位置都可以缺损,像三个角上的回字方框,直接影响初始定位,不能缺失。中间零散的部  分是内容编码,可以容忍缺损。



计算机的世界都是0和1,二维码再一次说明了这个问题







普通二维码存在的问题


普通二维码只是对文字、网址、电话等信息进行编码,不支持图片、音频、视频等内容,且生成二维码后内容无法改变,在信息内容较多时生成的二维码图案复杂,不容易识别和打印,正是由于存在这些特性故称之为静态二维码。静态二维码的好处就是无需联网也能识别,但是有些时候在线下场景经常需要打印二维码出来让用户去扫码,或者在一些运营场景下需要对用户的扫码情况进行数据统计和分析,再使用普通的二维码就无法提供这些功能了,这时候就要使用动态二维码了





动态二维码(活码)及其原理


动态二维码也称之为活码,内容可变但是二维码不变。支持随时修改二维码的内容且二维码图案不变,可跟踪扫描统计数据,支持存储大量文字、图片、文件、音视、视频等内容,同时生成的图案简单易扫。


实际上二维码是按照指定的规则编码后的一串字符串,通常情况下是一个网址,在二维码出现之前,打开浏览器输入网址即可访问相应的网站,而有了二维码之后,我们扫描二维码,首先会做一次从二维码到文本的解析、转换,然后根据解析出来的文本结果判断是否是链接,是则跳转到这个链接,尽管我们操作方式改变了,但其原理是相同的。 


二维码对外暴露的是同一个网址,服务端只需要对这个网址做个二次跳转就行,这个对外暴露固定不变的网址也称为“活址”。




静态二维码和动态二维码(活码)的区别


比较项普通二维码动态二维码(活码)
内容修改不支持可以随时修改
内容类型支持文字、网址、电话等支持文字、图片、文件、音视、视频等内容
二维码图案内容越多越复杂活码图案简单
数据统计不支持支持
样式排版不支持支持


汉信码 -- 中国自主开发的二维码标准


汉信码是一种全新的二维矩阵码,由中国物品编码中心牵头组织相关单位合作开发,完全具有自主知识产权,支持任意语言编码、汉字信息编码能力超强、极强抗污损、抗畸变识读能力、识读速度快、信息密度高、信息容量大、纠错能力强等突出特点,达到国际领先水平。和国际上其他二维条码相比,更适合汉字信息的表示,而且可以容纳更多的信息。



物品编码中心于2003年申请了国家"十五"重大科技专项课题,并与我国多家自动识别技术企业合作,开展汉信码技术研究工作。2005年12月26日该课题顺利通过国家标准委组织的项目验收。2007年8月23日《汉信码》国家标准正式颁布,并于2008年2月1日正式实施。 




 汉信码生成:tuzim.net/barcode/han…
 汉信码识别:https://tuzim.net/hxdecode



它的主要技术特色是:
①.具有高度的汉字表示能力和汉字压缩效率
汉信码支持GB18030中规定的160万个汉字信息字符,并且采用12比特的压缩比率,每个符号可表示12~2174个汉字字符。


②.信息容量大
在打印精度支持的情况下,每平方英寸最多可表示7829个数字字符, 2174个汉字字符, 4350个英文字母。


③.编码范围广
汉信码可以将照片、指纹、掌纹、签字、声音、文字等凡可数字化的信息进行编码。


④.支持加密技术
汉信码是第一种在码制中预留加密接口的条码,它可以与各种加密算法和密码协议进行集成,因此具有极强的保密防伪性能。


⑤.抗污损和畸变能力强
汉信码具有很强的抗污损和畸变能力,可以被附着在常用的平面或桶装物品上,并且可以在缺失两个定位标的情况下进行识读。


⑥.修正错误能力强
汉信码采用世界先进的数学纠错理论,采用太空信息传输中常采用的Reed-Solomon纠错算法,使得汉信码的纠错能力可以达到30%。


⑦.可供用户选择的纠错能力
汉信码提供四种纠错等级,用户可以根据自己的需要在8%、15%、23%和30%各种纠错等级上进行选择,从而具有高度的适应能力。


⑧.容易制作且成本低
利用现有的点阵、激光、喷墨、热敏/热转印、制卡机等打印技术,即可在纸张、卡片、PVC、甚至金属表面上印出汉信码。由此所增加的费用仅是油墨的成本,可以真正称得上是一种“零成本”技术。


⑨.条码符号的形状可变
汉信码支持84个版本,可以由用户自主进行选择,最小码仅有指甲大小。


⑩.外形美观
汉信码在设计之初就考虑到人的视觉接受能力,所以较之现有国际上的二维条码技术,汉信码在视觉感官上具有突出的特点。



汉信码实现了我国二维码底层技术的后来居上,在我国多个领域行业实现规模化应用,为我国应用二维码技术提供了可靠核心技术支撑。





********** 延伸 **********


一维码 (条形码)


一维码也叫条形码,它是由不同宽度的黑条和白条按照一定的顺序排列组成的平行线图案,它的宽度记录着数据信息,长度没有记录信息,条形码常用于标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等信息,大部分食品包装袋背后都会印有条形码。


 全球的条形码标准都是由一个叫GS1的非营利性组织管理和维护的,通常情况下条形码由 95 条红或黑色的平行竖线组成,前三条是由黑-白-黑 组成,中间的五条由白-黑-白-黑-白组成,最后的三条和前三条一样也是由黑-白-黑组成,这样就把一个条形码分为左、右两个部分。剩下的 84 (95-3-5-3=84) 条按每 7 条一组分为 12 组,每组对应着一个数字,不同的数字的具体表示因编码方式而有所不同,不过都遵循着一个规律:右侧部分每一组的白色竖线条数都是奇数个。这样不管你是正着扫描还是反着扫描都是可以识别的。


 中国使用的条形码大部分都是 EAN-13 格式的,条形码数字编码的含义从左至右分别是前三位标识来源 国家编码 ,比如中国为:690–699,后面的 4 ~ 8 位数字代表的是厂商公司代码,但是位数不是固定的,紧接着后面 的 9~12 位是商品编码,第 13 位是校验码,这就意味着公司编码越短,剩余可用于商品编码的位数也越多,可表示的商品也就越多,当然公司代码出售价格也相应更昂贵,另外用在商品上的 EAN-13 条码是要到 国家物品编码中心去申请的。





作者:似水流年QC
来源:juejin.cn/post/7258201505337131065

收起阅读 »

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术? 首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?


首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。


那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?


先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。


为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。


对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。


对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。


那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?


所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?



也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。


出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。


那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。


其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。



第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。


我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。


换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。



所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。


那怎么样能够保证你自己有一个技术细节的敏感度?


我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。


当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。


自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。


总结


所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。


当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


作者:ali老蒋
来源:juejin.cn/post/7257784425044705340

收起阅读 »

生存or毁灭?QQ空间150万行代码的涅槃重生

腾小云导读 今年是 QQ 空间诞生的第十八年,空间客户端团队也在它十八岁生日前夕完成了架构升级。因为以前不规范的多团队协同开发,导致代码逐渐劣化,有着巨大的风险。于是 QQ 空间面对庞大的历史债务,选择了重构升级,不破不立。这里和大家分享一下在重构过程中遇到的...
继续阅读 »

腾小云导读


今年是 QQ 空间诞生的第十八年,空间客户端团队也在它十八岁生日前夕完成了架构升级。因为以前不规范的多团队协同开发,导致代码逐渐劣化,有着巨大的风险。于是 QQ 空间面对庞大的历史债务,选择了重构升级,不破不立。这里和大家分享一下在重构过程中遇到的问题和解题思路,欢迎阅读。


目录


1 空间重构项目的背景


2 为什么要重构


3 空间的架构是如何崩坏的


4 架构的生命力


5 渐进式重构如何实现


6 如何保证架构的扩展性与复用性7 如何降低复杂度并长期可控


8 如何防止劣化


9 性能优化


10 项目重构成果总结


11 展望


18年前,QQ 空间上线,迅速风靡全网,成为了很多人的青春回忆。18年后的今天,QQ 空间的生命力依然强劲,是很多年轻用户的首选社交平台。


而作为最老牌的互联网产品之一,QQ 空间的代码也比较陈旧,代码运行环境复杂,维护成本高,整体架构亟需一场升级。


01、空间重构项目的背景


作为一个平台型的入口,空间承担了为很多兄弟业务引流的责任,许多团队在空间的代码里协作开发。加上自身多年累积的功能迭代,空间的业务变得非常复杂。业务的复杂带来了架构的复杂,架构的复杂意味着维护成本的升高。多年来空间的业务交接频繁,多个团队接手。交到我们团队手上时,空间的代码已经一言难尽。


这里先简单介绍一下空间的业务形态:空间目前主要的入口是在手 Q 里,我们叫做结合版。同时独立版的空间 App 还在维护(没错,空间独立 App 仍然还有一批忠实观众)。可以看到,空间有一套独立于手 Q 之外的架构,结合版与独立版会共用大量技术组件和业务组件。



02、为什么要重构?


空间是一个祖上很阔的业务,代码量非常庞大,单统计结合版的代码,就超过了150w 行。同时空间的代码运行环境也极为复杂,涉及5个进程和2个插件。随着频繁的交接和多团队的协同开发,空间的代码逐渐劣化,各项代码质量的指标几乎都在手 Q 里垫底。


空间的代码成了著名的原始森林 - 进得去出不来。代码的劣化导致历史 bug 难以收敛,即使一行代码不改,每个版本也会新增历史 bug30+。


面对如此庞大的历史债务,空间已经到了寸步难行,不破不立的地步,重构势在必行。所以,借着空间 UI 升级的契机,空间团队开始空间历史上最大规模的一次重构。


03、空间的架构是如何逐步劣化的?


跳出棋局,站在今天的角度回头看,可以发现空间的代码是个典型案例,很好地展示了一个干净的架构是如何逐步劣化的。



3.1 扩展性低,异化代码无处安放


结合版与独立版涉及大量的代码复用,包括组件、页面和跨 App 的复用等。但由于前期架构扩展性不高,导致异化的业务代码无处安放,开始侵入底层技术组件。底层组件代码开始受到污染。


3.2 代码未隔离且缺乏编程范式


空间是个平台型的业务,广告、会员、游戏、直播、小世界等团队都会在空间的代码里开发。由于没有做好代码隔离,各团队的代码耦合在一起,各写各的。同时由于缺乏编程范式,同一个类中的代码风格迥异。破窗效应发生,污染开始扩散。


3.3 维护成本暴增,恶性循环


空间的业务逻辑本身就很复杂,代码的劣化使其复杂度暴增,后续接手团队已有心无力,只能缝缝补补又三年,恶性循环。


最后陷入怪圈: 代码很乱但是稳定,开发道理都懂但确实不敢动。



3.4 Feeds 流的崩坏


以空间的 Feeds 流为例,最开始的架构思路是很清楚的,核心功能在基类实现,上层业务可以低成本地开发一个新的 Feeds 流页面。同时做了很多动态化和容器化的设计,来满足迭代效率。



但后续的需求迅速膨胀,异化出18种 Feeds 流场景,单 Feeds 流可能出现60多种卡片。这导致基类代码与 Feed View 中的代码迅速膨胀。同时 N 个团队在同一批代码中开发,代码行数和圈复杂度逐渐劣化。



04、架构的生命力


痛定思痛,在进行空间重构前的首件事情就是总结经验,避免重蹈覆辙。如何保证这次重构平稳落地并且避免后续每三年一重构?


我们总结了四点:


渐进式重构:高速公路换轮胎,如何平稳落地? 提高扩展性和复用性:是否能低成本迁移到其他业务,甚至是其他 App? 复杂度长期可控:n 个团队跑来做两年需求,复杂度会不会变高? 做好防劣化:劣化代码被引入,能否快速发现?

空间的重构都围绕着这四个问题来进行。


05、渐进式重构如何实现?


作为一个亿级日活的业务,空间出现线上问题很容易引起大量投诉。高速公路换轮胎,小步快跑是最合适的方式。因此,平稳落地的关键是渐进式重构,避免步子迈得太大导致工作量扩散。


要做到渐进式重构,核心是保证两点:


一个复杂的大问题能被分解为许多个小问题,可针对小问题重构和回滚; 系统随时都是可用状态。每解决一个小问题,都可以针对性的测试和上线。

为了实现以上两点,我们基于以下几点来进行改造:



5.1 先拆解,后治理


我们并没有立即开始对旧代码进行重写,而是先基于团队的 RFW-Part 框架对老代码进行拆解。Part 自带生命周期,可以保证老代码平移前后的运行逻辑一致。


尽管代码逻辑没有翻新,但大问题被拆解为一个个小问题,我们再根据优先级对单个 Part 进行重构。保证无论重构了多少,空间都是可用状态,能立即上线验证。


RFW-Part 框架后文会有介绍,此处不做展开。


5.2 架构融合


我们彻底抛弃了空间老的技术组件,与团队内部沉淀的 RFWComponent 进行架构融合,同时也积极接入手 Q 统一的 UI 体系。保证开发能专注于业务中间层开发。


5.3 提效前置,简化运行环境


在进行业务重构前,我们先还了一部分技术债。包括去插件化、进程统一、工程结构优化和编译优化等。这些工作都在业务重构前完成并上线验证,简化了空间代码的运行环境,提升开发效率,保证了重构工作的敏捷性,达到了针对单点问题快速重构快速验证的目的。



06、如何保证架构的扩展性与复用性?


扩展性和复用性是软件工程永恒的话题。空间历史架构并没有很好处理这两点,其他业务接入时难以处理异化逻辑,使异化逻辑侵入底层代码。同时为了强行实现结合版和独立版的代码复用,使不同的场景耦合在一起,互相干扰。


为了提高架构的扩展性和复用性,我们重新设计了空间的架构层级。


6.1 业务层打薄,专注中间层


为了避免代码跨层级污染,我们对架构的分层比以往更细,隔离做得更严格。


底层技术组件基于 RFW 框架。RFW 中的组件更干净,没有任何业务侵入,能在其他 App 开箱即用。


中间层负责对 RFW 组件和手 Q 运行环境做桥接,并对底层组件进行扩展,实现一些空间相关但与具体场景无关的功能。中间层的代码能在一周之内迁移到其他 App。



6.2 业务层打薄,专注中间层


RFWComponent 是一线开发在实际业务中沉淀出的一套组件库,目前由空间和小世界团队共同维护。所有组件都经过了线上业务的验证,保证了易用性和扩展性。组件也很完整,开箱即用。


最重要的是,RFW 的核心组件都可由上层注入代理实现,这使其并不依赖于手 Q 的运行环境,也避免了业务侧逻辑入侵底层代码。


目前整套架构已在空间、小世界、频道、基础等团队深度使用。空间也是第一个使用这套架构重构老代码的业务,整个过程非常省心。



07、如何降低复杂度并长期可控?


7.1 组合代替继承,Part + Section,拆!


什么是 RFW-Part?RFW-Part 是团队内部沉淀的一套页面级的 UI 容器架构,Part 可感知页面的生命的周期,功能在内部闭环。不同 Part 无法感知对方存在,代码是严格隔离的。



但是 Part 是页面级框架,无法解决 Feeds 流列表复杂的问题,Section 架构作为 Part 的补充,主要解决列表以及 ItemView 的拆解问题。其设计思路与 Part 框架一致。



基于 Part 和 Section 架构,我们将空间的代码拆分为了一个个标准的集装箱。代码复杂度和上手难度大大降低。新人内包入职一周便可独立开发,三天就完成了新功能此刻的消息页。



7.2 使用 Part 架构重塑超级页面


空间80%的流量和功能都集中在好友动态页和个人主页两个 Feeds 流页面,尽管内部已基于 mvvm 分层,但单层内的复杂度仍然过高:



以空间的好友动态页为例,我们将页面不同功能的代码都拆分到一个个 Part 里,Fragment 仅作为一个容器,负责组装自己需要的 Part。



最终页面被拆分为27个 Part,页面代码由6000多行减少到320行。很多 Part 可以直接拿去被其他 Feeds 流页面复用。



7.3 使用 Section 框架重塑 Feeds 流


经过 Part 的改造,页面级的功能都被拆分为子模块。但 Feeds 流整体作为一个 Part,复杂度仍然过高,我们需要设计一套新的框架,对 Feeds 流中的卡片进一步拆解。


7.3.1 空间老的 Feeds 流框架


这里先介绍一下空间老的 Feeds 流框架 - Ditto。


Ditto 框架魔改了 Android 原生的布局体系。其将一个卡片按位置分为不同 Area,每个 Area 作为一个容器。不同类型的卡片根据服务端下发的数据在 Area 内部做异化。


而每个 Area 的布局由 json 文件下发,Ditto 框架解析后使用 canvas 自绘,完成显示。



这套架构的优势是动态化能力强,服务端可定义任意样式,但缺点同样明显:


代码复杂度持续膨胀; 各业务代码耦合; 功能代码分散,AB 测试不友好; 难以扩展。

7.3.2 优化方向


为了降低复杂度,我们决定按以下方向优化:


中心化 -> 去中心化; 代码物理隔离; 内部闭环,动态开关; 组装者模式,方便扩展。

7.3.3 Section 框架架构设计


和 Part 一样,我们将一个卡片按照功能逻辑拆分为一个个 Section,形成一个 Section 池。不同卡片根据需要组装自己需要的 Section 即可。


Section 的 UI、数据、业务都是内部闭环的。不同 Section 互不感知,保证了代码物理隔离。


每个 Section 会与 ViewStub 绑定,布局可以按需加载。ViewStub 与 Section 是一对多的关系,Section 在查找 ViewStub 前会先去缓存池找,这样实现了多个 Section 修改同一个 View,保证 Section 拆得尽可能细。



上图中各模块的具体职责如下:


Section:某一切片的完整 UI+逻辑; ViewStub:与 Section 一对多,按需加载; Assembler:负责组装 Section,可根据页面异化; SectionManager:绑定数据、分发生命周期; DataCenter:Feeds 相关数据在各页面间的同步; IOC 框架:控制反转,用于 Section 与页面交互。

Section 整体的结构图如下:



7.3.4 落地效果


基于这套 Feeds 流框架,我们完成了历史卡片的梳理和重构:


接入36种 Feed,拆分52个 Section,下线28种 Feed; 重构4个核心页面,单类代码不超过500行; 单条 Feed 开发时间缩短一半; 广告/增值团队一个版本即完成历史功能迁移。

7.4 完善通信设计,保证代码隔离不被打破


Part 和 Section 之间会有许多通信的需求,比如数据同步,不同模块交互等。为了保证代码隔离不被打破,我们设计了比较完善的通信机制:


页面与 Part:ViewModel + LiveData; Part 与 Part:页面级事件,事件只在 PartHost 内部生效,无需注册与反注册; 页面与页面:DataCenter 数据同步。


7.5 异化逻辑抽离,复杂度持续可控


除此之外,另一种容易打穿架构的元素是异化逻辑。比如同一张卡片在不同的页面需要显示不同效果,比如数据埋点的参数需要从页面最外层传递到 Section。针对这种跨层级通信的场景,我们设计了一套 IOC 框架来完成依赖注入,将异化逻辑拆分到了一个个 IOC 实现类中。



IOC 机制的核心是:View 树回溯 + ViewTag 存储 + 接口中心管理。我们注册时将 IOC 实现类与 View 绑定,查找时基于 View 树来回溯,保证了 O(N) 的复杂度,且可以跨越任意层级。



过去,即使传递一个 pageId 参数,也要一层层传递:



现在,层级再深我们也可以很方便拿到需要的 IOC 实现。


08、升级方案


8.1 容灾设计


站在用户的角度,其实对重构与否并没有太大感知,用户只关心稳定性是否有下降。如此大规模的重构,一行代码引起的崩溃便能使几个月的努力功亏一篑。我们上线前的首要目标便是保证用户使用不受影响,不求有功,但求无过。


因此,我们在上线前做了很多容灾设计,保证空间的核心功能可用性。



8.1.1 动态开关


我们在空间的中间层埋了配置,能通过配置下架任意的 Part 或 Section。业务层编写代码时不用再单独为每个小模块添加开关,只要基于框架做好细粒度的拆分即可。


8.1.2 崩溃保护


同时,我们做了崩溃保护的设计,保证非核心功能崩溃不会影响核心功能的使用:


崩溃时进行关键词匹配,达到指定频率时禁用/降级相关功能; 自动对 Part/Section/页面/Feed 做关键词匹配,无需注册; 非必要功能可手动注册关键词,添加保护。

8.2 性能监控


同时,为了防止性能劣化,我们做了很多性能监控。


针对线上:


利用手 Q RMonitor 框架的监控和我们自己上报的滑动流畅度指标,来监控页面整体的流畅度; 通过在框架层打点,来监控每一个 Part、Section 或 Feed 的耗时。有劣化的模块引入时能快速发现; 实现 RFWTracer 框架,自动在页面启动流程中打点,统计页面启动各阶段的耗时。

针对线下:


我们基于 ARTMethodHook 框架,实现对具体 View 耗时的监控,能快速定位到出问题的控件,节约开发定位性能问题的时间。


整体监控体系如图:



实际效果如图:



09、性能优化


第一次灰度后,我们尴尬地发现启动速度并没有大幅提升,流畅率甚至发生了降低。因此我们做了首屏启动和流畅度的专项优化:


9.1 首屏启动优化


我们重新梳理了启动流程中的数据处理,在启动前和启动后做了一定优化:




  • 布局异步渲染


我们将首屏启动前,会根据缓存提前计算需要的布局,实现布局异步预加载。同时,为了保证 Context 的正确性,我们 Hook 了 Activity 的启动流程,提前准备好空的 Activity 对象用于异步 inflate,并在启动后绑定真实的 Context。




  • 精准预加载


在首屏启动前读取缓存,提前计算首屏 Feed 对应的 Section 布局并异步加载。



  • 生命周期扩展


扩展 Part 生命周期,各个 Part 的次要功能在首屏展示后初始化。



  • 优化后的效果


空间好友动态页的冷启动速度提升56%,热启动速度提升53%。


9.2 列表性能优化


经过分析,我们发现列表卡顿的原因集中在两点:


Item 复用率低,导致频繁创建新 View; 布局嵌套多,测量较慢。

解决思路:


边滑边异步 inflate:为了解决频繁创建新 View 的问题,我们在滑动时,会提前计算后面卡片所需的 ViewStub,并提前异步加载好。 自定义组件,降低层级,提前计算高度:列表中部分组件测量性能较差,比如部分嵌套 RecyclerView 的组件,会频繁触发子 RecyclerView 的测量,拉高整体测量耗时。对于这些组件,我们使用自定义组件的方式进行了替换。降低布局层级,并且提前计算高度,设置布局的高度为固定值,防止频繁测量。

优化后的效果:完成优化后,空间首页 FPS 完成了反超,相比老版本提升了 4.9%。


10、项目重构成果总结


从我们 AB 测试的实验数据来看,重构的整体结果是比较正向的,代码质量提升与性能提升带来了业务指标的提升,业务指标的提升也带来广告指标的提升。



11、展望


空间的代码历史悠久,错综复杂,使得空间业务在很长一段时间都处于维护状态,难以快速开发新的需求。最大的三个模块是压在空间业务上的三座大山:Feeds 流、相册和发表。通过这次架构升级,我们完成空间底层架构的焕新,完全重写了最复杂的 Feeds 流场景,同时相册模块也已经重构了一半。等剩余模块重构完成,空间的祖传代码就被全部重写了。面向未来,我们也能够更迅速地支撑新需求的落地,让十八岁的 QQ 空间焕然新生,重新上路。欢迎转发分享~


-End-


原创作者|尹述迪

收起阅读 »

iOS pod EaseIMKit库如何放在本地使用

在使用环信EaseIMKit库的时候,发现有些开发者需要改动库中的一些逻辑,或者有UI上的一些调整,如果直接去改pods里面的库,在之后的库版本升级会把之前修改过的代码覆盖掉,这个时候我们就需要pod指向本地的库,去比较好的实现本地组件化,也不会在pod in...
继续阅读 »

在使用环信EaseIMKit库的时候,发现有些开发者需要改动库中的一些逻辑,或者有UI上的一些调整,如果直接去改pods里面的库,在之后的库版本升级会把之前修改过的代码覆盖掉,这个时候我们就需要pod指向本地的库,去比较好的实现本地组件化,也不会在pod install的时候造成其他的冲突.

那么EaseIMKit库如何放在本地使用呢?本文教你五步实现~~

1、我们先去环信官网下载 SDK Demo

下载后,在文件包里可以看到EaseIMKit这个文件夹

2、也可以通过pod search EaseIMKit查找对应的版本,pod install到本地后再做相对应的操作。

3、我们可以把上图红框里的EaseIMKit文件夹拷贝到自己的项目里,确保文件夹里相对应的文件,如下图所示


4、这个时候可以在podfile文件里设置pod指向本地的路径,设置的方法如下图所示


5、 最后,再执行pod install就可以了

收起阅读 »

单线程 Redis 如此快的 4 个原因

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w… 作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。 然而,在 Redis 内部采用的也只是单线程的...
继续阅读 »

本文翻译自国外论坛 medium,原文地址:levelup.gitconnected.com/4-reasons-w…


作为内存数据存储,Redis 以其速度和性能而闻名,通常被用作大多数后端服务的缓存解决方案。


然而,在 Redis 内部采用的也只是单线程的设计。


为什么 Redis 单线程设计会带来如此高的性能?如果利用多个线程并发处理请求不是更好吗?


在本文中,我们将探讨使 Redis 成为快速高效的数据存储的设计选择。


长话短说


Redis 的性能可归因于 4 个主要因素



  • 基于内存存储

  • 优化的数据结构

  • 单线程架构

  • 非阻塞IO


让我们一一剖析一下。



推荐博主开源的 H5 商城项目waynboot-mall,这是一套全部开源的微商城项目,包含三个项目:运营后台、H5 商城前台和服务端接口。实现了商城所需的首页展示、商品分类、商品详情、商品 sku、分词搜索、购物车、结算下单、支付宝/微信支付、收单评论以及完善的后台管理等一系列功能。 技术上基于最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中间件。分模块设计、简洁易维护,欢迎大家点个 star、关注博主。


github 地址:github.com/wayn111/way…



基于内存存储




Redis 是在内存中进行键值存储。


Redis 中的每次读写操作都相当于从内存的变量中进行读写。


访问内存比直接访问磁盘快几个数量级,因此Redis 比其他数据存储快得多。


优化的数据结构




作为内存数据存储,Redis 利用各种底层数据结构来高效存储数据,无需担心如何将它们持久化到持久存储中。


例如,Redis list 是使用链表实现的,它允许在列表的头部和尾部附近进行恒定时间 O(1) 插入和删除。


另一方面,Redis sorted set 是通过跳跃列表实现的,可以实现更快的查询和插入。


简而言之,无需担心数据持久化,Redis 中的数据可以更高效地存储,以便通过不同的数据结构进行快速检索。


单线程




Redis 中的写入和读取速度非常快,并且 CPU 使用率从来不是 Redis 关心的问题。


根据 Redis 官方文档,在普通 Linux 系统上运行时,Redis 每秒最多可以处理 100 万个请求。


通常瓶颈来自于网络 I/O, Redis 中的处理时间大部分浪费在等待网络 I/O 上。


虽然多线程架构允许应用程序通过上下文切换并发处理任务,但这对 Redis 的性能增益很小,因为大多数线程最终会在 I/O 中被阻塞。


所以 Redis 采用单线程架构,有如下好处



  • 最大限度地减少由于线程创建或销毁而产生的 CPU 消耗

  • 最大限度地减少上下文切换造成的 CPU 消耗

  • 减少锁开销,因为多线程应用程序需要锁来进行线程同步,而这容易出现错误

  • 能够使用各种“线程不安全”命令,例如 Lpush


非阻塞I/O




为了处理传入的请求,服务器需要在套接字上执行系统调用,以将数据从网络缓冲区读取到用户空间。


这通常是阻塞操作,线程被阻塞并且在完全接收到来自客户端的数据之前不能执行任何操作。


为什么我们不能在只有确定套接字中的数据已准备好读取时,才执行系统调用嘞?


这就是 I/O 多路复用发挥作用的地方。


I/O 多路复用模块同时监视多个套接字,并且仅返回可读的套接字。


准备读取的套接字被推送到单线程事件循环,并由相应的处理程序使用响应式模型进行处理。


总之,



  • 网络 I/O 速度很慢,因为其阻塞特性,

  • Redis 收到命令后可以快速执行,因为这在内存中执行,操作速度很快,


所以 Redis 做出了以下决定,



  • 使用 I/O 多路复用来缓解网络 I/O 缓慢问题

  • 使用单线程架构减少锁开销


结论




综上所述,单线程架构是 Redis 团队经过深思熟虑的选择,并且经受住了时间的考验。


尽管是单线程,Redis 仍然是性能最高、最常用的内存数据存储之一。

作者:waynaqua
来源:juejin.cn/post/7257783692563611685

收起阅读 »

SpringBoot可以同时处理多少请求?

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。 首先,我们需要了解一...
继续阅读 »

SpringBoot是一款非常流行的Java后端框架,它可以帮助开发人员快速构建高效的Web应用程序。但是,许多人对于SpringBoot能够同时处理多少请求的疑问仍然存在。在本篇文章中,我们将深入探讨这个问题,并为您提供一些有用的信息。


首先,我们需要了解一些基本概念。在Web应用程序中,请求是指客户端向服务器发送的消息,而响应则是服务器向客户端返回的消息。在高流量情况下,服务器需要能够同时处理大量的请求,并且尽可能快地响应这些请求。这就是所谓的“并发处理”。


SpringBoot使用的是Tomcat作为默认的Web服务器。Tomcat是一种轻量级的Web服务器,它可以同时处理大量的请求。具体来说,Tomcat使用线程池来管理请求,每个线程都可以处理一个请求。当有新的请求到达时,Tomcat会从线程池中选择一个空闲的线程来处理该请求。如果没有可用的线程,则该请求将被放入队列中,直到有线程可用为止。


默认情况下,SpringBoot会为每个CPU内核创建一个线程池。例如,如果您的服务器有4个CPU内核,则SpringBoot将创建4个线程池,并在每个线程池中创建一定数量的线程。这样可以确保服务器能够同时处理多个请求,并且不会因为线程过多而导致性能下降。


当然,如果您需要处理大量的请求,您可以通过配置来增加线程池的大小。例如,您可以通过修改application.properties文件中的以下属性来增加Tomcat线程池的大小:


server.tomcat.max-threads=200

上述配置将使Tomcat线程池的最大大小增加到200个线程。请注意,增加线程池大小可能会导致服务器资源消耗过多,因此应该谨慎使用。


除了Tomcat之外,SpringBoot还支持其他一些Web服务器,例如Jetty和Undertow。这些服务器也都具有良好的并发处理能力,并且可以通过配置来调整线程池大小。


最后,需要注意的是,并发处理能力不仅取决于Web服务器本身,还取决于应用程序的设计和实现。如果您的应用程序设计得不够好,那么即使使用最好的Web服务器也无法达到理想的并发处理效果。因此,在开发应用程序时应该注重设计和优化。


总之,SpringBoot可以同时处理大量的请求,并且可以通过配置来增加并发处理能力。但是,在实际应用中需要根据具体情况进行调整,并注重应用程序的设计和优化。希望本篇文章能够帮助您更好地理解SpringBo

作者:韩淼燃
来源:juejin.cn/post/7257732392541618237
ot的并发处理能力。

收起阅读 »

随着鼠标移入,图片的切换跟着修改背景颜色(Vue3写法)

web
先看看效果图吧 下面来看实现思路 又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的, 我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来 1.背景颜色不是固定的,是随着图片的切换动态...
继续阅读 »

先看看效果图吧


image.png


image.png


下面来看实现思路


又是摸鱼的下午,无聊来实现了一下这个效果,记录一下,说不定以后有这需求,记一下放到官网上也是OK的,
我这里提供一种实现方法,当然你们想用放大加模糊也是可以的,想怎么来就怎么来


1.背景颜色不是固定的,是随着图片的切换动态改变


原理:

1.当鼠标移入到某一张图片时,拿到这张图片

2.我们就可以把这张图片画到canvas里,就可以获取到每一个像素点

3.我们的背景是需要渐变的,我们是需要三种颜色的渐变,当然也可以有很多种,看你们的心情

4.我们就要计算出前三种的主要颜色,但是每个像素点的颜色非常非常多,好多颜色也非常相近,我们通过肉眼肯定看不出来的,这个时候就要用到计算机了

5.需要一种近似算法(颜色聚合算法)了,就是把好多相近的颜色聚合成一种颜色,当然我们就要用到第三方库(colorthief)了


准备好html


<template>
<div class="box">
<div class="item" v-for="item in 8" :key="item" :class="item === hoverIndex ? 'over' : ''">
<img crossorigin="anonymous" @mouseenter="onMousenter($event.target, item)" @mouseleave="onMousleave"
:src="`https://picsum.photos/438/300?id=${item}`" alt=""
:style="{ opacity: hoverIndex === -1 ? 1 : item === hoverIndex ? 1 : 0.2 }">
// 设置透明度
</div>
</div>
</template>


scss


.box {
height: 100vh;
display: flex;
justify-content: space-evenly;
align-items: center;
flex-wrap: wrap;
background-color: rgb(var(--c1), var(--c2), var(--c3));
}

.item {
border: 1px solid #fff;
margin-top: 50px;
transition: 0.8s;
padding: 5px;
box-shadow: 0 0 10px #00000058;
background-color: #fff;
}

img {
transition: .8s;
}

npm安装colorthief库


npm i colorthief

导入到文件中


import ColorThief from "colorthief";

因为这是一个构造函数,所以需要创建出一个实例对象


const colorThief = new ColorThief()
const hoverIndex = ref<number>(-1) //设置变换样式的响应式变量

重点函数:鼠标移入事件onMousenter


getPalette(img,num) img是dom元素,是第三库需要将其画入到canvas中,所以需要在img标签中添加一个允许跨域的属性 crossorigin="anonymous",不然会报错

num是需要提取几种颜色,同样也会返回多少个数组

返回的是一个promise,需要await


const onMousenter = async (img: EventTarget | null, i: number) => {
hoverIndex.value = i //将响应式变量改成自身,样式就生效了
const colors = await colorThief.getPalette(img, 3)
console.log(colors); //获取到三个数组,将其数组改造成rgb格式
const [c1, c2, c3] = colors.map((c: string[]) => `rgb(${c[0]},${c[1]},${c[2]})`)//将三个颜色解构出来
html.style.setProperty('--c1', c1) //给html设置变量,下面有步骤
html.style.setProperty('--c2', c2)
html.style.setProperty('--c3', c3)
}

鼠标移出事件


将响应式变量初始化,将背景颜色改为白色


const onMousleave = () => {
hoverIndex.value = -1
html.style.setProperty('--c1', '#fff')
html.style.setProperty('--c2', '#fff')
html.style.setProperty('--c3', '#fff')
}

获取html根元素


const html = document.documentElement

在主文件index.html给html设置渐变变量


<style>
html{
background-image: linear-gradient(to bottom, var(--c1), var(--c2),var(--c3));
}
</style>

image.png
需要注意的是colorthief使用的时候需要给img设置跨域,不然会报错,还有就是给html设置渐变变量


🔥🔥🔥好的,到这里基本上就已经实现了,看着代码也不多,也没啥技术含量,全靠三方库干事,主要是记录生活,方便未来cv


作者:井川不擦
来源:juejin.cn/post/7257733186158903356
收起阅读 »

几何算法:判断两条线段是否相交

web
‍ ‍大家好,我是前端西瓜哥。 如何判断两条线段(注意不是直线)是否有交点? 传统几何算法的局限 上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。 一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1)...
继续阅读 »


‍大家好,我是前端西瓜哥。


如何判断两条线段(注意不是直线)是否有交点?


传统几何算法的局限


上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。


一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)),两条线段是两个两点式,这样就是 二元一次方程组 了 ,就能求出两条直线的交点。


然后判断这个点是否在其中一条线段上。如果在,说明两线段相交,否则不相交。


看起来不错,但这里要考虑直线垂直或水平于坐标轴的特殊情况,还有两条直线平行导致没有唯一解的情况,除数不能为 0 的情况。


特殊情况实在是太多了,能用是能用,但不好用。


那么,有其他的更好的解法吗?


有的,叉乘。


叉乘是什么?


叉乘(cross product)是线性代数的一个概念,也叫外积、叉积、向量积,是在三维空间中两个向量的二元运算的结果,该结果为一个向量。


但那是严格意义上的。实际也可以用在二维空间的二维向量中,不过此时它们的叉乘结果变成了标量。


假设向量 A 为 (x1, y1),向量 B 为 (x2, y2),则叉乘 AxB 的结果为 x1 * y2 - x2 * y1


(注意叉乘不满足交换律)


在几何意义上,这个叉乘结果的绝对值对应两个向量组成的平行四边形的面积。


此外可通过符号判断向量 A 变成向量 B 的旋转方向。


如果叉乘为正数,说明 A 变成 B 需要逆时针旋转(旋转角度小于 180 度);


如果为负数,说明 A 到 B 需要顺时针旋转;


如果为 0,说明两个向量平行(或重合)


叉乘解法的原理


回到题目本身。


假设线段 1 的端点为 A 和 B,线段 2 的端点为 C 和 D。


图片


我们可以换另一个角度去解,即判断线段 1 的两个端点是否在线段 2 的两边,然后再反过来比线段 2 的两点是否线段 1 的两边。


这里我们可以利用上面 叉乘的正负代表旋转方向的特性


以上图为例, AB 向量到 AD 向量位置需要逆时针旋转,AB 向量到 AC 向量则需要顺时针,代表 C 和 D 在 AB 的两侧,对应就是两个叉乘相乘为负数。


function crossProduct(p1: Point, p2: Point, p3: Point)number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

const [a, b] = seg1;
const [c, d] = seg2;

// d1 的符号表示 AB 旋转到 AC 的旋转方向
const d1 = crossProduct(a, b, c);


只是判断了 C 和 D 在 AB 线段的两侧还不行,因为可能还有下面这种情况。


图片


所以我们还要再判断一下,A 和 B 是否在 CD 线的的两侧。计算过程同上,这里不赘述。


一般实现


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  return d1 * d2 < 0 && d3 * d4 < 0;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];

console.log(isSegmentIntersect(seg1, seg2)); // true


注意,这个算法认为线段的端点刚好在另一条线段上的情况,不属于相交。


考虑点在线段上或重合


如果你需要考虑线段的端点刚好在另一条线段上的情况,需要额外在叉乘为 0 的情况下,再判断一下线段 1 的端点是否在另一个线段的 x  和 y 范围内。


对应的算法实现:


type Point = [numbernumber];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function onSegment(p: Point, seg: [Point, Point]): boolean {
  const [a, b] = seg;
  const [x, y] = p;
  return (
    x >= Math.min(a[0], b[0]) &&
    x <= Math.max(a[0], b[0]) &&
    y >= Math.min(a[1], b[1]) &&
    y <= Math.max(a[1], b[1])
  );
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  if (d1 * d2 < 0 && d3 * d4 < 0) {
    return true;
  }
 
  // d1 为 0 表示 C 点在 AB 所在的直线上
  // 接着会用 onSegment 再判断这个 C 是不是在 AB 的 x 和 y 的范围内
  if (d1 === 0 && onSegment(c, seg1)) return true;
  if (d2 === 0 && onSegment(d, seg1)) return true;
  if (d3 === 0 && onSegment(a, seg2)) return true;
  if (d4 === 0 && onSegment(b, seg2)) return true;

  return false;
}

// 测试
const seg1: [PointPoint] = [
  [00],
  [11],
];
const seg2: [PointPoint] = [
  [01],
  [10],
];
const seg3: [PointPoint] = [
  [00],
  [22],
];
const seg4: [PointPoint] = [
  [11],
  [10],
];
// 普通相交情况
console.log(isSegmentIntersect(seg1, seg2)); //  true
// 线段 1 的一个端点刚好在线段 2 上
console.log(isSegmentIntersect(seg3, seg4)); // true


结尾


总结一下,判断两条线段是否相交,可以判断两条线段的两端点是否分别在各自的两侧,对应地需要用到二维向量叉乘结果的正负值代表向量旋转方向的特性。


我是前端西瓜哥,关注我,学习更多几何算法。



作者:前端西瓜哥
来源:juejin.cn/post/7257547252540751909

收起阅读 »

在字节的程序员的 2023 年中总结

2023 年已经过去了,回顾这一年,作为字节的程序员,我想分享一下我的总结。 首先,2023 年是一个非常特殊的一年。全球疫情已经得到有效控制,人们的生活逐渐恢复正常。在这个背景下,科技行业得到了更多的关注和投资。作为一名程序员,我感到非常幸运能够在这个行业中...
继续阅读 »

2023 年已经过去了,回顾这一年,作为字节的程序员,我想分享一下我的总结。


首先,2023 年是一个非常特殊的一年。全球疫情已经得到有效控制,人们的生活逐渐恢复正常。在这个背景下,科技行业得到了更多的关注和投资。作为一名程序员,我感到非常幸运能够在这个行业中工作。


在这一年中,我参与了很多有趣的项目。其中最令我印象深刻的是我们团队开发的一款智能家居系统。这个系统可以通过语音控制来控制家里的各种设备,比如灯光、温度、音响等等。用户可以通过手机 App 或者智能音箱来控制家居设备。这个项目不仅让我学习了很多新技术,还让我感受到了科技带来的便利和乐趣。


除了技术方面的学习和成长,我也意识到了作为一名程序员的责任和使命。在这个信息时代,程序员们不仅仅是技术人员,更是社会的建设者和推动者。我们所开发的软件和系统,不仅仅是为了满足商业需求,更应该为社会带来更多的价值和福利。比如,在疫情期间,我们团队开发了一款在线医疗咨询系统,让患者可以在线上咨询医生,减少了人员聚集和传染的风险。这种为社会做贡献的感觉真的很棒。


当然,在这一年中也遇到了很多挑战和困难。比如,我们团队在开发一个大型项目时遇到了很多技术难题和进度压力。但是,通过团队合作和不断努力,最终我们成功地完成了项目,并得到了客户的高度评价。这也让我更加深刻地认识到团队合作和自我提升的重要性。


总之,2023 年对我来说是一个非常充实和有意义的一年。在未来的日子里,我会继续努力学习和提升自己,为社会做出更多的贡献。同时也祝愿所有的程序员们都能在自己的岗位上取

作者:韩淼燃
来源:juejin.cn/post/7257733186158805052
得更好的成绩和发展。

收起阅读 »

如何写出一手好代码(上篇 - 理论储备)?

无论是刚入行的新手还是已经工作多年的老司机,都希望自己可以写一手好代码,这样在代码 CR 的时候就可以悄悄惊艳所有人。特别是对于刚入职的新同学来说,代码写得好可以帮助自己在新环境快速建立技术影响力。因为对于从事 IT 互联网研发工作的同学来说,技术能力是研发同...
继续阅读 »

无论是刚入行的新手还是已经工作多年的老司机,都希望自己可以写一手好代码,这样在代码 CR 的时候就可以悄悄惊艳所有人。特别是对于刚入职的新同学来说,代码写得好可以帮助自己在新环境快速建立技术影响力。因为对于从事 IT 互联网研发工作的同学来说,技术能力是研发同学的立身之本,而写代码的能力又是技术能力的重要体现。但可惜的是理想很丰满,现实很骨感。结合慕枫自己的经验来看,我们在工作中其实没那么容易可以看到写得很好的代码。造成这种情况的原因也许很多,但是无论什么原因都不应该妨碍我们对于写好代码的追求。今天慕枫就和大家探讨下到底怎样做才能写出一手大家都认为好的代码?


哪些因素制约好代码的产生?


我们首先来分析下到底哪些因素造成了现实工作中好代码难以产出。因为只有搞清楚了这个问题才能对症下药,这样在我们自己写代码的时候才能尽量避免这些问题影响我们写好代码。


假如让我们说出哪些是烂代码,我们也许会罗列出来代码不易理解、没有注释、方法或者类词不达意、分层不合理、不够抽象、单个方法过长、单个类过长、代码难以维护每次改动都牵一发动全身、重复代码过多等等,这些都是我们在实际项目开发过长中经常遇到的代码问题。那么到底是什么原因造成了现实项目中有这么多的代码问题呢?慕枫认为主要存在以下三方面的原因。



1、项目倒排时间不够


项目需求倒排导致没有时间在写代码前好好进行设计,所以只能先快速满足需求等后面有时间再优化(大概率是没有时间的)。这就造成技术同学在写代码的时候怎么快怎么写,优先把功能实现了再说,很多该考虑的细节就不会考虑那么多,该处理的异常没有进行处理,所以可能写出来的代码可以说是一次性代码,只针对当前的业务场景,基本没什么扩展性可言。


2、团队技术氛围不足


团队内技术氛围不是很浓厚,本来你是想好好把代码写好的,但是发现大家都在短平快的写代码,而且没有太多人关心代码写的好不好,只关心需求有没有按时完成。在这样的团队氛围影响之下,自己写出来的代码也在慢慢地妥协。像在阿里这样的一线互联网公司,团队中的代码文化还是很强的,很多技术团队在需求上线前必须要进行代码 CR,CR 不过的代码不允许上线。因此好的团队技术氛围会促使你不得不把代码写好,否则在代码 CR 的时候就等着接受暴风雨般的吐槽吧。


3、自身技术水平有限


第三个原因就是自身的技术水平有限,设计模式不知道该在什么样的业务场景下使用,框架的高级用法没有掌握,经验不足导致异常情况经常考虑不到。自己本身没有把代码写好的追求,总想着能满足需求代码能跑就行。


以上大概是我们实际工作中导致我们不能产出好代码最主要的三大原因,第一个原因我们基本无法改变,因为在互联网行业竞争本身就非常激烈,谁能先推出新业务优化用户体验,谁就能占得市场先机。因此项目倒排必定是常有的事情,也是无法避免的事情。第二个原因,如果你自己是团队的 TL,那么尽量在团队中去营造代码 CR 的文化,提升团队中的技术氛围。因为代码是技术团队的根本,所有的业务效果落地都需要通过代码来实现,因此好的代码可以帮助团队减少 Bug 出现的概率、提升大家的代码效率从而达到降低人力物力成本的目的。如果你不是团队的 TL,同时团队中的技术氛围也没那么足,那么我们也不要放弃治疗,先把自己负责的模块的代码写好,一点点影响团队,逐渐唤起大家对于好代码的重视。


前两个因素都属于环境因素,也许我们不好改变,但是对于第三个因素,我觉得我们可以通过理论知识的学习,不断的代码实践以及思考总结是可以改变的,因此本文主要还是讨论如何通过改变自己来把代码写好。


到底什么是好代码?


要想写出好的代码,首先我们得知道什么样的代码才是好代码。但是好这个字本身就具有较强的主观性,正所谓一千个读者心中就有一千个哈姆雷特。因此我们需要先统一一下好代码的标准,有了标准之后我们再来探讨到底怎么做才能写出好代码。


我相信大家肯定听说过代码可读性、代码扩展性、可维护性等词汇来描述好代码的特点,实际上这些形容词都是从不同方面对代码进行了阐述。但是在慕枫看来,在实际的项目开发中,可维护性以及高鲁棒性是好代码的两个比较核心的衡量标准。因为无论是开发新需求还是修复 Bug,都是在原有的平台代码中进行修改,如果原来代码的扩展性比较强,那么我们编码的时候就就可以做到最小化修改,降低引入问题的风险。而鲁棒性高的代码在线上出现 Bug 的概率相对来说就第一点,对于维护线上服务的稳定性具有重要意义。


可维护性


我们都知道代码开发并不是一个人的工作,通常涉及到很多人团队合作。因此慕枫认为代码的可维护性是好代码的第一要义。而可维护性主要体现在代码可读容易理解以及修改方便容易扩展这两方面,下面分别进行阐述说明。


代码可读


我们写出来的代码不仅仅要自己能看得懂自己写的代码,别人也应该可以轻松看得懂你的代码。在一线的互联网大厂中工作内容发生变化是常有的事情,如果别人接手我们的代码或者我们接手别人的代码时,可读性强的代码无疑可以减少大家理解业务的时间成本。因为代码是最直接的业务表现,那些所谓的设计文档要么过时要么写的非常粗略,基本不太能指导我们熟悉业务。那么什么样的代码称得上可读性强呢?


命名准确


无论是包的命名、类的命名、方法的命名还是变量的命名都能很准确地表达业务含义,让人可以看其名知其义。命名应该和实际的代码逻辑相匹配,否则不合适的命名只会让人丈二和尚摸不着脑袋误导看代码的同学。以前看代码的时候我看过以 main 作为类中的方法名称,所以得看完这个方法的实现逻辑才能明白它到底干什么的,这对于后期维护的同学来说非常不友好。


代码注释


另外就是必要的注释,有些同学非常自信觉得自己写的代码很好懂,根本不需要写什么注释。结果自己过了一两个月再回头看自己的代码的时候,死活想不起来某段代码为什么要这么写。当然我们不必每一行代码都写注释,但是该注释的地方就要写注释,特别是一些逻辑比较复杂,业务性比较强的地方,既方便自己以后排查问题也方便后面维护的同学理解业务。因此不要对自己写的代码过于自信,间隔时间一长也许连你自己都未必记得代码为什么这么写。


结构清晰


无论是服务的包结构还是代码结构都体现了技术同学对于技术的理解,因此即便是不深入看代码逻辑,通过包结构的划分、模块的划分类结构的设计已经基本可以判断出来项目的代码质量了。我们在进行包结构设计的时候可以遵循依赖倒置的原则,让非核心层依赖核心层。



可扩展性


随着业务需求的不断变化,技术同学免不了在原有的代码逻辑中进行修改。因此项目代码的可扩展性直接影响着后期维护的成本。如果改一个小需求就需要对原有的代码大动干戈,修改的地方越多引入 Bug 的风险就会越大。我们都知道线上的故障有七八成都是由于变更引起的,因此可扩展性强的代码可以有效控制变更的范围。


高鲁棒性


当我们说到代码鲁棒性高的时候,实际就是说代码比较健壮,能够应对各种输入,即便出现异常也会有对应的异常处理机制进行响应而不至于直接崩溃。而项目开发不是一个人的工作,通常都是团队合作,因此我们写的代码无时无刻不在和别人的代码进行交互,所以我们负责的代码模块总是在处理可能正常可能异常的输入。如果不能对可能出现的异常输入进行妥善的防御性处理,那么可能就会造成 Bug 的产生,严重情况下甚至会影响系统正常运行。因此好的代码除了方便扩展方便维护之外,它必定也是高鲁棒性的,否则如果每天 Bug 满天飞,哪有时间和精力去琢磨代码的可扩展性,大部分精力都用来修复 Bug,长此以往自己也会感觉身心俱疲,总是感觉自己没什么成长。


如何写出好代码?


强烈内在驱动


为什么我把强烈的内在驱动摆在首要位置,主要是因为我觉得程序员只有有了想把代码写好的愿望,才能真正驱动自己写出来好代码。否则即便掌握了各种设计原则以及优化技巧,但是自己没有写好代码的内在驱动,总是觉得程序又不是不能用,或者觉得代码和自己有一个能跑就行,亦或是抱着后面有时间再优化的态度(基本是没时间)是不可能写好代码的。因此首先我们得有写好代码的内在驱动和愿望,我们才能有把代码写好的可能。不过话又说回来,内在驱动是基础,全是感情没有技巧肯定也不行。


沉淀业务模型


谈完了内在驱动这个感情,我们就要来看看要掌握哪些技巧才能帮助我们写出来好代码,首当其冲的就是业务领域模型,因为它是领域业务在工程代码中的落地也是整个服务的核心,不过遗憾的是很多同学并没有意识到它的重要性,甚至经常会把数据模型和业务模型相混淆。而我自己在在团队中落地 DDD 领域驱动设计的时候,被技术同学问过比较多的问题就是数据库表对应的数据实体满足不了业务需要吗?为什么还需要业务领域模型?那么想要回答这些问题,我们得先搞清楚到底什么是领域模型,它到底能给技术团队带来什么。


从本质上来说领域模型就是我们对于本行业业务领域的认知,体现了你对行业认知的沉淀以及外化表现。那么怎么体现你对行业领域业务认知的深度呢?领域模型就是很好的验证手段,对行业认知越深刻的同学构建的领域模型越能够刻画现实中的业务场景,我们也可以认为领域模型是现实世界业务场景到代码世界的映射,同时它也是公司重要的业务资产。那么每个行业的业务认知又是从哪里来的呢?实际上就从实际的业务场景中抽象出来的。所以领域模型的建立通常都是伴随着业务需求的出现。因此领域模型是核心,包含了业务概念以及概念之间的关系,它可以帮助团队统一认识以及指导设计。



但是领域建模具有一定的门槛,其中包含了很多难以理解的概念,这也造成了在很多技术团队中难以落地。但是在阿里等国内一线互联网公司却有着广泛的应用,因为 DDD 领域驱动设计可以指导我们应对复杂系统的设计开发,控制系统复杂度,帮助我们划分业务域,将业务模型域实现细节相分离。所以慕枫觉得让大家认识到 DDD 领域驱动设计以及领域模型的的重要性比如何玩转 DDD 本身更加重要。



另外在这里不得不提一下数据模型和领域模型的区别,在实际的工作中我发现很多同学都容易将这两者混淆。领域模型关注的是业务场景下的领域知识,是业务需求中概念以及概念之间的关系,它的存在就是显示的精确的表达业务语义。而数据模型关注的是业务数据如何存储,如何扩展以及如何操作性能更高。因此他们关注的层面不同,领域模型关注业务,数据模型关心实现。


这里可以举个例子给大家说明一下,假设有这样的业务场景,告警规则中存在一个规则范围的概念,主要可以给出不同的告警取值判断的范围,比如某个接口调用次数失败的最大值,或者设备在线数量不能低于某个最小值等等,因此有了如下简化版本的领域模型。



那么在实际实现落地的时候,就很自然想到将 AlarmRule 以及 RuleRange 分别用一个表进行进行存储。这其实就是把领域模型和数据模型混淆的典型例子,实际上我们没有必要搞两张表来存储,一张表其实就够了,主要有以下两个原因:


1、写代码的时候我们维护一张表肯定比维护两张表操作起来更加方便;


2、另外万一后面 ruleRange 有新的变化,增减了新的判断条件,我们还得要修改 rule_ranged 字段,不利于后期的扩展。



因此我们用一张表来就进行存储就好了,多一个 json 类型的字段,专门存储阈值判断范围。只不过在领域模型中我们需要把 c_rule_range 定义为一个对象,这样在代码层面操作起来比较方便。



牢记设计原则


无论设计原则还是设计模式,都是先驱们在以往大量软件设计开发实践中总结出来的宝贵经验,因此我们在项目开发中完全可以站在巨人的肩膀上利用这些设计原则指导我们进行编码。当然如果我们想熟练使用这些设计原则,就必须先要理解他们,搞清楚这些设计原则到底是为了解决什么问题而产生的。


我们不妨仔细想一想,平日时间里技术同学的开发工作基本上都是在已有的服务中进行新需求开发或者在原有的逻辑中修修改改。因此如果因为一个需求需要修改原有代码逻辑,我们总是希望修改的地方越少越好,否则如果修改的地方多了,那么引入的 Bug 风险就会越大。即便是项目需要进行重构的情况,那我们也希望重构后的服务或者组件可以满足高内聚低耦合的大要求,这样在未来进行需求开发的时候可以更加方便的进行修改。这也是我们希望我们开发的代码高内聚低耦合的原因。可以看得出来,设计原则的核心思想就是帮助技术人员开发的软件平台能够更好地应对各种各样的需求变化,从而最终达到降低维护成本,提高工作效率的目的。


当我们说到设计原则的时候,通常都会想到 SOLID 五大原则,这里所说的设计原则主要包括 SOLID 原则、迪米特法则。


单一职责原则


对于一个方法、类或者模块来说,它的职责应该是单一的,方法、类或者模块应该只负责处理一个业务。这个原则应该很好理解,当我们在写代码的时候,无论是方法、类以及模块都应该从功能或者业务的角度考虑将无关的逻辑抽离出去。为什么这么做呢?主要还是为了能够实现代码业务功能的原子化操作,这样即便未来进行修改的时候影响的范围也会变得有限。如果我们不遵守单一职责原则,那么在修改代码逻辑的时候很可能影响了其他业务的逻辑,造成修改影响范围不可控的情况。



You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.



不过需要说明的是,这里的所说的单一职责是针对当前的业务场景来说的,也许随着业务的发展和场景的扩充,原来满足单一职责的方法、类或者模块可能现在就不满足了需要进一步的拆分细化。


开闭原则


慕枫认为开闭原则与其说它是一种设计原则,不如说它是一种软件设计指导思想。无论我们编写框架代码还是业务代码都可以在开闭原则这样的核心思想指导下进行设计。



Software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。



所谓开闭原则指的就是我们开发的框架、模块以及类等软件实体应该对扩展开放,对修改关闭。这个原则看上去很容易理解,但是在进行项目实际落地的时候却不是一件容易的事情。因为对于扩展以及修改并没有明确的定义,到底什么样的代码才是扩展,什么样的代码才是修改?这些问题不搞清楚的话,我们很难把开闭原则落地到实际的项目开发中。


结合自己的开发经验可以这么理解,假设我们在项目中开发一个功能的时候,如果能做到不修改已有代码逻辑,而是在原有代码结构中扩展新的模块、类或者方法的话,那么我们认为代码是䄦开闭原则的。当然这也不是绝对的,比如假设你修改一个原有逻辑中的判断条件的阈值,那只能在原有代码逻辑中进行修改。总不能因为要满足这个原则非要搞出来。所以我觉得我们不必要教条的去追求满足开闭原则,而是从大方向上以及整体上考虑满足开闭原则。


里氏替换原则


在面向对象思想构建的程序中,子类对象可以替换程序中任何地方出现的父类对象,同时还能保证程序的逻辑不变以及正确性不变,这就是里氏替换原则的字面理解。不知道大家有没有发现,这个里氏替换原则看上去和 Java 中的多态一样一样的。实际上他们还是有区别的,多态是面向对象编程的特性,是重要的代码实现思路。而里氏替换原则是一种设计原则,约定子类不能破坏父类定义好的逻辑以及异常处理。


比如在仓储业务域中,父类中有对拣货任务进行排序的 sortPickingTaskByTime()方法,它是按照任务创建的时间对到来的拣货任务进行排序,那么我们在子类实现的时候如果在 sortPickingTaskByTime()方法内部按照拣货任务涉及的商品品类进行排序,那么明显是不符合里氏替换原则的,但是从多态的角度来说或者从语法的角度来说却没有问题。


里氏替换原则的核心思想就是按照约定办事,父类约定好了的行为,子类实现需要严格遵守。那么里氏替换原则对于实际编码有什么指导意义呢?比如上文所说的 sortPickingTaskByTime()排序方法,如果父类中的算法实现效率不高,我们可以在子类中进行优化,有了里氏替换原则就可以通过子类改进当前已有的实现。另外父类中的方法定义就是契约,可以指导我们后面的编码。


接口隔离原则


所谓接口隔离说的是接口调用方不应该被迫依赖它不需要的接口。怎么理解这句话呢?按照慕枫自己的理解,接口调用方只关心和自己业务相关的接口,其他不相关的接口应该隔离到其他接口中。



Clients should not be forced to depend upon interfaces that they do not use。



从扩展能力层面来看,我们定义接口的时候按照原子能力进行定义,避免了定义一个大而全的接口,这样在进行扩展的时候就可以按照具体的原子能力来进行,这样无论是灵活性还是通用性上面都会更加满足需求。


从实现上来说,如果实现方仅仅需要实现它以来的接口功能就好,它不需要的接口功能就不需要实现,这样也会大大降低代码实现量。当我们扩展或者修改代码的时候能够做到最小化的修改。


依赖倒置原则                                                                                      依赖倒置原则不太容易理解,但是我们在实际的项目开发中却每一天都在使用,只是我们可能没太在意罢了。                  



High-level modules shouldn't depend on low-level modules. Both modules shoud depend on abstractions.In addition,abstractions shouldn't depend on details.Details depend on abstractions.



按照字面意思理解,高层级模块不应该依赖低层级模块,同时两者都应该依赖于抽象。另外抽象不应该依赖于细节,细节应该依赖于抽象。用大白话来说主要是两个核心点,一是面向接口编程,另一个是基础层依赖核心层。


面向接口编程这个应该很好理解,因为接口定义了清晰的协议规范,研发同学可以基于接口进行开发。



                                                                     


迪米特法则                                                                                         


迪米特法则看名字是一点不知道它是干什么的,简单来说就是类和类之间能不要有关系就不要有关系,实在没办法必须要有关系的那也尽量只依赖必要的接口。这样说起来感觉还是比较抽象。看下面的图就明白了,左边的各个模块拆分比较独立,符合单一职责原则,同时模块间只依赖它所需要的模块,而下图右边的模块拆分不够独立,A 模块本来只需要依赖 F 模块,但是 FG 模块颗粒度较大,导致不得不依赖 G 模块的接口,显然这是不符合迪米特法则的。                                                                                            



当我们有了写出来的代码能够实现高内聚低耦合、易扩展以及易维护愿景之后,那就要好好学习一些代码实现的设计原则,这些设计原则在战略层面可以指导我们扩展性强的代码应该往哪些方向进行设计考虑。而有了指导思想之后,结合不同场景下的设计模式就自然催生出来我们想要的结果。



运用设计模式


设计模式是先驱们在实践的基础上总结出来可以落地的代码实现模板,针对一些业务场景提供代码级解决方案。我们根据各个设计模式的能力特点可以将 23 种设计模式分类为创建型模式、结构型模式以及行为型模式。这里不再对设计模式进行展开说明,后面有时间可以写系列文章专门进行介绍。不过我们需要清楚的是这 23 种设计模式就是程序员写代码打天下的招式,而提升代码扩展性才是最终目的。



面向失败编码


代码中的异常处理往往最能体现技术同学的编码功力。完成一个需求并不难,但是能够考虑到各种异常情况,在异常发生的时候依然可以得到预想输出的代码,却不是每个程序员都能写出来的。  因此无论是写代码还是系统设计,都要有面向失败进行设计的意识,每一个业务流程都要考虑如果失败了应该怎么办,尽可能考虑周全可能会出现的意外情况,同时针对这些意外情况设计相应的兜底措施,以实现防御性编码。


这里假设有这样的业务场景,当我们的业务中有调用外部服务接口的逻辑,那么我们在编写这部分代码的时候就需要考虑面向失败进行编码。因为调用外部接口有可能成功,有可能失败。如果接口调用成功自然没什么好说的,继续执行后续的业务逻辑就好。但是如果调用失败了怎么办,是直接将调用异常返回还是进行重试,如果重试还是失败应该怎么办,需不需要设计下重试的策略,比如连续重试三次都失败的话,后续间隔固定时间再进行重试等等。当然我们并不需要在每个这样的业务流程中这么做,在一些比较核心的业务链路中不能出错的流程中要有兜底措施。



总结


本文主要从理论层面为大家介绍写好代码的需要哪些知识储备,下一篇会从具体业务场景出发,具体实操怎么结合这些理论知识来把代码写好。不过我们必须认识到好代码是需要不断打磨的,并非一朝一夕就能练就,总是需要在不断的实践,不断的思考,不断的体会以及不断的沉淀中实现代码能力的提升。左手设计原则,右手设计模式,心中领域模型再加上强烈的内在驱动,我相信我们有信心一定可以写出一手好代码。


作者:慕枫技术笔记
来源:juejin.cn/post/7257518360099405883
收起阅读 »

技术人如何快速融入团队?

写在前面:文末「拓展阅读」的两篇文章写得很好,提纲挈领,推荐阅读。本文偏个人感悟,教学不敢说,日后若有更深刻的感悟会再重新整理。 很多人在进入新团队时会焦虑,害怕做不好,害怕才不配位,不知道如何开展工作。这是一种正常现象,因为针对「融入团队」这件事,我们没有...
继续阅读 »

写在前面:文末「拓展阅读」的两篇文章写得很好,提纲挈领,推荐阅读。本文偏个人感悟,教学不敢说,日后若有更深刻的感悟会再重新整理。



很多人在进入新团队时会焦虑,害怕做不好,害怕才不配位,不知道如何开展工作。这是一种正常现象,因为针对「融入团队」这件事,我们没有刻意练习,没有找到一套行之有效的方法。


下面,我将结合《程序员的底层思维》 这本书介绍的方法,以及个人实践经验,来聊聊如何快速融入团队。


本文适合有 3 年以上的技术工作者阅读,低年限或者非技术同学也有一定的参考意义。


工作拆解


对于一个企业而言,核心组成要素无非就是人、业务、技术、文化。因此工作的开展可以从这四个角度出发,并逐层拆解,力争从陌生变熟悉。





目标:熟悉组织结构、人员分工,并与未来可能有合作关系的人建立关系。


行动:



  1. 了解组织结构

  2. 了解人员分工

  3. 建立关系




业务


目标:熟悉业务,对产品定位、用户人群、行业现状有一定了解。


行动:



  1. 了解业务现状

  2. 梳理业务流程

  3. 理解用户




技术


目标:熟悉团队技术现状,方便后续开展工作



切勿一上来就高谈阔论、方法论,推翻重构,对过往保持敬畏。



行动:



  1. 熟悉架构,包括系统架构、领域模型、代码结构

  2. 了解研发流程,从一个小需求入手,掌握相关的流程和权限

  3. 先小后大,以点破面。从小点突破,比如性能优化,先拿到业绩,再准备大的规划。




文化


目标:熟悉企业文化


行动:



  1. 理解公司使命

  2. 理解业务愿景

  3. 理解公司价值观,并做到知行合一




心态调整



不着急,不害怕,不要脸 — 冯唐《冯唐成事心法》



上一章讲的是「术」,是方法论,但光会「术」有可能会碰壁,因为心态问题。



  • 不着急:每个人到一个新团队,总想着快速理解业务、快速出成绩,来证明自己的价值。可以理解,但是不必着急,多给自己和他人一些时间,做好规划,安排好时间尽力而为即可,切勿急功近利。

  • 不害怕:不害怕事情失败,培养成长性思维,相信明天的自己比今天更优秀。记住一句话:成功是一时的,成长是一辈子的;还有一句老话:失败是成功之母。大不了,重头再来。

  • 不要脸:不怕丢脸、不怕打脸。很多人进入新团队,不敢发言不敢提问,殊不知这是露脸的好机会,可以让更多人更快地认识自己。还有一种是怕向年龄或资历更小的人提问,觉得丢人,选择自己研究导致浪费时间。孔子有云 “不耻下问”,改变心态,对方对某块事物的理解就是比自己熟,不害怕提问,帮助自己更快地获取知识并融入团队。


以上,不着急、不害怕、不要脸,改变心态,方能更好的融入。


总结


融入团队是需要刻意练习的。


先调整心态,不着急、不害怕、不要脸。


再逐步拆解工作,按人、业务、技术、文化四个方向开展。


最后会发现,「融入团队」这件事,其实和做题一样简单,唯一的变量,也就是人而已。



作者:francecil
来源:juejin.cn/post/7257774805431877689

收起阅读 »

一名(陷入Android无法自拔的)大二狗的年中总结

前言 大家好,我是入行没多久,不会写代码的RQ。一名来自双非学校的大二狗。2023时间已经过去了一半,我的大学生活也过去了一半。借着这个2023年中总结的话题我也想给我的前半段大学生活做一个总结和记录(希望大家原谅理工男的表达能力,已经在学着写博客了🙃)。 大...
继续阅读 »

前言


大家好,我是入行没多久,不会写代码的RQ。一名来自双非学校的大二狗。2023时间已经过去了一半,我的大学生活也过去了一半。借着这个2023年中总结的话题我也想给我的前半段大学生活做一个总结和记录(希望大家原谅理工男的表达能力,已经在学着写博客了🙃)。


大学之前


大学之前,我的高中初中都是在一个小乡镇度过,每天都是过着教室、食堂、厕所三点一线的生活。可能偶尔会和几个好兄弟打打球,开开黑。那时候一心只读圣贤书,从未碰过电脑(也只有偶尔去网吧玩玩电脑游戏),也未曾了解过任何跟代码相关的东西。只有在高三快毕业了,学校进行志愿填报培训的时候,我才在想我想干什么。


qq_pic_merged_1689681281489.jpg


我想学编程,我想搞钱,我要成为编程高手!!哈哈哈,当时的确是这么想的,因为我一直都觉得会电脑,会编程的“大黑客”很酷!。然而结果是


qq_pic_merged_1689681309027.jpg


当时我还一时兴起,在京东上买了本0基础学python的书:


IMG_20230718_152056.jpg


奈何高三学业繁忙没时间看,而且也没有电脑实操,看了十几页压根不知道在讲什么,之后这本书也就放着吃灰了。现在看来当时确实挺傻×的。后来上了大学,自学了python,这本书也就送给了室友。


高三的时候大家都想着我要上某某985!我要上某某211!当然我也不例外。然而等到高考出分,才知道现实是多么残酷。我的高考成绩也只够报一个末流的211,报不了什么985。思虑最终我报了一个专业性比较强的双非计算机。因为我觉得一个专业不对口的末流211不如一个专业好的双非。


上大学前我是保持怀疑的,我没有任何相关编程经验,甚至是接触电脑的机会都少。不过幸运的是家里人都支持我,给我买了一台不错的笔记本。那个暑假我加入了我们学校的新生群,我发现原来大家都是卷王。有初中就开始接触编程的,有高中就学完的java的,有暑假已经快把c语言学完了的。为了不落后,高考完的那个后半个暑假我也在偷偷学c语言,能力有限,到开学也才学到指针多一点点(指针这个东西对于当时的我简直就是噩梦)。


大一


大一开学后,我同大多数人一样,满怀期待地踏进了向往的大学生活。在第一次年级集中会上,我收到了一份宣传单。那是一份我们学校的一个互联网组织的宣传单。分有产品、视觉、后端、移动、前端、运维几个部门。听说里面全是编程大牛,学校里顶尖技术人员的集聚地。这不就是我想成为的人吗?于是我下定决心我要加入他们。


大一的时候大部分课余时间都花在了这个叫做红岩网校工作站的课程上面。大一上半个学期学会了javase,下半个学期开始学写APP,会写几个简单的Activity页面,当时我还写了个整蛊APP(只是简单将声音放到最大然后播放整蛊音乐lost-rivers。哈哈哈这个不提倡,小心被打)。当然学校课程我也没有忘记,我记得c语言期末大作业自己写了个贪吃蛇和俄罗斯方块:


image.png


image.png


一行一行敲了八九百行,对于当时还是编程小白的我是个不小的成就了。


后来的一整个寒假都在写我们移动开发部的寒假考核,也是我人生中的第一个项目--彩云天气app(地址就不贴了,现在看来写的代码就是💩)。


大一的下学期,开学自学了Kotlin语言,从此再也不想用java了😭。之后也是按照网校的课程学了jetpack、rxjava、retrofit、MvvM等等。到了五一,写了自己的第二个项目--星球app(时间管理类app),也是网校的期中考核(当然也顺利通过啦~)。后面自学了python,简单写了一个抢课的脚本(以后再也不怕抢不到课了😭)。之后自己租了个服务器用python搭了个QQ机器人,后面搞到网校招新群里去玩了。不得不说Bot社区真的不错,文档什么的都很完善,对QQ机器人感兴趣的可以试试(概览 | Bot (baka.icu))。


大一的暑假,我留在了学校参加了网校的暑期培训。培训期间简单研究了一下Android性能优化跟LeakCanary,然后也是写了自己的第三个项目:开眼APP(RQ527/KaiYan,图片可能寄掉了。)


最终呢也是没有辜负自己的努力通过了最终的考核成为了网校的干事:


mmexport1689672953594.jpg

总的来说,大一学年算是踏入了编程的门吧,没有在荒废中度过。同时也要感谢网校给了我这个机会😁。


大二


大二的课余时间主要都花在了给移动开发部门培养新血液的事情上面。因为我的上一届也就是带我们的学长他们大三了,准备考研的考研,就业的就业,自然教学的任务就落到了我们头上。期间上了三节课,我发现给他们上课的同时也是给我自己上课。学习一个东西最有效的方式就是给别人讲懂。


这是大二刚开学的宣讲会😁:


IMG_20221003_185411.jpg


1664878518099.jpeg


大二期间我还了解了一下ktor和compose,嗯~,不算深入吧,简单写了几个demo。
同时自己也接手了一个多人项目,跟我们部门的另外一个人写一个类似于微博投票表决的项目,不过还没上线。


下半个学期自己用hexo+butterfly搭了个个人博客网站:rq527.github.io (还没钱买域名,暂时先用github吧😭),页面大概长这样:


image-20230719144318074


image-20230719144342289


个人思考


我认识到了什么



  • 接受自己的平庸,接受任何方面的平庸。

  • 永远不要斤斤计较

  • 杜绝一分钟热度,永远保持一颗热忱的心

  • 打铁还需自身硬

    从入行Android 开发以来,网上很多人都说 “Android 开发早就凉了,现在就是死路一条”,“现在学Android就是49年入国军!”等等。但是我身边同行的人还不是能找到实习,找到工作。我的意思是,什么事情都是需要自己有实力。





说实话,上了大学我最痛惜的是那些曾经交好的朋友也逐渐不联系了,一张通知书撕裂了一群人,以后再见也不知道是什么时候了。


未来的事情



  • 管理移动开发部

  • 找实习(目标是进大厂)


盘点一下要做的事情,发现太多了,主要的方向是这两个。人外有人,天外有天,比你牛逼的人还有很多,一直保持学习吧🤕!


最后


最后我想说很感谢家里人的支持,他们没有说反对我,强制要求我当老师,当警察等等,而是支持我所做的一切。同时也很感谢那个她,陪我一起成长,学习,愿意和我分享快乐,听我诉说(世上最幸运的事情莫过于此了吧😁)。也很感谢网校给我这么一个平台,让我认识了很多志

作者:RQ527
来源:juejin.cn/post/7257056512610517048
同道合的兄弟和伙伴。

收起阅读 »

前端:需要掌握哪些技能才能找到满意的工作?

如果你在找前端工作,你一定求助过不少大佬传授找工作和面试经验,而你得到的答案肯定很多时候就是简单的一句话:把 html、css、 js 基础学扎实,再掌握vue或react前端框架之一就可以了。 真的是这样吗?技术上看似乎没问题,但是找工作不只要从技术上下手,...
继续阅读 »

如果你在找前端工作,你一定求助过不少大佬传授找工作和面试经验,而你得到的答案肯定很多时候就是简单的一句话:

把 html、css、 js 基础学扎实,再掌握vue或react前端框架之一就可以了。

真的是这样吗?技术上看似乎没问题,但是找工作不只要从技术上下手,还要从个人目标和公司的招人标准综合进行考量,然后你还需要掌握一套有逻辑、有结构的面试回答技巧。接下来我们逐一分析一下,相信你看完之后就有了方向和方法,一定能找到满意的工作。

个人目标

现在我们的教育并没有太着重于个人目标和职业规划的设定,但找工作与其关系特别大。如果你想找一个大厂,那么准备方向就跟创业公司完全不一样。我们分别来看一下这两种情况。

大厂

大厂可能更看重你的 htmlcss 和 JavaScript 基础,以及数据结构、算法和计算机网络。你的准备方向就应该是这些基础方面的东西。另外还有一些原理方面的知道,比如你要做 vue 或者 react 开发,那就要知道 virtual dom 和 diff 算法的原理。

创业公司

如果你的目标是创业公司(这种公司的发展前景不可预测,可能大展宏图,也可能半途而废),你需要有大量的实战经验,因为创业公司为了抢占市场,产品的开发进度一般都会特别紧张,你需要去了就能够立刻干活;而理论方面的东西则会关注的少一些。针对面试,你需要去准备相关技术(比如 React 或 Vue) 的实战项目经验。

所以要想知道学到什么程度才能去找工作,首先得明确一下你的目标,是想去大厂,还是去创业公司,然后分别进行准备和突破。

公司要求

接下来再看一下公司的招聘要求,好多公司都写的特别专业、全面,除了基本语法、框架外,还要求有兼容性调整、性能优化、可视化经验,或者是掌握一些小众框架。这些招聘信息其实描述的是最佳人选,几乎在100个里面才能挑出1个来,而这种大牛级别的人自己也向往更好的工作机会,所以可能根本不会跟你有竞争关系。公司这么写招聘要求目的只有一个,就是找一个技能越全的人越好。

事实上,你只需满足要求的百分之80%,70%,甚至 50% 都有可能获得这份工作机会,因为面试不光看技术,还要看眼缘、人缘:如果面试官觉得你们投缘的话,你即使有不会的问题,他也会主动引导你帮你回答上来;要是不投缘(有些比较250的面试官),那就算你会的再多,他也会觉得你很菜(你不懂他懂的)。所以说那些招聘要求就只作为参考就好了,可以作为你以后的学习路线。不过这些技能还是掌握的越多越好,技多不压身,你可以一边面试一边准备,这样也不会互相影响。

技术能力

分析完外界的因素之后,来看一下咱们需要具体掌握哪些技术。

基础

作为一名前端工程师,htmlcssJavaScript 基础是一定要掌握牢固的,所有的语法点都必须要掌握,然后还要熟识面试必考的题,比如 ES6 及后面的新特性原型链Event Loop 等等。这些不是从学校学来的,而是为了面试专门突击准备的,需要反复的去看,去研究,最后把它们理解并记住。

框架

掌握这些基础之后,就需要看一下前端比较火爆的框架,react 和 vue。大厂用 React 的比较多,中小型公司用 vue 的比较多,当然这也不是绝对的。据我目前的经验来看,React 的薪水还是比较高的,不过看你自己喜好,喜欢做什么就做什么,从这两个框架中选一个深入去学,后面有时间再去研究另外一个。具体学习和准备方法可以

  • 先学基础用法,再学高级用法,最后掌握框架原理,比如:React / Vue,Redux / Vuex ,因为面试官通常喜欢问这方面的问题。针对这些一定要去看看别人的总结,然后自己研究一下,会更容易理解并记住。了解原理后,有时间再去研究一下源码,对于面试会更有帮助。
  • 理论准备完之后,实战肯定也少不了,无论是校招还是社招,无论是面大厂还是面小厂,都需要应聘者有实战经验。因为光会纸上谈兵,编码能力不够也不会有公司愿意去培养。实战就建议大家自己去网上找一些项目的灵感,然后动手去做一下。刚开始可能会觉得自己技术不够,也没有一个全局的概念,这些都是正常的过程,可以跟一些课程或者书籍,或者是网上的一些资源,学习一下,免费或收费的都可以。收费的好处就是它有一个完整的体系,让你从全局上有一条路径顺着走下去,就能完成一个目标。而免费资源需要你有充裕的时间,因为在遇到问题的时候,需要你一点一点去研究。不过在完成之后,回顾一下你的项目开发过程,也会在脑子里形成体系,再把之前看过的所有资料整理一下,也就学会了,只是时间上会比较长。
  • 有些公司的实战经验要求的比较丰富,比如兼容性调整和性能优化。这种经验就需要你在开发项目中,刻意去创造问题的场景,然后解决它。比如说兼容性调整,你就得在项目中体验一下不同浏览器对于JS和CSS 特性的支持程度,然后按需调整。而性能优化则就需要从网络请求、图片加载、动画和代码执行效率下手。

这些你搞懂了之后,基本上百分之七八十的公司都可以面过去。

软技能

上面说的是必备的硬性技术技能,还有一些必要的软技能,用以展示个人性格和工作能力。最重要的一项软技能是沟通能力。

沟通能力

沟通能力,对于面试或是汇报工作都是必须的。它跟你的自信程度是完全挂钩的,你只有自信之后才能有更好的沟通和表达能力,如果唯唯诺诺,低三下四,那么在面试或汇报工作的时候就会支支吾吾,颠三倒四。

举个例子:好多人,包括我本人,在面试的时候都会紧张,而我又属于那种特别紧张的,有些技术可能本来是熟悉的,但面试的时候人家换一个问法、或者气氛比较紧张的话,大脑就会一片空白,想说也说不出来,特别吃亏。要解决这个问题,**就要相信自己就是什么都会,面试官也不见得比自己会的多,然后面试前事先准备好常见面试题的答案,以及过往的工作经验,可以极大的增加自信。**当准备面试题的时候,可以采用框架的形式进行组织,下边介绍两个常用框架用来回答工作经验类和原理类的问题。

STAR 框架

对于工作经验相关的问题,可以使用框架组织回答,比如亚马逊北美那边面试会提前会告诉你,用一个叫STAR的框架回答问题:

  • S 是说 situation,事件/问题发生的场景。
  • T 指的是 task,在这个场景下你要解决的问题或者要完成的任务。
  • A 是 action,行动,要解决上边那些 tasks,你需要付出哪些行动?比如说第1步先去调试代码,然后第2步再去检查一下哪个变量出问题了,描述清楚每一步行动。
  • R 是 result,结果,这些行动有了什么样的结果,是成功了还是失败了,对你来说有什么帮助或者增长了什么教训。又或者往大里了说,给公司带来了什么效益。
    这样一整套就比较有逻辑。

原理回答框架

再说原理概念类的问题的回答,也是要有一套逻辑的,就比如说解释一下某某技术的工作原理,那么你要:

  • 解释一下这个技术是干什么的(What)。
  • 它有什么好处(Why)。
  • 分析一下这个技术内部用了哪些核心算法或机制,从外到里,或者由浅入深的给它剖析出来。如果是能拆解的技术,那就把每个部分或者组件的作用简单的描述一下(How)。
  • 最后再给他总结一下这个技术的核心部分。
    例如,你要回答 react 工作原理的问题:
  • 可以先说一下 React 是做什么的它是一个构建用户界面的库。
  • 然后它使用了(从浅一点的方面) virtual dom 把组件结构放在了内存中,用于提升性能。
  • 组件刷新的时候又使用了 diff 算法,根据状态的变化去寻找并更新受影响的组件(然后继续深入 diff 算法…)。
  • 再底层一些, React 分为了 React 核心和 React-dom,核心负责维护组件结构,React-dom 负责组件渲染,而 React 核心又使用了 Fiber 架构等等等等。
  • 如果你深入阅读过它的源代码也可以再结合源码给面试官详细介绍一下,最后再总结一下 react 加载组件、渲染组件和更新组件的过程,这个就是它的工作原理。

总结

这些就是前端工程师要学到什么程度才能去找工作、以及怎么找工作的一些个人看法。你需要:

设定个人目标。
辩证看待公司的招聘要求。
掌握硬技能和软技能(沟通能力)。
使用 STAR 框架和 WWH 框架组织面试回答。
按照这些方向去准备的话,一定可以会找到满意的工作。如果找到了还请记得回来炫耀一下,如果觉得文章有帮助请点个赞吧~感谢!


作者:江咏之
链接:https://juejin.cn/post/7234028496087056445
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »