Защо да настояваме всички реализации на интерфейс да разширяват базов клас?

Току-що разглеждах кода на Java Hamcrest в GitHub и забелязах, че са използвали стратегия, която изглеждаше неинтуитивна и неудобна, но ме накара да се чудя дали не пропускам нещо.

Забелязах в API на HamCrest, че има интерфейс Matcher и абстрактен клас BaseMatcher. Интерфейсът Matcher декларира този метод с този javadoc:

    /**
     * This method simply acts a friendly reminder not to implement Matcher directly and
     * instead extend BaseMatcher. It's easy to ignore JavaDoc, but a bit harder to ignore
     * compile errors .
     *
     * @see Matcher for reasons why.
     * @see BaseMatcher
     * @deprecated to make
     */
    @Deprecated
    void _dont_implement_Matcher___instead_extend_BaseMatcher_();

След това в BaseMatcher този метод се реализира по следния начин:

    /**
     * @see Matcher#_dont_implement_Matcher___instead_extend_BaseMatcher_()
     */
    @Override
    @Deprecated
    public final void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
        // See Matcher interface for an explanation of this method.
    }

Разбира се, това е едновременно ефективно и сладко (и невероятно неудобно). Но ако намерението е всеки клас, който имплементира Matcher, да разшири и BaseMatcher, защо изобщо да използвате интерфейс? Защо просто не направите Matcher абстрактен клас на първо място и всички други съпоставители да го разширят? Има ли някакво предимство да го правиш по начина, по който го направи Hamcrest? Или това е чудесен пример за лоша практика?

РЕДАКТИРАНЕ

Някои добри отговори, но в търсене на повече подробности предлагам премия. Мисля, че въпросът за обратната / двоичната съвместимост е най-добрият отговор. Въпреки това, бих искал да видя въпроса за съвместимостта по-подробен, в идеалния случай с някои примери на код (за предпочитане в Java). Също така, има ли нюанс между "обратна" съвместимост и "двоична" съвместимост?

ДОПЪЛНИТЕЛНО РЕДАКТИРАНЕ

7 януари 2014 г. -- pigroxalot предостави отговор по-долу, препращайки към този коментар в Reddit от авторите на HamCrest. Насърчавам всички да го прочетат и ако го намерите за информативен, гласувайте за отговора на pigroxalot.

ОЩЕ ДОПЪЛНИТЕЛНО РЕДАКТИРАНЕ

12 декември 2017 г. - отговорът на pigroxalot беше премахнат по някакъв начин, не знам как се е случило това. Жалко... тази проста връзка беше много информативна.


person James Dunn    schedule 20.12.2013    source източник
comment
Предполагам, че ако променят BaseMatcher в бъдеще, те няма да нарушат кода, който зависи от използването на обекти тип Matcher. Също така се чудя дали помага при инжектиране на зависимост (просто странно предположение, тъй като не съм професионалист).   -  person Hovercraft Full Of Eels    schedule 21.12.2013
comment
Java (заедно с повечето модерни OO езици) не позволява множествено наследяване, така че Hamcrest изглежда казва, че не искаме друг вид клас да реализира този интерфейс, като същевременно го прави интерфейс вместо абстрактен клас. Защо са го направили е мистерия за мен.   -  person Brian A. Henning    schedule 21.12.2013
comment
Ако не искаха някой да имплементира интерфейс, трябваше да го направят internal. Това изглежда като ужасен хак от някой, който не знае как да програмира правилно.   -  person Federico Berasategui    schedule 21.12.2013
comment
Честно казано, мисля, че тук имате пример за лоша практика и ако искате да знаете защо са го направили, ще трябва да ги попитате директно.   -  person T.J. Crowder    schedule 21.12.2013
comment
@HighCore няма internal в Java; сигурно мислите за C#.   -  person ajb    schedule 21.12.2013
comment
Единственото възможно предимство, за което се сещам, е, че интерфейсът ви дава възможност да използвате Java проксита и абстрактният клас може да съдържа логика, която е необходима на всички съпоставители. Не съм сигурен обаче дали използват проксита под капака. Това ми се струва странно, сега ми е много любопитно   -  person jeff    schedule 21.12.2013
comment
@ajb Знам това, java изглежда има скапано заместване обаче, точно както при всяка друга функция на C#.   -  person Federico Berasategui    schedule 21.12.2013
comment
Това изглежда като начин за поддържане на обратна двоична съвместимост с предишни реализации, внедряване на Matcher директно, но за да се гарантира, че никой вече не го прави чрез нарушаване на съвместимостта на изходния код.   -  person JB Nizet    schedule 21.12.2013
comment
@JBNizet Чудя се дали бихте могли да разработите повече относно двоичната съвместимост? Изглежда, че с добавянето на сладкия метод предишните имплементации така или иначе ще се повредят.   -  person James Dunn    schedule 21.12.2013
comment
Не. Класът ще бъде зареден добре. Това ще доведе само до грешка по време на изпълнение (NoSuchMoethodError, IIRC), ако липсващият метод е бил извикан. Това е, което позволява на старите JDBC драйвери да работят добре на последните JRE, въпреки че много методи са добавени към Connection, Statement, ResultSet и т.н. Ако не извикате неимплементираните методи, няма проблем.   -  person JB Nizet    schedule 21.12.2013
comment
@JBNizet новите методи трябва да хвърлят SQLException по подразбиране, което е много по-добро от Error.   -  person ZhongYu    schedule 21.12.2013
comment
@zhong.j.yu Те не могат да хвърлят SQLException по подразбиране, тъй като те са методи, добавени към интерфейси, а методите в интерфейсите (до Java 8) не могат да имат никаква реализация по подразбиране.   -  person JB Nizet    schedule 21.12.2013
comment
@JBNizet да, но те могат/трябва да го направят сега, вижте Iterator.remove()   -  person ZhongYu    schedule 21.12.2013
comment
@zhong.j.yu Java 8 все още не е достатъчно широко разпространена, за да могат авторите на JDBC драйвери да разчитат на нея.   -  person Donal Fellows    schedule 21.12.2013
comment
Страхотен въпрос, проницателна дискусия. Толкова странно, но интересно...   -  person Jake Toronto    schedule 03.10.2014
comment
Какво се случи с отговора на @pigroxalot?   -  person Jake Toronto    schedule 03.10.2014
comment
Или @pigroxalot, или някой друг го е изтрил, което е тъжно -- това беше добър отговор и щях да му дам моята награда, ако беше отговорил навреме. Със сигурност се радвам, че актуализирах въпроса си, за да включа връзката в неговия отговор.   -  person James Dunn    schedule 03.10.2014


Отговори (7)


git log има този запис от декември 2006 г. (около 9 месеца след първоначалното регистриране):

Добавен е абстрактен клас BaseMatcher, който всички съвпадения трябва да разширяват. Това позволява бъдеща съвместимост на API [sic] с развитието на интерфейса на Matcher.

Не съм се опитвал да разбера подробностите. Но поддържането на съвместимост и непрекъснатост с развитието на системата е труден проблем. Това наистина означава, че понякога се оказвате с дизайн, който никога, никога, никога не бихте създали, ако сте проектирали цялото нещо от нулата.

person ajb    schedule 20.12.2013
comment
Обобщаването на допълнителната функционалност, предоставена от абстрактния клас, би направило този добър отговор страхотен. - person David Harkness; 22.12.2013
comment
@DavidHarkness се съгласи. Следователно, наградата. - person James Dunn; 24.12.2013
comment
Отбелязвайки този отговор като приет, тъй като беше първият даден, който според мен е правилен. Иска ми се да се разработи по-подробно. - person James Dunn; 29.12.2013
comment
@TJamesBoone Мисля, че ще трябва да попитате някой от разработчиците на Hamcrest за повече подробности. Успях да намеря записа git log и си помислих, че ще е от полза, но наистина не знам нищо за вътрешността. - person ajb; 29.12.2013

Но ако намерението е всеки клас, който имплементира Matcher, да разшири и BaseMatcher, защо изобщо да използвате интерфейс?

Не е точно това намерението. Абстрактните базови класове и интерфейси предоставят напълно различни „договори“ от гледна точка на ООП.

Иинтерфейсът е договор закомуникация. Интерфейсът е имплементиран от клас, за да покаже на света, че се придържа към определени комуникационни стандарти и ще даде специфичен тип резултат в отговор на конкретно повикване със специфични параметри.

Абстрактен базов клас е договор за изпълнение. Абстрактните базови класове са наследени от клас, за да осигурят функционалност, която се изисква от базовия клас, но е оставена на изпълнителя да предостави.

В този случай и двете се припокриват, но това е само въпрос на удобство - интерфейсът е това, което трябва да внедрите, а абстрактният клас е там, за да улесни прилагането на интерфейса - няма никакво изискване да използвате този базов клас, за да бъде в състояние да предложи интерфейса, той е там само за да направи по-малко работа за това. Вие по никакъв начин не сте ограничени в разширяването на базовия клас за вашите собствени цели, без да се интересувате от договора за интерфейс или в внедряването на персонализиран клас, внедряващ същия интерфейс.

Дадената практика всъщност е доста често срещана в стария COM/OLE код и други рамки, улесняващи междупроцесните комуникации (IPC), където става фундаментално отделянето на изпълнението от интерфейса - което е точно това, което се прави тук.

person Niels Keurentjes    schedule 21.12.2013
comment
Не е точно намерението... Не съм съгласен. Съгласен съм с по-голямата част от отговора ви, но мисля, че Hamcrest даде да се разбере доста ясно, че те не искат някой да имплементира интерфейса, без също така да разшири абстрактния клас (въпреки че все още е технически възможно) . В противен случай защо да си правите труда да пишете неудобен и функционално безполезен метод, който ще затрупа API? Това е отвъд въпрос на удобство. Ако това беше всичко, те можеха просто да отбележат абстрактния клас в javadocs на интерфейса. - person James Dunn; 23.12.2013

Мисля, че това, което се случи, е, че първоначално API за съпоставяне беше създаден под формата на интерфейс.
След това, докато внедрявахме интерфейса по различни начини, беше открита обща кодова база, която след това беше преработена в класа BaseMatcher.

Така че моето предположение е, че интерфейсът на Matcher е запазен, тъй като е част от първоначалния API, а описателният метод след това е добавен като напомняне.

След като претърсих кода, открих, че интерфейсът може лесно да бъде премахнат, тъй като се изпълнява САМО от BaseMatcher и в 2 тестови единици, които лесно могат да бъдат променени, за да използват BaseMatcher.

Така че, за да отговоря на въпроса ви - в този конкретен случай няма предимство да го правите по този начин, освен че не нарушавате реализациите на Matcher на други хора.

Що се отнася до лошата практика? Според мен е ясно и ефективно - така че не, не мисля така, просто малко странно :-)

person Jurgen    schedule 26.12.2013
comment
Въпреки че този отговор не навлезе в всички подробности, които търсех (вижте редакцията ми в долната част на въпроса ми), смятам, че е най-близо до заслужаването на наградата. Следователно присъждам наградата за този отговор. - person James Dunn; 29.12.2013

Hamcrest осигурява съвпадение и само съвпадение. Това е малка пазарна ниша, но изглежда, че се справят добре. Реализациите на този интерфейс на Matcher са разпръснати в няколко библиотеки за тестване на единици, вземете например ArgumentMatcher и в глупаво голямо количество малки анонимни копиране-поставяне на реализации в модулни тестове.

Те искат да могат да разширят Matcher с нов метод, без да нарушават всички тези съществуващи класове за изпълнение. Те биха били адски ъпгрейд. Само си представете внезапно всички ваши класове unittest да показват гневни червени компилационни грешки. Гневът и раздразнението биха убили пазарната ниша на hamcrest с един бърз замах. Вижте http://code.google.com/p/hamcrest/issues/detail?id=83 за малък вкус на това. Също така, критична промяна в hamcrest ще раздели всички версии на библиотеки, които използват Hamcrest, на преди и след промяната и ще ги направи взаимно изключващи се. Отново адски сценарий. Така че, за да запазят известна свобода, те се нуждаят от Matcher да бъде абстрактен базов клас.

Но те също са в подигравателния бизнес и интерфейсите са много по-лесни за подиграване от базовите класове. Когато хората от Mockito тестват Mockito, те трябва да могат да се подиграват на съвпадащия. Така че те също се нуждаят от този абстрактен базов клас, за да имат интерфейс на Matcher.

Мисля, че са обмислили сериозно вариантите и са открили, че това е най-малко лошата алтернатива.

person flup    schedule 26.12.2013

Има интересна дискусия за това тук. За да цитирам nat_pryce:

здрасти Написах оригиналната версия на Hamcrest, въпреки че Джо Уолнс добави този странен метод към базовия клас.

Причината е особеност на езика Java. Както каза коментатор по-долу, дефинирането на Matcher като базов клас би улеснило разширяването на библиотеката, без да прекъсва клиентите. Добавянето на метод към интерфейс спира компилирането на всички изпълняващи класове в клиентския код, но нови конкретни методи могат да се добавят към абстрактен базов клас, без да се нарушават подкласовете.

Има обаче функции на Java, които работят само с интерфейси, по-специално java.lang.reflect.Proxy.

Затова дефинирахме интерфейса на Matcher, така че хората да могат да пишат динамични реализации на Matcher. И ние предоставихме базовия клас, за да могат хората да го разширят в техния собствен код, така че техният код да не се повреди, докато добавяме още методи към интерфейса.

Оттогава добавихме метода describeMismatch към интерфейса на Matcher и клиентският код наследи изпълнение по подразбиране, без да се счупи. Ние също предоставихме допълнителни базови класове, които улесняват прилагането на describeMismatch без дублиране на логика.

И така, това е пример защо не можете да следвате сляпо някои общи „най-добри практики“, когато става въпрос за дизайн. Трябва да разбирате инструментите, които използвате, и да правите инженерни компромиси в този контекст.

РЕДАКТИРАНЕ: отделянето на интерфейса от базовия клас също помага за справяне с проблема с крехкия базов клас:

Ако добавите методи към интерфейс, който е имплементиран от абстрактен базов клас, може да се окажете с дублирана логика или в базовия клас, или в подкласове, когато те бъдат променени, за да имплементират новия метод. Не можете да промените базовия клас, за да премахнете тази дублирана логика, ако това промени API, предоставен на подкласове, защото това ще разбие всички подкласове - не е голям проблем, ако интерфейсът и реализациите са в една и съща кодова база, но лоша новина, ако сте автор на библиотека.

Ако интерфейсът е отделен от абстрактния базов клас -- тоест, ако правите разлика между потребители на типа и изпълнители на типа -- когато добавяте методи към интерфейса, можете да добавите имплементация по подразбиране към базовия клас, която няма прекъсване на съществуващите подкласове и въвеждане на нов базов клас, който осигурява по-добра частична реализация за нови подкласове. Когато някой дойде да промени съществуващите подкласове, за да имплементира метода по по-добър начин, тогава може да избере да използва новия базов клас, за да намали дублираната логика, ако има смисъл да го направи.

Ако интерфейсът и базовият клас са от един и същи тип (както някои предложиха в тази тема) и след това искате да въведете множество базови класове по този начин, вие сте заседнали. Не можете да въведете нов супертип, който да действа като интерфейс, защото това ще наруши клиентския код. Не можете да преместите частичната реализация надолу по йерархията на типове в нов абстрактен базов клас, защото това ще наруши съществуващите подкласове.

Това се отнася толкова много за признаци, колкото интерфейси и класове в стил Java или C++ множествено наследяване.

person pigroxalot    schedule 07.01.2014
comment
Благодаря ти! Тази статия ми помогна да го разбера много по-ясно, отколкото преди. Това е което търсих! - person James Dunn; 07.01.2014

Java8 вече позволява нови методи да бъдат добавяни към интерфейс, ако съдържат реализации по подразбиране.

interface Match<T>

    default void newMethod(){ impl... }

това е страхотен инструмент, който ни дава много свобода в дизайна и еволюцията на интерфейса.

Но какво ще стане, ако наистина искате да добавите абстрактен метод, който няма изпълнение по подразбиране?

Мисля, че трябва просто да продължите и да добавите метода. Ще разбие някои съществуващи кодове; и ще трябва да се оправят. Всъщност не е голяма работа. Вероятно бие други решения, които запазват двоичната съвместимост с цената на прецакване на целия дизайн.

person ZhongYu    schedule 20.12.2013
comment
Може ли някой да ми обясни как тази функция за добавяне на код в интерфейси е подобрение? Изглежда ми като ужасен HACK, който нарушава самата природа на OOP чрез въвеждане на изпълним код в интерфейси, които концептуално трябва да бъдат Contracts и да не съдържат код - person Federico Berasategui; 21.12.2013
comment
@HighCore Позволява на интерфейс както да декларира незадължителен метод към договора, така и да дефинира поведението му последователно, когато изпълнението или избере да го пропусне. Добър пример е Iterator.remove, чиято документация предвижда, че тези, които не поддържат премахване на елементи, трябва да хвърлят конкретно изключение. Използването на тази функция позволява на интерфейса да наложи това програмно. - person David Harkness; 22.12.2013
comment
@DavidHarkness, това звучи като това, за което са abstract classes. Това не обяснява защо java е поела по този ужасен път на нарушаване на OOP още по-лошо чрез въвеждане на изпълним код в интерфейсите. C# разрешава това по красив начин, като въвежда Extension Methods, докато java изглежда полага големи усилия, за да накара разработчиците да създадат още по-скапан код. - person Federico Berasategui; 23.12.2013
comment
HighCore Това предоставя едно от предимствата на множественото наследяване, което ми липсва от C++ и Python. Много ми харесва, че всеки език решава подобни проблеми по различен начин. Всекиму неговото предполагам. - person David Harkness; 23.12.2013
comment
Методите по подразбиране нямат достъп до никакво вътрешно състояние на обекта, така че не вредят на капсулирането. По същество те са същите като статичния метод, с изключение на това, че могат да бъдат заменени. Не виждам какво е счупено от това. - person MikeFHay; 23.12.2013
comment
свещените писания ясно посочват, че само еретици прилагат методи в интерфейси. - person Idan Arye; 23.12.2013

Но ако намерението е всеки клас, който имплементира Matcher, да разшири и BaseMatcher, защо изобщо да използвате интерфейс? Защо просто не направите Matcher абстрактен клас на първо място и всички други съпоставители да го разширят?

Чрез разделянето на интерфейса и имплементацията (абстрактният клас все още е имплементация) вие спазвате Принцип на инверсия на зависимостта. Не бъркайте с инжектиране на зависимост, нищо общо. Може да забележите, че в Hamcrest интерфейсът се съхранява в пакет hamcrest-api, докато абстрактният клас е в hamcrest-core. Това осигурява слабо свързване, тъй като изпълнението зависи само от интерфейсите, но не и от друго изпълнение. Една добра книга по тази тема е: Интерфейсно ориентиран дизайн: с шаблони.

Има ли някакво предимство да го правиш по начина, по който го направи Hamcrest? Или това е чудесен пример за лоша практика?

Решението в този пример изглежда грозно. Мисля, че коментарът е достатъчен. Създаването на такива методи за мъничета е излишно. Не бих следвал този подход.

person Mikhail    schedule 24.12.2013
comment
Съгласен съм и разбирам принципа на инверсия на зависимостта. Това, което ме хваща е, че въпреки технически спазването на принципа на инверсия на зависимостта и ниското свързване, Hamcrest настоява, че не им харесва, като ви принуждава да внедрите безполезен метод. Все още смятам, че бинарната съвместимост е най-вероятното обяснение, но +1 за споменаването на принципа на инверсия на зависимостта. - person James Dunn; 24.12.2013
comment
Мисля, че това е пример за лоша практика. Честно казано, не разбирам идеята ви за двоичната съвместимост. - person Mikhail; 25.12.2013