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