[Alg] Ontwerpen van Data-access en business classes

Pagina: 1
Acties:
  • 149 views sinds 30-01-2008
  • Reageer

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
De meesten onder jullie weten wel dat je - zeker in een serieuze applicatie- de business logica, de data (access) logica en de presentatielaag van elkaar gescheiden moet houden.

Nu vroeg ik me eigenlijk af hoe je best de communicatie tussen de data access logica en de business logica verwezenlijkt. Ik heb al op het 'Architecture center' van Microsoft dit artikel gelezen, maar echt veel materie over de communicatie tussen de verschillende tiers ben ik niet tegengekomen.

Nu, wat is volgens jullie de beste manier om die communicatie te laten verlopen?


Stel, je hebt volgende data-access en business-logic class:

code:
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
// Data access class
class PersonDAL
{
   private string _connectionString;

   public string ConnectionString
   {
      set
      {
          _connectionString = value;
      }
   }

   public void Save(Person) 
   {
   }
  
   public Person Load(int pk) 
   {
   }

   public bool Delete(Person)
   {
   }
}

// Business Logic class
public class Person
{
   private int            _id;
   private string       _naam;
   private DateTime _geboortedatum;

   // Hier komen nog properties en BL-methods.
}


Hoe laat je die classes nu het best met elkaar samenwerken/communiceren?

Situatie 1:
Je doet de communicatie in de presentatie-laag:
code:
1
2
3
4
5
6
PersonDAL daPerson = new PersonDAL();
daPerson.ConnectionString = getConnectionString();

Person p1 = daPerson.Load(1);
// doe wat wijzingen aan p1
daPerson.Save(p1);


of
Situatie 2:
Integreer je de data-access componenten beter in de business logic component:
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person
{
    private int _id;
    private string _naam;
    private DateTime _geboorteDatum;

    public Person(int personId)
    {
       PersonDAL daPerson = new PersonDAL();
       daPerson.ConnectionString = getConnStr();
       daPerson.Load(personId);
    }
}


In dat artikel van MS wordt de 2de manier niet vermeld, ergens in de appendices zie je wel de 1ste manier vermeld staan. Ik denk zelf dat de 1ste manier ook de betere is; de BL en DAL zijn volledig van elkaar gescheiden, maar zorgt imho niet voor zo'n 'abstracte' code als situatie 2 in de presentatielaag.
Dus, welke situatie vinden jullie beter, en waarom, of, hebben jullie nog een andere manier die volgens jullie beter is?

Dit gaat dus niet over het 'echte' ontwerp van bovenstaande classes, maar eerder over de communicatie ertussen

[ Voor 3% gewijzigd door whoami op 18-12-2002 10:10 ]

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

Verwijderd

Bij Situatie 1 is de DAL niet echt mooi gescheiden van de rest. Dus dat lijkt mij persoonlijk niet echt een mooie oplossing. Versie B lijkt me al veel beter, maar waarom doe je ConnectionString niet in een algemene class waar elke DAL-class van overerft?
Zoals LLBLGen dat doet :).

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
Verwijderd schreef op 18 december 2002 @ 10:13:
Bij Situatie 1 is de DAL niet echt mooi gescheiden van de rest. Dus dat lijkt mij persoonlijk niet echt een mooie oplossing. Versie B lijkt me al veel beter,
Idd, maar in versie B heb je die DAL wel verweven in je Business logic.
maar waarom doe je ConnectionString niet in een algemene class waar elke DAL-class van overerft?
Zoals LLBLGen dat doet :).


Typewerk te besparen enzo...
(Check ook het laatste regeltje. :P)

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • EfBe
  • Registratie: Januari 2000
  • Niet online
Je kunt ook dit doen:
- Je maakt een dal met daarin classes per tabel. Dus Person in de DAL doet alle interactie op de Person tabel.
- Dan maak je een collection based class (derived van bv CollectionBase) en daarmee bouw je de class Persons. Dit is een BL class
- Je maakt een class Person in de BL, waarbij je de constructor zo hebt gebouwd dat de ID meegegeven aan de constructor de DAL.Person class gebruikt voor het lezen van de data uit de Person tabel behorende bij het record met key gelijk aan de ID die is meegegeven. De constructor vult dan membervariabelen met de data gelezen mbv de DAL class. Properties bieden deze members dan weer aan.

Je kunt dan je Persons collection zo bouwen dat deze een propertie Person heeft, die in feite een instance maakt van BL.Person en de data laadt, bv:

Person oMyPerson = Persons.Person(3);

oMyPerson wordt dan een BL.Person object met de data van Person '3'.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


Acties:
  • 0 Henk 'm!

  • Bobco
  • Registratie: Januari 2001
  • Laatst online: 30-10-2023

Bobco

I used to dream about Verona.

Niet dat ik hier echt heel erg veel verstand van heb, maar ik wil toch een paar opmerkingen plaatsen.

A. Waarom zou je in de BL willen weten of een object persistent is of niet? Ik zou in dit geval denk ik liever met een beheercollectie werken:

Java:
1
2
3
4
public class PersonCollection {
  public Person getPerson(int id);
  public void addPerson(int id);
} 


Je kunt deze PersonCollection ook meteen gebruiken als cache.

B. Ik zou de BL classes die persistent moeten zijn zelf laten bepalen welke attributen ze wel en en niet op willen laten slaan. Procesgegevens moeten ze zelf maar opbouwen, alleen de 'echte' data hoeft persistent te zijn.

Overigens is dit wel een interessant onderwerp. Iedereen weet dat je de lagen moet scheiden, maar goede implementatie voorbeelden zijn er niet zo veel.

With the light in our eyes, it's hard to see.


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
EfBe schreef op 18 december 2002 @ 10:20:
Je kunt ook dit doen:
- Je maakt een dal met daarin classes per tabel. Dus Person in de DAL doet alle interactie op de Person tabel.
Ff miereneuken.... Volgens dat MS artikel, maak je best niet per tabel een DAL, maar eerder per 'logische unit'. (Bv, voor een DAL voor een person die de tabellen tblPerson en tblAdress oid bevat).
- Dan maak je een collection based class (derived van bv CollectionBase) en daarmee bouw je de class Persons. Dit is een BL class
- Je maakt een class Person in de BL, waarbij je de constructor zo hebt gebouwd dat de ID meegegeven aan de constructor de DAL.Person class gebruikt voor het lezen van de data uit de Person tabel behorende bij het record met key gelijk aan de ID die is meegegeven. De constructor vult dan membervariabelen met de data gelezen mbv de DAL class. Properties bieden deze members dan weer aan.

Je kunt dan je Persons collection zo bouwen dat deze een propertie Person heeft, die in feite een instance maakt van BL.Person en de data laadt, bv:

Person oMyPerson = Persons.Person(3);

oMyPerson wordt dan een BL.Person object met de data van Person '3'.

Snap ik niet zo goed. :+
Kan je mbhv een klein beetje code dat wat verduidelijken?

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • gorgi_19
  • Registratie: Mei 2002
  • Laatst online: 13:29

gorgi_19

Kruimeltjes zijn weer op :9

Ik ben dit probleem ook tegengekomen. Ook heeft Microsoft naar mijn idee in de voorbeeldapplicatie ASPNetForums hier een nette oplossing voor gemaakt. (en mogelijkheid drie.. :P)

Deze houdt in:

1. Creeer een interface, waarin je alle databasefuncties definieert
2. Creeer een aparta dataclass, waarin je deze functies implementeert
(vb:
code:
1
    Public Function GetThumbnails(ByVal ModuleID As Integer) As ThumbnailCollection Implements IDataProviderBase.GetThumbnails

3. Creeer een class DataProvider met een public shared functie getInstance. Deze returned class, gemaakt in 2, als Interface
4. Creeer een aparte class, welke functies heeft om de data aan te roepen
In mijn voorbeeld:
code:
1
2
3
4
5
6
7
8
Namespace CMS

    Public Class Thumbnails
        Public Overloads Shared Function GetThumbnails(ByVal moduleId As Integer) As ThumbnailCollection
            ' Create Instance of the IDataProviderBase
            Dim dp As IDataProviderBase = DataProvider.Instance()
            Return dp.GetThumbnails(moduleId)
        End Function


Volgens mij heb ik het een beetje vaag neergezet; als mensen het uitgebreid willen zien met meer code.... :)

Het voordeel van deze werkwijze vindt ik ook dat je op deze manier niet aan een database gebonden bent..

[ Voor 5% gewijzigd door gorgi_19 op 18-12-2002 10:33 ]

Digitaal onderwijsmateriaal, leermateriaal voor hbo


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
gorgi_19, heb je soms geen linkje naar dat artikel?

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • gorgi_19
  • Registratie: Mei 2002
  • Laatst online: 13:29

gorgi_19

Kruimeltjes zijn weer op :9

whoami schreef op 18 december 2002 @ 10:37:
gorgi_19, heb je soms geen linkje naar dat artikel?
Het is geen artikel, het is een voorbeeldapplicatie van Microsoft.. :)

Het staat op www.asp.net/forums/download

[ Voor 4% gewijzigd door gorgi_19 op 18-12-2002 10:41 ]

Digitaal onderwijsmateriaal, leermateriaal voor hbo


Acties:
  • 0 Henk 'm!

  • Bobco
  • Registratie: Januari 2001
  • Laatst online: 30-10-2023

Bobco

I used to dream about Verona.

whoami schreef op 18 december 2002 @ 10:26:
[...]

Ff miereneuken.... Volgens dat MS artikel, maak je best niet per tabel een DAL, maar eerder per 'logische unit'. (Bv, voor een DAL voor een person die de tabellen tblPerson en tblAdress oid bevat).
Lijkt me inderdaad wel een goed plan. Relationele databases en OO zijn niet echt een goede combinatie. Het werkt wel, maar soms wringt het een beetje. Dat een object Persoon weet wat z'n adres is is niet meer dan logisch, maar in de database zou je dat inderdaad kunnen splitsen in een aantal tabellen.
Snap ik niet zo goed. :+
Kan je mbhv een klein beetje code dat wat verduidelijken?
Volgens mij bedoelt EfBe precies wat ik ook al heb gezegd. Maak een collectie waar je de BL objecten uit haalt. De collectie weet waar de objecten vandaan gehaald moeten worden. Dit levert meteeen een mooie mogelijkheid tot caching op omdat je BL nooit rechtstreeks naar de database gaat maar altijd via een object dat de collectie beheerd.

With the light in our eyes, it's hard to see.


Acties:
  • 0 Henk 'm!

  • tijn
  • Registratie: Februari 2000
  • Laatst online: 01-08 16:49
whoami schreef op 18 december 2002 @ 10:09:
Nu vroeg ik me eigenlijk af hoe je best de communicatie tussen de data access logica en de business logica verwezenlijkt. Ik heb al op het 'Architecture center' van Microsoft dit artikel gelezen, maar echt veel materie over de communicatie tussen de verschillende tiers ben ik niet tegengekomen.
Er zijn vele wegen die naar Rome leiden en volgens mij is er geen enkele methode die de beste is. Ik heb er al verscheidene geprobeerd en allemaal hebben ze wel hun voor- en nadelen.
Momenteel ga ik bij grote projecten uit van 4 lagen: Presentation, Business, DataAccess en Domain. De Domain laag bevat de klassen waarmee tussen de verschillende lagen gecommuniceerd wordt en bevatten alleen data. Eigenlijk is de Domain laag een afsplitsing van de Business laag, waardoor de Business laag eigenlijk alleen nog maar methodes overhoudt. De ASP.NET Forums applicatie werkt ook min of meer op deze manier (al moet je wel goed kijken om het er uit te kunnen halen, want qua naamgeving is het allemaal niet erg helder).
Bij kleinere projecten laat ik de Domain laag weg en komen de Business en DataAccess laag wat knusser tegen elkaar aan te zitten.
Toch ben ik nog steeds zoekende naar de ultieme oplossing. Vorige week Patterns of Enterprise Application Architecture besteld. Wellicht komen we daar weer een stapje verder mee...

Cuyahoga .NET website framework


Acties:
  • 0 Henk 'm!

  • Alarmnummer
  • Registratie: Juli 2001
  • Laatst online: 09-07-2024

Alarmnummer

-= Tja =-

[heeft nog niet alles gelezen]
Ik ben oa op dit moment ook wat aan het verdiepen in deze problematiek. In java hebben ze een design pattern hiervoor: DAO Data Access Objects (Alhoewel deze ook wel bij andere talen voor zal komen maar onder een andere naam).

Wat het idee is, is dat je idd een scheiding gaat aanbrengen tussen je business laag en je datalaag. En verder ga je als je het een beetje goed ontwerp, die Datalaag opzetten mbv een interface. En implementaties kan je leveren mbv een factory. Hierdoor zit je niet vast aan een gekozen persitence oplossing.

[ Voor 12% gewijzigd door Alarmnummer op 18-12-2002 14:02 ]


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
tijn schreef op 18 december 2002 @ 13:29:
[...]

Toch ben ik nog steeds zoekende naar de ultieme oplossing. Vorige week Patterns of Enterprise Application Architecture besteld. Wellicht komen we daar weer een stapje verder mee...

't Ziet er wel een goed boek uit....
* whoami heeft 'm ook maar besteld. :Y)

offtopic:
/me heeft vorige week 7 boeken gekocht. :X
(gelukkig hebben die 7 boeken samen mij maar 55 euro gekost)
/me vraagt zich af wanneer hij dat allemaal gaat lezen.

[ Voor 15% gewijzigd door whoami op 18-12-2002 14:14 ]

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

  • Alarmnummer
  • Registratie: Juli 2001
  • Laatst online: 09-07-2024

Alarmnummer

-= Tja =-

whoami schreef op 18 december 2002 @ 14:03:

[...]

't Ziet er wel een goed boek uit....
* whoami heeft 'm ook maar besteld. :Y)
Ik zal hem ook even op mijn lijstje zetten :)

Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
Verwijderd schreef op 18 december 2002 @ 10:13:
Bij Situatie 1 is de DAL niet echt mooi gescheiden van de rest. Dus dat lijkt mij persoonlijk niet echt een mooie oplossing.


:?
In die situatie vind ik het eigenlijk meer gescheiden dan in situatie 2. In situatie 2 zijn de DAL objects verweven in de business logic

* whoami moet dan ook nog ff die ASP.NET fora bekijken...

[ Voor 7% gewijzigd door whoami op 18-12-2002 15:50 ]

https://fgheysels.github.io/


Acties:
  • 0 Henk 'm!

Verwijderd

whoami schreef op 18 december 2002 @ 15:48:
:?
In die situatie vind ik het eigenlijk meer gescheiden dan in situatie 2. In situatie 2 zijn de DAL objects verweven in de business logic
Ja, maar, in situatie 1 moet je in je presentation laag (of waar je je BO ook gebruikt) nog weten dat je eerst de ConStr moet setten. Verder moet je in je DAL weten hoe je BO eruit ziet.
In situatie twee weet de BO van de DAL, en de Presentation Layer van de BO, maar verder weten ze niks. Dat lijkt mij iets netter :).

Acties:
  • 0 Henk 'm!

  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 19-08 16:50
Een snapshot uit de documentatie voor een n-tier systeem zoals wij die op de HIO in eindhoven voor Oce aan het maken zijn:

Data Access is gebaseerd op Table Data Mapping (Martin Fowler, [Patterns of Enterprise Application Architecture 2002]), oftewel per entiteit een object welke methodes biedt om queries uit te voeren op de database. Dit laatste is ook wel bekend als een Gateway per tabel. Wij hebben er expliciet voor gekozen NIET een in-memory O/R model (framework) te bouwen vanwege de complexiteit.

Afbeeldingslocatie: http://www.connexx.nl/medewerkers/p.gielens/pub/clip_image001.gif

De Table Data Gateway retourneert DataTable objecten en worden via een wrapper geconverteerd naar Recordset objecten, zodat deze over COM grenzen gebruikt kunnen worden. Een Recordset is een generieke data structuur voor tabellen en rijen welke een 1:1 image zijn van de relationele data opgeslagen in de database. De Data Access laag is bewust stateless gehouden, zodat zaken als concurrency en persistentie op de achtergrond blijven. Ter begeleiding van conflicterende operaties op de relationele data wordt gebruik gemaakt van Stored Procedures & COM+ services.

Acties:
  • 0 Henk 'm!

  • tijn
  • Registratie: Februari 2000
  • Laatst online: 01-08 16:49
paulgielens schreef op 18 december 2002 @ 17:46:
Een snapshot uit de documentatie voor een n-tier systeem zoals wij die op de HIO in eindhoven voor Oce aan het maken zijn:
Gegenereerd met LLBLGen of nie? >:)

Cuyahoga .NET website framework


Acties:
  • 0 Henk 'm!

  • PhoneTech
  • Registratie: Mei 2000
  • Laatst online: 09-09 13:25
Ziet er redelijk als LLBLGen code uit ja!

Ik ben deze aan het upgraden met leuke functies zoals per tabel aangeven welke kolommen moet worden gegenereerd en dat je een keuze hebt om ook SqlDataReaders terug te geven (voor de performance)

Acties:
  • 0 Henk 'm!

  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 19-08 16:50
LLBLGen indeed

ps: bij grote projecten leer je de beperkingen van LLBLGen wel kennen, balen dat ik mijn oude SelectBy code kwijt ben.

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
Verwijderd schreef op 18 december 2002 @ 15:55:
[...]
Ja, maar, in situatie 1 moet je in je presentation laag (of waar je je BO ook gebruikt) nog weten dat je eerst de ConStr moet setten. Verder moet je in je DAL weten hoe je BO eruit ziet.
In situatie twee weet de BO van de DAL, en de Presentation Layer van de BO, maar verder weten ze niks. Dat lijkt mij iets netter :).


Inderdaad, de layers zijn beter afgeschermd van elkaar, maar het zetten van de connectionstring in de BO vind ik zo geen goed idee.

Stel, je hebt een .NET webapplicatie. Dan ga je die connectiestring halen uit de web.config. Als je diezelfde classes wilt gebruiken in een Windows applicatie, dan ga je die connectionstring ergens anders vandaan halen; dan zouden die BO classes of DAL classes dus moeten aangepast worden.
Door situatie1 toe te passen, specifieer je in de presentation layer vanwaar die connectionstring moet komen, en is dat dus makkelijker aanpasbaar.

https://fgheysels.github.io/


  • EfBe
  • Registratie: Januari 2000
  • Niet online
whoami schreef op 19 December 2002 @ 08:32:
Inderdaad, de layers zijn beter afgeschermd van elkaar, maar het zetten van de connectionstring in de BO vind ik zo geen goed idee.
Stel, je hebt een .NET webapplicatie. Dan ga je die connectiestring halen uit de web.config. Als je diezelfde classes wilt gebruiken in een Windows applicatie, dan ga je die connectionstring ergens anders vandaan halen; dan zouden die BO classes of DAL classes dus moeten aangepast worden.
Nee hoor. Als je de AppSettingsReader class gebruikt leest deze gewoon de appSettings tag uit, of dat in een web.config file staat of in een app.config file, dat maakt niets uit. Zodoende is je DAL wel degelijk portable tussen webapps en normale apps. Vandaar dat het bewaren van je connectionstring in een .config file beter is dan deze ergens hardcoded in te declareren, temeer omdat je via een .config file je connectionstring meteen kunt aanpassen, mocht bv je applicatie worden verplaatst naar een andere server, passwords wijzigen etc.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
Is het eigenlijk wel een goed idee om je connectionstring in een app.config file te plaatsen. (Ik denk aan security).
Gebruik maken van de web.config voor een web-app is idd de beste plaats om de connectionstring te zetten, de normale gebruiker kan daar toch niet aan.
Bij een app.config echter lijkt het me dat de gebruiker van de applicatie gewoon toegang heeft tot die connectionstring in die config-file (dus hij kan evt. ook de username/pwd zien). Je kunt die connectionstring natuurlijk encrypten, maar toch....
of hoe zit dat juist met die app.config, 'k heb daar nog geen ervaring mee.

https://fgheysels.github.io/


  • EfBe
  • Registratie: Januari 2000
  • Niet online
whoami schreef op 19 December 2002 @ 09:48:
Is het eigenlijk wel een goed idee om je connectionstring in een app.config file te plaatsen. (Ik denk aan security).
Gebruik maken van de web.config voor een web-app is idd de beste plaats om de connectionstring te zetten, de normale gebruiker kan daar toch niet aan.
Bij een app.config echter lijkt het me dat de gebruiker van de applicatie gewoon toegang heeft tot die connectionstring in die config-file (dus hij kan evt. ook de username/pwd zien). Je kunt die connectionstring natuurlijk encrypten, maar toch....
of hoe zit dat juist met die app.config, 'k heb daar nog geen ervaring mee.
Heb je wel gelijk in. Veelal is het dan ook nuttig om met windows authentication te werken en roles in SQLServer: de rechten op de sp's en tables definieer je middels roles en niet users. Daarna voeg je de windows users toe aan SQLServer en aan de betreffende database en daarna aan de roles naar keuze. Hierdoor heb je een connectionstring waar geen username/password in staat en waarbij je toch per user security kunt regelen. Voor win32 apps lijkt me dat wel de beste keuze.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • Scare360
  • Registratie: Juli 2001
  • Laatst online: 19-08 16:50
whoami schreef op 19 December 2002 @ 09:48:
Is het eigenlijk wel een goed idee om je connectionstring in een app.config file te plaatsen. (Ik denk aan security).
Gebruik maken van de web.config voor een web-app is idd de beste plaats om de connectionstring te zetten, de normale gebruiker kan daar toch niet aan.
Bij een app.config echter lijkt het me dat de gebruiker van de applicatie gewoon toegang heeft tot die connectionstring in die config-file (dus hij kan evt. ook de username/pwd zien). Je kunt die connectionstring natuurlijk encrypten, maar toch....
of hoe zit dat juist met die app.config, 'k heb daar nog geen ervaring mee.
Voor load balancing kun je vaak niet veel anders.

Acties:
  • 0 Henk 'm!

  • Alarmnummer
  • Registratie: Juli 2001
  • Laatst online: 09-07-2024

Alarmnummer

-= Tja =-

tijn schreef op 18 December 2002 @ 13:29:
[...]
Vorige week Patterns of Enterprise Application Architecture besteld. Wellicht komen we daar weer een stapje verder mee...
2x besteld :) En ik zal mijn bevindingen hier ook plaatsen. Het lijkt me een heel aardig boek (heb het al even bekeken op amazon en heeft hele goeie recenties gekregen).

[ Voor 18% gewijzigd door Alarmnummer op 23-12-2002 18:28 ]


Acties:
  • 0 Henk 'm!

  • whoami
  • Registratie: December 2000
  • Laatst online: 13:23
Ik ben ondertussen ook dit topic aan het doornemen....

Alarmnummer schreef op 23 december 2002 @ 18:28:
[...]


2x besteld :) En ik zal mijn bevindingen hier ook plaatsen. Het lijkt me een heel aardig boek (heb het al even bekeken op amazon en heeft hele goeie recenties gekregen).


Ik heb het ook besteld (1x). (Wie besteld een boek nu 2x? Je kan hetzelfde boek ook 2x lezen hoor.... ;) :+ )

https://fgheysels.github.io/

Pagina: 1