开发者社区> ljking7002-> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

【组件设计开发】采用领域驱动设计设计和开发可组装的组件

简介: 采用领域驱动设计设计和开发可组装的组件
+关注继续查看

领域驱动设计是Eric Evans出版的《Domain Driven Design》一书中提出的,其在关于治理复杂的软件方面的效果被业内普遍认可。本文尝试理清一些重要概念,为使用领域驱动设计研发可组装的组件提供一种思路。

业务建模

领域驱动设计是一种业务建模的方法,业务建模的重要性往往容易被忽视,缺少业务建模往往就导致软件代码质量差,难以扩展和演进。业务建模不仅是一个寻找解决方案的过程,首先是一个定义问题的过程,清楚正确的定义问题,可以大大降低实现成本,错误的问题定义,实现的时候会出现各种奇技淫巧,往往软件质量也更低。

比如某公司差旅系统需要员工将发票导入系统进行报销,财务部门希望系统能够自动识别发票的种类(如餐饮,住宿,交通),以及对应的金额,从而统计各种类员工差旅的花销。
image.png
如果不对问题仔细定义,直接着手设计解决方案,一种陷入技术思维的实现方式可能是如下通过图像识别:

  1. 我们需要能够扫描员工上传的发票图片。
  2. 通过训练机器学习,只要有足够多的发票样本,我们可以让机器自动识别发票的种类和金额。

但如果对问题稍加定义,从业务的视角去思考,可能会得出完全不一样的实现方式:

  1. 让员工提交差旅的时候选择发票类型和对应的金额。
  2. 并上传对应的发票,以供后台财务人员审核。

以上两种方式,假设团队从无到有进行交付,图像识别+机器学习相对于让用户输入相关信息的方式,实现成本天差地别。

业务建模的目标

业务建模的目标是定义问题,并让所有参与者都能接受,包括业务方和技术方,然后在特定的架构约束下去落地实现。

image.png
领域驱动设计中,大家耳熟能详的一些词汇,如统一语言(Ubiquitous Language),大声建模,知识消化(Knowledge Crunch),战术模式等,都围绕这些展开,如统一语言,大声建模是为了达成共识,战术模式,实体,值对象,聚合,仓储等都是为了实现落地的总结。

统一语言

下次你参加需求评审会,或者设计讨论会的时候,不妨认真听一下,你会听到一些常用的业务术语,这些业务术语会用于描述功能,一些显而易见的业务术语会被用于编码实现,用作具体的对象名称,类名,或者方法名。寻找这些术语的过程,其实就是建模的过程,而寻找出来的这些术语,构成模型本身,模型本身也构成每个团队,产品或项目独有的统一语言(Ubiquitous Language)。

image.png

业务人员和技术人员建立一个通用的统一语言,用于沟通需求,描述实现成本。统一语言通常是在讨论中逐渐形成的。能够用于统一语言的领域模型一定是具有丰富业务知识的有血有肉的领域模型,领域模型的血和肉,是在充分讨论,并进行知识消化(Knowledge Crunch)的过程中形成的。

比如如下领域模型

image.png
上述领域模型包含了一个航班可以登记多名旅客的业务知识。对于这样的一个业务知识,我们可能在代码中有对应的实现:

@Data
class Flight {
    List<Passenger> passengers;
    List<Seat> seats;

    public void register(Passenger passenger) {
        this.passengers.add(passenger);
    }
}

public void booking(Flight flight, Passenger passenger) {
    flight.register(passenger);
}

在这样的模型基础上,航空公司一个典型的超卖的业务规则,可以通过修改如下代码来达到:
image.png

public void booking(Flight flight, Passenger passenger) {
    long maxBooking = Math.round(flight.getSeats().size() * 1.1);
    if (flight.getPassengers().size() + 1 > maxBooking) {
        throw new RuntimeException("reject due to over booking");
    }
    flight.register(passenger);
}

这样的实现,虽然程序能够工作,但是一条重要的业务规则被隐藏在了卫语句中,随着代码越来越多,人员更迭,这条隐藏的业务规则就无法被明显的表达了,非业务人员很难将这条业务规则在软件内部找到对应的位置。

经过讨论,消化知识,我们可以将隐藏的业务只是显化,我们可以得到如下领域模型:
image.png
对应的代码实现,我们可以显示的定义一个单独的OverbookingPolicy类:

class OverbookingPolicy {
    void checkOverbooking(Flight flight, Passenger passenger) {
        long maxBooking = Math.round(flight.getSeats().size() * 1.1);
        if (flight.getPassengers().size() + 1 > maxBooking) {
            throw new RuntimeException("reject due to over booking");
        }
    }
}

public void booking(Flight flight, Passenger passenger) {
    overbookingPolicy.checkOverbooking(flight, passenger);
    flight.register(passenger);
}

这个简单的例子看到了模型作为统一语言,如何与代码实现相互影响和绑定,以提高代码的表达能力。

综上,统一语言为业务人员和技术人员提供沟通的媒介,并且与编码实现绑定。修改模型即修改代码

当模型变得庞大,关系变得复杂的时候,为了控制软件的复杂度就需要划分边界了。

image.png

边界的划分受到很多因素制约,其中很重要的一部分是康威定律,软件架构受制于组织结构,所以讨论软件架构如何与组织结构动态协调,甚至改变团队拓扑结构,这部分的研究称之为战略设计。将边界内具体落地的模型细节的处理和研究,称之为战术设计

image.png

战术设计

Eric Evans在战术设计中提出的一系列模式,已经逐渐被社区广泛接受,在一些技术框架中也被广泛应用。在面向对象编程语言中,一个运行的程序由成千上万的对象组成,不同的对象有不同的职责,对于一些明显的对象特征,如实体,值对象,这些概念以被业内普遍接受。

实体/值对象

实体(Entity)

很多对象不是通过他们的属性定义的,而是通过连续性和标识定义的。当一个对象由其标识(而不是属性)区分时,在模型中应该主要通过标识来确定该对象的定义。从技术上实体是一个跨越时间的对象,对象本身是可变的额,只要ID未发生改变就仍然是同一个对象。这里的ID不同与Java语言本身的对象ID,而是从业务角度切实存在的一个标识。

值对象(Value Object)

很多对象没有概念上的标识,它们描述了一个事物的某种特征。比较两个值对象是否相同的依据是看其属性是否相同,值对象通常是不可变的,如果值发生变化,则是另外一个对象。

实体还是值对象?

一个对象是实体还是值对象,并没有硬性规定,取决于业务场景,比如如果我们对电影票建模,不同的问题定义(或业务场景),根据所关心的不同业务特征,同样是电影票,可能是实体,也可能是值对象。

如果一张电影票,上面通过座位号座位ID,那么电影票就是一个实体,ID变了,实体本身就变了。但如果我们构建系统所解决的电影院售票问题,是像中国早年间录像厅一样,只是一个入场券,而不跟具体座位绑定,即不要求特定的ID,那么电影票就是一个值对象。

聚合(Aggregate)

丰富的业务知识,不止是存在与领域对象的属性里面,属性只是业务数据,而对于复杂程度更高的业务规则,往往发生在对象与对象产生关系的时候。一些对象放在一起,紧密协作,完成业务动作,适合被放在一起讨论或交流。领域驱动设计使用聚合(Aggregate)的概念来表示这种富含业务知识的模型关系。

image.png
同一个聚合内的对象,具有同样的生命周期。比如我们用一种更简单的方式来表示聚合:

image.png
一个订单(Order)中包含多条订单行(OrderItem)。一辆汽车(Vehicle)包含4个轮胎(Wheel)。上图表示了两个聚合。每一个聚合都有一个聚合根(Aggregate Root),聚合根作为聚合内一个重要的特殊实体,维护着整个聚合的业务一致性。

在实现层面,聚合操作需要程序员遵循一些特定的原则:

  1. 聚合内需要保证业务一致性。
  2. 业务操作从聚合根入口。
  3. 聚合内对象的生命周期一致,聚合根从数据库中删除,则聚合内其他对象也应该删除。
  4. 聚合根需要有全局ID,聚合内其他对象只要求局部ID即可。

一个聚合的典型例子是:

class OrderItem {
    String name;
}

class Order {
    String id;
    List<OrderItem> items;
}

对于添加订单行这样的业务动作的实现,如果不从聚合根入口:

List<OrderItem> orderItems = order.getItems();
orderItems.add(new OrderItem("牛奶"));

这样的问题是,业务一致性容易被打破,比如对于订单一个常见的需求是限制一个订单内的某种商品数量,比如每个订单限购两台苹果手机。如果没有一个专门的地方来固话这条业务规则的话,这个业务规则可能就会被打破,系统中就可能出现脏数据。

采用聚合的方式,由聚合根入口的方式如下:

class Order {
    String id;
    
    List<OrderItem> items;
    
    void addOrderItem(OrderItem orderItem) {
        //校验订单限制
        this.items.add(orderItem);
    }
}

order.addOrderItem(new OrderItem());

外部只需根据ID获得order, 然后调用order的方法进行操作。对于订单限制在统一的地方去做,只要不直接改数据库数据,对于Order的操作在程序内部都由聚合跟入口,因此业务一致性得以保证。这是一个简单的例子,对于并发修改等复杂场景,聚合根也能发挥优势。

当然聚合中并不要求只能是集合代表关系,一些集合操作可以放到集合对象上。集合对象用于解决如分页,大聚和等问题等提供方法,如:

class Order {
    String id;
    OrderItems items;
}

class OrderItems {
    Paged<OrderItem> items; //无需将对象全加载到内存中。
    
    Integer size() {
        return total; //从数据库中查总数。
    }
}

领域服务(Domain Service)

领域模型表达了静态概念,聚合表达了概念与概念之间的关系,业务流程在其中并未表达。领域服务用于建模一个具体的需要多个领域概念交互的流程性的稳定的业务知识。

image.png
非贫血模型中,动作是实体或值对象上的方法,但有时候一个方法的职责很难归到某一个对象上。这时候就引出了领域服务。领域服务操作的聚合根。抽象领域服务之前,先想想能不能抽象业务该概念,如无法抽象,则使用领域服务。

应用服务(Application Service)

稳定的业务逻辑放在领域服务中,非业务逻辑,如鉴权,校验,或者不稳定易变的业务逻辑放在应用服务中。应用服务与领域服务的区别:

  1. 领域服务关注稳定的业务逻辑。
  2. 应用服务关注非稳定业务逻辑的编排。

应用服务和领域服务的区别是变化频率,无论是应用服务还是领域服务,都应该尽量薄,因为很容易将业务知识隐藏在过程式代码中。
image.png

仓库(Repository)

对象在系统中存在两种状态,被加载到内存当中参与计算的内存态,以及持久化在数据库中的持久态,对象在这两种状态之间来回转换。

image.png
仓库负责按聚合的方式来做这两个状态之间的转换。对于非聚合根以外的对象使用Repository实际上是坏味道,因为通过这样的Repository可以获得一个聚合的一部分,这样就破坏了聚合的原则,导致一致性的维护出现问题。

工厂(Factory)

复杂对象的构造器,其实就是设计模式中的工厂模式,其职责是创建聚合。不是所有的领域模型都需要对应的工厂。有时候构造函数就够了。

image.png

发现领域模型

发现领域模型的过程,就是建模的过程,建模的过程可以多种多样,事件风暴工作坊作为一种注重讨论过程的建模方法,提供了建模的特定流程,一定程度上约束了无边际的讨论。

image.png
事件风暴通过工作坊的形式,聚集领域专家和技术团队,在一面墙旁边进行领域建模,过程中会大声的讨论,产生分歧,达成共识。事件风暴工作坊通过四个高阶步骤来完成建模。

  1. 事件风暴
  2. 命令风暴
  3. 寻找领域模型
  4. 划分边界

寻找领域模型会罗列所有的业务概念和聚合,划分边界会过渡到战略设计部分。

示例

image.png

通过一个简单的例子,来看看采用事件风暴的形式,如何来发现领域模型(战术设计),并最终划分边界(战略设计),战略设计我们在后面战略设计章节讨论。示例有如下简化的业务规则(本故事纯属虚构):

  1. 旅客可以选择航班,并决定是否预订,支付预订订单。
  2. 航空公司对于每个航班可以有一定比例的超卖。
  3. 旅客到达急产过后,机场可以根据当前登机状态,广播通知未登记的旅客。

不经过详细讨论和思考,直觉上我们可以会划分三个子域,(后台管理,预订,机场登机通知),根据经验直觉来的设计,不一定是合理的。

第一步、事件风暴
按照时间顺序,头脑风暴出所有领域事件。领域事件是领域专家关心的,在业务上真实发生的,有业务价值的,在系统中产生痕迹的事件。用橙色的卡片(贴纸)代表领域事件。所有领域事件都使用“名词已动词”的过去完成时结构,如“订单已创建”。对于我们示例的事件流,可放大下图查看。
image.png

时间允许的话,我们可以先按照时间梳理场景化的用户旅程地图,然后根据用户旅程地图,来寻找领域事件。哪些东西算是领域事件呢?我们问如下的一些问题:

  1. 是否在系统内产生了某种数据?
  2. 是否触发了某种流程?
  3. 是否导致了系统的状态变化?
  4. 是否往外发出了消息?

结合一些例子,比如查询是否是一个领域事件?查询取决于是否在系统中留痕,比如一个用户行为分析系统,查询也是用户的一种行为统计类型,那么查询就是领域事件。所以这取决于领域专家是否关心。再比如数据已存储是一个领域事件吗?这或许是一个技术事件,但是领域专家并不关心是否已存储,领域事件应该面向业务,而不是面向技术。

第二步、命令风暴
按照上一步风暴出来的事件,为所有事件寻找触发事件的命令,以及对应的触发源。用蓝色的卡片(贴纸)代表命令,所有命令都采用“动词”的形式。

image.png
可能的触发源:

  1. 用户界面
  2. 外部系统调用
  3. 定时任务
  4. 其他事件

一些注意事项:

  • 命令名字会变成模型的行为名称。最终会通过方法名体现。
  • 命令不只是简单的把事件反过来。需要思考其业务语言。
  • 对于用户界面的命令需要设计用户界面。

第三步、寻找领域模型
通过寻找名词的方法,把时间线上的相同名词放在一起,用大的黄色卡片(贴纸)标识,把命令放在其左边,对应的事件放在右边。
image.png

这一步寻找出来的领域概念,已经有其价值。因为他定义了业务的关键问题,同时也能指导代码实现,比如业务概念就是一个类,命令可以是方法名,事件是状态的变化,如果是基于事件的架构风格,则向外广播事件。并且是在工作坊中共创的结论,默认是所有相关的人达成了共识。

第四步、识别聚合
仔细思考,讨论,哪些东西是否应该在一个聚合里,哪些应该是不同的聚合。参考聚合的原则。
image.png
注意事项:

  1. 不同的群体建模,可能形成不同的模型
  2. 重要的是模型能解决业务问题,并且能被大家理解,达成共识。
  3. 模型没有对错,设计会影响实现而已。

我们在战略设计部分会来看事件风暴工作坊接下来关于战略设计的考虑。

战略设计

随着系统的增长,它会变得越来越复杂,当我们无法通过分析对象来理解系统的时候,就需要掌握一些操纵和理解大型模型的技术了。

为了解决多个模型的问题,我们需要明确定义模型的范围——模型的范围是软件系统中一个有界的部分,这部分只应用一个模型,并尽可能保持统一。

根据团队组织、软件系统的各个部分的用法以及物理表现(代码和数据库模式)等来设置模型的边界,在这些边界中严格保持模型的一致性,而不要受到边界之外问题的干扰和混淆。

image.png

统一语言在同一个上下文内要求无歧义。同一个概念在不同的上下文名字可能一样,但关注点不一样。

在事件风暴工作坊中,找出领域模型后,对于复杂项目,可能会有很多领域模型,此时就需要进行战略设计,边界划分。

第五步、划分边界(限界上下文)-- 接前文事件风暴
划分边界有很多种角度,不同角度在不同的项目中所占比重可能不一样,

第一种:按照业务耦合度划分

image.png

第二种:按照弹性边界划分

弹性是云所带来的好处,由于弹性可以减少计算和运营成本,所以在云原生架构下,弹性边界用于划分边界,有时候可能比业务边界还重要。
image.png

那么弹性边界是否就与业务边界一模一样呢?答案是不一定,比如随着业务的发展,航班信息检索,和真正下单预订的容量可能会不一样。可能在不同的弹性边界。

第三种、按照技术异构划分

假设有一个PDF导出的需求,我们需要把用户在网页端看到的机票,导出为PDF。

  1. 因为使用了chrome无头浏览器,浏览器天生对Javascript友好,所以这个上下文采用Javascript实现。
  2. 由于系统其余部分使用Java实现,所以单独拆分一个上下文。不同的功能有自己所合适的技术,更适合放到不同的边界。

image.png

第四种、按照业务变化评率划分

三个月引入一个新需求的模块,与2年才引入一个新需求的模块,如果拆分到两个模块,可以有效的隔离变化的传播。

第五种、按团队组织结构划分

康威定律对于软件架构的作用实际比我们想象的更大:

康威定律(康威法则,Conway’s Law)
“设计系统的架构受制于产生这些设计的组织和沟通结构。” —M. Conway

按照人员组织来划分边界,并按照团队维护不同的边界,往往更稳定。战略设计同样可以用于指导组织结构,这就是逆康威定律,用软件架构设计中划分的边界来设计组织结构。

划分微服务

image.png
有了边界(限界上下文)后,微服务的划分就可以参考这些边界,理想情况下一个微服务就可以对应一个限界上下文。但是考虑到微服务的管理成本,需要结合自身的基础设施管理能力来权衡。

编码实现

任何建模方法,如果无法落地实现,无法指导程序员如何写代码,都不能算上是成功的建模方法。实际上软件的复杂度除了复杂的业务以外,实际存在与代码当中。模型落地到代码有什么挑战呢?

模型落地到代码的挑战

用代码把领域模型表达出来并不简单,需要很强的抽象能力。加上代码中存在的各种技术顾虑。如下代码示例,模型代码只占一小部分,长期下去,要从这段代码中找出哪些是表达业务的代码变得几乎不可能。写出计算机认识的代码很简单,写出人能读的代码才是挑战。
image.png

为了让表达业务的模型代码更显示的表达出来,架构师们在架构上想了很多办法,无论哪种架构风格,几乎都会有专门的一层,或一个地方,来存放模型相关的代码,比如经典的分层架构:

经典分层架构的问题

image.png
经典分层架构中模型层的目的是存放模型代码。上层依赖下层,下层不能依赖上层,目的是为了隔离变化传播,让不稳定的层依赖稳定的层。但是从实际情况来看,基础设施层的变化速率并不低,即基础设施层并不是最稳定的层,原因是我们对于基础设施的变革也没有停止过,从物理机,到虚拟机,到云计算,到微服务,以及FaaS,从Oracle为代表的大型关系型数据库,到轻量的MySQL关系型数据库,到NoSQL的探索,不同的消息中间件等等,这些基础设施或中间件产品,本身也在不断更新换代,所以模型层依赖于基础设施层并不能有效的隔离变化。

六边形架构的问题

image.png
六边形架构把模型放在一个绝对独立的相对核心位置,不再依赖于基础设施,而是让基础设施变成一种端口/适配器。理想情况下,六边形架构可以让模型非常纯粹的表达业务,几乎完全的隔离了技术顾虑。但落地的时候也会碰到一些现实的实际问题,一个最大的实际问题就是,模型始终需要被持久化,由于没有永远不宕机,无限大内存的计算机,所以持久化无法避免。使用六边形架构的实现,对于模型的操作通常会有如下步骤:

  1. 通过Repository找到对应的聚合。
  2. 完成业务操作。
  3. 持久化整个聚合的变化到数据库。

用代码描述:

public void flightStart(Request request) {
    Optional<Flight> flight = flightRepository.findById(request.flightId);
    flight.ifPresent(flight::start);
    flightRepository.save(flight);
}

这里的问题是,flight的变化会导致多余字段的数据库更新,在一些特殊情况下会带来性能下降,所以六边形架构依赖于一个精巧的ORM框架来解决这些问题。

美好的执念

绝对独立的领域层是美好的执念,接受领域层的不独立,往往更合适。

“Technology can help you implement a Model or can fight you all the way. When it fights you or doesn’t provide good mechanisms you just workaround with it.”
—Someone says Eric Evans said this.

技术能够帮助你实现模型,也会阻碍你实现,如果阻碍了你实现,那就想办法学会与其相处。

首先应该被打破的是对基础数据库的依赖,因为持久化必然存在,所以我们应该大方承认模型依赖于基础库的持久化,而其他基础设施则不一定,不是所有模型变化都必须发一条消息出来。所以我们认为对基础库的依赖是合理的依赖,是合理的类聚。

这对我们的代码有什么影响呢?我们可以写这样的代码:

public void flightStart(Request request) {
    Optional<Flight> flight = flightRepository.findById(request.flightId);
    flight.ifPresent(flight::start); //运行后持久化就完成了
}

class Flight {

    @Autowired
    FlightMapper flightMapper;

    void start() {
        this.status = FlightStatus.STARTED;
        this.flightMapper.updateStatus(this); //对数据库进行更新
    }
}

在六边形架构的基础上,我们可以得到如下一张架构示意图:
image.png

这样的架构也需要一些解决一些细节问题,比如上述代码示例中的FlightMapper,一定会引起一部分程序员的不舒适,如果说不舒适是可以通过改变认知,放弃执念来解决,那么现实是采用Spring这段代码都不工作,因为Flight从数据库中通过Repository重新构造出来的,对于熟悉Mybatis框架的程序员来说,因为Flight由ORM实例化,Spring(依赖注入框架)并未参与,所以@Autowired会抛出无法找到Bean的异常。不过这个问题是可以通过参与框架的生命周期来解决的,比如我们可以参与Mybatis的构建对象的过程,让Spring可以去注入依赖:

public class InjectableObjectFactory extends DefaultObjectFactory {

    private AutowireCapableBeanFactory beanFactory;

    InjectableObjectFactory(AutowireCapableBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Override
    public <T> T create(Class<T> type) {
        T t = super.create(type);
        autowire(t);
        return t;
    }

    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        T t = super.create(type, constructorArgTypes, constructorArgs);
        autowire(t);
        return t;
    }

    private <T> void autowire(T t) {
        if (t != null) {
            beanFactory.autowireBean(t);
        }
    }
}

@Configuration
public class MybatisConfiguration {

    @Autowired
    AutowireCapableBeanFactory beanFactory;

    @Bean
    ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setObjectFactory(new InjectableObjectFactory(beanFactory));
    }
}

组件设计

组件设计中如何划分组件,边界是什么,与限界上下文的思路如出一辙,我们可以将限界上下文根据可组装的属性,做出映射,则得出一个系统如何可以由若干组件组成。

总结

本文从业务建模出发,描述了领域驱动设计战术战略设计的基础知识,并通过事件风暴工作坊的介绍给出了实际的例子,在编码实现中给出了一些落地的挑战和解决方法,同时为组件设计提供了一种思路。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云概述及建站(2)|学习笔记
快速学习阿里云概述及建站
66 0
【万字长文】从零配置一个vue组件库
【万字长文】从零配置一个vue组件库
88 0
钉钉宜搭6月15日版本更新:手写签名和定位组件来啦!
本次版本更新主要针对流程、表单进行了组件能力升级,新增了手写签名和定位2个组件,同时升级地址、人员和部门3个组件。
2226 0
RVB2601应用开发实战系列一: Helloworld最小系统
RVB2601开发板是基于CH2601芯片设计的生态开发板,其具有丰富的外设功能和联网功能,可以开发设计出很多有趣的应用。为了开发者更好的了解如何在CH2601上开发应用,本文介绍了如何移植对接CH2601芯片到YoC最小系统,开发第一个我的helloworld程序。
410 0
双11技术播报特别篇-阿里云中间件双11项目负责人涯海专访
本期双11技术播报特别篇,我们邀请到了阿里云中间件团队的大队长涯海,他将给我们分享整个团队在双十一中遇到的好玩的事情以及一些技术突破。
3765 0
我用阿里云部署的个人网站并帮兄弟表白
兰州大学信息科学与工程学院的一位计算机技术专业研究生,乘阿里云专属云翼,在其上部署了个人博客和自主开发的文本标记系统,极大释放成本,提高了开发效率。
3249 0
(六):Winelib开发组件2
版权声明:您好,转载请留下本人博客的地址,谢谢 https://blog.csdn.net/hongbochen1223/article/details/49682313 话接上节!! (二):编译资源文件:wrc 为了编译资源,你应该使用Wine资源编译器,简写为wrc,该编译器会生成一个二进制.res文件。
718 0
备份恢复4.2——rman恢复基础概念
rman恢复与用户管理的备份恢复一样,都分为完全恢复和不完全恢复,都需要工作在archivelog模式下。 rman10g之后只保留了0级和1级备份,1级备份分为:cumulative(累积增量)和differential(差异增量)两种模式,如果不加关键词...
989 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
《前端智能化实践》——逻辑代码生成
立即下载
ui-model,跨框架复用
立即下载
为并行图数据处理提供高层抽象/语言
立即下载