4

インターフェイスの分離単一の責任の原則に従おうとしていますが、すべてをまとめる方法について混乱しています。

ここでは、いくつかのインターフェイスをより小さく、より直接的なインターフェイスに分割した例を示します。

public interface IDataRead
{
    TModel Get<TModel>(int id);
}

public interface IDataWrite
{
    void Save<TModel>(TModel model);
}

public interface IDataDelete
{        
    void Delete<TModel>(int id);
    void Delete<TModel>(TModel model);
}

少し簡略化しました (where読みやすさを妨げる条項がいくつかありました)。

現在、私はSQLiteを使用していますが、このパターンの利点は、たとえばAzureなどの別のデータ ストレージ方法を選択した場合に、変更に適応する機会が得られることです。

これで、これらの各インターフェイスの実装ができました。それぞれの簡単な例を次に示します。

public class DataDeleterSQLite : IDataDelete
{
    SQLiteConnection _Connection;

    public DataDeleterSQLite(SQLiteConnection connection) { ... }

    public void Delete<TModel>(TModel model) { ... }
}

... 

public class DataReaderSQLite : IDataRead
{
    SQLiteConnection _Connection;

    public DataReaderSQLite(SQLiteConnection connection) { ... }

    public TModel Get<TModel>(int id) { ... }
}

// You get the idea.

今、私はそれをすべてまとめるのに問題があります。一般的な考え方は、クラス (実際の実装) ではなくインターフェイスDatabaseを使用するクラスを作成することだと確信しています。だから、私はこのようなものを思いついた:

public class Database
{
    IDataDelete _Deleter;
    ...

    //Injecting the interfaces to make use of Dependency Injection.
    public Database(IDataRead reader, IDataWrite writer, IDataDelete deleter) { ... }
}

ここでの質問は、、、、およびインターフェイスをクライアントIDataReadにどのように公開する必要があるかということです。インターフェイスにリダイレクトするようにメソッドを書き直す必要がありますか? このような:IDataWriteIDataDelete

//This feels like I'm just repeating a load of work.
public void Delete<TModel>(TModel model)
{
    _Deleter.Delete<TModel>(model);
}

私のコメントを強調すると、これは少しばかげているように見えます。クラスを適切に分離された実装に分離するために多くの苦労をしましたが、今ではすべてを 1 つのメガクラスにまとめています。

次のように、インターフェイスをプロパティとして公開できます。

public IDataDelete Deleter { get; private set; }

これは少し気分が良くなりますが、クライアントは、使用する必要があるインターフェイスを決定するという面倒を経験する必要はありません.

ここでポイントを完全に見逃していますか?ヘルプ!

4

6 に答える 6

2

本当の答えではありませんが、コメントが許す以上のものをここに入れたかったのです。リポジトリ パターンを使用しているように感じるので、すべて IRepository でまとめることができます。

interface IRepository
{
    T Get<TModel>(int id);
    T Save<TModel>(TModel model);
    void Delete<TModel>(TModel model);
    void Delete<TModel>(int id);
}

これで、上記のような具体的なデータベースを作成できます。

class Database : IRepository
{
    private readonly IDataReader _reader;
    private readonly IDataWriter _writer;
    private readonly IDataDeleter _deleter;

    public Database(IDataReader reader, IDataWriter writer, IDataDeleter deleter)
    {
        _reader = reader;
        _writer = writer;
        _deleter = deleter;
    }

    public T Get<TModel>(int id) { _reader.Get<TModel>(id); }

    public T Save<TModel>(TModel model) { _writer.Save<TModel>(model); }

    public void Delete<TModel>(TModel model) { _deleter.Delete<TModel>(model); }

    public void Delete<TModel>(int id) { _deleter.Delete<TModel>(id); }
}

はい、表面的には不必要な抽象化のように見えますが、多くの利点があります。@moarboilerplateが彼の答えだと言ったように、「ベスト」プラクティスが製品の提供の邪魔にならないようにしてください。製品によって、従う必要がある原則と、製品に必要な抽象化レベルが決まります。

上記のこのアプローチを進めることの 1 つの簡単な利点を次に示します。

class CompositeWriter : IDataWriter
{
    public List<IDataWriter> Writers { get; set; }

    public void Save<TModel>(model)
    {
        this.Writers.ForEach(writer =>
        {
            writer.Save<TModel>(model);
        });
    }
}

class Database : IRepository
{
    private readonly IDataReader _reader;
    private readonly IDataWriter _writer;
    private readonly IDataDeleter _deleter;
    private readonly ILogger _logger;

    public Database(IDataReader reader, IDataWriter writer, IDataDeleter deleter, ILogger _logger)
    {
        _reader = reader;
        _writer = writer;
        _deleter = deleter;
        _logger = logger;
    }

    public T Get<TModel>(int id)
    {
        var sw = Stopwatch.StartNew();

        _writer.Get<TModel>(id);

        sw.Stop();

        _logger.Info("Get Time: " + sw. ElapsedMilliseconds);
    }

    public T Save<TModel>(TModel model)
    {
         //this will execute the Save method for every writer in the CompositeWriter
         _writer.Save<TModel>(model);
    }

    ... other methods omitted
}

これで、機能を拡張する場所を確保できます。上記の例は、さまざまな IDataReader を使用して、すべての IDataReader にログ記録とタイミングを追加することなくそれらの時間を計測する方法を示しています。これは、データを実際に複数のストアに格納できる複合 IDataWriter を作成する方法も示しています。

はい、抽象化には配管が付属しており、必要がないように感じるかもしれませんが、プロジェクトの寿命によっては、将来の技術的負債を大幅に節約できます。

于 2015-09-18T18:09:06.977 に答える
2

ここでの問題は、IDataRead、IDataWrite、および IDataDelete インターフェイスをクライアントに公開する方法です。

したがって、これらのインターフェースを作成すると、それらはすでにクライアントに公開されています。クライアントは、 を使用して消費クラスに注入される依存関係として使用できますDependency Injection

クラスを適切に分離された実装に分離するために多くの苦労をしましたが、今ではすべてを 1 つのメガクラスにまとめています。

ISP実装ではなくインターフェースを分離することです。あなたの原因では、これらのインターフェイスを 1 つのクラスに実装することもできます。これは、実装で高い結束を実現できるためです。クライアントは、これらのインターフェースを 1 つのクラスに実装していることさえ認識していません。

public class Database : IDataRead, IDataWrite, IDataDelete
{
}

これは次のようになります。

public interface IRepository : IDataRead, IDataWrite, IDataDelete
{
}

ただし、 を遵守する利点が失われるため、そうするべきではありませんISP。インターフェイスを分離し、他のインターフェイスを集約する別のインターフェイスを作成しました。そのため、インターフェイスを使用するすべてのクライアントは、IRepositoryすべてのインターフェイスを実装する必要があります。と呼ばれることもありinterface soup anti-patternます。

ただし、クライアントは、使用する必要があるインターフェイスを決定するという面倒な作業を行う必要はありません。

実際、あなたはここでポイントを逃していると思います。クライアントは自分が何をしたいのか、ISPクライアントが必要のないメソッドの使用を強制されるべきではないことを教えてくれるものを知っている必要があります。


あなたが示した例では、従うISPと非対称データアクセスを簡単に作成できます。CQRSこれは建築ではおなじみの概念です。読み取りと書き込みを分けたいと想像してください。そして、実際にそれを達成するために、既存のコードを変更する必要はありません (おかげで、 にも準拠していますOCP)。あなたがする必要があるのは、IDataReadインターフェースの新しい実装を提供し、この実装をDependency Injectionコンテナに登録することです

于 2015-09-19T08:09:00.610 に答える
1

リポジトリを設計するときは、常に読み書きの観点から考えてます。

これは、現在これらのインターフェースを使用していることを意味します。

/// <summary>
/// Inform an underlying data store to return a set of read-only entity instances.
/// </summary>
/// <typeparam name="TEntity">The entity type to return read-only entity instances of.</typeparam>
public interface IEntityReader<out TEntity> where TEntity : Entity
{
    /// <summary>
    /// Inform an underlying data store to return a set of read-only entity instances.
    /// </summary>
    /// <returns>IQueryable for set of read-only TEntity instances from an underlying data store.</returns>
    IQueryable<TEntity> Query();
}

/// <summary>
/// Informs an underlying  data store to accept sets of writeable entity instances.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IEntityWriter<in TEntity> where TEntity : Entity
{
    /// <summary>
    /// Inform an underlying data store to return a single writable entity instance.
    /// </summary>
    /// <param name="primaryKey">Primary key value of the entity instance that the underlying data store should return.</param>
    /// <returns>A single writable entity instance whose primary key matches the argument value(, if one exists in the underlying data store. Otherwise, null.</returns>
    TEntity Get(object primaryKey);

    /// <summary>
    /// Inform the underlying  data store that a new entity instance should be added to a set of entity instances.
    /// </summary>
    /// <param name="entity">Entity instance that should be added to the TEntity set by the underlying data store.</param>
    void Create(TEntity entity);

    /// <summary>
    /// Inform the underlying data store that an existing entity instance should be permanently removed from its set of entity instances.
    /// </summary>
    /// <param name="entity">Entity instance that should be permanently removed from the TEntity set by the underlying data store.</param>
    void Delete(TEntity entity);

    /// <summary>
    /// Inform the underlying data store that an existing entity instance's data state may have changed.
    /// </summary>
    /// <param name="entity">Entity instance whose data state may be different from that of the underlying data store.</param>
    void Update(TEntity entity);
}

/// <summary>
/// Synchronizes data state changes with an underlying data store.
/// </summary>
public interface IUnitOfWork
{
    /// <summary>
    /// Saves changes tot the underlying data store
    /// </summary>
    void SaveChanges();
}

はエンティティの作成と削除の両方が可能であり、誰も完全に実装できないため漏れやすい抽象化であるため、IEntityWriterは少しやり過ぎであり、 に違反する可能性があると言う人もいるかもしれませんが、まだ完璧な方法を見つけていません。SRPIReadEntitiesIQueryable<TEntity>

Entity Framework の場合は、これらすべてのインターフェイスを実装します。

internal sealed class EntityFrameworkRepository<TEntity> : 
    IEntityReader<TEntity>, 
    IEntityWriter<TEntity>, 
    IUnitOfWork where TEntity : Entity
{
    private readonly Func<DbContext> _contextProvider;

    public EntityFrameworkRepository(Func<DbContext> contextProvider)
    {
        _contextProvider = contextProvider;
    }

    public void Create(TEntity entity)
    {
        var context = _contextProvider();
        if (context.Entry(entity).State == EntityState.Detached)
        {
            context.Set<TEntity>().Add(entity);
        }
    }

    public void Delete(TEntity entity)
    {
        var context = _contextProvider();
        if (context.Entry(entity).State != EntityState.Deleted)
        {
            context.Set<TEntity>().Remove(entity);
        }  
    }

    public void Update(TEntity entity)
    {
        var entry = _contextProvider().Entry(entity);
        entry.State = EntityState.Modified;
    }

    public IQueryable<TEntity> Query()
    {
        return _contextProvider().Set<TEntity>().AsNoTracking();
    }

    public TEntity Get(object primaryKey)
    {
        return _contextProvider().Set<TEntity>().Find(primaryKey);
    }

    public void SaveChanges()
    {
        _contextProvider().SaveChanges();
    }
}

そして、コマンド ハンドラを に依存しIWriteEntities<MyEntity>、クエリ ハンドラをに依存しますIReadEntities<MyEntity>。エンティティの保存 ( を使用IUnitOfWork) は、IoC を使用したデコレーター パターンによって行われます。

于 2015-09-21T08:03:08.687 に答える
1

インターフェイスの分離について話すときは (単一の責任であっても)、論理的に関連し、互いに適合して意味のある完全なエンティティを形成する一連の操作を実行するエンティティを作成することについて話します。

アイデアは、クラスがデータベースからエンティティを読み取り、新しい値で更新できる必要があるということです。しかし、クラスはローマの天気を取得して NYSE の株価を更新することはできません!

読み取り、書き込み、削除用に個別のインターフェイスを作成するのは少し極端です。ISP は、インターフェイスに操作を 1 つだけ配置するというルールを文字どおり課しているわけではありません。理想的には、読み取り、書き込み、削除が可能なインターフェースは、完全な (ただし関連のない操作でかさばらない) インターフェースになります。ここで、インターフェース内の操作は互いに重なってはなりrelatedません。dependent

したがって、従来、次のようなインターフェースを持つことができます

interface IRepository<T>
{
    IEnumerable<T> Read();
    T Read(int id);
    IEnumerable<T> Query(Func<T, bool> predicate);
    bool Save(T data);
    bool Delete(T data);
    bool Delete(int id);
}

クライアントコードに渡すことができるこのインターフェイスは、クライアントにとって完全に理にかなっています。また、基本的な一連のルールに従う任意のタイプのエンティティと連携できます (たとえば、各エンティティは整数 ID によって一意に識別される必要があります)。

また、ビジネス/アプリケーション層クラスが、このように実際の実装クラスではなく、このインターフェースのみに依存している場合

class EmployeeService
{
    readonly IRepository<Employee> _employeeRepo;

    Employee GetEmployeeById(int id)
    {
        return _employeeRepo.Read(id);
    }

    //other CRUD operation on employee
}

その後、ビジネス/アプリケーション クラスは、データ ストア インフラストラクチャから完全に独立します。好きなデータ ストアを柔軟に選択し、このインターフェイスの実装を使用してコードベースにプラグインするだけです。

OracleRepository : IRepository必要に応じて、正しいものを使用および/またはMongoRepository : IRepository注入することができますIoC

于 2015-09-18T18:22:16.247 に答える