博客记录-day002-java方法、可变参数、本地方法、构造方法+原型模式、单例模式、适配器模式+DDD基础知识

51 阅读21分钟

一、沉默王二-面向对象编程

1.java中的方法

访问权限:它指定了方法的可见性。Java 提供了四种:

  • public:该方法可以被所有类访问。
  • private:该方法只能在定义它的类中访问。
  • protected:该方法可以被同一个包中的类,或者不同包中的子类访问。
  • default:如果一个方法没有使用任何访问权限修饰符,那么它是 package-private 的,意味着该方法只能被同一个包中的类可见。

没有使用 static 关键字修饰,但在类中声明的方法被称为实例方法,在调用实例方法之前,必须创建类的对象

public class InstanceMethodExample {
    public static void main(String[] args) {
        InstanceMethodExample instanceMethodExample = new InstanceMethodExample();
        System.out.println(instanceMethodExample.add(1, 2));
    }

    public int add(int a, int b) {//实例方法
        return a + b;
    }
}

当一个方法被 static 关键字修饰时,它就是一个静态方法。换句话说,静态方法是属于类的,不属于类实例的(不需要通过 new 关键字创建对象来调用,直接通过类名就可以调用)。

public class EvenOddDemo {
    public static void main(String[] args) {
        findEvenOdd(10);
        findEvenOdd(11);
    }

    public static void findEvenOdd(int num) {//静态方法
        if (num % 2 == 0) {
            System.out.println(num + " 是偶数");
        } else {
            System.out.println(num + " 是奇数");
        }
    }
}

没有方法体的方法被称为抽象方法,它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 abstract 关键字创建抽象方法和抽象类。


abstract class AbstractDemo {
    abstract void display();
}

public class MyAbstractDemo extends AbstractDemo {
    @Override
    void display() {
        System.out.println("重写了抽象方法");
    }

    public static void main(String[] args) {
        MyAbstractDemo myAbstractDemo = new MyAbstractDemo();
        myAbstractDemo.display();
    }
}

2.java中可变参数

可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(is-a)的值作为参数。就像下面这样。意思就是尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位。

其实也很简单。当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法

我们要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中。 假如真的需要重载带有可变参数的方法,就必须在调用方法的时候给出明确的指示,不要让编译器去猜。

public static void main(String[] args) {
    String [] strs = null;
    print(strs);

    Integer [] ints = null;
    print(ints);
}

public static void print(String... strs) {
}

public static void print(Integer... ints) {
}

3.java native 本地方法

native 用来修饰方法,用 native 声明的方法表示该方法的实现在外部定义,可以用任何语言去实现它,比如说 C/C++。 简单地讲,一个 native Method 就是一个 Java 调用非 Java 代码的接口。

native 语法:

  • ①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。
  • ②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。
  • ③、返回值可以是任意类型

我们在日常编程中看到 native 修饰的方法,只需要知道这个方法的作用是什么,至于别的就不用管了,操作系统会给我们实现,初学的时候也不需要太过深入。”

4.Java构造方法

“在 Java 中,构造方法是一种特殊的方法,当一个类被实例化的时候,就会调用构造方法只有在构造方法被调用的时候,对象才会被分配内存空间。每次使用 new 关键字创建对象的时候,构造方法至少会被调用一次。”

“如果你在一个类中没有看见构造方法,并不是因为构造方法不存在,而是被缺省了,编译器会给这个类提供一个默认的构造方法。就是说,Java 有两种类型的构造方法:无参构造方法和有参构造方法。”

“注意,之所以叫它构造方法,是因为对象在创建的时候,需要通过构造方法初始化值——描写对象有哪些初始化状态。”

构造方法必须符合以下规则:

  • 构造方法的名字必须和类名一样;
  • 构造方法没有返回类型,包括 void;
  • 构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。

如果一个构造方法中没有任何参数,那么它就是一个默认构造方法,也称为无参构造方法

通常情况下,无参构造方法是可以缺省的,我们开发者并不需要显式的声明无参构造方法,把这项工作交给编译器就可以了。


public class Bike {
    Bike(){
        System.out.println("一辆自行车被创建");
    }

    public static void main(String[] args) {
        Bike bike = new Bike();
    }
}

有参数的构造方法被称为有参构造方法,参数可以有一个或多个。有参构造方法可以为不同的对象提供不同的值。如果没有有参构造方法的话,就需要通过 setter 方法给字段赋值了。

public class ParamConstructorPerson {
    private String name;
    private int age;

    public ParamConstructorPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) {
        ParamConstructorPerson p1 = new ParamConstructorPerson("沉默王二",18);
        p1.out();

        ParamConstructorPerson p2 = new ParamConstructorPerson("沉默王三",16);
        p2.out();
    }
}

在 Java 中,构造方法和方法类似,只不过没有返回类型。它也可以像方法一样被重载。构造方法的重载也很简单,只需要提供不同的参数列表即可。编译器会通过参数的数量来决定应该调用哪一个构造方法

/**
 * @author 沉默王二,一枚有趣的程序员
 */
public class OverloadingConstrutorPerson {
    private String name;
    private int age;
    private int sex;

    public OverloadingConstrutorPerson(String name, int age, int sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public OverloadingConstrutorPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age + " 性别 " + sex);
    }

    public static void main(String[] args) {
        OverloadingConstrutorPerson p1 = new OverloadingConstrutorPerson("沉默王二",18, 1);
        p1.out();

        OverloadingConstrutorPerson p2 = new OverloadingConstrutorPerson("沉默王三",16);
        p2.out();
    }
}

image.png 复制一个对象可以通过下面三种方式完成

  • 通过构造方法
  • 通过对象的值
  • 通过 Object 类的 clone() 方法
/**
 * @author 沉默王二,一枚有趣的程序员
 */
public class ClonePerson implements Cloneable {
    private String name;
    private int age;

    public ClonePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {//clone方法赋值
        return super.clone();
    }

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        ClonePerson p1 = new ClonePerson("沉默王二",18);
        p1.out();

        ClonePerson p2 = (ClonePerson) p1.clone();
        p2.out();
    }
}

通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())。

二、小博哥-设计模式

1.原型模式

原型模式主要解决的问题就是创建重复对象,而这部分对象内容本身比较复杂,生成过程可能从库或者RPC接口中获取数据的耗时较长,因此采用克隆的方式节省时间。

其实这种场景经常出现在我们的身边,只不过很少用到自己的开发中,就像;

  1. 你经常Ctrl+CCtrl+V,复制粘贴代码。
  2. Java多数类中提供的API方法;Object clone()
  3. 细胞的有丝分裂。

需要实现一个上机考试抽题的服务,因此在这里建造一个题库题目的场景类信息,用于创建;选择题问答题

优化前:

itstack-demo-design-4-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── AnswerQuestion.java
                └── ChoiceQuestion.java

原型模式重构后:

itstack-demo-design-4-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │   ├── Topic.java
    │           │   └── TopicRandomUtil.java
    │           ├── QuestionBank.java
    │           └── QuestionBankController.java 
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

image.png

  • 以上的实际场景模拟了原型模式在开发中重构的作用,但是原型模式的使用频率确实不是很高。如果有一些特殊场景需要使用到,也可以按照此设计模式进行优化。
  • 另外原型设计模式的优点包括;便于通过克隆方式创建复杂对象、也可以避免重复做初始化操作、不需要与类中所属的其他类耦合等。但也有一些缺点如果对象中包括了循环引用的克隆,以及类中深度使用对象的克隆,都会使此模式变得异常麻烦。
  • 终究设计模式是一整套的思想,在不同的场景合理的运用可以提升整体的架构的质量。永远不要想着去硬凑设计模式,否则将会引起过度设计,以及在承接业务反复变化的需求时造成浪费的开发和维护成本。
  • 初期是代码的优化,中期是设计模式的使用,后期是把控全局服务的搭建。不断的加强自己对全局能力的把控,也加深自己对细节的处理。可上可下才是一个程序员最佳处理方式,选取最合适的才是最好的选择。

2.单例模式

在设计模式中按照不同的处理方式共包含三大类;创建型模式结构型模式行为模式,其中创建型模式目前已经介绍了其中的四个;工厂方法模式抽象工厂模式生成器模式原型模式,除此之外还有最后一个单例模式

单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。

静态类:

public class Singleton_00 {

    public static Map<String,String> cache = new ConcurrentHashMap<String, String>();
    
}

  • 以上这种方式在我们平常的业务开发中非常常见,这样静态类的方式可以在第一次运行的时候直接初始化Map类,同时这里我们也不需要到延迟加载再使用。
  • 在不需要维持任何状态下,仅仅用于全局访问,这个使用静态类的方式更加方便。
  • 但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式。
  • 单例模式有一个特点就是不允许外部直接创建

懒汉模式(线程不安全):

当有多个实体访问时不安全

public class Singleton_01 {

    private static Singleton_01 instance;

    private Singleton_01() {
    }

    public static Singleton_01 getInstance(){
        if (null != instance) return instance;
        instance = new Singleton_01();
        return instance;
    }

}

懒汉模式(线程安全):

此种模式虽然是安全的,但由于把锁加到方法上后,所有的访问都因需要锁占用导致资源的浪费。如果不是特殊情况下,不建议此种方式实现单例模式。

public class Singleton_02 {

    private static Singleton_02 instance;

    private Singleton_02() {
    }

    public static synchronized Singleton_02 getInstance(){
        if (null != instance) return instance;
        instance = new Singleton_02();
        return instance;
    }

}

饿汉模式(线程安全):

  • 此种方式与我们开头的第一个实例化Map基本一致,在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可。
  • 但此种方式并不是懒加载,也就是说无论你程序中是否用到这样的类都会在程序启动之初进行创建。
public class Singleton_03 {

    private static Singleton_03 instance = new Singleton_03();

    private Singleton_03() {
    }

    public static Singleton_03 getInstance() {
        return instance;
    }

}

使用类的内部类(线程安全)

  • 使用类的静态内部类实现的单例模式,既保证了线程安全又保证了懒加载,同时不会因为加锁的方式耗费性能。
  • 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。
  • 此种方式也是非常推荐使用的一种单例模式
public class Singleton_04 {

    private static class SingletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    private Singleton_04() {
    }

    public static Singleton_04 getInstance() {
        return SingletonHolder.instance;
    }

}

双重锁校验(线程安全)

public class Singleton_05 {

    private static volatile Singleton_05 instance;

    private Singleton_05() {
    }

    public static Singleton_05 getInstance(){
       if(null != instance) return instance;
       synchronized (Singleton_05.class){
           if (null == instance){
               instance = new Singleton_05();
           }
       }
       return instance;
    }

}

3.实战适配器模式

在业务开发中我们会经常的需要做不同接口的兼容,尤其是中台服务,中台需要把各个业务线的各种类型服务做统一包装,再对外提供接口进行使用。而这在我们平常的开发中也是非常常见的。

image.png 优化前:

itstack-demo-design-6-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── mq
                │   ├── create_account.java
                │   ├── OrderMq.java
                │   └── POPOrderDelivered.java
                └── service
                    ├── OrderServicejava
                    └── POPOrderService.java

初次优化(if-else):

itstack-demo-design-6-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── create_accountMqService.java
                └── OrderMqService.java
                └── POPOrderDeliveredService.java

优化后:

itstack-demo-design-6-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   ├── InsideOrderService.java
                │   └── POPOrderAdapterServiceImpl.java
                ├── MQAdapter.java
                ├── OrderAdapterService.java
                └── RebateInfo.java

image.png 先是做MQ适配,接收各种各样的MQ消息。当业务发展的很快,需要对下单用户首单才给奖励,在这样的场景下再增加对接口的适配操作。

三、小型支付商城

1.DDD架构概念

1.1充血模型

指将对象的属性信息与行为逻辑聚合到一个类中,常用的手段如在对象内提供属于当前对象的信息校验拼装缓存Key不含服务接口调用的逻辑处理等。

充血模型(Rich Domain Model)是领域驱动设计(DDD)中的一个重要概念。它强调在领域模型中将业务逻辑与数据紧密结合,创建可操作的对象。这与贫血模型(Anemic Domain Model)相对,后者仅保存数据,没有业务逻辑。

在充血模型中,主要的领域概念包括:

  1. 实体(Entity):实体是具有唯一标识(ID)的对象,表示领域中的一个具体对象。其状态和行为是变化的,生命周期是关键的。实体的身份是业务逻辑决定的,而不是由其属性决定的。

  2. 值对象(Value Object):值对象是没有唯一标识的对象,通常用于描述某些特定的属性或特征。值对象是不可变的(immutable),同样的值代表同样的对象。值对象通常用于封装字段并提供一些操作功能。

  3. 聚合(Aggregate):聚合是一组相关实体和值对象的集合,它们按某种方式聚合在一起以保持一致性。聚合有一个根(Aggregate Root),通过根对象进行外部访问和控制,确保聚合内部的一致性。

区别:

  • 实体 vs 值对象:实体有唯一标识,可以改变其状态;值对象没有唯一标识,通常是不可变的,只用于表达数据。
  • 聚合 vs 实体/值对象:聚合是一个更大的结构,包含多个实体和值对象,以便管理对象之间的关系。

1.2领域模型

指特定业务领域内,业务规则、策略以及业务流程的抽象和封装。在设计手段上,通过风暴模型拆分领域模块,形成界限上下文。最大的区别在于把原有的众多 Service + 数据模型的方式,拆分为独立的有边界的领域模块。每个领域内创建自身所属的;领域对象(实体、聚合、值对象)、仓储服务(DAO 操作)、工厂、端口适配器Port(调用外部接口的手段)等。

那么,现在这里有几个概念;领域服务、领域对象、仓储定义、事件消息、端口适配器。我们先来看他们是怎么从贫血模型演变过来的,在细分讲解每个概念。 image.png

1.3实体、聚合、值对象

实体

是依托于持久化层数据以领域服务功能目标为指导设计的领域对象。持久化PO对象是原子类对象,不具有业务语义,而实体对象是具有业务语义且有唯一标识的对象,跟随于领域服务方法的全生命周期对象。如;用户PO持久化对象,会涵盖,用户的开户实体、授信实体、额度实体对象。也包括如商品下单时候的购物车实体对象。这个对象也通常是领域服务方法的入参对象。

实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。

值对象

这个对象在领域服务方法的生命周期过程内是不可变对象,也没有唯一标识。它通常是配合实体对象使用。如为实体对象提供对象属性值的描述,比如;一个公司雇员的级别值对象,一个下单的商品收货的四级地址信息对象。所以在开发值对象的时候,通常不会提供 setter 方法,而是提供构造函数或者 Builder 方法来实例化对象。这个对象通常不会独立作为方法的入参对象,但做可以独立作为出参对象使用。

值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。

值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。

聚合

当你对数据库的操作需要使用到多个实体时,可以创建聚合对象。一个聚合对象,代表着一个数据库事务,具有事务一致性。聚合中的实体可以由聚合提供创建操作,实体也被称为聚合根对象。一个订单的聚合,会涵盖;下单用户实体对象、订单实体、订单明细实体和订单收货四级地址值对象。而那个作为入参的购物车实体对象,已经被转换为实体对象了。—— 聚合内事务一致性,聚合外最终一致性。

聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理

每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。

1.4 仓储和适配器

在 DDD 的设计方法中,领域层做到了只关心领域服务实现。最能体现这样设计的就是仓库和适配器的设计。通常在 Service + 数据模型的设计中,会在 Service 中引入 Redis、RPC、配置中心等各类其他外部服务。但在 DDD 中,通过仓储和适配器以及基础设施层的定义,解决了这部分内容。

2.DDD 建模方法

DDD 的建模过程,是以一个用户为起点,通过行为命令,发起行为动作,串联整个业务。而这个用户的起点最初来自于用例图的分析。用例图是用户与系统交互的最简表示形式,展现了用户和与他相关的用例之间的关系。通过用例图,我们可以分析出所有的行为动作。

在 DDD 中用于完成用户的行为命令和动作分析的过程,是一个四色建模的过程,也称作风暴模型。在使用 DDD 的标准对系统建模前,一堆人要先了解 DDD 的操作手段,这样才能让产品、研发、测试、运营等了解业务的伙伴,都能在同一个语言下完成系统建模。

image.png

  • 蓝色 - 决策命令,是用户发起的行为动作,如;开始签到、开始抽奖、查看额度等。
  • 黄色 - 领域事件,过去时态描述。如;签到完成、抽奖完成、奖品发放完成。它所阐述的都是这个领域要完成的终态。
  • 粉色 - 外部系统,如你的系统需要调用外部的接口完成流程。
  • 红色 - 业务流程,用于串联决策命令到领域事件,所实现的业务流程。一些简单的场景则直接有决策命令到领域事件就可以了。
  • 绿色 - 只读模型,做一些读取数据的动作,没有写库的操作。
  • 棕色 - 领域对象,每个决策命令的发起,都是含有一个对应的领域对象。

实际案例:

image.png 需求分析:

image.png

image.png

image.png

3.DDD工程模型

说到开发代码为啥需要架构,就像买了个房子,为啥要隔出厨房、客厅、卧室、卫生间一样,核心目的就是让不同的职责分配到不同的区域内。虽然在代码中是没有马桶要放卫生间、沙发要放客厅、床要放卧室。但他有一系列的科目信息要引入到工程。

3.1 工程结构设计

在 DDD 之前,我们一直用 MVC 的分层结构承接这些内容: image.png

虽然这些架构并不是专门为 DDD 而出,但巧的是这些架构都在 DDD 一书发表之后陆续推出新的架构模型。同时这些架构的分层设计方式也都与 DDD 非常契合,在这些架构下也可以很好的落地 DDD 设计方法。

整洁架构的分包形式,会将所有的外部依赖使用和工程内要对外的,统一定义到适配器层。这里可以理解为对适配和对内适配。 image.png

六边形架构,会把本身提供到外部的放到trigger,让接口调用、消息监听、任务调度,都可以统一一个入口处理。而对于需要调用外部同类的能力统一放到 infrastructure 基础设施层,包括;数据库、缓存、配置、调用其他方的接口。

image.png

3.2领域模型设计

  • 方式1;DDD 领域科目类型分包,类型之下写每个业务逻辑。
  • 方式2;业务领域分包,每个业务领域之下有自己所需的 DDD 领域科目。

image.png

DDD 领域驱动设计的中心,主要在于领域模型的设计,以领域所需驱动功能实现和数据建模。一个领域服务下面会有多个领域模型,每个领域模型都是一个充血结构。一个领域模型 = 一个充血结构

image.png

  • model 模型对象;

    • aggreate:聚合对象,实体对象、值对象的协同组织,就是聚合对象。
    • entity:实体对象,大多数情况下,实体对象(Entity)与数据库持久化对象(PO)是1v1的关系,但也有为了封装一些属性信息,会出现1vn的关系。
    • valobj:值对象,通过对象属性值来识别的对象 By 《实现领域驱动设计》
  • repository 仓储服务;从数据库等数据源中获取数据,传递的对象可以是聚合对象、实体对象,返回的结果可以是;实体对象、值对象。因为仓储服务是由基础层(infrastructure) 引用领域层(domain),是一种依赖倒置的结构,但它可以天然的隔离PO数据库持久化对象被引用。

  • service 服务设计;这里要注意,不要以为定义了聚合对象,就把超越1个对象以外的逻辑,都封装到聚合中,这会让你的代码后期越来越难维护。聚合更应该注重的是和本对象相关的单一简单封装场景,而把一些重核心业务方到 service 里实现。此外;如果你的设计模式应用不佳,那么无论是领域驱动设计、测试驱动设计还是换了三层和四层架构,你的工程质量依然会非常差。

  • 对象解释

    • DTO 数据传输对象 (data transfer object),DAO与业务对象或数据访问对象的区别是:DTO的数据的变异子与访问子(mutator和accessor)、语法分析(parser)、序列化(serializer)时不会有任何存储、获取、序列化和反序列化的异常。即DTO是简单对象,不含任何业务逻辑,但可包含序列化和反序列化以用于传输数据。