Одна из основных целей программирования, поддерживаемого такими библиотеками, как Cats, - сделать недопустимые состояния недопустимыми. В идеальном мире, в соответствии с этой философией, было бы невозможно создать экземпляр Config
с недопустимыми данными о членах (используя библиотеку, подобную уточненную , где сложные ограничения могут быть выражены в и отслеживается системой типов или просто скрывает небезопасные конструкторы). В чуть менее совершенном мире все еще возможно создать недействительные экземпляры Config
, но не рекомендуется, например, с помощью безопасных конструкторов (например, ваш метод validatePerson
для Person
).
Похоже, что вы находитесь в еще менее совершенном мире, где у вас есть экземпляры Config
, которые могут содержать или не содержать недействительные данные, и вы хотите проверить их, чтобы получить "новые" экземпляры Config
, которые вы знать действительны. Это вполне возможно, а в некоторых случаях разумно, и ваш метод validateConfig
является совершенно законным способом решения этой проблемы, если вы застряли в этом несовершенном мире.
Недостатком, однако, является то, что компилятор не может отследить разницу между уже проверенными Config
экземплярами и еще не проверенными. У вас будет Config
экземпляров, плавающих в вашей программе, и если вы хотите узнать, были ли они уже проверены или нет, вам придется проследить все места, откуда они могли прийти. В некоторых случаях это может быть просто замечательно, но для больших или сложных программ это не идеально.
Подводя итог: в идеале вы должны проверять Config
экземпляры всякий раз, когда они создаются (возможно, даже делая невозможным создание недействительных), так что вам не нужно помнить, хорош ли какой-либо данный Config
или нет - система типов может запомнить для вас. Если это невозможно, например, из-за API или определения, которые вы не контролируете, или если это просто кажется слишком обременительным для простого варианта использования, то то, что вы делаете с validateConfig
, совершенно разумно.
В качестве сноски, так как вы говорите выше, что вам интересно более детально взглянуть на Refined, то, что она предоставляет вам в подобной ситуации, - это способ избежать еще большего числа функций формы A => ValidationResult[A]
. Прямо сейчас ваш validateName
метод, например, вероятно, принимает String
и возвращает ValidationResult[String]
. Вы можете сделать точно такой же аргумент против этой подписи, как у меня против Config => ValidationResult[Config]
выше - как только вы работаете с результатом (сопоставляя функцию с Validated
или чем-то еще), у вас просто есть строка и тип не говорит вам, что это уже было подтверждено.
То, что Refined позволяет вам сделать, это написать такой метод:
def validateName(in: String): ValidationResult[Refined[String, SomeProperty]] = ...
… где SomeProperty
может указывать минимальную длину или тот факт, что строка соответствует определенному регулярному выражению и т. Д. Важным моментом является то, что вы не проверяете String
и возвращаете String
, который только вы знаете что-то о - вы проверяете String
и возвращаете String
, о котором компилятор знает что-то (через оболочку Refined[A, Prop]
).
Опять же, это может быть (хорошо, вероятно, является) излишним для вашего варианта использования - вам может быть приятно узнать, что вы можете продвинуть этот принцип (отслеживание проверки типов) еще дальше в вашей программе.