Чи "Якщо метод повторно використовується без змін, покладіть метод в базовий клас, а ще створіть інтерфейс"?


10

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

Він каже:

Уявіть кожен новий метод, який ви збираєтеся реалізувати. Для кожного з них врахуйте це: чи буде цей метод реалізований більш ніж одним класом саме в такому вигляді, без будь-яких змін? Якщо відповідь «так», створіть базовий клас. У будь-якій іншій ситуації створіть інтерфейс.

Наприклад:

Розглянемо класи catта dog, які розширюють клас mammalта мають єдиний метод pet(). Потім ми додаємо клас alligator, який нічого не розширює і має єдиний метод slither().

Тепер ми хочемо додати eat()метод до всіх них.

Якщо реалізація eat()методу буде точно такою ж cat, dogі alligatorми повинні створити базовий клас (скажімо, animal), який реалізує цей метод.

Однак якщо його реалізація alligatorвідрізняється найменшим чином, нам слід створити IEatінтерфейс і зробити його mammalта alligatorреалізувати.

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

Чи варто дотримуватися цього правила?


27
alligatoreatЗвичайно, реалізація відрізняється тим, що вона приймає catі dogяк параметри.
sbichenko

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

2
Коли ви малюєте себе в кутку, найкраще опинитися в тій найближчій до дверей.
JeffO

10
Найбільша помилка, яку роблять люди, вважає, що інтерфейси - це просто порожні абстрактні класи. Інтерфейс - це спосіб програміста сказати: "Мені все одно, що ти мені даєш, доки він дотримується цієї конвенції". Потім повторне використання коду має здійснюватися (в ідеалі) за допомогою композиції. Ваш колега помиляється.
riwalk

2
СС Proff мого вчив , що суперклас повинен бути is aі інтерфейси acts likeабо is. Так собачий is aссавець і acts likeїдкий. Це скаже нам, що ссавець повинен бути класом, а їдкий повинен бути інтерфейсом. Це завжди було дуже корисним посібником. Сторінка: Прикладом isможе бути The cake is eatableабо The book is writable.
MirroredFate

Відповіді:


13

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

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

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

Процитуйте Rich Hickey: Simple! = Easy


Чи можете ви надати основний приклад PetEatingBehaviorреалізації?
sbichenko

1
@exizt По-перше, я погано вибираю імена. Так що PetEatingBehavior, мабуть, не той. Я не можу дати тобі реалізацію, оскільки це лише приклад іграшки. Але я можу описати кроки рефакторингу: він має конструктор, який бере всю інформацію, яку використовують коти / собаки для визначення eatметоду (зубний екземпляр, екземпляр шлунка тощо). Він містить лише код, загальний для котів і собак, який використовується в їх eatметоді. Вам просто потрібно створити PetEatingBehaviorчлен і переадресувати його дзвінки на dog.eat/cat.eat.
Саймон Бергот

Я не можу повірити, що це єдина відповідь багатьох, щоб відмовитись від спадщини :( Наслідування не для повторного використання коду!
Danny Tuppeny

1
@DannyTuppeny Чому б і ні?
sbichenko

4
@exizt Наслідування у класі - велика справа; ви приймаєте (ймовірно, постійну) залежність від чогось, що для багатьох мов ви можете мати лише одну. Повторне використання коду є досить тривіальною справою для використання цього часто-єдиного базового класу. Що робити, якщо ви закінчите з методами, де ви хочете поділитися ними з іншим класом, але він вже має базовий клас через повторне використання інших методів (або навіть, що це частина "реальної" ієрархії, що використовується поліморфно (?)). Використовуйте спадщину, коли ClassA - це тип ClassB (наприклад, автомобіль успадковує від транспортного засобу), а не тоді, коли він ділиться деякими внутрішніми деталями впровадження :-)
Danny Tuppeny

11

Чи варто дотримуватися цього правила?

Це гідне правило, але я знаю досить багато місць, де я його порушую.

Для справедливості я використовую (абстрактні) базові класи набагато більше, ніж мої однолітки. Я роблю це як захисне програмування.

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

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


1
Читаючи цю відповідь через 3 роки, я вважаю, що це є чудовим моментом: Вибираючи використовувати базовий клас, а не інтерфейс, ви говорите "це основна функціональність класу (і будь-якого спадкоємця), і це недоречно змішувати
вольово-невільно

9

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

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

Ось як викладав мою професію різниці:

Суперкласи повинні бути, is aа інтерфейси - acts likeабо is. Так собачий is aссавець і acts likeїдкий. Це скаже нам, що ссавець повинен бути класом, а їдкий повинен бути інтерфейсом. Прикладом isможе бути The cake is eatableабо The book is writable(створення обох eatableта writableінтерфейсів).

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

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

Всього два мої копійки.


6

На прохання виходу, я розширюю свій коментар на більш довгу відповідь.

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

Бібліотека .NET є прекрасним прикладом. Наприклад, коли ви пишете функцію, яка приймає IEnumerable<T>, те, що ви говорите, "Мені все одно, як ви зберігаєте свої дані. Я просто хочу знати, що я можу використовувати foreachцикл.

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

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

Використовувати спадщину лише тому, що вам подобається звучання слів «повторне використання коду» - досить погана ідея. Посібник із створення коду від Google робить це досить чітким:

Склад часто доречніший за спадщину. ... [B] оскільки код, що реалізує підклас, поширюється між базовим і підкласом, це може бути складніше зрозуміти реалізацію. Підклас не може змінювати функції, які не є віртуальними, тому підклас не може змінити реалізацію.

Щоб проілюструвати, чому успадкування не завжди є відповіддю, я буду використовувати клас під назвою MySpecialFileWriter †. Людина, яка сліпо вірить, що спадкування - це рішення всіх проблем, може стверджувати, що вам слід намагатися успадкувати FileStream, щоб не дублювати FileStreamкод. Розумні люди визнають, що це нерозумно. Вам слід просто мати FileStreamоб’єкт у своєму класі (як локальну або змінну членів) і використовувати його функціональність.

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

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

Зрештою, настрої вашого колеги не мають глибини та розуміння, необхідних для прийняття обґрунтованого рішення. Дослідіть тему і розгадайте самі.

† Ілюструючи спадщину, всі використовують тварин. Це марно. За 11 років розвитку я ніколи не писав клас на ім'я Cat, тому не збираюся використовувати його як приклад.


Отже, якщо я вас правильно зрозумів, ін'єкція залежності надає ті ж можливості повторного використання коду, що й успадкування. Але для чого б ви тоді використали спадщину? Чи надаєте ви футляр для використання? (Я дуже цінував того, з FileStream
ким

1
@exizt, ні, це не забезпечує однакові можливості повторного використання коду. Він забезпечує кращі можливості повторного використання коду (у багатьох випадках), оскільки він підтримує реалізацію. Класи взаємодіють з явним чином через свої об'єкти та їх публічний API, а не неявно набувають можливостей і повністю змінюють половину функціональності, перевантажуючи існуючі функції.
riwalk

4

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

Вам у цьому випадку краще, абстрагуючи поведінку, як це:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

Або застосуйте схему відвідувачів, коли FoodProcessorнамагаються погодувати сумісних тварин ( ICarnivore : IFoodEater, MeatProcessingVisitor). Думка про живих тварин може заважати вам думати про те, щоб зірвати їх травний тракт і замінити його на загальний, але ви дійсно робите їм послугу.

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

Так, так, базові класи мають своє місце, правило діє (часто, не завжди, як це правило), але продовжуйте шукати поведінку, яку ви можете виділити.


1
IFoodEater є надмірним. Що ще ви очікуєте, щоб мати можливість їсти їжу? Так, тварини як приклад - жахлива річ.
Ейфорія

1
А що з м’ясоїдними рослинами? :) Серйозно, одна з головних проблем із тим, що в цьому випадку не використовуються інтерфейси, полягає в тому, що ви тісно пов'язали концепцію їжі з тваринами, і це набагато більше роботи, щоб запровадити нові абстракції, для яких базовий клас Animal не підходить.
Джулія Хейвард

1
Я вважаю, що "їсти" - це неправильна ідея: ідіть більш абстрактно. Як щодо IConsumerінтерфейсу. М'ясоїди можуть споживати м'ясо, травоїдні тварини споживають рослини, а рослини споживають сонячне світло і воду.

2

Інтерфейс - це дійсно договір. Немає функціоналу. Це залежить від виконавця. Немає вибору, оскільки треба виконати контракт.

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

Наприклад:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

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

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


1

Ні.

Інтерфейси - це те, як реалізуються методи. Якщо ви базуєте своє рішення щоразу використовувати інтерфейс на тому, як будуть реалізовані методи, то ви робите щось не так.

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

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


Зачекайте, навіщо реалізовуватись eat()у кожній тварині? Ми можемо зробити mammalце реалізовувати, чи не так? Я розумію, що ви ставитеся до вимог.
sbichenko

@exizt: Вибачте, я неправильно прочитав ваше запитання.
Ейфорія

1

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

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

Інтерфейси корисні, коли вам потрібно однаково взаємодіяти з набором класів, але ви не можете передбачити або не перейматися їх реальною реалізацією. Візьмемо для прикладу щось на зразок інтерфейсу Collection. Господь знає лише безліч способів, коли хтось може вирішити реалізувати колекцію предметів. Список пов'язаних? Список масиву? Стек? Чергу? Цей метод SearchAndReplace не хвилює, йому просто потрібно передати щось, що може викликати "Додати, видалити" та "GetEnumerator".


1

Я якось дивлюсь відповіді на це питання сам, щоб побачити, що мають сказати інші люди, але я скажу три речі, які я схильний думати про це:

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

  2. Немає нічого поганого в перекручуванні методу, виклику super.method()всередині нього та дозволі решті його тіла робити те, що не заважає базовому класу. Це не порушує Принципу заміни Ліскова .

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

Тож я б взяв те, що було сказано, із зерном солі.


5
People put way too much faith into interfaces, and too little faith into classes.Смішно. Я схильний вважати, що все навпаки.
Саймон Бергот

1
"Це не порушує Принципу заміни Ліскова". Але це надзвичайно близько, щоб стати порушенням LSP. Краще будьте в безпеці, ніж вибачте.
Ейфорія

1

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

Інтерфейс - це договір.

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

Спадщина з іншого боку - це механізм повторного використання. У цьому випадку, я думаю, що гарне семантичне міркування може допомогти вам вирішити, який метод слід подати. Наприклад, хоча всі тварини можуть спати і сплять однаково, я не буду використовувати для цього базовий клас. Спочатку я ставлю Sleep()метод в IAnimalінтерфейс як наголос (семантичний) на те, що потрібно, щоб бути твариною, потім використовую базовий абстрактний клас для котів, собак та алігатора як техніку програмування, щоб не повторюватися.


0

Я не впевнений, що це найкраще правило. Ви можете поставити abstractметод у базовий клас і дозволити кожному класу його реалізувати. Оскільки кожен mammalмає їсти, це було б сенсом робити. Тоді кожен mammalможе використовувати свій єдиний eatметод. Зазвичай я використовую лише інтерфейси для зв'язку між об'єктами або як маркер для позначення класу як чогось. Я думаю, що надмірне використання інтерфейсів спричиняє неохайний код.

Я також погоджуюсь з @Panzercrisis про те, що правило, як правило, має бути лише загальним принципом. Не те, що слід писати каменем. Безперечно, будуть часи, коли це просто не працює.


-1

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

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

Один клас не вимагає інтерфейсу.

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

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

Улюблений склад над спадщиною - все це відходить.

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