你是否遇到过这样的框架,它非常简单又是轻量级的,很容易上手,然而当你的项目变得复杂的时候它能自我进化成功能强大的重量级框架,而不需要把整个项目重写? 我是从来没见过。

先让我们来看一下项目的生命周期。通常,当一个新项目开始时,我们不知道它能持续多久,所以我们希望它尽可能简单。大多数项目都会在短时间内夭折,所以它们并不需要复杂的框架。然而,其中有一些击中了用户的痛点并受到欢迎,我们就会不断地对它们改进,使它们变得越来越复杂。结果就是原来简单的框架和设计已经远远不能满足需求,剩下的唯一方法就是重写整个项目,并引入强大的重量级框架。如果项目持续受欢迎,我们可能需要多次重写整个项目。

这时一个能自我进化的框架就展现出优势。我们可以在项目开始时使用这个轻量级框架,并只在确实需要时才将其进化为重量级框架, 在这个过程中我们不需要重写整个项目或更改任何业务逻辑代码,当然你需要对创建结构(struct)的代码(也叫程序容器)做一些修改。但这个修改比起修改业务逻辑或重写整个项目不知要容易多少倍。

这听起来太棒了,但有这样的东西吗?很长一段时间以来,我都认为这是不可能的,直到最近竟然找到了一个。

去年,我创建了一个基于清晰架构(Clean Architecture)的框架,并写了一系列关于它的文章。请查看"清晰架构(Clean Architecture)的Go微服务" 。它使用工厂方法设计模式来创建对象(结构),功能非常强大,但有点重。我希望能把它改的轻一些,这样简单的项目也能使用。但我发现任何强大的框架都是重量级的。没有一个框架是轻量级的但同时又非常强大,正如鱼与熊掌不可兼得。我在这上面花了不少时间,最后终于找到了一个方法,就是让框架能够自我进化。

解决方案

我们可以将一个项目的代码分为两部分,一部分是业务逻辑(Business Logic),其中所有调用都基于接口,不涉及具体对象(结构)。另一部分是为这些接口创建具体对象(结构(struct)),我们可以称之为程序容器(Application Container)(详情参见"清晰架构(Clean Architecture)的Go微服务: 程序容器(Application Container)") 。这样,我们就可以让业务逻辑保持不变,而使程序容器自我进化。大多数程序容器都用依赖注入来将对象(结构)注入到业务逻辑中,“Spring”就是一个很好的例子。但是,要使框架能够自我进化,关键是不能直接使用依赖注入作为这两部分之间的接口。相反,你必须使用一个非常简单的接口。当然,你依然可以使用依赖注入,但这只是在程序容器内部,因此只是程序容器的实现细节。

下面就是框架的结构图.

serviceTmpl1.jpg

程序容器和业务逻辑之间的接口

程序容器和业务逻辑之间的接口应该非常简单。唯一的功能就是让业务逻辑能获取具体对象(结构)。在清晰架构中,大多数情况下你只需要获取用例(Use Case)。

下面就是程序容器的接口:

type Container interface {
    // BuildUseCase creates concrete types for use case and it's included types.
    // For each call, it will create a new instance, which means it is not a singleton
    BuildUseCase(code string) (interface{}, error)

    // This should only be used by container and it's sub-package
    // Get instance by code from container.
    Get(code string) (interface{}, bool)

    // This should only be used by container and it's sub-package
    // Put value into container with code as the key.
    Put(code string, value interface{})

}

如何让程序容器进化

我定义了三种模式的程序容器,从最简单到最复杂,你可以直接使用。你也可以定义新的程序容器模式,只要它遵循上面的接口即可。你可以随时将程序容器替换为其他模式,而无需更改业务逻辑代码。

初级模式

这是最简单的模式,它不涉及任何设计模式。它的最大优点是简单,易学,易用。绝大多数的项目都可以从此模式开始。使用这种模式可以在一天之内创建整个项目。如果项目很简单,在一小时内完成都是有可能的。如果你不再需要这个项目,就可以一点也不可惜地丢弃它。缺点是它提供的功能非常简单,所有配置信息都是以硬编码的形式写在程序中,既不灵活也不强大。最适合POC(概念验证)类型的项目。具体实例可查看 "订单服务" 。这是一个事件驱动的微服务项目,旨在提供订单服务。

以下是初级模式的结构图,框内是程序容器:

orderApp.jpg

增强模式

这种模式类似于初级模式,主要改进是增加了配置参数管理。在这种模式下,配置参数不再是硬编码在代码中的,它们是在结构(struts)中定义的。你也可以对它们进行校验。更改程序配置要容易得多,你可以在单个文件里看到项目的所有配置参数,从而掌握整个程序的全貌。该框架仍然非常简单,不涉及任何设计模式。当项目已经稳定并且需要某种结构时,可以切换到这种模式。具体实例可查看"支付服务". 这是一个事件驱动的微服务项目,旨在提供支付服务。

以下是增强模式的结构图,框内是程序容器:

paymentApp.jpg

高级模式

当你有一个复杂项目时,你需要一个功能强大的框架来与之匹配。你可能会有一些比较复杂的需求,如更改所用的数据库或动态更改配置参数(不需更改代码)。这时,你可以将项目升级为高级模式。它将在程序容器中使用依赖注入。具体实例可查看"Service template 1"。 这是一个清晰架构(Clean Architecture)的微服务框架。

以下是高级模式的结构图,框内是程序容器,它的文件结构看起来有很大的不同。

serviceTmpl1App.jpg

如何升级

假设你有一个新项目,最容易的启动方式的是复制整个“订单服务”项目,然后将里面的结构(struct)更改为你的结构,并完成业务逻辑代码。在这个过程中,你可以保留“订单服务”项目的目录结构和一些接口。过了一段时间,你发现需要升级到高级模式。这时,最简单的方法是从“servicetmp1”项目中复制“app”文件夹,并替换你的项目中的“ app”文件夹,然后对程序容器进行相应的修改。完成之后,你无需更改业务逻辑中的任何代码,一切都应该可以正常工作。如果你了解这个框架,整个过程应该不会超过一天时间,甚至更短都有可能。

此方案的关键元素

要想框架能够自我进化,它必须按照特定的方式进行设计和创建。以下是框架的四个关键元素。

  • 程序结构
  • 程序容器
  • 基于接口的业务逻辑
  • 可插拔的第三方接口库

基于接口(Interface)的业务逻辑

前面已经讲了程序结构和程序容器,这里主要讲解业务逻辑。基于接口的业务逻辑是框架能自我进化的关键。在应用程序的业务逻辑部分,你可能有不同类型的元素,例如“用例(use case)”,“域模型(domain model)”,“存储库(repository)”和“域服务(domain service)”。除了“域模型(domain model)”或“域事件(domain event)”之外,业务逻辑中的几乎所有元素都应该是接口(而不是结构(struct))。有关程序设计和项目结构的详细信息,请查看"清晰架构(Clean Architecture)的Go微服务: 程序设计"

内部接口

在业务逻辑中有两种不同类型的接口。一种是内部接口,另一种是外部接口。内部接口是在应用程序内部使用的接口(通常不能与其他程序共享),例如“用例”,它是清晰架构中的重要元素。以下是“RegistrationUseCaseInterface”用例的接口。

type RegistrationUseCaseInterface interface {
    RegisterUser(user *model.User) (resultUser *model.User, err error)

    UnregisterUser(username string) error
    
    ModifyUser(user *model.User) error
    
    ModifyAndUnregister(user *model.User) error
}

可插拔的第三方接口库

通常业务逻辑需要与外部世界交互并使用它们提供的服务,例如,日志服务、消息服务等等。这些都是外部接口,常常可以被很多应用程序共享。在领域驱动设计中,它们被称为“应用服务(application service)”。 通常有许多库或应用程序可以提供这样的服务, 但你不希望将应用程序与它们中的任何一个绑定。最好是能随时替换任何服务而又不需要更改代码。

问题是每个服务都有自己的接口。理想的情况是,我们已经有了标准接口,所有不同的服务提供者都遵循相同的接口。这将是开发者的梦想成真。Java有一个“JDBC”的接口,它隐藏了每个数据库的实现细节,使我们能按照统一的方式处理不同的SQL数据库。不幸的是,这种成功并没有扩展到其他领域。

要想让框架变得很轻量的一个关键是把服务都变成标准接口,并把它们移到框架之外,使之成为第三方库,其中不仅包含了标准接口,同时也封装了支持这个接口的库。这样这个第三方库就变成了可插拔的标准组件。为了让应用程序基于接口设计,我创建了三个通用接口分别用于日志记录、消息传递和事务管理。创建一个好的标准接口是非常困难的,由于我在上面这些领域都不是专家,因此这些自建的接口离标准接口有一定差距。但对于我的应用程序来说,这已经足够。我希望各个领域的专家能尽快制定出标准接口。在没有标准接口之前,可以自定义接口,为以后切换到标准接口做好准备。

下面是日志的通用接口:

type Logger interface {
    Errorf(format string, args ...interface{})
    Fatalf(format string, args ...interface{})
    Fatal(args ...interface{})
    Infof(format string, args ...interface{})
    Info(args ...interface{})
    Warnf(format string, args ...interface{})
    Debugf(format string, args ...interface{})
    Debug(args ...interface{})
}

这个第三方库的结构是与框架或应用程序的结构相匹配的,这样才能与框架很好地对接。关于如何创建一个第三方库,我会单独写一篇文章["事件驱动的微服务-创建第三方库"]来讲解。

框架(framework)或者库(Lib)?

框架和库之间的争论已经持续了很久了。大多数人更喜欢库而不是框架,因为它是轻量级的并更加灵活。但为什么我要创建一个框架而不是一个库呢? 因为你仍然需要一个框架来将所有不同的库组织在一起(不论它是自建的或是第三方的)。因此你通常要用很多库,但只要一个框架。问题是有用的框架都太重了,我们需要一个轻量级的好用的框架。

因为业务逻辑中的元素都是基于接口的,我们可以把框架视为总线(接口总线),将任何基于接口的服务插入其中。这就是所谓的可插拔框架,它实现了框架与库的完美结合。

在这个框架之下,一个应用程序的生态由三部分组成,一个是可进化的框架;另一个是可插拔的第三方标准接口(这个接口是可以不依赖于任何框架而单独使用的),例如上面提到的日志接口;最后是支持标准接口的具体实现库,例如对日志功能来讲就是"zap""Logrus"。 而可进化的框架就成了把它们串接起来的主线。

与其它框架的比较

本文的框架是基于清晰架构(Clean Architecture) 的。你可以在很多其他框架中看到相似的元素,比如Java中的“Spring”,它也有程序容器并大量地使用了依赖注入。本框架唯一的新东西是自我进化。

通常,大多数框架都试图通过使用多种设计模式来应对未来的不确定性。而它需要复杂的逻辑,这就不可避免地将这种复杂性写入到代码中。这就使得多数有用的框架都很重,不论学习和使用都难度较高。但如果未来的情况与预计的并不相符,那么这种内置的复杂性就得不到利用,而变成巨大的负担。“Spring”就是一个很好的例子,它非常强大但也很重,适合复杂的项目,但是对于简单的项目就很浪费。本框架在设计时彻底改变了思路,不对未来做任何假设,因此就不需预先在代码中引入复杂的设计模式。你可以从最简单的框架开始,只有当你的程序变得很复杂并需要与之匹配的框架时,才进化成复杂的框架。当然你的程序必须遵从一定的设计结构,这里面的关键是基于接口的设计。当前,我们已进入了微服务时代,大多数项目都是小的服务,这对能够自我进化框架的需求就变得更为强烈。

应用程序如何使用框架?

在清晰架构中,“用例”是一个关键组件。如果你想了解一个应用程序,就从这里开始。业务逻辑只需要获得用例一个接口,就可以完成需要的任何操作,因为所有其它需要的接口都包含在“用例”中。

在业务逻辑中,“用例”被定义成接口而不是结构(struct)。在运行时,你需要获得用例的具体实现结构(struct)并将其注入到业务逻辑中。它的步骤是这样的,首先创建容器,然后构建具体的用例,最后调用“用例”中的函数。

如何调用“用例”

下面是构建程序容器的代码。

func buildContainer(filename string) (container.Container, error) {
    container, err := app.InitApp(filename)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    return container, nil
}

下面是程序容器中的函数"InitApp()"(在文件"app.go"里),调用它来初始化容器。

func InitApp(filename...string) (container.Container, error) {
    err := initLogger()
    if err != nil {
        return nil, err
    }
    return initContainer()
}

下面是用来创建"Registration"用例的帮助函数,它在文件"serviceTmplContainer.go"里。

func GetRegistrationUseCase(c container.Container) (usecase.RegistrationUseCaseInterface, error) {
    key := config.REGISTRATION
    value, err := c.BuildUseCase(key)
    if err != nil {
        //logger.Log.Errorf("%+v\n", err)
        return nil, errors.Wrap(err, "")
    }
    return value.(usecase.RegistrationUseCaseInterface), nil
}

下面是调用"Registration"用例的代码,它先调用"GetRegistrationUseCase"来得到用例,然后再调用“用例”里面的"RegisterUser()"函数。

func testRegisterUser(container container.Container) {
    ruci, err := containerhelper.GetRegistrationUseCase(container)
    if err != nil {
        logger.Log.Fatal("registration interface build failed:%+v\n", err)
    }
    created, err := time.Parse(timea.FORMAT_ISO8601_DATE, "2018-12-09")
    if err != nil {
        logger.Log.Errorf("date format err:%+v\n", err)
    }

    user := model.User{Name: "Brian", Department: "Marketing", Created: created}

    resultUser, err := ruci.RegisterUser(&user)
    if err != nil {
        logger.Log.Errorf("user registration failed:%+v\n", err)
    } else {
        logger.Log.Info("new user registered:", resultUser)
    }
}

结论

本文介绍了一个能够自我进化的轻量级的清晰架构框架。当创建一个新项目时你可以从最简单的轻量级的框架开始。当此项目不断发展变得复杂时,框架可以自我进化为一个功能强大的重量级框架。在此过程中,不需要更改任何业务代码。目前它有三种模式,分别是初级模式,增强模式和高级模式。最复杂的是高级模式,它基于依赖注入,非常强大。我创建了三个简单的应用程序来说明展示如何使用它,每个程序对应一种模式。

源码:

完整的源码:

索引:

1 "清晰架构(Clean Architecture)的Go微服务"

2 "清晰架构(Clean Architecture)的Go微服务: 程序容器(Application Container)"

3 "订单服务"

4 "支付服务"

5 "Service template 1"

6 "zap"

7 "Logrus"

8 "清晰架构(Clean Architecture)的Go微服务: 程序设计"

9 ["事件驱动的微服务-创建第三方库"]

10 The Clean Architcture


倚天码农
206 声望162 粉丝

不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考