OOD/Java Interfaces / Plugin Programmierung

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:

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
 
Re: OOD/Java Interfaces

Original geschrieben von Christian Fein
Oft höre ich die Frage, was bringen einem Interfaces.

Ein typisches Beispiel für Interfaces sind einfach Listener.
Man nehme z.B. eine Anwendung mit drei Fenstern - ein Hauptfenster und zwei Unterfenster, die gleichzeitig geöffnet sein können. (ob Fenster oder Dialog ist egal). Nun sind z.B. Daten für Anzeigen in den Unterfenstern im Hauptfenster änderbar.
Dann konstruiere ich mir ein Interface DataChangeListener mit einer Methode dataChanged(). Danach lege ich im Hauptfenster eine (leere) Liste aller DataChangeListener an. Anschliessend implementiere ich in den Unterfenstern das Interface DataChangeListener mit der Methode dataChanged(). Dabei löse ich in dem dataChanged() des ersten Unterfensters z.B. eine Neuberechnung einer Tabelle aus und im zweiten Unterfenster ein Neuzeichnen eines Diagramms. Dann werden die beiden Unterfenster als DataChangeListener im Hauptfenster der Liste der DataChangeListener hinzugefügt.
Damit ist schon fast die ganze Arbeit getan. Nun nur noch eine Methode im Hauptfenster implementieren, die bei allen DataChangeListenern (in der Liste) das dataChanged() aufruft. Üblicherweise bekommt sie einen bezeichnenden Namen wie fireDataChanged().
Nun brauche ich lediglich an jeder gewünschten oder erforderlichen Stelle im Hauptfenster, wenn sich relevante Daten ändern (also wenn Daten im Hauptfenster neu geladen, editiert oder gelöscht werden z.B.), die auch die Unterfenster betreffen, ein fireDataChanged() aufrufen und die Unterfenster werden aktualisiert.

Das mag auf den ersten Blick etwas umständlich aussehen, hat aber zwei immense Vorteile:
1. Als Listener im Hauptfenster kann sich JEDES beliebige Objekt registrieren, was das Interface DataChangeListener implementiert. D.h. es müssen keine zwei Unterfenster sein, die sich als DataChangeListener im Hauptfenster registrieren!
2. Jedes weitere Unterfenster kann ich ohne große Mühe ebenfalls als Listener im Hauptfenster einhängen, ohne irgendwelche Verzwickungen zu stören o.ä.

Wem bei meinem kleinen Exkurs oben etwas aufgefallen ist:
Diese fire...Changed()-Methoden kommen immer wieder vor. Man sehe z.B. fireTableDataChanged(), was immer wieder im TableModel einer Tabelle aufgerufen wird und genau nach dem o.a. Schema arbeitet und alle Listener informiert.

Ich hoffe, das Prinzip ist jetzt etwas klarer geworden.
 
Hi Snape...

was du da beschreibst wollt ich auch immer schonmal können....
Hast du dazu BeispielCode?
 
Hi,
so auf die Schnelle ein Grundgerüst nicht, die Klassen sind schon ordentlich aufgebläht. Aber das wäre ein prima Thema für ein Tutorial. Wenn Du Glück hast und ich Pech (Jobabsage), lasse ich mich vielleicht dazu hinreißen, je ein Tut für Listener und für TableRows zu schreiben.
Vielleicht probierst Du es bis dahin nach meiner o.a. Anleitung, und wenn Du nicht weiter weißt oder Probleme bekommst, frag hier einfach nach.
 
dafür gibts ne nette klasse: java.beans.PropertyChangeSupport, die per Delegation eingebracht wird. in swing wird die bereits in Component eingeführt. Trotzdem gibts einen separaten Mechanismus für ActionListener s?! naja.

hier ist ein beispielcode für das ActionListener interface.
swing kenn ich net, hab nur kurz ein beispiel aus einem sun tut angepasst.

Code:
package main;

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class Main {

	public class MyJFrame extends JFrame implements ActionListener {
		private static final long serialVersionUID = 1L;

		private JLabel label = new JLabel("no data");

		public MyJFrame() {
			add(label);
		}

		public void actionPerformed(ActionEvent e) {
			if( e.getSource() instanceof JTextComponent ) {
				JTextComponent tf = (JTextComponent) e.getSource();
				label.setText(tf.getText());
			}
		}
	}

	protected JTextField textField;

	protected JButton addChildButton;

	protected int nextXPos = 100;

	protected JFrame frame;

	protected void addChildFrame() {
		MyJFrame childFrame = new MyJFrame();
		childFrame.setLocation(nextXPos, 100);
		nextXPos += 60;
		childFrame.pack();
		childFrame.setVisible(true);
		textField.addActionListener(childFrame);
	}

	protected JButton getAddChildButton() {
		if (addChildButton == null) {
			addChildButton = new JButton("add child widget");
			addChildButton.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent e) {
					addChildFrame();
				}
			});
		}
		return addChildButton;
	}

	public Main() {
		frame = new JFrame("ListenerExample");
		frame.setLayout(new GridLayout(2, 1));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		textField = new JTextField(20);
		frame.getContentPane().add(textField);
		frame.getContentPane().add(getAddChildButton());

		frame.pack();
		frame.setVisible(true);
	}

	public static void main(String[] args) {
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				new Main();
			}
		});
	}

}

stripped down code von JTextField
Code:
    public synchronized void addActionListener(ActionListener l) {
        listenerList.add(ActionListener.class, l);
    }

    protected void fireActionPerformed() {
        Object[] listeners = listenerList.getListenerList();
        // other stuff

        for (int i = listeners.length-2; i>=0; i-=2) {
            if (listeners[i]==ActionListener.class) {
                ((ActionListener)listeners[i+1]).actionPerformed(e);
            }          
        }
    }

erklärung siehe post von Snape.

eine variation des themas: der datenaustausch kann entweder direkt erfolgen( dann ist der neue Wert direkt in der Signatur, siehe PropertyChangeSupport) oder indirekt, indem der Listener nur erfährt, dass sich das observierte Objekt geändert hat (siehe code).

btw hallo forum :)
 
Zurück