16

KISS整洁架构之领域驱动开发

 4 years ago
source link: http://dockone.io/article/9763
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Aze2mi6.jpg!web

我最一开始所用的标题是”完美整洁架构之领域驱动设计”。是不是容易引起争议?是的。听上去有些自大?也许是的。但是我是一个理想主义的,解决问题的组织者,我从没有见过一个软件架构可以燃起我的火焰,让我如此兴奋。所以我认为,这个提议非常简单,异常优雅,十分合适并且可以延伸的。

所以,作为一个谦逊的人,我决定把这篇文章取名为KISS整洁架构之领域驱动开发。很明显,KISS原则表示你必须保持使事情(软件架构)非常非常的简单,甚至使它极度的简单。

在简化复杂度这件事情上,整洁架构和领域驱动设计比我过去20多年软件开发经验中所见过的任何架构都要好。软件应该像华夫饼一样简单,而不是像意大利面一样。

计算机编程的精华部分就是去控制复杂度 - Brian Kernighan

这篇文章讲述的是如何去实现整洁架构。例子中的代码使用了Kotlin以及Spring (给Java专家的提示,方法的返回类型在冒号后面)。很抱歉并没有为这些代码去GitHub上去创建一个简单的项目。

第一件事

让我从一些基本的方面开始说起。无论软件有多复杂,它所包含的仍然是输入,处理以及输出。内聚性表示这三个基础部分是不同的并且独立的。正如DDD所强调的,输出和输出是其中比较不稳定的部分,而业务处理是其中相对稳定的部分。输入和输出可能具有非常复杂的逻辑,但它们仅仅是输入,输出,和业务领域没有任何关系。

最上层

我最上层的程序包(package)是根据整洁架构所定义的层次去组织的。必须记住的是,层次之间的依赖关系必须是从上至下的。你可以注意到,我在领域(domain)包的命名前面加上了字母x为了使层次之间是按照依赖方向排序的!这是不是变得更加容易理解了?

appConfig/ // application configuration, wires it all together, knows about everything

gateway/ // all input and output, knows about domain classes below

usecase/ // use cases for all application functionality, does gateway input and output ONLY via interfaces on this layer

xdomain/ // business domain classes that do not know or care about anything else above this layer

还有比这更简单的吗?所有的东西都包含在这四个层次里面。在每个层次内部,程序包可以通过功能特性,类型或者其他任何合理的方式去组织。但是这四个层次使开发者明确的知道这个软件的意义,从业务以及技术的角度让开发知道这个软件的用途。这一点非常重要,如果不理解软件的用途,编写代码也将变得困难重重。

软件工程的目的是为了控制复杂度,而不是创造它 – Dr. Pamela Zave

应用/配置层

上面提到的appConfig层是一个应用的入口,也可以在这一层为应用做一些初始化配置。把这一层代码写对会使改变输入和输出变得非常的简单,可能只需要改一行代码,甚至只是更改一个命名。

@SpringBootApplication

class Application(

private val hqCartGateway: HqCartGateway, // implements CartGateway interface

private val itemGatewayImpl: ItemGatewayImpl, // implements ItemGateway interface

private val promoGateway: PromoGateway) // also implements ItemGateway interface

) {

fun main(args: Array<String>) {

SpringApplication.run(Application::class.java, *args)

}

// These usecase beans are needed for proper dependency injection.

// A use case may need two different implementations of the same interface, for example, ItemGateway.

// Spring cannot know which each one should be. Other class signatures would be Spring-injected.

//

// class AddItemUseCase(

// private val hqCartGateway: CartGateway, // only 1 CartGateway implementation in the app

// private val itemGatewayImpl: ItemGateway, // 2 ItemGateway implementations

// private val promoGateway: ItemGateway)

@Bean

fun addItemUseCase(): AddItemUseCase {

return AddItemUseCase(hqCartGateway, itemGateway, promoGateway)

}

}

网关入口层

网关入口层(gateway)可能是微服务事件中最重要的一个层次了。这是一个应用通向世界的大门。我仿佛听见Pinky和Brain在说(Pinky和Brain是美国著名的一个动画片)。

Pinky: Brain, 我们今天要做什么?

Brain: 做我们一直做的事情,Pinky, 再开一个大门,然后去接管世界

在网关入口层的每一个程序包内都封装了一个连接到应用的不同服务,包括一个本地的数据库。用领域驱动开发的概念来说,它们代表着限界上下文.

你也可以说这一层包含了应用的所有的输入-输出,读取-写入,发送-接收, 发布-消费。应用中连接其他服务的部分都应该包含在这一层里面。这一层中的任何包都不需要了解其他的网关,也不需要了解任何的业务逻辑。它们了解的仅仅是一个领域对象(一个向下的依赖关系)以致于它们可以在它们的数据对象以及应用的数据对象之前做转换,适配。用例层(usecase)仅仅通过包含在用例层的接口与网关层进行交互。

每一个包都会有三个程序类:请求类,响应类和端口类。所有的特定的,底层的关于连接的技术代码都会在端口类里。所有与网关层的通信代码都需要包含端口类里、通常一个不同的端口代表着一个不同的网关入口。

请求类是向外发送的网关对象,它可以将领域对象映射转换而来。响应类是向内的网关对象,它可以将自己映射转换为领域对象。注意,应用中可能会有多种不同类型的请求类和响应类。

你可能注意到,在其他的一些地方,人们会用基础设施层(infrastructure)和适配器层(adapter)去代替网关入口层。我就觉得,这两个术语就像服务和处理一样显得有一些宽泛。并且,不像这两个更加偏技术的术语,命名为网关入口层更能清楚的表示连接到真实世界的通道。我们要更多的使用KISS原则!

appConfig/

gateway/

hq/

    HqCartGateway.kt // see code below

    HqRequest.kt

    HqResponse.kt

    HqPort.kt

item/

promo/

rest/

    cart/

        CartController.kt

        CreateCartRequest.kt

        CreateCartResponse.kt

        UpdateCartRequest.kt

        UpdateCartResponse.kt

    item/

        ItemController.kt

        ItemRequest.kt

        ItemResponse.kt

usecase/

xdomain/

@Service

class HqCartGateway(

    private val hqRequest: HqRequest,

    private val hqResponse: HqResponse,

    private val hqPort: HqPort)

: CartGateway {

override fun createCart(cart: Cart): Cart {

    val cartPostRequest = hqRequest.mapPostRequestFrom(cart)

    val cartPostResponse = hqPort.createCart(cartPostRequest)

    return hqResponse.mapToCart(cart.storeId, cartResponse)

}

override fun updateCart(cart: Cart): Cart {

    ...

}

}

class HqResponse(...) {

fun mapToCart(...): Cart {

    ...

}

private fun validate() {

    // validate Response NOT business data

}

}

用例层

用例层包含应用处理特定行为的逻辑。这些用例包含了领域对象之间,或者领域对象和网关入口层的交互。它们会清晰的展示应用中的每一个用例究竟是干什么的。所有麻烦的输入输入代码被简单的接口方法调用给替代了。所有这些的复杂性被抽取到了网关入口层中。

更理想的是,这使整个应用包含的所有的用例类都能在一个屏幕中完整的显示出来,它清晰的展示了应用中所包含的所有业务操作。

有时候,一个用例类没有必要去包含整个特性的实现代码。一个特性的实现代码可以由多个用例组成。如果一个类包含了一整个特性的实现代码并且将一些用例用私有方法实现,代码逻辑就会不可避免的变得杂乱无章,最终变得像意大利面一样显得混乱。更好的做法是,将一些特性封装在子程序包中。

appConfig/

gateway/

usecase/

cart/

    CartGateway.kt

    CreateCartUseCase.kt

    UpdateCartUseCase.kt

    AddItemUseCase.kt

    RemoveItemUseCase.kt

item/

    ItemGateway.kt

xdomain/

class AddItemUseCase(

private val cartGateway: CartGateway,

private val itemGateway: ItemGateway,

private val promoGateway: ItemGateway

) {

// The whole enchilada!!!

fun addItem(cart: Cart, item: Item): Cart {

    val storedItem = itemGateway.findItem(item)

    val promo = promoGateway.findItem(item)

    storedItem.calcFinalPrice(promo)

    cart.addItem(storedItem)

    val storedCart = cartGateway.save(cart)

    return storedCart

}

}

领域逻辑层

领域逻辑层中包含了应用的业务对象,DDD术语中的实体,聚合以及值对象。除非这个对象是一个聚合类,否则每一个对象应该仅仅关注自己内部的业务逻辑。更重要的是,它们对于它们的上层应该一无所知。它们不关心上层的输入,输出对象,也不关心这些输入输出对象是如何在用例层中使用。

注意:这些对象可能包含一些并不是它们自己业务逻辑中用到的字段。这是因为它们需要有能力在所有的网关请求/响应类做转换。这个做法消除了应用与网关入口数据之间繁杂的牵连。对象中包含所有网关入口所需要的字段也使得网关入口对象可以变得更加清晰,整洁。因为它们只需要访问它们感兴趣的领域对象字段而忽略其他的。这大大的降低了整个应用的复杂度。

appConfig/

gateway/

usecase/

xdomain/

Cart.kt

Item.kt

Order.kt

Promo.kt

data class Cart(

    val storeId: String,

    val cartId: String,  // or UUID

    var status: CartStatus? = null,

    val items: ArrayList<Item> = arrayListOf()

) {

fun addItem(item: Item): Cart {

    items.add(item)

  validateCart()

    return this

}

private fun validateCart() {

    // validate business rules NOT gateway Responses

}

}

总结

所以,这样做给我们带来了什么?非常清晰的关注点分离!非常符合SOLD原则的软件应用。

试着想象一下,假设需要找到为购物车中的物品计算税金的代码逻辑,还会是一件难事吗?当然不,我们只需要去用例层中找相应的类即可。

再想象一下,当添加一个特性的时候,还会因为将代码放在哪而犯难吗?答案也是不。

整洁架构和领域驱动设计可以使我们构建最优雅的软件应用,并且它们也可以适用于处理最复杂的软件系统。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK