Професионална програма
Loading...
RoYaL avatar RoYaL Trainer 6847 Точки

ООП - Рестриктиране на сигнатурите на методите в класовете наследници

Достигнах до интересен казус, който отскоро рисърчвам за да събера мнения, също ще ми е интересно и вашето. Реално го достигнах докато пишех на РНР, но (не) работи по абсолютно същия начин и в 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). Какви проблеми може да възникнат, ако желаното от мен поведение беше възможно?

 

4
C# OOP Basics
greps avatar greps 3 Точки

Защо не го позволява комилаторът:

Този код трябва да е абсолютно валиден (спрямо класовата структура и това, което базовият клас обещава):

        Repository repo = new UserRepository();
        Entity entity = new OtherEntity();
        repo.Save(entity);

От гледна точка на компилатора, всяко Repository трябва да може да се замести в горния код и да работи абсолютно нормално. Няма как това да стане, ако приемаш по-специализиран клас в някое от тях. Така че от гледна точка на компилатора, твоят код е грешен (дори никога да нямаш горния случай).

 

Решение:

abstract class Repository<T> where T : Entity
{
    public abstract void Save(T entity);
}

class UserRepository : Repository<User>
{
    public override void Save(User entity)
    {
        
    }
}

Поне в C#, с Generics е възможно да се променя връщания/приемания тип (виж Co/Contra-variance). Но в дадения случай дори това не е необходимо.

3
RoYaL avatar RoYaL Trainer 6847 Точки

Благодаря ти за отговора. Прочетох статията за Covariance и Contravariance в уикипедия - има доста полезна информация. Generics за съжаление в РНР не съществуват (като изключим Hack, където за щастие ги има).

P.S.: От моя гледна точка в дадения от теб код, Entity entity когато се инициализира трябва да е от тип UserEntity за да може да го подадеш на UserRepository. Т.е. горния случай така или иначе е случай, който не искам да се случва, защото OtherEntity не е UserEntity.

2
greps avatar greps 3 Точки

Ако не искаш да се случва, гледай да го направиш невалиден спрямо типовата система (в C# - с generics, не мога да помогна за PHP).

Иначе горните 3 реда са примерни. Реално начина, по който ти е дефинирана класовата йерархия, казва "Repository има нужда само от Entity и нищо по-специализирано". Тъй като всяко UserRepository е Repository, това трябва да важи и за него.

0