软件设计模式

本文详细介绍了设计模式中的工厂模式、单例模式和装饰者模式,通过披萨店项目、咖啡馆订单系统和气象站项目等实例,阐述了这些模式在软件开发中的应用和优势。工厂模式用于创建对象,简化了对象的实例化过程;单例模式确保类只有一个实例,常用于管理共享资源;装饰者模式则动态地将责任附加到对象上,增加了代码的灵活性。这些模式提升了代码的可维护性和扩展性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考资料

《设计模式:可复用面向对象软件的基础》
韩顺平老师 - scala

设计模式的作用

  1. 设计模式能让专业人士之间交流方便;
  2. 提高代码的易维护;
  3. 设计模式是编程经验的总结,即通用的编程应用场景的模式化、套路化,站在软件设计层面思考。例如所谓单例模式,就让你拿不到第二个实例。

什么是设计模式

设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式并不是代码,而是某类问题的通用解决方案。
设计模式代表了最佳的实践
这些解决方案是众多软件开发人员经过相当长的时间的试验和错误所总结出来的。
设计模式的本质提高软件的维护性、通用性和扩展性,并降低软件的复杂度。
设计模式并不局限于某种语言,Java、PHP、C++都有设计模式。

设计模式的分类

模式一般分为3个大类,细分为23种小类,

  1. 创建者模式
    单例模式抽象工厂模式,建造者模式,工厂模式,原型模式
  2. 结构型模式
    适配器模式,桥接模式,装饰模式,组合模式,外观模式,享元模式,代理模式
  3. 行为型模式
    模版方法模式,命令模式,迭代器模式,观察者模式,中介者模式,备忘录模式,解释器模式(interpreter模式),状态模式,策略模式,职责链模式(责任链模式),访问者模式

工厂模式

以披萨店项目为例

这个例子就是GoF那本书中用到的例子,韩顺平老师用scala讲了出来。
该例子是:开一个pizza店,目前有GreekPizza和PepperPizza这2种pizza,且不论什么pizza,都需要经过prepare、bake、cut、box共4步。这个项目还要求便于pizza种类的扩展,便于维护,完成pizza订购功能。

简单工厂模式

先从一个简单的开始,简单工厂模式并不是上面说的23种GoF模式之一,但是是工厂模式家族中最简单实用的模式,而且有助于我们理解工厂模式和抽象工厂模式。
简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。该模式定义了一个类,这个类用于创建对象,这个类内部封装了实例化对象的行为/代码
在软件开发中,当我们需要大量的创建某种、某类或者某批对象时,就需要用到工厂模式。

使用传统方式实现

首先定义一个Pizza类,在这个Pizza类中定义成员name和4个方法对应制作Pizza的4个步骤,代码如下,由于GreekPizza和PepperPizza的制作原材料不同,因此prepare()方法在Pizza类中不实现,将来在各种Pizza子类中另行实现。因为Pizza类中有未实现的方法,Pizza类应该被定义为抽象类,

// Pizza.scala
abstract class Pizza {
  var name: String = _

  // 假定,每种pizza的准备原材料不同,因此prepare()方法在这里不实现
  // 因此这个Pizza类应该是抽象的
  def prepare()  // 抽象方法

  def cut(): Unit ={
    println(this.name + " cutting ...")
  }

  def bake(): Unit ={
    println(this.name + " baking ...")
  }

  def box(): Unit ={
    println(this.name + " boxing ...")
  }
}

接着就定义GreekPizza和PepperPizza,分别去继承Pizza类并实现prepare()方法,代码如下,

// GreekPizza.scala
class GreekPizza extends Pizza {
  override def prepare(): Unit = {
    this.name = "GreekPizza"
    println(this.name + " preparing ...")
  }
}
// PepperPizza.scala
class PepperPizza extends Pizza {
  override def prepare(): Unit = {
    this.name = "PepperPizza"
    println(this.name + " preparing ...")
  }
}

为了保持良好的代码管理,关于pizza的类就放入到/src/../Pizza/路径下。
其他关于pizza订购和pizza店的代码放在/src/../Order/路径下。那么在这个路径下写代码实现orderPizza,代码如下,

// OrderPizza.scala
import chapter17.SimpleFactory.PizzaStore.Pizza._
import scala.io.StdIn
import scala.util.control.Breaks._

class OrderPizza {
  var orderType: String = _
  var pizza: Pizza = _
  breakable{
    do {
      println("请输入pizza类型:")
      orderType = StdIn.readLine()
      
      if (orderType.equals("greek")) {
        // 创建pizza
        this.pizza = new GreekPizza
      } else if(orderType.equals("pepper")) {
        // 创建pizza
        this.pizza = new PepperPizza
      } else {
        break()
      }

      this.pizza.prepare()
      this.pizza.bake()
      this.pizza.cut()
      this.pizza.box()
    } while (true)
  }
}

另外新建一个object,实现主方法,并调用上面的OrderPizza,代码如下,

// PizzaStore.scala
object PizzaStore {
  def main(args: Array[String]): Unit = {
    new OrderPizza
  }
}

在这里run程序,可以实现相应的功能,提示你输入某种pizza,输入greek就会打印显示GreekPizza的4个步骤,输入pepper就会打印显示PepperPizza的4个步骤,输入其他就会退出(break)。总体的文件结构如下,

PizzaProject/
  Order/
    OrderPizza.scala
    PizzaStore.scala
  Pizza/
    Pizza.scala
    GreekPizza.scala
    PepperPizza.scala

这里总结一下传统实现方法的优缺点:

  • 优点是好理解,简单易操作
  • 缺点是违反了设计模式的ocp原则,即对外扩展开放,对修改关闭

为了体现传统方法的劣势,现在这个pizza店项目增加了需求,除了现在可以OrderPizza之外,还要支持批发pizza功能、现场SalePizza功能和外卖pizza的功能,每种功能逻辑都是一样的,根据输入的不同,new不同的pizza对象出来。
现在思考,如果pizza种类变化怎么办,例如新增CheesePizza,是不是需要同时在所有功能中进行相应的如下变动?现在是4个功能都需要增加对CheesePizza的支持,如果要在30个功能呢?如果继续增加功能呢?

      if (orderType.equals("greek")) {
        // 创建pizza
        this.pizza = new GreekPizza
      } else if(orderType.equals("pepper")) {
        // 创建pizza
        this.pizza = new PepperPizza
      } else if(orderType.equals("cheese")) {
        // 增加对CheesePizza的支持
        this.pizza = new CheesePizza
      } else {
        break()
      }

针对上面这种情况,就把创建pizza对象的过程封装到一个类中,这样如果有新的pizza种类时,只需要修改这个类即可。其他需要创建pizza对象的代码就不需要修改了。这就是简单工厂方法。

使用简单工厂方法实现

定义一个可以实例化Pizza对象的类,这个类封装创建对象的代码,在/Order路径下,新建一个对象(比class更简单,如果是class,就new这个class再调用createPizza方法),名为SimpleFactory,代码如下,

// SimplePizzaFactory.scala
import chapter17.SimpleFactory.PizzaStore.Pizza._

object SimpleFactory {
  // 提供了一个创建pizza的方法,有需要创建pizza时调用该方法即可
  def createPizza(t: String): Pizza = {
    var pizza: Pizza = null
    if (t.equals("greek")) {
      // 创建pizza
      pizza = new GreekPizza
    } else if (t.equals("pepper")) {
      // 创建pizza
      pizza = new PepperPizza
    } else if (t.equals("cheese")) {
      pizza = new CheesePizza
    }
    pizza
  }
}

也就是说SimpleFactory的createPizza方法将原来OrderPizza中new披萨对象的代码提取并抽象出来,那么OrderPizza相应做如下改动,

// OrderPizza.scala
import chapter17.SimpleFactory.PizzaStore.Pizza._
import scala.io.StdIn
import scala.util.control.Breaks._

class OrderPizza {
  var orderType: String = _
  var pizza: Pizza = _
  breakable{
    do {
      println("请输入pizza类型:")
      orderType = StdIn.readLine()
      // 使用了简单工厂模式
      pizza = SimpleFactory.createPizza(orderType)
      if (pizza == null) break()

      this.pizza.prepare()
      this.pizza.bake()
      this.pizza.cut()
      this.pizza.box()
    } while (true)
  }
}

OrderPizza原先代码中,new pizza的那一堆if-else语句变成了一句pizza = SimpleFactory.createPizza(orderType),其他诸如批发、外卖等功能需要new pizza的地方,就都这样改。如果将来pizza种类变更,例如新增糖果披萨,那么就只需要修改SimpleFactory.createPizza()方法即可,增加else if对糖果披萨的支持。

工厂方法模式

需求扩展

披萨项目有新的需求,原先不同口味的披萨下面有新的子类披萨,例如北京奶酪披萨、北京胡椒披萨、伦敦奶酪披萨、伦敦胡椒披萨。

仍使用简单工厂模式完成

可以创建不同的简单工厂类,比如BJPizzaSimpleFactory、LDPizzaSimpleFactory,这里代码就不完成了。简单工厂模式在这种情况下有局限性,考虑到项目将来的规模,软件的可维护性、可扩展性不好。

工厂方法模式

定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类
首先,必须创建新的pizza种类,代码如下,路径在/Pizza/下,

// BJCheesePizza.scala
class BJCheesePizza extends Pizza {
  override def prepare(): Unit = {
    this.name = "BJCheesePizza"
    println(this.name + " preparing ...")
  }
}

// BJPepperPizza.scala
class BJPepperPizza extends Pizza {
  override def prepare(): Unit = {
    this.name = "BJPepperPizza"
    println(this.name + " preparing ...")
  }
}

同理新建伦敦相应的披萨种类LDCheesePizza和LDPepperPizza。
这里已经慢慢发现有类爆炸的趋势了,将来装饰者模式可以解决这个问题。
回到工厂方法模式,简单工厂模式的OrderPizza.scala中使用了SimpleFactory中定义的createPizza方法,而在工厂模式中,直接将createPizza方法抽象定义在OrderPizza类中,另外新创建OrderBJPizza类和OrderLDPizza类去继承OrderPizza并实现各自的createPizza方法,代码如下,路径在/Order/下,

// OrderPizza.scala
import chapter17.FactoryMethod.Pizza._
import scala.io.StdIn
import scala.util.control.Breaks._

// 这里将OrderPizza做成抽象类
abstract class OrderPizza {
  var orderType: String = _
  var pizza: Pizza = _
  breakable{
    do {
      println("请输入pizza类型:")
      orderType = StdIn.readLine()
      // 使用了工厂方法模式,createPizza方法在子类中实现
      pizza = createPizza(orderType)
      if (pizza == null) break()

      this.pizza.prepare()
      this.pizza.bake()
      this.pizza.cut()
      this.pizza.box()
    } while (true)
  }

  // 抽象的方法,createPizza(orderType),让各个子类去实现这个方法
  def createPizza(t: String): Pizza
}

下面是OrderBJPizza的代码实现,

// OrderBJPizza.scala
import chapter17.FactoryMethod.Pizza._

class OrderBJPizza extends OrderPizza {
  override def createPizza(t: String): Pizza = {
    var pizza: Pizza = null
    if (t.equals("pepper")) {
      // 创建pizza
      pizza = new BJPepperPizza
    } else if (t.equals("cheese")) {
      pizza = new BJCheesePizza
    }
    pizza
  }
}

下面是OrderLDPizza的代码实现

// OrderLDPizza.scala
import chapter17.FactoryMethod.Pizza._

class OrderLDPizza extends OrderPizza {
  override def createPizza(t: String): Pizza = {
    var pizza: Pizza = null
    if (t.equals("pepper")) {
      // 创建pizza
      pizza = new LDPepperPizza
    } else if (t.equals("cheese")) {
      pizza = new LDCheesePizza
    }
    pizza
  }
}

需要任何子类口味的披萨,就new相应子类的工厂,代码如下,

object PizzaStore {
  def main(args: Array[String]): Unit = {
    // 使用工厂方法模式
    // 将BJ和LD的pizza的实现方法下沉到OrderBJPizza和OrderLDPizza中
    new OrderBJPizza
  }
}

这就是工厂方法模式,目录结构如下,

PizzaProject/
  Order/
    OrderPizza.scala
    PizzaStore.scala
    OrderBJPizza.scala
    OrderLDPizza.scala
  Pizza/
    Pizza.scala
    GreekPizza.scala
    PepperPizza.scala
    CheesePizza.scala
    BJCheesePizza.scala
    BJPepperPizza.scala
    LDCheesePizza.scala
    LDPepperPizza.scala

抽象工厂模式

抽象工厂模式定义了一个trait用于创建相关或有依赖关系的对象簇,而无需指明具体的类。
抽象工厂模式将简单工厂模式和工厂方法模式进行整合。
从设计层面看,抽象工厂模式就是对简单工厂模式的改进,或者说进一步抽象。
将工厂抽象成2层:AbsFactory(抽象工厂)和具体实现的工厂子类。程序员可以根据创建对象的类型使用对应的工厂子类。这样将单个的简单工厂类变成了工厂簇,更利于代码的维护和扩展。

首先定义一个名为AbsFactorytrait,里面有一个没有实现的createPizza()方法,

// AbsFactory.scala
import chapter17.AbstractFactory.Pizza.Pizza

trait AbsFactory {
  // 一个抽象方法
  def createPizza(t: String): Pizza
}

接着定义各个工厂去继承这个trait,代码如下,

// BJFactory.scala
import chapter17.AbstractFactory.Pizza._

// 这是一个实现了AbsFactory的一个子工厂类
// 如果希望订购BJ的pizza就使用这个工厂子类即可
class BJFactory extends AbsFactory {
  override def createPizza(t: String): Pizza = {
    var pizza: Pizza = null
    if (t.equals("cheese")) {
      pizza = new BJCheesePizza
    } else if (t.equals("pepper")) {
      pizza = new BJPepperPizza
    }
    pizza
  }
}
// LDFactory.scala
import chapter17.AbstractFactory.Pizza._

class LDFactory extends AbsFactory {
  override def createPizza(t: String): Pizza = {
    var pizza: Pizza = null
    if (t.equals("cheese")) {
      pizza = new LDCheesePizza
    } else if (t.equals("pepper")) {
      pizza = new LDPepperPizza
    }
    pizza
  }
}

至于抽象工厂的使用,代码如下,

// OrderPizza.scala
import chapter17.AbstractFactory.Pizza._
import scala.io.StdIn
import scala.util.control.Breaks._

// 当我们使用抽象工厂模式后,我们订购一个pizza的思路是,
// 1. 接收一个子工厂的实例,根据该子工厂的创建要求去实例化
class OrderPizza {
  var absFactory: AbsFactory = _
  // 多态
  def this(absFactory: AbsFactory) {
    this
    breakable {
      var orderType: String = null
      var pizza: Pizza = null
      do {
        println("请输入pizza的类型,使用抽象工厂模式:")
        orderType = StdIn.readLine()
        pizza = absFactory.createPizza(orderType)
        if (pizza == null) break()
        pizza.prepare()
        pizza.bake()
        pizza.cut()
        pizza.box()
      } while(true)
    }
  }
}

与工厂方法模式不同,抽象工厂模式是在主方法里实例化OrderPizza,并给OrderPizza传入一个工厂类。代码如下,

// PizzaStore.scala
object PizzaStore {
  def main(args: Array[String]): Unit = {
    // 使用工厂方法模式
    // 将BJ和LD的pizza的实现方法下沉到OrderBJPizza和OrderLDPizza中
    new OrderPizza(new BJFactory)
  }
}

这里写一下设计模式的依赖抽象原则,我将来再理解,

  • 创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,方法返回这个类。也有的书上说,变量不要直接持有具体类的引用,例如最好不要这些写代码:
val dog = new Dog
  • 不要让类继承具体类,而是继承抽象类或者trait
  • 不要覆盖基类中已经实现的方法

单例模式

什么是单例模式

保证在整个软件系统中,某个类只能存在一个对象实例。

单例对象的应用场景

AKKA中的ActorSystem就是一个单例,ActorSystem不能有多个实例。

Scala中没有静态的概念,所以为了实现Java中单例模式的功能,可以直接采用类对象(伴生对象)方式构建单例对象。

单例模式的两种实现方式

懒汉式

创建文件TestSingleTon.scala,在这个文件中实现单例模式及其测试。

第1步,将想要实现单例的类的构造方法私有化,代码如下

// 将SingleTon的构造方法私有化
class SingleTon private() {}

第2步,在该类的伴生对象中,写一个方法,返回这个SingleTon类,注意伴生对象在Java底层对应的是SingleTon$.class,代码如下,

// 懒汉式
object SingleTon {  // 对应的是SingleTon$类
  private var s: SingleTon = null
  def getInstance = {
    if(s == null) {
      s = new SingleTon
    }
    s
  }
}

从上面代码可以看出,懒汉式就是先判断这个类的对象是否被创建过,如果已经创建了,就不再创建。

第3步,测试,这个文件的完整代码如下,

object TestSingleTon {
  def main(args: Array[String]): Unit = {
    val instance1 = SingleTon.getInstance
    val instance2 = SingleTon.getInstance

    if (instance1 == instance2) println("Equal!!")  // 相等,说明instance1和instance2是同一个对象
  }
}

// 将SingleTon的构造方法私有化
class SingleTon private() {}

// 懒汉式
object SingleTon {  // 对应的是SingleTon$类
  private var s: SingleTon = null
  def getInstance = {
    if(s == null) {
      s = new SingleTon
    }
    s
  }
}

饿汉式

饿汉式的代码和懒汉式差不多,我直接把代码贴过来,

object TestSingleTon2 {
  def main(args: Array[String]): Unit = {
    val ins1 = SingleTon2.getInstance
    val ins2 = SingleTon2.getInstance
    if (ins1 == ins2) println("相等!")
  }
}

// 将SingleTon的构造方法私有化
class SingleTon2 private() {}

// 饿汉式
object SingleTon2 {  // 对应的是SingleTon$类
  private val s: SingleTon2 = new SingleTon2

  def getInstance = {
    s
  }
}

这就是饿汉式,在伴生对象中上来就直接new一个对象出来,并且可以用val变量,getInstance方法直接将单例对象返回。

装饰者模式(Decorator)

以咖啡馆项目为例

咖啡馆订单系统项目需求:

  • 咖啡种类/单品咖啡:Espresso(意式浓缩咖啡)、ShortBlack、LongBlack、Decaf
  • 调料:Milk、Soy、Chocolate
  • 要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便
  • 使用OO(面向对象)来计算不同种类咖啡的费用:客户可以点单品咖啡,也可以单品咖啡+调料组合

很差的方案

一个很差的方案就是单品咖啡各自是类,单品咖啡与调料的组合也单独为类,那么单品咖啡就是4个类,各加一种调料就有12个类,如果各加2中调料呢?如果将来新增一种单品咖啡或者调料就会导致类的数量倍增。这就导致了类爆炸。

稍有改进的方案

将调料内置到Drink类,并添加hasMilk()setMilk()等方法添加调料或者判断某种调料是否有添加,这样就不会造成类的数量过多。

稍有改进的方案

但是这种方案,在增删调料种类时,代码维护量仍然很大,Drink.scala会慢慢变得巨长无比。

装饰者模式

装饰者模式原理:

  1. 装饰者模式就像打包一个快递
    a. 主体:比如衣服、鞋子等买的东西
    b. 包装:比如报纸填充、塑料纸、纸板等
  2. Component,主体,就是前面咖啡店例子中的Drink,用于让单品咖啡继承的类
  3. ConcreteComponent和Decorator,其中ConcreteComponent指的是单品咖啡,Decorator指的是各种调料
  4. 在Component和ConcreteComponent之间,如果ConcreteComponent类有很多,还可以设计一个缓冲层,将共有的部分提取出来,抽象成一个类

装饰者模式定义

动态的将新功能附加到对象上,在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则(ocp)

装饰者模式的咖啡店项目解决方案

装饰者模式解决方案

2份chocolate+1份milk装饰的LongBlack

说明:

  • 首先有一份Milk+longblack【milk装饰longblack】
  • 使用一份chocolate装饰【milk + longblack】
  • 使用一份chocolate装饰【chocolate+ milk + longblack】
  • cost使用递归计算

装饰者模式代码实现

首先新建2个目录,一个叫coffee用于放单品咖啡类,另一个叫decorator用于放调料。
然后新建一个Drink.scala抽象类,提取单品咖啡的共有部分,这个Drink.scala抽象类就是装饰者模式中的Component,代码如下,

// Drink.scala

// 说明
// 1. Drink表示饮品,是一个抽象类
abstract class Drink {
  var description = ""
  private var price = 0.0f

  def setDescription(description: String): Unit ={
    this.description = description
  }

  def getDescription(): String = {
    description + " price: " + this.getPrice()
  }

  def getPrice(): Float ={
    price
  }

  def setPrice(price: Float): Unit ={
    this.price = price
  }
  // 将计算价格的方法做成一个抽象方法cost()
  def cost(): Float
}

这里在Component和ConcreteComponent之间做一个缓冲层,Coffee.scala放在coffee路径下,用Coffee去继承Drink并实现cost()方法,这样做的好处是,将来咖啡店扩展业务出现奶茶业务,可以与咖啡互相独立。

import chapter17.Decorator.Drink

class Coffee extends Drink{
  override def cost(): Float = {
    super.getPrice()
  }
}

下面再创建单品咖啡,即ConcreteComponent,去继承上面这个缓冲层,

// 这个是单品咖啡,在装饰者模式中的ConcreteComponent
class Espresso extends Coffee {
  // 使用主构造器
  super.setDescription("Espresso")
  super.setPrice(6.0f)
}

其他单品咖啡与Espresso类似,这里就不贴了。这样,ConcreteComponent这部分就完成了。
下面来完成Decorator

// Decorator.scala

import chapter17.Decorator.Drink

class Decorator extends Drink{

  // obj就是被装饰的对象Drink
  // obj可以是单品咖啡,也可以是单品咖啡+调料的组合
  private var obj: Drink = null

  def this(obj: Drink){
    this
    this.obj = obj
  }

  // 这里实现了cost()方法,使用了递归方式
  override def cost(): Float = {
    super.getPrice() + obj.cost()
  }

  // 获取信息时,也需要递归的获取
  override def getDescription(): String = {
    super.getDescription() + "&&" + obj.getDescription()
  }
}

具体的调料,就需要去继承这个Decorator类,代码如下,这里只贴Chocolate,其他调料类似,

// Chocolate.scala
import chapter17.Decorator.Drink

class Chocolate(obj: Drink) extends Decorator(obj) {
  setDescription("Chocolate")
  // 1份巧克力3块钱
  setPrice(3.0f)
}

那么我们现在来看如何使用装饰者模式完成咖啡店下订单,新建CoffeeBar.scala,首先我们点一份单品咖啡Decaf,下面代码可以输出价格和名字,

// CoffeeBar.scala
import chapter17.Decorator.coffee.Decaf

object CoffeeBar {
  def main(args: Array[String]): Unit = {
    println("Coffee Bar ...")

    val order: Drink = new Decaf // 点一个Decaf单品咖啡
    println("order1 price: " + order.cost())
    println("order1 description: " + order.getDescription())

    println("-----------------------")
  }
}

接着我们点一份LongBlack,并添加1份Milk,代码如下,

    var order2: Drink = new LongBlack
    order2 = new Milk(order2)
    
    println("order2 price: " + order2.cost())
    println("order2 description: " + order2.getDescription())

通过cost()方法就可以计算订单价格。那么对于order2,我们继续加调料,现在加2份chocolate,代码如下,

    // 点一份LongBlack,并加入1份Milk和2份Chocolate
    var order2: Drink = new LongBlack
    order2 = new Milk(order2)
    order2 = new Chocolate(order2)
    order2 = new Chocolate(order2)

    println("order2 price: " + order2.cost())
    println("order2 description: " + order2.getDescription())

CoffeeBar.scala的完整代码如下,

// CoffeeBar.scala
import chapter17.Decorator.coffee._
import chapter17.Decorator.decorator._

object CoffeeBar {
  def main(args: Array[String]): Unit = {
    println("Coffee Bar ...")

    val order1: Drink = new Decaf // 点一个Decaf单品咖啡
    println("order1 price: " + order1.cost())
    println("order1 description: " + order1.getDescription())

    println("-----------------------")

    // 点一份LongBlack,并加入1份Milk和2份Chocolate
    var order2: Drink = new LongBlack
    order2 = new Milk(order2)
    order2 = new Chocolate(order2)
    order2 = new Chocolate(order2)

    println("order2 price: " + order2.cost())
    println("order2 description: " + order2.getDescription())
  }

代码的文件结构如下,

Decorator/
  Drink.scala
  CoffeeBar.scala
  coffee/
    Coffee.scala
    Decaf.scala
    Espresso.scala
    LongBlack.scala
    ShortBlack.scala
  decorator/
    Decorator.scala
    Chocolate.scala
    Milk.scala
    Soy.scala

这种装饰模式最大的好处,就是,将来我们增加调料或者单品咖啡,整体代码结构都不需要变,只需要单独增加相应的class,然后在使用装饰模式时使用新的调料或者单品咖啡即可。

观察者模式(Observer)

气象站项目

项目需求:

  • 气象站可以将每天测量到的温度、湿度、气压等以公告板的形式发布出去,比如发布到自己的网站;
  • 需要设计开放型API,便于其他第三方公司也能接入气象站获取数据;
  • 提供温度、气压和湿度的接口;
  • 测量数据更新时,要能实时的通知给第三方

通过对气象站项目的分析,可以初步设计一个WeatherData类,如下图所示,

通过dataChange()方法更新公告板

说明:

  • 通过getXxx方法,可以让第三方公司接入,并得到相关信息
  • 当数据有更新时,气象站通过调用dataChange()方法更新数据,当第三方再次获取数据时,就能拿到最新数据,当然也可以推送

一个不好的解决方案

示意图如下,

不好的解决方案示意图

WeatherData有数据更新时,其dataChange方法会去调用CurrentConditionsupdate()方法更新公告板数据。

代码如下,首先是WeatherData.scala

// WeatherData.scala
// 这个类可以理解为,气象局维护的、提供天气情况数据的核心类
class WeatherData {
  private var mTemperature: Float = _ // 温度
  private var mPressure: Float = _    // 气压
  private var mHumidity: Float = _    // 湿度
  // 这个成员很重要,可以理解为气象局的天气公告板
  private var mCurrentConditions: CurrentConditions = _

  // 构建WeatherData对象时,将mCurrentConditions传入
  def this(mCurrentConditions: CurrentConditions) {
    this
    this.mCurrentConditions = mCurrentConditions
  }

  def getTemperature(): Float ={
    mTemperature
  }
  def getPressure(): Float = {
    mPressure
  }
  def getHumdity(): Float = {
    mHumidity
  }

  // 将最新数据update给公告板mCurrentConditions
  def dataChange(): Unit ={
    mCurrentConditions.update(getTemperature(),getPressure(),getHumdity())
  }

  // 设置最新的天气数据的方法
  def setData(mTemperature: Float, mPressure: Float, mHumidity: Float) = {
    this.mTemperature = mTemperature
    this.mPressure = mPressure
    this.mHumidity = mHumidity
    // 要去修改公告板的最新数据
    dataChange()
  }
}

然后是气象局公告板的实现,主要有update()display()两个方法,

// CurrentConditions.scala
// 气象局的天气公告板
class CurrentConditions {
  private var mTemperature: Float = _ // 温度
  private var mPressure: Float = _    // 气压
  private var mHumidity: Float = _    // 湿度

  def display(): Unit ={
    println("***Today Temprature: " + mTemperature + " ***")
    println("***Today Pressure: " + mPressure + " ***")
    println("***Today Humidity: " + mHumidity + " ***")
  }

  def update(mTemperature: Float, mPressure: Float, mHumidity: Float): Unit ={
    // 更新天气公告板的数据
    this.mTemperature = mTemperature
    this.mPressure = mPressure
    this.mHumidity = mHumidity
    // 显示
    display()
  }
}

这个方案使用的代码如下,

// InternetWeather.scala
object InternetWeather {
  def main(args: Array[String]): Unit = {
    // 创建一个气象局天气公告板
    val mCurrentConditions = new CurrentConditions()
    // 再创建一个WeatherData,并将公告板mCurrentConditions设置
    val mWeatherData = new WeatherData(mCurrentConditions)
    // 比如天气情况变化,这里设置最新数据
    mWeatherData.setData(30, 150, 40)
  }
}

这个方案存在很严重的问题,不能动态的让第三方接入气象站获取数据,比如说新浪想获取数据,那么就需要仿照CurrentConditions.scala新建自己的类,并且修改WeatherData的构造器和dataChange()方法,增加对新浪的支持。那么如果又有一家公司想获取天气数据,就仍然需要做这样的代码修改,违反了ocp原则。

观察者模式

观察者模式原理

观察者模式类似于订牛奶的业务,

  • 奶站:Subject
  • 客户:Observer

其中Subject有登记注册、移除和通知的功能。例如一个客户小杨想喝牛奶,就去奶站注册登记register,每天早晨奶站有了新牛奶,就通知奶站的注册客户来取牛奶。将来某一天,小杨不想喝奶了,就去奶站取消订购,奶站remove小杨这个客户,那么每天早晨就不会通知小杨来取牛奶了。

那么Subject就至少需要3个功能,

  • registerObserver 注册
  • removeObserver 移除
  • notifyObservers 通知所有注册的用户,根据不同需求,可以是更新数据,让用户来取,也可能是实施推送,看具体需求来定

对象之间是多对一的依赖关系时,就可以使用观察者模式,被依赖的对象是Subject,依赖者是ObserverSubject通知Observer变化,比如奶站就是Subject,用户是Observer

观察者模式设计方案

观察者模式设计方案

设计2个trait,一个是Subject,定义气象数据类WeatherDataSt去混入该特质。气象数据类WeatherDataSt中有三个方法,registerObserverremoveObserver是给Observer使用的,可以注册也可以移除,notifyObserver是给自己用的。

观察者模式的好处

观察者模式设计后,会以集合的方式来管理用户(Observer),包括注册、移除和通知。

这样,我们增加观察者(这里可以理解为一个新的公告板),就不需要去修改核心类WeatherDataSt,它可以作为一个独立的进程保持运行,无需重新加载。

观察者模式代码实现

首先定义两个trait,一个是Subject,另一个是Observer,不同的角色将来去混入不同的trait,代码如下,

// Subject.scala
trait Subject {
  // 注册
  def registerObserver(o: Observer)
  // 移除
  def removeObserver(o: Observer)
  // 通知
  def notifyObserver()
}
// Observer.scala
trait Observer {
  // 抽象方法,等待具体的观察者实现
  def update(mTemperature: Float, mPressure: Float, mhumidity: Float)
}

然后就可以按照角色来混入了,我们先写数据核心类,作为Subject,代码如下,

// WeatherDataSt.scala
import scala.collection.mutable.ListBuffer

class WeatherDataSt extends Subject {
  // 集合,用于管理所有的观察者Observer
  private val mObservers: ListBuffer[Observer] = ListBuffer()

  private var mTemperature: Float = _ // 温度
  private var mPressure: Float = _    // 气压
  private var mHumidity: Float = _    // 湿度

  def getTemperature(): Float ={
    mTemperature
  }
  def getPressure(): Float = {
    mPressure
  }
  def getHumdity(): Float = {
    mHumidity
  }
  
  def dataChange(): Unit ={
    // 一旦天气变化,就通知所有注册的观察者
    notifyObserver()
  }

  // 天气变化,设置最新的天气数据的方法
  def setData(mTemperature: Float, mPressure: Float, mHumidity: Float) = {
    this.mTemperature = mTemperature
    this.mPressure = mPressure
    this.mHumidity = mHumidity
    // 要去修改公告板的最新数据
    dataChange()
  }

  // 注册
  override def registerObserver(o: Observer): Unit = {
    // 一旦有观察者注册,就把这个观察者加入集合
    mObservers.append(o)
  }

  // 移除,比如某个第三方不想订阅了
  override def removeObserver(o: Observer): Unit = {
    if(mObservers.contains(o)) {
      // 不要使用remove方法,因为remove方法使用index移除
      // 直接使用 -= 可以直接删除这个观察者
      mObservers -= o
    }
  }

  // 通知,天气变化了,就通知所有注册的观察者
  override def notifyObserver(): Unit = {
    for(observer <- mObservers) {
      observer.update(mTemperature, mPressure, mHumidity)
    }
  }
}

Subject角色的代码中,我们看到用了一个ListBuffer集合去管理Observer

notifyObserver方法调用了观察者所混入的Observerupdate方法,那么现在来看一个观察者的代码实现,

// CurrentConditions.scala
// 相当于气象局的公告板,也作为一个观察者
class CurrentConditions extends Observer {
  private var mTemperature: Float = _
  private var mPressure: Float = _
  private var mHumidity: Float = _

  override def update(mTemperature: Float, mPressure: Float, mHumidity: Float): Unit = {
    this.mTemperature = mTemperature
    this.mPressure = mPressure
    this.mHumidity = mHumidity

    // 显示
    display()
  }

  def display(): Unit ={
    println("*** Today Temprature: " + mTemperature + " ***")
    println("*** Today Pressure: " + mPressure + " ***")
    println("*** Today Humidity: " + mHumidity + " ***")
  }
}

观察者模式的使用代码如下,

// InternetWeather.scala
object InternetWeather {
  def main(args: Array[String]): Unit = {
    // 创建气象站自己的公告板,也应该是一个观察者,因为获取数据
    val mCurrentConditions = new CurrentConditions
    
    // 创建核心数据类,也就是观察者模式的Subject
    val mWeatherDataSt = new WeatherDataSt

    // mCurrentConditions注册
    mWeatherDataSt.registerObserver(mCurrentConditions)
    
    // 现在天气变化,设置最新的数据,那么mCurrentConditions就应该显示出天气数据
    mWeatherDataSt.setData(30.0f, 40.0f, 100.0f)
  }
}

从上面的代码可以看出,可以创建一个观察者,然后使用registerObserver方法注册,就可以在天气数据更新时,收到通知。那么如果又来一个观察者,只需要另外写一个类,与CurrentConditions类似,还可以添加自己的业务逻辑,然后在主方法添加下面两行代码,

    // 创建新浪天气公告板
    val sinaCurrentConditions = new SinaCurrentConditions
    // sinaCurrentConditions注册
    mWeatherDataSt.registerObserver(sinaCurrentConditions)

这样,一旦核心数据类Subject调用了setData方法,注册的观察者就都可以被notify到。

Java中已经内置了Observer模式,见java.util.Observable,是一个Java类,已经实现了addObserverdeleteObservernotifyObservers方法。

代理模式Proxy(未完待续)

代理模式基本介绍

  • 代理模式为一个对象提供一个替身,以控制对这个对象的访问
  • 被代理的对象可以是一个远程对象创建开销大的对象或需要安全控制的对象
  • 代理模式有不同的形式(比如远程代理、静态代理、动态代理),都是为了控制与管理对象访问

糖果机项目

项目需求:

  • 公司需要将糖果机放置在本地(本地监控)和外地(远程监控),进行糖果销售,并监视销售情况
  • 给糖果机插入硬币、转动手柄就可以购买糖果,就是自动售卖机
  • 可以监控糖果机的状态和销售情况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值