Formal Architecture/API Proposal #11
Replies: 3 comments 3 replies
-
|
This is the promised operator report — a rundown of what the ReactiveUI org projects actually consume from DynamicData today, project by project. One thing worth saying up front: where we currently roll our own change-set machinery (in ReactiveUI.Routing), we'd be happy to drop that and adapt to VNext-style operators instead — we'd just want the equivalents documented so we know what to migrate onto. ReactiveUI.RoutingThis is our heaviest user, but mostly of DynamicData types, not operators. It now produces its own change-tracking (
ReactiveUI.Validation
Sextant (Sextant.Avalonia)
ReactiveUI.SourceGenerators (BindableDerivedList generator)
Smaller consumers (tests & samples)
Distinct operators in use across the ecosystem
|
Beta Was this translation helpful? Give feedback.
-
|
Follow-up to the operator report — here's where our projects currently hand-roll collection/lifetime management that vNext operators would own. I've included the usage pattern behind each one so you can see the behavioural contract an operator would need to satisfy, not just the call site. The dominant pattern: keyed cache + per-item subscribe + dispose-on-removalBy far the most common shape across the ecosystem is a ReactiveUI core —
Akavache —
Akavache — shutdown paths (
Fusillade —
Sextant — navigation stacks
ReactiveUI.Validation (already a DD consumer — remaining gaps)
Lower priority / gatedReactiveUI.Uno — the example app SQLiteStudio view-models build a Net askThe single highest-leverage capability for us is a lean keyed cache paired with |
Beta Was this translation helpful? Give feedback.
-
|
Some discussions:
Those are some notes I've had reading through the doc. The ideas seem good. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
So, after all the different experiments and "playtests" I've done, both on this repo, and elsewhere, I think I've got a good handle on what I think the architecture and public APIs should be. I'd like to summarize and get some feedback before calling this all a final design, to move forward with an MVP that ReactiveUI can start using, as well as some small demo apps on my end.
Strap in, cause it's probably gonna be a long read. I'm not a big fan of giant walls of text myself, but I think it's warranted here.
Branding
DynamicDataVNextwas never intended to be the formal name of the project (at least in my mind), and a decision on this can still probably be postponed, but I think it's time to consider options:DynamicData2orDynamicDataV2The most obvious candidate. Also the most boring. Kinda hate it, honestly, but there's definitely something to be said for keeping the DynamicData branding.
ReactiveUI.DynamicDataorReactiveUI.CollectionsIf this project is gonna be built on top of
ReactiveUI.Primitives, maybe it makes sense to just put it under theReactiveUIumbrella? Of course, if we end up implementing a shim layer to provide support for both Primitives andSystem.Reactivethis makes less sense. And I'm leaning towards having compatibility with both, if possible.ReactiveCollectionsThis is the name I gave to one of my prototypes, and it aligns well with what most of the API names are that I'll list out below. As a
reactivemarblesproject, I think it fits pretty well, but as I mentioned above, we would probably lose out on DynamicData brand recognition.Change Modeling
For me, the core architecture of how DynamicData models changes and change sets was the biggest reason to pursue a whole VNext project. The existing change modeling isn't bad but there's a ton of room for improvement in both readability and performance, and it's such a core part of DynamicData that changing it would mean VERY wide-scale breaking changes, that can't really be done incrementally.
Domain structure
My proposal consists of 3 domains, corresponding to different types of collections that are being virtualized:
Distinct,Ordered, andKeyed. Each domain consists of the following types:XXXChangestructmodeling a single item change, includingType, derivedCategory, and the item itself.XXXChangeTypeNonetype, for compatibility and clarity.XXXChangeSetstructmodeling a collection of (ordered) single-item changes. IncludesType, the changes themselves, and private fields that may be useful for letting consumers inspect the changes.FirstAdditionIndexfield that allows the changeset to build a zero-allocation iterator over just the additions or just the removals, within aResetoperation.ChangeSettypes utilizeImmutableArray<T>to build and store the lists of changes, allowing for a lot of optimization in memory allocations, based on how much work Microsoft has put into optimizing that library. In particular, VNext runs circles around DynamicData for things like single-change changesets, clears, and resets.XXXChangeSet.BuilderImmutableArray<T>.Builder,ImmutableList<T>.Builder, etc. that provide a safe and memory-optimal way to build changesets up from individual changes, and guarantee correctness of the output, with regard to what the changeset'sTypereports as.XXXClear,XXXReset, etc.ChangeSettype provides methods for "interpreting" the changes within, depending on the type of the changeset, by returning a zero-allocation data structure that wraps the changes, and lets the consumer skip various checks they'd have to make, were they to iterate over the changes directly.Resetchanges, which always consist ofRemovechanges for all existing items in the collection, followed byAddchanges for all new items. EachXXXChangeSettype has an.AsReset()method that will throw if the changeset'sTypeis notReset, but will otherwise return anXXXResetstruct that contains zero-allocation iterators forRemovalsandAdditions, which iterate over the items directly, without requiring a consumer to check theTypeof each change, as they go.XXXChangeStreamIObservable<XXXChangeSet<T>>, it'sXXXChangeStream<T>. This is a minimal struct that contains anIObservable<XXXChangeSet<T>>, but it also contains metadata about the stream.XXXChangeStreamtypes, metadata is passed from operator to operator ONCE, during stream construction, before a single changeset has even been produced. Not only does this reduce changeset size, it means that operators can use that metadata to actually CHANGE their internal implementations, to optimize for the scenario in play.Refreshsupport within operators. In DynamicData, we have quite a few operators that have a dedicated "immutable" variant, that doesn't support refreshes (.FilterImmutable(),.TransformImmutable(), etc.) because there's a LOT of optimization to be had when you can assume that items will never mutate (and thus, thatRefreshchanges can never occur). Putting this kind of metadata withinXXXChangeStreamtypes allows consumers to declare whether they want to support mutability ONCE, up-front, at the source collection, and have that info carry down from operator to operator, instead of having to make a decision about what operator to call, and with what optional parameters, at every step of a query.There's also a few types shared by all the domains:
ChangeCategoryNone,Addition,Removal, andOther. It helps power the shared logic that all three domains use, when it comes to generating correctResettype changesets.ChangeSetTypeEmpty,Update,Clear, andReset. It allows consumers to optimizeClearandResetoperations by skipping the iteration of every individual change, if they don't actually need to.Optional<T>Optional<T>in the BCL, folks will continue to re-implement their own, and I'm no exception. I just don't see the point in pulling in any third party dependency that has one, if that's the ONLY thing we'd need. Lemme know if there's maybe something in ReactiveUI I could use, or if this might cause some conflicts.The
DistinctDomainI.E. virutalized reactive
HashSet<T>collections. Allows consumers to do useful things that they couldn't in DynamicData, like an O(1).Contains().I thought this might be really niche, but in the very first demo app I started building to playtest things,
Distinct().Contains()actually came up as one of the first queries I wanted to write.The
OrderedDomainI.E. virtualized reactive
List<T>collections. Every item has a specific index, within the collection, that operators must track. Unlike theListdomain in DynamicData, this does NOT support any level of "maybe it's ordered, maybe it's not", or more specifically "maybe this item has a known-index, and maybe it doesn't, you have to handle both cases". That's been a big headache for me, at least, when it comes to working on DD'sListoperators.The other big thing I want to do for this domain is NOT try and support everything that any consumer may find useful. Really, that's gonna be a theme for this whole project, I think, but here in particular. Most notably, there will be no
Ordered.Filter()operator. If you want to filter your collection, you HAVE to do it inDistinctorKeyeddomains, and then you apply.Sort()to move to theOrdereddomain. The.Filter()operators inListhave proven to be very difficult to maintain, and pretty terrible for performance. Definitely open to flexibility on this point, though.The
KeyedDomainI.E. virtualized reactive
Dictionary<TKey, TValue>collections. Our comfort zone. The thing we wish we could just make everyone use, exclusively, and forget about all the other stuff. Every item has an associated key value that SHOULD be unique within the collection. And may or may not be derived from the item, via a selector.public enum KeyedChangeType
{
None,
Addition,
Removal,
Replacement
}
Similar to what I said with regard to the
Ordereddomain, there will be NO partial sorting support here. If you want sorting, your.Sort()operator takes you out of this domain, and into theOrdereddomain.Source Collections
Another one of my big goals in this project was to try and align our APIs with Microsoft APIs, whenever possible. There's a HUGE potential readability benefit for leveraging APIs that consumers are already familiar with. I mentioned that earlier with regard to
ImmutableArray<T>, and how I've modeled portions of theXXXChangeSetAPIs after that. Here, I'm talking about one of the most-familiar APIs to basically ALL .NET developers:System.Collections.Generic.To see what I mean, here's the collection APIs in the
Distinctdomain.And also the shared interfaces
These interfaces have a couple several implementations (so far):
ChangeTrackingHashSet<T>IObservableSet<T>andIObservableReadOnlySet<T>interfaces, but justISet<T>andIReadOnlySet<T>. It captures changes as they occur into aBufferedChangescollection, that's backed by aDistinctChangeSet<T>.Builder, which consumers can then call.CaptureAndClear()on, to build formal changesets.ReactiveHashSet<T>ObservableCache/ObservableListeqvuialent. But it does work slightly differently, in that it's not actually externally mutable. You feed it anIObservable<DistinctChangeSet<T>>, and it materializes the changes into a collection that you can query, and re-publishes the stream for further subscribers. Logically equivalent to an RX.Publish(), or a DD.ToObservable()ObservableHashSet<T>SourceCache/SourceListequivalent. This is really just a wrapper aroundChangeTrackingHashSet<T>.SubjectHashSet<T>or obviouslySourceHashSet<T>to mirror DD. Happy to consider more, but I settled on "Observable" as making the most sense to me.Stream Metadata
I talked about metadata earlier, being stuff we can attach to each
XXXChangeStreamtype, and there's two pieces that I've put into place, withinDistinctChangeStream:I talked about item mutability and
Refreshchanges before, so here it is.DistinctItemOptionsis a constructor parameter for all of the collections in theDistinctdomain:ChangeTrackingHashSet<T>,ReactiveHashSet<T>, andObservableHashSet<T>. So, you specify it at the same time as the type of item that it describes: when you construct the collection.The other piece of metadata that I think will prove really useful is the
IEqualityComparer<T>here. This lets us match whatHashSet<T>supports, by not requiring items to implementIEquatable<T>to get custom equality, whereas DynamicData really doesn't support that. Some DynamicData operators allow you to inject anIEqualityComparer<T>into the operator itself, but it's only per-operator. If you need equality comparison at several points in the query, you have to manually hand the comparer to each one. This will be even MORE useful in theKeyeddomain, because we'll be able to supply aKeyCompareras well as aValueComparerthe same wayDictionary<TKey, TValue>does.Operators
I don't have too much to say about operators, but I will mention that my intention is, again to leverage familiarity with existing Microsoft APIs. I.E.
System.Linq. AndSystem.Reactive.Linq, I suppose. Maybe put operators in aDynamicDataVNext.Linqnamespace? So, operator names should be.Select(),.Where(), etc. wherever possible.I can drop a sample of what one of my prototype operator implementation looks like, though.
Conclusion
I'd like to make sure everyone is on board with this as a surface, before I invest more heavily into it, so gimme any qualms or concerns or suggestions you might have.
For now, I'm going to start pulling this all together from the handful of different prototypes I've got lying around, and work towards getting a publishable package here (or wherever) that ReactiveUI can start referencing. I figure @glennawatson and whoever else over there can start build a list of the kinds of operators they need, once they start referencing the project.
Beta Was this translation helpful? Give feedback.
All reactions