设计不同类型的相关项目的集合

时间:2018-10-17 23:11:27

标签: c# wpf mvvm data-binding

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);

使用这样的设置,我可以显示包含书籍,作者和书签的单数列表。我可以深入研究一本书,并查看特定的书作者和书签。

问题1:我想深入研究一位作者,并查看所有作者的书。最重要的是,我想查看该作者所有书籍的所有书签。

一个简单的解决方案是将适当的列表添加到彼此的类中。例如。 public List<Book> BooksAuthor类。但是,当您开始添加更多类别(例如GenrePublisherLanguage等)时,这变得不受欢迎

问题2:我还希望能够按任何选定标签的编号对列表进行排序,包括任何相关标签类型:

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

作者拥有的书签数量应该是其所有书籍的所有书签的总和。

什么是构造此类数据,而不必为每种类型维护多个查找集合的好方法?使用Book作为真理的来源不是一个好方法吗?理想情况下,我可以按任何标记类型进行排序。

在我的实际应用程序中,我已经解决了问题1:深入研究例如。一个Author,我在Book中找到了一个与之匹配的作者的所有MyItems,然后从书籍列表中拉出所有GenrePublisher等拉。通过这种方式,我可以根据作者提供的标签显示作者拥有的所有标签。 (我在列表视图模型的范围内进行此操作,因为我知道我要深入研究哪个项目并可以访问主MyItems集合)

但是,使用这种方法,我似乎无法解决问题2。为了能够在Bookmarks.Count上进行排序,我需要将其移至Base并以某种方式针对每种相关标记类型进行填充。

如何在不给每种BookAuthor访问全局项目集合的情况下,将这种信息公开给每种非Bookmark标签类型(感觉像是一个很大的-否),还是维护每种标签类型的所有相关标签的列表(这简直让人感到效率低下)?

编辑1: 您可以定义“标签”和“标签类型”并给出更多示例吗?

我正在使用标记定义我想放入列表中的任何种类的商品。 BookAuthorBookmark都是标签。 LanguageGenre也将是标签。一本书包含作者和语言,就像语言包含书籍和作者一样。

Edit2: 即使您不打算拥有一个备份数据库,也应该从重新学习实体关系模型知识中受益

我知道这是一个高度关系的结构。我确实有一个后备数据库,但是如何将其存储在数据库中与如何构造要绑定到ListCollectionView

的数据无关

4 个答案:

答案 0 :(得分:4)

  

使用Book作为真理的来源不是一个好方法吗?

以当前的关系数据库模式为例,其中Book以外的任何表都没有指向其他任何对象的外键引用。您始终必须浏览Book表以查找包含书籍的任何关系。在您给出的示例中,您必须遍历整个书籍集,才能找到单个作者创作的所有书籍。如果您的参考文献朝另一个方向发展,则只需查找单个作者,然后查看其Books属性。

您目前如何获得尚未写过任何书的作者名单?您必须扫描书籍列表,以获得没有拥有书籍的每个作者的列表,然后在该列表中找到每个 not 的作者。

  

我如何在不给每个作者或书签访问全局项目集合(感觉像是大禁忌)或维护每个项目的所有相关标签的列表的情况下,将这种信息公开给每种非Book标签类型标签类型(感觉确实效率很低)?

您将需要代表每个项目上每种标记类型的属性-确实没有办法解决。如果您希望根据列表中的书签数量对列表中的项目进行排序,那么每个项目都需要提供其书签的数量。

但是属性不必由预先计算的列表支持。它们可以有效地指导您如何进行适当的联接以获得所需的信息。例如,Bookmarks的{​​{1}}属性将使用Author属性来获取书签列表:

Books

如果需要,您还可以缓存结果。

如果您选择继续不让任何实体的引用返回到public IEnumerable<Bookmark> Bookmarks => this.Books.SelectMany(b => b.Bookmarks); ,而在模型类中使Book可用,则可以对指向{{ 1}}。例如,在MyItems中:

Book

不过,我不建议您这样做,因为您对它感觉不正确是正确的。它将模型的实现链接到单独的,不相关的数据结构。我的建议是实现与列表的直接关系,并对要排序的其他所有内容使用计算属性。

答案 1 :(得分:4)

我想我希望类型之间的关系尽可能地空灵。尽管大多数类型很容易关联,但有些类型具有复合键或奇数关系,而您却永远不知道...所以我将从这些类型本身对相关类型的发现进行外部化。我们中只有少数幸运的人具有全局唯一的一致密钥类型。

我可以想象让您所有的类型既是观察者又是可观察的。我从来没有大声地做过这样的事情...至少,不是这样,但这是一个有趣的可能性...并且给了500点,我认为值得一试;;)

我正在使用Tag一词来关注您的评论。也许Base对您来说更有意义?无论如何,在下文中,Tag是一种通知观察标签并侦听可观察标签的类型。我将observables设为Tag.Subscription的列表。通常,您只需要列出IDisposable个实例,因为通常可以提供所有这些。这样做的原因是Tag.Subscription可以让您发现基础的Tag ...,以便您可以在派生类型中为类型的列表属性抓取订阅(如以下{{1}中所示) }和Author。)

我建立了Book订阅者/通知者机制,使其本身没有 values 就可以工作……只是为了隔离该机制。我假设大多数Tag都有值...但是也许会有例外。

Tag

如果绝对所有标签都具有值,则可以将以下实现与前述实现合并...但是我认为将它们分开会更好。

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;
      }
    }
  }
}

虽然看起来有点忙,但主要是原始订阅机制和可处理性。派生类型将非常简单。

请注意受保护的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( ); } } } } 方法。我开始将其放入界面中,但意识到让外界可以访问它可能不是一个好主意。

所以...举个例子;这是一个示例Notify()。注意Author是如何建立相互关系的。并非每种类型都具有这样的方法...但是它说明了这样做是多么容易:

AddBook

...和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; } } } 将相似。关于相互关系的另一种想法;如果您意外地通过BookBook定义了关系,则没有害处,没有犯规……因为订阅机制只是悄悄地跳过了重复项(我确定是测试过这种情况):

Author

...最后,通过一点测试来查看它是否有效:

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;
    }
  }
}

...吐出这个:

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 );
}

虽然我的列表属性为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... ,但您可以使它们成为延迟加载的列表。您需要能够使列表的后备存储失效,但这可能会从您的可观察对象中自然流出。

所有这一切都有数百种方法。我试图不被带走。不知道...需要做一些测试才能弄清楚这是多么实用...但是思考确实很有趣。

编辑

我忘记说明...书签了。我猜书签的值是可更新的页码?像这样:

IEnumerable<T>

然后,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 } } } 可能具有Book属性:

IEnumerable<Bookmark>

整洁的是,作者的书签就是他们的书的书签:

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... <--//
}

对于牛来说,我让书签采用了关于建筑的书……只是为了说明另一种方法。根据需要混合和匹配;-)请注意,书签没有书籍列表...仅是一本书籍...因为它更适合模型。有趣的是,您可以从一个书签中解析所有书籍的书签:

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

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

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

...并轻松获得所有作者的书签:

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

答案 2 :(得分:2)

在这种情况下,我会在书籍,作者甚至书签中使用Id。 书籍/作者之间的任何关系都可以通过具有作者ID的书籍和具有书籍ID的作者来捕获。它还保证书/作者将是唯一的。

您为什么感到需要让Book,Author和Bookmark类从同一个基类继承?您是否要使用共享功能?

对于您要寻找的功能,我想说一些扩展方法可能真的有用,例如

int GetWrittenBooks(this Author author)
{
    //either query your persistent storage or look it up in memory
}

我要说,请确保您在类中不要添加太多功能。例如,您的Book课对可能的作者生日不承担任何责任。如果“作者”的生日在“作者”类中,则“书”不应访问作者的生日,也不应拥有“作者”,而只能访问作者。这本书的作者只是“感兴趣”,仅此而已。

作者也一样:例如,它与Book x第150页上的字母数量无关。这是这本书的责任,与作者无关。

tl; dr:Single responsibility principle / Separation of concerns

答案 3 :(得分:0)

基本上,您可以使用标准实体模型方法来处理此问题,就像处理任何数据库一样,只是在您的数据库处于内存中的情况下。

public class Repository
{
    public List<Book> GetBooksByAuthor(Author author) {} // books by author
    public List<Author> GetAuthors() {}
    // ...
}

这里的另一个优点是,您可以以更有效的方式实现每个查找。 如果以后想将其交换为数据库,则可以为Repository引入接口。

每个实体都可以保留对存储库的引用,因此您可以在实体级别上提供别名以方便使用:

public class Author
{
    private Repository repo;
    public Author(Repository repo)
    {
        this.repo = repo;
    }
    // ... 
    public List<Book> Books => this.repo.GetBooksByAuthor(this);
}

根据数据完整性的重要性,您还可以选择其他形式的集合,这样可以防止对查询进行修改或将更改列表保存到每个实体等等。

如果它是一个内部库,则可以简化许多这样的考虑以简化代码。