Laden...

Übermittelung generischer Bedingungen bei Verarbeitung von Datensätzen

Erstellt von ModelViewPresenter vor 11 Jahren Letzter Beitrag vor 11 Jahren 4.183 Views
ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren
Übermittelung generischer Bedingungen bei Verarbeitung von Datensätzen

Hallo,

es geht bei mir um Folgendes: ich lese eine CSV-Datei ein. Auf diesen Zeilen sollen Prüfungen auf gewisse Bedingungen ausgeführt werden. Sei Spalte 1 ein Zahlenwert, dann könnte man beispielsweise prüfen, WENN Spalte1.Wert > xy, DANN tue dies. Das "dies" bedeutet in meinem Fall, schreibe genau diesen Datensatz, bei dem die Bedingung erfüllt ist, in eine andere CSV-Datei.

Ich könnte natürlich nun hergehen, eine Klasse schreiben, die die Zeilen liest und Methoden, die auf jeder dieser Zeilen die Prüfungen ausführen und entsprechend die Daten wegschreiben. Das möchte ich aber nicht, sondern ich möchte diese Prüfungen generisch hinzufügen oder entfernen können.

Die Frage ist, wie macht man das am besten?

Da man hier keine Tipps bekommt, ohne vorher selbst Ideen zu haben. Hier kurz, was ich mir überlegte:

Ich dachte an etwas wie den Java-Executor-Service oder eine Art Command Pattern, wobei ich die Befehle in einer Liste ablege. Ich müsste dann auf jeder gelesenen CSV-Zeile alle Prüfungsbefehle ausführen. Diese Befehle müssten dann Schreibmethoden auf einem entsprechenden CSV-Schreiber ausführen. Was für diese Idee spricht, ist die Einfachheit, mit der sich der Vorgang parallelisieren lässt, denn ich erwarte CSV-Files mit mehreren Millionen Einträgen. Klar, auch das sollte sequentiell noch recht fix gehen, aber man weiß ja nie... Jedenfalls könnte ich dann in einer Queue die Zeilen/Befehle ablegen und in einer Producer-Consumer-Verfahren abarbeiten.

Bin auf eure Anregungen gespannt
MVP

F
10.010 Beiträge seit 2004
vor 11 Jahren

Wo ist da jetzt das Problem?

Beim anlegen einer List<IValidationCommand>()?
Beim erzeugen der Queue?
Bei Validationrules?
Serialisieren der Rules in/aus XML?

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Wo ist da jetzt das Problem?

Problem grundsätzlich keins. Ich wollte nur wissen, ob das, was ich vorhabe, good oder bad practice ist. Die Umsetzung sollte dann kein großes Problem sein.

Serialisieren der Rules in/aus XML?

Ist eine sehr gute Idee.

2.187 Beiträge seit 2005
vor 11 Jahren

Hallo ModelViewPresenter,

zuerst musst du einen Datentyp wählen um die Zeile zu repräsentieren. Ich Empfehle string[] oder eine eigene Klasse/Interface das einen Indexer hat der string zurück gibt. (Bei den Beispielen nehm ich string[] an.)

Als nächstes brauchst du eine Lise für die Bedingungen. Je nach dem ob du die auszuführen Aktion je Bedinung festlegen willst oder immer die gleiche Ausführst ein Dictionary oder eine Liste.
Man sollte die generische Variante der Liste/Dictionary verwenden, also muss man auch den Typ der Bedingung festlegen. Das .Net-Framework bietet hier zu System.Predicate<T> an aber eine eigen Klasse zu schreiben wäre auch nicht verkehrt.


IDictionar<Predicate<string[]>,Action<string[]>> dictionary = new Dictionar<Predicate<string[]>,Action<string[]>>();
...
foreach(string zeile in File.ReadAllLines(...))
{
  string[] array = Split(zeile); // Trenner, Leerzeilen, etc.
  foreach(Predicate<string[]> predikat in dictionary.Keys)
  {
    if(predikat(array)) dictionary.Value(array);
  }
}

Der interesantere Teil ist die Bedingungen zu laden. Ich würde hier auf die *.config-Datei gehen und es selbst generieren. Und man sollte sich vorher überlegen ob man eine kleine Boolsche-Algebra (Nicht, Und, Oder) in die Konfiguration einbaut oder nicht.

Gruß
Juy Juka

C
224 Beiträge seit 2009
vor 11 Jahren

Hi ModelViewPresenter,

Deine Idee finde ich gut.

So würde ich es machen:
Ich würde eine Spalten-CSV anlegen.
In dieser steht, welche Spalte eine Nummern-, Datums-, ..., Textspalte ist.

Dann würde ich die Daten-CSV Datei laden.
Der jeweilige Datentyp muss dann der entsprechenden Spalte aus der Spalten-CSV entsprechen.

Eventuell kann man statt der Spalten-CSV Datei auch eine Assembly Datei laden.
Aber um so komplizierter etwas ist, um so anfälliger ist es.

Gruß,
CoLo

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Hallo!

Danke für die wertvollen Tipps. Ich überlege ich gerade, wie ich den Prozess sauber beeden kann.

Ich programmiere aktuell in Java und nicht in C#, darum kann ich die Delegatenlösung nicht verwenden. Weil die Kompetenz hier höher ist als in den deutschsprachigen Java-Foren, schreibe ich aber lieber hier.

Was ich also aktuell tue ist, dass ich ein "Finished" in die Queue schiebe. Auf dieses wird dann wieder eine ValidationCommand ausgeführt, was prüfen muss, ob der Inhalt "Finished" ist, wenn ja feuert der Invoker, das ExecutionCommand zum Beenden.


public class Producer implements Runnable {

	private BlockingQueue<String[]> lineQueue;
	private CSVReader csvReader;

	public Producer(BlockingQueue<String[]> lineQueue, CSVReader csvReader) {
		this.lineQueue = lineQueue;
		this.csvReader = csvReader;
	}

	public void run() {
		try {
			while (csvReader.getNextLine() != null) {
				lineQueue.put(new String[] { "bla", csvReader.getValueByName("GEBURTSTAG") });
			}

			lineQueue.put(new String[] { "Finished" });
			csvReader.closeStream();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

public class Consumer implements Runnable {

	private BlockingQueue<String[]> lineQueue;
	private AbstractInvoker<String[]> commandInvoker;

	public Consumer(BlockingQueue<String[]> lineQueue, AbstractInvoker<String[]> commandInvoker) {
		this.lineQueue = lineQueue;
		this.commandInvoker = commandInvoker;
	}

	public void run() {
		boolean isRunning = true;

		while (isRunning) {
			String[] lineItem = null;
			try {
				lineItem = lineQueue.take();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if (lineItem[0].equals("Finished")) {
				isRunning = false;
				commandInvoker.invokeCommand(new String[] { "Finished" });
			} else {
				commandInvoker.invokeCommand(lineItem);
			}

		}
	}
public class FireEndPredicate implements IValidationPredicate<String[]> {

	public boolean validate(String[] obj) {
		return obj[0].equals("Finished") ? true : false;
	}

}
public class FireEnd implements IExecutionCommand<String[]> {

	private CSVWriter myWriter;
	private OutputStream outStream;

	public FireEnd(OutputStream outStream) throws IOException {
		this.outStream = outStream;

		myWriter = new CSVWriter(outStream);

	}

	public void execute(String[] obj) {
		myWriter.closeStream();
		System.out.println("END");
	}
}


public class Invoker<T> extends AbstractInvoker<T> {
	/*
	 * (non-Javadoc)
	 * 
	 * @see 
	 */
	@Override
	public void invokeCommand(T parameter) {
		for (IValidationPredicate<T> string : getPredicateMap().keySet()) {
			if (string.validate(parameter)) {
				getPredicateMap().get(string).execute(parameter);
			}
		}
	}

}

Was mich daran stört, ist der Aufwand, der betrieben werden muss, nur um den Stream zu closen.

Geht das auch besser? Ist es sinnvoller, dies nicht als ExecutionCommand auszuführen? Wenn ja, müsste der Consumer eine Referenz auf den Stream erhalten, um das Closen anzustoßen, was aber irgendwo ein wenig gegen das Grundkonzept verstoßen würde, oder nicht?

Vielen Dank
MVP

5.742 Beiträge seit 2007
vor 11 Jahren

Hallo MVP,

wenn du tatsächlich action-spezifische Prädikate verwendet, würde ich diese nicht in einer separaten Klasse implementieren, sondern gleich in die Klasse packen.

Statt eines "Magic-Strings" würde ich zum Beenden eher ein leeres Array verwenden.

Der Producer könnte dann am Ende ein "Event" feuern (unter Java halt einen Observer benachrichtigen), woraufhin dann der Stream geschlossen wird; dies muss IMHO nicht in einer Aktion erfolgen.

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Hi winSharp93,

das mit den Magic Strings ist natürlich richtig.

wenn du tatsächlich action-spezifische Prädikate verwendet, würde ich diese nicht in einer separaten Klasse implementieren, sondern gleich in die Klasse packen.

Was meinst du an dieser Stelle mit "gleich in die Klasse packen"?

Der Producer könnte dann am Ende ein "Event" feuern (unter Java halt einen Observer benachrichtigen), woraufhin dann der Stream geschlossen wird; dies muss IMHO nicht in einer Aktion erfolgen.

Wäre sicher auch eine gute und saubere Lösung. Was den Faktor "Aufwand" angeht, ist der nicht geringer als derjenige beim Event "feuern". (Da ich den Observer in Java leider mit Interfaces implementieren muss)

5.742 Beiträge seit 2007
vor 11 Jahren

Was meinst du an dieser Stelle mit "gleich in die Klasse packen"?

In deinem Beispiel würde ich FireEndPredicate und FireEnd in eine Klasse packen (abgesehen davon, dass es ja durch das "Event" ersetzt wird).

Weiterer Vorteil bei dem Observer ist, dass du mehrere Aktionen am Ende durchführen kannst (z.B. Benachsrichtigung des Users, Schließen der Streams, Anstoßen der weiteren verarbeitung, ...)

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Kann man wohl machen. Widerstrebt aber sicher dem Single Responsibility Prinzip.

Weiterer Vorteil bei dem Observer ist, dass du mehrere Aktionen am Ende durchführen kannst (z.B. Benachsrichtigung des Users, Schließen der Streams, Anstoßen der weiteren verarbeitung, ...)

Da gebe ich dir recht.

2.187 Beiträge seit 2005
vor 11 Jahren

Hallo ModelViewPresenter,

anstelle der Delegaten gehen natürlich auch Interfaces.

Und wegen "Single Responsibility Prinzip": Sie es doch ehr so, dass die Klasse nicht für das Schließen verantwortlich ist, sondern für die Steuerung des Ablaufs, also der Klasse die wiederum für das Schließen verantwortlich ist rechtzeitig bescheid geben muss (ob über Methodenaufruf oder Event oder etc. ist hier dann ehr eine Frage wie lose oder eng man koppeln möchte).

Gruß
Juy Juka

5.742 Beiträge seit 2007
vor 11 Jahren

@JuyJuka: Ich glaube das mit dem SRP war mehr auf den ersten Absatz bezogen.

Widerstrebt aber sicher dem Single Responsibility Prinzip

Klar - man kann jede Methode in ihre eigene Klasse packen 😉
In diesem Fall würde allerdings eher zu einer Funktionseinheit im Sinne besserer Kohärenz tendieren.

Klar - sobald manche Regeln wiederverwendet werden bzw. manche Aktionen mit unterschiedlichen Regeln zusammen eingesetzt werden, macht eine Trennung mehr als Sinn.
Solange dies jedoch nicht der Fall ist, schafft eine Trennung IMHO mehr Nachteile als Vorteile.

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Ich möchte das Thema noch einmal kurz aufleben lassen. Wie eingangs sagte, möchte ich je nach Bedingung eine andere Aktion ausführen, die einem Fall darin besteht, die gelesen Zeile in eigene separate CSV-Datei abzulegen. Ich möchte allerdings auch den Head schreiber. Beim Header handelt es sich allerdings um einen Spezialfall, da dieser nicht unbedingt validiert werden muss.

Ich sehe da zwei Möglichkeiten:

  1. Um konsequent zu bleiben, lege ich den Header in die Queue. Natürlich würde beim Nehmen aus der Queue dieser Header von allen Prädikaten abgeprüft. Demnach müsste ich eine eigenes Prädikat samt Aktion für den Header vorhalten. Dann stellt sich die Frage, wie mache ich in einem String[]-Objekt sauber kenntlich, dass es ein Header ist.

  2. Im Konstruktor des Schreiber-Ojektes (in jede CSV-Datei braucht ein eigenes) führe ich eine Methode aus, die den Konstruktor schreibt. Damit entziehe ich das Header-Schreiben dem Validierungsprozess. Der Nachteil wäre, dass ich dem Schreiber-Objekt von außen mitteilen müsste, wie der Header aussieht. Ich müsste alles quasi "drumherum" für den Header einen eigenen Lese-Schreib-Prozess anlegen, der abseits aller Validierungs- und Postprozesse läuft.

5.742 Beiträge seit 2007
vor 11 Jahren

Hallo ModelViewPresenter,

wovon hängt denn der Inhalt des Headers ab?

Von den Aktionen, die rein schreiben?
Hat jede Aktion quasi ihren eigenen Header?
Oder kann eine einizige Aktion quasi auch in mehrere CSV-Dateien schreiben, wobei der Eintrag dann aber je nach Header unterschiedlich aussieht (d.h. unterschiedliche Felder enthält)?

Im ersten Fall - wenn jede Aktion ihre eigene CSV Datei hat - wäre es folglich Aufgabe der Aktion ihre CSV-Datei entsprechend vorzubereiten.

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Alle sollen den gleichen Header haben. Dieser wird vorgegeben von der Datei, aus der gesplittet wird.

Beispiel:

Personen.csv
Name, Status (=> Header)
Werner, 5
Meyer, 5
Schwarz, 6

Als Ergebnis würde ich dann, wenn ich nach Status prüfe Folgendes haben wollen:

Personen_mit_Status5.csv
Name, Status (=> Header)
Werner, 5
Meyer, 5

Personen_mit_Status6.csv
Name, Status (=> Header)
Schwarz, 6

In diesem Falle hätte ich zwei Aktionen: die eine, die in "Personen_mit_Status5.csv" schreibt und die andere in "Personen_mit_Status6.csv". Dabei können die Daten in Feldern jeweils unterschiedlich nachbereitet werden. (Erstmal nebensächlich) Ich habe somit zwei unterschiedliche Outputstreams, die zwar für sich unterschiedliche Datensätze schreiben, aber am Anfang den gleichen Header schreiben sollten.

5.742 Beiträge seit 2007
vor 11 Jahren

Ich habe somit zwei unterschiedliche Outputstreams, die zwar für sich unterschiedliche Datensätze schreiben, aber am Anfang den gleichen Header schreiben sollten.

So in etwa würde ich das wohl modellieren:


public interface IAction<T> {
   void Execute(T data);
}
//...
public interface IWriter<T> {
   void WriteEntry(T item);
}
//...
class MyAction : IAction<Person> {
   public MyAction(IWriter<Person> writer) //...

   public void Execute(Person person) {
       //Do something
       this._writer.Write(person);
   }
}

//...
var firstWriter = new Writer<Person>(stream);
firstWriter.addColumn(p => p.FirstName, "Vorname");
firstWriter.addColumn(p => p.LastName, "Nachname");
//...

var fooAction = new FooAction(firstWriter);
var fooAction2 = new FooAction(secondWriter);
var barAction = new BarAction(thirdWriter);

So hättest du die Aufgabengebiete a) Regeln (hier nicht dabei) b) Aktionen und c) Persitierung getrennt, wobei - wenn die Aktionen nur aus "writer.Write(person)" bestehen würden, auch mit den Writern zusammengelegt werden könnten.

Ein Writer kriegt dabei mitgeteilt, welche Spalten er jeweils schreiben soll und generiert anhand dieser Informationen den Header selbst; dabei wissen die Aktionen nichts davon, dass letztlich CSV erzeugt wird bzw. welche Spalten relevant sind.

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Danke, ich seh da ein Mapping. Was mich interessieren würde ist, wann und wie du Spalteüberschriften "Vorname" und "Nachname" konkret schreibst. Das ist ja was anderes als in eine Datenbank zu schreiben, weil dort Tabellen mit Überschriften schon vorhanden sind und ich diese nicht selbst anlege.

5.742 Beiträge seit 2007
vor 11 Jahren

Danke, ich seh da ein Mapping. Was mich interessieren würde ist, wann und wie du Spalteüberschriften "Vorname" und "Nachname" konkret schreibst.

Entweder, wenn das erste Objekt geschrieben wird oder man designt das so, dass man eine spezielle EndInit-Methode hat, die das erledigt und die zwingend aufegerufen werden muss.

Man könnte auch ganz fancy eine Factory mit Fluent-Interfaces schreiben:


var writer = writerFactory.create<Person>(stream)
   .WithMapping(p => p.FirstName, "Vorname")
   .WithMapping(p => p.Birthday.ToShortDateString(), "Geburtstag")
   .Create();

(Hier würde das die Create-Methode übernehmen).

Wichtig ist aber jedenfalls, dass der Writer das schreibt, da in diesem hinterlegt ist, welche Spalten überhaupt benötigt werden; in der Aktion (sofern diese wie gesagt noch komplexere Aufgaben durchführt), wäre diese Information hingegen nicht unbedingt sinnvoll aufgehoben.

ModelViewPresenter Themenstarter:in
67 Beiträge seit 2011
vor 11 Jahren

Entweder, wenn das erste Objekt geschrieben wird oder man designt das so

Dann müsste ich mir natürlich irgendwo merken, ob es das erste Mal ist. Naiv gedacht, würde ich dazu irgendwo ein Flag setzen, was ich erstmal als unschön empfinde.

Man könnte auch ganz fancy eine Factory mit Fluent-Interfaces schreiben:

Kann ich - glaube ich - so mit Java-Bordmitteln erstmal nicht machen. Sicher könnte ich eine Factory schreiben, was sicher auch sinnvoll ist, da ich sehr viele Objekte bauen muss, welche z.T. per DI übertragen werden.

Übergebe nun vorerst dem Writer den Header, wobei der Writer dann beim Initialisieren den Header schreibt.