C
C#2y ago
ero

❔ Creating a "Lazy" Factory with a Fluent API

I'm looking to create a kind of factory, which creates commands instead of the actual results. The results are then conditionally returned upon a call to the factory's Run() or Execute() method. The reason for the "laziness" of the factory is that executing one of these commands may fail (TryExecute()). This command is then skipped (the failure may be logged) and tried again later, at which point it may no longer fail. Assume these result objects;
interface IFoo { }
abstract class FooBase : IFoo { }

class Foo<T> : FooBase { }
class Bar<T> : FooBase { }
class Qux : FooBase { }
interface IFoo { }
abstract class FooBase : IFoo { }

class Foo<T> : FooBase { }
class Bar<T> : FooBase { }
class Qux : FooBase { }
These objects are all "created" differently (as in, they have different constructor parameters), meaning the factory must also have MakeFoo<T>, MakeBar<T>, and MakeQux. But unfortunately, that's not it. All implementations of FooBase take an instance of IFoo as a sort-of parent. This can be nested infinitely. Let's assume something like this;
Foo<int> foo1 = new(/* */);
Foo<long> foo2 = new(foo1, /* */);
Foo<byte> foo3 = new(foo2, /* */);
Foo<int> foo1 = new(/* */);
Foo<long> foo2 = new(foo1, /* */);
Foo<byte> foo3 = new(foo2, /* */);
This means that the factory needs some way to create a parent which then has at least 1 or more children. I also want to modify some of the objects' properties fluently;
factory.MakeFoo<T>(/* */).Prop1(/* */).Prop2(/* */);
factory.MakeFoo<T>(/* */).Prop1(/* */).Prop2(/* */);
Potential implementations I've considered use a setup such as this;
interface IMakeFooCommand { }
abstract class MakeFooCommandBase : IMakeFooCommand { }

class MakeFooCommand<T> : MakeFooCommandBase { }
class MakeBarCommand<T> : MakeFooCommandBase { }
class MakeQuxCommand : MakeFooCommandBase { }
interface IMakeFooCommand { }
abstract class MakeFooCommandBase : IMakeFooCommand { }

class MakeFooCommand<T> : MakeFooCommandBase { }
class MakeBarCommand<T> : MakeFooCommandBase { }
class MakeQuxCommand : MakeFooCommandBase { }
interface ILazyFactory { }
class LazyFactory : ILazyFactory { }
interface ILazyFactory { }
class LazyFactory : ILazyFactory { }
46 Replies
ero
eroOP2y ago
The API could look something like this;
LazyFactory factory =
LazyFactory.New
.MakeFoo<T>(/* */).Prop1(/* */)
.MakeBar<T>(/* */).Prop2(/* */).Prop3(/* */)
.MakeQux(/* */)
.MakeParent(/* */)
.MakeFoo<T>(/* */)
.MakeParent(/* */)
.MakeBar<T>(/* */);
LazyFactory factory =
LazyFactory.New
.MakeFoo<T>(/* */).Prop1(/* */)
.MakeBar<T>(/* */).Prop2(/* */).Prop3(/* */)
.MakeQux(/* */)
.MakeParent(/* */)
.MakeFoo<T>(/* */)
.MakeParent(/* */)
.MakeBar<T>(/* */);
But this quickly runs into issues with "un-nesting". That is, if you want to "go up a parent". Perhaps a WithChildren(/* */) method on IMakeFooCommand could solve this...?
bLanco
bLanco2y ago
can you try this?
Thinker
Thinker2y ago
Couldn't you do .MakeFoo<T>(foo => foo.Prop1(/* */)) where each level of "nesting" would just be a lambda?
ero
eroOP2y ago
how do you delete someone else's message
bLanco
bLanco2y ago
i cam n
ero
eroOP2y ago
Mh, how do you mean? MakeFoo<T> still needs constructor arguments
Thinker
Thinker2y ago
hmm, true also was this written by ChatGPT
ero
eroOP2y ago
and very specifically the MakeX methods take a params int[] arg at the end
Thinker
Thinker2y ago
Another idea
Make*<T>(/* */).Prop1(/* */).Back
.Make*<T>(/* */).Prop2(/* */).Back
// ...
Make*<T>(/* */).Prop1(/* */).Back
.Make*<T>(/* */).Prop2(/* */).Back
// ...
Each builder would have a Back property which is the previous builder.
ero
eroOP2y ago
the "previous builder"? there's only one builder
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP2y ago
i still need some advice on this i'll elaborate a bit. assume this setup;
interface IFoo { }
abstract class FooBase
{
private /* */ _valueThatMustBeResolved; // some type, doesn't matter
}

class Foo<T> : FooBase { }
class Bar<T> : FooBase { }
class Qux : FooBase { }
interface IFoo { }
abstract class FooBase
{
private /* */ _valueThatMustBeResolved; // some type, doesn't matter
}

class Foo<T> : FooBase { }
class Bar<T> : FooBase { }
class Qux : FooBase { }
interface IValueResolver
{
bool TryResolve(/* */);
}

struct ResolveValueA : IValueResolver { }
struct ResolveValueB : IValueResolver { }
struct ResolveValueC : IValueResolver { }
interface IValueResolver
{
bool TryResolve(/* */);
}

struct ResolveValueA : IValueResolver { }
struct ResolveValueB : IValueResolver { }
struct ResolveValueC : IValueResolver { }
the resolvers are there to simply attempt and resolve this critical value in FooBase. without that resolved value, no instances of FooBase can function. the resolving is happening when the factory gets "activated" if the resolving fails, the command is skipped and attempted again later and the desired objects (Foo<T>, Bar<T>, Qux) are not created the part i'm stuck on is choosing whether to create these types of creation commands, which hold both the ctor arguments of the FooBase implementations, and the resolver, or to create the FooBase implementations directly, and set that value which must be resolved from outside the classes (which feels very dirty) that's one of the things i'm stuck on. the other is how to handle potentially infinite nesting of parents;
factory
.MakeFoo<T>(/* */)
.MakeParent(/* */)
.WithChildFoo<T>(/* */)
.WithChildBar<T>(/* */)
.MakeParent(/* */)
.WithChildBar<T>(/* */)
.WithChildQux(/* */)
// have to somehow "escape" this parent
.WithChildFoo<T>(/* */)
// have to somehow "escape" this parent
.MakeFoo<T>(/* */);
factory
.MakeFoo<T>(/* */)
.MakeParent(/* */)
.WithChildFoo<T>(/* */)
.WithChildBar<T>(/* */)
.MakeParent(/* */)
.WithChildBar<T>(/* */)
.WithChildQux(/* */)
// have to somehow "escape" this parent
.WithChildFoo<T>(/* */)
// have to somehow "escape" this parent
.MakeFoo<T>(/* */);
this has been making me lose my mind for the past week or so i just need some tips on good and proper design here
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
ero
eroOP2y ago
should i elaborate on anything?
Kouhai
Kouhai2y ago
pardon my naivety, but can't you just keep track of parents and pop the top most parent when you need to escape?
ero
eroOP2y ago
well that's the easy part absolutely no issues there
Kouhai
Kouhai2y ago
Right So what's the problem with the current proposed API 😅 ?
ero
eroOP2y ago
well, for one, i'm not exactly sure how to even do the parents thing like, do i have an IMakeFooCommand _parent; within each IMakeFooCommand? and even then, how do i get the result of that? how do i make sure the children are only ever evaluated when the parent was successful? how do i evaluate the children at all?
Kouhai
Kouhai2y ago
Hmm basically you want
factory
.MakeFoo<T>(/* */) // [ Foo 0 ]
.MakeParent(/* */)
.WithChildFoo<T>(/* */) // Only get's evaluated if the parent [Foo 0] was successful
.WithChildBar<T>(/* */) // [Bar 0] Only get's evaluated if the parent [Foo 0] was successful
.MakeParent(/* */)
.WithChildBar<T>(/* */) // Only get's evaluated if the parent [Bar 0] was succesful
.WithChildQux(/* */) // Only get's evaluated if the parent [Bar 0] was successful
factory
.MakeFoo<T>(/* */) // [ Foo 0 ]
.MakeParent(/* */)
.WithChildFoo<T>(/* */) // Only get's evaluated if the parent [Foo 0] was successful
.WithChildBar<T>(/* */) // [Bar 0] Only get's evaluated if the parent [Foo 0] was successful
.MakeParent(/* */)
.WithChildBar<T>(/* */) // Only get's evaluated if the parent [Bar 0] was succesful
.WithChildQux(/* */) // Only get's evaluated if the parent [Bar 0] was successful
Is my understanding correct?
ero
eroOP2y ago
ah, no. MakeFoo<T> and MakeParent are separate things MakeFoo<T> would create a MakeFooCommand<T> and MakeParent would create any IMakeFooCommand. doesn't really matter which (i do have a concrete type in mind, but it doesn't really matter too much)
Kouhai
Kouhai2y ago
Ah okay, and all subsequent calls after MakeParent shouldn't get evaluated unless their parent's command was successful
ero
eroOP2y ago
yeah
Kouhai
Kouhai2y ago
I assume IMakeFooCommand shouldn't have reference to any of it's children's commands, right think ?
ero
eroOP2y ago
that'd feel like bad practice, i think
Kouhai
Kouhai2y ago
yup, I feel the same
ero
eroOP2y ago
here's the actual real world example, by the way; https://paste.mod.gg/gniwudkzsulo (don't mind the lousy naming)
Kouhai
Kouhai2y ago
Yeah that helps illustrate the problem, lemme try something on a smaller scale and see if works think
ero
eroOP2y ago
the final classes (Pointer<T>, SpanPointer<T>, StringPointer, SizedStringPointer) basically have the same ctor signature as the overloads with nint baseAddress in the factory the reason being that the other overloads rely on finding a processmodule in the target process this obviously can't be found if the process isn't even running yet and the module may not be found on the first pass through or the module may not be found at all because the user misspelled something, or what have you this is why the factory is "lazy" and only returns what actually succeeded
Kouhai
Kouhai2y ago
Okay, I'm a bit confused now Would the offsets be known before hand? I don't think so, right?
ero
eroOP2y ago
They would, yes
Kouhai
Kouhai2y ago
Oh okay, that should make it a tiny bit easier
ero
eroOP2y ago
This is a different story when we get to Unity where it'll take a bunch of strings which correspond to field names, and we infer the type and offsets just from that via mono API calls...
Kouhai
Kouhai2y ago
I think something like this could work, but unnesting feels a bit dirty with the Top and Previous props https://paste.mod.gg/hjknydjgepys/0
BlazeBin - hjknydjgepys
A tool for sharing your source code with the world!
ero
eroOP2y ago
thanks, i'll check it out tomorrow you do still have empty MakeParent calls which aren't really valid, but i assume that's just for the sake of simplicity for the sample?
Kouhai
Kouhai2y ago
Yeah pretty much I think the general design idea might work, but unnesting is kinda bad, it's brittle
Thinker
Thinker2y ago
Dunno if it helps but I just made little test and ended up with this API
var foo = FooBuilder.Create()
.WithNested()
.WithBar()
.WithQux()
.WithString("a")
.Parent
.Parent
.WithNested()
.WithBar()
.WithQux()
.WithString("b")
.Parent
.Parent
// Nest ad infinitum
.Parent
.Parent
.WithBar()
.WithQux()
.WithString("c")
.Parent
.Parent
.Build();
var foo = FooBuilder.Create()
.WithNested()
.WithBar()
.WithQux()
.WithString("a")
.Parent
.Parent
.WithNested()
.WithBar()
.WithQux()
.WithString("b")
.Parent
.Parent
// Nest ad infinitum
.Parent
.Parent
.WithBar()
.WithQux()
.WithString("c")
.Parent
.Parent
.Build();
record Foo(Foo? Nested, Bar Bar);
record Bar(Qux Qux);
record Qux(string String);
record Foo(Foo? Nested, Bar Bar);
record Bar(Qux Qux);
record Qux(string String);
And this only builds the nested builders once Build() is called.
ero
eroOP2y ago
i'm not sure that's right
Thinker
Thinker2y ago
The thing I mostly just wanted to make a prototype of is the "go up a parent" thing you mentioned way back
ero
eroOP2y ago
i don't really think it makes sense like this though maybe take a look at this ILazyPointerFactory and IChildPointerFactory are different types so you can't just "go uppies" one
Thinker
Thinker2y ago
that's quite the interface
ero
eroOP2y ago
because if you're in the "first" parent (which is already a IChildPointerFactory), then "going up one" would return an ILazyPointerFactory but if you're in the second or later parents, then going up one would return an IChildPointerFactory again which is what kouhai meant with the unnesting being annoying
Thinker
Thinker2y ago
So child builders can only be built "downwards"
ero
eroOP2y ago
not sure what you mean by that
Thinker
Thinker2y ago
nvm
public interface ILazyPointerFactory
{
IChildPointerFactory<ILazyPointerFactory> MakeParent(...);
}

public interface IChildPointerFactory
{
IChildPointerFactory Make<T>(int nextOffset, params int[] offsets);

IChildPointerFactory MakeSpan<T>(int nextOffset, params int[] offsets);

// etc.
}

public interface IChildPointerFactory<TPrevious> : IChildPointerFactory
{
TPrevious Previous { get; }

IChildPointerFactory<IChildPointerFactory<TPrevious>> MakeParent(...);
}
public interface ILazyPointerFactory
{
IChildPointerFactory<ILazyPointerFactory> MakeParent(...);
}

public interface IChildPointerFactory
{
IChildPointerFactory Make<T>(int nextOffset, params int[] offsets);

IChildPointerFactory MakeSpan<T>(int nextOffset, params int[] offsets);

// etc.
}

public interface IChildPointerFactory<TPrevious> : IChildPointerFactory
{
TPrevious Previous { get; }

IChildPointerFactory<IChildPointerFactory<TPrevious>> MakeParent(...);
}
again not sure if this is what you want, just a thought
ero
eroOP2y ago
that's what i was thinking of, yeah the other option is
public interface ILazyPointerFactory
{
IFirstChild MakeParent(...);
}

public interface IChildPointerFactory
{
INestedChild MakeParent(...);
}

public interface IFirstChild
: IChildPointerFactory
{
ILazyPointerFactory Previous { get; }
}

public interface INestedChild
: IChildPointerFactory
{
IChildPointerFactory Previous { get; }
}
public interface ILazyPointerFactory
{
IFirstChild MakeParent(...);
}

public interface IChildPointerFactory
{
INestedChild MakeParent(...);
}

public interface IFirstChild
: IChildPointerFactory
{
ILazyPointerFactory Previous { get; }
}

public interface INestedChild
: IChildPointerFactory
{
IChildPointerFactory Previous { get; }
}
Accord
Accord2y ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
Want results from more Discord servers?
Add your server