`
chenchao051
  • 浏览: 137547 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Scala设计模式

阅读更多

Scala设计模式

       

       我的话: 在国外网站上看到一篇文章,里面详细描述了很多设计模式,并且用Java及Scala两种语言描述,清晰的让我们看到各种常规的设计模式,在Scala中是如何在语言特性层面直接支持的。基于文章很nice,我利用今天的空闲时间将其翻译,希望大家能一起学习,讨论。翻译比较倡促,也就两小时左右,有何不当,请在下面留言指出。多谢!

ps:翻译工作我是直接在google docs完成的,结果到iteye这个巨无语的编辑器中,有些格式显示不对劲,大家将就着看吧,抱歉了。

 

------------------------------------------------------------------------------------------------------------------------------------

本文展示了一些经典的软件设计模式在Scala中的实现。

       所谓设计模式,就是针对在软件设计过程中出现的一些共性问题,从而产生的一种可重用的解决方案。设计模式不是已完成的代码,而更像是一个可以在不同场景下解决问题的通用模板。

       模式是由一些设计的最佳实践组成的,可以帮助我们避免一些问题,并且能增加代码的可读性,及加快开发进度。

       经典的设计模式(一般指GoF)都是基于面向对象的。他们展示了类与对象间的关系和行为。这些模式并不能很好的应用到纯函数式编程语言上,但是既然Scala是一种结合了面向对象编程和函数式编程的语言,那Scala还是能够采用这些模式的,甚至是在函数式风格的Scala代码中。

       很多时候设计模式被认为是某种语言缺乏一些特性的信号。在此种情况下,当一种语言提供了相关特性以后,这些模式可以被简化或者索性消除。得益于Scala富有表现力的语法,很多经典设计模式都可以直接实现。

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

 

  • 创建型设计模式

            1、工厂方法模式

            2、延迟加载模式

            3、单例模式

  • 结构型模式

            1、适配器模式

            2、装饰模式

  • 行为型

            1、值对象模式

            2、空值模式

            3、策略模式

            4、命令模式

            5、责任链模式

            6、依赖注入模式

 

一、工厂方法模式

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

       工厂方法允许:

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

       2、选择需要初始化的类

       3、缓存对象

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

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

 

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

public interface Animal {}

private class Dog implements Animal {}

private class Cat implements Animal {}

public class AnimalFactory {
    public static Animal createAnimal(String kind) {
        if ("cat".equals(kind)) return new Cat();
        if ("dog".equals(kind)) return new Dog();
        throw new IllegalArgumentException();
    }
}

AnimalFactory.createAnimal("dog");

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

trait Animal
private class Dog extends Animal
private class Cat extends Animal

object Animal {
  def apply(kind: String) = kind match {
    case "dog" => new Dog()
    case "cat" => new Cat()
  }
}

Animal("dog")

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

 

优势:                                           

  • 重用基类名字

  • 标准并且简洁

  • 类似于构造器调用

劣势:

  • 仅限于静态工厂方法

 

二 、延迟初始化模式

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

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

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

 

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

 

private volatile Component component;

public Component getComponent() {
    Component result = component;
    if (result == null) {
        synchronized(this) {
            result = component;
            if (result == null) {
                component = result = new Component();
            }
        }
    }
    return result;
}

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

lazy val x = {
  print("(computing x) ")
  42
}

print("x = ") 
println(x) 

// x = (computing x) 42

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

 

优势:

  • 语法简洁

  • 延迟变量能够持有null值

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

劣势:

  • 对初始化行为缺乏控制

三、单例模式

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

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

 

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

public class Cat implements Runnable {
    private static final Cat instance = new Cat();
 
    private Cat() {}
 
    public void run() {
        // do nothing
    }

    public static Cat getInstance() {
        return instance;
    }
}

Cat.getInstance().run()

 

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

 

object Cat extends Runnable {
  def run() {
    // do nothing
  }
}

Cat.run()

优势:

  • 含义明确

  • 语法简洁

  • 按需初始化

  • 线程安全

劣势:

  • 对初始化行为缺乏控制

 

四、适配器模式

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

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

public interface Log {
    void warning(String message);
    void error(String message);
}

public final class Logger {
    void log(Level level, String message) { /* ... */ }
}

public class LoggerToLogAdapter implements Log {
    private final Logger logger;

    public LoggerToLogAdapter(Logger logger) { this.logger = logger; }

    public void warning(String message) {
        logger.log(WARNING, message);
    }
    
    public void error(String message) {
        logger.log(ERROR, message);
    }
}

Log log = new LoggerToLogAdapter(new Logger());

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

trait Log {
  def warning(message: String)
  def error(message: String)
}

final class Logger {
  def log(level: Level, message: String) { /* ... */ }
}

implicit class LoggerToLogAdapter(logger: Logger) extends Log {
  def warning(message: String) { logger.log(WARNING, message) }
  def error(message: String) { logger.log(ERROR, message) }
}

val log: Log = new Logger() 

 

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

 

优势:

  • 含义清晰

  • 语法简洁

劣势:

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

 

五、装饰模式

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

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

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

public interface OutputStream {
    void write(byte b);
    void write(byte[] b);
}

public class FileOutputStream implements OutputStream { /* ... */ }

public abstract class OutputStreamDecorator implements OutputStream {
    protected final OutputStream delegate;

    protected OutputStreamDecorator(OutputStream delegate) {
        this.delegate = delegate;
    }

    public void write(byte b) { delegate.write(b); }
    public void write(byte[] b) { delegate.write(b); }
}

public class BufferedOutputStream extends OutputStreamDecorator {
    public BufferedOutputStream(OutputStream delegate) {
        super(delegate);
    }

    public void write(byte b) {
        // ...
        delegate.write(buffer)
    }
}

new BufferedOutputStream(new FileOutputStream("foo.txt"))

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

trait OutputStream {
  def write(b: Byte)
  def write(b: Array[Byte])
}

class FileOutputStream(path: String) extends OutputStream { /* ... */ }

trait Buffering extends OutputStream {
  abstract override def write(b: Byte) {
    // ...
    super.write(buffer)
  }
}

new FileOutputStream("foo.txt") with Buffering // with Filtering, ...

 

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

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

优势:

  • 含义清晰

  • 语法简洁

  • 保持了对象一致性

  • 无需显式的代理

  • 无需中间层的装饰类

劣势:

  • 静态绑定

  • 没有构造器参数

 

六、值对象模式

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

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

 

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

public class Point {
    private final int x, y;

    public Point(int x, int y) { this.x = x; this.y = y; }

    public int getX() { return x; }

    public int getY() { return y; }

    public boolean equals(Object o) {
        // ...
        return x == that.x && y == that.y;
    }

    public int hashCode() {
        return 31 * x + y;
    }

    public String toString() {
        return String.format("Point(%d, %d)", x, y);
    }
}

Point point = new Point(1, 2)

 

 

 

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

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

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

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

type Point = (Int, Int) // Tuple2[Int, Int]

val point: Point = (1, 2)

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

 

case class Point(x: Int, y: Int)

val point = Point(1, 2)

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

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

优势:

  • 语法简洁

  • 预定义元组类

  • 内置辅助方法

劣势:

       无!



七、空值模式

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

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

public interface Sound {
    void play();
}

public class Music implements Sound {
    public void play() { /* ... */ }
}

public class NullSound implements Sound {
    public void play() {}
}

public class SoundSource {
    public static Sound getSound() {
    	return available ? music : new NullSound();
    }
}

SoundSource.getSound().play();

 

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

trait Sound {
  def play()
} 
  
class Music extends Sound {
    def play() { /* ... */ }
}

object SoundSource {
  def getSound: Option[Sound] = 
    if (available) Some(music) else None
}
  
for (sound <- SoundSource.getSound) {
  sound.play()
}

 

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

 

优势:

  • 预定义类型

  • 明确的可选择性

  • 内置结构支持

劣势:

 

  • 比较冗长的用法

八、策略模式

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

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

public interface Strategy {
    int compute(int a, int b);
}

public class Add implements Strategy {
    public int compute(int a, int b) { return a + b; }
}

public class Multiply implements Strategy {
    public int compute(int a, int b) { return a * b; }
}

public class Context  {
    private final Strategy strategy;

    public Context(Strategy strategy) { this.strategy = strategy; }

    public void use(int a, int b) { strategy.compute(a, b); }
}

new Context(new Multiply()).use(2, 3);

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

 

type Strategy = (Int, Int) => Int 

class Context(computer: Strategy) {
  def use(a: Int, b: Int)  { computer(a, b) }
}

val add: Strategy = _ + _
val multiply: Strategy = _ * _

new Context(multiply).use(2, 3)

 

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

 

优势:

  • 语法简洁

劣势:

  • 通用类型

 

九、命令模式

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

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

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

public class PrintCommand implements Runnable {
    private final String s;

    PrintCommand(String s) { this.s = s; }

    public void run() {
        System.out.println(s);
    }
}

public class Invoker {
    private final List<Runnable> history = new ArrayList<>();

    void invoke(Runnable command) {
        command.run();
        history.add(command);
    }
}

Invoker invoker = new Invoker();
invoker.invoke(new PrintCommand("foo"));
invoker.invoke(new PrintCommand("bar"));

 

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

object Invoker {
  private var history: Seq[() => Unit] = Seq.empty

  def invoke(command: => Unit) { // by-name parameter
    command
    history :+= command _
  }
}

Invoker.invoke(println("foo"))
  
Invoker.invoke {
  println("bar 1")
  println("bar 2")
}

 

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

 

优势:

  • 语法简洁

劣势:

  • 通用类型

 

十、责任链模式

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

public abstract class EventHandler {
    private EventHandler next;

    void setNext(EventHandler handler) { next = handler; }

    public void handle(Event event) {
        if (canHandle(event)) doHandle(event);
        else if (next != null) next.handle(event);
    }

    abstract protected boolean canHandle(Event event);
    abstract protected void doHandle(Event event);
}

public class KeyboardHandler extends EventHandler { // MouseHandler...
    protected boolean canHandle(Event event) {
        return "keyboard".equals(event.getSource());
    }

    protected void doHandle(Event event) { /* ... */ }
}

KeyboardHandler handler = new KeyboardHandler();
handler.setNext(new MouseHandler());

 

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

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

case class Event(source: String)

type EventHandler = PartialFunction[Event, Unit]

val defaultHandler: EventHandler = PartialFunction(_ => ())

val keyboardHandler: EventHandler = {
  case Event("keyboard") => /* ... */
}

def mouseHandler(delay: Int): EventHandler = {
  case Event("mouse") => /* ... */
}

keyboardHandler.orElse(mouseHandler(100)).orElse(defaultHandler)

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

 

优势:

  • 语法简洁

  • 内置逻辑

劣质:

  • 通用类型

 

十一、依赖注入模式

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

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

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

public interface Repository {
    void save(User user);
}

public class DatabaseRepository implements Repository { /* ... */ }

public class UserService {
    private final Repository repository;

    UserService(Repository repository) {
        this.repository = repository;
    }

    void create(User user) {
        // ...
        repository.save(user);
    }
}

new UserService(new DatabaseRepository());

   

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

 

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

trait Repository {
  def save(user: User)
}

trait DatabaseRepository extends Repository { /* ... */ }

trait UserService { self: Repository => // requires Repository
  def create(user: User) {
    // ...
    save(user)
  }
}

new UserService with DatabaseRepository

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

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

优势:

  • 含义明确

  • 语法简洁

  • 静态检查

劣势:

  • 编译期配置

  • 形式上可能有点冗长

 

编后记:

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

 

 

原文地址:http://pavelfatin.com/design-patterns-in-scala/

 

 

2
2
分享到:
评论
2 楼 SINCE1978 2015-11-01  
还没细看,似乎取材自一本书《scala与clojure设计模式》,中文版国内好像刚上市。
1 楼 dogstar 2013-10-23  
共享google doc啊

相关推荐

    scala设计模式

    Scala设计模式是软件开发领域内一项重要的议题。设计模式作为面向对象设计的通用解决方案,其核心目的是为了解决在软件设计和开发中反复出现的问题。设计模式不是直接可用的代码,而是提供了一种方式,指导我们如何...

    Scala和设计模式.pdf

    ### Scala与设计模式 #### 一、概述 随着软件工程的发展,设计模式逐渐成为软件开发中的重要组成部分。《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software...

    scala pattern design 设计模式

    ### Scala设计模式详解 #### 一、概述 在软件开发领域,设计模式是解决特定问题的一种通用可重用方案。本书《Scala设计模式》由John Hunt撰写,旨在为读者提供一系列实用的设计模式,并通过Scala语言进行实现。...

    基于Scala与Java的高效设计模式源码

    该项目是关于Scala与Java的高效设计模式源码,主要使用Scala语言,同时包含Java代码。项目文件总计52个,具体包括28个Java源文件、11个...该项目是上海Scala爱好者聚会的主题,旨在探讨和展示Scala设计模式的应用。

    scala design patterns源书代码

    Scala设计模式:源码解析 Scala设计模式是面向对象编程和函数式编程融合的语言中的一种重要概念,它提供了一种组织代码、解决常见问题的模板。本书“Scala Design Patterns”结合了理论与实践,通过一系列的示例...

    Scala Design Patterns 无水印pdf

    Scala Design Patterns 英文无水印pdf pdf所有页面使用FoxitReader和PDF-XChangeViewer测试都可以打开 本资源转载自网络,如有侵权,请联系上传者或csdn删除 本资源转载自网络,如有侵权,请联系上传者或csdn...

    scala 相关书籍

    1. **《Scala设计模式》** 这本书深入探讨了如何在Scala中实现和应用设计模式。它不仅讲解了传统的Java设计模式,还介绍了Scala特有的模式,如类型类、隐式转换和特质。读者将学习如何利用Scala的高级特性来构建可...

    scala学习资料(带书签)

    8. **设计模式应用**:Scala中可以实现多种设计模式,如工厂模式、单例模式、建造者模式等。学习如何在Scala中适当地应用这些模式,能提高代码的可读性和可维护性。 9. **类型系统与模式**:Scala的类型系统支持高级...

    Scala 程序设计 中+英文

    "Scala程序设计第二版高清PDF"是学习这一强大语言的重要资源,无论你是初学者还是有经验的开发者,都可以从中获益。 首先,Scala的设计目标是提高开发者的生产力,它简化了Java的复杂性,同时引入了许多现代编程...

    Scala高级语言设计

    Scala语言特点包括其简洁的语法,强大的类型推断,以及模式匹配能力,这些都是函数式编程的重要组成部分。随着大数据处理和分布式计算的流行,Scala的应用趋势在迅速增长,特别是在Apache Spark这样的大数据处理框架...

    图灵书籍(Scala程序设计(第2版).pdf+Scala程序设计-JAVA虚拟机多核编程实战.pdf)

    Scala程序设计(第2版)很可能详尽地介绍了Scala的基本语法、类与对象、模式匹配、高阶函数、类型系统、并发编程等核心概念,以及可能包含了一些新的语言特性或最佳实践。 **JAVA虚拟机多核编程实战** 这本书则关注...

    《scala语言规范.pdf 》中文、英文版 《scala程序设计》pdf、epub格式 低分打包下载

    - Scala语言规范是Scala编程的基础,它定义了语言的所有语法和语义,包括类型系统、类和对象、函数和方法、模式匹配、并发等核心概念。 - 《Scala语言规范.pdf》是这个规范的中文译版,对于中文使用者来说,这是一...

    scala-patterns::trophy:Scala4You设计模式

    Scala设计模式 :musical_keyboard: 目录 :index_pointing_up: 概括 Scala设计模式旨在为自适应应用程序原型提供各种用例场景和体系结构概念。 mkdocs.yml # Configuration file. docs/ readme.md # General ...

    Scala程序设计第二版

    4. **模式匹配**:Scala的模式匹配功能强大,不仅可以用于解构数据结构,还能用于条件判断和异常处理。书中会有专门章节探讨这一特性。 5. **类型系统**:Scala拥有丰富的类型系统,包括类型推断、类型类、隐式转换...

    Scala-Design-Patterns-Second-Edition:Packt发行的《 Scala Design Patterns,第二版》

    Scala设计模式-第二版这是发布的的代码存储库。 它包含从头到尾完成本书所必需的所有支持项目文件。关于这本书设计模式通过帮助开发人员编写易于维护,有效运行并对公司或相关人员有价值的出色软件,使开发人员的...

    scala-2.11.8.rar

    你可以通过阅读源码来学习设计模式、编程技巧和最佳实践,同时也可以跟踪代码的执行流程,提高调试能力。此外,源码还可以帮助你理解和适应Scala的最新发展,以便在实际项目中更好地利用这个语言。 总结: "scala-...

Global site tag (gtag.js) - Google Analytics