-
-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for record hierarchies #12
Comments
Noting down for future: Let's start with the easiest scenario: closed hierarchy. So all base classes are abstract, and all leaf classes (non-abstract) are sealed. This gives us full knowledge about the hierarchy and allows for safe and well-implemented equalities, updates and base calls. |
A good reference point with a working implementation: https://github.com/WarHub/wham/blob/af6b15ec1ee8c988e59b4f4223e3bb8673d8a06d/src/WarHub.ArmouryModel.Source.CodeGeneration/Generators/RecordCorePartialGenerator.cs |
Constructor is easy: parameter list is base.Properties.Concat(this.Properties), and pass base.Properties to
The most troublesome is the Update needs to create a new instance with changed value. So the approach in the linked
An example hierarchy like that in the linked repo is This hackaround is required because a method cannot be overridden with a different return type. This approach also allows multiple abstract classes deriving one from another. An example hierarchy with multiple abstract classes in linked repo is One difference from the simpler hierarchy is in
|
With that in mind, a design for the inheritance records could be written as follows. We need to know the following about a given record:
This suggests extending RecordDescriptor with the following: public bool IsSealed { get; }
public bool IsAbstract { get; }
public RecordDescriptor BaseDescriptor { get; } Note: the actual design may differ. For example, maybe instead of a base descriptor we have a Then the following rules apply (
Those rules should suffice for handling all three expected scenarios:
|
The "new" keyword can help out making it all a bit easier. // Not an abstract base class
public partial class BaseClass {
public string BaseProperty1 { get; }
public BaseClass Update(string baseProperty1) => null;
public class Builder {
}
}
public class NextClass : BaseClass {
public string NextClassProperty { get; }
public NextClass Update(string baseProperty1, string nextClassProperty) => null;
// new keyword gives us our builder back
public new class Builder {
}
}
// Rare case, an inheriting class without any extra properties
public class NextClassWithoutMoreProperties : BaseClass {
// new keywork here helps out with the Update method
public new NextClassWithoutMoreProperties Update(string baseProperty1) => null;
public new class Builder {
}
} |
BaseClass @base = new NextClass("a", "b");
var baseBuilder = @base.ToBuilder(); The
So, in full, the Builders and Update, skipping Withs, would look like that: public class A
{
public int Foo { get; }
// generated
public A(int foo) => Foo = foo;
public A Update(int foo) => UpdateA(foo);
protected virtual A UpdateA(int foo) => new A(foo);
public Builder ToBuilder() => ToBuilderA();
protected virtual Builder ToBuilderA() => new Builder { Foo = Foo };
public class Builder
{
public int Foo { get; set; }
public A ToImmutable() => ToImmutableA();
protected virtual A ToImmutableA() => new A(Foo);
}
}
public class B : A
{
public string Bar { get; }
// generated
public B(int foo, string bar) : base(foo) => Bar = bar;
public B Update(int foo, string bar) => UpdateB(foo, bar);
protected virtual B UpdateB(int foo, string bar) => new B(foo, bar);
protected sealed override A UpdateA(int foo) => UpdateB(foo, Bar);
public new Builder ToBuilder() => ToBuilderB();
protected sealed override A.Builder ToBuilderA() => ToBuilderB();
protected virtual Builder ToBuilderB() => new Builder { Foo = Foo, Bar = Bar };
public new class Builder : A.Builder
{
public string Bar { get; set; }
public new B ToImmutable() => ToImmutableB();
protected sealed override A ToImmutableA() => ToImmutableB();
protected virtual B ToImmutableB() => new B(Foo, Bar);
}
} This looks like it could work. But it looks damn complex. Unbound (not abstract+sealed only) inheritance makes it impossible to safely implement equality though. And it's more complicated. I'd prefer to start with the closed hierarchy approach, as most actual hierarchies should be doable as such, and only after that's solid and working, consider addition of support for open hierarchies. |
Well done. [Record]
public partial class Event {
public Guid Id { get; }
public TimeStamp At { get; }
} I was prepared to create an interface instead, and have all inheriting event objects implement that interface, whilst I would manually copy-paste those two property fields into each one. public interface IEvent {
Guid Id { get; }
TimeStamp At { get; }
} Having the inheritance feature is tempting, but so far all the implementations I can think of would not have great performance. |
oooo now I'm thinking, why not autogen the interface properties!!! public interface IEvent {
Guid Id { get; }
TimeStamp At { get; }
}
[Record(GenerateInterfaces(typeof(IEvent)))] // The IEvent fields will be auto-genned!!! This will compile just fine.
public partial class SomeEvent : IEvent {
string EventData { get; }
} If custom behaviour is needed based on interface properties, those behaviours could be implemented as extension methods for the interface, replacing the need for any "base class" methods to be inherited. |
In your scenario, why would this not be enough? public interface IEvent
{
Guid Id { get; }
TimeStamp At { get; }
}
[Record]
public abstract partial class EventBase : IEvent
{
public Guid Id { get; }
public TimeStamp At { get; }
} This is compatible with the initial proposal, and makes the base class sufficient for use with derived events? |
Well, I'd really like to keep requiring all the properties being user-defined. I could imagine generating an interface from a record, like [Record(GeneratedInterfaceName = "IEvent")]
public abstract partial class EventBase
{
public Guid Id { get; }
public TimeStamp At { get; }
}
// generates
public interface IEvent
{
Guid Id { get; }
TimeStamp At { get; }
}
partial class EventBase : IEvent
{
// ...
} But not the other way around. |
That'd require calling base class ctor with appropriate parameters.
We'd need some stub method probably to get a hint what parameters to pass, like:
That'd also require modifications to mutators, making them virtual for non-sealed classes and possibly more. That's just off the top of my head.
The text was updated successfully, but these errors were encountered: