Quale codice dovrebbe essere incluso in una classe astratta?

9

Ultimamente mi preoccupo dell'uso di classi astratte.

A volte una classe astratta viene creata in anticipo e funziona come un modello di come funzionano le classi derivate. Ciò significa, più o meno, che forniscono alcune funzionalità di alto livello ma lascia fuori determinati dettagli da implementare per classi derivate. La classe astratta definisce la necessità di questi dettagli mettendo in atto alcuni metodi astratti. In questi casi, una classe astratta funziona come un progetto, una descrizione di alto livello della funzionalità o qualsiasi altra cosa si voglia chiamare. Non può essere utilizzato da solo, ma deve essere specializzato per definire i dettagli che sono stati lasciati fuori dall'implementazione di alto livello.

Altre volte, accade che la classe astratta venga creata dopo la creazione di alcune classi "derivate" (poiché la classe genitore / astratto non è presente non sono ancora state derivate, ma tu sai cosa intendo). In questi casi, la classe astratta viene solitamente utilizzata come luogo in cui è possibile inserire qualsiasi tipo di codice comune che le classi derivate attuali contengano.

Avendo fatto le osservazioni di cui sopra, mi chiedo quale di questi due casi dovrebbe essere la regola. Qualunque tipo di dettagli dovrebbe essere gorgogliato nella classe astratta solo perché attualmente sono comuni in tutte le classi derivate? Dovrebbe esserci il codice comune che non fa parte di una funzionalità di alto livello?

Il codice che potrebbe non avere alcun significato per la classe astratta è lì solo perché è comune per le classi derivate?

Lasciatemi fare un esempio: la classe astratta A ha un metodo a () e un metodo astratto aq (). Il metodo aq (), in entrambe le classi derivate AB e AC, utilizza un metodo b (). Dovrebbe essere spostato b in A? Se sì, allora nel caso qualcuno guardasse solo A (facciamo finta che AB e AC non ci siano), l'esistenza di b () non avrebbe molto senso! È una cosa negativa? Qualcuno dovrebbe essere in grado di dare un'occhiata in una classe astratta e capire cosa sta succedendo senza visitare le classi derivate?

Per essere onesti, al momento di chiederlo, tendo a credere che scrivere una classe astratta che abbia senso senza dover cercare nelle classi derivate è una questione di codice pulito e architettura pulita. Ι non mi piace molto l'idea di una classe astratta che si comporta come un dump per ogni tipo di codice che sembra essere comune in tutte le classi derivate.

Che cosa pensi / fai pratica?

    
posta Alexandros Gougousis 28.10.2017 - 01:09
fonte

6 risposte

4

Let me give an example: Abstract class A has a method a() and an abstract method aq(). The method aq(), in both derived classes AB and AC, uses a method b(). Should b() moved to A ? If yes, then in case someone looks only at A (let's pretend AB and AC are not there), the existence of b() would make no much sense! Is this a bad thing? Should someone be able to have a look in an abstract class and understand what is going on without visiting the derived classes?

Ciò che ti chiedi è dove posizionare b() , e, in un altro senso, una domanda è se A è la scelta migliore come super-classe immediata per AB e AC .

Sembra che ci siano tre scelte:

  1. lascia b() in AB e AC
  2. crea una classe intermedia ABAC-Parent che eredita da A e che introduce b() , e viene quindi utilizzata come super classe immediata per AB e AC
  3. metti b() in A (non sapendo se un'altra futura classe AD vorrà b() o no)
  1. soffre di non essere DRY .
  2. soffre di YAGNI .
  3. così, questo lascia questo.

Fino a quando un'altra classe AD che non vuole b() si presenta, (3) sembra la scelta giusta.

Quando viene presentato un AD , allora possiamo rifattorizzare l'approccio in (2) - dopotutto è un software!

    
risposta data 28.10.2017 - 05:21
fonte
2

Una classe astratta non è destinata a essere la base di dumping per varie funzioni o dati che vengono gettati nella classe astratta perché è conveniente.

L'unica regola empirica che sembra fornire l'approccio orientato agli oggetti più affidabile ed estensibile è " Preferisci la composizione sull'ereditarietà ". Una classe astratta viene probabilmente considerata come una specifica di interfaccia che non contiene alcun codice.

Se si dispone di un metodo che è un tipo di metodo di libreria che accompagna la classe astratta, un metodo che è il mezzo più probabile per esprimere alcune funzionalità o comportamenti che le classi derivate da una classe astratta richiedono in genere, allora ha senso per creare una classe tra la classe astratta e altre classi in cui questo metodo è disponibile. Questa nuova classe fornisce una particolare istanziazione della classe astratta che definisce una particolare alternativa o percorso di implementazione fornendo questo metodo.

L'idea di una classe astratta è di avere un modello astratto di ciò che le classi derivate che effettivamente implementano la classe astratta dovrebbero fornire per quanto riguarda il servizio o il comportamento. L'ereditarietà è facile da utilizzare e molte volte le classi più utili sono composte da varie classi che utilizzano un schema di mix .

Tuttavia ci sono sempre le domande sul cambiamento, cosa cambierà e come cambierà e perché cambierà.

L'ereditarietà può portare a fragili e fragili corpi di origine (vedi anche Ereditarietà: basta smettere di usarlo già! ).

The fragile base class problem is a fundamental architectural problem of object-oriented programming systems where base classes (superclasses) are considered "fragile" because seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction. The programmer cannot determine whether a base class change is safe simply by examining in isolation the methods of the base class.

    
risposta data 28.10.2017 - 03:28
fonte
1

La tua domanda suggerisce uno o un approccio a classi astratte. Ma penso che dovresti considerarli solo come un altro strumento nella tua cassetta degli attrezzi. E poi la domanda diventa: per quali lavori / problemi sono le classi astratte lo strumento giusto?

Un eccellente caso d'uso è implementare il modello di metodo del modello . Metti tutta la logica invariante nella classe astratta e la logica variante nelle sue sottoclassi. Si noti che la logica condivisa in sé è incompleta e non funzionale. La maggior parte delle volte si tratta di implementare un algoritmo, in cui diversi passaggi sono sempre gli stessi, ma almeno un passaggio varia. Metti questo passo come un metodo astratto che viene chiamato da una delle funzioni all'interno della classe astratta.

Sometimes an abstract class is created in advance and work as a template of how the derived classes would work. That means, more or less, that they provide some high level functionality but leaves out certain details to be implemented by derived classes. The abstract class defines the need for these details by putting in place some abstract methods. In such cases, an abstract class works like a blueprint, a high level description of the functionality or whatever you wanna call it. It cannot be used on its own, but must be specialized to define the details that have been left out of the high level implementation.

Penso che il tuo primo esempio sia essenzialmente una descrizione del modello del metodo template (correggimi se sbaglio), quindi considererei questo come un caso d'uso perfettamente valido di classi astratte.

Some other times, it happen that the abstract class is created after the creation of some "derived" classes (since the parent/abstract class is not there they have not been derived, yet, but you know what I mean). In these cases, the abstract class is usually used as a place where you can put any kind of common code that current derived classes contain.

Per il tuo secondo esempio, penso che usare le classi astratte non sia la scelta ottimale, perché ci sono metodi superiori per gestire la logica condivisa e il codice duplicato. Supponiamo che tu abbia una classe astratta A , classi derivate B e C e che entrambe le classi derivate condividano qualche logica sotto forma di metodo s() . Per decidere l'approccio corretto per eliminare la duplicazione, è importante sapere se il metodo s() è parte dell'interfaccia pubblica o meno.

Se non lo è (come nel tuo esempio concreto con metodo b() ), il caso è abbastanza semplice. Basta creare una classe separata da essa, estraendo anche il contesto necessario per eseguire l'operazione. Questo è un classico esempio di composizione sull'eredità . Se c'è poco o nessun contesto necessario, una semplice funzione di supporto potrebbe già essere sufficiente come suggerito in alcuni commenti.

Se s() fa parte dell'interfaccia pubblica, diventa un po 'più complicato. Partendo dal presupposto che s() non ha nulla a che fare con B e C essendo correlato a A , non devi mettere s() all'interno di A . Quindi dove metterlo allora? Vorrei discutere per dichiarare un'interfaccia separata I , che definisce s() . Poi di nuovo, dovresti creare una classe separata che contenga la logica per implementare s() e sia B che C dipendono da esso.

Infine, qui è un collegamento a una risposta eccellente di un'interessante domanda su SO che potrebbe aiutarti a decidere quando andare per un'interfaccia e quando per una classe astratta:

A good abstract class will reduce the amount of code that has to be rewritten because it's functionality or state can be shared.

    
risposta data 28.10.2017 - 10:46
fonte
1

Mi sembra che manchi il punto di orientamento dell'oggetto, sia logicamente che tecnicamente. Descrivi due scenari: raggruppamento del comportamento comune dei tipi in una classe base e polimorfismo. Queste sono entrambe applicazioni legittime di una classe astratta. Ma se sia necessario rendere la classe astratta o meno dipende dal modello analitico, non dalle possibilità tecniche.

Dovresti riconoscere un tipo che non ha incarnazioni nel mondo reale, ma getta le basi per tipi specifici che esistono. Esempio: un animale. Non esiste una cosa come un animale. È sempre un cane o un gatto o qualsiasi altra cosa, ma non esiste un vero animale. Eppure Animal li inquadra tutti.

Quindi parli di livelli. I livelli non fanno parte dell'accordo quando si tratta di ereditarietà. Né si riconoscono dati comuni o comportamenti comuni, si tratta di un approccio tecnico che probabilmente non aiuterà. Dovresti riconoscere uno stereotipo e quindi inserire la classe base. Se non esiste uno stereotipo di questo tipo, è possibile che tu stia meglio con un paio di interfacce implementate da più classi.

Il nome della tua classe astratta insieme ai suoi membri dovrebbe avere senso. Deve essere indipendente da qualsiasi classe derivata, sia tecnicamente che logicamente. Come Animal potrebbe avere un metodo astratto Eat (che sarebbe polimorfico) e le proprietà astratte booleane IsPet e IsLifestock, che ha senso senza conoscere gatti o cani o maiali. Qualsiasi dipendenza (tecnica o logica) dovrebbe andare solo in un modo: dalla discendente alla base. Neanche le classi base dovrebbero avere alcuna conoscenza delle classi discendenti.

    
risposta data 28.10.2017 - 18:56
fonte
0

Primo caso , dalla tua domanda:

In such cases, an abstract class works like a blueprint, a high level description of the functionality or whatever you wanna call it.

Secondo caso:

Some other times ... the abstract class is usually used as a place where you can put any kind of common code that current derived classes contain.

Quindi la tua domanda :

I am wondering which of these two cases should be the rule. ... Should code that may have no meaning for the abstract class itself be there just because it happens to be common for the derived classes?

IMO hai due diversi scenari, quindi non puoi disegnare una singola regola per decidere quale design deve essere applicato.

Per il primo caso, abbiamo cose come Metodo modello modello già menzionato in altre risposte.

Per il secondo caso, hai fornito l'esempio della classe astratta A che riceve un metodo b (); IMO il metodo b () deve essere spostato solo se ha senso per TUTTE le altre classi derivate. In altre parole, se lo spostate perché è usato in due punti, probabilmente questa non è una buona scelta, perché domani potrebbe esserci una nuova classe derivata concreta su cui b () non ha alcun senso. Inoltre, come hai detto, se guardi a una classe separatamente e b () non ha senso in quel contesto, allora probabilmente è un suggerimento che questa non sia una buona scelta di design.

    
risposta data 03.11.2017 - 14:13
fonte
-1

Ho avuto esattamente le stesse domande qualche tempo fa!

Ciò a cui sono arrivato quando mi imbatto in questo numero, scopro che esiste un concetto nascosto che non ho trovato. E questo concetto molto probabilmente dovrebbe essere espresso con alcuni valore-oggetto . Hai un esempio molto astratto, quindi temo di non poter dimostrare cosa intendo con il tuo codice. Ma ecco un caso tratto dalla mia pratica. Avevo due classi che rappresentavano un modo per inviare richieste a risorse esterne e analizzare la risposta. C'erano delle somiglianze nel modo in cui sono state formulate le richieste e in che modo sono state analizzate le risposte. Ecco come appariva la mia gerarchia:

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

Tale codice indica che io uso in modo errato l'ereditarietà. È così che può essere refactored:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

Da allora tratto molto attentamente l'eredità perché sono stato ferito molte volte.

Da allora sono arrivato a un'altra conclusione sull'ereditarietà. Sono fermamente contrario a ereditarietà basata sulla struttura interna . Rompe l'incapsulamento, è fragile, è procedurale dopotutto. E quando decompone il mio dominio nel modo giusto , l'ereditarietà semplicemente non avviene molto spesso.

    
risposta data 01.11.2017 - 16:39
fonte

Leggi altre domande sui tag