Christian Fein
Erfahrenes Mitglied
Oft höre ich die Frage, was bringen einem Interfaces.
Dieses Beispiel zeigt den Einsatz von Interfaces für sehr modulare erweiterbare Programmierung.
Es sollen Usernamen von der Persistenz (z.b Datenbank) gelesen und ausgegeben werden. Ein weniger modulares Beispiel könnte folgendermassen aussehen:
Dieser Code ist soweit richtig, das er das gewünschte Ziel erreicht. Es werden Daten ausgelesen, in ein String Array gepackt und zurückgegeben.
Der Nachteil ist jedoch das dieser Code schlecht austauschbar ist. Ändert sich die Persistenz (statt eine Datenbank, ein normales Textfile, oder ein Verzeichnisdienst) sind beide Klassen (Users und DbUsers) anzupassen und auszutauschen. Schön wäre es wenn mann eine Schnittstelle definieren könnte um nur mit dieser zu arbeiten.
Die gute Nachricht: mann kann!
Das Mittel hierzu Interfaces (Schnittstellen).
Wir definieren die Schnittstelle (was die Daten liefernde Klassen können müssen):
Wir benötigen nur eine definierte Schnittstellen-Methode
Denkbar währe auch die definition: saveUser(String user); und weitere.
Da wir jetzt eine Schnittstelle haben können wir die Datenbank Klasse definieren die diese Schnittstelle implementiert:
Es hat sich nicht viel bei der Klasse geändert. Nur das jetzt das Interface IUsersProvider implementiert wird.
Die Methodendefinition getUsers() muss der in dem Interface definierten Methode entsprechen. Ebenso habe ich die Klasse in DbUsersProvider umbenannt-
Wir können die Klasse Users jetzt so anpassen das usersProvider vom Typ des Interfaces ist:
IUsersProvider usersProvider = new DbUsersProvider();
Da die "ist ein " Regel besagt das ein Object der Abgeleiteten Klasse (DbUsersProvider) auch immer ein Typ der Basisklasse (hier das Interface IUsersProvider) ist. Wir können somit die Instanz in einer Variable des Typs IUsersProvider speichern.
Das hat den Vorteil, das diese Variable alle Objecte von Klassen die dieses Interface implementieren aufnehmen kann.
Das Problem: Noch immer müssen wir den Hinteren Teil der Instanzierung von
usersProvider abändern wenn eine andere Klasse genutzt werden soll. Java bietet
die schöne Möglichkeit anhand eines Klassennamens (vom Typ String) eine Klasse zu instanzieren.
Dies geschieht durch:
Class.forName([vollständiger Klassename]).newInstance();
Dies kommt uns zu gute, weshalb wir die zu Instanzierende Klasse die das Interface implementiert zur Laufzeit bestimmen können.
Ich habe den Klassennamen in ein Property File ausgelagert.
Also ändern wir die Klasse Users so um das die im Propertys File definierte Klasse instanziert wird.
Damit bestimmt der Eintrag in der Properties Datei welche Klasse (die das Interface implementieren muss) die Daten liefert.
Dies ist äusserst Modular, und die Art wie mann Pluginmechanismen schreiben kann um seine Anwendung leicht erweiterbar zu designen.
Um diese Modularität zu testen, schreiben wir noch eine Klasse DebugUsersProvider
Wir ändern den Eintrag in der Users.property entsprechend um:
Beim Test wird wie erwartet ausgegeben:
user0
user1
Dieses Beispiel zeigt den Einsatz von Interfaces für sehr modulare erweiterbare Programmierung.
Es sollen Usernamen von der Persistenz (z.b Datenbank) gelesen und ausgegeben werden. Ein weniger modulares Beispiel könnte folgendermassen aussehen:
Code:
// Klassse de.tutorials.falsch.Users
package de.tutorials.falsch;
public class Users {
private String[] users;
public Users() {
DbUsers dbUsers = new DbUsers();
dbUsers.connect();
users = dbUsers.getUsers();
}
public void print() {
if(users==null) return;
for(int i =0; i < users.length; i++)
System.out.println(users[i]);
}
public static void main(String[] args) {
Users users = new Users();
users.print();
}
}
//klasse de.tutorials.falsch.DbUser
package de.tutorials.falsch;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
public class DbUsers {
private Connection conn;
public boolean connect() {
String line = "null";
String[] dbconn = new String[3];
try {
Class.forName("net.sourceforge.jtds.jdbc.Driver");
String server = "jdbc:jtds:sqlserver://xxxxx/ontimetest";
String user = "xxxuser";
String pass = "xxxpass";
conn = java.sql.DriverManager.getConnection(server,user,pass);
return true;
}
catch (Exception e) {
// loggen usw
} finally {
// resourcen freigeben
}
return false;
}
public String[] getUsers() {
PreparedStatement pStm;
try {
pStm = conn.prepareStatement("Select username from users");
ResultSet rs = pStm.executeQuery();
ArrayList list = new ArrayList();
while(rs.next()) {
list.add(rs.getString(1));
}
String[] users = new String[list.size()];
users = (String[]) list.toArray(users);
conn.close();
return users;
} catch (SQLException e) {
e.printStackTrace();
try { conn.close(); } catch(Exception e) {}
}
return null;
}
}
Dieser Code ist soweit richtig, das er das gewünschte Ziel erreicht. Es werden Daten ausgelesen, in ein String Array gepackt und zurückgegeben.
Der Nachteil ist jedoch das dieser Code schlecht austauschbar ist. Ändert sich die Persistenz (statt eine Datenbank, ein normales Textfile, oder ein Verzeichnisdienst) sind beide Klassen (Users und DbUsers) anzupassen und auszutauschen. Schön wäre es wenn mann eine Schnittstelle definieren könnte um nur mit dieser zu arbeiten.
Die gute Nachricht: mann kann!
Das Mittel hierzu Interfaces (Schnittstellen).
Wir definieren die Schnittstelle (was die Daten liefernde Klassen können müssen):
Code:
package de.tutorials.richtig;
public interface IUsersProvider {
public String[] getUsers();
}
Wir benötigen nur eine definierte Schnittstellen-Methode
Denkbar währe auch die definition: saveUser(String user); und weitere.
Da wir jetzt eine Schnittstelle haben können wir die Datenbank Klasse definieren die diese Schnittstelle implementiert:
Code:
package de.tutorials.richtig;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
public class DbUsersProvider implements IUsersProvider {
private Connection conn;
private Connection connect() {
String line = "null";
String[] dbconn = new String[3];
try {
Class.forName("net.sourceforge.jtds.jdbc.Driver");
String server = "jdbc:jtds:sqlserver://xxxxx/ontimetest";
String user = "xxxuser";
String pass = "xxxpass";
conn = java.sql.DriverManager.getConnection(server,user,pass);
return conn;
}
catch (Exception e) {
// loggen usw
} finally {
// resourcen freigeben
}
return null;
}
/* (non-Javadoc)
* @see de.tutorials.richtig.IUsersProvider#getUsers()
*/
public String[] getUsers() {
if(conn==null)
conn = connect();
PreparedStatement pStm;
try {
pStm = conn.prepareStatement("Select username from users");
ResultSet rs = pStm.executeQuery();
ArrayList list = new ArrayList();
while(rs.next()) {
list.add(rs.getString(1));
}
String[] users = new String[list.size()];
users = (String[]) list.toArray(users);
conn.close();
return users;
} catch (SQLException e) {
e.printStackTrace();
try { conn.close(); } catch(Exception e) {}
}
return null;
}
}
Es hat sich nicht viel bei der Klasse geändert. Nur das jetzt das Interface IUsersProvider implementiert wird.
Die Methodendefinition getUsers() muss der in dem Interface definierten Methode entsprechen. Ebenso habe ich die Klasse in DbUsersProvider umbenannt-
Wir können die Klasse Users jetzt so anpassen das usersProvider vom Typ des Interfaces ist:
IUsersProvider usersProvider = new DbUsersProvider();
Da die "ist ein " Regel besagt das ein Object der Abgeleiteten Klasse (DbUsersProvider) auch immer ein Typ der Basisklasse (hier das Interface IUsersProvider) ist. Wir können somit die Instanz in einer Variable des Typs IUsersProvider speichern.
Das hat den Vorteil, das diese Variable alle Objecte von Klassen die dieses Interface implementieren aufnehmen kann.
Code:
public class Users {
private String[] users;
public Users() {
IUsersProvider usersProvider = new DbUsersProvider();
users = userProvider.getUsers();
}
public void print() {
if(users==null) return;
for(int i =0; i < users.length; i++)
System.out.println(users[i]);
}
public static void main(String[] args) {
Users users = new Users();
users.print();
}
}
Das Problem: Noch immer müssen wir den Hinteren Teil der Instanzierung von
usersProvider abändern wenn eine andere Klasse genutzt werden soll. Java bietet
die schöne Möglichkeit anhand eines Klassennamens (vom Typ String) eine Klasse zu instanzieren.
Dies geschieht durch:
Class.forName([vollständiger Klassename]).newInstance();
Dies kommt uns zu gute, weshalb wir die zu Instanzierende Klasse die das Interface implementiert zur Laufzeit bestimmen können.
Ich habe den Klassennamen in ein Property File ausgelagert.
Code:
# Datei [PROJECTROOT]/src/de/tutorials/richtig/Users.propery
users.provider=de.tutorials.richtig.DbUsersProvider
Also ändern wir die Klasse Users so um das die im Propertys File definierte Klasse instanziert wird.
Code:
package de.tutorials.richtig;
import java.io.InputStream;
import java.util.Properties;
public class Users {
private String[] users;
public Users() throws Exception {
Properties props = new Properties();
InputStream stream
= getClass().getResourceAsStream("Users.properties");
props.load(stream);
String className = (String) props.get("users.provider");
IUsersProvider usersProvider = (IUsersProvider)
Class.forName(className).newInstance();
users = usersProvider.getUsers();
}
public void print() {
if(users==null) return;
for(int i =0; i < users.length; i++)
System.out.println(users[i]);
}
public static void main(String[] args) {
try {
Users users = new Users();
users.print();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//users.print();
}
}
Damit bestimmt der Eintrag in der Properties Datei welche Klasse (die das Interface implementieren muss) die Daten liefert.
Dies ist äusserst Modular, und die Art wie mann Pluginmechanismen schreiben kann um seine Anwendung leicht erweiterbar zu designen.
Um diese Modularität zu testen, schreiben wir noch eine Klasse DebugUsersProvider
Code:
package de.tutorials.richtig;
public class DebugUsersProvider implements IUsersProvider {
/* (non-Javadoc)
* @see de.tutorials.richtig.IUsersProvider#getUsers()
*/
public String[] getUsers() {
String[] users = new String[2];
users[0] = "user0";
users[1] = "user1";
return users;
}
}
Wir ändern den Eintrag in der Users.property entsprechend um:
Code:
users.provider=de.tutorials.richtig.DebugUsersProvider
Beim Test wird wie erwartet ausgegeben:
user0
user1