Builder на Kotlin c контролем наборов значений при компиляции

от автора

Задача

Допустим, у нас есть 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, там так и сделано), но тогда возникает две проблемы:
1. Для записи видео требуется указать набор параметров, который становится необязательным, если мы записываем только аудио (и наоборот), и это надо как-то контролировать.
2. Один параметр может «тянуть» за собой другие. К примеру, если мы укажем 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()

Недостатки

В качестве недостатка можно указать большой объем кода, который потребуется написать вручную, и при добавлении параметров придется дописывать интерфейсы и их реализации.

Кроме того, если есть сложные комбинации параметров, то это всё будет сложно поддерживать.

Ссылки

Похожие идеи упоминаются:

  1. SO (англ)

  2. С интерфейсами, обобщениями, таблицами переходов (habr)


ссылка на оригинал статьи https://habr.com/ru/post/673066/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *