Die Community zu .NET und Classic VB.
Menü

Schnittstellen in VB entwickeln und verwenden

 von 

Einleitung 

"Interface" bedeutet "Schnittstelle". Was sich zunächst recht unspektakulär anhört, ist bei näherer Betrachtung eine interessante Möglichkeit, Objekten einheitliche Zugriffsmethoden und Eigenschaften zur Verfügung zu stellen.

Im täglichen Umgang mit Visual Basic kommt man mit Schnittstellen in Kontakt, sobald man Klassen, ebenso wie Dialoge erstellt, jedoch merkt man es nicht. In jeder Klasse ist die Schnittstelle "IUnknown" implementiert, ohne dass man dies explizit selbst tun muss.

Was und wofür sind Schnittstellen?  

Schnittstellen sind Daten- und Zugriffsstrukturen, die den Zugriff und die Steuerung auf bestimmte Objekte vereinheitlichen sollen. Ein Beispiel aus dem realen Leben ist ein Schloss. Es gibt zig verschiedene Modelle, aber allen gemeinsam ist, dass man den Schließmechanismus bedienen kann und prüfen kann, ob das Schloss gerade ver- oder entriegelt ist.

In der EDV werden Schnittstellen immer dann benutzt, wenn mit verschiedenen Objekten die gleichen Aktionen unabhängig von ihren Daten durchgeführt werden können. Eine Collection kann z. B. jede beliebige Instanz jeder beliebigen Klasse aufnehmen. Dies geht jedoch nur, weil alle Klassen die Schnittstelle "IUnknown" verwenden. Wäre das nicht der Fall, müsste man für jede Klasse eine jeweils eigene Collection erstellen.

Schnittstellen sind abstrakte Klassen. Das heißt, sie enthalten zwar alle notwendigen Eigenschaften und Methoden, es wird aber nie eine Instanz einer solchen Klasse gebildet. Auch ist es nicht nötig, die Methoden der Schnittstelle bereits mit Code zu füllen, da dieser Code selten bis nie zur Anwendung kommt.

VB bietet bei den selbst erstellten Schnittstellen jedoch einen Fallstrick, den es in den meisten anderen Programmiersprachen nicht gibt: Man kann Objekte dieser Klassen erzeugen. C/C++ macht dies bei Verwendung des Schlüsselwortes "Abstract" unmöglich.

In der Namensgebung sollten sich Schnittstellen dadurch von anderen Klassen unterscheiden, dass ihnen ein I (großes i) voransteht. Beispiel: "IUnknown".

Eine einfache Schnittstelle  

Kommen wir zu einem einfachen Beispiel: zwei Zahlen lassen sich miteinander vergleichen. Mal ist die eine Zahl größer, mal die andere. Gelegentlich kommt es auch vor, dass beide Zahlen gleich sind. Zeichenketten lassen sich ebenfalls vergleichen. Wie Zeichenketten verglichen werden, möge der geneigte Leser selbst in Erfahrung bringen, dies soll nicht Gegenstand dieser Kolumne sein.

Nun da wir wissen, dass man beides vergleichen kann, liegt es eigentlich nahe, eine gemeinsame Schnittstelle für den Vergleich zu definieren:

'   Klasse ISortable
Option Explicit

Public Function Compare(ByRef CompareWith As ISortable) As ISortEnum
    '   Diese Methode führt den Vergleich durch. In der Klasse
    '   'ISortable' wird jedoch nicht verglichen, die Klasse stellt
    '   nur die Schnittstelle zur Verfügung.
End Function

Listing 1: Die Schnittstelle "ISortable"

Statt "ISortEnum" könnte man auch eine Zahl zurückgeben lassen, mit einer eigenen Enumeration ist es aber deutlich schöner. Dabei orientieren wir uns an der Rückgabe von StrComp.

'   Modul modISort
Option Explicit

Public Enum ISortEnum
    iseLower = -1
    iseEqual = 0
    iseGreater = 1
End Enum

Listing 2: Das Modul "modISort"

Die Schnittstelle verwenden  

Nun benötigen wir eine Klasse, die die Schnittstelle verwendet. Wie oben schon erwähnt, können Zeichenketten sortiert werden. Nun lässt sich der Datentyp String aber schlecht um eine eigene Schnittstelle erweitern, also muss eine Klasse her, die sowohl eine Zeichenkette aufnehmen kann als auch unsere Schnittstelle verwendet.

'   Klasse CString
Option Explicit

'   Hier wird die Schnittstelle eingebunden
Implements ISortable

Private mvarText As String

Public Property Get Text() As String
    Text = mvarText
End Property

Public Property Let Text(ByVal sText As String)
    mvarText = sText
End Property

Private Function ISortable_Compare( _
                            CompareWith As ISortable) As ISortEnum
    '   Hilfsvariable
    Dim objText As CString

    '   Sind die Datentypen gleich?
    If (TypeName$(Me) <> TypeName$(CompareWith)) Then
        '   Nein, den Entwickler darauf aufmerksam machen
        Call Err.Raise(13)
    Else
        '   Um an die Eigenschaften des Objektes heranzukommen, muß
        '   das von der Schnittstelle auf das Objekt verwiesen werden
        Set objText = CompareWith

        '   Ja, Hier wird der eigentliche Vergleich durchgeführt
        ISortable_Compare = StrComp(mvarText, objText.Text)
    End If
End Function

Listing 3: Die Klasse "CString"

Wird jetzt eine Instanz der Klasse CString erstellt, bietet die IntelliSense-Funktion nur die Eigenschaft "Text" an. Das ist gut, so verwirren wir uns (und andere Entwickler) nicht mit unnötigem Schnickschnack. Woher soll ein anderer Entwickler auch wissen, wofür die Funktion "Compare" gut sein soll?

Erste Demo  

Jetzt haben wir also eine Klasse mit einer Schnittstelle. Aber was bringt uns das? Nun ja, so ist der Nutzen noch relativ bescheiden. Im Moment können wir zwei Instanzen der Klasse "CString" noch nicht einmal miteinander vergleichen.
Also muss eine kleine Demo-Anwendung her, die uns die bereits vorhandenen Möglichkeiten zeigt.

Option Explicit

Private Function Compare(Comp1 As ISortable, _
                         Comp2 As ISortable) As ISortEnum
    Compare = Comp1.Compare(Comp2)
End Function

Public Sub Main()
    Dim s1 As CString
    Dim s2 As CString
    
    Set s1 = New CString
    s1.Text = "Apfel"
    
    Set s2 = New CString
    s2.Text = "Birne"
    
    Debug.Print Compare(s1, s2)
    
    Set s1 = Nothing
    Set s2 = Nothing
End Sub

Listing 4: Erste Demo

Wer jetzt fleißig testet, wird sehen, dass der Vergleich ordentlich durchgeführt wird, obwohl ISortabel_Compare nirgends aufgerufen wird. Gehen wir im Einzelschritt durch den Quelltext, sehen wir, dass die Methode Compare des Objektes Comp1 (und damit s1) die Methode doch aufruft. Woran liegt das?

Das liegt daran, dass die Methode "Compare" der Schnittstelle aufgerufen wird. Diese Schnittstelle wird von der Klasse CString benutzt, der Aufruf wird also an die Implementation dieser Schnittstelle in der Klasse CString weitergereicht.

Eine weitere Schnittstelle  

Wir haben jetzt also eine Klasse, die die Daten einer Instanz davon mit den Daten einer anderen Instanz dieser Klasse vergleichen kann. Die halbe Miete auf dem Weg zur Sortierung haben wir also bereits.

Jetzt könnten wir die Objektinstanzen der Klasse CString in eine Collection (oder ein Datenfeld) schreiben und könnten diese sortierten. Das macht aber nur einmal Spaß, wenn man so etwas mehrfach (vielleicht sogar mehrfach im gleichen Projekt) hat, müsste man sich die Arbeit jedesmal aufs Neue machen. Was liegt also näher als uns auch dafür eine Schnittstelle zu schreiben?

'   Klasse ISort
Option Explicit

Public Sub Sort()
    '   Diese Methode sortiert die angegebenen Daten. In der Klasse
    '   'ISort' wird jedoch nicht sortiert, die Klasse stellt nur
    '   die Schnittstelle zur Verfügung.
End Sub

Listing 5: Schnittstelle "ISort"

Die sortierbare Liste  

Jetzt haben wir eine zweite Schnittstelle. Diese wird jedoch nicht der Klasse CString hinzugefügt, weil man ein Objekt schlecht sortieren kann. Für eine Sortierung brauchen wir eine Menge an Objekten, die alle in eine Liste kommen.

'   Klasse CStrings
Option Explicit

Implements ISort

Private mcol As VBA.Collection

Public Property Get Count() As Long
    Count = mcol.Count
End Property

Public Property Get Item(ByVal Index As Variant) As CString
    '   Diese Eigenschaft muss unter "Extras" ->
    '   "Prozedurattribute" als Standardeigenschaft festgelegt
    '   werden.
    Set Item = mcol.Item(Index)
End Property

Public Function AddItem(ByVal sText As String) As CString
    Dim objNew As CString
    
    Set objNew = New CString
    objNew.Text = sText
    
    Call mcol.Add(objNew)
    
    Set AddItem = objNew
    
    Set objNew = Nothing
End Function

Public Sub Remove(ByVal Index As Variant)
    Call mcol.Remove(Index)
End Sub

Public Function NewEnum() As IUnknown
    '   Diese Methode muss unter "Extras" ->
    '   "Prozedurattribute" ausgeblendet und mit
    '   Prozedur-ID = -4 belegt werden
    Set NewEnum = mcol.[_NewEnum]
End Function

Private Sub Class_Initialize()
    Set mcol = New VBA.Collection
End Sub

Private Sub Class_Terminate()
    Set mcol = Nothing
End Sub

Private Sub ISort_Sort()
    '   Objekte sortieren
    Call modISort.SortCollection(mcol)
End Sub

Listing 6: Die Aufzählungsklasse "CStrings"

Zusätzlich muss das Modul "modISort" um die Methode "SortCollection" ergänzt werden. Dazu kommen wir gleich.

Jetzt haben wir also die Liste fertig, sie muss nur noch gefüllt werden. Einer Methode und einer Eigenschaft dürfen wir eine Extrawurst braten, damit wir die enthaltenen Daten später auch mit For-Each-Next auslesen können.

Die Sortiermethode  

Wie weiter oben bereits erwähnt wird nun das Modul "modISort" um den eigentlichen Sortieralgorithmus ergänzt. Im Prinzip ist es unerheblich, ob man den Algorithmus auf Basis eines Datenfeldes oder einer Collection implementiert.

Zu Demonstrationszwecken ziehe ich Sortieren mit Quicksort [Tipp 0188] heran, der für die Demonstration ein wenig angepasst wurde. Es kann aber auch jeder andere Sortieralgorithmus verwendet werden.

Private Sub QuickSort(ByRef UA() As ISortable, _
                                    ByVal LB As Long, ByVal UB As Long)
    Dim P1 As Long, P2 As Long
    Dim Ref As ISortable, Temp As ISortable

    P1 = LB
    P2 = UB
    
    Set Ref = UA((P1 + P2) / 2)
    
    Do
        Do While (UA(P1).Compare(Ref) = iseLower)
            P1 = P1 + 1
        Loop
 
        Do While (UA(P2).Compare(Ref) = iseGreater)
            P2 = P2 - 1
        Loop

        If (P1 <= P2) Then
            Set Temp = UA(P1)
            Set UA(P1) = UA(P2)
            Set UA(P2) = Temp
            
            P1 = P1 + 1
            P2 = P2 - 1
        End If
    Loop Until (P1 > P2)

    If (LB < P2) Then Call QuickSort(UA, LB, P2)
    If (P1 < UB) Then Call QuickSort(UA, P1, UB)
End Sub

Public Sub SortCollection(ByRef Unsorted As VBA.Collection)
    Dim objItem() As ISortable
    Dim i As Long
    
    '   Nur weitermachen, wenn Daten vorhanden sind
    If (Unsorted.Count <> 0) Then
        '   Versuchen, ein Objekt aus der Liste zu referenzieren.
        '   Kennt das Objekt die Schnittstelle 'ISortable' nicht,
        '   wird dem Entwickler sein Fehler mitgeteilt.
        On Error Resume Next
        Err.Clear
        ReDim objItem(0)
        Set objItem(0) = Unsorted.Item(1)
        On Error Goto 0
        If (objItem(0) Is Nothing) Then
            Call Err.Raise(13)
        Else
            '   Daten in ein Array schieben
            ReDim objItem(1 To Unsorted.Count)
            For i = LBound(objItem) To UBound(objItem)
                Set objItem(i) = Unsorted.Item(i)
            Next i
            
            '   Daten sortieren
            Call QuickSort(objItem, 1, Unsorted.Count)
            
            '   Daten zurück in die Collection schieben
            Set Unsorted = New VBA.Collection
            For i = LBound(objItem) To UBound(objItem)
                Call Unsorted.Add(objItem(i))
            Next i
            
        End If
    End If
End Sub

Listing 7: Sortieralgorithmus im Modul "modISort"

Jetzt brauchen wir noch eine zentrale Anlaufstelle für die Sortierwünsche. Da bietet sich eine Methode im Modul "modISort" an. Gleichzeitig erschweren wir damit, dass ein übereifriger Entwickler eine Collection an unsere Sortiermethode übergibt, die die nötige Schnittstelle nicht kennt.

Public Sub DoSort(ByRef Unsorted As ISort)
    '   Los geht es mit dem Sortieren
    Call Unsorted.Sort
End Sub

Listing 8: Sortierung anstoßen

Vollständige Demo  

Hier angelangt haben wir jetzt zwei Schnittstellen definiert, mit der man Daten sortieren kann. Nun fehlt eine Demonstration unserer Schnittstellen.

Option Explicit

Public Sub Main()
    Dim AlleStrings As CStrings
    Dim i As Long
    
    Set AlleStrings = New CStrings
    
    Call AlleStrings.AddItem("Birne")
    Call AlleStrings.AddItem("Apfel")
    Call AlleStrings.AddItem("Banane")
    Call AlleStrings.AddItem("Zitrone")
    Call AlleStrings.AddItem("Melone")
    Call AlleStrings.AddItem("Mango")
    Call AlleStrings.AddItem("Maracuja")

    Debug.Print "Daten vor der Sortierung:"
    For i = 1 To AlleStrings.Count
        Debug.Print AlleStrings.Item(i).Text
    Next i
    Debug.Print
    
    Call modISort.DoSort(AlleStrings)
    
    Debug.Print "Daten nach der Sortierung:"
    For i = 1 To AlleStrings.Count
        Debug.Print AlleStrings.Item(i).Text
    Next i
    
    Set AlleStrings = Nothing
End Sub

Listing 9: Schnittstellen vorführen

Ausblick  

Was haben wir durch das Schnittstellenkonzept nun gewonnen? Auf den ersten Blick drei weitere Dateien, die in ein Projekt eingebunden werden.
Bei näherer Betrachtung erkennt man jedoch sofort, dass das Sortieren völlig unabhängig der gespeicherten Daten geschieht. Im Sortieralgorithmus ist nichts über den Datentyp der gespeicherten Daten bekannt, der Datentyp ist für die Sortierung völlig uninteressant. Die Schnittstellen und das Modul sind so unabhängig vom Projekt, dass sie in jedem beliebigen Projekt ohne Anpassungen lauffähig sind.

Die Methode "ISortable_Compare" kann beliebig komplex werden, solange am Ende ein eindeutiges Ergebnis des Vergleiches steht. Beispielsweise kann man eigene Datentypen vergleichen, indem man jedes einzelne Mitglied des Datentyps solange miteinander vergleicht, bis ein Unterschied zu erkennen ist.

Ich benutze für Objekte, die ich in Collections schreibe, sehr oft einen Schlüssel. Diese Eigenschaft und den Code zur Erstellung dieses Schlüssels kann man nun z. B. in die Klasse "ISortable" aufnehmen, so dass der Schlüssel auch beim Sortieren zur Verfügung steht. Oder man erstellt sich dafür eine eigene Schnittstelle.

Mit freundlichen Grüßen
Helge Rex

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.