Архитектура / композиция приложения на F #

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

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

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

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

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 функцией высшего порядка, но эти две функции больше не взаимозаменяемы. Если подумать, их, наверное, и не должно быть.

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

Для декоратора определение будет следующим:

let GetDingseForId id = AsdFilteredDingse GenerateDingse

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

let GetDingseForId id = GenerateDingse

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

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


person TeaDrivenDev    schedule 08.03.2014    source источник


Ответы (1)


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

Сначала я бы написал 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.

Декоратор

Исходный декоратор немного сложнее, но не намного. В общем, вместо того, чтобы добавлять тип 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 - это еще одна функция с той же сигнатурой, поэтому она больше похожа на шаблон декоратора. Составить его можно так:

let composed id = GenerateDingse |> AdsFilteredDingse id

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

let result = Client composed

или встроенный вот так:

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

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

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

person Mark Seemann    schedule 08.03.2014
comment
Спасибо Марку за обстоятельный ответ. Однако мне все еще неясно, где составлять приложение с более чем тривиальным количеством функций. Тем временем я перенес небольшое приложение C # на F # и опубликовал его здесь для ознакомления; Я реализовал там упомянутый реестр как модуль Composition, по сути, корень композиции DI для бедняков. Это жизнеспособный путь? - person TeaDrivenDev; 10.03.2014
comment
@TeaDrivenDev Как вы, возможно, заметили, я обновил сообщение, добавив ссылку на сообщение в блоге, которое копает немного глубже. Когда дело доходит до композиции, одна из интересных особенностей F # заключается в том, что компилятор предотвращает циклические ссылки, поэтому композиция и разделение намного безопаснее, чем в C #. Код, с которым вы связались, создается ближе к концу, так что выглядит хорошо. - person Mark Seemann; 10.03.2014
comment
Альтернативное определение функций может быть: let AdsFilteredDingse = Seq.filter (fun d -> d.Lol = "asd") и let composed = GenerateDingse >> AdsFilteredDingse. Таким образом, вы сможете использовать оператор композиции функций и не указывать идентификатор из AdsFilteredDingse. Но опять же, как упомянул Марк, на самом деле это не декоратор в оригинальном объектно-ориентированном смысле слова. - person Simon Stender Boisen; 12.03.2014
comment
Я пробовал это, но это дает мне ошибку компилятора, хотя сигнатура composed выглядит хорошо: error FS0030: Ограничение значения. Предполагается, что для значения'formed 'общий тип val составлен: (' _a - ›seq ‹Dings›). Либо сделайте аргументы для 'created' явными, либо, если вы не собираетесь делать его универсальным, добавьте тип аннотация. - person Mark Seemann; 12.03.2014