Toon posts:

Ontwerpkeuze voor uitbreidbare classes in library

Pagina: 1
Acties:

Onderwerpen

Vraag


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
De situatie: ik heb een .NET library geschreven voor een API van een derde partij, deze library kan gebruikt worden voor interactie met de API. De API is in de basis zeer eenvoudig, je kan er een zoekopdracht aan sturen en je krijgt resultaat terug. Dit resultaat is qua structuur wel enorm complex, mijn library doet het parsen en returnt het resultaat als simpele classes met properties.

Nu is het zo dat de API een aantal varianten heeft, je hebt een basis resultaat met bepaalde velden, maar afhankelijk van de variant kunnen er velden bij komen. Op dit moment heb ik alles op een hoop gegooid, maar dat vind ik niet ideaal, omdat als je de basis variant gebruikt, je resultaat vervuild wordt met allerlei properties waar je niks mee te maken hebt. Ook zijn sommige properties classes die ook weer uitbreidbaar kunnen zijn.
Voorbeeld:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SearchResult
{
    // standaard properties voor alle varianten
    public string MyString { get; set; }
    public ComplexType MyType { get; set; }

    // extra veld uit andere variant
    public string MyExtraString { get; set;}
    // resultaat uit derde variant
    public string YetAnotherString { get; set; }
}

public class ComplexType
{
    public int Number { get; set; }
}


Waar ik nu mee worstel is hoe dit op de beste manier op te lossen is. Een deelt is natuurlijk op te lossen met inheritance:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SearchResult
{
    // standaard property voor alle varianten
    public string MyString { get; set; }
}

public class ExtraSearchResult : SearchResult
{
    // extra veld uit andere variant
    public string MyExtraString { get; set;}
}

public class YetAnotherSearchResult : ExtendedSearchResult
{
    // resultaat uit derde variant
    public string YetAnotherString { get; set; }
}

public class ExtraComplexType : ComplexType
{
    public int ExtraNumber { get; set; }
}


Maar hoe ga je hier dan mee om, en wat doe je met de ExtraComplexType
C#:
1
2
3
4
public ICollection<SearchResult> Search(string query)
{
       // .. 
 }


Ik denk dat het in ieder geval wel een goed idee is om de enkele Search te vervangen door specifieke methods die het juiste type SearchResult returnen (er zijn in totaal 9 varianten)
C#:
1
2
3
4
5
6
7
8
9
public ICollection<ExtraSearchResult> ExtraSearch(string query)
{
    // .. 
}

public ICollection<YetAnotherSearchResult> YetAnotherSearch(string query)
{
    // .. 
}


Dan hou je nog het probleem dat een ExtraSearchResult geen ComplexType, maar ExtraComplexType moet returnen. Ik kwam zelf op twee opties.
Optie 1, dit werkt wel, maar voelt niet helemaal lekker
C#:
1
2
3
4
5
public class ExtraSearchResult : SearchResult
{
    // ..
    public new ExtraComplexType MyType { get; set; }
}


Optie 2 is om ExtraComplexType niet van ComplexType te laten erven, maar dat vind ik ook niet ideaal omdat het resultaat dan verspreid wordt over meerdere properties.
C#:
1
 public ExtraComplexType ExtraMyType { get; set; }


Er is ook een optie 3 dat in het eenvoudige voorbeeld zou werken, maar in de praktijk niet (met tien properties die een class zijn is dat geen doen):
C#:
1
2
3
4
5
6
7
8
9
public ICollection<SearchResult<ComplexType>> Search(string query)
public ICollection<ExtraSearchResult> ExtraSearch(string query)

public class SearchResult<TComplexType>
{
    public TComplexType MyType { get; set; }
}

public class ExtraSearchResult : SearchResult<ExtraComplexType>


Ik hoop dat het probleem na dit lange verhaal een beetje duidelijk is en ben benieuwd naar jullie meningen :) Het voorbeeld is nu in C#, maar hetzelfde probleem en eventuele oplossingen zullen in andere talen ook van toepassing zijn.

Kater? Eerst water, de rest komt later

Alle reacties


  • RobIII
  • Registratie: December 2001
  • Laatst online: 00:02

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

Wat je volgens mij wil is een (type) discriminator; dat maakt 't een stuk makkelijker om je poplymorphe result weer te interpreteren. Het is in essentie een extra property ($type bijvoorbeeld, maar kan vanalles zijn) dat gebruikt kan worden om onderscheid te maken tussen de verschillende classes.

[Voor 26% gewijzigd door RobIII op 04-01-2023 12:07]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Roses are red Violets are blue, Unexpected ‘{‘ on line 32.

Over mij


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
@RobIII dat lijkt er wel een beetje op ja, maar dat gaat specifiek over (de)serializen van JSON data. Mijn probleem zit juist in de structuur van de objecten, dus dat is een stap eerder dan het eventueel (de)serializen van het resultaat.

Kater? Eerst water, de rest komt later


  • farlane
  • Registratie: Maart 2000
  • Laatst online: 28-01 23:43
Dat mapt (bijna) 1 op 1 toch?

Somniferous whisperings of scarlet fields. Sleep calling me and in my dreams i wander. My reality is abandoned (I traverse afar). Not a care if I never everwake.


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
@farlane ik weet niet precies wat je bedoelt?

Anyway, ik heb nu denk ik een aanpak die wel redelijk werkbaar is, een kleine variant op de inheritence optie, maar dan iets minder ver doorgevoerd en met wat duplicatie van bepaalde properties. Dus bijvoorbeeld een SearchResultBase waarin globale properties van built-in types staan (strings, booleans e.d.) en meer specifieke classes met hun eigen properties en dan ook eventuele eigen specifieke varianten. Dus iets als dit
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SearchResultBase
{
    // standaard property voor alle varianten
    public string MyString { get; set; }
}

public class SearchResult : SearchResultBase
{
    public ComplexType MyType { get; set; }
}

public class ExtraSearchResult : SearchResult
{
    public  ExtraComplexType MyType { get; set; }
}

Er moet alleen af en toe wat gecast worden naar specifieke versies, maar dat is wel te doen.
Het mooie is dat ik op deze manier ook parsers kan laten erven van elkaar, dus de ExtraSearchResultParser roept eerst z'n base SearchResultParser aan, en doet daarna nog het parsen van z'n eigen properties.

Kater? Eerst water, de rest komt later


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
Toch weer terug met een probleem. Ik dacht een betere oplossing te hebben gevonden voor het moeten casten, weer als voorbeeld deze classes (iets anders dan in de vorige post, er is nu 1 base class en alleen directe child classes)
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SearchResultBase
{
    // standaard property voor alle varianten
    public string MyString { get; set; }
}

public class SearchResult : SearchResultBase
{
    public ComplexType MyType { get; set; }
}

public class OtherSearchResult : SearchResultBase
{
    public string OtherString { get; set; }
}


En dan een generic interface met implementaties:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface IParser<T> where T : SearchResultBase
{
    public void Parse(string source, T target);
}

public class BaseParser : IParser<SearchResultBase>
{
    public void Parse(string source, SearchResultBase target)
    {
        target.MyString = "string";
    }
}

public class StandardParser : IParser<SearchResult>
{
    public void Parse(string source, SearchResult target)
    {
        target.MyString = "string";
        target.MyType = new ComplexType();
    }
}

public class OtherParser: IParser<OtherSearchResult>
{
    public void Parse(string source, OtherSearchResult target)
    {
        target.MyString = "string";
        target.OtherString = "other";
    }
}


Ter illustratie, in de oude situatie was het dit:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface IParser
{
    public void Parse(string source, SearchResultBase target);
}

public class StandardParser : IParser
{
    public void Parse(string source, SearchResultBase target)
    {
        target.MyString = "string";
        // hier dan een cast nodig om bij MyType te kunnen
        ((SearchResult)target).MyType = new ComplexType();
    }
}


So far so good, werkt perfect, echter heb ik ook een factory die de juiste parser teruggeeft en daar loop ik nu op stuk..
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ParserFactory
{
    public static IParser<SearchResultBase> GetSearcher(string type)
    {
        switch (type)
        {
            case "a":
                return new BaseParser(); // OK
            case "b":
                return new StandardParser(); // compile error
            case "c":
                return new OtherParser(); // compile error
            default: throw new ArgumentException();
        }
    }

// Cannot implicitly convert type 'StandardParser' to 'IParser<SearchResultBase>'. An explicit conversion exists (are you missing a cast?)
}


Dus ondanks dat SearchResult gewoon een SearchResultBase is, vindt de compiler dat niet goed genoeg. Ik heb van alles geprobeerd om het werkend te krijgen, maar het lijkt gewoon niet mogelijk te zijn :'(

PS. dit werkt wel >:) maar dat is zo omslachtig dat ik dan beter terug naar de oude manier kan.
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class StandardParser : IParser<SearchResult>, IParser<SearchResultBase>
{
    public void Parse(string source, SearchResult target)
    {
        target.MyString = "string";
        target.MyType = new ComplexType();
    }

    public void Parse(string source, SearchResultBase target)
    {
        Parse(source, (SearchResult)target);
    }
}

Kater? Eerst water, de rest komt later


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Je loopt tegen Co/Contra variantie aan. Is het niet mogelijk om je SearchResult ook te laten creeren door je Parser?

Dan krijg je zoiets

C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Base
{
}

public class Sub : Base
{
    
}

public interface IParser<out T> where T : Base
{
    public T Parse(string source);
}

public class BaseParser : IParser<Base>
{
    public Base Parse(string source)
    {
        throw new NotImplementedException();
    }
}

public class SubParser : IParser<Sub>
{
    public Sub Parse(string source)
    {
        return new Sub();
    }
}

public class ParserFactory
{
    public IParser<Base> Create()
    {
        return new SubParser();
    }
}

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
@Woy Dat gaat helaas niet, het gaat in de pratkijk om een complex stuk JSON (de response van een zoek request). Ik enumerate vervolgens de properties van het JSON object en de factory geeft de juiste parser terug, die dus alleen die specifieke JSON property weet te verwerken. Iedere parser vult dus een stukje van het SearchResult(Base) object, vandaar dat het steeds aan de Parse method wordt meegegeven.

Kater? Eerst water, de rest komt later


  • Webgnome
  • Registratie: Maart 2001
  • Laatst online: 18:47
Maar dan kan het toch wel? Uiteindelijk komt er een eind aan die parsing en is er een samengesteld object. Op dat moment weet je toch wat je precies terug moet geven ( welke variant ) ?

Strava, Twitter


  • ocf81
  • Registratie: April 2000
  • Niet online

ocf81

Gewoon abnormaal ;-)

Kan je dit niet met een decorator pattern oplossen? Ik neem aan dat iemand weet welke dienst wordt afgenomen bij het gebruik van de API?

© ocf81 1981-infinity
Live the dream! | Politiek Incorrecte Klootzak uitgerust met The Drive to Survive
Bestrijd de plaag van wokeness! | <X> as a Service --> making you a poor & dependent slave


  • Haan
  • Registratie: Februari 2004
  • Laatst online: 21:28
Webgnome schreef op woensdag 25 januari 2023 @ 19:53:
Maar dan kan het toch wel? Uiteindelijk komt er een eind aan die parsing en is er een samengesteld object. Op dat moment weet je toch wat je precies terug moet geven ( welke variant ) ?
Ik weet bij voorbaat al precies welke variant teruggeven gaat worden, dit wilde ik d.m.v. generics ook aan de parsers doorgeven, wat dus opzich prima kan, maar niet in mijn combinatie met de factory voor het creeëren van de parsers.
Om het nog wat meer context te geven, dit is (vereenvoudigd) de parser class zelf
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JsonDocumentParser<T> where T : SearchResultBase, new()
{
    public ResultModel<T> ParseSearchResult(string api, string searchResult)
    {
        var resultModel = new ResultModel<T>();
        var json = JsonDocument.Parse(searchResult);
        
        foreach (var element in json.RootElement.EnumerateArray())
        {
            var result = new T();

            foreach (var prop in element.EnumerateObject())
            {
                var parser = ParserFactory.GetParser(api, prop.Name);
                parser?.Parse(prop, result);
            }

            resultModel.Results.Add(result);
        }

        return resultModel;
    }
}

(zie voor een parser voorbeed-implementatie "Ter illustratie, in de oude situatie was het dit:" in mijn vorige post)
Vrij recht-toe-recht-aan dus en werkt opzich als een trein. (En een grote verbetering t.o.v. de eerste versie waar alles in 1 class stond van vele honderden regels :9 )

@ocf81 Decorator pattern heb ik ook wel over nagedacht, maar dat is denk ik niet precies wat ik nodig heb, of het zou weer een compleet nieuwe aanpak vereisen.

Kater? Eerst water, de rest komt later

Pagina: 1


Tweakers maakt gebruik van cookies

Tweakers plaatst functionele en analytische cookies voor het functioneren van de website en het verbeteren van de website-ervaring. Deze cookies zijn noodzakelijk. Om op Tweakers relevantere advertenties te tonen en om ingesloten content van derden te tonen (bijvoorbeeld video's), vragen we je toestemming. Via ingesloten content kunnen derde partijen diensten leveren en verbeteren, bezoekersstatistieken bijhouden, gepersonaliseerde content tonen, gerichte advertenties tonen en gebruikersprofielen opbouwen. Hiervoor worden apparaatgegevens, IP-adres, geolocatie en surfgedrag vastgelegd.

Meer informatie vind je in ons cookiebeleid.

Sluiten

Toestemming beheren

Hieronder kun je per doeleinde of partij toestemming geven of intrekken. Meer informatie vind je in ons cookiebeleid.

Functioneel en analytisch

Deze cookies zijn noodzakelijk voor het functioneren van de website en het verbeteren van de website-ervaring. Klik op het informatie-icoon voor meer informatie. Meer details

janee

    Relevantere advertenties

    Dit beperkt het aantal keer dat dezelfde advertentie getoond wordt (frequency capping) en maakt het mogelijk om binnen Tweakers contextuele advertenties te tonen op basis van pagina's die je hebt bezocht. Meer details

    Tweakers genereert een willekeurige unieke code als identifier. Deze data wordt niet gedeeld met adverteerders of andere derde partijen en je kunt niet buiten Tweakers gevolgd worden. Indien je bent ingelogd, wordt deze identifier gekoppeld aan je account. Indien je niet bent ingelogd, wordt deze identifier gekoppeld aan je sessie die maximaal 4 maanden actief blijft. Je kunt deze toestemming te allen tijde intrekken.

    Ingesloten content van derden

    Deze cookies kunnen door derde partijen geplaatst worden via ingesloten content. Klik op het informatie-icoon voor meer informatie over de verwerkingsdoeleinden. Meer details

    janee