Нещодавно я робив SOLID у C # до досить екстремального рівня, і в якийсь момент зрозумів, що по суті я не займаюся нічим іншим, як компонуванням функцій сьогодні. І після того, як я нещодавно знову почав розглядати F #, я зрозумів, що це, мабуть, був би набагато більш доречним вибором мови для більшості того, що я зараз роблю, тому я хотів би спробувати перенести реальний проект C # на F # як доказ концепції. Я думаю, що міг би отримати реальний код (дуже неідіоматично), але я не уявляю, як би виглядала архітектура, яка дозволяє мені працювати так само гнучко, як у C #.
Я маю на увазі це те, що у мене є багато маленьких класів та інтерфейсів, які я складаю за допомогою контейнера IoC, і я також багато використовую такі шаблони, як Decorator та Composite. Це призводить до (на мій погляд) дуже гнучкої та розвинутої загальної архітектури, яка дозволяє мені легко замінити або розширити функціональність у будь-якій точці програми. Залежно від того, наскільки великою є необхідна зміна, мені може знадобитися лише написати нову реалізацію інтерфейсу, замінити його в реєстрації IoC і закінчити. Навіть якщо зміни більші, я можу замінити частини графіку об’єктів, тоді як решта програми просто стоїть так, як це було раніше.
Тепер у F # у мене немає класів та інтерфейсів (я знаю, що можу, але я думаю, що це ще не той момент, коли я хочу займатись фактичним функціональним програмуванням), у мене немає введення конструктора і у мене немає IoC контейнери. Я знаю, що можу робити щось на зразок шаблону "Декоратор", використовуючи функції вищого порядку, але це, схоже, не дає мені такої ж гнучкості та ремонтопридатності, як класи з введенням конструктора.
Розглянемо ці типи 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")
I love how much less code this is, but I lose flexibility. Yes, I can call AsdFilteredDingse
or GenerateDingse
in the same place, because the types are the same - but how do I decide which one to call without hard coding it at the call site? Also, while these two functions are interchangeable, I now cannot replace the generator function inside AsdFilteredDingse
without changing this function as well. This isn't very nice.
Next attempt:
let GenerateDingse id =
// create list
let AsdFilteredDingse (generator : System.Guid -> Dings list) id =
generator id |> List.filter (fun x -> x.Lol = "asd")
Now I have composability by making AsdFilteredDingse a higher order function, but the two functions are not interchangeable anymore. On second thought, they probably shouldn't be anyway.
What else could I do? I could mimic the "composition root" concept from my C# SOLID in the last file of the F# project. Most files are just collections of functions, then I have some kind of "registry", which replaces the IoC container, and finally there is one function that I call to actually run the application and that uses functions from the "registry". In the "registry", I know I need a function of type (Guid -> Dings list), which I'll call GetDingseForId
. This is the one I call, never the individual functions defined earlier.
For the decorator, the definition would be
let GetDingseForId id = AsdFilteredDingse GenerateDingse
To remove the filter, I'd change that to
let GetDingseForId id = GenerateDingse
The downside(?) of this is that all functions that use other functions would sensibly have to be higher order functions, and my "registry" would have to map all functions that I use, because the actual functions defined earlier can't call any functions defined later, in particular not those from the "registry". I might also run into circular dependency issues with the "registry" mappings.
Does any of this make sense? How do you really build an F# application to be maintainable and evolvable (not to mention testable)?
Composition
module, essentially a "poor man's DI" composition root. Is that a viable way to go?