Are PHP un răspuns la clasele generice de tip Java? (Programare, Java, Php, Oop, Generice, Moștenire)

Jonathan a intrebat.
a intrebat.

În timpul construirii unui cadru MVC în PHP am dat peste o problemă care ar putea fi rezolvată cu ușurință folosind genericele în stil Java. O clasă abstractă Controller ar putea arăta cam așa:

abstract class Controller {

abstract public function addModel(Model $model);

Ar putea exista un caz în care o subclasă a clasei Controller ar trebui să accepte doar o subclasă de Model. De exemplu, ExtendedController ar trebui să accepte doar ReOrderableModel în metoda addModel, deoarece oferă o metodă reOrder() la care ExtendedController trebuie să aibă acces:

class ExtendedController extends Controller {

public function addModel(ReOrderableModel $model) {

În PHP, semnătura metodei moștenite trebuie să fie exact aceeași, astfel încât indicația de tip nu poate fi schimbată cu o clasă diferită, chiar dacă clasa moștenește tipul de clasă indicat în superclasă. În java, aș face pur și simplu acest lucru:

abstract class Controller<T> {

abstract public addModel(T model);


class ExtendedController extends Controller<ReOrderableModel> {

public addModel(ReOrderableModel model) {

Dar în PHP nu există suport pentru generice. Există vreo soluție care să respecte în continuare principiile OOP?

EditațiSunt conștient de faptul că PHP nu necesită deloc indicarea tipului, dar poate că este o OOP proastă. În primul rând, nu este evident din interfață (semnătura metodei) ce fel de obiecte ar trebui să fie acceptate. Astfel, dacă un alt dezvoltator ar dori să folosească metoda, ar trebui să fie evident că sunt necesare obiecte de tipul X, fără ca acesta să fie nevoit să se uite prin implementare (corpul metodei), ceea ce reprezintă o încapsulare proastă și încalcă principiul ascunderii informațiilor. În al doilea rând, deoarece nu există siguranță de tip, metoda poate accepta orice variabilă invalidă, ceea ce înseamnă că este nevoie de o verificare manuală a tipului și de lansarea de excepții peste tot!

Comentarii

  • în php puteți trece orice tip de obiect doriți fără să vă faceți griji…  > Por dinamic.
  • Sunt deja conștient de acest lucru, dar poate că este o OOP proastă. În primul rând, nu este evident din interfață (semnătura metodei) ce fel de obiecte ar trebui să fie acceptate. Astfel, dacă un alt dezvoltator ar dori să utilizeze metoda, ar trebui să fie evident că numai obiectele de tipul X ar trebui să fie utilizate, fără ca acesta să fie nevoit să se uite prin implementare (corpul metodei), ceea ce reprezintă o încapsulare proastă și încalcă principiul ascunderii informațiilor. În al doilea rând, deoarece nu există siguranță a tipurilor, metoda poate accepta orice variabilă invalidă, ceea ce înseamnă că este nevoie de o verificare manuală a tipurilor și de lansarea de excepții peste tot. –  > Por Jonathan.
  • Este, de asemenea, o OOP proastă, deoarece încalcă LSP, ExtendedController-ul dvs. este specializat controlerul în loc să-l extindă. Dacă addModel pe părintele poate accepta un Model care necesită o subclasă addModel să accepte doar un ReOderableModel este un oop extrem de prost și o încălcare a PSL. Nu va funcționa nici în Java în niciun mod rezonabil. Acest lucru este de acum 2 ani, așa că probabil că știți toate acestea până acum… –  > Por Benjamin Gruenbaum.
  • Rețineți că există un RFC pentru generice, , în prezent în curs de redactare. –  > Por BenMorel.
8 răspunsuri
GordonM

Se pare că funcționează pentru mine (deși aruncă un avertisment Strict) cu următorul caz de test:

class PassMeIn
{

}

class PassMeInSubClass extends PassMeIn
{

}

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeInSubClass $class)
    {
        parent::processClass ($class);
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);

Dacă avertismentul strict este ceva ce nu doriți cu adevărat, îl puteți evita în felul următor.

class ClassProcessor
{
    public function processClass (PassMeIn $class)
    {
        var_dump (get_class ($class));
    }
}

class ClassProcessorSubClass extends ClassProcessor 
{
    public function processClass (PassMeIn $class)
    {
        if ($class instanceof PassMeInSubClass)
        {
            parent::processClass ($class);
        }
        else
        {
            throw new InvalidArgumentException;
        }
    }
}

$a  = new PassMeIn;
$b  = new PassMeInSubClass;
$c  = new ClassProcessor;
$d  = new ClassProcessorSubClass;

$c -> processClass ($a);
$c -> processClass ($b);
$d -> processClass ($b);
$d -> processClass ($a);

Un lucru pe care ar trebui să-l aveți în vedere totuși, aceasta nu este strict cea mai bună practică în termeni OOP. Dacă o superclasă poate accepta obiecte dintr-o anumită clasă ca argument de metodă, atunci toate subclasele sale ar trebui să poată accepta și ele obiecte din clasa respectivă. Împiedicarea subclaselor de a procesa clasele pe care superclasa le poate accepta înseamnă că nu puteți utiliza subclasa în locul superclasei și să fiți 100% sigur că va funcționa în toate cazurile. Practica relevantă este cunoscută sub numele de principiul de substituție Liskov și prevede că, printre altele, tipul de argumente ale metodelor nu poate fi decât mai slab în subclase, iar tipul valorilor de returnare nu poate fi decât mai puternic (intrarea nu poate fi decât mai generală, iar ieșirea nu poate fi decât mai specifică).

Este o problemă foarte frustrantă și eu însumi m-am confruntat cu ea de multe ori, așa că, dacă ignorarea ei într-un anumit caz este cel mai bun lucru de făcut, atunci vă sugerez să o ignorați. Dar nu vă faceți un obicei din asta sau codul dvs. va începe să dezvolte tot felul de interdependențe subtile care vor fi un coșmar pentru depanare (testarea unitară nu le va detecta, deoarece unitățile individuale se vor comporta așa cum se așteaptă, problema este interacțiunea dintre ele). Dacă o ignorați, comentați codul pentru a-i anunța pe ceilalți despre aceasta și pentru a le spune că este o alegere de proiectare deliberată.

Comentarii

  • Știți ceva? Nici măcar nu am încercat să văd dacă de fapt doar a funcționat haha. Pur și simplu am citit că nu funcționează și am crezut-o. Cred că pot folosi simbolul @ doar pentru a suprima avertismentele. Vă puteți gândi la vreun motiv pentru care ar fi o idee foarte proastă? Este fie asta, fie a doua soluție de mai jos. –  > Por Jonathan.
  • În general, trebuie să evitați cu adevărat operatorul @, pentru că, în esență, înseamnă să ascundeți un mesaj de eroare sub preș. În acest caz particular, probabil că nu va duce la probleme pe termen lung, dar nu vă puteți baza pe asta, deoarece avertismentele E_STRICT sunt indicii că limbajul se poate schimba la un moment dat și nu mai acceptă ceea ce faceți. Oricum, nu sunt sigur că ați putea să-l utilizați pentru a suprima erorile dintr-o declarație de metodă. Ați putea seta o valoare error_reporting care să excludă E_STRICT, deși, din nou, acest lucru ar însemna doar să ascundeți problema sub preș. –  > Por GordonM.
  • Manualul PHP are următorul lucru de spus despre E_STRICT: In PHP 5 a new error level E_STRICT is available. As E_STRICT is not included within E_ALL you have to explicitly enable this kind of error level. Enabling E_STRICT during development has some benefits. STRICT messages will help you to use the latest and greatest suggested method of coding, for example warn you about using deprecated functions. Așadar, aș spune că cea mai bună abordare este să dezactivați E_STRICT pe serverul de producție, dar să îl lăsați activat pe serverul de testare/dezvoltare. –  > Por GordonM.
  • +1 pentru detalierea unui aspect al principiului de substituție Liskov. –  > Por Frederik Krautwald.
Sven

Ceea ce a inventat lumea Java nu trebuie să fie întotdeauna corect. Cred că am detectat aici o încălcare a principiului de substituție Liskov, iar PHP are dreptate să se plângă de acest lucru în modul E_STRICT:

Citati Wikipedia: „Dacă S este un subtip al lui T, atunci obiectele de tip T dintr-un program pot fi înlocuite cu obiecte de tip S fără a modifica niciuna dintre proprietățile dezirabile ale programului respectiv.”

T este controlorul dumneavoastră. S este ExtendedController. Ar trebui să puteți utiliza ExtendedController în toate locurile în care funcționează Controller-ul fără a întrerupe nimic. Schimbarea indicatorului de tip al metodei addModel() strică lucrurile, deoarece în fiecare loc în care a fost trecut un obiect de tip Model, indicatorul de tip va împiedica acum trecerea aceluiași obiect dacă nu este accidental un ReOrderableModel.

Cum se poate scăpa de acest lucru?

ExtendedController poate lăsa typehint-ul așa cum este și poate verifica ulterior dacă a primit o instanță de ReOrderableModel sau nu. Acest lucru ocolește plângerile PHP, dar încă strică lucrurile în ceea ce privește substituția Liskov.

O modalitate mai bună este crearea unei noi metode addReOrderableModel() concepută pentru a injecta obiecte ReOrderableModel în ExtendedController. Această metodă poate avea indicatorul de tip de care aveți nevoie și poate apela în mod intern doar la addModel() pentru a plasa modelul în locul în care este așteptat.

Dacă aveți nevoie de un ExtendedController pentru a fi utilizat în locul unui Controller ca parametru, știți că metoda dvs. de adăugare a ReOrderableModel este prezentă și poate fi utilizată. Declarați în mod explicit că Controller nu se va potrivi în acest caz. Fiecare metodă care se așteaptă ca un Controller să fie trecut nu se va aștepta la addReOrderableModel() să existe și nu va încerca niciodată să îl apeleze. Fiecare metodă care așteaptă ExtendedController are dreptul de a apela această metodă, deoarece trebuie să existe.

class ExtendedController extends Controller
{
  public function addReOrderableModel(ReOrderableModel $model)
  {
    return $this->addModel($model);
  }
}

Powerslave

Soluția mea de rezolvare este următoarea:

/**
 * Generic list logic and an abstract type validator method.
 */    
abstract class AbstractList {
    protected $elements;

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

    public function add($element) {
        $this->validateType($element);
        $this->elements[] = $element;
    }

    public function get($index) {
        if ($index >= sizeof($this->elements)) {
            throw new OutOfBoundsException();
        }
        return $this->elements[$index];
    }

    public function size() {
        return sizeof($this->elements);
    }

    public function remove($element) {
        validateType($element);
        for ($i = 0; $i < sizeof($this->elements); $i++) {
            if ($this->elements[$i] == $element) {
               unset($this->elements[$i]);
            }
        }
    }

    protected abstract function validateType($element);
}


/**
 * Extends the abstract list with the type-specific validation
 */
class MyTypeList extends AbstractList {
    protected function validateType($element) {
        if (!($element instanceof MyType)) {
            throw new InvalidArgumentException("Parameter must be MyType instance");
        }
    }
}

/**
 * Just an example class as a subject to validation.
 */
class MyType {
    // blahblahblah
}


function proofOfConcept(AbstractList $lst) {
    $lst->add(new MyType());
    $lst->add("wrong type"); // Should throw IAE
}

proofOfConcept(new MyTypeList());

Deși acest lucru încă diferă de genericul Java, minimizează destul de mult codul suplimentar necesar pentru a imita comportamentul.

De asemenea, este un pic mai mult cod decât unele exemple date de alții, dar – cel puțin mie – mi se pare mai curat (și mai asemănător cu omologul Java) decât cele mai multe dintre ele.

Sper că unii dintre voi îl vor găsi util.

Orice îmbunătățiri față de acest proiect sunt binevenite!

Comentarii

  • Cred că ar fi trebuit să adăugați codul la una dintre clasele SPL din PHP, cum ar fi ArrayObject sau SplDoublyLinkedList –  > Por Sven.
  • @Sven Ei bine, da. De asemenea, este evident că nu este o interfață completă pe care Java List<?> oferă, ci un subset care este cel mai des utilizat. De asemenea, poate fi îmbunătățită în continuare prin introducerea unei AbstractValidatedList clasă care implementează validatorul, dar care apelează la copii pentru o clasă abstractă. getElementType() care returnează numele clasei. Sunt ușor depășit în ceea ce privește PHP, deoarece în ultimul an am lucrat aproape exclusiv cu J2EE. Mulțumesc pentru că ați subliniat anumite lucruri! –  > Por Powerslave.
  • Întrebarea nu se referă la liste, cred eu. Este despre moștenirea în PHP. –  > Por Sven.
  • @Sven Ei bine, conceptul de bază poate fi aplicat la alte clase, după cum doriți. Principala diferență față de genericele Java este că veți obține erori de execuție, dar niciun IDE nu se va plânge de necorelări de tip. –  > Por Powerslave.
  • @Sven Clasele generice nu sunt rezolvate în clase concrete separate în momentul compilării, ci au scopul de a nu trebui să folosească Object ca tip de subiect, ceea ce ar elimina siguranța tipurilor din cod. În schimb, într-o manieră asemănătoare cu cea a șabloanelor, puteți specifica cu ce tip real ar trebui să lucreze o anumită instanță. Ele sunt rezolvate în sensul că aceeași clasă are o referință la clasa subiectului; practic, o înfășoară. Acest lucru asigură o reutilizare ridicată a codului într-un mediu puternic tipizat. Deși PHP nu este unul dintre acestea, OOP în sine este de natură tipizată, astfel încât ar putea apărea necesitatea unei astfel de funcționalități. –  > Por Powerslave.
Starx

Am mai trecut prin același tip de problemă. Și am folosit ceva de genul acesta pentru a o rezolva.

Class Myclass {

    $objectParent = "MyMainParent"; //Define the interface or abstract class or the main parent class here
    public function method($classObject) {
        if(!$classObject instanceof $this -> objectParent) { //check 
             throw new Exception("Invalid Class Identified");
        }
        // Carry on with the function
    }

}

Comentarii

  • Soluție creativă! S-ar putea să fac acest lucru împreună cu sugestia de tip. Astfel încât să verifice tipul superclasei și apoi să facă o verificare manuală pentru subclasă. –  > Por Jonathan.
  • @jonathan, dacă tipărești o superclasă, atunci obiectul transmis trebuie să fie obiectul acelei clase. Tocmai de aceea, soluția mea nu o folosește, în schimb, verifică instanța mai târziu pentru a o verifica. –  > Por Starx.
  • Ceea ce vreau să spun este că addModel(Model $model) va verifica dacă este un model, dar apoi, în corpul metodei, pot verifica manual făcând if($model instanceof ReOrderableModel). ReOrderableModel extinde Model, astfel încât ar fi permisă prin metoda addModel(Model $model). Vreți să spuneți că acest lucru nu ar funcționa? –  > Por Jonathan.
Christian Gollhardt

Puteți lua în considerare să treceți la Hack și HHVM. Acesta este dezvoltat de Facebook și este complet compatibil cu PHP. Puteți decide să utilizați <?php sau <?hh

Suportă că ceea ce doriți:

http://docs.hhvm.com/manual/en/hack.generics.php

Știu că acesta nu este PHP. Dar este compatibil cu acesta și, de asemenea, îmbunătățește dramatic performanța.

Comentarii

  • De fapt, nu este complet compatibil cu PHP. Există o mulțime de lucruri mici care nu sunt compatibile, dar care, împreună, reprezintă o mare diferență. –  > Por jurchiks.
artragis

Puteți face acest lucru în mod murdar, trecând tipul ca al doilea argument al constructorului

<?php class Collection implements IteratorAggregate{
      private $type;
      private $container;
      public function __construct(array $collection, $type='Object'){
          $this->type = $type;
          foreach($collection as $value){
             if(!($value instanceof $this->type)){
                 throw new RuntimeException('bad type for your collection');
             }  
          }
          $this->container = new ArrayObject($collection);
      }
      public function getIterator(){
         return $this->container->getIterator();
      }
    }

Comentarii

  • Aceasta este o soluție interesantă! Ca și soluția lui Starx, cu excepția faptului că clasa este trecută ca parametru frumos. –  > Por Jonathan.
rckd

Pentru a oferi un nivel ridicat de analiză statică a codului, o tastare strictă și utilizare, am venit cu această soluție: https://gist.github.com/rickhub/aa6cb712990041480b11d5624a60b53b

/**
 * Class GenericCollection
 */
class GenericCollection implements IteratorAggregate, ArrayAccess{
    /**
     * @var string
     */
    private $type;

    /**
     * @var array
     */
    private $items = [];

    /**
     * GenericCollection constructor.
     *
     * @param string $type
     */
    public function __construct(string $type){
        $this->type = $type;
    }

    /**
     * @param $item
     *
     * @return bool
     */
    protected function checkType($item): bool{
        $type = $this->getType();
        return $item instanceof $type;
    }

    /**
     * @return string
     */
    public function getType(): string{
        return $this->type;
    }

    /**
     * @param string $type
     *
     * @return bool
     */
    public function isType(string $type): bool{
        return $this->type === $type;
    }

    #region IteratorAggregate

    /**
     * @return Traversable|$type
     */
    public function getIterator(): Traversable{
        return new ArrayIterator($this->items);
    }

    #endregion

    #region ArrayAccess

    /**
     * @param mixed $offset
     *
     * @return bool
     */
    public function offsetExists($offset){
        return isset($this->items[$offset]);
    }

    /**
     * @param mixed $offset
     *
     * @return mixed|null
     */
    public function offsetGet($offset){
        return isset($this->items[$offset]) ? $this->items[$offset] : null;
    }

    /**
     * @param mixed $offset
     * @param mixed $item
     */
    public function offsetSet($offset, $item){
        if(!$this->checkType($item)){
            throw new InvalidArgumentException('invalid type');
        }
        $offset !== null ? $this->items[$offset] = $item : $this->items[] = $item;
    }

    /**
     * @param mixed $offset
     */
    public function offsetUnset($offset){
        unset($this->items[$offset]);
    }

    #endregion
}


/**
 * Class Item
 */
class Item{
    /**
     * @var int
     */
    public $id = null;

    /**
     * @var string
     */
    public $data = null;

    /**
     * Item constructor.
     *
     * @param int    $id
     * @param string $data
     */
    public function __construct(int $id, string $data){
        $this->id = $id;
        $this->data = $data;
    }
}


/**
 * Class ItemCollection
 */
class ItemCollection extends GenericCollection{
    /**
     * ItemCollection constructor.
     */
    public function __construct(){
        parent::__construct(Item::class);
    }

    /**
     * @return Traversable|Item[]
     */
    public function getIterator(): Traversable{
        return parent::getIterator();
    }
}


/**
 * Class ExampleService
 */
class ExampleService{
    /**
     * @var ItemCollection
     */
    private $items = null;

    /**
     * SomeService constructor.
     *
     * @param ItemCollection $items
     */
    public function __construct(ItemCollection $items){
        $this->items = $items;
    }

    /**
     * @return void
     */
    public function list(){
        foreach($this->items as $item){
            echo $item->data;
        }
    }
}


/**
 * Usage
 */
$collection = new ItemCollection;
$collection[] = new Item(1, 'foo');
$collection[] = new Item(2, 'bar');
$collection[] = new Item(3, 'foobar');

$collection[] = 42; // InvalidArgumentException: invalid type

$service = new ExampleService($collection);
$service->list();

Chiar dacă ceva de genul acesta s-ar simți mult mai bine:

class ExampleService{
    public function __construct(Collection<Item> $items){
        // ..
    }
}

Sper că genericele vor ajunge în curând în PHP.

celsowm

O alternativă este combinația dintre operator splat + indicii tipizate + matrice privată:

<?php

class Student {
    
    public string $name;
    public function __construct(string $name){
        $this->name = $name;
    }
    
}

class Classe {
    
    private $students = [];
    
    public function add(Student ...$student){
        array_merge($this->students, $student);
    }
    
    public function getAll(){
        return $this->students;
    }
    
}

$c = new Classe();
$c->add(new Student('John'), new Student('Mary'), new Student('Kate'));