C# miért jó interface-el deklarálni? IStore store = new BookStore () ; vs. BookStore store = new BookStore () ;?
A BookStore megvalóstja az IStore-t.
Van bármilyen különbség a két deklaráció között? A store sima BookStore lesz mindkét esetben, nem?
Köszönöm a válaszokat.
Jó egészség minden korban jól jön.
Akkor biztosan valami fontos beosztásban vagy, mert hatodikon és öregek otthonán kívül igazából semmi más nem lepne meg. Volt, hogy vízvezetékszerelő segített info érettségi példák megoldásában ugyanitt. Már akkor sem ment:)
én gyári munkással csinálok közös játékokat :) szóval nem feltétlen a foglalkozás határozza meg a tudást és a tehetséget :)
modellezéshez és 2D textúrázáshoz elég jól ért a munkájától függetlenül
Igazi álommunkának hangzik, én is elsősorban játékkészítés miatt választottam ezt az irányt, de most már csak túl akarom élni az egyetemet.
Ha bejön a játék és felvesztek embereket nem kizárt, hogy 10+ év múlva már én is használható leszek ilyesmire. A motiváció meg van. Sok sikert addig is.
köszi, neked is :)
nekem hobbiból lett szakma, ha neked ez nyűg és szenvedés, akkor nem biztos, hogy a programozás a legjobb irány, de a játékfejlesztésben más területen is hasznos lehetsz
"Szóval
IStore store = new BookStore();
formulával igazából egy interface-t példányosítunk. Olyan mintha
IStore store = new IStore();
csak persze ez nem megengedett. A store-nak ilyenkor köze sincs a Bookstore-hoz, hiába szerepel a deklarációban."
Ez egy akkora zagyva marhaság, hogy ha ezt sikerült megértened, akkor messzebb vagy megértesben, mint ahonnan indultál.
Nyilván nem egy egysoros példából fogod megérteni a dolgot, hanem gyakorlati problémákon keresztül, de röviden tömören az interfacek lényege, hogy függetleníteni tudod az egyes kódrészeket a konkrét impelementációtól.
Ez nagyon hasznos, ha például változtatni kell a konkrét implementáción egy későbbi fejlesztés során, vagy csak több különböző működést akarunk megvalósítani, illetve automatizált tesztelések során is.
Egy tipikus gyakorlati példa az adatbázis hozzáférés és a Repository design pattern.
A legtöbb programot nem érdekli, hogy adatbázisból vagy máshonnan jön-e az adat, ezért azt elrejthetjük egy IRepository interfész mögé, amit megvalósíthat egy DatabaseRepository, vagy akár egy WebApiRepoditory is, a hívó fél szempontjából nem számít.
Na megyek inkább vízvezetékszerelőnek...
Ha valaki van még itt, akkor lenne még egy tisztázó kérdésem.
Szóval a Bookstore:IStore
deklaráljuk
IStore store = new BookStore();
Most csak olyan metódusókat érek el, amit az IStore előír, de ha ezeket használom, akkor a bookstore-ban megvalósított verziót használom (hiszen az IStoreban csak paraméterek vannak)
Tehát kaptam egy lekorlátozott Bookstore példányt. Ha a Bookstore-ban csak az IStore által előírt metódusok (meg események, stb.) vannak, akkor elvileg ugyanazt érem el, mintha egy Bookstore-t példányosítanék (most csak a pillanatnyi működés szempontjából)
A példányt vissza kell konvertálnod az eredeti típusába, hogy elérd az adattagjait és a metódusait:
((BookStore)store).
vagy
(store 𝐚𝐬 Bookstore).
Persze az is előfordulhat, hogy más típusú példányt akarsz explicit konvertálni BookStore-rá, pl egy IStore gyűjteményben, ami futásidőben hibához vezethet.
Ezért, ha nem ismert pontosan előtted a példány típusa, érdemes beágyazni egy elágazásba:
𝐢𝐟 (store 𝐢𝐬 BookStore)
{...}
A konkrét implementáció (class) és az absztrakció (interface) között van egy nagyon nagy különbség, amit egy (lebutított) példán keresztül be is mutatok neked.
1) Vannak eszközök a világban, amik árammal működnek, mint például a hajszárító, a vasaló, stb.
2) És vannak dolgok, amik áramot állítanak elő, például az aggregátor, az atomerőmű, a vízerőmű stb.
Tehát ha megfigyeled, kapcsolat van a kettő között - az árammal működő eszközök nem tudnak működni az áramot előállító dolgok nélkül. Szóval naívan ezt így implementálnánk:
class Atomerőmű {
.. public Áram GetÁram() {
.. .. //előállítjuk az áramot
.. }
}
class Vasaló {
.. public void Működik(Atomerőmű áramforrás) {}
}
És ha ezt használni is akarjuk, valami ilyesmit írunk:
Atomerőmű erőmű = new Atomerőmű();
Vasaló vasaló = new Vasaló();
vasaló.Működik(erőmű);
A probléma ezzel az, hogyha van egy másik áramforrásod:
class Vízerőmű {
.. public Áram GetÁram() {...}
}
... akkor ez a kód már le sem fog fordulni:
Vízerőmű erőmű = new Vízerőmű();
Vasaló vasaló = new Vasaló();
vasaló.Működik(erőmű);
Vagyis van egy vasalód, ami csak olyan árammal hajlandó működni, amit atomerőmű állított elő. Ez - valljuk be - elég balf_sz egy vasaló. Te azt írtad, hogy ez a vasaló csak atomerőművel működhet, holott a vasaló működésének a kritikus pontja az, hogy áramod legyen valahonnan (!) - mindegy honnan. Tehát neked nem egy létező konkrét dologra van szükséged, hanem valamire, ami azt a szolgáltatást tudja nyújtani, amire az adott eszköz működéséhez szükség van.
Szóval, kicsit kiforgatva a fenti két számozott pontot:
1) Vannak eszközeid, amelyek igénybe vesznek egy adott szolgáltatást,
2) míg vannak más eszközeid, amik ezt a szolgáltatást nyújtják.
Mi lenne, ha magát a szolgáltatást, mint fogalmat írnánk le? Itt két eszközhöz nyúlhatunk, az absztrakt osztályhoz és az interfészhez. A kettő közötti különbséget kifejtem, egyelőre ragadjunk le az interfészeknél:
interface IÁramforrás {
.. Áram GetÁram();
}
Ezzel még önmagában semmit nem értem el. Csak annyit, hogy leírtam azt, hogy egy adott szolgáltatást hogyan lehet nyújtani és hogyan lehet igénybe venni. Ha ezt a szolgáltatást nyújtani akarod, akkor csinálsz egy osztályt, ami ezt az interfészt implementálja:
class Atomerőmű : IÁramforrás {
.. public Áram GetÁram() {...}
}
class Vízerőmű : IÁramforrás {
.. public Áram GetÁram() {...}
}
Ha csinálni akarsz egy eszközt, ami ezt a szolgáltatást igénybe tudja venni, akkor kérd be paraméterként:
class Vasaló {
.. public void Működik(IÁramforrás áramforrás) {...}
}
Innentől kezdve a Vasaló számára tökmindegy, mi nyújtja azt a szolgáltatást, amire szüksége van a működéshez, így használhatsz egyaránt Atomerőművet és Vízerőművet is:
IÁramforrás áramforrás = new Atomerőmű();
IÁramforrás áramforrás2 = new Vízerőmű();
Vasaló v = new Vasaló();
v.Működik(áramforrás);
v.Működik(áramforrás2);
Azért ezt érezzük, hogy ez már sokkal közelebb áll ahhoz, ahogy a világunk működik. De mint mondtam, az interfész csak egy módszer ennek a megvalósítására. A másik eszköz, ami szóba jöhet, az az absztrakt osztály. A kettő között az a különbség, hogy absztrakt osztályt akkor használunk, ha a belőle leszármazott osztályoknak van olyan közös kódja, ami a leszármazott osztályok nagy részében (nem feltétlen kell mindegyikben) megegyezik. Így ezeket az implementációkat is leírhatjuk. Az interfész erre nem ad lehetőséget - mivel azzal csak elvárásokat támaszthatunk az őt implementáló osztályok felé.
Egy ténylegesen programozói példával élve képzeld el a "gondoltam egy számot, találd ki" játékot. A működéséhez 3 dologra van szükséged:
1) Kell valami, ami tud adni egy véletlenszámot egy általam megadott intervallumon belül,
2) kell egy játékos,
3) kell maga a játéklogika.
Ha csapatban dolgozol és mondjuk van egy ehhez hasonló összetettebb feladat, máris 3 fejlesztőnek szét lehet dobálni, lerövidítve a fejlesztési időt. Mondjuk te csinálod a játéklogikát, szóval meghatározod interfészekkel, hogy mit vársz el a véletlenszám-generátortól és a játékostól, és ezeket leírod egy-egy interfésszel:
interface IRandomNumberGenerator {
.. int GetRandomNumber(int minInclusive, int maxExclusive);
}
interface IPlayer {
.. //Ezzel üdvözöljük a felhasználót és közöljük vele, hogy a szám, amit kigondoltunk, ez és eközé esik
.. void Welcome(int min, int max);
.. //Be kell tudnunk kérni a játékos tippjét:
.. int GetGuess();
.. //Tudnunk kell értesíteni, hogy:
.. // - A szám, amit megadott, kisebb, mint amire mi gondoltunk
.. // - A szám, amit megadott, nagyobb, mint amire mi gondoltunk
.. // - Eltalálta a kigondolt számot, azaz megnyerte a játékot
.. void NotifyTheGuessedNumberIsSmaller();
.. void NotifyTheGuessedNumberIsLarger();
.. void NotifyWin();
}
Ezt a két interfészt odaadod a fejlesztőknek, akik dolgoznak rajta, neked pedig nem kell foglalkoznod azzal, hogy ők ezt hogyan implementálják. Azt kell csak tudnod, hogy az osztályokban, amiket kapsz, ezek a metódusok benne lesznek, szóval elkezdhetsz dolgozni a játéklogikán. Ami valahogy így fog kinézni:
class GameLogic {
.. private readonly IRandomNumberGenerator randomGenerator;
.. private readonly IPlayer player;
.. public GameLogic(IRandomNumberGenerator rg, IPlayer p) {
.. .. randomGenerator = rg;
.. .. player = p;
.. }
}
Persze felmerül a kérdés, hogy "de hát a fejlesztési folyamat közben hogyan próbálom ki, hogy amit csináltam, tényleg jó-e?". Úgy, hogy unit tesztet írsz rá (sőt, akár tesztvezérelt fejlesztést használsz, és előbb írsz tesztet, mint implementációt), és ezeket az interfészeket mock-olod például a Moq framework-kel. Ettől függetlenül még elküldhetsz a fenébe, hogy "na ne hülyéskedj már, ezt a main-en belül megírom nagyon max. 20 sorban, erre csináljak belőle 70 sort?" - amire én azt mondom, hogy:
1) Sok sikert kívánok a tesztek megírásához, (megsúgom, ha a main-en belül összekókányolod, nagyon nehéz lesz tesztelni)
2) amit ígyis-úgyis meg kell csinálnod, hiszen a megrendelődnek adni kell egy bizonyítékot/dokumentumot, hogy tényleg azt a terméket szállítottad le neki azokkal a funkciókkal, amiben megállapodtatok.
A másik kérdés, ami felmerülhet, hogy emiatt rengeteg példányosítást kell kézzel elvégezned, mire a dolog működni fog. Ez csak egy egyszerű kis játék, de mégis 3 példányosításból áll. Mi van akkor, ha kell adatbázis-elérés, fájlkezelés, e-mail küldés és egy csomó más, tehát nemhogy 3 osztályból, de 35 osztályból áll a programod. Azt is így kézzel kell megcsinálnod?
A válasz az, hogy nem. Erre az a megoldás, hogy egy dependency injection rendszert fogunk csatasorba állítani, amiben megmondjuk, hogy melyik interfészhez melyik implementációt akarjuk használni. Ez valahogy így néz ki:
1) felkonfiguráljuk a dependency injection container-t, hozzáadva a service-eket:
ServiceCollection services = new ServiceCollection();
services.Add<IPlayer, LocalPlayer>();
services.Add<IRandomNumberGenerator, RandomGenerator>();
services.Add<GameLogic>();
2) véglegesítjük a konfigurációt, ami által létrejön egy olyan valami, ami már képes létrehozni bármilyen service-t, amit definiáltunk az előbbi lépésben:
IServiceProvider serviceProvider = services.CreateServiceProvider();
3) elkérjük a fő service-t:
GameLogic game = serviceProvider.GetService<GameLogic>();
game.Run();
További olvasmányra ajánlom neked a refaktor.hu oldalon található tiszta kód cikksorozatot, ahol minderről szó van.
Közben eszembe jutott, hogy van egy másik aspektusa is a dolognak. Mégpedig az, hogyha azt szeretnéd, hogy korlátozd a végrehajtható műveletek számát.
Mondjuk, ha van egy osztályod, ami megadja azt, hogy egy könyvtárban milyen könyvek vannak, akkor azt lusta ember módjára lehet csinálni így:
class Library {
.. private readonly List<Book> books;
.. public List<Book> Books => books;
.. public void AddBook(Book b, Customer c) {
.. .. books.Add(b);
.. .. c.AddDiscount(new PercentDiscount(10));
.. }
}
Mivel a könyvtár lehetővé teszi azt, hogy a beiratkozottak a könyvtárnak adhassanak könyveket, ezért lett egy AddBook nevű metódusa, ami elvégzi a hozzá tartozó adminisztratív munkákat is (például kedvezményt ad a következő kölcsönzési időszakra, stb.) Ezzel van egy igen komoly probléma, ugyanis ezt a metódust simán megkerülheted:
Library l = new Library();
l.Books.Add(new Book());
Sőt, törölhetsz is belőle, ami pedig nem szabályos üzleti művelet:
l.Books.Remove(l.Books.First(x => x.Title == "Valami cím"));
Jó módszer lehet korlátozni a lehetséges műveletek számát ezen a Books property-n, hogy ne lehessen ilyeneket megtenni - például kívülről csak olvashatóvá teszed a listát:
class Library {
.. private readonly List<Book> books;
.. public IReadOnlyList<Book> Books => books;
}
Ezzel a következő műveleteket hagytad csak jóvá:
1) Bejárhatod a listát ciklussal
2) Megkérdezheted, hány darab könyv van a listában
3) Lekérdezhetsz egy adott könyvet.
De nem tudsz hozzáadni és törölni.
Kapcsolódó kérdések:
Minden jog fenntartva © 2025, www.gyakorikerdesek.hu
GYIK | Szabályzat | Jogi nyilatkozat | Adatvédelem | Cookie beállítások | WebMinute Kft. | Facebook | Kapcsolat: info(kukac)gyakorikerdesek.hu
Ha kifogással szeretne élni valamely tartalommal kapcsolatban, kérjük jelezze e-mailes elérhetőségünkön!