[英]Type safety with generics for modulable backends in C# ?
考慮編寫一個依賴於后端的軟件,該后端必須是模塊化的。 只要后端適合單個類,通過定義一些接口並讓幾個后端類實現它很容易實現:
interface IBackend { ... }
class BackendA : IBackend { ... }
class BackendB : IBackend {...}
現在,假設使用后端需要保存中間數據,其內部依賴於后端。 同樣,它由接口和具體類表示:
interface IFoo { ... }
class FooA : IFoo { ... }
class FooB : IFoo { ... }
Foo的構造函數沒有公開,我們可以依賴IBackend接口中的工廠方法:
interface IBackend {
IFoo createFoo(some arguments)
}
class BackendA : IBackend {
override FooA createFoo(some arguments) {
return new FooA(some arguments)
}
}
}
使用來自公共抽象層的接口無處不在,但現在BackendA的代碼現在被迫在需要時接受IFoo
而不是FooA
,並將其轉換為FooA
:
interface IBackend {
void soSomethingWithFoos(IFoo f1, IFoo f2);
}
class BackendA : IBackend {
override void doSomethingWithFoos(IFoo f1, IFoo f2) {
FooA fa1 = (FooA)f1;
FooA fa2 = (FooA)f2;
// do something
}
}
此外,這不是類型安全的,因為粗心的用戶可以立即實現不同的后端,並在編譯器的鼻子下將對象從一個后端傳遞到另一個后端:
BackendA ba = new BackendA();
BackendB bb = new BackendB();
ba.doSomethingWithFoo(bb.createFoo(some args)); // Typechecks but is clearly incorrect
為了改善這一點,我們可以為所有常見接口添加一個類型參數: IBackend<T>
, IFoo<T>
等...並用它來區分后端: BackendA : IBackend<A>
, BackendB : IBackend<B>
, FooA : IFoo<A>
。 然后我們可以:
interface IBackend<T> {
public IFoo<T> createFoo(some arguments)
}
這樣,編譯器就不允許混合來自不同后端的對象。 然而,這並不十分令人滿意:來自某些后端的代碼BackendA
仍然必須將IFoo<A>
為FooA
,即使這是類型安全的,只要FooA是實現IFoo<A>
的唯一類。 此外,願意獨立於后端的代碼現在已經遍布整個地方的泛型。
為了解決這個問題,如果我們假設我們需要表示后端的類數量是有限且常量的,我們可以通過使用所有具體類參數化接口來避免強制轉換:
interface IBackend<T, FooT, BarT> where FooT : IFoo<T>, BarT : IBar<T> {
FooT createFoo();
...
}
我甚至不知道這是否是有效的語法,也不知道這是否真的解決了問題。
這是瘋了嗎? 那里有一種類型安全的方式嗎? 或者沒有使用泛型有更好的方法嗎?
您可以通過稍微顛倒角色來解決這個問題。 而不是讓IBackend
像這樣采用IFoo
並相信IFoo
總是與相應的IBackend
,例如
interface IBackend {
IFoo createFoo();
void doSomethingWithFoo(IFoo foo);
}
您可以更改此項以讓IFoo
記住它來自哪個IBackend
。
interface IBackend {
IFoo createFoo();
}
interface IFoo {
void doSomethingWithBackend();
}
class BackendA : IBackend {
IFoo createFoo() {
return new FooA(this);
}
void doSomethingWithFoo(FooA foo) { ... }
}
class FooA : IFoo {
private BackendA backend;
FooA(BackendA backend) {
this.backend = backend;
}
void doSomethingWithBackend() {
backend.doSomethingWithFoo(this);
}
}
如果您的設計應該按照您的描述進行模塊化,則演員應該完全沒必要。 演員(通常)是低於模塊化系統的標志。
為了使事物完全模塊化,接口IFoo
應捕獲使用IFoo
實現實例所必需的操作。 棘手的部分是為您的問題找到足夠高的抽象。 在那之后,提供一個表示該抽象的接口變得微不足道。 當然,這通常說起來容易做起來難,但如果你正在尋找完全類型安全的模塊化,那么我認為這是最佳選擇。
你想要的是一種叫做“虛擬類型”的東西,它基本上使類型參數成為可以被派生類覆蓋的虛擬東西。 用於推廣這一想法的典型用例是當您擁有一系列類型時,所有類型都必須一致地覆蓋。
他們看起來像這樣:
class Foo {
virtual type T = Bar;
virtual type R = Baz;
abstract T CreateT() {
}
}
class FooDerived : Foo {
override type T = BarDerived;
}
當你在foo上使用類型成員T時,你會看到一個Bar。 在FooDerived上,您會看到BarDerived。 這對程序員來說非常方便。
顯然C#沒有這個功能。
我所知道的支持虛擬類型的一種語言是Beta。
你可以在這里讀到它:
http://en.Wikipedia.org/wiki/BETA
要帶走的一件重要事情是,這不是靜態類型安全的。 當編譯器支持它時,它需要大量的運行時檢查才能實際上是類型安全的。
最接近靜態類型的等價物將是變體接口(C#支持),但是對於每個“實體”類型,您將需要一個類型參數,這將是不實用的。 引用接口會非常嘈雜( IBackend<T1,...,T10>
)。 更重要的是,variant參數既可以用作參數,也可以用作返回類型。 不是都。 鑒於您的示例代碼,這對您不起作用。
這是我的主要觀點。 你想要做的是固有的動態類型。 所以,我不會嘗試靜態輸入。 這只是讓事情變得比他們需要的更復雜。 嘗試更有活力。 這可能會簡化后端和實體接口。 此外,您的后端界面似乎有點“健談”。 這通常表示“非最佳抽象”。 最好的界面是廣泛而簡單的想想IEnumerable或IDisposable。
理想情況下,您的后端界面應如下所示:
interface IBackend {
void Run(IFrontEndStuff);
}
我不確定這是否是您正在尋找的,但一種方法可能是一個抽象基類Foo,然后是一個動態調度到具體處理程序的處理程序。 就像是:
void FooHandler(dynamic foo)
{
FooHandlerImpl(foo);
}
void FooHandlerImpl(FooA foo)
{
//whatever you do with FooA
}
void FooHandlerImpl(FooB foo)
{
//whatever you do with FooB
}
像這樣的東西......
class Program
{
static void Main(string[] args)
{
IBackend addon = new FooA();
Console.WriteLine("Enter something if you like");
var more = Console.ReadLine();
var result = Runtime(addon);
Console.WriteLine("Result: {0}", result ?? "No Output :o(");
}
static object Runtime(IBackend addon, string more = null)
{
var need = addon as INeed;
if (need != null)
need.Input = more;
addon.Execute();
var give = addon as IGive;
if (give != null)
return give.Output;
return null;
}
}
public interface IBackend
{
void Execute();
}
public interface INeed
{
string Input { set; }
}
public interface IGive
{
string Output { get; }
}
public class FooA : IBackend, INeed, IGive
{
public void Execute()
{
Console.WriteLine(this.Input ?? "No input :o(");
if (!string.IsNullOrWhiteSpace(this.Input))
this.Output = "Thanks!";
}
public string Input { private get; set; }
public string Output { get; private set; }
}
如果您的加載項/后端項不提供接口,那么應用程序的該部分將忽略它。 你可以用泛型來收緊這個問題,但我不確定這是否真的有必要。
使用集合和委托,您甚至可以使其更加抽象,從而提高運行時靈活性。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.