Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Architecting a collection of related items of different types

public class Base : ICustomItem
{
}

public class Book : Base
{
    public List<Author> Authors;
    public List<Bookmark> Bookmarks;
}

public class Author : Base
{
}

public class Bookmark : Base
{
}

public ObservableCollection(ICustomItem) MyItems;

public ListCollectionView MyItemsView { get; } = new ListCollectionView(MyItems);

With a setup like this, I can show a singular list containing books, authors, and bookmarks. I can drill down into a book, and see that specific books authors and bookmarks.

Problem #1: I'd like to drill down into an author, and see all of the authors books. On top of that, I'd like to see all the bookmarks for all the books by that author.

An easy solution is to add the appropriate lists to each other class. eg. public List<Book> Books to the Author class. However, this gets out of hand when you start adding more categories (eg. Genre, Publisher, Language, etc)

Problem 2: I'd also like to be able to sort my list by the number of any selected tag, including any relevant tag type:

MyItemsView.SortDescriptions.Add(new SortDescription("Bookmarks.Count", Descending);

The number of bookmarks an author has should be the sum of all the bookmarks for all their books.

What is a good way to architect this kind of data so as to not have to maintain multiple lookup collections for every type? Is using Book as a source of truth not a good approach? Ideally I'd be able to sort by any tag type.

In my real application, I have solved problem #1: when drilling down into eg. an Author, I find all the Book in MyItems with a matching author, and then pull all the Genre, Publisher, etc from the list of books pulled. In this way I can display all the tags an author has based on the tags their books provided. (I do this in the scope of my list view model, as I know which item I am drilling down into and have access to the main MyItems collection)

However, using this approach I can't seem to solve problem #2. To be able to sort on Bookmarks.Count, I need to move it into Base and somehow populate it for each relevant tag type.

How can I expose this kind of information to each non-Book tag type without giving each Author or Bookmark access to the global item collection (which feels like a big no-no), or maintaining lists of all relevant tags for each tag type (which just feels really painfully inefficient)?

Edit 1: Can you define "tag" and "tag type" and give a few more examples?

I'm using tag to define any kind of item i'd like to put into my list. A Book, an Author, a Bookmark are all tags. Language and Genre would also be tags. A book has authors, and languages, just like a language has books and authors.

Edit2: Even if you don't plan to have a backing database, you should benefit from brushing up on your Entity-Relationship Model knowledge

I understand this is a highly relational structure. I do have a backing database but how it is stored in the database has little relevance with how to structure the data to be bound to a ListCollectionView

like image 876
Julien Avatar asked Oct 17 '18 23:10

Julien


People also ask

What are the different types of data structure?

Basically, data structures are divided into two categories: Linear data structure. Non-linear data structure.

What is the full meaning of architecture?

architecture, the art and technique of designing and building, as distinguished from the skills associated with construction. The practice of architecture is employed to fulfill both practical and expressive requirements, and thus it serves both utilitarian and aesthetic ends.

What are examples of data structures?

Some examples of Data Structures are arrays, Linked List, Stack, Queue, etc. Data Structures are widely used in almost every aspect of Computer Science i.e. Operating System, Compiler Design, Artifical intelligence, Graphics and many more.

What is data structures and algorithm?

A data structure is a method of organizing data in a virtual system. Think of sequences of numbers, or tables of data: these are both well-defined data structures. An algorithm is a sequence of steps executed by a computer that takes an input and transforms it into a target output.


1 Answers

I think I'd want the relationships between types to be as ethereal as possible. While most types are easily relatable, some have compound keys or odd relationships, and you just never know...so I'd externalize the finding of related types from the types themselves. Only a lucky few of us have a globally unique consistent key type.

I could imagine letting all your types be both observers and observable. I've never done such a thing out loud...at least, not like this, but it's an interesting possibility...and given 500 points, I figured it would be worth noodling around with ;-)

I'm using the term Tag to kinda follow your commentary. Maybe Base makes more sense to you? Anyway, in the following, a Tag is a type that notifies observing tags and listens to observable tags. I made the observables be a list of Tag.Subscription. Normally, you would just have a list of IDisposable instances, since that's all an observable typically provides. The reason for this is that Tag.Subscription lets you discover the underlying Tag...so that you can scrape your subscriptions for your types' list properties in derived types (as shown below in a Author and Book.)

I set up the Tag subscriber/notifier mechanism to work without values per se...just to isolate the mechanism. I assume most Tags would have values...but perhaps there are exceptions.

public interface ITag : IObservable<ITag>, IObserver<ITag>, IDisposable
{
  Type TagType { get; }
  bool SubscribeToTag( ITag tag );
}

public class Tag : ITag
{
  protected readonly List<Subscription> observables = new List<Subscription>( );
  protected readonly List<IObserver<ITag>> observers = new List<IObserver<ITag>>( );
  bool disposedValue = false;

  protected Tag( ) { }

  IDisposable IObservable<ITag>.Subscribe( IObserver<ITag> observer )
  {
    if ( !observers.Contains( observer ) )
    {
      observers.Add( observer );
      observer.OnNext( this ); //--> or not...maybe you'd set some InitialSubscription state 
                               //--> to help the observer distinguish initial notification from changes
    }
    return new Subscription( this, observer, observers );
  }

  public bool SubscribeToTag( ITag tag )
  {
    if ( observables.Any( subscription => subscription.Tag == tag ) ) return false; //--> could throw here
    observables.Add( ( Subscription ) tag.Subscribe( this ) );
    return true;
  }

  protected void Notify( ) => observers.ForEach( observer => observer.OnNext( this ) );

  public virtual void OnNext( ITag value ) { }

  public virtual void OnError( Exception error ) { }

  public virtual void OnCompleted( ) { }

  public Type TagType => GetType( );

  protected virtual void Dispose( bool disposing )
  {
    if ( !disposedValue )
    {
      if ( disposing )
      {
        while ( observables.Count > 0 )
        {
          var sub = observables[ 0 ];
          observables.RemoveAt( 0 );
          ( ( IDisposable ) sub ).Dispose( );
        }
      }
      disposedValue = true;
    }
  }

  public void Dispose( )
  {
    Dispose( true );
  }

  protected sealed class Subscription : IDisposable
  {
    readonly WeakReference<Tag> tag;
    readonly List<IObserver<ITag>> observers;
    readonly IObserver<ITag> observer;

    internal Subscription( Tag tag, IObserver<ITag> observer, List<IObserver<ITag>> observers )
    {
      this.tag = new WeakReference<Tag>( tag );
      this.observers = observers;
      this.observer = observer;
    }

    void IDisposable.Dispose( )
    {
      if ( observers.Contains( observer ) ) observers.Remove( observer );
    }

    public Tag Tag
    {
      get
      {
        if ( tag.TryGetTarget( out Tag target ) )
        {
          return target;
        }
        return null;
      }
    }
  }
}

If absolutely all tags have values, you could merge the following implementation with the foregoing...but I think it just feels better to separate them out.

public interface ITag<T> : ITag
{
  T OriginalValue { get; }
  T Value { get; set; }
  bool IsReadOnly { get; }
}

public class Tag<T> : Tag, ITag<T>
{
  T currentValue;

  public Tag( T value, bool isReadOnly = true ) : base( )
  {
    IsReadOnly = isReadOnly;
    OriginalValue = value;
    currentValue = value;
  }

  public bool IsReadOnly { get; }

  public T OriginalValue { get; }

  public T Value
  {
    get
    {
      return currentValue;
    }
    set
    {
      if ( IsReadOnly ) throw new InvalidOperationException( "You should have checked!" );
      if ( Value != null && !Value.Equals( value ) )
      {
        currentValue = value;
        Notify( );
      }
    }
  }
}

While this looks a bit busy, it's mostly vanilla subscription mechanics and disposability. The derived types would be drop-dead simple.

Notice the protected Notify() method. I started off putting that into the interface, but realized that it's probably not a good idea to make that accessible from the outside world.

So...onto examples; here's a sample Author. Notice how the AddBook sets up mutual relations. Not every type would have a method like this...but it illustrates how easy it is to do:

public class Author : Tag<string>
{
  public Author( string name ) : base( name ) { }

  public void AddBook( Book book )
  {
    SubscribeToTag( book );
    book.SubscribeToTag( this );
  }

  public IEnumerable<Book> Books
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Book )
        .Select( o => ( Book ) o.Tag );
    }
  }

  public override void OnNext( ITag value )
  {
    switch ( value.TagType.Name )
    {
      case nameof( Book ):
        Console.WriteLine( $"{( ( Book ) value ).CurrentValue} happened to {CurrentValue}" );
        break;
    }
  }
}

...and Book would be similar. Another thought regarding the mutual relation; if you accidentally defined the relation both through Book and Author, there's no harm, no foul...because the subscription mechanism just quietly skips duplications (I tested the case just to be sure):

public class Book : Tag<string>
{
  public Book( string name ) : base( name ) { }

  public void AddAuthor( Author author )
  {
    SubscribeToTag( author );
    author.SubscribeToTag( this );
  }

  public IEnumerable<Author> Authors
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Author )
        .Select( o => ( Author ) o.Tag );
    }
  }

  public override void OnNext( ITag value )
  {
    switch ( value.TagType.Name )
    {
      case nameof( Author ):
        Console.WriteLine( $"{( ( Author ) value ).CurrentValue} happened to {CurrentValue}" );
        break;
    }
  }
}

...and finally, a little test harness to see if any of it works:

var book = new Book( "Pride and..." );
var author = new Author( "Jane Doe" );

book.AddAuthor( author );

Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
  Console.WriteLine( writer.Value );
}

Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
  Console.WriteLine( tome.Value );
}

author.AddBook( book ); //--> maybe an error

Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
  Console.WriteLine( writer.Value );
}

Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
  Console.WriteLine( tome.Value );
}

...which spit out this:

Jane Doe happened to Pride and...
Pride and... happened to Jane Doe

book's authors...
Jane Doe

author's books...
Pride and...

book's authors...
Jane Doe

author's books...
Pride and...

While I had the list properties being IEnumerable<T>, you could make them be lazily loaded lists. You'd need to be able to invalidate the list's backing store, but that might flow pretty naturally from your observables.

There are hundreds of ways to go with all this. I tried to not get carried away. Don't know...it would take some testing to figure out how practical this is...but it was sure fun to think about.

EDIT

Something I forgot to illustrate...bookmarks. I guess a bookmark's value is an updateable page number? Something like:

public class Bookmark : Tag<int>
{
  public Bookmark( Book book, int pageNumber ) : base( pageNumber, false )
  {
    SubscribeToTag( book );
    book.SubscribeToTag( this );
  }

  public Book Book
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Book )
        .Select( o => o.Tag as Book )
        .FirstOrDefault( ); //--> could be .First( ) if you null-check book in ctor
    }
  }
}

Then, a Book might have an IEnumerable<Bookmark> property:

public class Book : Tag<string>
{
  //--> omitted stuff... <--//

  public IEnumerable<Bookmark> Bookmarks
  {
    get
    {
      return
        observables
        .Where( o => o.Tag is Bookmark )
        .Select( o => ( Bookmark ) o.Tag );
    }
  }

  //--> omitted stuff... <--//
}

The neat thing about that, is authors' bookmarks are their books' bookmarks:

public class Author : Tag<string>
{
   //--> omitted stuff... <--//

   public IEnumerable<Bookmark> Bookmarks => Books.SelectMany( b => b.Bookmarks );

   //--> omitted stuff... <--//
}

For yuks, I made the bookmark take a book on construction...just to illustrate a different approach. Mix and match as needed ;-) Notice that the bookmark doesn't have a list of books...just a single book...because that more correctly fits the model. It's interesting to realize that you could resolve all a books bookmarks from a single bookmark:

var bookmarks = new List<Bookmark>( bookmark.Book.Bookmarks );

...and just as easily get all the authors bookmarks:

var authBookmarks = new List<Bookmark>( bookmark.Book.Authors.SelectMany( a=> a.Bookmarks ) );
like image 197
Clay Avatar answered Oct 04 '22 07:10

Clay