Работа с классами в F # (изменчивость vs неизменяемость/члены против свободных функций)

В настоящее время я выполняю exercism.io дорожку F #. Для всех, кто этого не знает, он решает небольшие проблемы с TDD-стилем, чтобы изучить или улучшить язык программирования.

Последние две задачи касались использования классов в F # (или типов, поскольку они вызывается в F #). Одна из задач использует BankAccount, который имеет баланс и статус (открытый/закрытый) и может быть изменен с помощью функций. Использование было таким (взято из тестового кода):

let test () =
    let account = mkBankAccount () |> openAccount
    Assert.That(getBalance account, Is.EqualTo(Some 0.0)

Я написал код, который делает тестовый проход, используя неизменный класс BankAccount, с которым можно взаимодействовать с использованием бесплатных функций:

type AccountStatus = Open | Closed

type BankAccount (balance, status) =
    member acc.balance = balance
    member acc.status = status

let mkBankAccount () =
    BankAccount (0.0, Closed)

let getBalance (acc: BankAccount) =
    match acc.status with
    | Open -> Some(acc.balance)
    | Closed -> None

let updateBalance balance (acc: BankAccount) =
    match acc.status with
    | Open -> BankAccount (acc.balance + balance, Open)
    | Closed -> failwith "Account is closed!"

let openAccount (acc: BankAccount) =
    BankAccount (acc.balance, Open)

let closeAccount (acc: BankAccount) =
    BankAccount (acc.balance, Closed)

Сделав много OO, прежде чем начать изучать F #, мне стало интересно. Как более опытные разработчики F # используют классы? Чтобы ответить на этот вопрос более просто, вот мои основные проблемы в классах/типах в F #:

  • Используется ли использование классов в типичном стиле OO в F #?
  • Рекомендуются ли неизменные классы? (Я нашел их в замешательстве в приведенном выше примере).
  • Каков предпочтительный способ доступа/изменения данных класса в F #? (Функции члена класса и функции get/set или free, которые позволяют создавать конвейеры? Что относительно статических элементов, которые позволяют создавать трубопроводы и предоставлять функции с подходящим пространством имен?)

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

Ответ 1

Используется ли использование классов в типичном стиле OO в F #?

Это не обескуражило, но это не первое место, на которое ушли бы самые опытные разработчики F #. Большинство разработчиков F # избегают подклассов и парадигм OO, а вместо этого идут с записями или дискриминационными объединениями и функциями для их работы.

Являются ли неизменяемые классы предпочтительными?

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

Каков предпочтительный способ доступа/изменения данных класса в F #? (Функции члена класса и функции get/set или free, которые позволяют создавать конвейеры? Что относительно статических элементов, которые позволяют создавать трубопроводы и предоставлять функции с подходящим пространством имен?)

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


Для вашего вышеуказанного кода было бы чаще использовать запись вместо класса, а затем помещать функции, которые работают с записью в модуль. "Неизменяемый класс", как и ваш, можно записать в виде записи более лаконично:

type BankAccount = { balance : float ; status : AccountStatus }

Как только вы это сделаете, работа с ним становится проще, поскольку вы можете использовать with для возврата измененных версий:

let openAccount (acc: BankAccount) =
    { acc with status = Open }

Обратите внимание, что было бы общим для включения этих функций в модуль:

module Account =
    let open acc =
       { acc with status = Open }
    let close acc =
       { acc with status = Closed }

Ответ 2

Вопрос: Не рекомендуется ли использование классов в типичной моделе OO в F #?

Это не против природы F #. Я думаю, что есть случаи, когда это оправдано.

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

F # для удовольствия и прибыли дает краткое резюме плюсы и минусы использования классов.

Ouestion: предпочтительны ли неизменные классы? (Я нашел их в замешательстве в приведенном выше примере)

Иногда да, иногда нет. Я считаю, что неизменность классов дает вам много преимуществ (проще рассуждать о инвариантах типа и т.д.), Но иногда неизменяемый класс может быть немного громоздким в использовании.

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

Каков предпочтительный способ доступа/изменения данных класса в F #? (Функции члена класса и функции get/set или free, которые позволяют создавать конвейеры? Что относительно статических членов, которые позволяют создавать трубопроводы и предоставлять функции с подходящим пространством имен?)

Трубопровод является канонической конструкцией в F #, поэтому я бы пошел на статический член. Если ваша библиотека потребляется на некоторых других языках, вы должны также включить getter и setter внутри класса.

EDIT:

FSharp.org содержит список конкретных правил проектирования, которые включают:

✔ Используйте классы, чтобы инкапсулировать изменяемое состояние, в соответствии со стандартом OO.

✔ Использовать дискриминационные союзы в качестве альтернативы иерархиям классов для создания древовидных данных.

Ответ 3

Есть несколько способов взглянуть на этот вопрос.

Это может означать несколько вещей. Для POCO предпочтительны неизменные F # records. Затем операции с ними возвращают новые записи с необходимыми полями.

type BankAccount { status: AccountStatus; balance: int }
let close acct = { acct with status = Closed } // returns a *new* acct record

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

Итак, вместо парадигмы OO acct.Close(); acct.PersistChanges() у вас будет let acct' = close acct; db.UpdateRecord(acct').

Однако для "сервисов" в "сервис-ориентированной архитектуре (SOA)" интерфейсы и классы совершенно естественны в F #. Например, если вы хотите использовать API Twitter, вы, вероятно, создадите класс, который обертывает все HTTP-вызовы так же, как и на С#. Я видел некоторые ссылки на идеологию "SOLID" в F #, которая полностью избегает SOA, но я никогда не выяснял, как сделать эту работу на практике.

Лично мне нравится сэндвич FP-OO-FP с комбинаторами Suave FP сверху, SOA с использованием Autofac посередине, а FP - внизу. Я считаю, что это хорошо работает и масштабируется.

FWIW также вы можете сделать свой BankAccount дискриминационный союз, если Closed не может иметь баланс. Попробуйте это в своих образцах кода. Одна из приятных вещей в F # заключается в том, что он делает нелогичные состояния непредсказуемыми.

type BankAccount = Open of balance: int | Closed