Parametrar till konsolprogram

Jag har skrivit en hel del program i arbetet och de allra flesta är konsolprogram, alltså program utan ett grafiskt gränssnitt.

Ganska sällan fungerar dessa utan parametrar eftersom utan parametrar blir ju programmen ganska statiska.

I Main()-proceduren får vi med oss alla parametrar som en array av string.


static void Main(string[] args)

Man kan säkert göra på en massa olika sätt, jag har vanligtvis loopat mig igenom  arrayen och med en switch tilldelat olika variabler ett värde. I slutet har man kollat så att alla nödvändiga variabler har ett värde som ser hyfsat ut. Ett av problemen är t.ex. att ta hand om olika sätt att presentera variablerna: -var, /var, –var, -var=value, -var:value och alla möjliga kombinationer av dessa.

Jag har nu hittat ett alternativ och en twist. Alternativet heter NDesk.Options. Det är på inget sätt nytt men det är faktiskt nytt för mig. Jag är inte van vid att söka efter ”plugins” eller vad man ska kalla det. Nå, NDesk.Options skapar förutsättningar för att hantera parametrar – argument – på ett väldigt bra sätt.

Man skapar s.k. OptionSets och när man parsar args så görs saker automagiskt i bakgrunden. Ett OptionSet skapas så här:

var p = new OptionSet () {
            { "n|name=", "the {NAME} of someone to greet.",
              v => names.Add (v) },
            { "r|repeat=", 
                "the number of {TIMES} to repeat the greeting.\n" + 
                    "this must be an integer.",
              (int v) => repeat = v },
            { "v", "increase debug message verbosity",
              v => { if (v != null) ++verbosity; } },
            { "h|help",  "show this message and exit", 
              v => show_help = v != null },
        };

Fast – och här kommer twisten – det finns ett ännu enklare sätt. Genom att använda NDesk.Option.Extensions blir riktigt enkelt.

var ops = new OptionSet();

var names = ops.AddVariableList<string>("n|name", "Name to greet.");
 var greet = ops.AddVariable<string>("g|greeting", "The greeting");
 var hlp = ops.AddSwitch("h|help", "Show this message.");

Förutom de tre funktionerna ovan finns även

AddVariableMatrix<T>

som returnerar en Key/Value collection. Mer hjälp kan få på deras GitHub.

NDesk.Option ordnar till det så fint att ”n|name” gör att alla kombinationer av dessa parametrar är OK utan att vi behöver bry oss om det; -n, -name –name, /n etc. Den fixar även parametervärdet, d.v.s. /n:Kalle eller –name=Kalle. Inte heller det behöver vi ta hand om själva.

En stor eloge till teamen bakom både NDesk.Options och NDesk.Options.Extensions!

Exempel:

 static void Main(string[] args)
 {
 var ops = new OptionSet();

 var names = ops.AddVariableList<string>("n|name", "Name to greet.");
 var greet = ops.AddVariable<string>("g|greeting", "The greeting");
 var hlp = ops.AddSwitch("h|help", "Show this message.");
 
 var res = ops.Parse(args);

 if (hlp.Enabled) ShowHelp(ops);
 
 foreach(var n in names)
 {
 Console.WriteLine(string.Format("{0} {1}",greet.Value,n));
 }
 
 }

 private static void ShowHelp(OptionSet p)
 {
 Console.WriteLine("Usage: greet [OPTIONS]+ message");
 Console.WriteLine("Greet a list of individuals with an optional mes
 Console.WriteLine();
 Console.WriteLine("Options:");
 
 p.WriteOptionDescriptions(Console.Out);
 }

En smart grej här är

p.WriteOptionDescriptions(Console.Out); 

Det tar med sig den andra parametern i vår .AddVariable<>(); så man behöver inte hantera det i sin hjälpprocedur.

Parametrar till konsolprogram

Factories

Jag håller på att skriva en liten övervakningsutility för databaser. Den ska i praktiken ställa frågor som ger ett numerärt svar och sedan jämför jag det svaret med förutbestämda tröskelvärden. Om det inte stämmer skickar jag larm på något sätt.

Frågorna ser ut ungefär så här:

 SELECT COUNT(messages) FROM SomeTable WHERE status='Not sent' 

En feature som jag tänker mig, mest för att det är kul, är att kunna använda olika respositories, eller förråd, för mina frågor. Till en början ska man kunna använda sig av databasen SQLite eller XML om man vill ha textbaserad konfiguration. Naturligtvis vill jag att man ska kunna bygga ut med t.ex. SQL Server, Oracle eller varför inte en helt vanlig textfil. Det viktiga här är att det kanske inte är jag som kodar den modulen så jag kan inte lägga in stöd för den i min huvudassembly eftersom den inte existerar än.

För att kunna göra så använder man sig av en Factory. En factory skapar ett objekt som inte finns från början och instansierar det som en vanlig klass.

Eftersom man vet vilket objekt man tänker jobba med kan man ju inte heller instansiera det som en känd klass så då måste man även använda sig av Interface, antingen ett man skapar själv eller ett ”färdigt”

Ex.

 

Jag skapar ett Interface som heter IDataSource. Det innehåller en Property och en procedur utan return type (void).


public interface IDataSource
{
string ReturnData { get; set; }
void ConnectToDataSource();

}

I en annan assembly – en .dll – finns en klass som implemeterar interfacet.


public class FetchData :IDataSource
{
 public string ReturnData { get; set; }

 public SQLiteConnection Connection { get; set; }

 public void GetData()
 {
 string sqlString = "SELECT name FROM table1";

 var cmd = Connection.CreateCommand();
 cmd.CommandText = sqlString;

 cmd.Connection = Connection;

 var reader = cmd.ExecuteReader();

 while (reader.Read())
 {
 ReturnData = reader["name"].ToString();

 }
 }

 public void ConnectToDataSource()
{
 SQLiteConnection _sqLiteCon = new SQLiteConnection("Data source=c:\\temp\\ConnectTest.sqlite; Version=3");

 Connection = _sqLiteCon;

 using (Connection)
 {
 Connection.Open();

 Console.WriteLine("SQLite connection state: {0}", Connection.State);

 GetData();

 }

 }
}

Jag skapar en Factory:

class DataFactory
 {
 public static IDataSource GetConnectionType()
 {

 string typeName = ConfigurationManager.AppSettings["RepositoryType"];

 Type repoType = Type.GetType(typeName);

 object repoInstance = Activator.CreateInstance(repoType);

 IDataCource conn = repoInstance as IDataSource;

 return conn;
 }
 }

Min Main()-procedur ser ut så här:

 IDataSource conn = DataFactory.GetConnectionType();

 conn.ConnectToDataSource();

 Console.WriteLine(conn.ReturnData.ToString());

 Console.WriteLine("----");

Slutligen måste jag också tala om för factoryn vilken typ av obekt den ska skapa. Det kan man göra hur man vill men jag har valt att göra det i min App.config:

<add key="RepositoryType" value="ConnectDB.SQLite.GetSQLiteData, ConnectDB.SQLite, Version=1.0.0.0, Culture=neutral"/>

Om vi nu tar det från början hoppar jag alltså in i Main(),
ber min Factory att skapa ett nytt IDataSource-objekt

IDataSource conn = DataFactory.GetConnectionType();

I factory-proceduren tar vi reda på vilken typ av objekt vi ska skapa och var vi ska leta.

//Först läser vi App.config och tar reda på vilket objekt vi ska skapa
string typeName = ConfigurationManager.AppSettings["RepositoryType"];
//Vi skapar objektet men det är ännu inte instansierat
 Type repoType = Type.GetType(typeName);

//Här skapar vi en ny instans av objektet
 object repoInstance = Activator.CreateInstance(repoType);

// Vi castar det till en typ vi kan hantera med vårt interface
 IDataSource conn = repoInstance as IDataSource;

//Vi returnerar objektet, instansierat och klart
 return conn;

Tillbaka i vår Main():

conn.ConnectToDataSource();

Eftersom vi har en instans av vårt objekt kan vi också använda oss av de procedurer och properties som objektet visar upp. I det här fallet en void som skapar en koppling till databasen, läser data och skriver det till propertyn ReturnData.

 public void ConnectToDataSource()
 {
//Skapa upp kopplet mot databasen
 SQLiteConnection _sqLiteCon = new SQLiteConnection("Data source=c:\\temp\\ConnectTest.sqlite; Version=3");

 Connection = _sqLiteCon;

 using (Connection)
 {
// Öppna kopplet mot databasen
 Connection.Open();

 Console.WriteLine("SQLite connection state: {0}", Connection.State);

//Hoppa vidare till proceduren GetData()
 GetData();

 }

 public void GetData()
 {

 string sqlString = "SELECT name FROM table1";

 var cmd = Connection.CreateCommand();
 cmd.CommandText = sqlString;

 cmd.Connection = Connection;

//Kör SQL-frågan i databasen
 var reader = cmd.ExecuteReader();

 while (reader.Read())
 {
//Tilldela propertyn ReturnData värdet från databasen
 ReturnData = reader["name"].ToString();

 }
 }</pre>
<pre>

I Main() kan vi nu läsa värdet i ReturnData och skriva det till console:


Console.WriteLine(conn.ReturnData.ToString());

 

Viktigt att veta är att vi har kört konstruktorn för klassen – objektet – vi har skapat. Hade vi känt till att objektet vi söker finns i namespacet ConnectDB.SQLite och hette FetchData hade vi från vår Main()-procedur helt enkelt gjort


using ConnectDB.SQLite;

FetchData conn = new FetchData();

så hade vi haft samma objekt. Problemet är ju att vi inte vet exakt vad vi vill ha eller var det finns.

Nu finns all SQLite-specifik kod i ConnectDB.SQLite.dll.  Jämför gärna med FetchData för XML-objektet ConnetDB.XML.dll:

 class FetchData : IDataSource
 {

 public string ReturnData { get; set; }

 public void ConnectToDataSource()
 {

 // There's nothing to connect to here so we just move on to data retrieval.
 GetData();

 }

 protected void GetData()
 {
 XDocument doc = XDocument.Load("C:\\Temp\\ConnectTest.xml");

 var names = from x in doc.Descendants("root")
 select x.Element("name").Value;

 foreach (var name in names)
 {
 ReturnData = name;

 }
 }

 }

I och med att vi VET att proceduren implementerar interfacet IDataSource vet vi också att det finns en procedur som heter ConnectDataSource och en Property som heter ReturnData. Vad ConnectDataSource gör eller hur man tilldelar ReturnData ett värde bryr vi oss inte om i vår Main-procedur.

Factories