Задача
Допустим, у нас есть 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 (англ)
