博客记录-day003-java访问权限修饰符、代码初始块、抽象类、接口+桥接模式、组合模式、装饰器模式

81 阅读18分钟

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

1. Java访问权限修饰符

在 Java 中,提供了四种访问权限控制:

  • 默认访问权限(包访问权限)
  • public
  • private
  • protected

1. 修饰类

  • 默认访问权限(包访问权限):用来修饰类的话,表示该类只对同一个包中的其他类可见
  • public:用来修饰类的话,表示该类对其他所有的类都可见

2. 修饰方法和变量

  • 默认访问权限(包访问权限):如果一个类的方法或变量被包访问权限修饰,也就意味着只能在同一个包中的其他类中显示地调用该类的方法或者变量,在不同包中的类中不能显式地调用该类的方法或变量。
  • private:如果一个类的方法或者变量被 private 修饰,那么这个类的方法或者变量只能在该类本身中被访问,在类外以及其他类中都不能显式的进行访问。
  • protected:如果一个类的方法或者变量被 protected 修饰,对于同一个包的类,这个类的方法或变量是可以被访问的。对于不同包的类,只有继承于该类的类才可以访问到该类的方法或者变量
  • public:被 public 修饰的方法或者变量,在任何地方都是可见的。

2.java代码初始块

代码初始化块用于初始化一些成员变量。

对象在初始化的时候会先调用构造方法,这是毫无疑问的,只不过,构造方法在执行的时候会把代码初始化块放在构造方法中其他代码之前,所以,先看到了‘代码初始化块’,后看到了‘构造方法’。

image.png

public class Example {
    // 静态变量
    public static int staticVar = 1;
    // 实例变量
    public int instanceVar = 2;

    // 静态初始化块
    static {
        System.out.println("执行静态初始化块");
        staticVar = 3;
    }

    // 实例初始化块
    {
        System.out.println("执行实例初始化块");
        instanceVar = 4;
    }

    // 构造方法
    public Example() {
        System.out.println("执行构造方法");
    }

    public static void main(String[] args) {
        System.out.println("执行main方法");

        Example e1 = new Example();
        Example e2 = new Example();

        System.out.println("e1的静态变量:" + e1.staticVar);
        System.out.println("e1的实例变量:" + e1.instanceVar);
        System.out.println("e2的静态变量:" + e2.staticVar);
        System.out.println("e2的实例变量:" + e2.instanceVar);
    }
}

在这个示例代码中,有一个静态变量 staticVar 和一个实例变量 instanceVar,以及一个静态初始化块和一个实例初始化块。在静态初始化块中,我们打印了一条消息并修改了静态变量的值;在实例初始化块中,我们也打印了一条消息并修改了实例变量的值。

执行静态初始化块
执行main方法
执行实例初始化块
执行构造方法
执行实例初始化块
执行构造方法
e1的静态变量:3
e1的实例变量:4
e2的静态变量:3
e2的实例变量:4

3.java抽象类

抽象类是不能实例化的,尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。

虽然抽象类不能实例化,但可以有子类。子类通过 extends 关键字来继承抽象类

抽象类中既可以定义抽象方法,也可以定义普通方法。

抽象类派生的子类必须实现父类中定义的抽象方法。

  • 当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。

  • 当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。

    1、抽象类不能被实例化。

    2、抽象类应该至少有一个抽象方法,否则它没有任何意义。

    3、抽象类中的抽象方法没有方法体。

    4、抽象类的子类必须给出父类中的抽象方法的具体实现,除非该子类也是抽象类。

4.java接口

在 Java 中,可以通过两种形式来达到抽象的目的,一种上一篇的主角——抽象类,另外一种就是今天的主角——接口。

接口通过 interface 关键字来定义,它可以包含一些常量和方法,来看下面这个示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 静态方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默认方法
    default void printDescription() {
        System.out.println("电子");
    }
}

反编译后:

public interface Electronic{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    {
        return electtronicType.equals("LED");
    }

    public void printDescription()
    {
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

1)接口中定义的变量会在编译的时候自动加上 public static final 修饰符(注意看一下反编译后的字节码),也就是说上例中的 LED 变量其实就是一个常量。

2)没有使用 privatedefault 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。也就是说上例中的 getElectricityUse() 其实是一个抽象方法,没有方法体——这是定义接口的本意。

3)从 Java 8 开始,接口中允许有静态方法,比如说上例中的 isEnergyEfficient() 方法。

4)接口中允许定义 default 方法也是从 Java 8 开始的,比如说上例中的 printDescription() 方法,它始终由一个代码块组成,为实现该接口而不覆盖该方法的类提供默认实现。

接口不允许直接实例化,否则编译器会报错。需要定义一个类去实现接口,见下例。

public class Computer implements Electronic {

    public static void main(String[] args) {
        new Computer();
    }

    @Override
    public int getElectricityUse() {
        return 0;
    }
}

什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果,鼠标左键点击窗口上的 X 号可以关闭窗口,点击超链接却可以打开新的网页。

多态可以通过继承(extends)的关系实现,也可以通过接口的形式实现。

public interface Shape {
    String name();
}
public class Circle implements Shape {
    @Override
    public String name() {
        return "圆";
    }
}
public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}
List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

变量 circleShape、squareShape 的引用类型都是 Shape,但执行 shape.name() 方法的时候,Java 虚拟机知道该去调用 Circle 的 name() 方法还是 Square 的 name() 方法。

多态存在的 3 个前提:

  • 1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。
  • 2、子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法。
  • 3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。

在编程领域,好的设计模式能够让我们的代码事半功倍。在使用接口的时候,经常会用到三种模式,分别是策略模式、适配器模式和工厂模式。

  • 策略模式的思想是,针对一组算法,将每一种算法封装到具有共同接口的实现类中,接口的设计者可以在不影响调用者的情况下对算法做出改变。并根据所传递的参数对象的不同而产生不同的行为

  • 适配器模式的思想是,针对调用者的需求对原有的接口进行转接。Coach 接口中定义了两个方法(defend() 和 attack()),如果类直接实现该接口的话,就需要对两个方法进行实现。如果我们只需要对其中一个方法进行实现的话,就可以使用一个抽象类作为中间件,即适配器(AdapterCoach),用这个抽象类实现接口,并对抽象类中的方法置空(方法体只有一对花括号),这时候,新类就可以绕过接口,继承抽象类,我们就可以只对需要的方法进行覆盖,而不是接口中的所有方法。

  • 所谓的工厂模式理解起来也不难,就是什么工厂生产什么,比如说宝马工厂生产宝马,奔驰工厂生产奔驰,A 级学院毕业 A 级教练,C 级学院毕业 C 级教练。有两个接口,一个是 Coach(教练),可以 command()(指挥球队);另外一个是 CoachFactory(教练学院),能 createCoach()(教出一名优秀的教练)。然后 ACoach 类实现 Coach 接口,ACoachFactory 类实现 CoachFactory 接口;CCoach 类实现 Coach 接口,CCoachFactory 类实现 CoachFactory 接口。当需要 A 级教练时,就去找 A 级教练学院;当需要 C 级教练时,就去找 C 级教练学院。

// 教练
interface Coach {
    void command();
}
// 教练学院
interface CoachFactory {
    Coach createCoach();
}
// A级教练
class ACoach implements Coach {
    @Override
    public void command() {
        System.out.println("我是A级证书教练");
    }
    
}
// A级教练学院
class ACoachFactory implements CoachFactory {
    @Override
    public Coach createCoach() {
        return new ACoach();
    }
    
}
// C级教练
class CCoach implements Coach {
    @Override
    public void command() {
        System.out.println("我是C级证书教练");
    }
}
// C级教练学院
class CCoachFactory implements CoachFactory {
    @Override
    public Coach createCoach() {
        return new CCoach();
    }    
}

public class Demo {
    public static void create(CoachFactory factory) {
        factory.createCoach().command();
    } 
    public static void main(String[] args) {
        // 对于一支球队来说,需要什么样的教练就去找什么样的学院
        // 学院会介绍球队对应水平的教练。
        create(new ACoachFactory());
        create(new CCoachFactory());
    }
}

在 Java 中,通过关键字 abstract 定义的类叫做抽象类。Java 是一门面向对象的语言,因此所有的对象都是通过类来描述的;但反过来,并不是所有的类都是用来描述对象的,抽象类就是其中的一种。

// 个人认为,一名教练必须攻守兼备
abstract class Coach {
	public abstract void defend();

	public abstract void attack();
}

接口(英文:Interface),在 Java 中是一个抽象类型,是抽象方法的集合;接口通过关键字 interface 来定义。接口与抽象类的不同之处在于:

  • 1、抽象类可以有方法体的方法,但接口没有(Java 8 以前)。
  • 2、接口中的成员变量隐式为 static final,但抽象类不是的。
  • 3、一个类可以实现多个接口,但只能继承一个抽象类
// 隐式的abstract
interface Coach {
	// 隐式的public
	void defend();
	void attack();
}
  • 接口是隐式抽象的,所以声明时没有必要使用 abstract 关键字;
  • 接口的每个方法都是隐式抽象的,所以同样不需要使用 abstract 关键字;
  • 接口中的方法都是隐式 public 的。

二、小傅哥-java设计模式

1.桥接模式

桥接模式的主要作用就是通过将抽象部分与实现部分分离,把多种可匹配的使用进行组合。说白了核心实现也就是在A类中含有B类接口,通过构造函数传递B类的实现,这个B类就是设计的

优化前:

itstack-demo-design-7-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── PayController.java

从上面的ifelse方式实现来看,这是两种不同类型的相互组合。那么就可以把支付方式支付模式进行分离通过抽象类依赖实现类的方式进行桥接,通过这样的拆分后支付与模式其实是可以单独使用的,当需要组合时候只需要把模式传递给支付即可。

桥接模式的关键是选择的桥接点拆分,是否可以找到这样类似的相互组合,如果没有就不必要非得使用桥接模式。

优化后:

itstack-demo-design-7-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design.pay
    │           ├── channel
    │           │   ├── Pay.java
    │           │   ├── WxPay.java
    │           │   └── ZfbPay.java
    │           └── mode
    │               ├── IPayMode.java
    │               ├── PayCypher.java
    │               ├── PayFaceMode.java
    │               └── PayFingerprintMode.java
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

image.png

  • 左侧Pay是一个抽象类,往下是它的两个支付类型实现:微信支付、支付宝支付。
  • 右侧IPayMode是一个接口,往下是它的两个支付模型:刷脸支付、指纹支付。
  • 那么,支付类型 × 支付模型 = 就可以得到相应的组合。
  • 注意,每种支付方式的不同,刷脸和指纹校验逻辑也有差异,可以使用适配器模式进行处理,这里不是本文重点不做介绍,可以看适配器模式章节。

2.组合模式

把相似对象(也可以称作是方法)组合成一组可被调用的结构树对象的设计思路叫做组合模式。

这种设计方式可以让你的服务组节点进行自由组合对外提供服务,例如你有三个原子校验功能(A:身份证B:银行卡C:手机号)服务并对外提供调用使用。有些调用方需要使用AB组合,有些调用方需要使用到CBA组合,还有一些可能只使用三者中的一个。那么这个时候你就可以使用组合模式进行构建服务,对于不同类型的调用方配置不同的组织关系树,而这个树结构你可以配置到数据库中也可以不断的通过图形界面来控制树结构。

所以不同的设计模式用在恰当好处的场景可以让代码逻辑非常清晰并易于扩展,同时也可以减少团队新增人员对项目的学习成本。

优化前:

itstack-demo-design-8-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── EngineController.java

接下来的重构部分代码改动量相对来说会比较大一些,为了让我们可以把不同类型的决策节点和最终的果实组装成一棵可被运行的决策树,需要做适配设计和工厂方法调用,具体会体现在定义接口以及抽象类和初始化配置决策节点(性别年龄)上。建议这部分代码多阅读几次,最好实践下。

优化后:

itstack-demo-design-8-02
└── src
    ├── main
    │   └── java
    │      └── org.itstack.demo.design.domain
    │          ├── model
    │          │   ├── aggregates
    │          │   │   └── TreeRich.java
    │          │   └── vo
    │          │       ├── EngineResult.java
    │          │       ├── TreeNode.java
    │          │       ├── TreeNodeLink.java    
    │          │       └── TreeRoot.java	
    │          └── service
    │              ├── engine
    │              │   ├── impl	
    │              │   │   └── TreeEngineHandle.java
    │              │   ├── EngineBase.java 
    │              │   ├── EngineConfig.java
    │              │   └── IEngine.java	
    │              └── logic
    │                  ├── impl	
    │                  │   ├── UserAgeFilter.java
    │                  │   └── UserGenderFilter.java
    │                  └── LogicFilter.java	
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

image.png

  • 首先可以看下黑色框框的模拟指导树结构;11112111112121122,这是一组树结构的ID,并由节点串联组合出一棵关系树。

  • 接下来是类图部分,左侧是从LogicFilter开始定义适配的决策过滤器,BaseLogic是对接口的实现,提供最基本的通用方法。UserAgeFilterUserGenerFilter,是两个具体的实现类用于判断年龄性别

  • 最后则是对这颗可以被组织出来的决策树,进行执行的引擎。同样定义了引擎接口和基础的配置,在配置里面设定了需要的模式决策节点。

     static {
          logicFilterMap = new ConcurrentHashMap<>();
          logicFilterMap.put("userAge", new UserAgeFilter());
          logicFilterMap.put("userGender", new UserGenderFilter());
     }   
    
包路径介绍
model.aggregatesTreeRich聚合对象,包含组织树信息
model.voEngineResult决策返回对象信息
model.voTreeNode树节点;子叶节点、果实节点
model.voTreeNodeLink树节点链接链路
model.voTreeRoot树根信息

总结:

  • 从以上的决策树场景来看,组合模式主要解决的是一系列简单逻辑节点或者扩展的复杂逻辑节点在不同结构的组织下,对于外部的调用是仍然可以非常简单的
  • 这部分设计模式保证了开闭原则,无需更改模型结构你就可以提供新的逻辑节点的使用并配合组织出新的关系树。但如果是一些功能差异化非常大的接口进行包装就会变得比较困难,但也不是不能很好的处理,只不过需要做一些适配和特定化的开发。

3.装饰器模式

装饰器的核心就是在不改原有类的基础上给类新增功能。不改变原有类,可能有的小伙伴会想到继承、AOP切面,当然这些方式都可以实现,但是使用装饰器模式会是另外一种思路更为灵活,可以避免继承导致的子类过多,也可以避免AOP带来的复杂性。

一般在业务开发的初期,往往内部的ERP使用只需要判断账户验证即可,验证通过后即可访问ERP的所有资源。但随着业务的不断发展,团队里开始出现专门的运营人员、营销人员、数据人员,每个人员对于ERP的使用需求不同,有些需要创建活动,有些只是查看数据。同时为了保证数据的安全性,不会让每个用户都有最高的权限。

那么以往使用的SSO是一个组件化通用的服务,不能在里面添加需要的用户访问验证功能。这个时候我们就可以使用装饰器模式,扩充原有的单点登录服务。但同时也保证原有功能不受破坏,可以继续使用。

优化前:

itstack-demo-design-9-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── LoginSsoDecorator.java

装饰器主要解决的是直接继承下因功能的不断横向扩展导致子类膨胀的问题,而是用装饰器模式后就会比直接继承显得更加灵活同时这样也就不再需要考虑子类的维护。

在装饰器模式中有四个比较重要点抽象出来的点:

  1. 抽象构件角色(Component) - 定义抽象接口
  2. 具体构件角色(ConcreteComponent) - 实现抽象接口,可以是一组
  3. 装饰角色(Decorator) - 定义抽象类并继承接口中的方法,保证一致性
  4. 具体装饰角色(ConcreteDecorator) - 扩展装饰具体的实现逻辑

通过以上这四项来实现装饰器模式,主要核心内容会体现在抽象类的定义和实现上。

优化后:

itstack-demo-design-9-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── LoginSsoDecorator.java
                └── SsoDecorator.java

image.png

  • 以上是一个装饰器实现的类图结构,重点的类是SsoDecorator,这个类是一个抽象类主要完成了对接口HandlerInterceptor继承。
  • 当装饰角色继承接口后会提供构造函数,入参就是继承的接口实现类即可,这样就可以很方便的扩展出不同功能组件。

总结:

  • 使用装饰器模式满足单一职责原则,你可以在自己的装饰类中完成功能逻辑的扩展,而不影响主类,同时可以按需在运行时添加和删除这部分逻辑。另外装饰器模式与继承父类重写方法,在某些时候需要按需选择,并不一定某一个就是最好。
  • 装饰器实现的重点是对抽象类继承接口方式的使用,同时设定被继承的接口可以通过构造函数传递其实现类,由此增加扩展性并重写方法里可以实现此部分父类实现的功能

三、小型支付商城系统

1.需求PRD讲解

以下为详细PRD交互流程,整个流程分为4部分,商城首页、商城服务、微信公众号、支付宝支付。

image.png

2.工程四色建模

  • 在我们这个小项目下,可以寻找的领域,包括;商品下单完成、支付订单创建完成、订单状态变更完成、登录评审创建和校验完成、保存登录状态完成。
  • 所有这些领域事件都是一种完成态(最终的结果态),比如说,要去吃饭,是决策的命令。是你的想法要付诸行动。之后你说吃完饭了,就是最终的领域事件的结果态。从付诸行动到怎么去吃饭,去哪吃饭,要跟谁吃饭,是业务流程。吃饭前要给谁打电话,询问是否开业中,是读模型。

image.png

登录校验: 微信扫码登录的流程主要包括;用户、浏览器、后端服务、公众号,这四个部分。我们可以先通过UML流程图,了解下整个调用关系。

image.png

商品交易:

  • 首先,用户在系统中创建订单(流水单),创建过程中需要判断是否存在未支付订单,存在则可以直接返回。另外还有一种可能,创建的订单存在,但没有支付单,也就是【掉单】。这是因为本身的业务系统和外部的支付创建(支付宝)不是一个事务,不能一起成功或失败,所以要做一些流程的校验。比如我们创建订单成功,但创建支付单失败。这个之后用户继续创建订单,就会优先使用这笔订单创建支付单。如果流程中没有存在的掉单,则直接创建支付单即可。
  • 之后,创建完支付宝订单,会由页面跳转到网络收银台,引导用户完成支付操作。
  • 最后,就是接收支付回调消息,更新本地的订单状态,以及推动后续流程。比如;发放商品、驱动物流、虚拟支付等。当然在实际的商城中,还会有逆向流程,比如商品有问题,或者用户主动发起退单。这个时候就要走逆向流程,退单、审核、退款流程。你可以尝试完成。 image.png