Я делаю 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 # для поддержки и эволюции (не считая проверяемого)?