ООП - Рестриктиране на сигнатурите на методите в класовете наследници
Достигнах до интересен казус, който отскоро рисърчвам за да събера мнения, също ще ми е интересно и вашето. Реално го достигнах докато пишех на РНР, но (не) работи по абсолютно същия начин и в C# и Java.
Да речем, че правя нещо като ORM. Имам абстрактен клас Repository. За всяка таблица от базата се правят по два класа с имено на таблицата и суфикс Entity или Repository. Да вземем например таблица users - от нея излизат две неща. UserEntity наследник на интерфейс Entity и UserRepository наследник на абстракетн клас Repository.
Repository от своя страна има абстрактен метод save() който приема за аргумент интерфейса Entity.
UserRepository наследява Repository и е задължен да overridе-не абстрактния метод. За мен е важно никой да не може да подаде на UserRepository различен наследник от UserEntity. За това слагам в сигнатурата UserEntity. Кодът изглежда нещо такова
interface Entity { }
class UserEntity : Entity { }
abstract class Repository
{
public abstract void save (Entity e);
}
class UserRepository : Repository
{
public override void save (UserEntity e)
{
}
}
Когато това се пусне за билд излиза грешка:
UserRepository.save(UserEntity) is marked as an override but no suitable method found to override
Напълно разбирам какъв е проблемът. Не разбирам защо това е проблем обаче. За C# компилатора това е проблем, защото тръгва нагоре по веригата и не намира abstract void save(UserEntity); Защо обаче не търси за родители на UserEntity е пълна мистерия за мен. Единствено мога да го свържа с това, че има method overloading. Т.е. ако премахна думичките abstract и override - то UserRepository ще се окаже, че има два метода save.
Това в РНР не е възможно, там override-а е имплицитен. Но грешката я има: Declaration must be compatible with Repository->save(entity : Entity)
Сега възможните решения горе-долу са ми ясни. Оставам класовете деца да приемат интерфейса, а с instanceof проверявам дали са от конкретен тип и хвърлям ексепшън, ако не са. Ако са - каствам ги към конкретния тип, за да се възползвам от методите на UserEntity и продължавам.
В РНР просто мога да направя:
public function save(Entity $entity) { }
В Repository и без никакъв проблем да го овъррайдна в UserRepository:
public function save(UserEntity $user)
Това, което ще изгубя е, че ако в някой наследник забравя да овъррайдна save() няма да ме накара да го направя.
Вашите мнения какви са? Защо са направени така компилаторите/интерпретаторите, че да не може да промениш сигнатурата с напълно compatible такава, просто малко по-рестриктивна. Аз не мога да се сетя добра причина за това, прочетох няколко мнения в гугъл, но там общо взето stating the obvious, че не е възможно да се случи, като цяло не прочетох добра причина това да е така. Според мен би било полезно да може да рестриктираш методите на чайлд класовете без да пишеш допълнителен код като if(user instanceof UserEntity). Какви проблеми може да възникнат, ако желаното от мен поведение беше възможно?
Благодаря ти за отговора. Прочетох статията за Covariance и Contravariance в уикипедия - има доста полезна информация. Generics за съжаление в РНР не съществуват (като изключим Hack, където за щастие ги има).
P.S.: От моя гледна точка в дадения от теб код, Entity entity когато се инициализира трябва да е от тип UserEntity за да може да го подадеш на UserRepository. Т.е. горния случай така или иначе е случай, който не искам да се случва, защото OtherEntity не е UserEntity.
Ако не искаш да се случва, гледай да го направиш невалиден спрямо типовата система (в C# - с generics, не мога да помогна за PHP).
Иначе горните 3 реда са примерни. Реално начина, по който ти е дефинирана класовата йерархия, казва "Repository има нужда само от Entity и нищо по-специализирано". Тъй като всяко UserRepository е Repository, това трябва да важи и за него.