Задача
Допустим, у нас есть MediaRecorder. Он должен уметь записывать видео, аудио, или и то, и другое. При этом, параметры для видео- и для аудиозаписи, конечно же, отличаются. Описание MediaRecorder выглядит как-то так:
data class MediaRecorder(
// video
var videoSource: VideoSource,
var resolution: Resolution,
var path: String,
// audio
var audioSource: AudioSource,
var audioEncoder: AudioEncoder, // указывается, если указан AudioSource
var noiseReduction: Boolean = false,
var noiseReductionLevel: Int = 0,
// общее
var outputFormat: OutputFormat
)
Очевидно, что если параметров становится много, то создание экземпляра такого класса становится неудобным.
Конечно, для решения этой проблемы, можно просто использовать обычный Builder (см. MediaRecorder в Android, там так и сделано), но тогда возникает две проблемы:
Для записи видео требуется указать набор параметров, который становится необязательным, если мы записываем только аудио (и наоборот), и это надо как-то контролировать.
Один параметр может "тянуть" за собой другие. К примеру, если мы укажем noiseReduction = true, то нам следует указать и noiseReductionLevel, либо ни то, ни другое.
При реализации "стандартного" Builder такие проверки можно написать, но выполнятся они уже будут при сборке объекта, то есть при выполнении, а хотелось бы, чтобы все параметры проверялись при компиляции.
Так и напишем такой сборщик.
Пишем свой Builder
Для реализации модифицируем MediaRecorder следующим образом:
Добавляем интерфейс для каждого параметра
lateinit var videoSource: VideoSource
private set
interface VideoSourceBuilder {
fun videoSource(value: VideoSource): ResolutionBuilder
}
Здесь при указании videoSource будет возвращаться не общий сборщик, как в классическом варианте, а "установщик следующего параметра". То есть как-то так:
lateinit var videoSource: VideoSource
private set
interface VideoSourceBuilder {
fun videoSource(value: VideoSource): ResolutionBuilder
}
lateinit var resolution: Resolution
private set
interface ResolutionBuilder { // указывается, если указан VideoSourceBuilder
fun resolution(resolution: Resolution): PathBuilder
}
lateinit var path: String
private set
interface PathBuilder {
fun path(path: String): OutputFormatBuilder
}
interface OutputFormatBuilder {
fun outputFormat(outputFormat: OutputFormat): MediaRecorderFinalBuilder
}
interface MediaRecorderFinalBuilder {
fun build(): MediaRecorder
}
Для того, чтобы можно было вызвать build, videoSource или audioSource, создадим еще общий интерфейс для сборщика, который уже будет возвращать собранный объект:
// VideoSourceBuilder, AudioSourceBuilder: сборку можно будет начать
// с одного из этих параметров
interface Builder : VideoSourceBuilder, AudioSourceBuilder
interface MediaRecorderFinalBuilder : Builder {
fun build(): MediaRecorder
}
Для "разветвления" нашей цепочки сборки, добавим еще интерфейс для "точки разветвления" такого вида:
// Либо указываем path, либо noiseReduction
interface Combine : PathBuilder, NoiseReductionBuilder
Теперь надо реализовать каждый интерфейс. Приведу сразу весь код целиком:
Релазиция MediaRecorder
package builder.sample
class MediaRecorder private constructor() {
companion object {
fun builder() = BuilderImpl()
class BuilderImpl : Builder {
private val mediaRecorder = MediaRecorder()
// Последний шаг сборки
// Или вызываем build(), или переходим к VideoSourceBuilder, AudioSourceBuilder
private inner class MediaRecorderFinalBuilderImpl : Builder by this@BuilderImpl, MediaRecorderFinalBuilder {
override fun build() = mediaRecorder
}
/**
* Реализуем ResolutionBuilder: устанавливаем resolution и возвращаем PathBuilder
*/
private inner class ResolutionBuilderimpl : ResolutionBuilder {
override fun resolution(resolution: Resolution): PathBuilder {
mediaRecorder.resolution = resolution
return PathBuilderImpl()
}
}
private inner class PathBuilderImpl : PathBuilder {
override fun path(path: String): OutputFormatBuilder {
mediaRecorder.path = path
return OutputFormatBuilderImpl()
}
}
private inner class OutputFormatBuilderImpl : OutputFormatBuilder {
override fun outputFormat(outputFormat: OutputFormat): MediaRecorderFinalBuilder {
mediaRecorder.outputFormat = outputFormat
return MediaRecorderFinalBuilderImpl()
}
}
private inner class NoiseReductionBuilderImpl : NoiseReductionBuilder {
override fun noiseReduction(): NoiseReductionLevelBuilder {
mediaRecorder.noiseReduction = true
return object : NoiseReductionLevelBuilder {
override fun noiseReductionLevel(noiseReductionLevel: Int): PathBuilder {
mediaRecorder.noiseReductionLevel = noiseReductionLevel
return PathBuilderImpl()
}
}
}
}
/**
* Точка ответвления: просто реализуем два интерфейса используя отдельные раилизации интерфейсов
*/
private inner class CombineImpl(pathBuilder: PathBuilderImpl = PathBuilderImpl(), noiseReductionBuilder: NoiseReductionBuilderImpl = NoiseReductionBuilderImpl())
: PathBuilder by pathBuilder, NoiseReductionBuilder by noiseReductionBuilder, Combine
private inner class AudioEncoderBuilderImpl : AudioEncoderBuilder<Combine> {
override fun audioEncoder(audioEncoder: AudioEncoder): Combine {
mediaRecorder.audioEncoder = audioEncoder
return CombineImpl()
}
}
override fun videoSource(value: VideoSource): ResolutionBuilder {
mediaRecorder.videoSource = value
return ResolutionBuilderimpl()
}
override fun audioSource(audioSource: AudioSource): AudioEncoderBuilder<Combine> {
mediaRecorder.audioSource = audioSource
return AudioEncoderBuilderImpl()
}
}
}
// video
lateinit var videoSource: VideoSource
private set
interface VideoSourceBuilder {
fun videoSource(value: VideoSource): ResolutionBuilder
}
lateinit var resolution: Resolution
private set
interface ResolutionBuilder { // указывается, если указан VideoSourceBuilder
fun resolution(resolution: Resolution): PathBuilder
}
lateinit var path: String
private set
interface PathBuilder {
fun path(path: String): OutputFormatBuilder
}
// audio
lateinit var audioSource: AudioSource
private set
interface AudioSourceBuilder {
fun audioSource(audioSource: AudioSource): AudioEncoderBuilder<Combine>
}
lateinit var audioEncoder: AudioEncoder // указывается, если указан AudioSource
private set
interface AudioEncoderBuilder<T> where T : PathBuilder, T : NoiseReductionBuilder {
fun audioEncoder(audioEncoder: AudioEncoder): T
}
var noiseReduction: Boolean = false
private set
interface NoiseReductionBuilder {
fun noiseReduction(): NoiseReductionLevelBuilder
}
var noiseReductionLevel: Int = 0
private set
interface NoiseReductionLevelBuilder {
fun noiseReductionLevel(noiseReductionLevel: Int): PathBuilder
}
// общее
lateinit var outputFormat: OutputFormat
private set
interface OutputFormatBuilder {
fun outputFormat(outputFormat: OutputFormat): MediaRecorderFinalBuilder
}
// Точки разветвления
interface Combine : PathBuilder, NoiseReductionBuilder
interface Builder : VideoSourceBuilder, AudioSourceBuilder
interface MediaRecorderFinalBuilder : Builder {
fun build(): MediaRecorder
}
}
enum class Resolution {
MIN, MAX
}
enum class VideoSource {
CAMERA, SCREEN
}
enum class AudioSource {
MIC, RADIO, SIGNAL_FROM_SPACE
}
enum class AudioEncoder {
AAC, OPUS
}
enum class OutputFormat {
AAC, THREE_GPP, MP4
}
Примеры
Теперь можно собирать наш MediaRecorder
val onlyVideo = MediaRecorder
.builder()
.videoSource(VideoSource.CAMERA)
.resolution(Resolution.MAX)
.path("PATH")
.outputFormat(OutputFormat.MP4)
.build()
Важно, что когда мы вызвали метод videoSource(...), мы просто не можем дальше написать ничего, кроме resolution(...) и далее по цепочке до последнего шага, на котором мы можем вызвать или build(), или audioSource(...) (и перейти на другую цепочку).
В данной реализации мы, конечно, можем вызвать на последнем шаге videoSource(...) снова и опять пройти по цепочке конфигурации видеозаписи (но это бессмысленно).
val onlyAudio = MediaRecorder
.builder()
.audioSource(AudioSource.MIC)
.audioEncoder(AudioEncoder.AAC)
.path("PATH")
.outputFormat(OutputFormat.AAC)
.build()
Комбинация двух цепочек:
val videoAndAudio = MediaRecorder
.builder()
.videoSource(VideoSource.CAMERA)
.resolution(Resolution.MAX)
.path("PATH")
.outputFormat(OutputFormat.MP4)
.audioSource(AudioSource.MIC)
.audioEncoder(AudioEncoder.AAC)
.path("PATH")
.outputFormat(OutputFormat.AAC)
.build()
Указать параметр, который требует указания дополнительного (noiseReduction и noiseReductionLevel):
val onlyAudioWithNoiseReduction = MediaRecorder
.builder()
.audioSource(AudioSource.MIC)
.audioEncoder(AudioEncoder.AAC)
.noiseReduction()
.noiseReductionLevel(10)
.path("PATH")
.outputFormat(OutputFormat.AAC)
.build()
При этом, нельзя написать такое:
MediaRecorder() // нет
MediaRecorder.builder().build() // нет
MediaRecorder
.builder()
.videoSource(VideoSource.CAMERA)
.build() // нет, не хватает resolution, path, outputFormat
MediaRecorder
.builder()
.audioSource(AudioSource.MIC)
.audioEncoder(AudioEncoder.AAC)
.noiseReduction()
.path("PATH") // нет, если указали noiseReduction, то где noiseReductionLevel?
.outputFormat(OutputFormat.AAC)
.build()
Недостатки
В качестве недостатка можно указать большой объем кода, который потребуется написать вручную, и при добавлении параметров придется дописывать интерфейсы и их реализации.
Кроме того, если есть сложные комбинации параметров, то это всё будет сложно поддерживать.
Ссылки
Похожие идеи упоминаются:
SO (англ)