[英]Is there a simple way to emulate Objective-C Categories in C#?
我有一個奇怪的設計情況,我以前從未遇到過......如果我使用Objective-C,我會用類別解決它,但我必須使用C#2.0。
首先,一些背景。 我在這個類庫中有兩個抽象層。 底層為掃描內容的組件實現了插件架構(抱歉,不能比這更具體)。 每個插件都會以一種獨特的方式進行掃描,但插件也可能因其接受的內容類型而異。 由於與本討論無關的各種原因,我不想通過插件界面公開泛型。 因此,我最終為每種內容類型提供了一個IScanner接口和一個派生接口。
頂層是一個便利包裝器,它接受包含各種部分的復合內容格式。 不同的掃描程序將需要復合的不同部分,具體取決於它們感興趣的內容類型。因此,我需要具有特定於每個IScanner派生接口的邏輯,該接口解析復合內容,查找所需的相關部分。
解決此問題的一種方法是簡單地向IScanner添加另一種方法並在每個插件中實現它。 但是,雙層設計的重點在於插件本身不需要了解復合格式。 解決這個問題的蠻力方法是在上層進行類型測試和向下轉換,但是需要謹慎維護這些方法,因為將來會添加對新內容類型的支持。 在這種情況下,訪客模式也很尷尬,因為實際上只有一個訪問者,但不同的可訪問類型的數量只會隨着時間而增加(即 - 這些是訪問者適合的相反條件)。 而且,當我真正想要的只是劫持IScanner的單一發送時,雙重發送感覺就像矯枉過正!
如果我使用的是Objective-C,我只需在每個IScanner派生的接口上定義一個類別,並在那里添加parseContent方法。 該類別將在上層定義,因此插件不需要更改,同時避免了類型測試的需要。 不幸的是,C#擴展方法不起作用,因為它們基本上是靜態的(即 - 與調用站點使用的引用的編譯時類型相關聯,而不是像Obj-C類別那樣掛鈎到動態調度)。 更不用說,我必須使用C#2.0,因此我甚至無法使用擴展方法。 :-P
那么在C#中解決這個問題是否有一種干凈而簡單的方法,類似於如何用Objective-C類別解決它?
編輯:一些偽代碼,以幫助使當前設計的結構清晰:
interface IScanner
{ // Nothing to see here...
}
interface IContentTypeAScanner : IScanner
{
void ScanTypeA(TypeA content);
}
interface IContentTypeBScanner : IScanner
{
void ScanTypeB(TypeB content);
}
class CompositeScanner
{
private readonly IScanner realScanner;
// C-tor omitted for brevity... It takes an IScanner that was created
// from an assembly-qualified type name using dynamic type loading.
// NOTE: Composite is defined outside my code and completely outside my control.
public void ScanComposite(Composite c)
{
// Solution I would like (imaginary syntax borrowed from Obj-C):
// [realScanner parseAndScanContentFrom: c];
// where parseAndScanContentFrom: is defined in a category for each
// interface derived from IScanner.
// Solution I am stuck with for now:
if (realScanner is IContentTypeAScanner)
{
(realScanner as IContentTypeAScanner).ScanTypeA(this.parseTypeA(c));
}
else if (realScanner is IContentTypeBScanner)
{
(realScanner as IContentTypeBScanner).ScanTypeB(this.parseTypeB(c));
}
else
{
throw new SomeKindOfException();
}
}
// Private parsing methods omitted for brevity...
}
編輯:為了澄清,我已經考慮過這個設計了很多。 我有很多原因,其中大多數是我無法分享的,原因就在於它的原因。 我還沒有接受任何答案,因為雖然有趣,但他們回避了原來的問題。
事實是,在Obj-C中,我可以簡單而優雅地解決這個問題。 問題是,我可以在C#中使用相同的技術,如果是這樣,怎么樣? 我不介意尋找替代方案,但公平地說這不是我問的問題。 :)
聽起來你說的是你的內容是這樣的:
+--------+ | part 1 | | type A | +--------+ | part 2 | | type C | +--------+ | part 3 | | type F | +--------+ | part 4 | | type D | +--------+
並且每個零件類型都有讀者。 也就是說,AScanner知道如何處理類型A的一部分中的數據(例如上面的部分1),BS掃描器知道如何處理類型B的一部分中的數據,等等。 我到目前為止對嗎?
現在,如果我理解你,你遇到的問題是類型閱讀器( IScanner
實現)不知道如何找到它們在復合容器中識別的部分。
您的復合容器是否可以正確枚舉單獨的部件(即,它是否知道一個部件何處結束而另一個部件何處開始),如果是這樣,每個部件是否都具有掃描儀或容器可以區分的某種標識?
我的意思是,數據的布局是這樣的嗎?
+-------------+ | part 1 | | length: 100 | | type: "A" | | data: ... | +-------------+ | part 2 | | length: 460 | | type: "C" | | data: ... | +-------------+ | part 3 | | length: 26 | | type: "F" | | data: ... | +-------------+ | part 4 | | length: 790 | | type: "D" | | data: ... | +-------------+
如果您的數據布局與此類似,掃描程序是否可以不向容器請求具有與給定模式匹配的標識符的所有部件? 就像是:
class Container : IContainer{
IEnumerable IContainer.GetParts(string type){
foreach(IPart part in internalPartsList)
if(part.TypeID == type)
yield return part;
}
}
class AScanner : IScanner{
void IScanner.ProcessContainer(IContainer c){
foreach(IPart part in c.GetParts("A"))
ProcessPart(part);
}
}
或者,如果容器可能無法識別零件類型,但掃描儀能夠識別其自己的零件類型,可能類似於:
delegate void PartCallback(IPart part);
class Container : IContainer{
void IContainer.GetParts(PartCallback callback){
foreach(IPart part in internalPartsList)
callback(part);
}
}
class AScanner : IScanner{
void IScanner.ProcessContainer(IContainer c){
c.GetParts(delegate(IPart part){
if(IsTypeA(part))
ProcessPart(part);
});
}
bool IsTypeA(IPart part){
// determine if part is of type A
}
}
也許我誤解了你的內容和/或你的架構。 如果是,請澄清,我會更新。
OP的評論:
- 掃描儀不應該對容器類型有任何了解。
- 容器類型沒有內置智能。 它與C#中的普通舊數據非常接近。
- 我不能改變容器類型; 它是現有架構的一部分。
我的回復太長,無法發表評論:
掃描儀必須有一些方法來檢索它們處理的部件。 如果您擔心IScanner
接口不應該知道IContainer
接口,以便將來可以自由更改IContainer
接口,那么您可以通過以下幾種方式之一進行妥協:
IContainer
從(或包含)派生的IPartProvider
接口。 這個IPartProvider
只提供服務部件的功能,因此它應該非常穩定,並且可以在與IScanner
相同的程序IScanner
定義,這樣您的插件就不需要引用定義IContainer
的程序集。 IScanner
),只需要代理人的知識。 和
從您編輯過的問題中的偽代碼看,您看起來並沒有真正從界面中獲得任何好處,並且正在將您的插件與主應用程序緊密耦合,因為每種掃描儀類型都有一個獨特的IScanner
派生,它定義了一個獨特的“ scan“方法和CompositeScanner
類為每個零件類型都有一個唯一的”解析“方法。
我會說這是你的主要問題。 您需要將插件(我假設是IScanner
接口的實現者)與主應用程序分離 - 我假設它是CompositeScanner
類所在的位置。 我的一個較早的建議是我將如何實現這一點,但具體細節取決於你如何parseType
X職能的工作。 這些可以抽象和概括嗎?
據推測,您的parseType
X函數與Composite
類對象進行通信以獲取所需的數據。 這些不能被移動到IScanner
接口上的Parse
方法,該接口通過CompositeScanner
類代理從Composite
對象獲取這些數據嗎? 像這樣的東西:
delegate byte[] GetDataHandler(int offset, int length);
interface IScanner{
void Scan(byte[] data);
byte[] Parse(GetDataHandler getData);
}
class Composite{
public byte[] GetData(int offset, int length){/*...*/}
}
class CompositeScanner{}
IScanner realScanner;
public void ScanComposite(Composite c){
realScanner.Scan(realScanner.Parse(delegate(int offset, int length){
return c.GetData(offset, length);
});
}
}
當然,這可以通過在IScanner
上刪除單獨的Parse
方法並簡單地將GetDataHandler
委托直接傳遞給Scan
(其實現可以調用私有Parse
,如果需要)來簡化。 然后代碼看起來與我之前的例子非常相似。
這種設計提供了我能想到的關注點和解耦的分離。
我只想到了一些你可能會覺得更可口的東西,事實上,可能會提供更好的關注點分離。
如果您可以讓每個插件“注冊”應用程序,您可以在應用程序中保留解析,只要插件可以告訴應用程序如何檢索其數據。 示例如下,但由於我不知道您的部件是如何識別的,因此我實現了兩種可能性 - 一種用於索引部件,另一種用於命名部件:
// parts identified by their offset within the file
class MainApp{
struct BlockBounds{
public int offset;
public int length;
public BlockBounds(int offset, int length){
this.offset = offset;
this.length = length;
}
}
Dictionary<Type, BlockBounds> plugins = new Dictionary<Type, BlockBounds>();
public void RegisterPlugin(Type type, int offset, int length){
plugins[type] = new BlockBounds(offset, length);
}
public void ScanContent(Container c){
foreach(KeyValuePair<Type, int> pair in plugins)
((IScanner)Activator.CreateInstance(pair.Key)).Scan(
c.GetData(pair.Value.offset, pair.Value.length);
}
}
要么
// parts identified by name, block length stored within content (as in diagram above)
class MainApp{
Dictionary<string, Type> plugins = new Dictionary<string, Type>();
public void RegisterPlugin(Type type, string partID){
plugins[partID] = type;
}
public void ScanContent(Container c){
foreach(IPart part in c.GetParts()){
Type type;
if(plugins.TryGetValue(part.ID, out type))
((IScanner)Activator.CreateInstance(type)).Scan(part.Data);
}
}
}
顯然,我已經非常簡化了這些例子,但希望你能得到這個想法。 此外,如果您可以將工廠(或工廠委托)傳遞給RegisterPlugin
方法,那么不是使用Activator.CreateInstance
,而是使用它。
我要嘗試...... ;-)如果在你的系統中有一個階段,當你填充IScanner
對象的“目錄”時,你可以想到用一個屬性來裝飾你的IScanner
,說明他們感興趣的Part
。然后您可以映射此信息並使用地圖驅動Composite
的掃描。 這不是一個完整的答案:如果我有一點時間,我會試着詳細說明......
編輯:一些偽代碼來支持我的困惑解釋
public interface IScanner
{
void Scan(IPart part);
}
public interface IPart
{
string ID { get; }
}
[ScannedPart("your-id-for-A")]
public class AlphaScanner : IScanner
{
public void Scan(IPart part)
{
throw new NotImplementedException();
}
}
[ScannedPart("your-id-for-B")]
public class BetaScanner : IScanner
{
public void Scan(IPart part)
{
throw new NotImplementedException();
}
}
public interface IComposite
{
List<IPart> Parts { get; }
}
public class ScannerDriver
{
public Dictionary<string, IScanner> Scanners { get; private set; }
public void DoScan(IComposite composite)
{
foreach (IPart part in composite.Parts)
{
IScanner scanner = Scanners[part.ID];
scanner.Scan(part);
}
}
}
不要原樣:它是出於解釋目的。
編輯:回答Colonel Kernel評論。 我很高興你發現它很有趣。 :-)在這個簡單的代碼反射草圖中,應該只在字典初始化期間(或在需要時)涉及,並且在此階段,您可以“強制”存在屬性(甚至使用其他方式映射掃描儀和部件)。 我說“強制執行”是因為,即使它不是編譯時限制,我認為在將其投入生產之前至少運行一次代碼;-)所以如果需要它可能是運行時約束。 我會說靈感是(非常非常輕微)與MEF或其他類似框架相似的東西。 只需2美分。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.