【DDD】学习笔记-领域模型与数据模型

news2024/12/23 23:20:34

领域模型与数据模型

领域驱动的设计模型最重要的概念就是聚合,同时,聚合还要受到限界上下文边界的控制。Eric Evans 之所以要引入限界上下文,其中一个重要原因就是因为我们“无法维护一个涵盖整个企业的统一模型”,于是需要限界上下文来“标记出不同模型之间的边界和关系”。当领域模型引入限界上下文与聚合之后,领域模型类与数据表之间就有可能突破类与表之间一一对应的关系。因此,在遵循领域驱动设计原则实现持久化时,需要考虑领域模型与数据模型之间的关系。

领域模型与数据模型的分离

资源库是持久化在领域层的抽象。一个资源库对应一个聚合,因此可以认为聚合是领域模型中最小的持久化单元。这是了解领域驱动设计对持久化影响的关键,也是在实现阶段,领域模型驱动设计有别于数据模型驱动设计的核心特征。先有领域模型,后有数据模型,采用领域模型驱动设计的过程,就应以限界上下文为基础,面向聚合进行领域模型设计。在确定了聚合内的各个实体与值对象之后,形成对限界上下文领域模型的细化,然后在实现阶段,再考虑该如何针对每个聚合内的对象进行持久化。

仍以薪资管理系统为例,对员工的管理和薪资结算分属两个不同的限界上下文:员工上下文(Employee Context)和薪资上下文(Payroll Context)。员工上下文关注员工基本信息的管理,薪资上下文需要对各种类型的员工进行薪资结算,这就会导致这两个限界上下文的领域模型都会包含 Employee 这个领域概念类。在考虑建立它们的持久化数据模型时,存在两种不同的设计方案:

  • 单库单表:在数据模型中统一建立一张员工表,然后在映射元数据中做好对应的配置。这一方案满足单体架构风格。
  • 多库多表:为不同的限界上下文建立不同的数据库,员工模型也映射不同的员工表,之间以共同的员工 ID 关联。这一方案符合微服务架构风格。

无论数据模型采用哪一种设计方案,它们的领域模型包括对聚合内实体与值对象的定义,界定的聚合边界都不应有任何区别,即做到领域模型的设计与持久化机制无关。在领域模型中,受到数据模型影响的应只限于ORM元数据定义。如下图所示的代码结构,应不受数据模型设计方案的影响:

31d8e84b-2647-4739-a5f6-ad9294ef052f.png

在领域模型中,员工上下文的 Employee 聚合根实体与薪资上下文的员工聚合根实体通过 EmployeeId 建立关联,薪资上下文中的 HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 三个聚合根实体之间没有任何关系。在设计领域模型时,不应该受到数据模型设计的干扰,但在实现领域模型时,就需要确定数据模型的设计方案,并在选定 ORM 框架的基础上,确定该如何映射领域模型到数据模型的实现方案,并编写代码实现。领域模型与数据模型彼此之间的关系如下图所示:

76245529.png

从概念上讲,HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 都是员工,似乎应为其建立以 Employee 为父类的继承体系。然而,若采用领域驱动设计,根据业务能力与领域关注点划分了限界上下文,它们又应该分属不同的限界上下文。如果仍然设计为继承体系,就会导致薪资上下文成为员工上下文的遵奉者。这正是对象范式的领域驱动设计与常规的面向对象设计不同之处,领域驱动设计在战略和战术层面尤为关注和强调限界上下文与聚合的边界控制力。这是在运用领域驱动设计进行落地实现时,尤其需要注意的一点。

领域模型

不同的限界上下文有着不同的领域模型,也有着不同的统一语言,因此在定义领域模型的类型时,需要注意区分限界上下文的边界。

由于员工上下文专注于对员工信息的管理,因此 Employee 类的定义包含了员工所有的基本属性,部分属性则因为体现了更小的内聚的领域概念,被定义为值对象:

package top.dddclub.payroll.employeecontext.domain;

public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    private EmployeeId employeeId;
    private String name;
    private Email email;
    private EmployeeType employeeType;
    private Gender gender;
    private Address address;
    private Contact contact;
    private LocalDate onBoardingDate;
}

薪资上下文关心的领域逻辑是计算每种员工的薪资。倘若不同类型员工仅存在薪资计算行为的差异,自然可以引入策略模式,将这一行为分离出来,并抽象为薪资计算的接口。然而,不同类型员工还存在完全不同的属性和对等的行为,钟点工需要提交工作时间卡,月薪雇员需要记录缺勤记录,销售人员需要提交销售凭条,它们之间唯一存在的共性就是 EmployeeId,除此之外,我们还需要维护它们各自的一致性。因此,针对薪资上下文的领域模型,可以为不同类型雇员建立不同的聚合,然后在薪资计算行为层面引入抽象,保持适度的扩展能力:

73601347.png

薪资上下文领域模型的聚合定义如下:

package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;

public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    private EmployeeId employeeId;
    private Salary salaryOfHour;
    private List<TimeCard> timeCards = new ArrayList<>();

    public Payroll payroll(Period settlementPeriod) { }
}

package top.dddclub.payroll.payrollcontext.domain.salariedemployee;

public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<SalariedEmployee> {
    private EmployeeId employeeId;
    private Salary salaryOfMonth;
    private List<Absence> absences = new ArrayList<>();

    public Payroll payroll(Period settlementPeriod) { }
}

package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;

public class  CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<CommissionedEmployee> {
    private EmployeeId employeeId;
    private Salary salaryOfTwoWeeks;
    private List<Commission> commissions = new ArrayList<>();

    public Payroll payroll(Period settlementPeriod) { }
}

引入聚合的目的在于保证领域概念的完整性,并确保外部的调用者不会因为直接访问聚合边界内除聚合根实体之外的其余对象而破坏这种完整性。例如在 HourlyEmployee 类的 payroll() 方法中,对 timecards 进行了验证,并通过 filterByPeriod() 方法过滤了不符合结算周期的工作时间卡:

public class HourlyEmployee extends... {
    public Payroll payroll(Period settlementPeriod) {
        // 对工作时间卡进行验证
        if (Objects.isNull(timeCards) || timeCards.isEmpty()) {
            return new Payroll(this.employeeId, settlementPeriod.beginDate(), settlementPeriod.endDate(), Salary.zero());
        }

        Salary regularSalary = calculateRegularSalary(settlementPeriod);
        Salary overtimeSalary = calculateOvertimeSalary(settlementPeriod);
        Salary totalSalary = regularSalary.add(overtimeSalary);

        return new Payroll(this.employeeId, settlementPeriod.beginDate(), settlementPeriod.endDate(), totalSalary);
    }

    private Salary calculateRegularSalary(Period period) {
        int regularHours = filterByPeriod(period)
                .map(TimeCard::getRegularWorkHours)
                .reduce(0, Integer::sum);
        return salaryOfHour.multiply(regularHours);
    }

    private Salary calculateOvertimeSalary(Period period) {
        int overtimeHours = filterByPeriod(period)
                .filter(TimeCard::isOvertime)
                .map(TimeCard::getOvertimeWorkHours)
                .reduce(0, Integer::sum);

        return salaryOfHour.multiply(OVERTIME_FACTOR).multiply(overtimeHours);
    }

    // 过滤不符合结算周期条件的工作时间卡
    private Stream<TimeCard> filterByPeriod(Period period) {
        return timeCards.stream()
                .filter(t -> t.isIn(period));
    }
}

通过测试驱动开发实现聚合的业务逻辑时,编写的单元测试也应体现这种聚合内的规则约束。例如,当钟点工缺少工作时间卡时,计算的薪资为 0,测试需要体现这一规则约束:

public class HourlyEmployeeTest {
    @Test
    public void should_be_0_given_null_timecard() {
        //given
        HourlyEmployee hourlyEmployee = EmployeeFixture.hourlyEmployeeOf(employeeId, null);

        //when
        Payroll payroll = hourlyEmployee.payroll(settlementPeriod);

        //then
        assertThat(payroll).isNotNull();
        assertThat(payroll.employeId().value()).isEqualTo(employeeId);
        assertThat(payroll.amount()).isEqualTo(Salary.zero());
    }
}

三个聚合根实体定义的 payroll(Period) 方法在抽象上保持了一致性,不过,由于员工在计算薪资之前还需要调用聚合资源库获取对应的员工对象列表,因此还需要引入领域服务做进一步封装,如 HourlyEmployeePayrollCalculator 服务:

public class HourlyEmployeePayrollCalculator {
    private HourlyEmployeeRepository employeeRepository;

    public void setRepository(HourlyEmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    public List<Payroll> execute(Period settlementPeriod) {
        List<HourlyEmployee> hourlyEmployees = employeeRepository.allEmployeesOf();
        return hourlyEmployees.stream()
                .map(e -> e.payroll(settlementPeriod))
                .collect(Collectors.toList());
    }
}

领域服务的 execute() 方法封装了对资源库的调用实现,它的抽象层次方才彻底抹掉了员工类型的差异,可由此引入策略模式:

public interface PayrollCalculator {
    List<Payroll> execute(Period settlementPeriod);
}

public class HourlyEmployeePayrollCalculator implements PayrollCalculator {}
public class SalariedEmployeePayrollCalculator implements PayrollCalculator {}
public class CommissionedEmployeePayrollCalculator implements PayrollCalculator {}

显然,聚合概念对于以对象范式建立的领域模型存在一定的约束和限制,当然,这种约束与限制也不妨看做是对设计的指导。实际上,倘若我们为钟点工、月薪雇员和销售人员建立了员工继承体系,一旦引入策略模式,就会导致员工类与薪资计算策略类之间存在“平行的继承体系”坏味道。如果没有聚合的要求,又可能会仅定义一个员工类,然后通过员工类型来区分各自的差异,导致产生一个承担了太多职责的过大的类,不利于维护员工与工作时间卡、缺勤记录与销售凭条之间的关系。当然,将它们都设计为一个个仅提供数据属性的贫血对象,自然就不可取了。

数据模型

如前所述,取决于限界上下文的边界以及系统选择的架构风格,存在两种迥然不同的数据模型:单库单表和多库多表。它们的数据模型自然不同,由此也会影响到领域模型到数据模型之间的映射关系。

单库单表的ORM映射

若采用单体架构,员工对应的数据模型最简单的设计就是单库单表,即创建 employees 表。由于员工与工作时间卡、缺勤记录和销售凭条之间都存在一对多关系,因此采用单库设计的数据模型如下所示:

74796269.png

领域驱动设计虽然隔离了领域模型与数据模型,但在实现持久化时,必须为持久化框架提供对象与关系的映射信息。倘若采用 JPA,就是通过 Java 标注来声明,这可以认为是持久化机制对领域模型的一点侵入。由于员工上下文和薪资上下文中的员工领域模型都映射自 employees 表,且薪资上下文的 HourlyEmployee、SalariedEmployee 与 CommissionedEmployee 聚合对应了各自类型的员工数据,故而在领域模型中设置映射信息时,需要作出一点点调整。映射元数据的声明如下所示:

package top.dddclub.payroll.employeecontext.domain;
@Entity
@Table(name="employees")
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    @EmbeddedId
    private EmployeeId employeeId;

    // 略去其余字段定义
}

package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;
@Entity
@Table(name = "employees")
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorOptions(force=true)
@DiscriminatorValue(value = "0")
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfHour;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "employeeId", nullable = false)
    private List<TimeCard> timeCards = new ArrayList<>();
}

package top.dddclub.payroll.payrollcontext.domain.salariedemployee;
@Entity
@Table(name = "employees")
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorOptions(force=true)
@DiscriminatorValue(value = "1")
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfMonth;

    @ElementCollection
    @CollectionTable(name = "absences", joinColumns = @JoinColumn(name = "employeeId"))
    private List<Absence> absences = new ArrayList<>();
}

package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;
@Entity
@Table(name = "employees")
@DiscriminatorColumn(name = "employeeType", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorOptions(force=true)
@DiscriminatorValue(value = "2")
public class CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfTwoWeeks;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "employeeId", nullable = false)
    private List<Commission> commissions = new ArrayList<>();
}

以上四个聚合类都通过 @Table(name = "employees") 将实体映射到 employees 表。由于这些聚合处于两个不同的限界上下文,各个聚合根实体面对的业务关注点也不相同,故而各自定义的字段存在差别。如 HourlyEmployee 类除了 employeeId 字段外,就仅定义了 salaryOfHour 与 timecards 字段,这两个字段却是 Employee 类所不具备的。站在数据模型的角度看,同一个表对应的领域类存在这样的差异是非常奇怪的,但如果你抛开数据表设计的影响,仅从业务去思考领域类的定义,你又会觉得合情合理。认识到这一差异的根源,有助于理解为何 Eric Evans 要将这一方法体系命名为领域驱动设计。

无论是 employees 表,还是 Employee 类,都定义了 employeeType 以区分员工的类型。这似乎是 JPA 采用 Single-Table 策略实现继承的标识列。然而如前所述,领域模型中的 HourlyEmployee、SalariedEmployee、CommissionedEmployee 与 Employee 之间并不存在继承关系;但在数据模型中,三种不同类型的员工数据却放在同一张表中,必然需要某种方式告知各自的 Repository:在管理聚合对象的生命周期时,需要对数据加以区分。

例如,在调用 HourlyEmployeeRepository 的查询方法时,就只能查询 employeeType 值为 0 的员工数据。要实现这一区分,就可以为 HourlyEmployee 聚合根实体添加 @DiscriminatorColumn 标注来区分它的员工类型。一旦为 HourlyEmployee 等聚合根实体设置了标识列声明,聚合根对应的资源库在查询数据库时,只会返回满足标识列值的数据记录,无需再额外添加对 employeeType 值进行判断的查询条件。反观 Employee 聚合根,由于员工上下文并不需要区分员工类型,它的实体定义反而无需添加标识列声明。

钟点工、月薪雇员和销售人员存在薪资结算的业务差异,这是将它们定义为不同聚合的主因。HourlyEmployee 维持了与 TimeCard 的一致性,SalariedEmployee 维持了与 Absence 的一致性,CommissionedEmployee 维持了与 Commission 的一致性,且它们都属于一对多的组合关系,在数据模型中也各自采用了表关联,并以员工 ID 作为从表的外键。

TimeCard 和 Commision 被定义为实体,因而在各自聚合根实体的定义中,使用了 @OneToMany 标注来映射这种一对多的组合关系;Absence 被定义为值对象,因此在 SalariedEmployee 类中使用了 @ElementCollection 标注来表达这种一对多的包含关系。

对比薪资上下文中这三个聚合之间的差异,会发现它们虽然都定义了 Salary 类型的字段,但字段名称却并不相同,分别代表日薪、月薪和双周薪。但是,该字段对应的数据列却是完全相同的,在领域模型中通过声明了列名的 Salary 值对象来匹配这种映射关系:

@Embeddable
public class Salary {
    private static final int SCALE = 2;

    @Column(name = "salary")
    private BigDecimal value;
}

多库多表的 ORM 映射

多库多表的数据模型发生了本质的变化,因为要创建的表分布在不同的数据库。由于薪资上下文的数据库不再包含 employees 表,因此需要为钟点工、月薪雇员和销售人员分别创建三张表,以及与之关联的 timecards、absences 和 commissions 表:

75352172.png

如果采用 JPA 规范实现资源库的持久化,就需要在各自的限界上下文中定义 persistence.xml 文件,定义不同的 Persistence Unit,并设置属性指向对应的数据库。

既然数据模型中员工相关的表名与结构都发生了变化,领域模型的映射元数据也要做相应的调整:

package top.dddclub.payroll.employeecontext.domain;
@Entity
@Table(name="employees")
public class Employee extends AbstractEntity<EmployeeId> implements AggregateRoot<Employee> {
    @EmbeddedId
    private EmployeeId employeeId;

    // 略去其余字段定义
}

package top.dddclub.payroll.payrollcontext.domain.hourlyemployee;
@Entity
@Table(name = "hourlyEmployees")
public class HourlyEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfHour;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "employeeId", nullable = false)
    private List<TimeCard> timeCards = new ArrayList<>();
}

package top.dddclub.payroll.payrollcontext.domain.salariedemployee;
@Entity
@Table(name = "salariedEmployees")
public class SalariedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfMonth;

    @ElementCollection
    @CollectionTable(name = "absences", joinColumns = @JoinColumn(name = "employeeId"))
    private List<Absence> absences = new ArrayList<>();
}

package top.dddclub.payroll.payrollcontext.domain.commissionedemployee;
@Entity
@Table(name = "commissionedEmployees")
public class CommissionedEmployee extends AbstractEntity<EmployeeId> implements AggregateRoot<HourlyEmployee> {
    @EmbeddedId
    private EmployeeId employeeId;

    @Embedded
    private Salary salaryOfTwoWeeks;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "employeeId", nullable = false)
    private List<Commission> commissions = new ArrayList<>();
}

这些聚合根实体都通过 @Table 指向了对应的数据表。注意,薪资上下文的三个员工聚合根实体不再声明 @DiscriminatorColumn 标注,这是因为每种类型的员工记录在物理上是分离的,每张表仅存储相同类型的员工数据,自然无需定义标识列来区分员工类型。当然,员工上下文的 employees 表仍然定义了 employeeType 列,但它对应的领域模型却不需要列标识。

如果去掉所有的 JPA 标注,如上的领域模型与单库单表的领域模型是完全一致的。这也充分说明了领域模型的设计应与数据模型无关。既然领域模型没有差异,聚合对应的资源库,以及领域服务自然也不会有任何差异。换言之,当我们将系统从单体架构迁移到微服务架构时,除了数据库需要进行调整,并考虑可能的数据迁移外,领域层的代码几乎不需要做任何调整。如果在编码时,还注意守护了聚合与限界上下文的边界,保证聚合之间的关联通过聚合 ID 进行(推而广之,限界上下文之间也不应共享领域模型),就可以保证领域层不受架构风格变化的影响,这既符合将业务复杂度与技术复杂度分离的原则,也满足整洁架构的设计思想。

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

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

相关文章

!!!Python虚拟环境改名后的坑!!!!

搞了一晚上终于弄好这python虚拟环境的问题了&#xff01;真的是坑啊&#xff01; 本来用的纯python环境下的虚拟环境&#xff0c;一时心血来潮&#xff0c;把电脑重新装了一遍&#xff0c;虚拟环境的目录也改了一下&#xff0c;结果虚拟环境再vscode中是可以使用&#xff0c;…

Llama2模型的优化版本:Llama-2-Onnx

Llama2模型的优化版本&#xff1a;Llama-2-Onnx。 Llama-2-Onnx是Llama2模型的优化版本。Llama2模型由一堆解码器层组成。每个解码器层&#xff08;或变换器块&#xff09;由一个自注意层和一个前馈多层感知器构成。与经典的变换器相比&#xff0c;Llama模型在前馈层中使用了不…

Sora领航AIGC时代:深度解读行业变革与AI工具全景图

随着人工智能技术的飞速发展&#xff0c;越来越多的企业和行业开始将AI融入其核心业务流程中。在这个背景下&#xff0c;Sora以其独特的视角和全面的解决方案&#xff0c;正引领着AIGC&#xff08;人工智能生成内容&#xff09;的趋势变革。 本文将对Sora进行深度解读&#xf…

docker打包当前dinky项目

以下是我的打包过程&#xff0c;大家可以借鉴。我也是第一次慢慢摸索&#xff0c;打包一个公共项目&#xff0c;自己上传。 如果嫌麻烦&#xff0c;可以直接使用我的镜像&#xff0c;直接跳到拉取镜像&#xff01; <可以在任何地方的服务器进行拉取> docker打包当前din…

静态时序分析:SDC约束命令set_input_transition详解

相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html 目录 指定端口转换时间 指定端口列表 简单使用 指定上升、下降沿 指定最大、最小条件 与set_clock_transition命令的区别 DC工具在使用set_drive和set_driving_cell建模输入端口驱动…

【深度学习】主要提出者【Hinton】中国大会最新演讲【通往智能的两种道路】

「但我已经老了&#xff0c;我所希望的是像你们这样的年轻有为的研究人员&#xff0c;去想出我们如何能够拥有这些超级智能&#xff0c;使我们的生活变得更好&#xff0c;而不是被它们控制。」 6 月 10 日&#xff0c;在 2023 北京智源大会的闭幕式演讲中&#xff0c;在谈到如…

【分布式事务 XA模式】MySQL XA模式详解

MYSQL中的XA事务 写在前面1. XA事务的基本原理2. MySQL XA事务操作 写在前面 MySQL 的 5.0.3 版本开始支持XA分布式事务&#xff0c;并且只有innoDB存储引擎支持XA事务。 1. XA事务的基本原理 XA事务本质上是一种基于两阶段提交的分布式事务&#xff0c;分布式事务可以理解成…

DIcom调试Planar configuration

最近和CBCT组同事调dicom图像 这边得图像模块老不兼容对方得dicom文件。 vtk兼容&#xff0c;自己写得原生解析不兼容。 给对方调好了格式&#xff0c;下次生成文件还会有错。 简单记录下&#xff0c;日后备查。 今天对方又加了 个字段&#xff1a;Planar configuration 查…

【Java程序员面试专栏 数据结构】六 高频面试算法题:字符串

一轮的算法训练完成后,对相关的题目有了一个初步理解了,接下来进行专题训练,以下这些题目就是汇总的高频题目,本篇主要聊聊数组,包括数组合并,滑动窗口解决最长无重复子数组问题,图形法解下一个排列问题,以及一些常见的二维矩阵问题,所以放到一篇Blog中集中练习 题目…

无人机精准定位技术,GPS差分技术基础,RTK原理技术详解

差分GPS的基本原理 差分GPS&#xff08;Differential GPS&#xff0c;简称DGPS&#xff09;的基本原理是利用一个或多个已知精确坐标的基准站&#xff0c;与用户&#xff08;移动站&#xff09;同时接收相同的GPS卫星信号。由于GPS定位时会受到诸如卫星星历误差、卫星钟差、大…

前端学习——vue学习

文章目录 1. < el-form> 属性 model、prop、rules2. v-bind 与 v-model3. v-if 与 v-show4. v-for 循环语句5. 计算属性 computed6. 监视属性 watch7. 下拉框 el-select、el-option8. 自定义事件9. async与await实现异步调用 1. < el-form> 属性 model、prop、rule…

【CSS-语法】

CSS-语法 ■ CSS简介■ CSS 实例■ CSS id 和 class选择器■ CSS 样式表■ 外部样式表(External style sheet)■ 内部样式表(Internal style sheet)■ 内联样式(Inline style)■ 多重样式 ■ CSS 文本■ CSS 文本颜色■ CSS 文本的对齐方式■ CSS 文本修饰■ CSS 文本转换■ CS…

httpd apache

虚拟主机 配置环境 [rootlocalhost ~]#cd /var/www/html/ [rootlocalhost html]#mkdir 123 [rootlocalhost html]#mkdir abc [rootlocalhost html]#ls 123 abc [rootlocalhost html]#cd 123/ [rootlocalhost 123]#echo 123 > index.html [rootlocalhost 123]#cd ../abc/ […

《The Art of InnoDB》第二部分|第4章:深入结构-磁盘结构-撕裂的页面(doublewrite buffer)

4.5 撕裂的页面 目录 4.5 撕裂的页面 4.5.1 双写缓冲区的作用 4.5.2 双写缓冲区的结构 4.5.3 双写缓冲区与Redolog的协同工作流程 4.5.2 双写缓冲区写入时机 4.5.3 禁用双写缓冲区 4.5.4 小结 未完待续... 上文我们学习了redo log的结构和其工作原理&#xff0c;它是一…

vue+nodejs+uniapp婚纱定制婚庆摄影系统 微信小程序 springboot+python

目前移动互联网大行其道&#xff0c;人人都手中拿着智能机&#xff0c;手机手机&#xff0c;手不离机&#xff0c;如果开发一个用在手机上的程序软件&#xff0c;那是多么的符合潮流&#xff0c;符合管理者和客户的理想。本次就是开发婚庆摄影小程序&#xff0c;有管理员&#…

LeetCode 2476.二叉搜索树最近节点查询:中序遍历 + 二分查找

【LetMeFly】2476.二叉搜索树最近节点查询&#xff1a;中序遍历 二分查找 力扣题目链接&#xff1a;https://leetcode.cn/problems/closest-nodes-queries-in-a-binary-search-tree/ 给你一个 二叉搜索树 的根节点 root &#xff0c;和一个由正整数组成、长度为 n 的数组 qu…

Java中的常量与变量:初探Java世界的基石

✨✨ 所属专栏&#xff1a; Java基石&#xff1a;深入探索Java核心基础✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 一. 常量与变量的概念 常量 变量 总结 二. 常量的分类 1. 字面常量 2. 常量变量 3. 枚举常量…

设计模式篇---观察者模式

文章目录 概念结构实例总结 概念 观察者模式&#xff1a;定义对象之间的一种一对多的依赖关系&#xff0c;使得每当一个对象状态发生改变时&#xff0c;其他相关依赖对象都得到通知并被自动更新。 观察者模式是使用频率较高的一个模式&#xff0c;它建立了对象与对象之间的依赖…

JavaScript 进阶02

深入对象 构造函数 构造函数是用于创建对象的函数。 <script> //构造函数 构造函数的首字母大写 function Obj(name,age,aaa){this.namenamethis.ageage } //调用函数 const obj1new Obj("小明",4) console.log(obj1) </script> 使用 new 关键字调用…

2023年海南房地产经纪机构备案需要具备哪些条件?

房地产业在海南占有非常重要的地位。 同样&#xff0c;海南也有很多房地产中介机构。 那么&#xff0c;2023年海南房产中介登记证如何办理呢&#xff1f; 海南房产中介注册需要什么条件&#xff1f; 办理海南房产中介机构登记需要提交哪些材料&#xff1f; ……今天博宇会计小编…