DDD系列 - 第2讲 从贫血模型、事务脚本到面向对象(富血模型)、DDD领域模型的跨越

news2024/11/25 19:34:54

目录

    • 一、灵魂拷问
    • 二、CRUD Boy现状
    • 三、贫血模型
    • 四、事务脚本
    • 五、从贫血模型演变到面向对象(富血模型)
    • 六、借助DDD领域模型摆脱事务脚本
    • 七、更多

一、灵魂拷问

Java作为面向对象的编程语言,使用Java编程的你面向对象了吗?

二、CRUD Boy现状

起初学习Java时,老师给我们介绍了Java语言各种面向对象的特性:封装(属性、方法)、继承、多态、关联关系(关联、依赖、组合、聚合)等等,当时觉得Java很牛B。工作了以后,写着写着代码连我们自己都没有觉察到我们写下Java代码已经变味了,好像丢失了面向对象分析与设计的精髓,似乎跟面不面向对象没啥关系了。作为一名 资深后端CRUD Boy,对于SpringMvc代码信手拈来,随便啥功能上来都这么整:

  • 数据库设计(实体表、关系表)
  • 设计接口、定义Controller
  • 使用Mybatis定义DAO接口、映射DB实体关系为Java POJO(实体对象、关系对象、贫血模型
  • 编写Service写业务逻辑(事务脚本)、调用DAO操作POJO

细心点就会发现,这里哪有面向对象啊?

  • POJO实体对象,仅有属性和getter/setter方法,对象的业务操作根本没有,严格来说不算是面向对象建模
  • Service有面向对象吗?完全就是面向过程编程,用方法(事务脚本)怼操作。
  • Controller为接口层,处理用户请求,纯技术维度的, 跟对象建模没关系。

三、贫血模型

假设存在实体:商品、商品标签,
且商品支持绑定多个商品标签。

数据库设计:

-- 商品表
CREATE TABLE goods (
	id bigint(20) NOT NULL COMMENT '主键ID',
	category_id bigint(20) NOT NULL COMMENT '所属分类ID',
	goods_name varchar(120) NOT NULL COMMENT '商品名称',
	manufacture_date date NOT NULL COMMENT '生成日期',
	expiration_date date NOT NULL COMMENT '过期日期',
	goods_weight decimal(10, 2) NOT NULL COMMENT '商品重量',
	goods_weight_unit varchar(16) NOT NULL COMMENT '重量单位',
	goods_desc varchar(1024) NOT NULL COMMENT '商品描述',
	goods_price decimal(20, 2) NOT NULL COMMENT '商品价格',
	goods_status int(11) NOT NULL COMMENT '商品状态(10已上架, 20已下架)',
	create_time datetime NOT NULL COMMENT '创建时间',
	update_time datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB;

-- 商品标签表
CREATE TABLE goods_tag (
	id bigint(20) NOT NULL COMMENT '主键ID',
	tag_name varchar(64) NOT NULL COMMENT '标签名称',
	tag_desc varchar(512) DEFAULT NULL COMMENT '标签描述',
	create_time datetime NOT NULL COMMENT '创建时间',
	update_time datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB;

--  商品标签绑定表
CREATE TABLE goods_tag_binding (
	goods_id bigint(20) NOT NULL COMMENT '商品ID',
	tag_id bigint(20) NOT NULL COMMENT '商品标签ID'
) ENGINE = InnoDB;

Java POJO:

/**
 * 基础数据对象
 */
@Data
public class BaseEntity {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}


/**
 * Goods数据对象
 */
@Data
@TableName("goods")
public class Goods extends BaseEntity {
    private Long categoryId;
    private String goodsName;
    private LocalDate manufactureDate;
    private LocalDate expirationDate;
    private BigDecimal goodsWeight;
    private String goodsWeightUnit;
    private String goodsDesc;
    private BigDecimal goodsPrice;
    private Integer goodsStatus;
}

/**
 * 商品标签
 */
@Data
@TableName("goods_tag")
public class GoodsTag extends BaseEntity {
    private String tagName;
    private String tagDesc;
}


/**
 * 商品与标签绑定关系 - 数据对象
 */
@Data
@TableName("goods_tag_binding")
public class GoodsTagBinding {
    private Long goodsId;
    private Long tagId;
}

通过以上代码,不难发现Goods、GoodsTag、GoodsTagBinding作为POJO,仅是对数据库表的映射,仅保留有getter/setter方法作为操作属性的手段,其中并没有任何实际的业务处理逻辑,实体对象中仅有属性没有操作,仅是作为数据的载体,如此即为贫血模型,而大量的业务操作都写到了Service实现中,导致Service过于臃肿。

四、事务脚本

由于DAO层采用了Mybatis,相较于完整的 ORM - 对象(Object)关系(Relation)映射,面向对象间的关联关系是无法直接通过Mybatis进行映射的,如以面向对象的方式建模商品和商品关联的标签,如下仅包含2个对象:Goods, GoodsTag,而商品与标签的关联关系可以通过Goods.goodsTags属性进行建模:

class Goods {
	private Long id;
	private String goodsName;
	//...
	//商品关联的标签集合
	private Set<GoodsTag> goodsTags;	
}

而在Mybatis中,除了Goods、GoodsTag对象,还需要格外引入数据库中的关系表GoodsTagBinding这第3个对象来表示商品和标签的关联关系,正是由于这种我们习以为常的映射,导致我们只能面向数据库编程,我们上层的业务逻辑只能针对Mybatis中的实体进行操作,除了需要操作业务中的对象实体Goods、GoodsTag,还不得不操作数据库中的关系对象GoodsTagBinding,使得本来的面向对象编程不得不变为面向数据库编程,如此即为事务脚本模式

事务脚本(Transaction Script)是一种应用程序架构模式,主要用于处理简单的业务场景。它将业务逻辑和数据库访问紧密耦合在一起,以便实现对数据的操作。
简单描述就是将“脚本”(SQL)进行打包,然后放在一个“事务”中运行。这也就是“事务脚本”命名的由来。

例如Service中新建商品的代码如下:

/**
 * 商品服务实现类
 */
@Service
public class GoodsServiceImpl implements GoodsService {

    @Resource
    private GoodsMapper goodsMapper;

    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private GoodsTagMapper goodsTagMapper;

    @Resource
    private GoodsTagBindingMapper goodsTagBindingMapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SingleResponse<GoodsVo> createGoods(GoodsCreateDto goodsCreateDto) {
        //验证新增商品分类、标签是否存在
        this.validateGoods(goodsCreateDto.getCategoryId(), goodsCreateDto.getTagIds());
        
        //创建商品实体
        Goods goods = BaseAssembler.convert(goodsCreateDto, Goods.class);
        //设置商品状态
        goods.setGoodsStatus(GoodsStatus.UNSHELVED.getValue());
        //保存商品
        this.goodsMapper.insert(goods);
  
        //批量保存新的标签绑定关系
        if (!CollectionUtils.isEmpty(goodsCreateDto.getTagIds())) {
            for (Long tagId : goodsCreateDto.getTagIds()) {
                GoodsTagBinding goodsTagBinding = new GoodsTagBinding(goods.getId(), tagId);
                this.goodsTagBindingMapper.insert(goodsTagBinding);
            }
        }
        //转换GoodsDto
        return SingleResponse.of(BaseAssembler.convert(goods, GoodsVo.class));
    }

    /**
     * 验证商品分类、标签是否存在
     *
     * @param categoryId  分类ID
     * @param goodsTagIds 标签ID集合
     */
    private void validateGoods(Long categoryId, Collection<Long> goodsTagIds) {
        //验证分类ID是否存在
        Boolean existCategory = this.categoryMapper.existCategoryId(categoryId);
        Validates.isTrue(existCategory, "category id does not exist");

        //若标签ID集合为空,则直接验证通过
        if (Objects.isNull(goodsTagIds) || goodsTagIds.isEmpty()) {
            return;
        }
        //查询存在的标签数量
        Integer existGoodsTagCount = this.goodsTagMapper.countByIdIn(goodsTagIds);
        Validates.isTrue(existGoodsTagCount.equals(goodsTagIds.size()), "goods tags don't exist");
    }
}

受限于使用Mybatis仅对数据表进行了直接映射:Goods、GoodsTag、GoodsTagBinding,
且使用了贫血模型,我们不得不在上层的Service中编写大量的业务逻辑,直接面向数据库进行编程,
在面向对象的维度,商品和其关联的标签是作为一个完整的对象模型的,但是在面向数据库层面就变成了3个对象,所以Service中新增商品的逻辑就要额外操作GoodsTagBinding这个对象,可参见如上代码中createGoods方法就是典型的事务脚本模式:

  • 验证商品标签、分类是否存在
  • 转换商品、通过setter设置商品状态、保存商品Goods
  • 转换商品标签绑定关系列表、批量保存绑定关系GoodsTagBinding

至此,CURD Boy的三宗罪 就都清晰了,你占了哪几样:

  • Mybatis
  • 贫血模型
  • 事务脚本

还有一种事务脚本思维,例如修改订单的状态,面向数据库的思维就会想着执行SQL,且仅去修改相关的属性,而面向对象思维则把订单看做一个整体,对订单状态的修改也是对订单的修改,不去关心底层是修改一个属性还是修改全部属性,如此也即引出了DDD中的仓库Repository模式

事务脚本模式:

//OrderServcieImpl.updateOrderStatus中核心代码
this.orderMapper.updateStatus(orderId, newOrderStatus);

//OrderMapper.updateStatus方法
@Update("update orders set order_status = #{newOrderStatus} where id = #{orderId}")
Integer updateStatus(Long orderId, Integer newOrderStatus);

仓库模式:

//OrderServcieImpl.updateOrderStatus中核心代码
Order order = this.orderRepository.findById(orderId);
order.modifyStatus(newOrderStatus);
this.orderRepository.save(order);

五、从贫血模型演变到面向对象(富血模型)

贫血模型并没有对业务操作进行建模,仅有属性没有操作,
对象变成了对数据表的映射,进而导致想当然的面向对象编程变成了面向数据库编程,
Service承担了几乎全部的业务逻辑,最终呈现出的就是Servcie中的事务脚本模式。
模式没有好坏之分,只要适合就好。
但如果想要真正体验到面向对象建模的优势,将大量的业务逻辑都封装到对应的实体对象中,则可以借鉴DDD所倡导的富血模型、领域建模等思想。

所谓富血模型,就是对象中包含了大量的业务操作,不仅仅是数据属性的载体。
例如修改订单发货状态时,贫血模型中OrderServcieImpl.deliverOrder方法定义如下:

/**
 * 订单服务实现类
 */
public class OrderServiceImpl implements OrderService {
    //......
    
  	@Transactional(rollbackFor = Throwable.class)
	@Override
	public Response deliverOrder(OrderDeliverDto orderDeliverDto) {
	    Order order = this.orderRepository.selectById(orderDeliverDto.getOrderId());
	    Validates.notNull(order, "order not exist!");
	    
	    //验证订单状态
	    Validates.isTrue(OrderStatus.PAID.getValue().equals(order.getOrderStatus()), "order status is not PAID");
	    //修改订单已发货状态
	    order.setOrderStatus(OrderStatus.DELIVERED.getValue());
	    order.setExpressCode(orderDeliverDto.getExpressCode());
	    order.setDeliverTime(orderDeliverDto.getDeliveryTime());
	    order.setUpdateTime(LocalDateTime.now());
	    //更新订单
	    this.orderRepository.updateById(order);
	    
	    return Response.buildSuccess();
	}
	
}

以上代码中,充斥着大量的order.set方法,代码可读性差,代码意图不明确。
使用富血模型进行建模,则可将大量的set方法都统一封装到Order对象中的deliverOrder方法中,由Order对象完成它本该完成的业务职责,提升了业务的内聚性以及代码的可读性、可维护性,调整后代码如下:

/**
 * Order实体对象
 */
@Getter
@TableName("orders")
public class Order {
    private Long id;
    private BigDecimal orderPrice;
    private String receiveAddress;
    private OrderStatus orderStatus;
    private LocalDateTime payTime;
    private String expressCode;
    private LocalDateTime deliverTime;
    private LocalDateTime completeTime;
    private LocalDateTime cancelTime;
    
    /**
     * 发货订单
     *
     * @param expressCode 快递单号
     * @param deliverTime 发货时间
     */
    public void deliverOrder(String expressCode, LocalDateTime deliverTime) {
        //验证订单状态
        Validates.isTrue(OrderStatus.PAID.equals(this.getOrderStatus()), "order status is not PAID");
        //修改订单状态
        this.orderStatus = OrderStatus.DELIVERED;
        this.expressCode = expressCode;
        this.deliverTime = deliverTime;
        super.updateTime = LocalDateTime.now();
    }
}

/**
 * 订单服务实现类
 */
public class OrderServiceImpl implements OrderService {
  //......
  
  @Transactional(rollbackFor = Throwable.class)
  @Override
  public Response deliverOrder(OrderDeliverCommand orderDeliverCommand) {
      Order order = this.orderRepository.findById(orderDeliverCommand.getOrderId());
      Validates.notNull(order, "order not exist!");
      
      //订单已发货
      order.deliverOrder(orderDeliverCommand.getExpressCode(), orderDeliverCommand.getDeliveryTime());
      //保存订单信息
      this.orderRepository.save(order);
      
      return Response.buildSuccess();
  }
}

六、借助DDD领域模型摆脱事务脚本

在DDD中,应用架构主要分为4层:接口层、应用层、领域层、基础设施层,具体层次划分可见下图:

注:
D3S(DDD with SpringBoot)为本作者使用DDD过程中开发的框架及示例,
源码地址:https://gitee.com/luoex/d3s

在这里插入图片描述

在DDD中,应用层、领域层为其核心,领域层更是核心中的核心,领域层中的领域对象通常包括:

  • 聚合Aggregate,可以理解为对象建模的边界,落到代码层面可对应一个包,包中包含高内聚的实体、值对象等,其中仅有一个实体作为聚合根AggregateRoot只有聚合根才是访问聚合的唯一入口,聚合外部的对象不能引用除聚合根实体之外的任何内部对象
  • 实体Entity(可作为聚合根),即对应业务对象及操作的建模(富血模型),通常可通过ID唯一标识。
  • 值对象ValueObject,通常作为实体的属性(不具备ID),亦可包含该值对象相关的业务操作。
  • 仓库Repository(接口形式),负责聚合根(实体)的持久化,此层无需考虑底层数据库的实体、关系表,仅将聚合根作为一个整体进行持久化,具体底层的实现可在基础设施层Infrastructure实现Respository接口 又或者 直接通过ORM框架实现(如JPA、Hibernate)。
  • 领域服务DomainService,不适合Entity、ValueObject中的逻辑均可放在DomainServcie,通常负责协调多个聚合根(实体)间的交互业务编排、大段复杂业务的编排等。
  • 防腐层ACL(接口形式),负责与外部系统的交互,之所以叫做防腐层意为隔离外部的变化,不受外部变化的腐蚀,比如HttpClient接口、调用第三方服务等接口通过ACL进行定义,具体底层的实现可在基础设施层Infrastructure实现ACL接口 又或者 直接通过诸如OpenFeign等接口定义形式。

DDD中提倡的便是 富血模型,即由领域层的领域对象承担业务逻辑(面向对象建模),上层的应用层(应用服务Application Service)仅是对领域对象的流程编排,特别轻量的一层,将所有的业务逻辑都下沉到领域层。也就是说应用服务Service操作的都是领域对象,大量的业务逻辑都是由领域对象来完成的,Service仅是负责协调、调用领域对象来完成业务,并将领域对象(聚合根) 作为一个整体交由 仓库Respository 进行持久化,如此便规避了贫血模式、事务脚本的编程方式,直接面向领域对象进行编程。

以最初的新建商品为例,使用DDD的面向对象模型如下:

/**
 * Goods数据对象(聚合根)
 */
@Data
@TableName("goods")
public class Goods extends BaseEntity {
    private Long categoryId;
    private String goodsName;
    private LocalDate manufactureDate;
    private LocalDate expirationDate;
    private BigDecimal goodsWeight;
    private String goodsWeightUnit;
    private String goodsDesc;
    private BigDecimal goodsPrice;
    private Integer goodsStatus;
    
    //商品绑定的标签ID集合
    private Set<Long> tags;

	/**
     * 修改商品标签
     *
     * @param tags 商品标签ID集合
     */
    public void modifyTags(Set<Long> tags) {
        this.tags = tags;
    }

    /**
     * 上架商品
     */
    public void shelve() {
        super.updateTime = LocalDateTime.now();
        this.goodsStatus = GoodsStatus.SHELVED;
    }

    /**
     * 下架商品
     */
    public void unshelve() {
        super.updateTime = LocalDateTime.now();
        this.goodsStatus = GoodsStatus.UNSHELVED;
    }
}


/**
 * 商品标签(聚合根)
 */
@Data
@TableName("goods_tag")
public class GoodsTag extends BaseEntity {
    private String tagName;
    private String tagDesc;
}

注:
此处示例代码中的商品和标签的关联关系定义如下:
Set<Long> tags(实为标签ID集合)
而之前介绍面向对象建模时商品和标签的关联关系定义如下:
Set<GoodsTag> tags

此处的细微差别即揭示了DDD有别于面向对象建模的不同之处,
在DDD中强调聚合边界,聚合根作为持久化的整体,对聚合根的持久化便要对其包含的实体、值对象均进行持久化,加载查询时也要全部查询,所以聚合粒度的控制尤为重要,此处使用ID集合表示关联关系,也是为了更好的控制聚合的粒度,后续会再写一篇关于聚合的文章重点介绍此处。

如上代码中,原来的Goods、GoodsTag、GoodsTagBinding在使用DDD建模后仅保留了Goods、GoodsTag这2个实体,而Goods和GoodsTag间的关联关系则使用Goods.tags这个对象建模维度中的属性来表示(区别于数据库建模的关系表GoodsTagBinding),如此隔离了底层数据库思维(事务脚本模式),以面向对象的思维来对操作Goods,Goods作为一个整体(实为DDD中的聚合根)交由GoodsRespority(仓库)完成持久化,对于商品标签的操作即变成对商品中的标签ID集合属性Set<Long> tags进行的操作,如此使用DDD建模后的Service代码调整如下:

/**
 * 商品服务实现类
 */
public class GoodsServiceImpl implements GoodsService {
    //......

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SingleResponse<GoodsVo> createGoods(GoodsCreateCommand goodsCreateCommand) {
        //创建商品实体
        Goods goods = GoodsAssembler.createGoods(goodsCreateCommand);
        
        //注:此处使用的Specification模式,负责规则验证,后续会在系列文章中单独介绍此模式
        //验证新增商品分类是否存在
        this.categoryExistSpecification.isSatisfiedBy(goods.getCategoryId());
        //验证商品标签是否存在
        this.goodsTagExistSpecification.isSatisfiedBy(goods.getTags());
        
        //保存商品
        goods = this.goodsRepository.save(goods);
        
        //转换GoodsDto
        return SingleResponse.of(BaseAssembler.convert(goods, GoodsVo.class));
    }
}

其中GoodsAssember.createGoods代码如下:

/**
 * Goods转换器
 */
public interface GoodsAssembler {

    static Goods createGoods(GoodsCreateCommand goodsCreateCommand) {
        return new Goods(
                goodsCreateCommand.getCategoryId(),
                goodsCreateCommand.getGoodsName(),
                goodsCreateCommand.getManufactureDate(), goodsCreateCommand.getExpirationDate(),
                goodsCreateCommand.getGoodsWeight(), WeightUnit.of(goodsCreateCommand.getGoodsWeightUnit()),
                goodsCreateCommand.getGoodsDesc(),
                goodsCreateCommand.getGoodsPrice(),
                GoodsStatus.UNSHELVED,
                goodsCreateCommand.getTagIds()
        );
    }
}

此处GoodsAssembler充当了工厂的角色,负责创建领域对象,此处的创建区别于对象构造函数的创建,DDD中通过工厂创建领域对象指的是初始创建,创建成功后会通过仓库持久化,强调领域对象的生命周期从此开始。而面向对象中的构造函数,可以在初始创建时使用,也可以在数据库中查询出来后被重新构造时使用,又或者对象转换时使用,又或者通过无参构造函数创建然后一堆setter设置等等。相较于工厂,构造函数的使用并没有显式的生命周期阶段标识意义,故在DDD中创建领域对象还是首推工厂模式,工厂的实现可以通过:

  • 领域层的工厂Factory
  • 领域层的Entity自身的创建方法
  • 应用层的转换器Assembler
  • 应用层的Command参数创建方法

本例中使用了应用层的转换器GoodsAssembler来实现工厂模式。

关于事务脚本、DDD建模的代码编排总结如下图:
在这里插入图片描述

七、更多

关于DDD的更多实战代码可参见:
D3S (DDD with SpringBoot) - https://gitee.com/luoex/d3s
D3S为本作者使用DDD过程中开发的框架及示例,旨在打造一款可落地的DDD(领域驱动设计)实现框架,提供了统一的DDD模型抽象,并扩展实现了DDD架构的一些关键特性,同时给出了截取自电商案例的基于D3S的示例实现(反模式、完整版、轻量版),力求打造一款即能发挥DDD的优势又能降低上手难度的DDD实现框架,也希望本示例能帮你迈出拥抱DDD的第一步。

后续会持续更新关于DDD的更多的知识、使用心得…敬请期待:

  • 聚合
  • 实体、值对象
  • 仓库
  • 工厂

参考:
https://www.martinfowler.com/eaaCatalog/transactionScript.html
https://www.martinfowler.com/bliki/AnemicDomainModel.html
https://mp.weixin.qq.com/s/7kYGnp0KJGozQZDH1ohnNg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1193459.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“三位一体”超级混沌工程主要特点及功能

“三位一体”超级混沌工程X-Chaos主要包括基础故障编排、业务场景故障编排、演练场景编排、故障库管理、演练场景管理、演练计划管理、演练观测和演练报告等模块&#xff0c;支持对传统架构、云环境以及国产化基础环境的IT系统进行故障演练。本文将介绍混沌工程主要特点及主要功…

剪贴板劫持--PasteJacker的使用

启动 PasteJacker [1] Windows [2] Linux [3] Exit第一次是让我们选择要攻击针对的目标系统&#xff0c;这里以Windows系统为例&#xff0c;即我自己的物理机 因此键入 1 &#xff0c;回车 [1] Download and execute a msfvenom backdoor using certutil (Web delivery Past…

现在就是成为“新程序员”的黄金时刻!

整理 | 王启隆 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; “自然语言代替了编程语言&#xff0c;大大地降低了程序员的门槛。现在&#xff0c;ChatGPT 将全球的知识库和代码都放在了你的手中&#xff0c;只要有想象力&#xff0c;人人都能成为「新程序员」…

一周成功拿下4个offer的软件测试面试题,面试必看系列

前言&#xff1a; 压到就是赚到&#xff0c;面试通过的机率就更大&#xff0c;干就完了铁子 【文章末尾给大家留下了大量的福利】 ​编辑 1、什么是兼容性测试&#xff1f;兼容性测试侧重哪些方面&#xff1f; 参考答案&#xff1a; 兼容测试主要是检查软件在不同的硬件平…

腾讯云2023年双11活动:云服务器1.8折起,还可领取9999元代金券!

2023年双11腾讯云推出了11.11云上盛惠大促活动&#xff0c;包括秒杀专区、服务器买赠、新人专区、代金券专区、境外优选、新老同享、续费专区以及热门上云场景等满足新用户、老用户、企业用户对云计算服务的各种需求。 一、腾讯云双11活动地址 活动入口&#xff1a;点此直达 …

【高等数学】导数的应用

导数的应用 1、洛必达法则1.1、引例1.2、内容1.3、证明1.4、洛必达的应用总结 1.5、注意 2、泰勒公式2.1、解决的问题2.2、引例2.3、内容2.3.1、带Peano余项的泰勒公式2.3.2、带Lagrange余项的泰勒公式2.3.3、麦克劳林公式2.3.4、几个初等函数的麦克劳林公式 2.4、证明2.5、泰勒…

智慧城市建设解决方案分享【完整】

文章目录 第1章 前言第2章 智慧城市建设的背景2.1 智慧城市的发展现状2.2 智慧城市的发展趋势 第3章 智慧城市“十二五”规划要点3.1 国民经济和社会发展“十二五”规划要点3.2 “十二五”信息化发展规划要点 第4章 大数据&#xff1a;智慧城市的智慧引擎4.1 大数据技术—智慧城…

css实战——清除列表中最后一个元素的下边距

需求描述 常见于列表的排版&#xff0c;如文章列表、用户列表、商品列表等。 代码实现 <div class"listBox"><div class"itemBox">文章1</div><div class"itemBox">文章2</div><div class"itemBox"…

java语法:继承与多态

导言: 在Java中&#xff0c;继承和多态是面向对象编程的两个重要概念&#xff0c;它们允许我们创建更加灵活和可扩展的代码。本文主要对继承和多态的语法和一些细节做一个介绍和解释。 目录 导言: 正文&#xff1a; 一.继承 1. 基本语法 2. 继承的特点 3.子类中访问父类…

新品上市|米尔RZ/G2UL核心板上市,助力工业4.0发展!

浩瀚的芯片海洋中能被人记住的寥寥无几&#xff0c;那些在人们脑海中留下印记的往往是踩中了时代的脉搏。32位ARMv7架构的A7/A8系列处理器自发布以来&#xff0c;以ARM9处理器的价格&#xff0c;升级了工业领域绝大部分应用需求&#xff0c;成为最近十年最受欢迎的通用工业级AR…

怎么写日语开发信?写外贸日语开发信技巧?

如何写好日语开发信&#xff1f;日语开发信格式是怎么样的&#xff1f; 无论您是初学者还是有经验的营销专家&#xff0c;都需要掌握一些关键技巧&#xff0c;以确保您的邮件在日本市场取得成功。蜂邮将向您介绍怎样写一封令人印象深刻的日语开发信&#xff0c;以吸引潜在客户…

2022年09月 Python(四级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 下列不是评判一个算法优劣的标准是?( ) A: 时间复杂度 B: 空间复杂度 C: 难易度 D: 健壮性 答案:C 评价算法的优劣是:时间复杂度,空间复杂度,健壮性,正确性,可读性。因此选…

Laplacian Redecomposition for Multimodal Medical Image Fusion

LRD方法 GDIE means ‘gradient-domain image enhancement’&#xff0c;DGR means ‘decision graph redecomposition’ MLD means ‘maximum local difference’ LEM means ‘local energy maximum’&#xff0c;OD means ‘overlapping domain’&#xff0c;LP means ‘L…

2023.11.8 hadoop学习-概述,hdfs dfs的shell命令

目录 1.分布式和集群 2.Hadoop框架 3.版本更新 4.hadoop架构详解 5.页面访问端口 6.Hadoop-HDFS HDFS架构 HDFS副本 7.SHELL命令 8.启动hive服务 1.分布式和集群 分布式: 多台服务器协同配合完成同一个大任务(每个服务器都只完成大任务拆分出来的单独1个子任务)集 群:…

Java jdbc连接Oracle时出现ORA-28040: No matching authentication protocol报错

一、问题描述 升级了oracle数据库版本后&#xff0c;同时也更新了oracle的驱动为ojdbc8.jar&#xff0c;Java重新通过jdbc连接Oracle时出现ORA-28040: No matching authentication protocol报错。 完整报错信息 java.sql.SQLException: ORA-28040: No matching authenticati…

重庆市5米数字高程(DEM)数据

重庆位于中国西南部、长江上游地区&#xff0c;地跨东经10511~11011、北纬2810~3213之间的青藏高原与长江中下游平原的过渡地带。东邻湖北、湖南&#xff0c;南靠贵州&#xff0c;西接四川&#xff0c;北连陕西&#xff1b;辖区东西长470千米&#xff0c;南北宽450千米&#xf…

uni-app——項目day01

配置uni-app開發環境 uni-app快速上手 | uni-app官网 创建项目 图中四个划线就是要配置的地方. 选择vue2还是vue3看个人选择。 目录结构 但是现在新版本创建的项目已经没有components目录了&#xff0c;需要自己创建。 项目运行到微信开发者工具 使用git管理项目 node-mod…

C++初阶(十)模板初阶

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、泛型编程1、如何实现一个通用的交换函数呢&#xff1f;2、引出模板 二、函数模板1、函数模…

蓝桥杯之模拟与枚举day1

Question1卡片(C/CA组第一题) 这个是一道简单的模拟枚举题目&#xff0c;只要把对应每次的i的各个位都提取出来&#xff0c;然后对应的卡片数目减去1即可。属于打卡题目。注意for循环的特殊使用即可 #include <iostream> using namespace std; bool solve(int a[],int n…

Android逆向fiddler抓包工具——理解HTTP协议

HTTP协议格式 HTTP协议是一种应用非常广泛的应用层协议&#xff0c;当我们在浏览器中输入一个URL(“网址”)时&#xff0c;浏览器就会给客户端发送一个HTTP请求&#xff0c;服务器收到请求之后&#xff0c;就会返回一个HTTP响应。 为了能够看到HTTP请求和响应的详细内容&…