Processing 也需要设计模式—— 桥接模式【Java】|8月更文挑战

358 阅读4分钟

这是我参与8月更文挑战的第4天,活动详情查看: 8月更文挑战

什么是设计模式

什么是设计模式?设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计程序精英们的经验总结

为什么使用设计模式?使用设计模式是为了可重用代码(不修改或者少修改代码来解决新的问题),减少重复工作、让代码更容易被他人理解、保证代码可靠性。

设计模式是编程过程中的一种中间层面的技巧或者经验(微观层面的如算法、数据结构、语法,宏观层面的如框架、工具箱、系统架构),是许多优秀的程序员通过大量实际项目的磨练所总结出来的 “内功秘籍”, 通过学习他们的高效的方法、总结出经验技巧,那么在我们遇到一个具体问题时,也更加容易想到优秀合理的解决方案。

 什么是桥接模式

桥接模式(Bridge Pattern)是属于结构型的设计模式,它将抽象部分与它的实现部分相分离,使它们都可以独立地变化。

  • Abstraction(抽象类):用于定义抽象类的接口,并且维护一个指向 Implementor 实现类的指针。它与 Implementor 之间具有关联关系(本立的 Dot.java)。

  • RefinedAbstraction(扩充抽象类):扩充由 Abstraction 定义的接口,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法(本例的 SphereDot.java)。

  • Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同(本例的 IEase.java)。

  • ConcreteImplementor(具体实现类):实现了 Implementor 定义的接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现。在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给 Abstraction 具体的业务操作方法(本例的各种缓动类)。

具体实例和代码注释

Bridge 接口的定义 IEase.java

interface IEase {
    float ease(float x);
}

接着就是一些缓动函数类(*.Java)的定义

import processing.core.*;

public class EasingEaseInOutCubic implements IEase {

  static PApplet app;

  public EasingEaseInOutCubic(PApplet papp) {
    app = papp;
  }

  public float ease(float x) {
    if (x < 0.5f)
      return 0.5f * app.pow(2f * x, 3f);
    else
      return 0.5f * app.pow(2f * (x - 1f), 3f) + 1f;
  }
}

import processing.core.*;

public class EasingEaseInOutQuint implements IEase {

  static PApplet app;

  public EasingEaseInOutQuint(PApplet papp) {
    app = papp;
  }

  public float ease(float x) {
    return x<.5f ? 16f*x*x*x*x*x : 1f+16f*(--x)*x*x*x*x;
  }
}

抽象 Dot 类的定义,注意我们里头定义了桥接器 Bridge 的成员变量 iease,在真正调用的时候它会负责桥接具体的缓动函数

public abstract class Dot {

  static PApplet app;

  float center;
  PVector pos;
  float velocity;
  float counter;
  float size;
  float step;
  float ease;
  
  boolean organize;
  
  /// @note Bridge
  IEase iease;

  Dot(PApplet papp) {
    app = papp;
  }

  void setup(float c) {
    counter = app.random(1000);
    this.center = c;
    pos = new PVector(center, center, center);
    velocity = app.random(0.005f, 0.01f);
    step = app.random(1000.f);
    ease = 0.06f;
    organize = false;
  }

  void update() {
    
    float width = app.width;
    float height = app.height;
    
    if (organize) {
      pos.x += ((app.round(pos.x / width * 10) * width / 10) - pos.x) * velocity;
      pos.y += ((app.round(pos.y / height * 10) * height / 10) - pos.y) * ease;
      pos.z += ((app.round(pos.z / height * 10) * height / 10) - pos.z) * ease;
    } else {
      counter += velocity;
      pos.x += ((app.noise(counter * 0.5f + step) - 0.25f) * width * 2 - pos.x) * ease;
      pos.y += ((app.noise(counter * 0.3f + step) - 0.25f) * height * 2 - pos.y) * ease;
      pos.z += ((app.noise(counter * 0.1f + step) - 0.25f) * height * 2 - pos.z) * ease;
    }
    size = app.map(pos.z / width, 0.0f, 0.5f, 3f, 10f);
  }

  abstract void draw(); 
}

具体的 Dot 类的定义,这里是球形为例,它主要实现了基类中未实现的 draw 方法

import processing.core.*;

public class SphereDot extends Dot {

  public float prevRotateAngle = 0.f;
  public float destRotateAngle = 0.f;
  public float rotateAngle = 0.f;
  public float changeTime = 60.f;
  public float lastChangeFrameCount = 0.f;

  SphereDot(PApplet papp) {
    super(papp);
  }

  void draw() {

    float frameCount = app.frameCount;
    boolean tempOrganize = frameCount % (changeTime * 2) > changeTime;
    if (organize != tempOrganize) {
      lastChangeFrameCount = frameCount;
      organize = tempOrganize;
      prevRotateAngle = rotateAngle;
      destRotateAngle += app.PI / 2;
    }
    
    app.push();
    app.translate(center, center, center);
    float easeLerp = iease.ease((frameCount - lastChangeFrameCount) / changeTime);
    rotateAngle = app.lerp(prevRotateAngle, destRotateAngle, easeLerp);
    app.rotateY(rotateAngle);

    prevRotateAngle = rotateAngle;
    destRotateAngle += app.PI / 2;

    app.translate(-center, -center, -center);
    app.pop();

    app.push();
    app.translate(pos.x, pos.y, pos.z);
    app.sphere(size);
    app.pop();

  }

}

这样一来,我们的客户端代码将非常的清爽

ArrayList<Dot> dots = new ArrayList<Dot>();

void setup() {
  
  size(512, 512, P3D);
  float center = width/2;
  ortho(-center, center, -center, center, 0, 1000);
  
  int totalDots = 100;
  for(int i=0; i<totalDots; i++){
    Dot dot = new SphereDot(this);

    /// @note 切换不同的缓动类
    //dot.iease = new EasingEaseInCubic(this);
    //dot.iease = new EasingEaseOutCubic(this);
    //dot.iease = new EasingEaseInOutCubic(this);
    dot.iease = new EasingEaseInOutQuint(this);
    dot.setup(center);
    dots.add(dot);
  }
  
  noStroke();
  //fill(255);
  frameRate(30);
} 

void draw() {
  background(0);  
  
  for(int i=0; i<dots.size(); i++){
    dots.get(i).update();
    dots.get(i).draw();
  }

}

通过桥接模式也很容易修改为方形粒子的效果 ,直接继承 Dot 类得到 BoxDot 类,然后还可以根据自己的喜好选择合适的缓动函数类,并不会影响(修改)之前定义的那些类【开放封闭原则

效果图

球形粒子的缓动特效

  1. EasingEaseInCubic

  2. EasingEaseOutCubic

  1. EasingEaseInOutCubic

  1. EasingEaseInOutQuint(修改缓动函数的同时,改为方形粒子)

桥接模式的优缺点

优点:

  • 分离抽象和实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。即抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便可以进行任意组合,从而获得多维度的组合对象。

  • 在很多情况下,桥接模式可以取代多层继承方案(Java 是单继承,C++ 是支持多继承)。多层继承违背了“单一职责原则”,复用性较差,且类的个数非常多。所以相比起来,桥接模式更好,它极大地减少了子类的个数。

  • 提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

缺点:

  • 增加了系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。

  • 需要能正确识别出系统中两个独立变化的维度,因此使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。