Архитектура коллекции связанных предметов разных типов - PullRequest
0 голосов
/ 18 октября 2018
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> Books до Author класса.Однако это выходит из-под контроля, когда вы начинаете добавлять другие категории (например, Genre, Publisher, Language и т. Д.)

Проблема 2: Я также хотел бы иметь возможность сортировать своисписок по номеру любого выбранного тега, включая любой соответствующий тип тега:

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

Количество закладок, которые имеет автор, должно быть суммой всех закладок для всех их книг.

Каков хороший способ создания данных такого типа, чтобы не требовалось поддерживать несколько коллекций поиска для каждого типа?Является ли использование Book источником истины плохим подходом?В идеале я мог бы сортировать по любому типу тега.

В моем реальном приложении я решил проблему № 1: при детализации, например, в.Author, я нахожу все Book в MyItems с соответствующим автором, а затем вытаскиваю все Genre, Publisher и т. д. из списка извлеченных книг.Таким образом, я могу отобразить все теги автора на основе тегов, предоставленных его книгами.(Я делаю это в рамках моей модели представления списка, поскольку я знаю, в какой элемент я сверляю детали и имею доступ к основной коллекции MyItems)

Однако, используя этот подход, я не могу показатьсярешить проблему № 2.Чтобы иметь возможность сортировки по Bookmarks.Count, мне нужно переместить его в Base и каким-то образом заполнить его для каждого соответствующего типа тега.

Как я могу предоставить такую ​​информацию каждому типу тега не Book, не предоставляя каждому Author или Bookmark доступ к глобальной коллекции элементов (которая выглядит как большая нет-нет),или ведение списков всех релевантных тегов для каждого типа тегов (что кажется крайне болезненно неэффективным)?

Редактировать 1: Можете ли вы определить «тег» и «тип тега» и привести еще несколько примеров?

Я использую тег для определения любого вида элемента, который я хотел бы добавить в свой список.A Book, Author, Bookmark - все это теги.Language и Genre также будут тегами.У книги есть авторы и языки, точно так же, как у языка есть книги и авторы.

Edit2: Даже если вы не планируете иметь резервную базу данных, вы должны получить выгоду от усовершенствования своей модели Entity-Relationship.знание

Я понимаю, что это очень реляционная структура.У меня есть резервная база данных, но то, как она хранится в базе данных, мало влияет на то, как структурировать данные для привязки к ListCollectionView

Ответы [ 4 ]

0 голосов
/ 22 февраля 2019

В принципе, вы можете подойти к этому стандартному подходу модели сущностей, как и к любой другой базе данных, просто в вашем случае ваша база данных находится в памяти.

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

В зависимости от того, насколько важна целостность данных, вы также можете выбрать другие формы коллекций, которые будут препятствовать изменениям в запросах, или сохранить список изменений для каждой сущности и т. Д.

, еслиэто внутренняя библиотека, многие из которых могут быть упрощены для упрощения кода.

0 голосов
/ 20 февраля 2019

В этой ситуации я бы использовал идентификаторы для книг, авторов и, возможно, даже для закладок.Любая связь между Книгой / Автором может быть захвачена Книгой, имеющей Идентификатор автора, и Автором, имеющим Идентификатор Книги, например.Это также гарантирует, что Книги / Авторы будут уникальными.

Почему вы чувствуете необходимость позволить классу Book, Author и Bookmark наследоваться от одного базового класса?Есть ли общая функциональность, которую вы хотите использовать?

Для функциональности, которую вы ищете, я бы сказал, что некоторые методы расширения могут быть очень полезны, например,

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

Я бы сказал, убедитесь, что вы не ставитеслишком много функциональности в ваших классах.Например, ваш класс Книги не имеет никаких обязанностей, например, относительно возможного дня рождения автора.Если день рождения автора будет в классе автора, книга не должна иметь доступа к дню рождения автора, равно как и авторы, а не только ссылки на авторов.Книгу просто «интересует», какого автора она имеет, ничего более / менее.

То же самое относится и к автору: она не имеет никакого отношения к количеству букв на странице 150 Книги.х, например.Это ответственность книги, и это не должно касаться автора.

tl; dr: Принцип единоличной ответственности / Разделение интересов .

0 голосов
/ 21 февраля 2019

Я думаю, я бы хотел, чтобы отношения между типами были как можно более неземными.В то время как большинство типов легко связаны, у некоторых есть составные ключи или странные отношения, и вы просто никогда не узнаете ... так что я бы извлек поиск связанных типов из самих типов.Только немногие счастливчики имеют глобально уникальный согласованный тип ключа.

Я мог бы представить, чтобы все ваши типы были и наблюдателями, и наблюдаемыми.Я никогда не делал этого вслух ... по крайней мере, не так, но это интересная возможность ... и, учитывая 500 очков, я решил, что стоило бы поболтать с ними; -)

Я использую термин Tag, чтобы следовать вашим комментариям.Может быть, Base имеет больше смысла для вас?Как бы то ни было, в дальнейшем Tag - это тип, который уведомляет наблюдающие теги и слушает наблюдаемые теги.Я сделал observables списком Tag.Subscription.Обычно у вас был бы список IDisposable экземпляров, поскольку это все, что обычно обеспечивает наблюдаемое.Причина этого заключается в том, что Tag.Subscription позволяет вам обнаружить базовый Tag ..., чтобы вы могли просматривать свои подписки для свойств списка типов в производных типах (как показано ниже в Author и Book.)

Я настроил механизм подписчика / уведомителя Tag на работу без значений как таковых ... просто для изоляции механизма.Я предполагаю, что большинство Tag s будут иметь значения ... но, возможно, есть исключения.

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

... и Book будут похожи.Еще одна мысль о взаимоотношениях;если вы случайно определили отношение как через Book, так и 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 );

... и так же легко получить все закладки авторов:

var authBookmarks = new List<Bookmark>( bookmark.Book.Authors.SelectMany( a=> a.Bookmarks ) );
0 голосов
/ 18 февраля 2019

Является ли использование Книги в качестве источника истины плохим подходом?

Думайте о вашей текущей установке как о схеме реляционной базы данных, в которой нет таблиц, кроме Book, имеющих внешний ключссылка, указывающая на что-либо еще.Вам всегда нужно сканировать таблицу Book, чтобы найти любые связи, содержащие книги.В приведенном вами примере вы должны пройти всю коллекцию книг, чтобы найти все книги, созданные одним автором.Если у вас есть ссылки в обратном направлении, вам нужно будет найти только одного автора, а затем посмотреть на его свойство Books.

Как бы вы в настоящее время получили список авторов, которые еще не написаликниги?Вам нужно будет отсканировать список книг, чтобы получить список всех авторов, у которых есть книга, а затем найти в этом списке каждого автора , а не .

Как я могу предоставлять такую ​​информацию каждому типу тега, не относящемуся к книге, не предоставляя каждому Автору или Закладке доступ к глобальной коллекции элементов (которая выглядит как большое нет-нет) или поддерживая списки всех соответствующих тегов для каждоготип тега (который кажется крайне болезненно неэффективным)?

Вам понадобятся свойства, которые представляют каждый тип тега для каждого элемента - на самом деле нет никакого способа обойти это.Если вы хотите, чтобы элементы в списке сортировались по количеству закладок, которые есть у каждой из них, тогда каждая из них должна предложить количество закладок, которые у нее есть.

Но свойства не должныопираться на предварительно вычисленные списки.Они могут фактически быть инструкциями о том, как сделать соответствующие объединения, чтобы получить необходимую информацию.Например, свойство Bookmarks Author будет использовать свойство Books для получения списка закладок:

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

Вы также можете кэшировать результат, если хотите.

Если вы решите продолжить, чтобы ссылки из какой-либо сущности не возвращались к Book, а вместо этого сделали MyItems доступным в ваших модельных классах, вы можете сделать то же самое для отношений, указывающих на Book.Например, в Author:

public IEnumerable<Book> Books => MyItems.OfType<Book>.Where(b => b.Authors.Contains(this));

я не рекомендую делать это, так как вы правы, что не чувствуете себя хорошо.Он объединяет реализацию вашей модели в отдельную несвязанную структуру данных.Я рекомендую реализовать прямые связи со списками и использовать вычисляемые свойства для всего, что вы хотите отсортировать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...