Scala: как избежать передачи одного и того же экземпляра объекта везде в коде - PullRequest
0 голосов
/ 27 января 2019

У меня сложный проект, который считывает конфигурации из БД через объект ConfigAccessor, который реализует два основных API: getConfig(name: String) и storeConfig(c: Config).

Из-за того, как проект в настоящее время разрабатывается, почтикаждый компонент должен использовать ConfigAccessor для связи с БД.Таким образом, поскольку этот компонент является объектом, его легко импортировать и вызывать его статические методы.

Теперь я пытаюсь построить некоторые модульные тесты для проекта, в котором конфигурации хранятся в hashMap в памяти.,Итак, прежде всего я отделил логику аксессора конфигурации от его хранилища (используя шаблон тортов).Таким образом, я могу определить свой собственный ConfigDbComponent при тестировании

class ConfigAccessor {
   this: ConfigDbComponent => 
   ...

. "Проблема" в том, что теперь ConfigAccessor является классом, что означает, что я должен создать его экземпляр в начале своего приложения ипередавайте это везде тому, кто в этом нуждается.Первый способ обойти этот экземпляр - использовать конструкторы других компонентов.Это станет довольно многословным (добавление параметра к каждому конструктору в проекте).

Что вы предлагаете мне сделать?Есть ли способ использовать какой-то шаблон проектирования, чтобы преодолеть это многословие, или какая-то внешняя библиотека насмешек была бы более подходящей для этого?

Ответы [ 2 ]

0 голосов
/ 28 января 2019

Вы можете сохранить глобальный ConfigAccessor и разрешить выбор средств доступа, таких как:

object ConfigAccessor {
  private lazy val accessor = GetConfigAccessor()

  def getConfig(name: String) = accessor.getConfig(name)
  ...
}

Для производственных сборок вы можете поместить логику в GetConfigAccessor, чтобы выбрать подходящий метод доступа на основе некоторых глобальных настроек, таких какtypesafe config.

Для модульного тестирования у вас может быть другая версия GetConfigAccessor для разных тестовых сборок, которая возвращает соответствующую реализацию теста.

Установка этого значения lazy позволяет вам контролироватьпорядок инициализации и, если необходимо, перед созданием компонентов выполните некоторые нефункциональные изменяемые операции в коде инициализации.


Обновите следующие комментарии

Производственный код будет иметь реализацию GetConfigAccessor примерно так:

object GetConfigAccessor {
  private val useAws = System.getProperties.getProperty("accessor.aws") == "true"

  def apply(): ConfigAccessor =
    if (useAws) {
      return new AwsConfigAccessor
    } else {
      return new PostgresConfigAccessor
  }
}

И AwsConfigAccessor, и PostgresConfigAccessor будут иметь свои собственные модульные тесты, чтобы доказать, что они соответствуют правильному поведению.Соответствующий метод доступа можно выбрать во время выполнения, установив соответствующее системное свойство.

Для модульного тестирования возможна более простая реализация GetConfigAccessor, примерно такая:

def GetConfigAccessor() = new MockConfigAccessor

Модульное тестированиевыполняется в рамках модульного тестирования, которое содержит ряд библиотек и фиктивных объектов, которые не являются частью производственного кода.Они построены отдельно и не включены в конечный продукт.Таким образом, эта версия GetConfigAccessor будет частью этого кода модульного тестирования и не будет частью конечного продукта.


С учетом всего этого я буду использовать эту модель только для чтения статические данные конфигурации, потому что это сохраняет работоспособность кода.ConfigAccessor - это просто удобный способ доступа к глобальным константам без их передачи в конструкторе.

Если вы также записываете данные, то это больше похоже на реальную БД, чем наконфигурации.В этом случае я бы создал собственные средства доступа для каждого компонента, которые предоставляют доступ к различным частям БД.Таким образом, становится ясно, какие части данных обновляются каждым компонентом.Эти средства доступа будут переданы компоненту и затем могут быть протестированы модульно с соответствующей имитационной реализацией как обычно.

Возможно, вам придется разделить ваши данные на static config и dynamicconfig и обрабатывать их отдельно.

0 голосов
/ 27 января 2019

Да, «правильный» путь - передать его в конструкторы.Вы можете уменьшить многословность, предоставив аргумент по умолчанию:

class Foo(config: ConfigAccessor = ConfigAccessor) { ... }

Существуют некоторые структуры "внедрения зависимостей", такие как guice или spring, построенные вокруг этого, но я не пойду туда, потому что я неfan.

Вы также можете продолжить использовать шаблон пирога:

 trait Configuration {
    def config: ConfigAccessor
 }

 trait Foo { self: Configuration => ... }

 class FooProd extends Foo with ProConfig
 class FooTest extends Foo with TestConfig

В качестве альтернативы используйте «статический установщик».Он сводит к минимуму изменения в существующем коде, но требует изменяемого состояния, которое на самом деле осуждается в scala:

object Config extends ConfigAccessor {
  @volatile private var accessor: ConfigAccessor = _

  def configurate(cfg: ConfigAccessor) = synchronized {
    val old = accessor
    accessor = cfg
    old
  }
  def getConfig(c: String) = Option(accessor).fold(
   throw new IllegalStateException("Not configurated!")
  )(_.getConfig(c))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...