Одним из наиболее распространенных шаблонов, которые мы используем в повседневной работе, является преобразование объектов из одного типа объекта в другой. Причины этого различны; Одна из причин заключается в том, чтобы различать внешние и внутренние реализации, а другая причина состоит в том, чтобы обогатить входящие данные дополнительной информацией или отфильтровать некоторые аспекты данных перед их отправкой пользователю. Есть несколько подходов для достижения этого преобразования между объектами:
- Наивный подход
- Синдром жира живота
- Хвост Ящерицы
- Страусиная дорога
Добавьте код конвертера к объекту явно:
1
2
3
4
5
|
case class ClassA(s : String) case class ClassB(s : String) { def toClassA = ClassA(s) } |
Хотя это самая простая и очевидная реализация, она связывает ClassA и ClassB вместе, и это именно то, чего мы хотим избежать.
Когда мы хотим выполнить преобразование между объектами, лучшим способом является рефакторинг логики вне класса, что позволяет нам тестировать ее отдельно, но при этом использовать ее в нескольких классах. Типичная реализация будет выглядеть так:
1
2
3
|
class SomeClass(c 1 : SomeConverter, c 2 : AnotherConverter, ...., cn, YetAnotherConverter) { ........... } |
Сам преобразователь может быть реализован как простой класс, например:
1
2
3
4
5
6
7
8
|
enum CustomToStringConverter { INSTANCE; public ClassB convert(ClassA source) { return new ClassB(source.str); } } |
Этот метод вынуждает нас включать все необходимые конвертеры для каждого класса, который требует этих конвертеров. У некоторых разработчиков может возникнуть соблазн издеваться над этими конвертерами, которые будут тесно связывать их тест с конкретными конвертерами. например:
1
2
3
4
5
6
|
// set mock expectations converter 1 .convert(c 1 ) returns c 2 dao.listObj(c 2 ) returns List(c 3 ) converter 2 .convert(c 3 ) returns o 4 someClass.listObj(o 0 ) mustEqual o 4 |
Что мне не нравится в этих тестах, так это то, что весь код протекает через логику преобразования, и в конце вы сравниваете результат, возвращаемый некоторыми имитациями. Если, например, одно из фиктивных ожиданий преобразователей не совсем точно сравнивает входной объект, и программист не будет сопоставлять входной объект и будет использовать оператор any , визуализирующий тестовый спор.
Еще одним вариантом использования Scala является возможность наследовать несколько признаков и снабжать код конвертера признаками. Позволяет нам смешивать и сочетать эти преобразователи. Типичная реализация будет выглядеть так:
1
2
3
|
class SomeClass extends AnotherClass with SomeConverter with AnotherConverter..... with YetAnotherConverter { ............... } |
Использование этого подхода позволит нам подключить конвертеры к нескольким реализациям, устраняя при этом необходимость (или побуждение) высмеивать логику преобразования в наших тестах, но возникает вопрос проектирования — это способность конвертировать один объект в другой, связанный с цель класса? Это также побуждает разработчиков накапливать все больше и больше черт в классе и никогда не удалять из него старые неиспользуемые черты.
Scala позволяет нам скрыть проблему и использовать неявные преобразования. Такой подход позволяет нам на самом деле скрыть проблему. Реализация теперь будет выглядеть так:
1
2
3
4
|
implicit def converto 0 too 2 (o 0 : SomeObject) : AnotherObj = ... implicit def convert 01 to 02 (o 1 : AnotherObject) : YetAnotherObj = ... def listObj(o 0 : SomeObj) : YetAnotherObj = dao.doSomethingWith(entity = o 0 ) |
На самом деле этот код конвертирует o0 в o1, потому что это то, что нужно для listObj. Когда результат возвращает o1 и неявно конвертирует его в o2. Приведенный выше код многое скрывает от нас и оставляет нас озадаченными, если инструменты не показывают нам эти преобразования. Хороший вариант использования, в котором работают неявные преобразования, — это когда мы хотим выполнить преобразование между объектом, имеющим одинаковую функциональность и назначение. Хорошим примером для них является преобразование между списками Scala и списками Java, оба в основном одинаковы, и мы не хотим засорять наш код во всех местах, где мы конвертируем между этими двумя.
Чтобы подвести итог проблем, с которыми мы столкнулись:
- Длинный и неиспользуемый список нежелательных черт или классов мусора в конструкторе.
- Черты, которые не представляют истинную цель класса.
- Код, который скрывает свой истинный поток. Чтобы решить все эти проблемы, Scala создала хороший шаблон с использованием неявных классов.
Чтобы написать код преобразования, мы можем сделать что-то вроде этого:
1
2
3
4
5
6
7
8
9
|
object ObjectsConveters { implicit class Converto 0 To 1 (o 0 : SomeObject) { def asO 1 : AnotherObject = ..... } implicit class Converto 1 To 2 (o 0 : AnotherObject) { def asO 2 With(id : String) : YetAnotherObject = ..... } |
Теперь наш код будет выглядеть так:
1
2
3
|
import ObjectsConveters. _ def listObj(o 0 : SomeObj) : YetAnotherObj = listObj(o 0 .asO 1 ).asO 2 With(id = "someId" ) |
Такой подход позволяет нам быть неявным и явным одновременно. Из приведенного выше кода вы можете понять, что o0 конвертируется в o1, а результат снова конвертируется в o2. Если преобразование не используется, IDE оптимизирует импорт из нашего кода. Наши тесты не побудят нас смоделировать каждый конвертер, что приведет к спецификациям, которые объясняют правильное поведение потока кода в нашем классе. Обратите внимание, что код конвертера проверен в другом месте. Такой подход позволяет нам писать более читаемый тест в других местах кода. Например, в наших тестах e2e мы уменьшаем количество определяемых нами объектов:
1
2
3
|
"some API test" in { callSomeApi(someId, o 0 ) mustEqual o 0 .aso 2 With(id = "someId" ) } |
Этот код теперь более читабелен и имеет больше смысла; мы передаем некоторые входные данные, и результат соответствует тем же объектам, которые мы использовали в нашем вызове API.
Ссылка: | Явное неявное преобразование от нашего партнера по JCG Ноама Алмога из блога Wix IO . |