2023年2月

在我们基于框架开发系统的时候,往往对一些应用场景的页面对进行了归纳总结,因此对大多数情况下的页面呈现逻辑都做了清晰的分析,因此在我们基于框架的基础上,增量式开发业务功能的时候,能够事半功倍。代码生成工具Database2Sharp承载着我们各种开发框架的快速开发逻辑,包括界面代码的生成、后端代码的生成等内容,本篇随笔介绍在这个基础上,增加Vue&Element 工作流页面的快速生成,以便减轻我们实际开发工作流页面的繁琐工作。

1、工作流查看、编辑页面的组件动态化

在我的随笔《
基于Vue的工作流项目模块中,使用动态组件的方式统一呈现不同表单数据的处理方式
》中曾经介绍过,由于我们动态挂载了工作流的查看页面、编辑页面,因此我们可以根据工作流表单的属性,来动态呈现所需要的页面内容,也就是组件动态化的处理方式。也就是类似我们下图所示

除了查看表单、编辑表单,还有一个对具体业务申请单的查询页面,因此一个工作流表单,包含了三个特定功能的页面,如下所示。

申请表单查看的实际效果界面如下所示。

创建具体的表单的时候,根据表单的编辑界面,录入不同的流程申请单的数据,以及附件、清单、流程用户等信息。

不同的表单,有不同的查询界面,可以提供更加进行的业务表单数据查询或者统计处理,如对于付款申请单,我们提供一个付款申请单的分页查询页面。

这些页面的内容,我们在项目框架中存放的位置如下所示。

2、结合代码生成工具快速生成页面代码

根据上面的规则,我们可以使用代码生成工具,根据数据库的信息,结合页面的呈现需要,把查看申请单、创建/编辑申请单、列表查询几个页面的内容,通过代码生成工具Database2Sharp来快速生成,只需要根据需要定义好页面模板即可。

这里的ABP的Vue &Element界面代码,即可以用来生成基于ABP后端的Vue&Element前端界面代码,也可以基于Bootstrap&VUE框架的Web API前端界面代码,我们在这里生成包含常规页面的api+views视图代码外,同时也生成工作流模块所需的三类页面代码,生成的代码只需要简单的在VSCode中进行增量式的合并即可使用,节省了非常多的前端代码编写或者裁剪工作。

我们看到生成的工作流模块内容,已经根据edit /view / list几个目录进行了区分,如下所示。

这样我们把生成的页面,复制到对应的项目框架目录中即可。

其中对于对于主从表单的处理,我们可以通过利用Vxe-table插件的方式直接录入数据的方式进行录入 。通过代码生成工具,也是根据关联表的信息,我们同时生成所需要的从表信息展示,供裁剪录入信息处理。

<el-form-itemlabel="明细清单">
  <div>
    <vxe-toolbar>
      <template#buttons>
        <vxe-buttonstatus="primary"content="新增"@click="insertEvent" />
        <vxe-buttonstatus="warning"content="删除"@click="removeSelectEvent" />
        <vxe-buttoncontent="提交"@click="saveEvent" />
      </template>
    </vxe-toolbar>
    <vxe-tableref="xTable"border show-overflow keep-source resizable show-overflow :data="list":edit-config="{trigger: 'click', mode: 'row', showStatus: true}">
      <vxe-columntype="checkbox"width="60" />
      <vxe-columntype="seq"width="60" />
      <vxe-columnfield="feeType"title="费用类型":edit-render="{name: '$select', options: feeTypeList}" />
      <vxe-columnfield="occurTime"title="发生时间":edit-render="{name: '$input', props: {type: 'date', placeholder: '请选择日期'}}":formatter="formatDate" />
      <vxe-columnfield="feeAmount"title="费用金额(元)":edit-render="{name: '$input', props: {type: 'float', digits: 2}}" />
      <vxe-columnfield="feeDescription"title="费用说明":edit-render="{name: 'input', attrs: {type: 'text'}}" />
    </vxe-table>
  </div>
</el-form-item>

最终展示的界面效果如下所示。

利用代码生成工具Database2Sharp,我们可以非常细致的定义我们所需要生成的代码逻辑,因此在项目开发中,可以快速生成界面代码以及后端C#的框架处理代码,这样增量式的开发,往往能够做到事半功倍,并且在生成的界面代码里面,我们提供了很多控件的展示例子,供修改调整,灵活度非常好,同时也是我们快速进阶前端和后端开发的试金石。

以及API的生成代码,部分注释供参考。

代码生成工具Database2Sharp为开发效率而生,同时也是开发的好帮手,我们信奉开发中,自己所想要的东西,往往就是大多数开发者所需要的,尽可能的信手拈来。

在ABP开发框架中应用服务层ApplicationService类中,都会提供常见的一些如GetAll、Get、Create、Update、Delete等的标准处理接口,而由于在ApplicationService类定义的时候,都会传入几个不同的类型作为泛型的参数,实现强类型的类型处理,本篇随笔对于分页查询排序的实现处理做一个详细的介绍,介绍其中对分页查询条件的定义,子类应用服务层的条件查询逻辑重写、排序逻辑重写等规则的处理。

1、ApplicationService类的泛型定义

例如我们定义User应用服务层的UserApplicationService的时候,传入了几个不同类型的参数作为基类的泛型约束类型,如下所示。

[AbpAuthorize]public class UserAppService: MyAsyncServiceBase<User, UserDto, long, UserPagedDto, CreateUserDto, UserDto>, IUserAppService

同类型的字典数据应用服务层的定义如下所示,可以看到和UserAppService类似的。

其中MyAsyncServiceBase则是我们自定义的一个基类对象,主要是根据传入不同的参数构造不同的强类型对象返回。

    public abstract class MyAsyncServiceBase<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput, TGetInput, TDeleteInput>: 
AsyncCrudAppService
<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput, TGetInput, TDeleteInput> where TEntity : class, IEntity<TPrimaryKey> where TEntityDto : IEntityDto<TPrimaryKey> where TUpdateInput : IEntityDto<TPrimaryKey> where TGetInput : IEntityDto<TPrimaryKey> where TDeleteInput : IEntityDto<TPrimaryKey>

这里UserApplicationService的服务层中参数的User类,对应是EFCore的领域对象,它的定义如下所示

    public class User : AbpUser<User>

由于User需要集成AbpUser基类的一些特性,因此有继承关系,它主要就是负责和数据库模型打交道的对象。

而如果不是类似User这样系统用到的基类对象,那么我们就需要如下定义,指定表单的名称,以及对象的约束条件了,如下字典的领域对象如下定义所示。

而 MyAsyncServiceBase 的第二个参数则是用于传递的DTO对象,可以认为它和数据库没有直接的关系,不过由于引入了AutoMapper,我们一般看它们的属性还是有很多相同的地方,不过DTO更加面向的是业务界面,而非存储处理。

如果对于一些界面特殊的数据信息,需要转换为领域对象的属性,则需要进行特别的自定义映射处理了。

如User的DTO对象定义如下所示。

而如果我们的DTO对象,不需要利用ABP进行参数内容的约束,那么可以更加简化一些条件,如下字典DTO对象所示。

对于类似下面的字典模块的应用服务层定义

其中第三个参数是主键ID的类型,如果为Int这是整形,这里是字符串类型,因此使用string。

第四个参数DictDataPagedDto就是分页查询的条件 ,这个DTO对象,主要就是获取客户端查询处理的条件的,因此可以根据需要查询的条件进行裁剪,默认利用代码生成工具Database2sharp生成的属性基本上包括了所有的数据库表属性名称了。如字典数据的查询条件比较简单,如下所示,除了包含一些分页条件信息外,就是包含所需要的查询条件属性了。

    /// <summary>
    ///用于根据条件分页查询/// </summary>
    public classDictDataPagedDto : PagedAndSortedInputDto
{
public DictDataPagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictDataPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public DictDataPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

2、分页查询排序的实现处理

前面我们介绍了应用服务层中利用泛型基类的参数定义,可以强类型返回各项不同数据接口,这种就是非常弹性化的设计模式了。

ABP+Swagger负责API接口的开发和公布,如下是API接口的管理界面。

进一步查看GetAll的API接口说明,我们可以看到对应的条件参数,如下所示。

这些是作为查询条件的处理,用来给后端获取对应的条件信息,从而过滤返回的数据记录的。

那么我们前端界面也需要根据这些参数来构造查询界面,我们可以通过部分条件进行处理即可,其中MaxResultCount和SkipCount是用于分页定位的参数。

我们来看看基类对于查询分页排序的处理函数,从而了解它的处理规则。

        public virtual async Task<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input)
{
//判断权限 CheckGetAllPermission();//获取分页查询的条件 var query =CreateFilteredQuery(input);//根据条件获取所有记录数 var totalCount = awaitAsyncQueryableExecuter.CountAsync(query);//对查询内容排序和分页 query =ApplySorting(query, input);
query
=ApplyPaging(query, input);//返回领域实体对象 var entities = awaitAsyncQueryableExecuter.ToListAsync(query);//构造返回结果集,并转换实体类为DTO对应 return new PagedResultDto<TEntityDto>(
totalCount,
entities.Select(MapToEntityDto).ToList()
);
}

其中 CreateFilteredQuery 、ApplySorting和 ApplyPaging 都是利用可以子类重写的函数实现弹性化的逻辑调整处理。

在基类中,默认的CreateFilteredQuery 提供了简单的返回所有列表的处理,并不处理查询条件,这个具体的条件过滤由子类实现逻辑的。

protected virtual IQueryable<TEntity>CreateFilteredQuery(TGetAllInput input)
{
returnRepository.GetAll();
}

而列表排序处理ApplySorting的基类函数,基类提供了标准的对Sorting 属性进行条件排序,否则就根据主键ID进行倒序排序处理,如下代码所示。

protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity>query, TGetAllInput input)
{
//Try to sort query if available var sortInput = input asISortedResultRequest;if (sortInput != null)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
returnquery.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used. if (input isILimitedResultRequest)
{
return query.OrderByDescending(e =>e.Id);
}
//No sorting returnquery;
}

而基类的分页的处理ApplyPaging逻辑,主要就是转换为标准的接口进行处理,如下代码所示。

protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity>query, TGetAllInput input)
{
//Try to use paging if available var pagedInput = input asIPagedResultRequest;if (pagedInput != null)
{
returnquery.PageBy(pagedInput);
}
//Try to limit query result if available var limitedInput = input asILimitedResultRequest;if (limitedInput != null)
{
returnquery.Take(limitedInput.MaxResultCount);
}
//No paging returnquery;
}

以上是标准基类提供的几个可以重写的默认实现,一般来说,我们会通过子类重写逻辑实现的方式进行逻辑重写的。

如对于字典模块的条件信息,我们可以进行重写,以便实现自定义的条件查询处理,如下DictDataAppService应用服务层的重写处理。

/// <summary>
///自定义条件处理/// </summary>
/// <param name="input"></param>
/// <returns></returns>
protected override IQueryable<DictData>CreateFilteredQuery(DictDataPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIf(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!string.IsNullOrEmpty(input.Value), t => t.Value ==input.Value)
.WhereIf(
!string.IsNullOrEmpty(input.DictType_ID), t => t.DictType_ID ==input.DictType_ID);
}

而对于属性比较复杂的查询,我们适当调整这个函数的处理基类,一般都可以根据代码生成工具进行生成的,特殊条件自己微调一下就没问题了。

如用户应用服务层类UserAppService的重写自定义条件的函数,代码如下所示。

/// <summary>
///自定义条件处理/// </summary>
/// <param name="input">查询条件Dto</param>
/// <returns></returns>
protected override IQueryable<User>CreateFilteredQuery(UserPagedDto input)
{
return Repository.GetAllIncluding(x => x.Roles) //base.CreateFilteredQuery(input) .WhereIf(input.ExcludeId.HasValue, t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.EmailAddress.IsNullOrWhiteSpace(), t => t.EmailAddress.Contains(input.EmailAddress)) //如需要精确匹配则用Equals .WhereIf(input.IsActive.HasValue, t => t.IsActive == input.IsActive) //如需要精确匹配则用Equals .WhereIf(input.IsEmailConfirmed.HasValue, t => t.IsEmailConfirmed == input.IsEmailConfirmed) //如需要精确匹配则用Equals .WhereIf(input.IsPhoneNumberConfirmed.HasValue, t => t.IsPhoneNumberConfirmed == input.IsPhoneNumberConfirmed) //如需要精确匹配则用Equals .WhereIf(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals .WhereIf(!input.PhoneNumber.IsNullOrWhiteSpace(), t => t.PhoneNumber.Contains(input.PhoneNumber)) //如需要精确匹配则用Equals .WhereIf(!input.Surname.IsNullOrWhiteSpace(), t => t.Surname.Contains(input.Surname)) //如需要精确匹配则用Equals .WhereIf(!input.UserName.IsNullOrWhiteSpace(), t => t.UserName.Contains(input.UserName)) //如需要精确匹配则用Equals .WhereIf(!input.UserNameOrEmailAddress.IsNullOrWhiteSpace(), t =>t.UserName.Contains(input.UserNameOrEmailAddress)|| t.EmailAddress.Contains(input.UserNameOrEmailAddress) ||t.FullName.Contains(input.UserNameOrEmailAddress))//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

可以看出会根据UserPageDto的属性不同,从而增加更多的处理条件,有的是完全匹配,有些这是模糊匹配,有些如日期则是范围匹配。

对于数值、日期等有区间范围的属性,我们条件的DTO对象中,往往都有一个Start和End的起始值参数的。

这样我们在利用Vue&Element的前端进行查询的时候,可以构造对应的区间参数了,如下前端代码所示。

有时候,为了简化前端的日期区间代码,我们可以通过辅助类来简化处理。

而自定义排序的处理,则可以根据实际的需要进行排序处理,对于自增长的ID类型,使用ID倒序显示倒是问题不大,而如果是字符串类型,本身是GUID的类型,那么使用ID类排序这是没有任何意义的,因此必须通过重写基类函数的方式实现逻辑重写。

/// <summary>
///自定义排序处理/// </summary>
/// <param name="query"></param>
/// <param name="input"></param>
/// <returns></returns>
protected override IQueryable<DictData> ApplySorting(IQueryable<DictData>query, DictDataPagedDto input)
{
//先按字典类型排序,然后同一个字典类型下的再按Seq排序 return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s =>s.Seq);
}

具体情况根据ID的特点或者排序的具体情况进行排序即可。

最后一项是分页的处理,则可以按标准的方式处理,默认可以不重写。

这样我们前面提到的几个函数的逻辑,我们根据实际情况重写部分逻辑即可,从而非常弹性化的实现了条件的处理,排序的处理,分页的处理等规则。

public virtual async Task<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input)
{
//判断权限 CheckGetAllPermission();//获取分页查询的条件 var query =CreateFilteredQuery(input);//根据条件获取所有记录数 var totalCount = awaitAsyncQueryableExecuter.CountAsync(query);//对查询内容排序和分页 query =ApplySorting(query, input);
query
=ApplyPaging(query, input);//返回领域实体对象 var entities = awaitAsyncQueryableExecuter.ToListAsync(query);//构造返回结果集,并转换实体类为DTO对应 return new PagedResultDto<TEntityDto>(
totalCount,
entities.Select(MapToEntityDto).ToList()
);
}

因此,不管是Winform端,或者Vue&Element的BS前端,都可以通过不同的条件信息进行快速的查询排序处理了。

菜单资源管理的列表界面界面如下所示

用户列表包括分页查询及列表展示、以及可以利用按钮进行新增、编辑、查看用户记录,或者对指定用户进行重置密码操作。

这篇文章我们不谈技术,聊点轻松的,那聊什么呢?聊一下最近很火的目标管理 OKR。不知道小伙伴你们的公司什么情况,我的公司今年开始推行 OKR,用了大半年的时间,感觉效果还不错,上周六又参加了一天的复盘培训会,刚好借此机会总结一下顺便跟大家分享一下这个优秀的工具。

什么是 OKR

OKRs:Objectives & Key Results(目标与关键结果)。是一种企业,团队,个人目标设定与沟通的工具,是通过结果去衡量过程的方法与实践。

上面的解释说起来可能比较虚无缥缈,用通俗的语言来说就是一种自我激励督促的手段。对企业来说以前对员工的考核大部分情况都是采用 KPI 的方式,只要员工完成了 KPI 就可以了。OKR 是一种与 KPI 类似的工具,但是两者的出发点是完全不同的。KPI 强调的硬性指标,自上而下分配的任务,完全完全指导考核结果;但是 OKR 不一样,OKR 是一种激励督促的方式,强调的是创新性与挑战性,而且有自上而下或者自下而上两种方式,并且最大的特点是 OKR 的打分不直接与绩效考核挂钩。当然这里说的是不直接,但是还是会影响的~~。

简史

OKR 的创立是英特尔前首席执行官安迪·格鲁夫,在他的书《格鲁夫给经理人的第一课》中,他解释了自己为何成功创造出了 OKR,主要是通过问自己两个问题:

  1. 我要去哪里?答案是目标(Objective)
  2. 我如何知道能否达到那里?答案就是关键结果(Key Results)

格鲁夫的学生约翰杜尔在英特尔的时候全程跟着格鲁夫参与到了 OKR 的实施过程,亲眼目睹了 OKRs 是如何帮助英特尔管理层和员工的,最后成功完成转型。随后约翰杜尔作为早期谷歌的投资者将 OKR 介绍给了谷歌的创始人拉里-佩奇和谢尔盖-布林,之后 OKR 目标管理办法在谷歌里面发展的一发不可收拾,成为了全员使用的一种自我管理工具。

OKR 的制定

Objective 目标

OKR 是一种不限制场景使用的一个工具,可以是在公司内部,也可以是个人生活学习使用,基本的思路与原理都是一样的,只是不同场景所制定的 O 是不一样的。对于我们个人来说,不管是工作还是生活在一个周期(这里的周期我们通常是一个季度,当然根据个人情况,按月或者其他时间段都是可以的)里面一般设定 3-4 个 O 是最合适的,太多的 O 容易导致时间精力太分散不容易聚焦,太少可能不能很好的满足自己的需求,这个可以根据个人情况去制定。

O 的制定我们需要注意这么几点:

  1. 目标要积极阳光:一个积极阳光的目标是可以激发人的斗志的,在制定目标的时候尽量采用积极和正能量的词汇,比如:大幅度提升,远远超过,显著提升,最成功等,目标必须要让人看到就很激动,很有激情。但是也要注意不能太虚,避免制定一些根本不现实的目标和使用一些不恰当的词汇。
  2. 目标要有挑战:OKR 的核心是强调创新性和挑战性,所以我们的目标要尽量的有挑战,那种十拿九稳的目标就不要制定了,就需要制定那种刚好跳一跳就能够到目标,这样在目标实现过后才会有真正的成就感。太容易的目标,不具有挑战性会使人失去斗志。具有挑战但是也要符合实际,不能设定根本无法达成的目标。
  3. 对团队来说目标要可执行和有价值:这个是毋庸置疑的,对于一个团队来说设定的目标如果没有价值和无法执行那就是无效的目标,制定出来也没有任何效果,只能失败。
  4. 透明性:OKR 是完全透明的,意思是说任何人制定的 OKR 其他人都能查阅的,完全透明。

Key Results

在制定好了一个 O 过后,我们就需要列出为了能支撑这个 O 我们需要完成的哪些关键的结果。每一个 O 尽量列出 3-4 个 KR,在写 KR 的时候我们也要注意几点:

  1. KR 必须是可以量化的:意思是说写 KR 的时候一定要带上量化指标,比如:每个月写四篇公众号文章。而不能说"每个月都要写几篇文章"。这样不确定的 KR 是不会有效果,KR 就是要把关键的指标写出来,有些东西只有写出来才能真正的被落实。
  2. KR 是关键结果而非工作清单:很多人一开始写 OKR 的时候容易把 KR 写成一个个的任务清单,这是不对的,每个任务清单属于 KR 的下层,我们叫做 Plan,所以 KR 是对一系列 P 的组合和有量化的概括。
  3. KR 的编写使用积极简洁的语言:跟目标类似,KR 的编写也需要使用积极的语言,并且保证语言的简洁和明确负责人,避免误解。
  4. KR 一定要能支撑指定的 O: 编写的 KR 一定要能支撑上层的 O,编写的时候一定要确定这个 KR 是否能支撑 O,不能则说明这个不是关键 KR 就不需要写,另外 KR 一定要从多个维度去写。

OKR 案例

前面说了那么多都是理论,这里给一个实际的 OKR 案例给大家看一下具体是什么样子的。另外提一下 OKR 的编写格式没有固定的要求,有人喜欢用 Excel,有人喜欢用 Word 或者 PPT,都可以,找到一种自己喜欢的方式就好,我个人比较喜欢用脑图的方式,这里就采用思维导图给大家展示一个。

image-20190725004350573

(Ps: 这个思维导图是用 Mac 版的印象笔记画的,有点小 bug 不过能用)

可以看到,上面的 OKR 案例,目标中使用了"加速"积极字样,四个 KR 中都有量化的指标,目标和关键结果都比较清晰。图中的 (5/10) 这种表示的是完成这个 KR 的信心指数,这个信心指数很有讲究的,如果太低了说明这个 KR 实现的难度太大,不建议采用,如果太高了,说明这个 KR 太容易完成了,缺乏挑战。所以我们在填写 KR 的时候需要找到那些有六成七成把握的关键结果。而且最后在回顾的时候, OKR 的打分和考核也会参考这个信心指数。

OKR 落实的注意项

企业内部

OKR 的落实主要分为两个方面,一个是企业内部,一个是个人生活。在企业内部如果要落实 OKR 的话一定要确保透明,不管是领导制定的 OKR 还是员工制定的 OKR 都要无条件透明,最好的方式就是录入系统或者打印贴出来。而且 OKR 在制定的过程中一定要通过共识会达到相关人员的共识才可以,不能闷着头自己制定,制定完了就不管其他人了。因为工作中很多时候我们的目标是跟公司的愿景是一致的,而且大家很有可能自己的一个目标是需要其他团队的同事支持的,所以一定要有共识会,让大家都知道你制定的 OKR 是什么,从而达到一致。

企业内部的落实还有一个很重要的注意项就是考核问题,OKR 跟之前 KPI 的考核方式完全不一样,前面提到了 OKR 在一个季度或者月结束后是会有打分的,OKR 的打分不直接影响考核,但是会被参考。另外考核也需要看每个 KR 的信心指数,因为OKR 的打分是个人自己打分的,分打的高或者低都是自己的意愿。这里有小伙伴可能会说,既然是自己打分,那肯定打高一点啊,那么问题来了,如果一个 KR 的分打的很高,那么是不是可以认为这个 KR 不够挑战呢?是不是太容易完成了呢?当然不排除 KR 确实很难,但是就是完成的很出色的这种可能性。

所以总的来是,企业内部 OKR 的考核是需要参考 KR 的信心指数以及打分情况综合考虑的。另外还有一点 OKR 是鼓励创新和挑战,所以是不存在惩罚的。

个人生活

OKR 除了在工作中使用,也完全可以用到个人家庭生活中,按照个人习惯可以指定自己或者家人的年度,季度或者月度的 OKR。比如你可以指定自己每个月要阅读两本书,以及每个月要锻炼身体五次等有意义的 KR。另外你完全可以指定一个年度理财计划目标,通过具体的阅读投资书籍,或者购买基金股票,以及其他方式的投资来支撑你的目标,从而列出一系列的关键 KR。

总结

今天我们介绍了一个很好的目标管理工具 OKR,这个工具是这两年在国内流行起来的,但是其实很早在国外就已经使用了,国内的普及相比而言就晚了很多了。想要深入学习 OKR 目标管理办法的同学可以关注 Java 极客技术公众号,在后台回复关键字 "OKR" 获取由 Java 极客技术团队分享的几个关于 OKR 目标管理办法的几本电子书文档,进行深入的学习。更多优质的资料可以通过回复关键字 "关键字" 来获取,有更多优质的资源等你获取。

上面是了 OKR 是无条件透明的,下面贴一下我自己下半年的个人 OKR。

image-20190725004350573

我们都知道程序在运行的过程中经常需要进行服务间的通信和交互,特别是在当下微服务的架构下,每个系统都会庞大那么为了提高服务间的通信效率以及数据传输的性能,我们往往都会将需要传输的数据进行序列化,然后再进行传输。

什么是序列化

关于序列化相信大家都很了解,在 Java 中我们经常就可以看到很多实体类或者 POJO 都会实现 Serializable 接口,有了解过 Serializable 接口的小伙伴应该都知道,这个接口是一个空接口,只是用来标记的。所谓序列化简单来说就是在传输对象之前将对象转换成二进制字节进行传输,接收端在收到二进制数据后再反序列化转化成普通对象。

所以说序列化最终的目的是为了对象可以跨平台存储和进行网络传输。之所以需要序列化是因为在网络传输的时候,我们需要经过 IO,而 IO 传输支持的就是字节数组这种格式,所以序列化过后可以更好的传输。另外反序列化就是根据字节数组反向生成对象,是一个逆向过程。

常见的序列化方式

既然知道了什么是序列化,那么接下来我们看看有哪些常见的序列化方式。

JSON

当下最流行的序列化方式无非是 JSON 了,而且 JSON 作为前后端交互使用最广泛的格式,形式如下。作为最通用的格式,各种语言都支持,并且可以支持复杂的对象。

{"name":"鸭血粉丝","age":4,"sex":"男"}

JSON 作为一个序列化方案,它的优点是可读性很高,跨平台跨语言支持;但是有个缺点那就是体积较大,很存在很多冗余内容,比如双引号,花括号。

相信 JSON 大家在工作中使用的肯定会广泛,阿里提供的 fastjson 包是我们项目中必不可少的一个依赖。

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
</dependency>

XML

<?xml version="1.0"?>
<Person version="4.0">
	<name>鸭血粉丝</name>
	<age>4</age>
	<sex>男</sex>
</Person>

前些年不管是使用 SSM,还是使用 Spring 都会有很多 XML 的配置文件,现在很多被注解代替了,但是 XML 还是支持使用的。另外有一些广电或者银卡等老系统里面会有很多基于 XML 的协议开发的系统和服务。

阿粉之前做项目就遇到过银行的项目,里面都是很古老的 XML 协议,对接起来真是头疼呀~

通过上面例子我们可以看到,XML 协议的优缺点跟 JSON 类似,优点也是可读性很强,跨平台跨语言支持,缺点也是体积大,容易内容多。可以看到为了记录一个字段的值,每个标签都需要成对存在,过于冗余了。

Protobuf

Protobuf 是谷歌提出的一种序列化协议,Protobuf 是一种接口定义语言,它与语言和平台无关。它是一种序列化结构化数据并通过网络传输的方式,使用 Protobuf 传输二进制文件,与 JSON 的字符串格式相比,它提高了传输速度。

这里提到 Protobuf 是一种接口定义语言,说明也是一种语言,既然是语言那就有自己的关键字以及规则,所以对于Protobuf 协议,我们需要创建一个后缀为 .proto 的文件,在文件里面我们需要定义出我们的协议内容。

syntax = "proto2";
package com.demo;
message Request {
    required int32 version = 1;
    required string id = 2;
    message Model  {
        required int32 id = 1;
        required string pid = 2;
        optional int32 width = 3;
        optional int32 height = 4;
        optional int32 pos = 5;
    }
    repeated Model model = 3;
}

message 关键字表示定义一个结构体,required 表示必须,optional 表示可选,此外还有字段的名称和类型。这个原始的 proto 文件是通用的,只要定义一次就好,不管使用哪种语言都可以通过 proto 工具自动生成对应语言的代码。

比如要生成 Java 代码,我们可以执行下面的命令

protoc --java_out=. demo.proto 就会在指定的目录下,生成对应的 Demo.java,想生成其他语言的代码,只需要修改命令执行的参数即可,生成的代码内容会有很多,可以不用管直接使用就行。

我们定义模型的结构一次,然后就可以使用生成的源代码轻松地使用 JavaPythonGoRubyC++ 等各种语言在各种数据流中写入和读取结构化数据。

Protobuf 的优点主要是性能高,体积小,缺点就是要学习一下特定的关键词以及要下载按照 Protobuf 命令工具。

Thrift

Thrift 也是一种序列化协议,具体的使用方式跟 Protobuf 类似,只不过 Thrift Facebook 提出来的一种协议。

Thrift是一种接口描述语言和二进制通讯协议,原由Facebook于2007年开发,2008年正式提交Apache基金会托管,成为Apache下的开源项目。

Thrift 是一个 RPC 通讯框架,采用自定义的二进制通讯协议设计。相比于传统的HTTP协议,效率更高,传输占用带宽更小。另外,Thrift是跨语言的。Thrift的接口描述文件,通过其编译器可以生成不同开发语言的通讯框架。

Thrift 的使用方式跟 Protobuf 类似,也是有一个 .thrift 后缀的文件,然后通过命令生成各种语言的代码,这里就不演示了。

除了上面提到的四种序列化方式之外,还有 HessianJDK 原生等序列化方式,就不一一介绍了。

序列化协议选择

前面提到是几种序列化的协议方式,那么对于我们平常项目中使用的时候,我们应该如何选择自己的协议呢?需要关注哪几个方面的内容呢?

每个协议有每个协议的特点,具体选择哪种协议我们要根据实际的场景来选择,比如说如果是前后端对接,那么自然是 JSON 最合适,应该网页的交互要求不需要太高,秒级别是可以接受的,所以我们可以更加关注可读性。但是如果是微服务之间的数据传输,那我们就可以选择 Protobuf 或者 Thrift 这种更高效的协议来进行传输,因为这种场景我们对于协议序列化的体积和速度都有很高的要求。

总结

今天阿粉给大家介绍了几种序列化的协议,相信大家在日常工作中必然会用到,上面提到的协议你是否都用过呢?欢迎在评论区留言探讨。

日常工作中 Map 绝对是我们 Java 程序员高频使用的一种数据结构,那 Map 都有哪些遍历方式呢?这篇文章阿粉就带大家看一下,看看你经常使用的是哪一种。

通过 entrySet 来遍历

1、通过 formap.entrySet() 来遍历

第一种方式是采用 forMap.Entry 的形式来遍历,通过遍历 map.entrySet() 获取每个 entrykeyvalue,代码如下。这种方式一般也是阿粉使用的比较多的一种方式,没有什么花里胡哨的用法,就是很朴素的获取 map 的 keyvalue

public static void testMap1(Map<Integer, Integer> map) {
    long sum = 0;
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

看过 HashMap 源码的同学应该会发现,这个遍历方式在源码中也有使用,如下图所示,

putMapEntries 方法在我们调用 putAll 方法的时候会用到。

2、通过 forIteratormap.entrySet() 来遍历

我们第一个方法是直接通过 forentrySet() 来遍历的,这次我们使用 entrySet() 的迭代器来遍历,代码如下。

public static void testMap2(Map<Integer, Integer> map) {
    long sum = 0;
    for (Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator(); entries.hasNext(); ) {
      Map.Entry<Integer, Integer> entry = entries.next();
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

3、通过 whileIteratormap.entrySet() 来遍历

上面的迭代器是使用 for 来遍历,那我们自然可以想到还可以用 while 来进行遍历,所以代码如下所示。

 public static void testMap3(Map<Integer, Integer> map) {
    Iterator<Map.Entry<Integer, Integer>> it = map.entrySet().iterator();
    long sum = 0;
    while (it.hasNext()) {
      Map.Entry<Integer, Integer> entry = it.next();
      sum += entry.getKey() + entry.getValue();
    }
    System.out.println(sum);
  }

这种方法跟上面的方法类似,只不过循环从 for 换成了 while,日常我们在开发的时候,很多场景都可以将 forwhile 进行替换。2 和 3 都使用迭代器 Iterator,通过迭代器的 next(),方法来获取下一个对象,依次判断是否有 next

通过 keySet 来遍历

上面的这三种方式虽然代码的写法不同,但是都是通过遍历 map.entrySet() 来获取结果的,殊途同归。接下来我们看另外的一组。

4、通过 for 和 map.keySet() 来遍历

前面的遍历是通过 map.entrySet() 来遍历,这里我们通过 map.keySet() 来遍历,顾名思义前者是保存 entry 的集合,后者是保存 key 的集合,遍历的代码如下,因为是 key 的集合,所以如果想要获取 key 对应的 value 的话,还需要通过 map.get(key) 来获取。

public static void testMap4(Map<Integer, Integer> map) {
    long sum = 0;
    for (Integer key : map.keySet()) {
      sum += key + map.get(key);
    }
    System.out.println(sum);
  }

5、通过 forIteratormap.keySet() 来遍历

public static void testMap5(Map<Integer, Integer> map) {
    long sum = 0;
    for (Iterator<Integer> key = map.keySet().iterator(); key.hasNext(); ) {
      Integer k = key.next();
      sum += k + map.get(k);
    }
    System.out.println(sum);
  }

6、通过 whileIteratormap.keySet() 来遍历

public static void testMap6(Map<Integer, Integer> map) {
    Iterator<Integer> it = map.keySet().iterator();
    long sum = 0;
    while (it.hasNext()) {
      Integer key = it.next();
      sum += key + map.get(key);
    }
    System.out.println(sum);
  }

我们可以看到这种方式相对于 map.entrySet() 方式,多了一步 get 的操作,这种场景比较适合我们只需要 key 的场景,如果也需要使用 value 的场景不建议使用 map.keySet() 来进行遍历,因为会多一步 map.get() 的操作。

Java 8 的遍历方式

注意下面的几个遍历方法都是是 JDK 1.8 引入的,如果使用的 JDK 版本不是 1.8 以及之后的版本的话,是不支持的。

7、通过 map.forEach() 来遍历

JDK 中的 forEach 方法,使用率也挺高的。

public static void testMap7(Map<Integer, Integer> map) {
    final long[] sum = {0};
    map.forEach((key, value) -> {
      sum[0] += key + value;
    });
    System.out.println(sum[0]);
  }

该方法被定义在 java.util.Map#forEach 中,并且是通过 default 关键字来标识的,如下图所示。这里提个问题,为什么要使用 default 来标识呢?欢迎把你的答案写在评论区。

8、Stream 遍历

public static void testMap8(Map<Integer, Integer> map) {
    long sum = map.entrySet().stream().mapToLong(e -> e.getKey() + e.getValue()).sum();
    System.out.println(sum);
  }

9、ParallelStream 遍历

 public static void testMap9(Map<Integer, Integer> map) {
    long sum = map.entrySet().parallelStream().mapToLong(e -> e.getKey() + e.getValue()).sum();
    System.out.println(sum);
  }

这两种遍历方式都是 JDK 8Stream 遍历方式,stream 是普通的遍历,parallelStream 是并行流遍历,在某些场景会提升性能,但是也不一定。

测试代码

上面的遍历方式有了,那么我们在日常开发中到底该使用哪一种呢?每一种的性能是怎么样的呢?为此阿粉这边通过下面的代码,我们来测试一下每种方式的执行时间。

public static void main(String[] args) {
   int outSize = 1;
    int mapSize = 200;
    Map<Integer, Integer> map = new HashMap<>(mapSize);
    for (int i = 0; i < mapSize; i++) {
      map.put(i, i);
    }
    System.out.println("---------------start------------------");
    long totalTime = 0;
    for (int size = outSize; size > 0; size--) {
      long startTime = System.currentTimeMillis();
      testMap1(map);
      totalTime += System.currentTimeMillis() - startTime;
    }
    System.out.println("testMap1 avg time is :" + (totalTime / outSize));
		// 省略其他方法,代码跟上面一致
}

为了避免一些干扰,这里通过外层的 for 来进行多次计算,然后求平均值,当我们的参数分别是 outSize = 1,mapSize = 200 的时候,测试的结果如下

当随着我们增大 mapSize 的时候,我们会发现,后面几个方法的性能是逐渐上升的。

总结

从上面的例子来看,当我们的集合数量很少的时候,基本上普通的遍历就可以搞定,不需要使用 JDK 8 的高级 API 来进行遍历,当我们的集合数量较大的时候,就可以考虑采用 JDK 8forEach 或者 Stream 来进行遍历,这样的话效率更高。在普通的遍历方法中 entrySet() 的方法要比使用 keySet() 的方法好。