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
Wo ist da jetzt das Problem?
Beim anlegen einer List<IValidationCommand>()?
Beim erzeugen der Queue?
Bei Validationrules?
Serialisieren der Rules in/aus XML?
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.
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
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
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
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.
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)
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, ...)
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.
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
@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.
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:
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.
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.
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.
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.
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.
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.
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.
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.