[英]Interface segregation and single responsibility principle woes
我正在嘗試遵循接口隔離和單一責任原則,但是我對如何將它們整合在一起感到困惑。
在這里,我有一些示例,我將一些接口拆分為更小,更直接的接口:
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
, IDataWrite
和IDataDelete
接口? 我應該重寫方法重定向到接口嗎? 像這樣:
//This feels like I'm just repeating a load of work.
public void Delete<TModel>(TModel model)
{
_Deleter.Delete<TModel>(model);
}
突出顯示我的評論,這看起來有點愚蠢,我把這些類分成很好的,分開的實現很麻煩,現在我把它們全部重新組合在一個巨型課程中。
我可以將接口公開為屬性,如下所示:
public IDataDelete Deleter { get; private set; }
這感覺好一點,然而,不應該期望客戶必須經歷決定他們需要使用哪個接口的麻煩。
我完全忽略了這一點嗎? 救命!
通過這個例子,如果你想根據接口的組合定義一個對象的功能,那么分解每種操作的能力就是這樣。
所以你可以擁有只有獲取的東西,以及其他獲取,保存和刪除的東西,以及其他只能保存的東西。 然后,您可以將它們傳遞給其方法或構造函數僅調用ISaves或其他任何內容的對象。 這樣他們就不會擔心知道某些東西是如何保存的,只是它會保存,這是通過接口公開的Save()方法調用的。
或者,您可以擁有一個數據庫實現所有接口的場景,但隨后將其傳遞給只關心觸發寫入,讀取或更新等的對象 - 所以當它傳遞給它時對象它作為適當的接口類型傳遞,並且執行其他操作的能力不會暴露給使用者。
考慮到這一點,您的應用程序很可能不需要這種類型的功能。 您可能無法利用來自不同來源的數據,並且需要抽象一種在它們之間調用CRUD操作的常用方法,這是首先要解決的問題,或者需要將數據庫的概念解耦為數據源,如與支持CRUD操作的對象相反,這是第二個解決的問題。 因此,請確保采用這種方式來滿足需求,而不是試圖遵循最佳實踐 - 因為這只是采用某種做法的一種方式,但它是否“最佳”只能在上下文中確定它正在解決的問題。
我完全忽略了這一點嗎? 救命!
我不認為你完全錯過它,你是在正確的軌道上,但在這種情況下走得太遠了。 您的所有CRUD功能都完全相互關聯,因此它們屬於單一界面, 可以承擔單一責任 。 如果你的界面暴露了CRUD函數和其他一些責任,那么在我看來,重構成單獨的接口是一個很好的選擇。
如果作為您的功能的消費者,我必須實例化插入,刪除等的不同類,我會來找你。
不是一個真正的答案,但我想在這里提出更多,而不是評論允許。 感覺您正在使用存儲庫模式,因此您可以使用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所說的那樣,不要讓“最佳”做法妨礙交付產品。 您的產品決定了您需要遵循的原則和產品所需的抽象級別。
以上是采用上述方法的一個快速好處:
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。
所以,是的,抽象確實帶來了一些管道,它可能感覺好像不需要,但根據你的項目的壽命,這可以為你節省大量的技術債務。
這里的問題是我應該如何向客戶端公開IDataRead,IDataWrite和IDataDelete接口?
如果您創建這些接口,則已將它們暴露給客戶端。 客戶端可以將其用作使用Dependency Injection
注入到使用類的Dependency Injection
。
我把很多東西分成很好的,分開的實現,然后我把它們全部重新組合在一個巨型課程中。
ISP
是關於分離接口而不是實現。 在您的事業中,您甚至可以在一個類中實現這些接口,因為您在實現中實現了高內聚。 客戶甚至不知道您在一個類中實現這些接口。
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
容器中注冊此實現
當我們討論界面隔離(甚至是單一責任)時,我們討論的是讓實體執行一組邏輯相關的操作,並將它們組合在一起形成一個有意義的完整實體。
這個想法是,一個類應該能夠從數據庫中讀取一個實體,並用新值更新它。 但是,一個班級不應該能夠獲得羅馬的天氣並更新紐約證券交易所的股票價值!
為讀,寫,刪除創建單獨的接口有點極端。 互聯網服務提供商實際上沒有強加規則來在接口中放置一個操作。 理想情況下,可以讀取,寫入,刪除的接口可以完成(但不具有相關操作的笨重)接口。 這里,接口中的操作應該相互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
注入正確的文件。
當我設計庫,我總以為在閱讀和寫作的條款。
這意味着我目前正在使用這些接口:
/// <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
有點矯枉過正,可能會違反SRP
,因為它可以創建和刪除實體,而且IReadEntities
是一個漏洞抽象,因為沒有人可以完全實現IQueryable<TEntity>
- 但仍然沒有找到完美的方式。
對於Entity Framework I,然后實現所有這些接口:
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的裝飾器模式完成的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.