[OOP] Klassen voor complexe getallen

Pagina: 1
Acties:

Acties:
  • 0 Henk 'm!

  • dtech
  • Registratie: Juni 2005
  • Laatst online: 19-09 15:37
Hallo allemaal,

ter oefening wil ik klass(en) voor complexe getallen maken in PHP en Java.
Nu zit ik een beetje te twijfelen over de (toch redelijk simpele) structuur, dus dacht ik: laat ik GoT eens om zijn mening vragen :)

Zoals gezegd gaat het om complexe getallen. Voor degenen die er onbekend mee zijn: het zijn getallen volgens "a + b*i" waarbij a en b reele ("normale") getallen zijn en i2 = -1.
Nu is er nog een andere vorm waarin je deze getallen kan opslaan, de polaire vorm: r(cos(a) + sin(a)*i, de andere heet de Cartetische vorm). De polaire vorm vereenvoudigd sommige moeilijkere berekeningen (maar maakt eenvoudige moeilijker)
Daar zit hem de crux, een complex getal heeft dus twee gepresentaties, nu zit ik te twijfelen hoe ik dit ga doen:

1. Beide opslaan in één klasse en afhankelijk van de handigste die gebruiken.
Voordeel: Beide zijn altijd beschikbaar
Nadelene:
- het kost (marginaal) meer geheugen en iets meer rekentijd (aangezien na elke berekening de andere vorm berekend moet worden). Ook dit is echter heel weinig.
- Ook is er een iets grotere kans op fouten, aangezien de conversie ergens vergeten kan worden.
- Er moet een enum/flag meegegeven worden in de constructor om te bepalen welke vorm wordt meegegeven, aangezien beide argumenten in beide gevallen integers zijn
2. Aparte klassen maken waarna de gebruiker zelf kan kiezen
2a: Abstracte "Complex" class met kinderen "CartComplex" en "PolComplex", eventueel met static methode die een van beide teruggeeft (dus bijv: Complex.getCart(a, b) of Complex.getPol(r, a)
Voordeel: berekeningen gaan effiecenter als de gebruiker weet wanneer wat beter is
Nadelen:
-Gebruiker heeft meer kennis nodig van complexe getallen.
- Het is iets meer typewerk

2b. Niet-abstracte "Complex" class die getallen als een bepaalde vorm opslaat en een child class die het als de andere vorm doet in geval de te maken berekeningen daarmee efficiënter kunnen worden gedaan.
Voordelen: Makkelijker in gebruik (enkel als de gebruiker heel zeker weet dat iets efficienter is kan hij de andere klasse gebruiken)
Nadelen: - Een bepaalde vorm wordt "Belangrijker" gemaakt dan de ander

Wat vinden jullie?
Overigens is dus puur om wat meer vaardigheid te krijgen in OOP en programmeren, dus de dingen misschien wat overengineered zijn is geen probleem :)

Acties:
  • 0 Henk 'm!

  • Alain
  • Registratie: Oktober 2002
  • Niet online
De operatie's zijn niet verschillend, alleen de manier waarop je het schrijft. Ik zou dan ook voor een abstracte class gaan die alleen operatie's bevat en subclasses om de abstracte class te vullen met informatie en de aanwezige informatie op een juiste manier te presenteren. :)

You don't have to be crazy to do this job, but it helps ....


Acties:
  • 0 Henk 'm!

  • bomberboy
  • Registratie: Mei 2007
  • Laatst online: 01:33

bomberboy

BOEM!

Deze vraag hoort mss meer thuis in het software engineering & architecture subforum?

Ik zou gewoon een van beide vormen kiezen voor interne opslag, maar je klasse wel de mogelijkheid geven om beide representaties aan de gebruiker te geven. En dus telkens op dat moment gewoon de conversie te doen.

Dus voor alle setters/getters/constructors bied je beiden aan, en intern doe je er mee wat je wil. Is een of andere berekening dan makkelijker of een bepaalde representatie handiger in een bepaald geval, dan gebruik je die gewoon en doet je klasse intern eventueel een conversie.

In ieder geval niet meerdere klassen gebruiken hiervoor. Stel dat je een .add(Complex c) wil implementeren, dan moet je toch nog ergens een conversie gaan doen, maar waar en hoe? In alle mogelijke gevallen levert dit rare hacks op. Je zou immers deze verschillende Complex klassen door elkaar moeten kunnen gebruiken.

Bovendien indien je dan toch die klasse voor complexe getallen maakt, en de gebruiker zou moeten kiezen welke gebruikt wordt op basis van de interne representatie, dan vervalt het nut van die klasse (namelijk een abstractie maken van het complex getal). Op dat moment kan je immers net zo goed zelf met de interne representatie werken want de meerwaarde van een wrapper object is dan minimaal.

Acties:
  • 0 Henk 'm!

  • dtech
  • Registratie: Juni 2005
  • Laatst online: 19-09 15:37
AlainS schreef op dinsdag 03 februari 2009 @ 20:47:
De operatie's zijn niet verschillend, alleen de manier waarop je het schrijft. Ik zou dan ook voor een abstracte class gaan die alleen operatie's bevat en subclasses om de abstracte class te vullen met informatie en de aanwezige informatie op een juiste manier te presenteren. :)
Dat lijkt me nou juist niet handig. De operaties zijn juist wel verschillendd bij de verschillende representaties. Machtsverhebben is bij polaire vorm bijvoorbeeld een eitje en bij cartetische vorm bijna onmogelijk zodra de macht groter dan 2,3 wordt. Juist hierom twijfel ik.

Acties:
  • 0 Henk 'm!

  • dtech
  • Registratie: Juni 2005
  • Laatst online: 19-09 15:37
bomberboy schreef op dinsdag 03 februari 2009 @ 20:51:
Ik zou gewoon een van beide vormen kiezen voor interne opslag, maar je klasse wel de mogelijkheid geven om beide representaties aan de gebruiker te geven. En dus telkens op dat moment gewoon de conversie te doen.
De klassen krijgen natuurlijk altijd de mogelijkheid om Im(z), Arg(z), Re(z) en Im(z) op te vragen.
Dus voor alle setters/getters/constructors bied je beiden aan, en intern doe je er mee wat je wil. Is een of andere berekening dan makkelijker of een bepaalde representatie handiger in een bepaald geval, dan gebruik je die gewoon en doet je klasse intern eventueel een conversie.
Dus eigenlijk 2b. Er zou dan eventueel nog een subklasse kunnen zijn die het op de andere manier opslaat.
Misschien doe ik dat wel en test ik vervolgens even hoeveel het echt uitmaakt in de praktijk :)
In ieder geval niet meerdere klassen gebruiken hiervoor. Stel dat je een .add(Complex c) wil implementeren, dan moet je toch nog ergens een conversie gaan doen, maar waar en hoe? In alle mogelijke gevallen levert dit rare hacks op. Je zou immers deze verschillende Complex klassen door elkaar moeten kunnen gebruiken.
Dat hoeft geen probleem te zij zolang ze oftewel erven van dezelfde parent klasse oftewel er één hoofdklasse is waarvan de andere erft. Je kunt dat overal gewoon een Complex accepteren. Aangezien impliciet downcasten toegestaan is en nooit problemen veroorzaakt (tenzij je verkeerd OO programmeert)
Bovendien indien je dan toch die klasse voor complexe getallen maakt, en de gebruiker zou moeten kiezen welke gebruikt wordt op basis van de interne representatie, dan vervalt het nut van die klasse (namelijk een abstractie maken van het complex getal). Op dat moment kan je immers net zo goed zelf met de interne representatie werken want de meerwaarde van een wrapper object is dan minimaal.
Daar heb je helemaal gelijk in

Acties:
  • 0 Henk 'm!

  • bomberboy
  • Registratie: Mei 2007
  • Laatst online: 01:33

bomberboy

BOEM!

dtech schreef op dinsdag 03 februari 2009 @ 21:59:
Dus eigenlijk 2b. Er zou dan eventueel nog een subklasse kunnen zijn die het op de andere manier opslaat.
Misschien doe ik dat wel en test ik vervolgens even hoeveel het echt uitmaakt in de praktijk :)
Ik zou er niet echt een aparte subclass van maken eigenlijk. Uiteindelijk hoeft de gebruiker van die klasse er niets van af te weten en blijft dit gewoon intern.
Dat hoeft geen probleem te zij zolang ze oftewel erven van dezelfde parent klasse oftewel er één hoofdklasse is waarvan de andere erft. Je kunt dat overal gewoon een Complex accepteren. Aangezien impliciet downcasten toegestaan is en nooit problemen veroorzaakt (tenzij je verkeerd OO programmeert)
Ik denk dat je nog een aantal problemen over het hoofd ziet (of ik zie problemen waar er geen zijn :) )

stel algemene Complex en subclasses ComplexCart en ComplexPol.
In Complex kan je iets als .add(Complex c) niet implementeren want je kent niets van de interne representatie van de kinderen.

In de kinderen zelf kan je perfect die logica implementeren, indien het argument van hetzelfde type is want je kent de interne representatie.
Indien het argument niet van het zelfde type is zit je vast want je weet eigenlijk niet welk type het is en er is niet echt een mooie methode om dit op te lossen.

In je subclasses can je uiteraard ook geen verschillende methoden voor elk type implementeren, want dan moet iedere subclass van elke andere weten en moet iedere class aangepast worden indien er een extra representatie wordt toegevoegd.

Zoals je voorstelt is één algemene klasse Complex met een kind voor de andere representatie misschien wel een oplossing, maar dan moet je voor berekeningen misschien wel de onefficiënte versie gebruiken. Je zal immers terugconverteren naar die algemene klasse.

Dus persoonlijk zou ik gewoon één klasse Complex definiëren die met beide representaties kan omgaan en intern de meest handige representatie gebruikt voor ieder type berekening. (b.v. cartesisch en voor het verheffen tot een macht even switcht naar polair en na de berekening weer terug).
Indien je dit graag wil, kan je intern in die Complex class dan nog altijd wel een klasse definiëren voor iedere representatie en die gebruiken. Maar voor de gebruiker van je klasse is het denk ik alleen maar onnodig complex indien dit ook extern zichtbaar is. Voor hem moet dit transparant zijn.

Acties:
  • 0 Henk 'm!

  • Nick_S
  • Registratie: Juni 2003
  • Laatst online: 23-09 02:56

Nick_S

++?????++ Out of Cheese Error

- Er moet een enum/flag meegegeven worden in de constructor om te bepalen welke vorm wordt meegegeven, aangezien beide argumenten in beide gevallen integers zijn
Dit lijkt me ook niet erg handig, maar zou ik, zoals je in je tweede ontwerp beschrijft, met factory methods implementeren en de eigenlijke constructor verbergen.

'Nae King! Nae quin! Nae Laird! Nae master! We willna' be fooled agin!'


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 22-09 16:37

.oisyn

Moderator Devschuur®

Demotivational Speaker

Ik zou gewoon de hele polaire vorm vergeten, dat is ook een beetje de consensus in alle complex-implementaties. Voor berekeningen die je het meeste doet, doorgaans optellen en vermenigvuldigen, is dat ook veruit het meest efficient. Trigonometrische functies zijn nog altijd relatief duur.

.edit: bliep, ik bedenk me ineens dat vermenigvuldigen en delen juist efficienter is in polaire vorm 8)7

[ Voor 15% gewijzigd door .oisyn op 04-02-2009 17:43 ]

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • dtech
  • Registratie: Juni 2005
  • Laatst online: 19-09 15:37
bomberboy schreef op dinsdag 03 februari 2009 @ 23:15:
stel algemene Complex en subclasses ComplexCart en ComplexPol.
In Complex kan je iets als .add(Complex c) niet implementeren want je kent niets van de interne representatie van de kinderen.
Dat kan zeker wel. de add methode kan gewoon gebruik maken van de this.Re() of this.getRealPart() methoden (en this.Im() natuurlijk). Deze methoden zijn dan abstract en door de kinderen te implementeren
Indien het argument niet van het zelfde type is zit je vast want je weet eigenlijk niet welk type het is en er is niet echt een mooie methode om dit op te lossen.
Idem, niet de data direct benaderen maar via de Re(), Im(), Arg() en Mod() functies.
[font size=small]Het is toch een gezegde in de informatie: elk probleem kan opgelost worden met een extra laag indirectie :P[/font]
In je subclasses can je uiteraard ook geen verschillende methoden voor elk type implementeren, want dan moet iedere subclass van elke andere weten en moet iedere class aangepast worden indien er een extra representatie wordt toegevoegd.
Zie bovenstaand. Subclasses kunnen natuurlijk wel intern de methoden overwriten/overloaden om efficienter gebruik te maken van de voordelen van een bepaalde representatie.
Bijvoorbeeld:
Complex klasse: public Complex add(Complex z);
ComplexCart klasse: public ComplexCart add(ComplexCart z); public ComplexCart add(ComplexPol z);
ComplexPol klasse: public ComplexPol add(ComplexCart z); public ComplexPol add(ComplexPol z);

(bij de add methode heeft dit niet zoveel zin, maar bij sommige andere wel)

Acties:
  • 0 Henk 'm!

  • bomberboy
  • Registratie: Mei 2007
  • Laatst online: 01:33

bomberboy

BOEM!

dtech schreef op woensdag 04 februari 2009 @ 17:20:
Dat kan zeker wel. de add methode kan gewoon gebruik maken van de this.Re() of this.getRealPart() methoden (en this.Im() natuurlijk). Deze methoden zijn dan abstract en door de kinderen te implementeren
Indien all kinderen dan toch die verschillende representatie moeten kunnen weergeven, waarom zou je dan in hemelsnaam verschillende implementaties willen. Deze aanpak leidt vooral tot niet onderhoudbare code duplicatie. Waarom dan niet één gewone Complex die verschillende representaties kan weergeven en intern gewoon de meest efficiënte gebruikt voor de gewenste actie?
Idem, niet de data direct benaderen maar via de Re(), Im(), Arg() en Mod() functies.
[font size=small]Het is toch een gezegde in de informatie: elk probleem kan opgelost worden met een extra laag indirectie :P[/font]
Zelfde antwoord, bovendien zei ik dat er geen mooie oplossing voor is. Dit beschouw ik echt niet als een mooie oplossing. O-)
Zie bovenstaand. Subclasses kunnen natuurlijk wel intern de methoden overwriten/overloaden om efficienter gebruik te maken van de voordelen van een bepaalde representatie.
Bijvoorbeeld:
Complex klasse: public Complex add(Complex z);
ComplexCart klasse: public ComplexCart add(ComplexCart z); public ComplexCart add(ComplexPol z);
ComplexPol klasse: public ComplexPol add(ComplexCart z); public ComplexPol add(ComplexPol z);
(bij de add methode heeft dit niet zoveel zin, maar bij sommige andere wel)
Maar dat wil zeggen dat al je subclasses alle andere mogelijke subclasses moeten kennen. Dus indien je later een nieuwe wil toevoegen moet je ze allemaal opnieuw aanpassen. :?

Acties:
  • 0 Henk 'm!

  • dtech
  • Registratie: Juni 2005
  • Laatst online: 19-09 15:37
bomberboy schreef op woensdag 04 februari 2009 @ 17:43:
[...]

Indien all kinderen dan toch die verschillende representatie moeten kunnen weergeven, waarom zou je dan in hemelsnaam verschillende implementaties willen.
Omdat beide weliswaar dezelfde data weergeven (daarom erven ze ook van dezelfde klasse) maar er wel conversie tussen zit.
Zelfde vraag: waarom zou je en arrays en arraylisten gebruiken, ze zijn immers toch naar elkaar te converteren en geven dezelfde data weer (Als je even uit gaat van een array van Objecten en even buiten beschouwing laat dat veel talen tegenwoordig generics hebben). Precies: omdat de ene in sommige gevallen handiger/sneller is dan de andere.
Maar dat wil zeggen dat al je subclasses alle andere mogelijke subclasses moeten kennen. Dus indien je later een nieuwe wil toevoegen moet je ze allemaal opnieuw aanpassen. :?
Het hoeft niet, ze kunnen optimaliseren als je de andere kennen. Als ze de andere niet kennen (maar ze erven wel van Complex) zal gewoon de Complex methode gebruikt worden, die dezelfde resultaten geeft.

Ik heb overigens de PHP implementatie bijna klaar, daarna ga ik die porten naar java (plus wat dingetjes die niet in PHP mogelijk zijn zoals fatsoenlijk overloaden en fatsoenlijke type hinting)

Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 22-09 16:37

.oisyn

Moderator Devschuur®

Demotivational Speaker

Beetje een paradoxale opmerking. Het systeem van PHP kent wat ze zelf noemen type hinting. Als het fatsoenlijk was geweest was het gewoon strict typing geweest, zoals in Java ;)

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • bomberboy
  • Registratie: Mei 2007
  • Laatst online: 01:33

bomberboy

BOEM!

dtech schreef op woensdag 04 februari 2009 @ 22:21:
Omdat beide weliswaar dezelfde data weergeven (daarom erven ze ook van dezelfde klasse) maar er wel conversie tussen zit.
Door deze opmerking bedacht ik me plots het volgende. Eigenlijk hoort de representatie van het getal (polair of cartesisch) niet echt thuis in de Complex klasse zelf. (Op een default presentatie na)

Voor de conversie van de ene presentatie naar de andere (externe weergave) zou je een soort van formatter kunnen gebruiken gelijkaardig aan de Formatters in de java API (bv. NumberFormatter)
Zelfde vraag: waarom zou je en arrays en arraylisten gebruiken, ze zijn immers toch naar elkaar te converteren en geven dezelfde data weer (Als je even uit gaat van een array van Objecten en even buiten beschouwing laat dat veel talen tegenwoordig generics hebben). Precies: omdat de ene in sommige gevallen handiger/sneller is dan de andere.
Beiden hebben toch wel een heel andere functionaliteit, terwijl beide Complex-varianten eigenlijk identieke functionaliteit aanbieden.
Ik denk dat een vergelijking tussen een ArrayList en een LinkedList (Java) meer op zijn plaats is. (beiden implementeren de List-interface en hebben dus een gemeenschappelijke parent, hoewel het hier wel om een interface gaat natuurlijk)
In dat opzicht kan ik je dan wel volgen.
Het hoeft niet, ze kunnen optimaliseren als je de andere kennen. Als ze de andere niet kennen (maar ze erven wel van Complex) zal gewoon de Complex methode gebruikt worden, die dezelfde resultaten geeft.
Ik blijf het lelijk vinden dat beide kinderen van elkaar zouden afweten. Ik kan me bv wel vinden in het ontwerp waarbij een ComplexCartesisch een add(ComplexCartesisch) heeft en die efficiënter kan uitvoeren, maar niet dat die dat ook voor een ander kind van Complex zou hebben. Dat lijkt me het ontwerp minder net te maken.
(Voor dit specifieke geval vind ik het nog steeds nutteloos eigenlijk, maar qua algemene structuur is dit niet slecht)

In praktijk kan ik eigenlijk geen echt voordeel bedenken waarom een approach met twee zuster-implementaties van Complex nuttiger zou zijn dan één algemene implementatie die afhankelijk van het type bewerking zelf een conversie doet van de ene representatie naar de andere.

En eigenlijk horen die bewerkingen eerder thuis in een andere klasse die deze bewerkingen groepeert ipv in Complex zelf. (cfr .sort() stop je ook niet in de List zelf, maar je implementeert iets dat sorteert met een List als argument). In dat geval wordt het nog lastiger indien je twee implementaties hebt want welk type wordt dan teruggegeven na het uitvoeren van een operatie?
Dan moet je telkens parallele methode-namen definiëren.

[ Voor 14% gewijzigd door bomberboy op 04-02-2009 23:54 . Reden: aanvulling ]


Acties:
  • 0 Henk 'm!

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Ik denk dat je keuze vooral moet afhangen van de precisie die je wil bereiken.
Speelt precisie geen zo'n grote rol, kies dan simpelweg voor die waarmee je het efficientst kan werken.

Wil je toch beide, dan kan je werken met GetReal(), GetIm(), GetRadius(), GetAngle().
Implementeer dan elke operator in functie van deze 4, afhankelijk van wat het eenvoudigst (en met hoogste precisie?) werkt.

Wil je de gebruiker een keuze geven tussen beide, dan kun je gaan subclassen, waarbij je enkel deze 4 functies implementeert. Bij de ene subclass zijn de eerste 2 "gratis" terwijl de andere 2 berekeningen vergen, bij de andere subclass is het andersom. Je operators geven ook telkens een object van de superclass terug en geven een polair of cartesiaans subclass terug afhankelijk van hoe ze gerekend hebben.

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
class ComplexImpl
{
public:
  virtual double GetReal() = 0;
  virtual double GetIm() = 0;
  virtual double GetRadius() = 0;
  virtual double GetAngel() = 0;
};
class CartesianComplex : ComplexImpl { ... }
class PolarComplex : ComplexImpl { ... }

class Complex
{
public:
   static Complex FromCartesian(double a, double b) { return Complex(new CartesianComplex(a, b)); }
   static Complex FromPolar(double r, double a) { return Complex(new PolarComplex(r, a)); }
   Complex Add(Complex other)
   {
      return FromCartesian(GetReal() + other.GetReal(), GetIm() + other.GetIm());
   }
   Complex Multiply(Complex other)
   {
     return FromPolar(GetRadius() * other.GetRadius(), GetAngle() + other.GetAngle());
   }
private:
   Complex(ComplexImpl* impl) : m_pImpl(impl) { }
   ComplexImpl* m_pImpl;
}


Het voordeel aan deze benadering is dat een gebruiker van Complexe getallen de volgorde van berekeningen kan toespitsen op het minimaliseren van inefficiente berekeningen.
CartA * (CartB + CartC) _ipv_ CartA * CartB + CartA * CartC om een heel simpel voorbeeld te geven.
Daarnaast kan de gebruiker de voor hem eenvoudigste representatie kiezen.
bvb FromPolar(10, 9) schrijft gemakkelijker dan de equivalente FromCart( ..., ...)

ASSUME makes an ASS out of U and ME


Acties:
  • 0 Henk 'm!

  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 13-09 00:05
.oisyn schreef op woensdag 04 februari 2009 @ 16:36:
.edit: bliep, ik bedenk me ineens dat vermenigvuldigen en delen juist efficienter is in polaire vorm 8)7
Nee, dat is een misvatting. Begrijpelijk, want dat is het wel voor mensen. Het probleem is dat je in theorie kunt volstaan met r=r1 ∙ r2 en ϕ = ϕ1 + ϕ1, maar vanwege numerieke stabiliteit wil je ϕ in de range [0, 2∙π] houden. Die laatste modulusoperatie kost je relatief veel.

Als het echt snel moet: gebruik SSE3. Het is misschien maar een vector met 2 elementen, maar dan nog past dat beter bij SSE3 dan bij normale registers.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein

Pagina: 1