Подтвердить что ты не робот

Архитектура/состав приложения в F #

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

Я имею в виду, что у меня есть много небольших классов и интерфейсов, которые я сочиняю с помощью контейнера IoC, и я также часто использую такие шаблоны, как Decorator и Composite. Это приводит к (на мой взгляд) очень гибкой и эволюционирующей общей архитектуре, которая позволяет мне легко заменять или расширять функциональные возможности в любой точке приложения. В зависимости от того, насколько велики требуемые изменения, мне может потребоваться только написать новую реализацию интерфейса, заменить его в регистрации IoC и сделать. Даже если изменение больше, я могу заменить части графа объектов, в то время как остальная часть приложения просто стоит, как и раньше.

Теперь с F # у меня нет классов и интерфейсов (я знаю, что могу, но я думаю, что помимо того, что я хочу выполнять фактическое функциональное программирование), у меня нет инъекции конструктора, t имеют контейнеры IoC. Я знаю, что могу сделать что-то вроде шаблона Decorator, используя функции более высокого порядка, но это, похоже, не дает мне такой же гибкости и ремонтопригодности, что и классы с введением конструктора.

Рассмотрим эти типы С#:

public class Dings
{
    public string Lol { get; set; }

    public string Rofl { get; set; }
}

public interface IGetStuff
{
    IEnumerable<Dings> For(Guid id);
}

public class AsdFilteringGetStuff : IGetStuff
{
    private readonly IGetStuff _innerGetStuff;

    public AsdFilteringGetStuff(IGetStuff innerGetStuff)
    {
        this._innerGetStuff = innerGetStuff;
    }

    public IEnumerable<Dings> For(Guid id)
    {
        return this._innerGetStuff.For(id).Where(d => d.Lol == "asd");
    }
}

public class GeneratingGetStuff : IGetStuff
{
    public IEnumerable<Dings> For(Guid id)
    {
        IEnumerable<Dings> dingse;

        // somehow knows how to create correct dingse for the ID

        return dingse;
    }
}

Я скажу, что мой контейнер IoC разрешает AsdFilteringGetStuff для IGetStuff и GeneratingGetStuff для своей собственной зависимости с этим интерфейсом. Теперь, если мне нужен другой фильтр или вообще удалить фильтр, мне может понадобиться соответствующая реализация IGetStuff, а затем просто измените регистрацию IoC. Пока интерфейс остается прежним, мне не нужно прикасаться к приложению внутри. OCP и LSP, включенные DIP.

Теперь что мне делать в F #?

type Dings (lol, rofl) =
    member x.Lol = lol
    member x.Rofl = rofl

let GenerateDingse id =
    // create list

let AsdFilteredDingse id =
    GenerateDingse id |> List.filter (fun x -> x.Lol = "asd")

Мне нравится, насколько это меньше, но я теряю гибкость. Да, я могу называть AsdFilteredDingse или GenerateDingse в одном и том же месте, потому что типы одинаковы - но как я могу решить, какой из них вызывать без жесткого кодирования на сайте вызова? Кроме того, хотя эти две функции взаимозаменяемы, я теперь не могу заменить функцию генератора внутри AsdFilteredDingse, не изменяя эту функцию. Это не очень приятно.

Следующая попытка:

let GenerateDingse id =
    // create list

let AsdFilteredDingse (generator : System.Guid -> Dings list) id =
    generator id |> List.filter (fun x -> x.Lol = "asd")

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

Что еще я мог сделать? Я мог бы подражать концепции "составной корень" из моего С# SOLID в последнем файле проекта F #. Большинство файлов - это всего лишь коллекции функций, тогда у меня есть какой-то "реестр", который заменяет контейнер IoC, и, наконец, есть одна функция, которую я вызываю для фактического запуска приложения и который использует функции из "реестра". В "реестре" я знаю, что мне нужна функция типа (Guid → Dings list), которую я назову GetDingseForId. Это тот, который я называю, а не отдельные функции, определенные ранее.

Для декоратора определение будет

let GetDingseForId id = AsdFilteredDingse GenerateDingse

Чтобы удалить фильтр, я бы изменил его на

let GetDingseForId id = GenerateDingse

Недостаток (?) заключается в том, что все функции, которые используют другие функции, разумно должны быть функциями более высокого порядка, и мой "реестр" должен будет отображать функции all, которые я использую, потому что фактические функции, определенные ранее, не могут вызывать функции, определенные позже, в частности не те из "реестра". Я мог бы также столкнуться с проблемами циклической зависимости с "отображениями реестра".

Есть ли в этом смысл? Как вы действительно создаете приложение F # для поддержки и эволюции (не считая проверяемого)?

4b9b3361

Ответ 1

Это легко, если вы осознаете, что объектно-ориентированная инжекция конструктора очень тесно связана с функциональным Partial Function Application.

Сначала я напишу Dings как тип записи:

type Dings = { Lol : string; Rofl : string }

В F # интерфейс IGetStuff можно свести к одной функции с сигнатурой

Guid -> seq<Dings>

Клиент, использующий эту функцию, примет его как параметр:

let Client getStuff =
    getStuff(Guid("055E7FF1-2919-4246-876E-1DA71980BE9C")) |> Seq.toList

Подпись для функции Client:

(Guid -> #seq<'b>) -> 'b list

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

Генератор

Функция генератора легко записать:

let GenerateDingse id =
    seq {
        yield { Lol = "Ha!"; Rofl = "Ha ha ha!" }
        yield { Lol = "Ho!"; Rofl = "Ho ho ho!" }
        yield { Lol = "asd"; Rofl = "ASD" } }

Функция GenerateDingse имеет такую ​​подпись:

'a -> seq<Dings>

Это на самом деле более общий, чем Guid -> seq<Dings>, но это не проблема. Если вы хотите создать Client с GenerateDingse, вы можете просто использовать его следующим образом:

let result = Client GenerateDingse

Что бы вернуть все три значения Ding из GenerateDingse.

декоратор

Оригинальный Decorator немного сложнее, но немного. В общем случае вместо добавления типа Decorated (внутренний) в качестве аргумента конструктора вы просто добавляете его как значение параметра в функцию:

let AdsFilteredDingse id s = s |> Seq.filter (fun d -> d.Lol = "asd")

Эта функция имеет такую ​​подпись:

'a -> seq<Dings> -> seq<Dings>

Это не совсем то, что мы хотим, но его легко составить с помощью GenerateDingse:

let composed id = GenerateDingse id |> AdsFilteredDingse id

Функция composed имеет подпись

'a -> seq<Dings>

Просто то, что мы ищем!

Теперь вы можете использовать Client с composed следующим образом:

let result = Client composed

который вернет только [{Lol = "asd"; Rofl = "ASD";}].

Вам не нужно сначала определять функцию composed; вы также можете составить его на месте:

let result = Client (fun id -> GenerateDingse id |> AdsFilteredDingse id)

Это также возвращает [{Lol = "asd"; Rofl = "ASD";}].

Альтернативный декоратор

Предыдущий пример хорошо работает, но на самом деле не украшает аналогичную функцию. Вот альтернатива:

let AdsFilteredDingse id f = f id |> Seq.filter (fun d -> d.Lol = "asd")

Эта функция имеет подпись:

'a -> ('a -> #seq<Dings>) -> seq<Dings>

Как вы можете видеть, аргумент f - это еще одна функция с одной и той же сигнатурой, поэтому она более близка к шаблону Decorator. Вы можете создать его так:

let composed id = GenerateDingse |> AdsFilteredDingse id

Снова, вы можете использовать Client с composed следующим образом:

let result = Client composed

или inline следующим образом:

let result = Client (fun id -> GenerateDingse |> AdsFilteredDingse id)

Дополнительные примеры и принципы для составления всего приложения с помощью F # см. мой онлайн-курс по функциональной архитектуре с F #.

Подробнее об объектно-ориентированных принципах и о том, как они относятся к функциональному программированию, см. мое сообщение в блоге о принципах SOLID и о том, как они применяются к FP.