尽管Scala还有一些基于语言特性的设计模式,单本文还是着重于介绍大家所周知的经典设计模式,因为这些设计模式被认为是开发者之间交流的工具。

  • 创建型设计模式

1、工厂方法模式

2、延迟加载模式

3、单例模式

  • 结构型模式

1、适配器模式

2、装饰模式

  • 行为型

1、值对象模式

2、空值模式

3、策略模式

4、命令模式

5、责任链模式

6、依赖注入模式

一、工厂方法模式

工厂方法模式将对实际类的初始化封装在一个方法中,让子类来决定初始化哪个类。

工厂方法允许:

1、组合复杂的对象创建代码

2、选择需要初始化的类

3、缓存对象

4、协调对共享资源的访问

我们考虑静态工厂模式,这和经典的工厂模式略有不同,静态工厂方法避免了子类来覆盖此方法。

在Java中,我们使用new关键字,通过调用类的构造器来初始化对象。为了实现这个模式,我们需要依靠普通方法,此外我们无法在接口中定义静态方法,所以我们只能使用一个额外的工厂类。

  1. public interface Animal {}
  2. private class Dog implements Animal {}
  3. private class Cat implements Animal {}
  4. public class AnimalFactory {
  5. public static Animal createAnimal(String kind) {
  6. if ("cat".equals(kind)) return new Cat();
  7. if ("dog".equals(kind)) return new Dog();
  8. throw new IllegalArgumentException();
  9. }
  10. }
  11. AnimalFactory.createAnimal("dog");

除了构造器之外,Scala提供了一种类似于构造器调用的特殊的语法,其实这就是一种简便的工厂模式。

  1. trait Animal
  2. private class Dog extends Animal
  3. private class Cat extends Animal
  4. object Animal {
  5. def apply(kind: String) = kind match {
  6. case "dog" => new Dog()
  7. case "cat" => new Cat()
  8. }
  9. }
  10. Animal("dog")

以上代码中,工厂方法被定义为伴生对象,它是一种特殊的单例对象,和之前定义的类或特质具有相同的名字,并且需要定义在同一个原文件中。这种语法仅限于工厂模式中的静态工厂模式,因为我们不能将创建对象的动作代理给子类来完成。

优势:

  • 重用基类名字

  • 标准并且简洁

  • 类似于构造器调用

劣势:

  • 仅限于静态工厂方法

二 、延迟初始化模式

延迟初始化是延迟加载的一个特例。它指仅当第一次访问一个值或者对象的时候,才去初始化他们。

延迟初始化可以延迟或者避免一些比较复杂的运算。

在Java中,一般用null来代表未初始化状态,但假如null是一个合法的final值的时候,我们就需要一个独立的标记来指示初始化过程已经进行。

在多线程环境下,对以上提到的标记的访问必须要进行同步,并且会采用双重检测技术(double-check)来保证正确性,当然这也进一步增加了代码的复杂性。

  1. private volatile Component component;
  2. public Component getComponent() {
  3. Component result = component;
  4. if (result == null) {
  5. synchronized(this) {
  6. result = component;
  7. if (result == null) {
  8. component = result = new Component();
  9. }
  10. }
  11. }
  12. return result;
  13. }

Scala提供了一个内置的语法来定义延迟变量.

  1. lazy val x = {
  2. print("(computing x) ")
  3. 42
  4. }
  5. print("x = ")
  6. println(x)
  7. // x = (computing x) 42

在Scala中,延迟变量能够持有null值,并且是线程安全的。

优势:

  • 语法简洁

  • 延迟变量能够持有null值

  • 延迟变量的访问是线程安全的

劣势:

  • 对初始化行为缺乏控制

三、单例模式

单例模式限制了一个类只能初始化一个对象,并且会提供一个全局引用指向它。

在Java中,单例模式或许是最为被人熟知的一个模式了。这是java缺少某种语言特性的明显信号。

在java中有static关键字,静态方法不能被任何对象访问,并且静态成员类不能实现任何借口。所以静态方法和Java提出的一切皆对象背离了。静态成员也只是个花哨的名字,本质上只不过是传统意义上的子程序。

  1. public class Cat implements Runnable {
  2. private static final Cat instance = new Cat();
  3. private Cat() {}
  4. public void run() {
  5. // do nothing
  6. }
  7. public static Cat getInstance() {
  8. return instance;
  9. }
  10. }
  11. Cat.getInstance().run()

在Scala中完成单例简直巨简单无比

  1. object Cat extends Runnable {
  2. def run() {
  3. // do nothing
  4. }
  5. }
  6. Cat.run()

优势:

  • 含义明确

  • 语法简洁

  • 按需初始化

  • 线程安全

劣势:

  • 对初始化行为缺乏控制

四、适配器模式

适配器模式能讲不兼容的接口放在一起协同工作,适配器对集成已经存在的各个组件很有用。

在Java实现中,需要创建一个封装类,如下所示:

  1. public interface Log {
  2. void warning(String message);
  3. void error(String message);
  4. }
  5. public final class Logger {
  6. void log(Level level, String message) { /* ... */ }
  7. }
  8. public class LoggerToLogAdapter implements Log {
  9. private final Logger logger;
  10. public LoggerToLogAdapter(Logger logger) { this.logger = logger; }
  11. public void warning(String message) {
  12. logger.log(WARNING, message);
  13. }
  14. public void error(String message) {
  15. logger.log(ERROR, message);
  16. }
  17. }
  18. Log log = new LoggerToLogAdapter(new Logger());

在Scala中,我们可以用隐式类轻松搞定。(注意:2.10后加的特性)

  1. trait Log {
  2. def warning(message: String)
  3. def error(message: String)
  4. }
  5. final class Logger {
  6. def log(level: Level, message: String) { /* ... */ }
  7. }
  8. implicit class LoggerToLogAdapter(logger: Logger) extends Log {
  9. def warning(message: String) { logger.log(WARNING, message) }
  10. def error(message: String) { logger.log(ERROR, message) }
  11. }
  12. val log: Log = new Logger()

最后的表达式期望的得到一个Log实例,而却使用了Logger,这个时候Scala编译器会自动把log实例封装到适配器类中。

优势:

  • 含义清晰

  • 语法简洁

劣势:

  • 在没有IDE的支持下会显得晦涩

五、装饰模式

装饰模式被用来在不影响一个类其它实例的基础上扩展一些对象的功能。装饰者是对继承的一个灵活替代。

当需要有很多独立的方式来扩展功能时,装饰者模式是很有用的,这些扩展可以随意组合。

在Java中,需要新建一个装饰类,实现原来的接口,封装原来实现接口的类,不同的装饰者可以组合起来使用。一个处于中间层的装饰者一般会用来代理原接口中很多的方法。

  1. public interface OutputStream {
  2. void write(byte b);
  3. void write(byte[] b);
  4. }
  5. public class FileOutputStream implements OutputStream { /* ... */ }
  6. public abstract class OutputStreamDecorator implements OutputStream {
  7. protected final OutputStream delegate;
  8. protected OutputStreamDecorator(OutputStream delegate) {
  9. this.delegate = delegate;
  10. }
  11. public void write(byte b) { delegate.write(b); }
  12. public void write(byte[] b) { delegate.write(b); }
  13. }
  14. public class BufferedOutputStream extends OutputStreamDecorator {
  15. public BufferedOutputStream(OutputStream delegate) {
  16. super(delegate);
  17. }
  18. public void write(byte b) {
  19. // ...
  20. delegate.write(buffer)
  21. }
  22. }
  23. new BufferedOutputStream(new FileOutputStream("foo.txt"))

Scala提供了一种更直接的方式来重写接口中的方法,并且不用绑定到具体实现。下面看下如何来使用abstract override标识符。

  1. trait OutputStream {
  2. def write(b: Byte)
  3. def write(b: Array[Byte])
  4. }
  5. class FileOutputStream(path: String) extends OutputStream { /* ... */ }
  6. trait Buffering extends OutputStream {
  7. abstract override def write(b: Byte) {
  8. // ...
  9. super.write(buffer)
  10. }
  11. }
  12. new FileOutputStream("foo.txt") with Buffering // with Filtering, ...

这种代理是在编译时期静态建立的,不过通常来说只要我们能在创建对象时任何组合装饰器,就已经够用了。

与基于组合(指需要特定的装饰类来把原类封装进去)的实现方式不一样,Scala保持了对象的一致性,所以可以在装饰对象上放心使用equals。

优势:

  • 含义清晰

  • 语法简洁

  • 保持了对象一致性

  • 无需显式的代理

  • 无需中间层的装饰类

劣势:

  • 静态绑定

  • 没有构造器参数

六、值对象模式

值对象是一个很小的不可变对象,他们的相等性不基于identity,而是基于不同对象包含的字段是否相等。

值对象被广泛应用于表示数字、时间、颜色等等。在企业级应用中,它们经常被用作DTO(可以用来做进程间通信),由于不变性,值对象在多线程环境下使用起来非常方便。

在Java中,并没有特殊语法来支持值对象。所以我们必须显式定义一个构造器,getter方法及相关辅助方法。

  1. public class Point {
  2. private final int x, y;
  3. public Point(int x, int y) { this.x = x; this.y = y; }
  4. public int getX() { return x; }
  5. public int getY() { return y; }
  6. public boolean equals(Object o) {
  7. // ...
  8. return x == that.x && y == that.y;
  9. }
  10. public int hashCode() {
  11. return 31 * x + y;
  12. }
  13. public String toString() {
  14. return String.format("Point(%d, %d)", x, y);
  15. }
  16. }
  17. Point point = new Point(1, 2)

在Scala中,我们使用元组或者样例类来申明值对象。当不需要使用特定的类的时候,元组就足够了.

  1. val point = (1, 2) // new Tuple2(1, 2)

元组是一个预先定义好的不变集合,它能够持有若干个不同类型的元素。元组提供构造器,getter方法以及所有辅助方法。

我们也可以为Point类定义一个类型别名

  1. type Point = (Int, Int) // Tuple2[Int, Int]
  2. val point: Point = (1, 2)

当需要一个特定的类或者需要对数据元素名称有更明确的描述的时候,可以使用样例类;

  1. case class Point(x: Int, y: Int)
  2. val point = Point(1, 2)

样例类将构造器参数默认为属性。样例类是不可变的,与元组一样,它提供了所有所需的方法。因为样例类是合法的类,所以它也可以使用继承及定义成员。

值对象模式是函数式编程中一个非常常用的工具,Scala在语言级别对其提供了直接支持。

优势:

  • 语法简洁

  • 预定义元组类

  • 内置辅助方法

劣势:

无!

七、空值模式

空值模式定义了一个“啥都不干”的行为,这个模式比起空引用有一个优势,它不需要在使用前检查引用的合法性。

在java中,我们需要定义一个带空方法的子类来实现此模式。

  1. public interface Sound {
  2. void play();
  3. }
  4. public class Music implements Sound {
  5. public void play() { /* ... */ }
  6. }
  7. public class NullSound implements Sound {
  8. public void play() {}
  9. }
  10. public class SoundSource {
  11. public static Sound getSound() {
  12. return available ? music : new NullSound();
  13. }
  14. }
  15. SoundSource.getSound().play();

所以,由getSound获得Sound实例再调用play方法,不需要检查Sound实例是否为空。更进一步,我们可以使用单例模式来限制只生成唯一的空对象。        Scala也采用了类似的方法,但是它提供了一个Option类型,可以用来表示可有可无的值。

  1. trait Sound {
  2. def play()
  3. }
  4. class Music extends Sound {
  5. def play() { /* ... */ }
  6. }
  7. object SoundSource {
  8. def getSound: Option[Sound] =
  9. if (available) Some(music) else None
  10. }
  11. for (sound <- SoundSource.getSound) {
  12. sound.play()
  13. }

在此场景下,我们使用for推导来处理Option类型(高阶函数和模式匹配也能轻松搞定此事)。

优势:

  • 预定义类型

  • 明确的可选择性

  • 内置结构支持

劣势:

  • 比较冗长的用法

八、策略模式

策略模式定义了一组封装好的算法,让算法变化独立于用户调用。需要在运行时选择算法时,策略模式非常有用。

在java中,一般先要定义一个接口,然后新建几个类分别去实现这个接口。

  1. public interface Strategy {
  2. int compute(int a, int b);
  3. }
  4. public class Add implements Strategy {
  5. public int compute(int a, int b) { return a + b; }
  6. }
  7. public class Multiply implements Strategy {
  8. public int compute(int a, int b) { return a * b; }
  9. }
  10. public class Context  {
  11. private final Strategy strategy;
  12. public Context(Strategy strategy) { this.strategy = strategy; }
  13. public void use(int a, int b) { strategy.compute(a, b); }
  14. }
  15. new Context(new Multiply()).use(2, 3);

在Scala中,函数是头等公民,可以直接实现如下(不得不说实现起来很爽)。

  1. type Strategy = (Int, Int) => Int
  2. class Context(computer: Strategy) {
  3. def use(a: Int, b: Int)  { computer(a, b) }
  4. }
  5. val add: Strategy = _ + _
  6. val multiply: Strategy = _ * _
  7. new Context(multiply).use(2, 3)

假如策略包含很多方法的话,我们可以使用元组或者样例类把所有方法封装在一起。

优势:

  • 语法简洁

劣势:

  • 通用类型

九、命令模式

命令模式封装了需要在稍后调用方法的所有信息,这些信息包括拥有这些方法的对象和这些方法的参数值。

命令模式适用于延时方法调用,顺序化方法调用及方法调用时记录日志。(当然还有其它很多场景)

在Java中,需要把方法调用封装在对象中。

  1. public class PrintCommand implements Runnable {
  2. private final String s;
  3. PrintCommand(String s) { this.s = s; }
  4. public void run() {
  5. System.out.println(s);
  6. }
  7. }
  8. public class Invoker {
  9. private final List<Runnable> history = new ArrayList<>();
  10. void invoke(Runnable command) {
  11. command.run();
  12. history.add(command);
  13. }
  14. }
  15. Invoker invoker = new Invoker();
  16. invoker.invoke(new PrintCommand("foo"));
  17. invoker.invoke(new PrintCommand("bar"));

在Scala中,我们使用换名调用来实现延迟调用

  1. object Invoker {
  2. private var history: Seq[() => Unit] = Seq.empty
  3. def invoke(command: => Unit) { // by-name parameter
  4. command
  5. history :+= command _
  6. }
  7. }
  8. Invoker.invoke(println("foo"))
  9. Invoker.invoke {
  10. println("bar 1")
  11. println("bar 2")
  12. }

这就是我们怎样把任意的表达式或者代码块转换为一个函数对象。当调用invoke方法的时候才会调用println方法,然后以函数形式存在历史序列中。我们也可以直接定义函数,而不采用换名调用,但是那种方式太冗长了。

优势:

  • 语法简洁

劣势:

  • 通用类型

十、责任链模式

责任链模式解耦了发送方与接收方,使得有更多的对象有机会去处理这个请求,这个请求一直在这个链中流动直到有个对象处理了它。        责任链模式的一个典型实现是责任链中的所有的对象都会继承一个基类,并且可能会包含一个指向链中下一个处理对象的引用。每一个对象都有机会处理请求(或者中断请求),或者将请求推给下一个处理对象。责任链的顺序逻辑可以要么代理给对象处理,要么就封装在一个基类中。

  1. public abstract class EventHandler {
  2. private EventHandler next;
  3. void setNext(EventHandler handler) { next = handler; }
  4. public void handle(Event event) {
  5. if (canHandle(event)) doHandle(event);
  6. else if (next != null) next.handle(event);
  7. }
  8. abstract protected boolean canHandle(Event event);
  9. abstract protected void doHandle(Event event);
  10. }
  11. public class KeyboardHandler extends EventHandler { // MouseHandler...
  12. protected boolean canHandle(Event event) {
  13. return "keyboard".equals(event.getSource());
  14. }
  15. protected void doHandle(Event event) { /* ... */ }
  16. }
  17. KeyboardHandler handler = new KeyboardHandler();
  18. handler.setNext(new MouseHandler());

由于以上的实现有点类似于装饰者模式,所以我们在Scala中可以使用abstract override来解决这个问题。不过Scala提供了一种更加直接的方式,即基于偏函数。

偏函数简单来说就是某个函数只会针对它参数的可能值的自己进行处理。可以直接使用偏函数的isDefinedAt和apply方法来实现顺序逻辑,更好的方法是使用内置的orElse方法来实现请求的传递。

  1. case class Event(source: String)
  2. type EventHandler = PartialFunction[Event, Unit]
  3. val defaultHandler: EventHandler = PartialFunction(_ => ())
  4. val keyboardHandler: EventHandler = {
  5. case Event("keyboard") => /* ... */
  6. }
  7. def mouseHandler(delay: Int): EventHandler = {
  8. case Event("mouse") => /* ... */
  9. }
  10. keyboardHandler.orElse(mouseHandler(100)).orElse(defaultHandler)

注意我们必须使用defaultHandler来避免出现“undefined”事件的错误。

优势:

  • 语法简洁

  • 内置逻辑

劣质:

  • 通用类型

十一、依赖注入模式

依赖注入可以让我们避免硬编码依赖关系,并且允许在编译期或者运行时替换依赖关系。此模式是控制反转的一个特例(用过Spring的同学都对这个模式熟烂了吧)。

依赖注入是在某个组件的众多实现中选择,或者为了单元测试而去模拟组件。

除了使用IoC容器,在Java中最简单的实现就是像构造器参数需要的依赖。所以我们可以利用组合来表达依赖需求。

  1. public interface Repository {
  2. void save(User user);
  3. }
  4. public class DatabaseRepository implements Repository { /* ... */ }
  5. public class UserService {
  6. private final Repository repository;
  7. UserService(Repository repository) {
  8. this.repository = repository;
  9. }
  10. void create(User user) {
  11. // ...
  12. repository.save(user);
  13. }
  14. }
  15. new UserService(new DatabaseRepository());

除了组合(“HAS-A”)与继承(“HAS-A”)的关系外,Scala还增加一种新的关系:需要(“REQUIRES -A”), 通过自身类型注解来实现。(建议大家去熟悉一下自身类型的定义与使用)

Scala中可以混合使用自身类型与特质来进行依赖注入。

  1. trait Repository {
  2. def save(user: User)
  3. }
  4. trait DatabaseRepository extends Repository { /* ... */ }
  5. trait UserService { self: Repository => // requires Repository
  6. def create(user: User) {
  7. // ...
  8. save(user)
  9. }
  10. }
  11. new UserService with DatabaseRepository

不同于构造器注入,以上方式有个要求:配置中的每一种依赖都需要一个单独的引用,这种技术的完整实践就叫蛋糕模式。(当然,在Scala中,还有很多方式来实现依赖注入)。

在Scala中,既然特质的混入是静态的,所以此方法也仅限于编译时依赖注入。事实上,运行时的依赖注入几乎用不着,而对配置的静态检查相对于运行时检查有很大的优势。

优势:

  • 含义明确

  • 语法简洁

  • 静态检查

劣势:

  • 编译期配置

  • 形式上可能有点冗长

编后记:

通过以上的描述,我希望能将为Java与Scala两种语言建立一座桥梁。能让Java开发者对Scala语法有个大致的了解,并且让Scala开发者能让语言所拥有的一些特性对应到更高更通用的抽象中。

英文转自:https://pavelfatin.com/design-patterns-in-scala/

Scala设计模式的更多相关文章

  1. 8. Scala面向对象编程(高级部分)

    8.1 静态属性和静态方法 8.1.1 静态属性-提出问题 有一群小孩在玩堆雪人,不时有新的小孩加入,请问如何知道现在共有多少人在玩?请使用面向对象的思想,编写程序解决 8.1.2 基本介绍 -Sca ...

  2. 大数据技术之_16_Scala学习_06_面向对象编程-高级+隐式转换和隐式值

    第八章 面向对象编程-高级8.1 静态属性和静态方法8.1.1 静态属性-提出问题8.1.2 基本介绍8.1.3 伴生对象的快速入门8.1.4 伴生对象的小结8.1.5 最佳实践-使用伴生对象解决小孩 ...

  3. Scala 的确棒

    我的确认为计算机学院应该开一门 Scala 的语言课程. 在这篇文章中,我会讲述为什么我会有这样的想法,在此之前,有几点我想要先声明一下: 本文无意对编程语言进行评比,我要讲述的主体是为什么你应该学习 ...

  4. Scala HandBook

    目录[-] 1.   Scala有多cool 1.1.     速度! 1.2.     易用的数据结构 1.3.     OOP+FP 1.4.     动态+静态 1.5.     DSL 1.6 ...

  5. 学习Scala: 初学者应该了解的知识

    Scala开发参照清单 这里列出在开发一个Scala工程中需要参照的资料. 官网网站 http://www.scala-lang.org/ 文档网站 http://docs.scala-lang.or ...

  6. [翻译]The Neophyte's Guide to Scala Part 12: Type Classes

    The Neophyte's Guide to Scala Part 12: Type Classes 过去的两周我们讨论了一些使我们保持DRY和灵活性的函数式编程技术,特别是函数组合,partial ...

  7. Swift中的设计模式

    设计模式(Design Pattern)是 对软件设计中普遍存在的各种问题,所提出的解决方案.这个术语是由埃里希·伽玛等人(Erich Gamma,Richard Helm,Ralph Johnson ...

  8. Scala入门系列(八):面向对象之trait

    基础知识 1 将trait作为接口使用 此时Trait就与Java中的接口非常类似,不过注意,在Scala中无论继承还是trait,统一都是extends关键字. Scala跟Java 8前一样不支持 ...

  9. Scala编程入门---面向对象编程之Trait高级知识

    trait调用链 Scala中支持让类继承多个Trait后,依次调用多个Trait中的同一个方法,只要让多个trait的同一个方法中,在最后都执行super.方法即可 类中调用多个trait中都有这个 ...

随机推荐

  1. HTML(总结)

    HTML 浏览器内核有哪些 Trident:IE Gecko:Firefox Webkit:Chrome Safari Presto:Opera(投奔Webkit) html5的一些新特性 1. 拖拽 ...

  2. 5.两分钟让你明白app后端有啥用

    app后端,也称为app后台,称呼不一样,但指的是同一个东西. 我一直都以app后端有啥用这个问题不用解释.但在网络上,有准备进行app创业的网友(是从传统行业过来的)问过这个问题,我这里就以app后 ...

  3. 玩转Spring MVC(三)----spring基本配置文件

    这篇文章总结一下spring mvc的基本配置,首先贴一张我的项目的目录截图,有一些多余的文件,大家不必在意: 用到的一些jar包在这:<a>http://download.csdn.ne ...

  4. Python操作Redis之设置key的过期时间

    对于一个已经存在的key,我们可以设置其过期时间,到了那个时间后,当你再去访问时,key就不存在了 有两种方式可以设置过期时间,一种是指定key从当前时间开始算起还能存活多久,时间单位有两个,一个是秒 ...

  5. Reading Code Is Hard

    注: 以下内容引自: https://blogs.msdn.microsoft.com/ericlippert/2004/06/14/reading-code-is-hard/ Reading Cod ...

  6. netcore 获取本地网络IP地址

    .net framework 下面可以用下面的代码获取到本地网络ip地址.netcore下面这个代码也依然可以用 System.Net.Dns.GetHostName() System.Net.Dns ...

  7. Python数据结构应用3——链表

    linked list(链表) 建立 Node 链表的基本组成就是一个个Node,每个Node都需要包括两部分内容,一部分是自身的data,另一部分是下一个Node的reference. class ...

  8. 关于JVM的垃圾回收(GC) 这可能是你想了解的

    目录 1 JVM中Java对象的分类 2 JVM的GC类型及触发条件 2.1 Young GC 2.2 Full GC 3 Java对象生成时的内存申请过程 3 Oracle JDK中的垃圾收集器 3 ...

  9. 十问 JVM

    今天我们来讨论下 Java 虚拟机,通过一系列常见的问题来逐渐深入了解 JVM 创建对象过程,内存布局,类加载以及 GC 回收算法等机制. 十问 JVM 问题整理: Java虚拟机创建对象的过程 (使 ...

  10. len(x) 击败 x.len(),从内置函数看 Python 的设计思想

    内置函数是 Python 的一大特色,用极简的语法实现很多常用的操作. 它们预先定义在内置命名空间中,开箱即用,所见即所得.Python 被公认是一种新手友好型的语言,这种说法能够成立,内置函数在其中 ...