Могу ли я создать экземпляры классов, содержащих значения с побочными эффектами на верхнем уровне? - PullRequest
0 голосов
/ 26 мая 2018

Этот вопрос относится к вопросу и совпадает с вопросом в Должен ли один провайдер типа переноса содержать значения, которые имеют побочные эффекты внутри класса? , любезно ответил Aaron M. Eshbach.

Iя пытаюсь реализовать в своем коде отличный совет на странице F# coding conventions

https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions.

Раздел Use classes to contain values that have side effects особенно интересен.Там написано

There are many times when initializing a value can have side effects, such as instantiating a context to a database or other remote resource. It is tempting to initialize such things in a module and use it in subsequent functions.

и приведен пример.Затем он указывает на три проблемы с этой практикой (я пропускаю их из-за недостатка места, но их можно увидеть в связанной статье) и рекомендует использовать простой класс для хранения зависимостей.

Следуя этому совету, который я реализовалпростой класс, содержащий значение с побочными эффектами:

type Roots() =
    let msg = "Roots: Computer must be one of THREADRIPPER, LAPTOP or HPW8"

    member this.dropboxRoot =
        let computerName = Environment.MachineName 
        match computerName with
        | "THREADRIPPER" -> @"C:\"
        | "HP-LAPTOP" -> @"C:\"
        | "HPW8" -> @"H:\"
        | _ -> failwith msg

Тогда я могу использовать его внутри функции

let foo (name: string) =
    let roots = Roots()
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Temp\" + name + ".csv")
    printfn "%s" path

foo "SomeName"

Пока все хорошо.В приведенном выше примере класс довольно «легкий», и я могу создать его экземпляр внутри любой функции.

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

let roots = Roots()

let csvPrinter (name: string) =
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
    printfn "%s" path

let xlsxPrinter (name: string) =
    let path = Path.Combine(roots.dropboxRoot,  @"Dropbox\Folder2\" + name + ".xlsx")
    printfn "%s" path

csvPrinter "SomeName"
xlsxPrinter "AnotherName"

Поэтому мой вопрос: если я создаю экземпляр класса Roots на верхнем уровне в модуле, я побеждаюцель создания класса, чтобы избежать проблем, описанных на странице F# coding conventions?Если это так, то как мне работать с вычислительно-интенсивными определениями?

1 Ответ

0 голосов
/ 27 мая 2018

Короткий ответ - да, это побеждает цель иметь такого рода обертку в первую очередь.

Однако в руководящих принципах для деревьев немного не хватает леса - реальная проблема заключается в более фундаментальном вопросе управления зависимостями с состоянием и внешними данными в среде, которая поддерживает чистоту функций и прозрачность ссылок, особенно когда выглядя на большую кодовую базу, которая должна расти и изменяться с течением времени (если мы смотрим на одноразовые скрипты, просто делайте то, что выполняет свою работу).Это больше в том, как поле roots заполняется и потребляется (как жестко закодированная, статическая зависимость), а не в том, обернуто ли значение в классе или нет.

Подход, который я бы рекомендовал здесьэто написать свою бизнес-логику как модуль (или несколько модулей) чистых функций и явно передать зависимости в качестве аргументов.Таким образом, вы откладываете принятие решений о зависимостях для вызывающей стороны.Это может пройти весь путь до точки входа в вашу программу (основная функция в консольном приложении, класс Startup в API и т. Д.).На языке ужасного ООП вы смотрите на эквивалент корня композиции - единственного места в вашей программе, где вы собираете свои зависимости.

Это может включать в себя обертку класса вокруг чисто функционального модуля, как предлагает соглашение, на которое вы ссылаетесь, но это не предрешено.У вас вполне может быть одна (побочная) функция для получения значения для вас, и вы можете просто передать это одно значение вниз.

let getDropboxRoot () : string option = 
    let computerName = Environment.MachineName 
    match computerName with
    | "THREADRIPPER" -> Some @"C:\"
    | "HP-LAPTOP" -> Some @"C:\"
    | "HPW8" -> Some @"H:\"
    | _ -> None        

let csvPrinter (dropboxRoot: string) (name: string) =
    let path = Path.Combine(dropboxRoot,  @"Dropbox\Folder1\" + name + ".csv")
    printfn "%s" path

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

Обертка класса вокруг него ничего не добавляет к этим свойствам.Это может обеспечить более приятный API для чуть большего количества шаблонов, но реальная проблема, которая обсуждается там, находится в другом месте.

...