Архитектура приложения / композиция в 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 #, чтобы его можно было поддерживать и развивать (не говоря уже о тестировании)?