Чи застарілі заняття / методи застарілі?


37

Я використовував для створення багатьох абстрактних класів / методів. Потім я почав використовувати інтерфейси.

Зараз я не впевнений, чи інтерфейси не роблять абстрактні класи застарілими.

Вам потрібен повністю абстрактний клас? Створіть замість цього інтерфейс. Вам потрібен абстрактний клас з деякою реалізацією в ньому? Створіть інтерфейс, створіть клас. Успадкуйте клас, реалізуйте інтерфейс. Додатковою перевагою є те, що деяким класам може не знадобитися батьківський клас, а вони просто реалізують інтерфейс.

Отже, чи застарілі абстрактні класи / методи?


Як щодо того, якщо обрана вами мова програмування не підтримує інтерфейси? Здається, я пам'ятаю, що це стосується C ++.
Бернар

7
@Bernard: у C ++ абстрактний клас - це інтерфейс у всіх, крім імені. Те, що вони також можуть робити більше, ніж «чисті» інтерфейси, не є недоліком.
gbjbaanb

@gbjbaanb: Я думаю. Я не пам'ятаю, щоб використовувати їх як інтерфейси, а скоріше, щоб забезпечити реалізацію за замовчуванням.
Бернард

Інтерфейси - це "валюта" посилань на об'єкти. Взагалі, вони є основою поліморфної поведінки. Абстрактні заняття служать іншому призначенню, що ідеально пояснило дедаллакс.
джиггі

4
Хіба це не так, як казати "чи застаріли види транспорту, тепер у нас є машини?" Так, більшість часу ти користуєшся автомобілем. Але чи вам коли-небудь потрібно щось, крім машини чи ні, не було б дійсно правильним сказати "мені не потрібно використовувати види транспорту". Інтерфейс майже такий же, як абстрактний клас без будь-якої реалізації, і зі спеціальною назвою, ні?
Джек В.

Відповіді:


111

Ні.

Інтерфейси не можуть забезпечити реалізацію за замовчуванням, абстрактні класи та методи можуть. Це особливо корисно, щоб уникнути дублювання коду у багатьох випадках.

Це також дуже приємний спосіб зменшити послідовне з'єднання. Без абстрактного методу / класів ви не можете реалізувати шаблон шаблону методу. Пропоную переглянути цю статтю у Вікіпедії: http://en.wikipedia.org/wiki/Template_method_pattern


3
@deadalnix Шаблон методу шаблону може бути досить небезпечним. Ви можете легко отримати сильно зв'язаний код і почати злому коду, щоб розширити шаблонний абстрактний клас, щоб обробити "лише ще один випадок".
Quant_dev

9
Це більше схоже на антидіаграму. Ви можете отримати точно те ж саме з композицією проти інтерфейсу. Те саме стосується часткових занять деякими абстрактними методами. Замість того , змушуючи код клієнта підклас і перевизначити їх, ви повинні мати здійснення бути введені . Дивіться: en.wikipedia.org/wiki/Composition_over_inheritance#Benefits
back2dos

4
Це не пояснює ВСЕ, як зменшити послідовне з'єднання. Я згоден, що для досягнення цієї мети існує кілька способів, але, будь ласка, відповідайте на проблему, про яку ви говорите, а не сліпо посилайтесь на якийсь принцип. Якщо ви не зможете пояснити, чому це пов'язано з реальною проблемою, ви закінчите займатися культавим програмуванням
deadalnix

3
@deadalnix: Скажімо, що послідовне з’єднання найкраще зводити до шаблону шаблону - це культове програмування вантажу (використовуючи стан / стратегію / заводський зразок, можливо, також спрацьовує), як і припущення, що його завжди потрібно скорочувати. Послідовне з'єднання часто є простим наслідком виявлення дуже дрібного контролю над чимось, що не означає нічого, крім компромісу. Це все ще не означає, що я не можу написати обгортку, яка не має послідовного з'єднання за допомогою складу. Насправді це набагато простіше.
back2dos

4
Тепер у Java є реалізовані за замовчуванням інтерфейси. Чи це змінює відповідь?
raptortech97

15

Абстрактний метод існує, щоб ви могли викликати його з базового класу, але реалізувати його у похідному класі. Тож ваш базовий клас знає:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Це досить приємний спосіб здійснити отвір у середньому шаблоні, не знаючи похідного класу про налаштування та очищення. Без абстрактних методів вам доведеться постійно покладатися на похідний клас, що реалізує DoTaskта запам'ятовує дзвінки base.DoSetup()та base.DoCleanup()весь час.

Редагувати

Також, завдяки deadalnix за публікацію посилання на шаблон шаблону методу , що я описав вище, не знаючи назви. :)


2
+1, я віддаю перевагу вашій відповіді до deadalnix '. Зауважте, що ви можете реалізувати метод шаблону більш прямолінійно, використовуючи делегатів (у C #):public void DoTask(Action doWork)
Джон

1
@Joh - правда, але це не обов'язково так добре, залежно від вашого API. Якщо ваш базовий клас є, Fruitа похідний клас - Apple, ви хочете зателефонувати myApple.Eat(), а не myApple.Eat((a) => howToEatApple(a)). Крім того, вам не хочеться Appleтелефонувати base.Eat(() => this.howToEatMe()). Я думаю, що чистіше просто перекрити абстрактний метод.
Скотт Вітлок

1
@Scott Whitlock: Ваш приклад використання яблук і фруктів трохи відірваний від реальності, щоб судити про те, чи віддавати спадщину переважніше делегатам взагалі. Очевидно, що відповідь "це залежить", тому це залишає багато місця для дебатів ... У будь-якому випадку, я вважаю, що шаблонні методи трапляються багато під час рефакторингу, наприклад, при видаленні дублікату коду. У таких випадках я зазвичай не хочу возитися з ієрархією типів, і я вважаю за краще триматися подалі від спадщини. Цей вид операції простіше зробити з лямбдами.
Джон

Це питання ілюструє більше, ніж просте застосування шаблону методу шаблонів - публічний / непублічний аспект відомий у спільноті C ++ як шаблон невіртуального інтерфейсу . Маючи загальнодоступну невіртуальну функцію, оберніть віртуальну - навіть якщо спочатку перша не робить нічого, крім виклику останнього - ви залишаєте пункт налаштування для налаштування / очищення, ведення журналів, профілювання, перевірок безпеки тощо, які можуть знадобитися пізніше.
Тоні

10

Ні, вони не застаріли.

Насправді існує неясна, але принципова різниця між абстрактними класами / методами та інтерфейсами.

якщо набір класів, у яких один із них має бути використаний, має спільну поведінку, якою вони поділяються (пов'язані класи, я маю на увазі), то перейдіть до абстрактних класів / методів.

Приклад: клерк, офіцер, директор - усі ці класи мають у собі CalculateSalary (), використовують абстрактні базові класи.CalculateSalary () можуть бути реалізовані по-різному, але є деякі інші речі, наприклад, GetAttendance (), наприклад, які мають загальне визначення в базовому класі.

Якщо у ваших класів немає нічого спільного (Незалежні класи у вибраному контексті) між ними, але у них є дія, яка сильно відрізняється в реалізації, перейдіть до інтерфейсу.

Приклад: класи корів, лавок, автомобілів, не пов'язані з телезопом, але Isortable може бути там, щоб сортувати їх у масиві.

Цю різницю зазвичай ігнорують, коли підходити з поліморфної точки зору. Але особисто я відчуваю, що бувають ситуації, коли одне є влучнішим, ніж інше з поясненої вище причини.


6

Окрім інших хороших відповідей, існує принципова різниця між інтерфейсами та абстрактними класами, про які ніхто конкретно не згадував, а саме, що інтерфейси набагато менш надійні і тому накладають набагато більший тягар тесту, ніж абстрактні класи. Наприклад, розгляньте цей код C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Мені гарантовано, що є лише два шляхи коду, які мені потрібно перевірити. Автор Frobbit може розраховувати на те, що frobber або червоний, або зелений.

Якщо замість цього ми скажемо:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Зараз я абсолютно нічого не знаю про наслідки цього дзвінка на Frob. Мені потрібно бути впевненим, що весь код у Frobbit є надійним проти будь-якої можливої ​​реалізації IFrobber , навіть реалізацій людьми, які є некомпетентними (поганими) чи активно ворожими мені чи моїм користувачам (куди гірше).

Абстрактні заняття дозволяють уникнути всіх цих проблем; використовуйте їх!


1
Проблему, про яку ви говорите, слід вирішити за допомогою алгебраїчних типів даних, а не згинання класів до точки, коли вони порушують принцип відкритого / закритого типу.
back2dos

1
По-перше, я ніколи не говорив, що це добре чи морально . Ти поклав це мені в рот. По-друге (це моя фактична думка): це не що інше, як поганий привід для відсутності мовної функції. Нарешті, є рішення без абстрактних класів: pastebin.com/DxEh8Qfz . На відміну від цього, ваш підхід використовує вкладені та абстрактні класи, викидаючи всі, крім коду, що вимагають безпеки, разом у велику кульку грязі. Немає вагомих причин, чому RedFrobber або GreenFrobber повинні бути прив'язані до обмежень, які ви хочете виконати. Це збільшує зв'язок і блокує багато рішень без будь-якої вигоди.
back2dos

1
Сказати, що абстрактні методи застаріли, без сумніву, неправильно, але стверджувати, з іншого боку, що їм слід віддавати перевагу над інтерфейсами, є помилковим. Вони просто інструмент для вирішення інших проблем, ніж інтерфейси.
Groo

2
"існує принципова різниця [...] інтерфейсів набагато менш надійні [...], ніж абстрактні класи". Я не погоджуюся, різниця, яку ви проілюстрували у своєму коді, покладається на обмеження доступу, які, на мою думку, не мають жодних причин суттєво відрізнятися між інтерфейсами та абстрактними класами. Це може бути так у C #, але питання є мовно-агностичним.
Джон

1
@ back2dos: Я просто хотів би зазначити, що рішення Еріка насправді використовує алгебраїчний тип даних: абстрактний клас - це сума. Виклик абстрактного методу - це те саме, що відповідність шаблону для набору варіантів.
Родрік Чапман

4

Ви самі це говорите:

Вам потрібен абстрактний клас з деякою реалізацією в ньому? Створіть інтерфейс, створіть клас. Успадкуйте клас, реалізуйте інтерфейс

це звучить багато роботи порівняно з "успадкувати абстрактний клас". Ви можете зробити роботу для себе, наблизившись до коду з точки зору "пуриста", але я вважаю, що мені достатньо це зробити, не намагаючись додати моє навантаження без жодної практичної користі.


Не кажучи вже про те, що якщо клас не успадковує інтерфейс (про який не згадувалося), потрібно писати функції переадресації на деяких мовах.
Sjoerd

Гм, додаткова «Багато роботи», яку ви згадали, полягає в тому, що ви створили інтерфейс і змусили його класи реалізувати - чи справді це виглядає як багато роботи? На додаток до цього, звичайно, інтерфейс дає вам можливість реалізувати інтерфейс з нової ієрархії, яка б не працювала з абстрактним базовим класом.
Білл К

4

Як я коментував пост @deadnix: Часткові реалізації є анти-шаблоном, незважаючи на те, що шаблон шаблону формалізує їх.

Чисте рішення для цього прикладу вікіпедії шаблону шаблонів :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Це рішення краще, оскільки воно використовує композицію замість спадкування . Шаблон шаблону вводить залежність між виконанням правил Монополії та реалізацією способів запуску ігор. Однак це дві абсолютно різні обов'язки, і немає вагомих причин їх з'єднувати.


+1. Як зауваження: "Часткові реалізації є антидіаграмою, незважаючи на те, що шаблон шаблону формалізує їх." Опис у wikipedia чітко визначає шаблон, лише приклад коду є "неправильним" (в тому сенсі, що він використовує успадкування, коли це не потрібно, і існує більш проста альтернатива, як показано вище). Іншими словами, я не думаю, що в цьому винна сама викрадка, лише те, як люди прагнуть її реалізувати.
Джон

2

Ні. Навіть запропонована альтернатива включає використання абстрактних класів. Крім того, оскільки ви не вказали мову, я збираюся йти прямо вперед і сказати, що загальний код - кращий варіант, ніж крихке успадкування в будь-якому випадку. Абстрактні класи мають суттєві переваги перед інтерфейсами.


-1. Я не розумію, що стосується "загального коду проти успадкування". Ви повинні проілюструвати або обґрунтувати, чому "абстрактні класи мають значні переваги перед інтерфейсами".
Джон

1

Абстрактні класи не є інтерфейсами. Вони - класи, які неможливо створити.

Вам потрібен повністю абстрактний клас? Створіть замість цього інтерфейс. Вам потрібен абстрактний клас з деякою реалізацією в ньому? Створіть інтерфейс, створіть клас. Успадкуйте клас, реалізуйте інтерфейс. Додатковою перевагою є те, що деяким класам може не знадобитися батьківський клас, а вони просто реалізують інтерфейс.

Але тоді у вас був би не абстрактний марний клас. Абстрактні методи потрібні для заповнення функціонального отвору в базовому класі.

Наприклад, з урахуванням цього класу

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Як би ви реалізували цю базову функціональність у класі, який не має ні Frob()ані IsFrobbingNeeded?


1
Інтерфейс також неможливо створити.
Sjoerd

@Sjoerd: Але вам потрібен базовий клас із спільною реалізацією; це не може бути інтерфейсом.
конфігуратор

1

Я творець сервлетської структури, де абстрактні класи відіграють істотну роль. Я б сказала більше, мені потрібні напів абстрактні методи, коли метод потрібно переосмислити в 50% випадків, і я хотів би, щоб попередження компілятора про цей метод не було відмінено. Я вирішую проблему, додаючи примітки. Повертаючись до свого питання, є два різних випадки використання абстрактних класів та інтерфейсів, і поки ніхто не застарілий.


0

Я не думаю, що вони застаріли інтерфейсами, але вони можуть бути застарілими за схемою стратегії.

Основне використання абстрактного класу - відкласти частину реалізації; сказати, "цю частину реалізації класу можна зробити різною".

На жаль, абстрактний клас змушує клієнта зробити це через спадкування . Тоді як схема стратегії дозволить вам досягти того ж результату без успадкування. Клієнт може робити екземпляри вашого класу, а не завжди визначати свої власні, і "реалізація" (поведінка) класу може динамічно змінюватися. Модель стратегії має додатковий плюс у тому, що поведінка може бути змінена під час виконання, не тільки під час проектування, а також має значно слабкіше з'єднання між типовими типами.


0

Я, як правило, стикався з набагато більшими проблемами обслуговування, пов'язаними з чистими інтерфейсами, ніж з ABC, навіть з ABC, які використовуються з багатократним успадкуванням. YMMV - не знаю, можливо, наша команда просто використала їх недостатньо.

Це означає, що якщо ми використовуємо реальну світову аналогію, то як багато застосовують чисті інтерфейси, повністю позбавлені функціональності та стану? Якщо я використовую USB як приклад, це досить стабільний інтерфейс (я думаю, ми зараз на USB 3.2, але він також підтримував зворотну сумісність).

Але це не інтерфейс без громадянства. Він не позбавлений функціональності. Це більше схоже на абстрактний базовий клас, ніж на чистий інтерфейс. Це насправді ближче до конкретного класу з дуже специфічними функціональними та державними вимогами, і лише абстракція полягає в тому, що пробки до порту є єдиною замінною частиною.

Інакше це просто "дірка" у вашому комп’ютері зі стандартизованим форм-фактором та значно слабкішими функціональними вимогами, які б нічого не зробили самостійно, поки кожен виробник не придумав власне обладнання, щоб змусити цю дірку щось робити, і в цей момент він стає набагато слабшим стандартом і не що інше, як «дірка» та конкретизація того, що він повинен робити, але немає центрального положення про те, як це зробити. Тим часом у нас може з’явитися 200 різних способів зробити це після того, як всі виробники обладнання спробують придумати власні способи приєднати функціональність та стан до цієї «дірки».

І тоді ми можемо мати певних виробників, які впроваджують різні проблеми в порівнянні з іншими. Якщо нам потрібно оновити специфікацію, у нас може бути 200 різних конкретних реалізацій USB-портів із абсолютно різними способами вирішення специфікації, що потребує оновлення та тестування. Деякі виробники можуть розробити фактичні стандартні реалізації, якими вони діляться між собою (ваш аналогічний базовий клас, що реалізує цей інтерфейс), але не всі. Деякі версії можуть бути повільнішими, ніж інші. Деякі можуть мати кращу пропускну здатність, але гірші затримки або навпаки. Деякі можуть використовувати більше енергії акумулятора, ніж інші. Деякі можуть відшаровуватися і не працювати з усім обладнанням, яке повинно працювати з портами USB. Деяким може знадобитися приєднання ядерного реактора для роботи, який має тенденцію піддавати користувачам радіаційне отруєння.

І саме це я знайшов, особисто, з чистими інтерфейсами. Можливо, є деякі випадки, коли вони мають сенс, як-от просто моделювати форм-фактор материнської плати щодо корпусу процесора. Аналогії форм-фактора дійсно є майже бездержавними і позбавленими функціоналу, як у аналогічної "дірки". Але я часто вважаю величезною помилкою команд вважати, що вони якимось чином вищі у всіх випадках, навіть не близьких.

Навпаки, я думаю, що набагато більше випадків було б вирішено краще ABC, ніж інтерфейси, якщо це два варіанти, якщо ваша команда не є настільки гігантською, що фактично бажано мати аналогічний вище еквівалент 200 конкуруючих реалізацій USB, а не один центральний стандарт підтримувати. У колишній команді, в якій я був, я фактично мусив боротися, просто послабити стандарт кодування, щоб дозволити ABC та багаторазове успадкування, і, головним чином, у відповідь на ці проблеми з технічним обслуговуванням, описані вище.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.