Java >> Java tutorial >  >> Tag >> class

Udvidelse af abstrakte klasser med abstrakte klasser i Java

Eksempelproblemet

Da jeg oprettede Java::Geci abstrakt klasse AbstractFieldsGenerator og AbstractFilteredFieldsGenerator Jeg stod over for et ikke alt for komplekst designproblem. Jeg vil gerne understrege, at dette problem og designet kan virke indlysende for nogle af jer, men under min nylige samtale med en juniorudvikler (min søn, specifikt Mihály, som også gennemgår mine artikler, fordi hans engelsk er meget bedre end mit) indså, at dette emne stadig kan være af værdi.

Alligevel. Jeg havde disse to klasser, felter og filtrerede felter generator. Den anden klasse udvider den første

12 abstract class AbstractFilteredFieldsGenerator                    extends AbstractFieldsGenerator {...

tilføjer ekstra funktionalitet og samtidig skal det give samme signatur til konkret implementering. Hvad betyder det?

Disse generatorer hjælper med at generere kode til en specifik klasse ved hjælp af refleksion. Derfor er inputinformationen, de arbejder på, en Class objekt. Feltgeneratorklassen har en abstrakt metode process() , som påkaldes for hvert felt. Den påkaldes fra en implementeret metode, der går over felterne og udfører påkaldelsen separat for hver. Når en konkret klasse extends AbstractFieldsGenerator og dermed implementerer denne abstrakte metode, så vil den blive kaldt. Når den samme konkrete klasse ændres, så den extends AbstractFilteredFieldsGenerator så vil den konkrete metode kun blive påberåbt for den filtrerede metode. Jeg ønskede et design, så den ENESTE ændring, der var nødvendig i betonklassen, er at ændre navnet.

Abstrakt klasse problemdefinition

Det samme problem beskrevet på en mere abstrakt måde:Der er to abstrakte klasser A og FF extends A og F giver noget ekstra funktionalitet. Begge erklærer den abstrakte metode m() som en betonklasse skal implementere. Når betonklassen C erklæringen er ændret fra C extends A til C extends F derefter påkaldelsen af ​​metoden m() bør ændres, men der bør ikke være andre ændringer i klassen C . Metoden m() påkaldes fra metoden p() defineret i klassen A . Sådan designer du F ?

Hvad er problemet med dette?

Udvider A kan gøres på to væsentligt forskellige måder:

  • F tilsidesætter m() gør det konkret at implementere den ekstra funktionalitet i m() og kalder en ny abstrakt metode, siger mx()
  • F tilsidesætter metoden p() med en version, der giver den ekstra funktionalitet (filtrering i eksemplet ovenfor) og kalder den stadig abstrakte metode m()

Den første tilgang opfylder ikke kravet om, at signaturen skal implementeres af betonklassen C bør forblive den samme. Den anden tilgang kaster den allerede implementerede funktionalitet af A til skraldet og genimplementerer det på en lidt anderledes måde. I praksis er dette muligt, men det vil helt sikkert være noget copy/paste-programmering. Dette er problematisk, lad mig ikke forklare hvorfor.

Roden til problemet

I teknik, når vi står over for et problem som det, betyder det normalt, at problemet eller strukturen ikke er godt beskrevet, og at løsningen er et sted i et helt andet område. Med andre ord er der nogle antagelser, der driver vores måde at tænke på, som er falske. I dette tilfælde er problemet, at vi antager, at de abstrakte klasser giver EN udvidelse "API" til at udvide dem. Bemærk, at API'en ikke kun er noget, du kan påberåbe dig. I tilfælde af en abstrakt klasse er API'en det, du implementerer, når du udvider den abstrakte klasse. Ligesom biblioteker kan levere forskellige API'er til forskellige måder at blive brugt på (Java 9 HTTP-klient kan send() og også sendAsync() ) abstrakte (og faktisk også ikke-abstrakte) klasser kan også give forskellige måder at udvides til forskellige formål.

Der er ingen måde at kode F på at nå vores designmål uden at ændre A . Vi har brug for en version af A der giver forskellige API til at skabe en konkret implementering og en anden, ikke nødvendigvis disjunkt/ortogonal en til at skabe en stadig abstrakt udvidelse.

Forskellen mellem API'erne i dette tilfælde er, at den konkrete implementering sigter mod at være i slutningen af ​​en opkaldskæde, mens den abstrakte udvidelse ønsker at tilslutte sig det sidste men et element i kæden. Implementeringen af ​​A skal levere API for at blive tilsluttet det sidste element i opkaldskæden. Dette er allerede løsningen.

Løsning

Vi implementerer metoden ma() i klassen F og vi vil have p() at ringe til vores ma() i stedet for at ringe direkte til m() . Ændring af A vi kan gøre det. Vi definerer ma() i A og vi kalder ma() fra p() . Versionen af ​​ma() implementeret i A skal ringe til m() uden videre at levere den originale "API" til konkrete implementeringer af A . Implementeringen af ​​ma() i F indeholder den ekstra funktionalitet (filtrering i eksemplet) og derefter kalder den m() . På den måde kan enhver konkret klasse udvide enten A eller F og kan implementere m() med nøjagtig samme signatur. Vi undgik også at kopiere/indsætte kodning med undtagelse af at kalde m() er en kode, der er den samme i de to versioner af ma() .

Hvis vi vil have klassen F kan udvides med flere abstrakte klasser end F::ma implementering bør ikke direkte kalde m() men snarere en ny mf() der kalder m() . På den måde kan en ny abstrakt klasse tilsidesætte mf() giver igen ny funktionalitet og påkald abstrakt m() .

Takeaway

  1. Programmering af abstrakte klasser er komplekst, og nogle gange er det svært at have et klart overblik over, hvem der ringer til hvem og hvilken implementering. Du kan overvinde denne udfordring, hvis du indser, at det kan være en kompleks sag. Dokumentér, visualiser, diskuter, uanset hvilken måde der kan hjælpe dig.
  2. Når du ikke kan løse et problem (i eksemplet, hvordan koder du F ) bør du udfordre miljøet (klassen A vi antog implicit at være uforanderlige ved formuleringen af ​​spørgsmålet:"Hvordan implementeres F ?”).
  3. Undgå at kopiere/indsætte programmering. (Pasta indeholder meget CH og gør din kode fed, arterierne bliver tilstoppede og til sidst vil hjertet af din applikation holde op med at slå.)
  4. Selvom det ikke er detaljeret i denne artikel, skal du være opmærksom på, at jo dybere abstraktionshierarkiet er, jo sværere er det at have et klart overblik over, hvem der ringer til hvem (se også punkt nummer 1).
  • Find en prøvedemoapplikation på https://github.com/verhas/abstractchain
  • Find originalen, en smule mere kompleks applikation, der har dette mønster på https://github.com/verhas/javageci

Java tag