První aplikace OOP – Pomoc v začátcích :-)

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Prochy
Člen | 91
+
0
-

Zdravím zdejší diskutující,

někdy dva roky zpátky jsem programoval jednu aplikaci v Nette, v té době jsem neměl žádné ponětí o OOP, nebo měl, ale neměl jsem čas to nějak více študovat. Takže jsem to psal jaksi na hulváta. Aplikace funguje v pořádku, ale kdyby někdo chtěl ukázat kód, tak bych se musel červenat až na prdeli. :-) Rád bych ji proto zkusil teď sám znova naprogramovat, ale už aspoň trochu více pomocí OOP, abych povýšil na vyšší úrověň. Byl bych vděčen, kdyby mě někdo aspoň trochu poradil. Může se to poté hodit dalším začínajícím v OOP. :-)

Pokud uvedu základní myšlenku aplikace: Jde o databázovou aplikaci jedné ordinace, kde jsou uloženy údaje o klientech a jejich návštěvách. Klient platí návštěvy buď hotově, poukázkami, nebo předplacenými kartami. Poté si může klient zakoupit nějaký výrobek (např. bandáž apod.) z ordinace, či nějaký návštěvník, který nemusí být klient. Všechny informace o návštěvách, placení se samozřejmě ukládají do databáze.

Tápu ze začátku v tom, jak správně pracovat s databází. Potřeboval bych vědět, jestli je ten postup úplně chybně nebo to je možný strávit. :-) Programování mám pouze jako koníčka, žádná obživa, ale rád bych měl trochu slušný kód. Doufám, že vás tím příliš neotravuji.

Začal bych asi tím nejjednoduším a to jsou číselníky. Tady jsem si vytvořil interface IListItem,IListItemModel, třída Item a poté třídy jednotlivých číselníků, je jich hodně, tak uvedu pouze jeden z nich.

IListItem:

<?php
namespace DZ\ListItem;
interface IListItem{
	public function getListItem();
	public function getIdItem($name);
	public function getNameItem($id);
	public function addItem(Item $item);
	public function removeItem(Item $item);
}
?>

IListItemModel:

<?php
namespace DZ\ListItem;
interface IListItemModel{
	public function getItem();
	public function addItem(Item $items);
	public function removeItem(Item $items);
}
?>

Item:

<?php
namespace DZ\ListItem;
class Item{
	private $id;
	private $name;
	private $active;

	public function __construct($values = null){
		if(isset($values['id']))
			$this->setId($values['id']);

		if(isset($values['name']))
			$this->setName($values['name']);

		if(isset($values['active']))
			$this->setActive($values['active']);
	}


	public function getId(){
		return $this->id;
	}

	public function getName(){
		return $this->name;
	}

	public function getActive(){
		return $this->active;
	}

	public function setId($id){
		$this->id = $id;
	}

	public function setName($name){
		$this->name = $name;
	}

}
?>

Třída číselníku Category:

<?php

namespace DZ\ListItem;

class Category implements IListItem {

    private $items = array();
    private $model;

    /** Inicializuje třídu a načte položky daného číselníku
     * @param IListItemModel model, který pracuje s daným číselníkem skrze databázi
     */
    public function __construct(IListItemModel $model) {
        $this->model = $model;
        $this->items = $model->getItems();
    }

    /** Vrátí všechny položky v číselníku
     * @return array(Item)
     */
    public function getListItem() {
        return $this->items;
    }

    /** Vrátí položku vyhledávanou podle názvu
     * @param string název položky
     * @return Item nalezená položka
     */
    public function getNameItem($nameItem) {
        foreach ($this->items as $item) {
            if ($item->getName() == $nameItem)
                return $item;
        }
        return null;
    }

    /** Vrátí položku vyhledávanou podle id
     * @param int id vyhledávané položky
     * @return Item nalezené
     */
    public function getIdItem($id) {
        foreach ($this->items as $item) {
            if ($item->getId() == $id) {
                return $item;
            }
        }

        return null;
    }

    /** Přídá položku do číselníku a aktualizuje seznam
     * @param Item položka přidávána do seznamu
     */
    public function addItem(Item $item) {
        $this->model->addItems($item);
        $this->items[] = $item;
    }

    /** Smaže položku z číselníku a aktualizuje seznam
     * @param Item položka předána ke smazání
     */
    public function removeItem(Item $removeItem) {
        $this->model->removeItems($removeItem);
        foreach ($this->items as &$item) {
            if($removeItem==$item){
                $item = null;
            }
        }
    }

}

?>

Třída modelu CategoryModel, který pracuje s databází:

<?php

namespace DZ\ListItem;

class CategoryModel implements IDialModel {

    private $db;
    private $colName = 'name';
    private $tableName = 'list_category';

    public function __construct(\DibiConnection $db) {
        $this->db = $db;
    }

    /** Vyhledá všechny položky v číselníku
     * @return array(Item[])
     */
    public function getItems() {
        $items = $this->db->query('SELECT * FROM %n', $this->tableName);
        foreach ($items as $item) {
            $arrayItems[] = new Item(array('id' => $item['id'], 'name' => $item[$this->colName]));
        }
        return $arrayItems;
    }

    /** Přidá položku do číselníku
     * @param Item prvek který má být přidán
     */
    public function addItem(Item $item) {
        $this->db->query('INSERT INTO %n(%n) VALUES(%s)', $this->tableName, $this->colName, $item->getName());
        $item->setId($this->db->getInsertId());
    }

    /** Odstraní položku z číselníku
     * @param Item prvek který má být smazán
     */
    public function removeItem(Item $item) {
        $this->db->query('DELETE FROM %n WHERE [id]=%i', $this->tableName, $item->getId());
    }

}
Jan Endel
Člen | 1016
+
0
-

Není to taková hrůza, ale mám pár poznámek:

  • nedělej interface pro interface, dělej ho jenom tam, kde má smysl
  • psát nad metodou removeItem phpDoc Odstraní položku z číselníku je trošku kontraproduktivní, kód by měl být samopopisný, což v tvém případě relativně je, komentáře jenom tam, kde jsou potřeba (kupříkladu při složitých výpočtech, větších regulárech, atp.)
  • máš zajímavý coding standart, ale rozhodně neuváděj koncovou značku ?> v souborech, které obsahují jenom php kód, může se za ně dostat nějaký netisknutelný znak a udělat ti záhadnou chybu, kterou budeš dlouho hledat, případně doporučím Nette coding standart
  • construct může mít i více parametrů, a konkrétně v tomto případě mi přijde, že Item by mohl mít property name povinnnou
  • pokud chceš udělat review aplikace, je fajn, to dát na GitHub, tam se dělá mnohem pohodlněji.
  • v addItem ti zmizí možnost tam dát položku rovnou aktivní
  • nedělej gettery a settery, když nemají přidanou logiku, požij magii
  • v Category nenačítej položky hned, zbytečně více dotazů třeba při vykreslování menu, tahej si je lazy
  • než mít dvě metody getNameItem a getIdItem není lepší mít getItem a nad ním se už dál dotazovat?
  • NULL je konstanta, píšeme ji velkými písmeny

Jinak koníčka chválím :-)

Prochy
Člen | 91
+
0
-

Díky, za odpověd a za rady do začátku. Mám možnost na tom dělat tak 1 nebo 2 dny v týdnu, proto až tak často odpovídat nebudu, snad to nebude vadit. :-)

nedělej interface pro interface, dělej ho jenom tam, kde má smysl
Právěže interfacu až tolik dobře nerozumím. Myslim si, že nám učitel vždycky vtloukal do hlavy, že všechno co předáváme jiné třídě by mělo být zamaskováno tím interfacem, ale nějak nevim např. jak to udělat pro tu třídu Item, kterou předávám dále např. třídě Category, jestli udělat rozhraní, které bude mít definováno metody getName(),getId() se mi zdá dost zbytečné.

pokud chceš udělat review aplikace, je fajn, to dát na GitHub, tam se dělá mnohem pohodlněji.
Hodil jsem to tam, nemám příliš zkušenosti s verzovacími systémy, tak snad to budu aktualizovat správně. :-)
https://github.com/…dz/ListItem/

construct může mít i více parametrů, a konkrétně v tomto případě mi přijde, že Item by mohl mít property name povinnnou

To už jsem opravil podle tvé rady, a celkem s ní souhlasim. :)

nedělej gettery a settery, když nemají přidanou logiku, požij magii
Magie znamená co? Použití funkcí __set(),__get() atd?

než mít dvě metody getNameItem a getIdItem není lepší mít getItem a nad ním se už dál dotazovat?
Uděláno podle tebe. Snad správně. :-)

psát nad metodou removeItem phpDoc Odstraní položku z číselníku je trošku
Myslel jsem, že je správné psát komentář nad každou metodou.

Trochu jsem poupravil strukturu a nějak nevim, jestli k lepšímu nebo horšímu. Pokud vezmu ty třídy pro číselníky (číselníků budu mít asi 10), téměř každý číselník bude mít totožné metody (nachlup stejné jen jiná tabulka), tak jsem vytvořil třídu od které se základ dědí, nevim jestli to je správně, nebo by bylo např. lepší udělat pouze jednu třídu pro číselníky, kde by v konstruktoru byl název tabulky a sloupce, ale to se mi nezdá asi jako správné řešení, jelikož kdyby se mi změnila struktura jednoho číselníku, musel bych pro něj měnit víc věcí.

Filip Procházka
Moderator | 4668
+
0
-

Něco o DI (a malá zmínka o interfacech) i tady

Jiří Nápravník
Člen | 710
+
0
-

Prochy napsal(a):
Právěže interfacu až tolik dobře nerozumím. Myslim si, že nám učitel vždycky vtloukal do hlavy, že všechno co předáváme jiné třídě by mělo být zamaskováno tím interfacem, ale nějak nevim např. jak to udělat pro tu třídu Item, kterou předávám dále např. třídě Category, jestli udělat rozhraní, které bude mít definováno metody getName(),getId() se mi zdá dost zbytečné.

Nestudoval jsi náhodou na ČVUT? Tam nám taky mlátili do hlavy dělat pro vše interface. Je to asi akademicky čistý, můžeš pak lépe vyměnit třídu za nějakou jinou, která má stejné metody. Například chceš mít třídu pro práci s databází, budeš chtít využívat pro postgres, mysql, oracle. Víš, že budou mít určitě metodu insert, delete, update atd. Tak uděláš interface s těmahle metodami a ty konkrétní adaptéry jej budou implementovat. Ale dělat interface, který má jen getName a getId je fakt zbytečné a nedává moc smysl.

Magie znamená co? Použití funkcí __set(),__get() atd?

ano. v případě nette stačí aby ten objekt dědil z Nette\Object, a bude to tak fungovat automagicky. Ale pokud ti přijde akademicky čisté mít interface pro vše, tak by se ti tohle taky nemělo líbit:)

Myslel jsem, že je správné psát komentář nad každou metodou.

předně má být kód, resp. názvy proměnných metod apod. co nevíce samopopisujicí a myslím, že z názvu removeItem člověk pozná jasně, o co jde.

Šaman
Člen | 2666
+
0
-

Interface: Ono má smysl dělat interface i pro jedinou metodu, pokud budeme mít více tříd toto interface splňující. (Třeba IRole a IResource jsou velmi jednoduchá rozhraní, ale klíčová k tomu, abychom s nějakým objektem uměli pracovat jako s rolí/zdrojem.) Pokud máš ale jednorázovou třídu, interface není nutné. Pokud by se časem projekt rozšiřoval a vytvořil bys bratrskou třídu, pak dopíšeš i interface.

Komentáře: Pokud bys chtěl někdy tvořit z PhpDocu dokumentaci, pak ano, má to smysl. Ale taková interní dokumentace smysl moc nemá. Takže i když jsem to dřív taky psal nad každou metodu, postupně jsem přestal psát komentáře u konstruktorů, renderFoo, setterů a getterů a po čase je pišu jen tam, kde není z názvu metody jasné, co dělá. Zdůrazňuji že z názvu, protože ten se napovídá. Kód metody by nás neměl zajímat, pokud danou třídu jen využíváme a neupravujeme přímo ji.
Co ale do anotací píšu, jsou všechny @param (kromě těch, které jsou zapsané přímo u parametru v kódu, není dobré mít jednu věc zapsanou dvakrát) a @return. Tyhle dvě věci se nám při napovídání hodí.

Magie: Místo psaní setterů a getterů můžeš použít anotaci @method. Zůstane zachované napovídání a pokud časem dopíšeš implicitní getter/setter, použije se.
//Edit: Samozřejmě jen pokud dědíme od NEtte\Object, nebo jiné třídy, která tohle umí (třeba LeanMapper entity).

<?php
/**
 * @method string getPokus()
 * @method setPokus(string)
 */
class UserManager extends Nette\Object
{
	protected $pokus;
?>

Editoval Šaman (9. 5. 2014 5:04)

Prochy
Člen | 91
+
0
-

Díky za další rady.

Filip Procházka napsal(a):

Něco o DI (a malá zmínka o interfacech) i tady

Díky mrknu na to.

Nestudoval jsi náhodou na ČVUT? Tam nám taky mlátili do hlavy dělat pro vše interface. Je to asi
akademicky čistý, můžeš pak lépe vyměnit třídu za nějakou jinou, která má stejné metody.

Nene ČVUT, studuju Mechatroniku v Liberci, měli jsme jen jeden semestr programování v C#, jinak C nebo assembler. Učitel se tím i živil, takže vypadal, že tomu hodně rozumí, a snažil se to do nás dost dostat. U dost tříd se mi to zdálo celkem zbytečný, jak on to řešil. Říkal sice, že tohle je malej program, ale že ve velkých projektech by jsme se bez toho neobešli, pokud by nám teda nestačil nějakej mišmaš. :-) Jen tak na ukázku, tohle jsme tam třeba dělali, hra videostop. Když vezmu např. jak navrhoval změnu barvy tlačítka, či textu tlačítka, tak jsem docela čuměl. :-)
www.filipprochazka.cz/videostop2.zip

ano. v případě nette stačí aby ten objekt dědil z Nette\Object, a bude to tak fungovat automagicky. Ale
pokud ti přijde akademicky čisté mít interface pro vše, tak by se ti tohle taky nemělo líbit:)

Jako ono to nemusí být stoprocentně čistě, ale aby to bylo lepší než ten můj předchozí kód, což bude asi vždycky. Chci aby to bylo aspoň trochu objektově správně.

Magie: Místo psaní setterů a getterů můžeš použít anotaci @method. Zůstane zachované napovídání a pokud
časem dopíšeš implicitní getter/setter, použije se.

To se mi libí to využiju. :-)

Šaman
Člen | 2666
+
0
-

Prochy napsal(a):
Nene ČVUT, studuju Mechatroniku v Liberci, měli jsme jen jeden semestr programování v C#, jinak C nebo assembler. Učitel se tím i živil, takže vypadal, že tomu hodně rozumí, a snažil se to do nás dost dostat. U dost tříd se mi to zdálo celkem zbytečný, jak on to řešil. Říkal sice, že tohle je malej program, ale že ve velkých projektech by jsme se bez toho neobešli, pokud by nám teda nestačil nějakej mišmaš. :-) Jen tak na ukázku, tohle jsme tam třeba dělali, hra videostop. Když vezmu např. jak navrhoval změnu barvy tlačítka, či textu tlačítka, tak jsem docela čuměl. :-)
www.filipprochazka.cz/videostop2.zip

Do .NETu nedělám, ale jestli nejsou někde schované, tak vidím interface jen dva. IActionListener a IDice.
U listeneru se rozhraní nabízí – může se jím stát jakákoliv třída implementující toto rozhraní (u tebe TimerStart, TimerStop, Counter a další).
U kostky taky – máš jednotné rozhraní a to splňuje několik implementací kostek v programu (SquareDice, ShapeDice). U kostky by se ještě nabízelo udělat to pomocí abstraktní kostky a jejich potomků, ale rozhraní tě bude do budoucna méně omezovat. (Kostka pak může dědit odjinud a rozhraní jen splnit.)
Nikde ale není rozhraní samoúčelně.

Při rozhodování, jestli použít rozhraní, nebo přímo název třídy, se musíš řídit trochu citem.
Pokud je možné, aby si programátor dopsal další instance podle sebe, nenuť ho dědit a použij rozhraní (IRole, IResource, IEmailSender, IAuthenticator). Pokud popisuješ obecnou vlastnost, kterou třída musí mít, abys s ní uměl pracovat, tak určitě rozhraní (ICountable, IArrayAccess, IFreezable).
Většinou se to týká základních entit modelu. Rozhraní je většinou o vyměnitelnosti.
Pokud ale máš konkrétní jednorázové třídy, rozhraní se k nim většinou nepíše (UserRepository, ServiceLocator, MyUsefulFunctions).

Jednoznačné pravidlo k tomu neexistuje, v určitých případech by dva zkušení programátoři zvolili každý jiný přístup. Pokud se nějaká třída používá jen na jednom místě, klidně se na rozhraní vykašli, i když cítíš, že by se někdy mohlo hodit. Pokud třídě přibude bráška, nebo pokud se začne používat častěji, rozhraní se dopíše.
Pokud s nějakou třídou pracuješ na více místech a cítíš, že rozhraní by se někdy mohlo hodit, použij ho.
Pokud je nějaká třída unikátní a nezaměnitelná (jádro aplikace, třeba ten ServiceLocator), tak rozhraní není potřeba (nikdy by se nevyužilo).

P.S. Používat ServiceLocator se nedoporučuje, právě proto, že je to nevyměnitelný a natvrdo nakóděný kus kódu v samém srdci aplikace. Ale když bys ho použil, tak nemá smysl mu psát rozhraní.