Trasformare il codice strutturale in codice orientato agli oggetti

3

Questa è una piccola sperimentazione da parte mia perché avevo scritto molto codice procedurale ai tempi della scuola, quindi ho difficoltà a pensare in modo OOP, cioè a trovare classi e rapporti appropriati tra loro. So che non posso farlo ogni volta ma ho bisogno di creare una sorta di correlazione che possa aiutarmi a capire se sto pensando nella giusta direzione o no.

Di seguito è riportato il codice di un problema di allocazione del parcheggio:

/* 

A parking lot can have many slots which has different size.
A slot can be occupied by at most two vehicles.

Checkin:

 * Find the slot for the vehicle based on size

Checkout:

 * Calculate total fare based on vehicle size and duration

Questions:

  * Design bottom up or top down i.e. Vehicle, Slot or ParkingLot 
*/ 

// Slot details, 3 slots
capacity = [4, 2, 2];

// Vehicle details, 4 vehicles
sizes  = [2, 1, 2, 2];
parked = [1, 0, 1, 1];
start  = ['001', nil, '002', '003'];
end    = ['005', nil, '006', '008'];

// slot vehicle relation
vehicleSlot = [0, nil, 0, 1];

function checkin (vehicle) {
  for slot, size in capacity:
    if capacity[slot] >= size[vehicle]:
      start[vehicle]  = currentTimestamp;
      vehicleSlot[vehicle] = slot;
      capacity[slot] -= size[vehicle];
      return true;
  return 'No slot empty'
}

function checkout (vehicle) {
  if (vehicleSlot[vehicle] == nil):
    return 'Invalid checkout'

  slot = vehicleSlot[vehicle];
  capacity[slot] += size[vehicle];
  end[vehicle] = currentTimestamp;
  vehicleSlot[vehicle] = nil;
  return pricingAlgo(vehicle);
}

function pricingAlgo (vehicle) {
  // size * duration
  var duration = (end[vehicle] - start[vehicle]);
  return sizes[vehicle] * duration;
}

Il codice potrebbe non essere completamente corretto ma mostra ciò che sto cercando di fare almeno. Devo sapere se esiste un modo per far evolvere questo codice in un buon programma OOP?

    
posta CodeYogi 18.08.2017 - 22:34
fonte

3 risposte

8

Il problema fondamentale qui non è procedurale rispetto a OOP. È basato su indice indiretto rispetto alla struttura dati.

Il codice che hai presentato sta sfruttando il potentissimo concetto di indirezione. Ma è nudo! Le cose sono quello che sono a causa di dove sono in qualche cosa indicizzata. Funziona, ma mi fa male alla testa. Principalmente perché lascia alcune cose come un'idea che non è possibile definire abbastanza per dargli un nome.

È talmente confuso che in effetti mi ci è voluto un po 'per rendermi conto che non stai affatto forzando la tua seconda regola aziendale:

A slot can be occupied by at most two vehicles

Il che significa che se la tua capacità è [4, 2, 2] dovrei essere in grado di montare veicoli la cui dimensione totale è 16. Il tuo codice scenderà al massimo a 8. O quello o è un bug per esprimere una capacità di slot come 3 o qualsiasi altro numero dispari. Che è solo una cosa pericolosa da fare per il programmatore di manutenzione.

L'indirzione è potente. Basta che probabilmente si possa applicare questa regola aggiungendo una matrice freeSlots , una nuova condizione sul if e un nuovo calcolo. Ma imploro che tu vada in un modo diverso. Perché? Per via di quanto sarebbe dispersa questa regola.

Questa regola è fondamentalmente una regola di costruzione. Non ti è permesso costruire un parcheggio che contenga un numero dispari di macchine. È estremamente allettante ignorare l'intero concetto di "slot" e preoccuparsi solo degli spazi che prendono una macchina ciascuno. Quindi assicurati di averne sempre un numero pari.

Ma ciò costringe ad emergere una regola nascosta: due spazi nello stesso slot DEVONO avere le stesse dimensioni! È allettante a questo punto rinunciare e dire "bene allora la soluzione OOP è creare un oggetto Slot che abbia A e B spazi.

Dico aspetta. Questa nuova regola è anche una regola di costruzione. Non ti è permesso costruire un parcheggio con un numero dispari di macchine e ogni spazio pari deve avere le stesse dimensioni del prossimo spazio dispari. Fallo e ora il concetto di slot scompare dopo la costruzione.

List<Spaces> spaces = new ParkingLot(4, 2, 2).toSpaces();

Fatto in questo modo, gli slot potrebbero ancora esistere, da qualche parte, ma qui non dobbiamo saperli. Il che è positivo poiché, proprio ora, non ci importa di loro.

Se in seguito risulta importante preservare il concetto di slot anche dopo la costruzione, possiamo aggiungerlo perché è nascosto in ParkingLot e non viene eseguito qui fuori con Vehicles . Il fattore complicante è anche se aggiungiamo che abbiamo bisogno di un modo per attraversarli. Il modello di visitatore potrebbe essere d'aiuto, ma dal momento che non dobbiamo risolverlo ora non lo farei adesso.

Ora, se sei un purista OOP, probabilmente ti stai lamentando del fatto che ho esposto degli spazi. Sì, potrei inserire tutto ciò che è necessario per eseguire il ciclo dell'elenco in ParkingLot . Se l'avessi fatto, avrei avuto questo aspetto:

Vehicle v1 = new Vehicle(2);
Vehicle v2 = new Vehicle(1);
Vehicle v3 = new Vehicle(2);
Vehicle v4 = new Vehicle(2);

Port out = new OutputPort();
Lot lot = new ParkingLot(out, 4, 2, 2)

JButton checkIn1=new JButton("Check in Vehicle 1");  
checkIn1.addActionListener(new ActionListener(){  
    public void actionPerformed(ActionEvent e){  
        lot.checkIn(v1);  
    }  
});  

JButton checkOut1=new JButton("Check out Vehicle 1");  
checkOut1.addActionListener(new ActionListener(){  
    public void actionPerformed(ActionEvent e){  
        lot.checkOut(v1);
    }  
});  

...

Perché tutto questo non ha senso? Perché hai bisogno di un modo per introdurre il tempo. Potresti farlo con Thread.sleep(1000) ma mi sentivo impertinente.

Sto trattando il Vehicle come un oggetto valore semplice perché non vedo alcuna regola aziendale in esso. Perciò può esporre le sue dimensioni con un getter. Ma non dovevo. L'avvio dello spettacolo con v1.checkIn(lot) nel codice JButton consentirebbe a Vehicle di nascondere la sua dimensione fino a quando non si sentirà come dire il lot con lot.checkIn(this, this.size) . In entrambi i casi, lot non ha bisogno di richiamare e decide autonomamente se il veicolo si adatta. Una volta che lot decide cosa succede, può riportare i risultati attraverso OutputPort. Che potrebbe essere un registro, un controllo della GUI o il tuo altoparlante audio per tutto quello che ci interessa.

C'è spazio qui per inserire il codice che hai già scritto (con un po 'di massaggio). Questo è quello che considererei un modo molto OOP di risolvere questo problema. Principalmente perché ogni comunicazione avviene in modo polimorfico. Gli OOPers della vecchia scuola chiamavano questo "passaggio di messaggi". Lot non sa che tipo di Vehicle sta ottenendo. I veicoli non sanno a cosa stanno effettuando il check-in. Tutto quello che c'è da sapere è come parlarsi.

    
risposta data 19.08.2017 - 02:19
fonte
3

Secondo Alex Orso nel suo corso di sviluppo software , il design OOP inizia con alcuni grammatica;

  1. Cerca nomi, in genere corrispondono a classi, posso vedere le parole vehicle , Slot e ParkingLot nel tuo codice
  2. Quindi cerca i verbi e associali ai nomi identificati, che renderebbero i metodi oggetto, qui hai checkin e checkout sono due metodi appartenenti alla classe del veicolo
  3. Quindi cerchi aggettivi e descrizioni delle classi identificate per definire gli attributi, ad es. size per Slot e anche per Vehicle
  4. Cerca relazioni tra queste classi, possiamo capire che un ParkingLot avrebbe uno o più Solt s di tipi diversi e che un Vehicle userebbe uno di quei Slot s forniti dal %codice%

È anche chiaro che esiste uno stato associato all'utilizzo di ParkingLot di Vehicle che ci aiuterebbe a calcolare il prezzo. Ciò implica una classe nascosta Slot utilizzata da Timer .

Ora dobbiamo sostituire le variabili e le funzioni globali con metodi e attributi incapsulati all'interno di ogni classe, in modo tale che solo questa classe sarebbe responsabile dell'uso. Pensateci in questo modo, quale classe tra i 4 identificati terrà traccia della sua occupazione, è il Slot , il Vehicle o il Slot ? E quale invierà in arrivo ParkingLot s per il check-in su Vehicle ? Il Slot contiene ParkingLot s oltre a Vehicle s e Slot ?

È così che penso al problema e al suo design. Quindi si reiterra la progettazione introducendo scenari e casi speciali finché non si dispone di un design valido.

Infine, ogni volta che hai un prodotto funzionante, puoi iniziare a consultare Modelli di design per avere un codice più efficiente ed elegante

    
risposta data 18.08.2017 - 23:23
fonte
-1

Prospettiva DDD

Vorrei iniziare questo esempio dalla prospettiva DDD. Un buon punto di partenza è cercare i limiti di coerenza durante comando (al contrario di query). Di conseguenza, trovo un aggregato, i principali blocchi costitutivi del DDD.

Nel tuo primo caso d'uso, check-in, un comando è di riservare uno slot adatto. Un limite di coerenza è che non deve essere superata una capacità di slot. Quindi un ovvio aggregato è una Slot. Ed è importante che almeno in questo caso d'uso non abbiamo bisogno di un aggregato di parcheggio, dal momento che il suo stato non è cambiato. Quindi ecco un servizio di applicazione per il caso di utilizzo corrente. Serve come luogo di ambientazione ambientale. Tutti i repository, i client API esterni, ecc. Vengono inseriti qui:

class CheckInApplication
{
    private $slotRepository;
    private $locker;

    public function act(Vehicle $vehicle)
    {
        $this->locker->lock();

        if (is_null($slot = $this->slotRepository->getSuitable($vehicle))) {
            $this->locker->unlock();
            return new NoVacantSpaceResponse();
        }

        $this->slotRepository->save($slot->reserve($vehicle));

        $this->locker->unlock();

        return SlotReservedResponse();
    }
}

Ed ecco come potrebbe apparire uno slot che si riserva:

class Slot
{
    private $id;
    private $capacity;
    private $stays;

    public function __construct(SlotId $id, Capacity $capacity, Stays $stays)
    {
        $this->id = $id;
        $this->capacity = $capacity;
        $this->stays = $stays;
    }

    public function reserve(Vehicle $vehicle)
    {
        if ($this->capacity < $vehicle->capacity()) {
            throw new Exception('Vacant space is not enough');
        }
        $this->capacity = $this->capacity - $vehicle->capacity();
        $this->stays->add(new Stay($this->vehicle, new DateTime('now')));
    }
}

Lo slot controlla le sue regole di integrità, che può ospitare un veicolo e passa all'oggetto Soggiorni. È a sua volta responsabile della conoscenza del dominio di Slot di poter ospitare non più di due veicoli:

class Stays
{
    private $stays;

    public function __construct(array $stays)
    {
        $this->stays = $stays;
    }

    public function add(Stay $stay)
    {
        if (sizeof($this->stays) > 1) {
            throw new Exception('Only two cars per slot allowed');
        }

        $this->stays[] = $stay;
    }
}

Puoi anche aggiungere un nuovo Stay a Stays , che contiene le informazioni su quale veicolo occupava lo slot e quando. Sarà utile un po 'più tardi.

Ho preso in considerazione due casi d'uso separati dal tuo scenario di pagamento. Quindi il mio secondo è in carica. Dopotutto, nessuno ti lascerà andare fino a quando non ti verrà addebitato. Ecco un servizio di applicazione:

class ChargeVehicleApplicationService
{
    private $repository;

    public function __construct(SlotRepository $repository)
    {
        $this->repository = $repository;
    }

    public function act(Vehicle $vehicle, SlotId $slotId)
    {
        try {
            return new ChargeResponse($this->repository->getById($slotId)->charge($vehicle));
        } catch (Exception $e) {
            return ErrorResponse($e->getMessage());
        }
    }
}
La classe

Slot viene aggiornata con il nuovo metodo:

public function charge(Vehicle $vehicle)
{
    $this->stays->get($vehicle)->charge();
}

Penso che sia una buona idea che la classe Stay calcoli l'addebito stesso. Dopo tutto, è un proprietario dei dati per una logica algoritmica. Ecco qui ( Stay class):

public function charge()
{
    return (DateTime('now') - $this->arrivedAt) * $this->vehicle->capacity();
}

E infine il terzo caso d'uso, rilasciando il veicolo (nella vita reale mi attenersi a un solo termine, ma ora visto che sto scrivendo il codice al volo potrei essere un po 'volubile). Come sempre, ecco un servizio di applicazione:

class CheckoutVehicleApplicationService
{
    private $repository;

    public function __construct(SlotRepository $repository)
    {
        $this->repository = $repository;
    }

    public function act(Vehicle $vehicle, SlotId $slotId)
    {
        return
            $this->repository
                ->save(
                    $this->repository
                        ->getById($slotId)
                            ->releaseFrom($vehicle)
                )
            ;
    }
}

Ecco la nuova funzionalità di Slot :

public function releaseFrom(Vehicle $vehicle)
{
    $this->capacity = $this->capacity + $vehicle->capacity();
    $this->stays->releaseFrom($vehicle);
}

Il metodo% Stay rimuove solo il Stay corrispondente:

public function releaseFrom(Vehicle $vehicle)
{
    $this->stays =
        array_filter(
            $this->stays,
            function (Stay $stay) use ($vehicle) {
                return !$stay->containsVehicle($vehicle);
            }
        );
}

Quindi, anche se questo modello e questo codice non sono pensati molto bene, ma mostra che con il concetto aggregato in mente e il pensiero degli oggetti nel cuore, si può arrivare a distribuire abbastanza responsabilità tra gli oggetti del dominio. E questo è buono, dal momento che ogni oggetto è abbastanza semplice.

RDD prospettiva

Questo approccio implica accenti leggermente diversi, sebbene abbia gli stessi principi OO fondamentali. E può essere combinato con DDD in qualsiasi modo tu voglia. Quindi quando utilizzo questa tecnica, di solito inizio con una storia di design. Di cosa tratta la tua app? Stai modellando un parcheggio. Il tuo software è tenuto a definire (da chi?) Se c'è una stanza libera per un veicolo, e se c'è, trovare uno slot adeguato al check-in. Alla cassa è necessario pagare un veicolo (da chi?) Una tariffa in base alle dimensioni del veicolo e alla durata del soggiorno.

Il prossimo passo che faccio è identificare gli oggetti candidati e le loro responsabilità. Consideriamo un caso utente per il check-in. Ho un parcheggio - contiene dati sulle slot. Quindi è una buona misura per rispondere alla domanda su uno spazio libero e per trovare uno slot adatto. Implementerebbe questo compito da solo? Io non la penso così Molto probabilmente il parcheggio collaborerà con le slot, che in realtà sono consapevoli del fatto che siano libere o meno, e se lo sono, allora quanto spazio rimane. Se c'è qualche posto libero, può accettare un veicolo. Quindi ho due oggetti con una responsabilità ciascuno.

Tecnicamente, di solito non ci sono repository e ogni oggetto è responsabile della sua responsabilità. Ad esempio, il salvataggio di un oggetto è considerato come la responsabilità di quell'oggetto. Lo stesso vale per la visualizzazione. Quindi il codice di solito sembra davvero strano per la maggior parte dei programmatori OO. Dato che ci sono già un sacco di codice qui, lascio un link di come potrebbe essere questo.

    
risposta data 26.11.2017 - 16:45
fonte