ASP.NET Interfaces 介面使用筆記

2021-06-17

筆記 ASP.NET 進行物件導向程式設計時,如何使用 Interface 提升程式的維護與擴充性。

logo

說明

首先回顧在物件導向設計中,如何重複使用程式碼的設計方式。

Concrete Class Inheritance

Without Compiler Check

High Coupling

Abstract Class Inheritance

One or More Properties / Methods are Abstract

Compiler Check

Unable new Object

Interfaces Implement

Contract, All Property And Method is Public

Without Implementation Details

Compiler Check

Code Reuse Not In Inheritance, In Runtime Class

Unable new Object

Interface 不能有預設實作的規範、所有成員都是 Public,在 C# 8.0 後有了改變, C# 8.0 提供了 Default Interface Methods。同時也支援在 Interface 中增加存取修飾子,以提供 Default Interface Method 所使用。


Interfaces in C# 8.0 gets a makeover

結合模式使用

Repository Pattern / Simple Factory Pattern

Repository Interface 約定提供存取資料的屬性與方法。

Repository Interface

public interface IRecordRepository
{
    IEnumerable<Record> GetRecord();
}

Concrete Class

實作的 Interface 的類別,提供在不同需求下的演算邏輯。

EntityFrameworkCore 自帶存取 SQLite 的方法,太棒了 😋

public class CSVRepository : IRecordRepository
{
    string path;

    public CSVRepository()
    {
        var filename = ConfigurationManager.AppSettings["CSVFileName"];
        path = AppDomain.CurrentDomain.BaseDirectory + filename;
    }

    public IEnumerable<Record> GetRecord()
    {
        ...
    }

}

public class SQLRepository : IRecordRepository
{
    DbContextOptions<RecordContext> options;

    public SQLRepository()
    {
        var optionsBuilder = new DbContextOptionsBuilder<RecordContext>();
        optionsBuilder.UseSqlite("Data Source=Record.db");
        options = optionsBuilder.Options;
    }

    public IEnumerable<Record> GetRecord()
    {
        using (var context = new RecordContext(options))
        {
            return context.Record.ToArray();
        }
    }
}

Repository Factory

使用工廠模式減少物件產生上的耦合。

public static class RepositoryFactory
{
    public static IRecordRepository GetRepository(string repositoryType)
    {
        IRecordRepository repo = null;
        switch (repositoryType)
        {
            case "CSV": 
                repo = new CSVRepository();
                break;
            case "SQL": 
                repo = new SQLRepository();
                break;
            default:
                throw new ArgumentException("repository type not exists");
        }

        return repo;
    }
}

Usage

實際使用時,分別提供不同的方法 (CSVDataFetch, SQLDataFetch),但都對應到相同的 PopulateList 方法的執行邏輯,只因傳入的字串不同,由工廠模式負責產生 repository。

private void CSVDataFetch()
{
    PopulateList("CSV");
}

private void SQLDataFetch()
{
    PopulateList("SQL");
}

private void PopulateList(string repositoryType)
{
    IRecordRepository repository =
      RepositoryFactory.GetRepository(repositoryType);

    var records = repository.GetRecord();
    foreach (var record in records)
    {
        Console.WriteLine(record);
    }
}

Dynamic Factory

上述結合 Repository Pattern 與 Factory Pattern 的方式是在 Comppile 的期間決定使用的 Repository 類別,而藉由 Dynamic Factory 的設計方式,可以轉為由 Runtime 決定使用的 Repository 類別,易於程式的變動與擴充,但不容易 Debug。特別適用的情境是不希望程式異動就要重新編譯所有程式,並且要更換所有端點設備程式的情況。

Dynamic Factory 將 Repository 類別的使用由 App.config / Web.config 決定,只要提供執行環境對應的 DLL 檔案,就可以動態的使用各 Repository 類別。

RepositoryDynamicFactory

public static class RepositoryDynamicFactory
{
    public static IRecordRepository GetRepository()
    {
        string repositoryType = ConfigurationManager.AppSettings["RepositoryType"];
        Type repositoryType = Type.GetType(repositoryType);
        object repository = Activator.CreateInstance(repositoryType);
        IRecordRepository recordRepository = repository as IRecordRepository;
        return recordRepository;
    }
}

App.config

config 中的 repositoryType 是以 assembly 的方式賦值,其中 RecordRepository.CSV.CSVRepository 為 namespace + class name,RecordRepository.CSV 為 namespace,Version=1.0.0.0 表示 assmebly 的版本。

CSV Repository 或 SQL Repository 以註解與否的方式作為選擇。

<!-- Settings for CSV Repository -->
<appSettings>
<add key="RepositoryType" value="RecordRepository.CSV.CSVRepository, 
        RecordRepository.CSV, Version=1.0.0.0, Culture=neutral"/>

<add key="CSVFileName" value="Records.txt"/>
</appSettings>

<!-- Settings for SQL Repository -->
<appSettings>
<add key="RepositoryType" value="RecordRepository.SQL.SQLRepository,
        RecordRepository.SQL, Version=1.0.0.0, Culture=neutral" />
</appSettings>

Usage

改為使用 DynamicFactory

private void PopulateList(string repositoryType)
{
    IRecordRepository repository =
      RepositoryDynamicFactory.GetRepository(repositoryType);

    var records = repository.GetRecord();
    foreach (var record in records)
    {
        Console.WriteLine(record);
    }
}

Reference 的設定

RepositoryDynamicFactory 需要將 RecordRepository.CSV、RecordRepository.SQL 專案加入參考。

而主要執行的程式則僅需將 RepositoryDynamicFactory 專案加入參考。

在編譯的時候藉由建置事件將 RecordRepository.CSV、RecordRepository.SQL 等 DLL 複製到主程式編譯結果中。

明確的實作介面, Explicit Interface Implementation

如果 Class 實作兩個以上的 Interface,而這些 Interface 有相同簽章的方法時,可以藉由明確的實作介面來區隔不同 Interface 所使用的方法邏輯。

從 Microsoft Documents 的範例可以看到,在沒有明確實作介面的情況下,不論是以 concrete type (SampleClass) 或者任一種 interface (IControl, ISurface) ,相同 signature 方法所呼叫的都是同一個方法 (paint)。

public interface IControl
{
    void Paint();
}
public interface ISurface
{
    void Paint();
}
public class SampleClass : IControl, ISurface
{
    public void Paint()
    {
        Console.WriteLine("Paint method in SampleClass");
    }
}
SampleClass sample = new SampleClass();
IControl control = sample;
ISurface surface = sample;

sample.Paint();
control.Paint();
surface.Paint();

藉由明確的實作,則可以區別 SampleClass type 的 sample, IControl type 的 control 以及 ISurface type 的 surface 所使用的方法。

public class SampleClass : IControl, ISurface
{
    public void Paint()
    {
        Console.WriteLine("Paint method in SampleClass");
    }

    void IControl.Paint()
    {
        Console.WriteLine("Paint method in IControl");
    }

    void ISurface.Paint()
    {
        Console.WriteLine("Paint method in ISurface");
    }
}

Usage

在實作 IEnumerable<T> 的時候,因為 IEnumerable<T> 必須實作 IEnumerable,因此 Concrete Class 會需要實作同簽名 GetEnumerator Method。

而在設計上會重複使用程式碼,讓 IEnumerable 也使用 IEnumerable<int> 的 GetEnumerator Method,因此先以 public 的方式宣告 IEnumerable<int> GetEnumerator,另在明確的實作 IEnumerable 的 GetEnumerator ,並且以呼叫類別中的 GetEnumerator 表示是使用 IEnumerable<int> 的 GetEnumerator Method。

為什麼 IEnumerable<int> 不使用明確的實作方式?

對 Compiler 而言,只要有時做 IEnumerable<T> 與 IEnumrable 的 GetEnumerator Method 即可,至於是否要明確的實作並不影響。此外如果 IEnumerable<T> 明確實作的方式,IEnumrable 就無法對其呼叫重複使用程式碼。

為什麼需要實作兩次 GetEnumerator?如果要提供的方法都是相同的演算邏輯。

對 Compiler 而言,因為 IEnumerable<T> 以及 IEnumerable 的 GetEnumerator 回傳型別是不同的(分別是IEnumerator<T> 以及 IEnumerator),因此需要有不同的實作。

public class OddGenerator : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator()
    {
        int i = 1;
        yield return i;
        while (true)
        {
            i += 2;
            yield return i;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

參考資料

Pluralsight - C# Interfaces

相關連結

ASP.NET MVC 從無到有打造一個應用系統

Visual Studio 入門教學