Laden...

Mit welchem Pattern kann ich generische Links zu versch. (Finanz)Websites herstellen?

Erstellt von BlackMatrix vor 3 Jahren Letzter Beitrag vor 3 Jahren 889 Views
B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 3 Jahren
Mit welchem Pattern kann ich generische Links zu versch. (Finanz)Websites herstellen?

Hallo,

entschuldigt den etwas unpräzisen Betreff des Threads, aber mir ist keine treffende Beschreibung eingefallen.

Ich versuche kurz meine Gedankengänge für den folgende Problem darzulegen und erhoffe mir ein paar Anregungen eurerseits, da ich schon des Öfteren auf ähnliche Themen gestoßen bin.

Im aktuellen Fall gilt es Links zu unterschiedliche Finanzwebseiten bereitzustellen. Dabei ist der Eingabeparameter in den meisten Fällen ein Aktiensymbol, dass zu einem Link gewandelt werden soll. So wird aus MSFT (Aktiensymbol für die Microsoft Aktie) bei der Webseite Marketwatch zu https://www.marketwatch.com/investing/stock/msft.

Im einfachsten Fall würde man eine statische Methode anbieten, die die Erstellung macht:

public static class MarketWatchLinkCreator
{
    public static Uri CreateLink(string symbol)
    {
        return new Uri("https://www.marketwatch.com/investing/stock/" + symbol);
    }
}

Nun ist das zwar ganz nett, weil man keine Instanzen erzeugen muss für die Linkgenerierung, aber es wäre ja auch schön, wenn man weitere Finanzwebseiten unter einer einheitlichen Schnittstelle generieren könnte. So könnte man SeekingAlpha in dem Gerüst unterbringen:

public interface ISymbolLinkCreator
{
     Uri CreateLink(string symbol);
}

public class SeekingAlphaLinkCreator : ISymbolLinkCreator
{
    public Uri CreateLink(string symbol)
    {
        return new Uri("https://seekingalpha.com/symbol/" + symbol);
    }
}

Jetzt gibt es aber Webseiten, wo das Aktiensymbol alleine nicht ausreicht. Man braucht dort bspw. noch die Heimatbörse um einen Link generieren zu können. Jetzt könnte man weitere Parameter entweder über den Konstruktor der Implementierung reinreichen (finde ich unsauber), oder man erweitert ISymbolLinkCreator um einen generischen Parameter.

public interface ISymbolLinkCreator<TInput>
{
     Uri CreateLink(TInput input);
}

sodass TInput in diesem speziellen Fall eine Klasse aus Symbol und Heimatbörse wird.

Hat meiner Meinung nach einige Nachteile, ich habe irgendwie eine zu generische Benamung des Parameters und darf den auch laut einigen Code-Analyzern nicht in der Implementierung umbenennen. Ein "input" bleibt ein Input und wird besonders dann kritisch, wenn man z.B. zu Comdirect kommt. Dort kann man auch so einen Link generieren und man kann das gleiche Interface

ISymbolLinkCreator<string>

implementieren, jedoch ist dort "input" nicht das Aktiensymbol wie in 95% der anderen Fälle, sondern die ISIN. Das scheint für mich als Aufrufer verwirrend. Weiterhin bleibt die Instanzierung, die ich zum Schluss ansprechen möchte.

Angenommen, ich möchte alle Linkgeneratoren instanzieren und habe alle Daten da, wie würdet ihr eine Implementierung vornehmen um sowas zu vermeiden:


public class LinkCreator
{
    private readonly IDataService _dataService;

    public LinkCreator(IDataService dataService)
    {
        _dataService = dataService;
    }

    public IReadOnlyCollection<Uri> GetLinks(string symbol)
    {
        var links = new List<Uri>();

        var seekingAlphaLink = new SeekingAlphaLinkCreator().CreateLink(symbol);
        var marketWatchLink = new MarketWatchLinkCreator().CreateLink(symbol);

        var isin = await _dataService.GetIsinAsync(symbol);

        var comdirectLink = new ComDirectLinkCreator().CreateLink(isin);

        var homeExchange = await _dataService.GetHomeExchangeAsync(symbol);

        var furtherLink = new FurtherLinkCreator().CreateLink(new ComplexInput
        {
            HomeExchange = homeExchange,
            Symbol = symbol
        });

        links.Add(seekingAlphaLink);
        links.Add(marketWatchLink);
        links.Add(comdirectLink);
        links.Add(furtherLink);

        return links;
    }
}

Gerade was die Instanziierung angeht, habe ich mir überlegt, ob man das evtl. über eine Factory-Methode mit generischen Parametern anbietet und dann Activor.CreateInstance verwendet, aber man hätte 2 Typparameter verwenden müssen, was mir weniger intutiv vorkommt.

Vielleicht habt ihr ja eine Meinung. Ich wäre sehr dankbar 😃

T
2.219 Beiträge seit 2008
vor 3 Jahren

Klingt grob nach Factory Pattern.
Ich würde das ganze einfach mit einer Factory Klasse lösen, die dir direkt die entsprechende Uri liefert.
Dann brauchst du auch keine große Vererbung oder Interfaces.
Bei Vererbung hättest du je nach Menge der Möglichkeiten dann auch enorm viele Klassen anzulegen, pro ISIN eine nur um eine Uri zu erhalten.
Im einfachsten Fall kannst dann per switch die entsprechende Uri liefern.

Nachtrag:
Okay, da du mehr als eine Uri brauchst, ist der Ansatz nicht sinnvoll.
Dann macht eine saubere Vererbung doch mehr Sinn!
Ich würde dann ein generelles Interface + Factory Klasse für die konkrete Instanz über ISIN zu erhalten.

Dann brauchst du eine Factory Klasse mit einer Methode, die dein Interface liefert und die ISIN als Parameter bekommt.
In der Methode kannst du dann ein switch für die ISIN machen und dann per case ein return new BlubLink machen.
Ich würde die Links dann schon fix in den entsprechenden Konkreten Klasse hinterlegen, falls diese nicht dynamisch sein können.

Dann müsstest du nicht mal einen Konstruktor anlegen, da die Instanzen schon die Links kennen und liefern.
Entsprechend wären die Eigenschaften nur per get Erreichbar.


public interface ILink
{
    Uri SymbolUri { get; }

    Uri HomeUri { get; }
}

public class SeekingAlphaLink : ILink
{
    public Uri SymbolUri { get; } = new Uri (""https://seekingalpha.com/symbol/...");

    public Uri HomeUri { get; } = new Uri ("https://www.marketwatch.com/investing/stock/...");
}

public class LinkFactory
{
    public ILink CreateLink(string isin)
    {
        case "SeekAlphaLinkISIN";
            return new SeekAlphaLink();

        default:
            throw new ArgumentException(nameof(isin), "Unbekannt ISIN");
    }
}

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.806 Beiträge seit 2008
vor 3 Jahren

Im Falle von Aktien ist das gar nicht so einfach umsetzbar; leider. Dafür sind die Parameter (oft) zu verschieden.
Ich kommuniziere auch mit verschiedenen APIs für Aktien konsistent ist das halt leider nich so.
Viele Aktien und ETFs haben auch je nach handelnder Börse unterschiedliche Symbole; teilweise sogar auf API-Anbieter Ebene - und teilweise mit zusätzlichen Parametern.

Der Factory Pattern hilft hier leider nur bedingt, wenn man unterschiedliche Parameter-Strukturen hat; man könnte natürlich die Symbole in Form von Option-Interfaces als Parameter anbinden - da gibt es viele Wege.

Von der Frage, was die "einzige" passende Architektur ist muss man sich aber verabschieden.
Hier passen womöglich zig verschiedene Pattern.

T
2.219 Beiträge seit 2008
vor 3 Jahren

@Abt
Da ich leider nicht direkt mit Börsen Apis arbeite, war mir dies leider nicht bekannt.
Dann ist der Weg wirklich nur bedingt nutzbar.

Dann müsste man eigentlich überlegen, diese Informationen z.B. aus einer DB zu laden.
Dann bräuchte man auch nur eine Klasse implementieren, die dann entsprechend gefüllt wird.

Im einfachsten Fall könnte man dies über eine Anwendungslokale DB umsetzen.
Tabelle könnte dann z.B. als Key die ISIN haben und einmal Symbol und Home Uri als einfache Strings.
Dann muss man nur noch einmal alle einladen, in einem Dictionary mit ISIN als Key vorhalten.
Dadurch kann man sich auch die unmengen an möglichen Implementierungen sparen.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 3 Jahren

@T-Virus

Ich glaube du hast das mit deinem Codebeispiel etwas falsch verstanden. Der Grundgedanke ist immer aus einem Eingabeparameter wie etwa dem Aktiensymbol den entsprechenden (klickbaren) Link zu unterschiedlichen Finanzwebseiten zu generieren. Das können so keine Propertys wie in deinem Bsp. sein. Die ISIN ist nur ein weiterer bzw. anderer Inputparameter. Er ersetzt das Aktiensymbol, welches normalerweise in 95% der Webseiten notwendig ist.
Im 2. Schritt sollen dann alle Implementierungen aufgesammelt werden um eine Liste aller Links anbieten zu können.

Ich habe nun eine geeignete, brauchbare Lösung gefunden, wie man zumindest die einzelne Linkgenerierung sauber aufbaut. Man nutzt mein generisches Interface von oben, implementiert diese aber nicht als string-Parameter aufgrund der Verwechslungsgefahr und dem zu generischen Parameternamen, sondern wrappt es in die eigentliche Isin und Aktiensymbol-Klasse und kann dann im Konstruktor zusätzlich gleich die Validierung vornehmen.

Hab das eben mal schnell runterprogrammiert:


public interface IStockWebsiteLinkCreator<TInput>
{
    Uri CreateLink(TInput input);
}

public class MarketWatchLinkCreator : IStockWebsiteLinkCreator<StockSymbol>
{
    Uri CreateLink(StockSymbol input)
    {
        return new Uri("https://www.marketwatch.com/investing/stock/" + input.Symbol);
    }
}

public class ComDirectLinkCreator : IStockWebsiteLinkCreator<Isin>
{
    Uri CreateLink(Isin input)
    {
        return new Uri("https://www.comdirect.de/inf/aktien/" + input.Isin);
    }
}

public class StockSymbol
{
    public StockSymbol(string symbol)
    {
        if (symbol.Length < 1 || symbol.Length > 5)
        {
            throw new FormatException(nameof(symbol));
        }

        Symbol = symbol;
    }

    public string Symbol { get; set; }
}

public class Isin
{
    public Isin(string isin)
    {
        if (/* Validierung */)
        {
            throw new FormatException(nameof(symbol));
        }

        Isin = isin;
    }

    public string Isin { get; set; }
}

Finde das sehr sauber. Nun muss nur noch ein schöner Weg für die Istanziierung gefunden werden.

W
955 Beiträge seit 2010
vor 3 Jahren

Warum verwendest du nicht String interpolation?

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 3 Jahren

Das erachte ich nur als sinnvoll, wenn wirklich ein string.Format gebraucht wird und nicht nur für das Anhängen eines Strings.

16.806 Beiträge seit 2008
vor 3 Jahren

wenn wirklich ein string.Format gebraucht wird und nicht nur für das Anhängen eines Strings.

Interpolation ist kein Format. Siehe Link, der Dir gegeben wurde.

In der Runtime ist das durchaus identisch / änhlich; der Sinn von Interpolation ist aber vor allem die Übersichtlichkeit.
Letzten Endes bleibt es Geschmackssache; Interpolation ist jedoch der empfohlene Weg.

$"https://www.marketwatch.com/investing/stock/{input.Symbol}"

Witzigerweise ist aber string-Format genau das Mittel für diese aktuelle Code Umsetzung.
Man würde einfach string Format für das Setzen des Symbols verwenden

string pattern = "https://www.marketwatch.com/investing/stock/{0}"
string pattern = "https://www.comdirect.de/inf/aktien/{0}"

var uri = string.Format(pattern, symbol);

Damit braucht man absolut null Factory Implementierung.


Deine Symbol-Klasse ist in der Form sinnfrei.
Die macht nichts anderes als ein String zu halten und zu validieren; macht so wenig Sinn.

Die Validierung ist auch in der Form gar nicht korrekt, weil es durchaus Symbols gibt mit mehr als 5 Stellen; zB bei Yahoo.

Viele Aktien haben hier den Marktplatz im Symbol-Parameter als Suffix, zB xxxx.DE

In der echten Welt funktioniert das allein kaum, weil der API Aufruf bei Aktien APIs unterschiedlich ist; eben zusätzliche Parameter benötigt werden.
Die Url alleine hilft nicht.

Daher lohnt sich Factory hier als Provider Implementierung.
Und ja, das Format nehme ich dann für das Setzen des Symbols in der Uri.


    public interface IStockTickerSymbolService
    {
        Task<SymbolData> GetSymbolData(string symbol);
    }
    public class ComdirectStockTickerOptions {
        public string SymbolUrl = "https://www.comdirect.de/inf/aktien/{0}";
    }
    public class YahooStockTickerSymbolService : IStockTickerSymbolService
    {
        public YahooStockTickerSymbolService(IOptions<YahooStockTickerOptions> optionsAccessor)
    }
    public class ComdirectStockTickerSymbolService : IStockTickerSymbolService
    {
        public ComdirectStockTickerSymbolService(IOptions<ComdirectStockTickerOptions> optionsAccessor)
    }

Prinzip:

  • In den App Options hinterlege ich, welchen StockTicker ich brauche. Eine Factory instanziiert diese.
  • Jeder StockTicker hat seine eigenen Options
  • Das Symbol ist gleich; die Stockticker-Options füllen die notwendigen Parameter (API Key, Market Data etc...)
B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 3 Jahren

Deine Implementierung mit string.Format funktioniert aber eben nur dann, wenn es einen einzigen Parameter gibt, der in die URL eingefügt werden muss. Spätestens, wenn die Heimatbörse nicht wie bei Yahoo Finance an der gleichen Stelle eingefügt werden muss, wird das Prinzip auch schon wieder gebrochen.

Deine Argumentation zur Symbolklasse verstehe ich nicht. Der Sinn der Klasse ist es doch genau für jeden der ein Symbol braucht, sei es als Parameter oder in irgendeiner Implementierung, dass er davon ausgehen kann ein valides Symbol bereits vorliegen zu haben, was zumindest vom Format her bereits valide ist. Ein Symbol ist ein 1-5 stelliger String (zumindest ist mir noch kein anderes vorgekommen). Da du dich auf Yahoo beziehst, vermute ich, du meinst, dass an das Symbol noch die Heimatbörse gehangen wird. Das ist dann kein Symbol mehr, sondern eben eine weitere Datenklasse, die aus Symbol und Heimatbörse besteht und der generische Typparameter (TInput) bei meinem IStockWebsiteLinkCreator werden würde.

Deine Optionsinjection finde ich aber ein spannenendes Muster und muss ich mir mal genauer zu Gemüte führen 😃

16.806 Beiträge seit 2008
vor 3 Jahren

Spätestens, wenn die Heimatbörse nicht wie bei Yahoo Finance an der gleichen Stelle eingefügt werden muss, wird das Prinzip auch schon wieder gebrochen.

Nein, denn Du kannst das Symbol - das Kernelement - überall setzen.
Weitere Parameter reichst Du (zumindest ich mach das so und habe noch keine API erlebt, wo das nicht geht) über Uri-Parameter an.

Ein Symbol ist ein 1-5 stelliger String (zumindest ist mir noch kein anderes vorgekommen.

Du vermischt die Business-Sicht eines Symbols mit der API-Sicht eines Symbols.
Ja, an der Börse hat ein Symbol 1-5 Zeichen. Die APIs interessiert diese Regel aber oft nicht, weil sie mehr Infos haben wollen, zB aufgrund technischer Implementierungen.

Das ist dann kein Symbol mehr, sondern eben eine weitere Datenklasse, die aus Symbol und Heimatbörse besteht

Nein, eben nicht.

Beispiel: Biontech hat das Symbol BNTX.
Du wirst aber kaum eine API finden, die mit diesem Symbol etwas anfangen kann. Auch hilft Dir hier auch 22UA nicht.

Yahoo will "22UA.DE" als Symbols-Parameter; einfach weil die Yahoo-API den Zielmarkt haben will; im Symbol-Parameter.
Deine Klasse Symbol bringt Dir also nichts, weil das Business-Objekt Symbol mit API-spezifsichen Eigenheiten nur schwer kombinierbar ist.

In diesem Fall ist das auch nicht die Heimatbörse, sondern der Markt.

Aber: Oppssi! Die Comdirekt API kann mit 22UA und auch mit 22UA.DE nichts anfangne; die wollen die ISIN oder das "echte" Symbol BNTX.

Ne weitere API (glaube IEX oder AV) wollen eine Guid haben, die hinter dem Symbol steckt.
D.h. Du musst die Guid als Parameter nehmen und eben nicht das Symbol.

Daher: Die Symbol Klasse - als API Parameter - macht so kein Sinn, weil leider die API unterschiedliche Implementierungen haben, was sie inhaltlich übergeben haben wollen.

Deine Optionsinjection finde ich aber ein spannenendes Muster und muss ich mir mal genauer zu Gemüte führen 😃

Ist das Standard Optionsmuster, das mit .NET Core 1.0 eingeführt wurde.