单例模式案例-日志类

70 阅读4分钟

1. apply方法的基本使用

Scala 中的 apply 方法是特殊方法

  • 可直接通过「对象名 (参数)」或「类名 (参数)」调用(无需显式写 .apply);
  • 常用于对象创建、参数转换、简化调用
  • 区别于 JavaScript:Scala 的 apply 不直接改变 this 指向(Scala 是静态类型语言,this 由上下文确定),核心作用是「简化实例化 / 方法调用」。

核心语法

  1. 伴生对象的 apply:用于创建类实例(替代 new 关键字);
  2. 类 / 特质的 apply:用于实例的函数式调用(类似方法重载)。

基本使用案例

scala
object ApplyBasicDemo extends App {
  // 1. 伴生对象的 apply:简化类实例化(无需 new)
  class Person(val name: String, val age: Int) {
    // 类的 apply:实例可直接像函数一样调用
    def apply(hobby: String): String = 
      s"我是 $name,年龄 $age,爱好 $hobby"
  }

  // 伴生对象(Scala 中与类同名的对象)
  object Person {
    // apply 方法:接收参数,返回类实例
    def apply(name: String, age: Int): Person = new Person(name, age)
  }

  // 直接通过 类名(参数) 调用 apply 创建实例(无需 new)
  val zhangsan = Person("张三", 20)
  println(zhangsan.name) // 输出:张三

  // 实例直接调用 apply(简化为 实例(参数))
  val intro = zhangsan("编程")
  println(intro) // 输出:我是 张三,年龄 20,爱好 编程

  // 2. 工具类的 apply:简化参数处理
  object MathUtils {
    // apply 实现求和(接收可变参数,模拟 JavaScript 数组传参)
    def apply(numbers: Int*): Int = numbers.sum
  }

  // 调用 apply 求和(直接传多个参数,或数组转可变参数)
  val sum1 = MathUtils(1, 2, 3)
  val sum2 = MathUtils.apply(Array(4,5,6): _*) // 数组转可变参数
  println(sum1) // 输出:6
  println(sum2) // 输出:15

  // 3. 集合中的 apply:Scala 内置集合大量使用 apply 创建实例
  val list = List(1,2,3) // 等价于 List.apply(1,2,3)
  val map = Map("name" -> "李四", "age" -> 25) // 等价于 Map.apply(...)
  println(list(0)) // 输出:1(调用 List 的 apply 按索引取值)
  println(map("name")) // 输出:李四(调用 Map 的 apply 按 key 取值)
}

2. apply实现单例模式

Scala 单例模式的核心是「伴生对象 + apply 方法」:

  • 伴生对象是天然单例(Scala 保证全局唯一);
  • 通过伴生对象的 apply 方法控制类实例的创建,确保仅生成一个实例。

实现思路

  1. 私有化类的构造器(private 修饰),禁止外部直接 new
  2. 伴生对象中维护唯一实例(闭包缓存);
  3. 伴生对象的 apply 方法:判断实例是否存在,不存在则创建,存在则直接返回。

代码实现

scala
object SingletonWithApply extends App {
  // 目标类:私有化构造器(private 修饰)
  class User private(val name: String, val age: Int) {
    override def toString: String = s"User($name, $age)"
  }

  // 伴生对象:实现单例逻辑
  object User {
    // 闭包缓存唯一实例(Option 类型避免空指针)
    private var instance: Option[User] = None

    // apply 方法:控制实例创建
    def apply(name: String, age: Int): User = {
      instance match {
        case Some(user) => user // 已存在,返回缓存实例
        case None => 
          val newUser = new User(name, age) // 首次创建
          instance = Some(newUser)
          newUser
      }
    }

    // 可选:提供获取实例的方法(非必须,apply 已简化调用)
    def getInstance: Option[User] = instance
  }

  // 测试:多次调用 apply,返回同一个实例
  val user1 = User("张三", 20)
  val user2 = User("李四", 25) // 后续参数无效,返回缓存的 user1

  println(user1 == user2) // 输出:true(同一实例)
  println(user1) // 输出:User(张三, 20)
  println(user2) // 输出:User(张三, 20)

  // 验证实例唯一性
  println(User.getInstance) // 输出:Some(User(张三, 20))
}

关键说明

  • 类构造器 private:确保外部无法通过 new User(...) 创建实例,只能通过伴生对象的 apply
  • Option[User]:Scala 中推荐用 Option 处理空值,避免 null 引发的空指针异常;
  • 伴生对象的 apply 是「工厂方法」,统一实例创建入口,控制单例逻辑。

3. 案例-日志类的基本实现

需求:实现单例日志类,支持日志级别(INFO/WARN/ERROR)、格式化输出、控制台打印,后续可对接 Writer 类写入文件。

实现思路

  1. 单例模式:通过伴生对象 apply 控制唯一实例;
  2. 日志级别:定义枚举类型(LogLevel),规范日志级别;
  3. 日志格式化:统一格式 [时间] [级别] 内容
  4. 核心方法:info/warn/error 对应不同级别日志。

完整代码

scala
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

// 日志级别枚举(Scala 3 枚举,Scala 2 可用 sealed trait + case object)
enum LogLevel:
  case INFO, WARN, ERROR

object LoggerDemo extends App {
  // 日志类:私有化构造器,单例模式
  class Logger private {
    // 时间格式化器(线程安全,复用)
    private val dateFormatter: DateTimeFormatter = 
      DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

    // 私有方法:格式化日志内容
    private def formatLog(level: LogLevel, message: String): String = {
      val time = LocalDateTime.now().format(dateFormatter)
      s"[$time] [${level.toString}] $message"
    }

    // 公共方法:不同级别日志
    def info(message: String): Unit = {
      val log = formatLog(LogLevel.INFO, message)
      println(log) // 控制台打印(后续可对接文件写入)
    }

    def warn(message: String): Unit = {
      val log = formatLog(LogLevel.WARN, message)
      println(log)
    }

    def error(message: String): Unit = {
      val log = formatLog(LogLevel.ERROR, message)
      println(log)
    }
  }

  // 伴生对象:单例工厂(apply 方法)
  object Logger {
    private var instance: Option[Logger] = None

    def apply(): Logger = instance match {
      case Some(log) => log
      case None => 
        val newLog = new Logger()
        instance = Some(newLog)
        newLog
    }
  }

  // 测试:获取单例日志实例并使用
  val logger = Logger()
  logger.info("应用启动成功")
  logger.warn("内存使用率过高(80%)")
  logger.error("数据库连接失败:Cannot connect to MySQL")

  // 验证单例
  val logger2 = Logger()
  println(s"是否单例:${logger == logger2}") // 输出:true
}

4. wirter类来实现文件写入功能

基于 Scala 标准库 java.io 和 scala.io,实现通用文件写入类,支持:

  • 同步写入、追加写入;
  • 自动创建目录;
  • 字符编码配置;
  • 兼容日志类调用。

设计思路

  1. 职责单一:仅处理文件写入逻辑(创建目录、写入内容、异常处理);
  2. 健壮性:自动创建文件所在目录,捕获 IO 异常;
  3. 易用性:提供简洁 API(write/append),支持字符串内容;
  4. 可复用:独立类,可对接日志类或单独使用。

完整代码

scala
import java.io.{BufferedWriter, File, FileWriter}
import java.nio.charset.StandardCharsets
import scala.util.control.NonFatal

object WriterDemo extends App {
  /**
   * 通用文件写入类
   * @param filePath 文件路径(绝对路径或相对路径)
   * @param encoding 字符编码(默认 UTF-8)
   */
  class Writer(
    private val filePath: String,
    private val encoding: String = StandardCharsets.UTF_8.name()
  ) {
    // 初始化:自动创建目录
    private val file = new File(filePath)
    createParentDirs()

    /**
     * 私有方法:创建文件所在的父目录(递归创建)
     */
    private def createParentDirs(): Unit = {
      val parentDir = file.getParentFile
      if (parentDir != null && !parentDir.exists()) {
        parentDir.mkdirs() // 递归创建多级目录
        println(s"[Writer] 目录已创建:${parentDir.getAbsolutePath}")
      }
    }

    /**
     * 同步写入文件(覆盖模式:清空原有内容)
     * @param content 要写入的内容
     * @return 写入成功返回 true,失败返回 false
     */
    def write(content: String): Boolean = {
      writeToFile(content, append = false)
    }

    /**
     * 同步追加写入文件(保留原有内容,新增内容追加到末尾)
     * @param content 要追加的内容
     * @return 写入成功返回 true,失败返回 false
     */
    def append(content: String): Boolean = {
      writeToFile(content, append = true)
    }

    /**
     * 私有核心方法:执行文件写入
     * @param content 内容
     * @param append 是否追加(true=追加,false=覆盖)
     */
    private def writeToFile(content: String, append: Boolean): Boolean = {
      var writer: BufferedWriter = null
      try {
        // 创建 FileWriter(append 参数控制写入模式)
        writer = new BufferedWriter(
          new FileWriter(file, encoding, append)
        )
        writer.write(content)
        writer.flush() // 强制刷新缓冲区
        println(s"[Writer] 写入成功:${file.getAbsolutePath}")
        true
      } catch {
        case NonFatal(e) => // 捕获非致命异常(避免程序崩溃)
          println(s"[Writer] 写入失败:${e.getMessage}")
          false
      } finally {
        if (writer != null) writer.close() // 关闭流,释放资源
      }
    }

    /**
     * 获取文件绝对路径
     */
    def getAbsolutePath: String = file.getAbsolutePath
  }

  // 伴生对象:简化 Writer 实例创建(apply 方法)
  object Writer {
    def apply(filePath: String, encoding: String = StandardCharsets.UTF_8.name()): Writer = 
      new Writer(filePath, encoding)
  }

  // ------------------------------
  // 测试 Writer 类
  // ------------------------------
  // 1. 创建 Writer 实例(目标文件:logs/app.log)
  val writer = Writer("logs/app.log")

  // 2. 测试追加写入(日志格式内容)
  val infoLog = "[2025-11-10 16:00:00] [INFO] 应用启动成功\n"
  val errorLog = "[2025-11-10 16:00:01] [ERROR] 数据库连接失败\n"
  writer.append(infoLog)
  writer.append(errorLog)

  // 3. 测试覆盖写入(替换原有内容)
  val newContent = "[2025-11-10 16:01:00] [INFO] 覆盖写入测试\n"
  writer.write(newContent)

  // 4. 查看文件路径
  println(s"文件路径:${writer.getAbsolutePath}")
}

关键特性

  1. 自动创建目录:若文件所在目录不存在(如 logs/app.log 中的 logs 目录),会自动递归创建;
  2. 异常安全:使用 try-catch-finally 捕获 IO 异常,确保流关闭,避免资源泄露;
  3. 编码支持:默认 UTF-8,可自定义编码(如 GBK);
  4. 写入模式write(覆盖)和 append(追加)分离,适配不同场景。

五、整合:日志类 + Writer 类(实现文件写入日志)

将前面的日志类与 Writer 类整合,让日志不仅打印到控制台,还能写入文件:

scala
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

enum LogLevel:
  case INFO, WARN, ERROR

object LoggerWithWriter extends App {
  // 1. 复用前面的 Writer 类(省略重复代码,直接使用)
  class Writer(/* 同前 */) { /* 同前 */ }
  object Writer { /* 同前 */ }

  // 2. 日志类:依赖 Writer 实现文件写入
  class Logger private(writer: Writer) {
    private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

    private def formatLog(level: LogLevel, message: String): String = {
      val time = LocalDateTime.now().format(dateFormatter)
      s"[$time] [${level.toString}] $message\n" // 换行,便于日志分行
    }

    // 日志方法:同时打印控制台 + 写入文件
    def info(message: String): Unit = {
      val log = formatLog(LogLevel.INFO, message)
      println(log.trim) // 控制台去换行(避免重复空行)
      writer.append(log) // 写入文件(追加模式)
    }

    def error(message: String): Unit = {
      val log = formatLog(LogLevel.ERROR, message)
      println(log.trim)
      writer.append(log)
    }
  }

  // 伴生对象:单例 + 初始化 Writer
  object Logger {
    private var instance: Option[Logger] = None

    // apply 方法:接收日志文件路径,初始化 Writer 和 Logger
    def apply(logFilePath: String = "logs/app.log"): Logger = instance match {
      case Some(log) => log
      case None => 
        val writer = Writer(logFilePath) // 初始化 Writer
        val newLog = new Logger(writer)
        instance = Some(newLog)
        newLog
    }
  }

  // 测试:日志同时打印到控制台和文件
  val logger = Logger()
  logger.info("应用启动成功(整合 Writer 类)")
  logger.error("数据库连接失败(写入文件)")

  // 验证文件写入结果:查看 logs/app.log 文件
}

总结

Scala 实现核心要点:

  1. apply 方法:简化实例创建(伴生对象)和方法调用(类实例),无「改变 this 指向」的概念(Scala 静态类型);
  2. 单例模式:通过「私有化类构造器 + 伴生对象 apply + 闭包缓存实例」实现,天然适配 Scala 语法;
  3. 日志类:枚举定义日志级别,格式化日志内容,单例模式确保全局唯一;
  4. Writer 类:基于 Java IO 封装,自动创建目录,支持覆盖 / 追加写入,异常安全,可独立复用。

所有代码均可直接在 Scala 3 环境中运行(Scala 2 需调整枚举定义为 sealed trait + case object),贴合 Scala 函数式 + 面向对象的混合编程风格。