为什么要重新选择后端技术

过去的一年2020对笔者来说是非常有价值的一年,笔者在公司工作上大部分精力都花费在基于TypeScript + React的Electron桌面开发及前端开发以及WorkPlus Lite移动端开发等工作上。

而在后端方面,2020年笔者在自己的一个业余项目上使用了Spring Boot技术,并整理抽象出了一个基于DDD领域驱动风格的开源框架mydddd-backend

笔者非常清楚,在后端技术方面,Spring仍然是主流,凭借强大的生态及完整的解决方案,Spring依然是大部分公司及团队的第一选择。这也是笔者在整理myddd-backend框架时为什么选择基于Spring Boot来实现的原因所在。因为笔者相信它能适合大多数团队。

进入2021年,笔者觉得需要重新关注下后端技术,以思考是否需要选择新的技术做为笔者后端技术解决方案,之所以有这种想法,也是基于以下几个原因

  • 在使用Spring Boot的过程中,仍然感觉它非常中规中矩,说不上哪不好,毕竟是一个完整的生态及解决方案。但始终感觉不到其的魅力所在。

  • 近些年兴起的一些新的编程理念与语言让笔者一直想尝试下,如响应式编程以及Kotlin这个号称Better Java的语言等。事实上,在Google的推动下,Kotlin被人接受的程度越来越高,使用的程序员也越来越多了。

  • 传统Java语言及阻塞式编程并无问题,笔者认为它仍是大多数团队与公司的第一选择。但非阻塞式的异步编程的优点也非常突出,如果程序员及团队能掌控,确实可以尝试。

在这些原因的影响下,笔者也一直在思考是否要重新选择新的技术栈来尝试。

经过一些思考与了解及尝试后,笔者选择了VertXKotlin的解决方案。在业余时间的一些尝试后,笔者对它是非常满意的,并认定它将是未来笔者在后端的主要技术栈。

为什么响应式编程没有成为主流?

如笔者上述所言,类似的响应式编程在性能上有极大的优势,但它一直未能成为主流。笔者也在思考这个现象。总结出部分原因如下:

原因一:思维差异+可维护性差

这些年,响应式编程的概念很火,但事实上一直未能成为主流。响应式编程有着非常好的性能优势,非阻塞式的实现机制比阻塞式的实现机制确实好很多,但它仍有一个非常难以解决的问题,那就是

响应式编程带来的异步编程思维并不符合人类的思维

人的思维是什么,我们理解一个事情的基本思维仍是面向对象及过程的,比如我们理解的上班是这样的

  1. 先起床
  2. 乘座交通工具去公司
  3. 早上开例会,安排任务
  4. 开始编码

如果就这件事,我们按照传统的面向对象及阻塞式的思维来编码,它是这样的

#这是伪代码,不要当真

class Coder{

  public void work(){
    this.getUp()
    this.driveToOfficePlace()
    this.joinMeeting()
    this.code()
  }

}

我们可以明显看到,这个编码与我们的思维是完全一致的,这就是我们所固有的习惯,阻塞式及同步的方式,是符合我们的思维的。

如果我们用一种响应式编程中的异步编程来实现,大致的代码可能是这样的

#这是伪代码,不要当真

class Coder{

  public void work(){
    this.getUp().onSuccess(()->{
      this.driveToOfficePlace(()->{
         this.joinMeeting(()->{
            this.code()
         })
      })
    })
  }

}

上面这个已经很简化了,事实上的业务不可能这么简洁,你可以想象当这个代码中充满各种代码细节时的场景。

大致上所有的异步编程都有这种风格,因为这种风格与我们人类思维上存在差异,所以有个非常著名的名字来称为它:回调地狱

当然,写Java的可能对这个不太清楚,但前些年,使用NodeJs的程序员对它可谓不所不知。事实上,移动端也一并存在类似的问题。

而且很明显,这种风格的代码在阅读与理解上相比更困难,这也是它可维护性较差的原因之一,也因此一并造成很多程序员写不好类似风格的代码,一方面思维上的不协调,而另一方面可维护性上也不是很好,而大多数公司和团队仍然有赖于大多数程序员的工作,这也是类似的编码模式一直未能成为主流的主要原因。

原因二:生态较差

如果我们说生态,那坦率的说,没有比Java语言生态更强大的语言了,这也是之所以这么多年,不喜欢Java的人非常多,但Java一直是后端的主力开发语言。 相比较而言,一些响应式的框架如果从生态上相比,就比Java差远了。类似RXJava等响应式编程语言,更多的是属于一个技术类库,其在生态上的不足也必然会阻碍一些程序员。

举例来说: 我如何用异步方式与数据库打交道?是否支持微服务?如何做OAUTH2权限?在Java的世界,你不需要为这些担忧,任何一个问题都有一大批成熟的解决方案。但在异步编程的世界,就相对差了很多。

为什么笔者会选择Vert.x与Kotlin的结合

但凡事并无绝对,基于对未来的一些考量,笔者还是希望能在这方面有所建树,所以近期关注并研究了一些技术。最终选择了Vert.xKotlin的结合。并对它们的表现非常满意

在尝试后,笔者认为至少在以下几个方面,它们是笔者值得选择的理由

  • 简洁优雅,而不失可维护性
  • 较为完整的生态
  • 性能上的绝对优势

理由一:简洁优雅,而不失可维护性

事实上,如笔者所述的前面的回调地狱问题,这个已经有较好的解决方案了。

如笔者在一个Electron桌面开发的代码中,是这样使用异步的

    #TypeScript代码
    public static async syncFavors(): Promise<Contact[]> {
        //从网络获取星标联系人,这实质上是一个异步行为
        const favors = await Contact.getNet().fetchFavorContacts();
        if (favors) {
            //存储到数据库中,这也是一个异步操作
            await Contact.getRespository().batchSaveFavors(favors);
        }
        return favors;
    }

如上述代码所示,在JS的世界中,早已出现了Promise与await的来解决这个问题。将非阻塞回调转成同步风格但实质还是非阻塞。

虽然Vert.x本身未提供类似的功能,但Kotlin协程则提供了。基于它们的结合,就算是在异步编程中,你也可以如同前端TS一样,写出类似风格的代码

    [@Test](https://my.oschina.net/azibug)
    fun testExists(vertx:Vertx, testContext: VertxTestContext){
        GlobalScope.launch {
            try {
                val user =  User(username = "lingen",age = 35)
                //这是一个异步代码,但我们用await()来解决回调
                val createdUser =  repository.save(user).await()
                //这又是一个异步
                var exists =repository.exists(User::class.java,createdUser.id).await()
                testContext.verify {
                    Assertions.assertTrue(exists)
                }
                testContext.completeNow()
            }catch (e:Exception){
                testContext.failNow(e)
            }

        }
    }

可以看出,Vert.xKotlin协程的结合,提供了类似的解决方案,使得我们在异步编程中,仍然能以符号人类思维的方式来编码。

几年前,Google将Kotlin取代Java选定为Android开发的第一语言,这并不是空穴来风的决定。想必Google也是在认真考察并认可这门语言才决定的。 事实上也确实如此,Kotlin号称Better Java,与其它JVM语言相比,它更简洁与优雅。

笔者仅举一例来说明

    private static DocumentRepository repository;

    private static DocumentRepository getDocumentRepository(){
        if(Objects.isNull(repository)){
            Document.repository = InstanceFactory.getInstance(DocumentRepository.class);
        }
        return Document.repository;
    }

而在Vert.x与Kotlin中,实现是这样的

    companion object {
        val repository:CommentRepository by lazy { InstanceFactory.getInstance(CommentRepository::class.java) }
    }

如上述代码所示,同一个获取仓储的方式,在Kotlin的代码中,比Java的实现好很多。

笔者就不举更多例了,Kotlin相对Java,确实是更加简洁与优雅,而又不失可维护性。

较为完整的生态

如笔者前述所言,类似的异步编程也好,响应式编程框架也好,在生态上都存在问题。表现为生态不够完善。 但这一点,在Vert.x反而是个优势。

之所以选择Vert.x,也是因为笔者在看到它的生态之后,才决定更进一步了解它。

Vert.x基本有自己的一整套生态,意味着你选择它,不用为项目中的任何一个维度的事情发愁,而且这些生态都是官方自己负责维护的,质量也比较有保证。

其在Web,数据库单元测试权限微服务支持消息事件机制,集群等有完整的解决方案。

如上图所示,Vert.x基本在每一方面都有自己的解决方案,这是非常明显的一个优势。

性能上的绝对优势

如果没有前两个优势,对笔者而言,这个优势并不足以成为可以考量的优势。因为,笔者始终将代码的可维护性放在第一重要位置来考量。

但如果有前两个优势,那这就成为另一个绝对优势

在国外的性能大对比中,Vert.x始终处于前列。

而基于Spring Boot的实现,则弱于Vert.x数倍。

结论

所以,综上所述,如果能写出简洁优雅的代码,生态又足够完善,又在性能上足够有优势。为什么不选择它?

myddd-vertx

所以,笔者在正基于Vert.x与Kotlin,按照领域驱动的理念,开发myddd-vertx框架。

这个框架已接近完成,后续也会如同myddd-backend一样,开源出来。

基于myddd-vertx 部分代码示例

class CommentRepositoryHibernate : EntityRepositoryHibernate(),CommentRepository {

    override suspend fun createComment(comment: Comment): Future<Comment> {
        val future = PromiseImpl<Comment>()
        require(comment.id == 0L)
        comment.created = System.currentTimeMillis()
        var created =  save(comment).await()
        created.rootCommentId = created.id
        created = save(created).await()
        future.onSuccess(created)
        return future
    }
}
[@Entity](https://my.oschina.net/u/1260961)
@Table(name = "comment",
    indexes = [Index(name = "index_comment_id",columnList = "comment_id"),
        Index(name = "index_root_comment_id",columnList = "root_comment_id"),
    ])
class Comment : BaseEntity() {

    /**
     * 关联文章
     */
    @Column(name = "comment_id")
    lateinit var commentId:String

    /**
     * 关联评论根ID
     */
    @Column(name = "root_comment_id")
    var rootCommentId:Long = 0

    /**
     * 关联回复评论ID
     */
    @Column(name = "parent_comment_id")
    var parentCommentId:Long = 0

    @Column(name = "level")
    var level:Int = 0

    /**
     * 昵称
     */
    var author:String? = null

    /**
     * 邮箱
     */
    var email:String? = null

    /**
     * 回复内容
     */
    lateinit var content:String

    companion object {
        val repository:CommentRepository by lazy { InstanceFactory.getInstance(CommentRepository::class.java) }
    }

    suspend fun createComment():Future<Comment> {
        return repository.createComment(this)
    }

    suspend fun createReplyComment(parentComment: Comment):Future<Comment> {
        return repository.createReplyComment(parentComment,this)
    }


}
    @Test
    fun testAddComment(vertx: Vertx, testContext: VertxTestContext){
        GlobalScope.launch {
            val comment = createComment()
            val created = commentRepository.createComment(comment).await()
            testContext.verify {
                Assertions.assertTrue(created.id > 0)
                Assertions.assertEquals(created.id,created.rootCommentId)
                Assertions.assertEquals(created.level,0)
            }

            testContext.completeNow()
        }
    }

以上,敬请期待!!!


更多优质文章,请访问笔者的个人网站 https://lingenliu.cc 或关公众号:【御剑轩】 - 致力于实践与传播优雅的编码之道

05-18 09:53