Які ролі синглів, абстрактних класів та інтерфейсів?


13

Я вивчаю OOP в C ++ і, хоча знаю визначення цих 3 понять, я не можу реально усвідомити, коли або як ним користуватися.

Давайте використаємо цей клас для прикладу:

class Person{
    private:
             string name;
             int age;
    public:
             Person(string p1, int p2){this->name=p1; this->age=p2;}
             ~Person(){}

             void set_name (string parameter){this->name=parameter;}                 
             void set_age (int parameter){this->age=parameter;}

             string get_name (){return this->name;}
             int get_age (){return this->age;}

             };

1. Синглтон

ЯК працює обмеження класу на роботу лише одного об’єкта?

CAN ви створити клас , який буде мати ТІЛЬКИ 2 примірників? А може, 3?

КОЛИ вживається однотонний рекомендований / необхідний? Це хороша практика?

2. Анотаційний клас

Наскільки я знаю, якщо існує лише одна чиста віртуальна функція, клас стає абстрактним. Отже, додаючи

virtual void print ()=0;

зробив би це, правда?

ЧОМУ вам потрібен клас, об’єкт якого не потрібен?

3.Інтерфейс

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

ЯКА основна відмінність між двома з них?

Спасибі заздалегідь!


2
Синглтон суперечливий, пошукайте його на цьому сайті, щоб отримати різні думки.
Вінстон Еверт

2
Варто також зазначити, що хоча абстрактні класи є частиною мови, не є одинаковими або інтерфейсними. Вони є моделями, які люди реалізують. Зокрема, Singleton - це те, що для роботи потрібно трохи розумного злому. (Хоча, звичайно, ви можете створити сингл тільки умовно.)
Gort the Robot

1
По одному, будь ласка.
JeffO

Відповіді:


17

1. Синглтон

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

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

Якщо ви хочете дізнатися більше про Singletons, ви можете почати з Вікіпедії, а особливо для C ++ у цій публікації .

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

2. Конспект заняття

Так, правильно. Лише один віртуальний метод позначить клас абстрактним.

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

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

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

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

3. Інтерфейси

Чистих інтерфейсів у C ++ немає в тому ж сенсі, що у вас на Java або C #. Єдиний спосіб створити його - це мати чистий абстрактний клас, який імітує більшу частину поведінки, яку ви хочете використовувати через інтерфейс.

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

Ви можете прочитати про специфікацію інтерфейсу для C # в MSDN, щоб мати кращу ідею:

http://msdn.microsoft.com/en-us/library/ms173156.aspx

C ++ забезпечить такий же тип поведінки, маючи чисто абстрактний клас.


2
Чистий абстрактний базовий клас дає вам усе, що робить інтерфейс. Інтерфейси існують у Java (та C #), оскільки мовні дизайнери хотіли запобігти багатократному успадкуванню (через головні болі, які він створює), але визнали дуже поширеним використання множинних успадковань, що не є проблематичним.
Gort the Robot

@StevenBurnap: Але не в C ++, що є контекстом питання.
DeadMG

3
Він запитує про C ++ та інтерфейси. "Інтерфейс" не є мовною особливістю C ++, але люди, безумовно, створюють інтерфейси в C ++, які працюють точно так само, як інтерфейси Java, використовуючи абстрактні базові класи. Вони робили це ще до того, як Java навіть існувала.
Gort the Robot


1
Те саме стосується Сінглтон. У C ++ обидва є моделями дизайну, а не мовними особливостями. Це не означає, що люди не говорять про інтерфейси на C ++ та про те, для чого вони потрібні. Поняття "інтерфейс" вийшло з таких компонентних систем, як Corba та COM, які були спочатку розроблені для використання в чистому C. В C ++ інтерфейси, як правило, реалізуються з абстрактними базовими класами, в яких усі методи є віртуальними. Функціонал цього ідентичний функціоналу Java-інтерфейсу. Таким чином, поняття інтерфейсу Java навмисно є підмножиною абстрактних класів C ++.
Gort the Robot

8

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

Singletons - Коли ви хочете, щоб усі викликові коди використовували один екземпляр змінних, з будь-яких причин у вас є такі варіанти:

  • Глобальні змінні - очевидно, немає інкапсуляції, більша частина коду поєднана з глобальними ... погано
  • Клас зі всіма статичними функціями - трохи краще, ніж прості глобали, але це дизайнерське рішення все одно веде вас до шляху, коли код покладається на глобальні дані і пізніше може бути дуже важко змінити. Крім того, ви не можете скористатися такими речами, як поліморфізм, якщо у вас є лише статичні функції
  • Синглтон - Хоча існує лише один екземпляр класу, реальна реалізація класу не повинна нічого знати про те, що він глобальний. Отже, сьогодні у вас може бути клас, який є однотонним, завтра ви можете просто зробити його конструктор загальнодоступним і дозволити клієнтам створювати декілька копій. Більшість клієнтських кодів, на які посилається синглтон, не доведеться змінювати, а реалізацію самого сингтона не потрібно змінювати. Єдина зміна полягає в тому, як клієнтський код в першу чергу набуває одиночної посилання.

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

То де б ви використовували одиночку? Ось кілька прикладів:

  • Ведення журналу - якщо ви хочете, щоб весь ваш процес мав єдиний журнал, ви можете створити об’єкт журналу та передати його всюди. Але що робити, якщо у вас є 100 000 000 рядків застарілого коду програми? змінити їх усіх? Або ви можете просто представити наступне та почати використовувати його будь-де:

    CLog::GetInstance().write( "my log message goes here" );
  • Кеш-з'єднання з сервером - це було щось, що мені довелося представити у нашій програмі. Наша база коду, і її було дуже багато, використовувалася для підключення до серверів коли завгодно. Здебільшого це було нормально, якщо тільки в мережі не було затримок. Нам було потрібно рішення та перероблення 10-річної програми насправді не було на столі. Я написав одиночний CServerConnectionManager. Потім я провів код і замінив виклики CoCreateInstanceWithAuth на однаковий дзвінок підпису, який викликав мій клас. Тепер після першої спроби з'єднання було кешовано, а решту часу спроби "підключити" були миттєвими. Деякі кажуть, що одинаки - це зло. Я кажу, що вони врятували мою недопалку.

  • Для налагодження ми часто знаходимо глобальну запущену таблицю об'єктів дуже корисною. У нас є кілька занять, які ми хотіли б відстежувати. Всі вони походять з одного базового класу. Під час інстанції вони викликають таблицю об'єктів однотонною і реєструються. Коли вони знищені, вони не реєструються. Я можу підійти до будь-якої машини, долучитися до процесу і створити список запущених об'єктів. Я був у продукті вже понад пів десятиліття, і я ніколи не відчував, що у нас коли-небудь виникала потреба у 2 "глобальних" об'єктних таблицях.

  • У нас є кілька відносно складних класів утиліти для аналізу пар рядків, які покладаються на регулярні вирази. Регулярні класи виразів повинні бути ініціалізовані, перш ніж вони можуть виконувати збіги. Ініціалізація дещо дорога, тому що тоді FSM формується на основі рядка розбору. Однак після цього до класу регулярних виразів можна безпечно отримати доступ до 100 потоків, оскільки один раз побудований FSM ніколи не змінюється. Ці класи парсера використовують внутрішньо сингли, щоб переконатися, що ця ініціалізація відбувається лише один раз. Це значно покращило продуктивність і ніколи не спричиняло жодних проблем через «злих одинаків».

Сказавши все це, вам потрібно мати на увазі, коли і де використовувати одиночні кнопки. 9 з 10 разів є кращим рішенням і, будь-якими способами, ви повинні використовувати це замість цього. Однак бувають випадки, коли одиночний абсолютно правильний вибір дизайну.

Наступна тема ... інтерфейси та абстрактні класи. По-перше, як згадували інші, інтерфейс IS абстрактний клас, але він виходить за рамки цього, встановлюючи, що він абсолютно НЕ реалізує. У деяких мовах інтерфейсне слово є частиною мови. У C ++ ми просто використовуємо абстрактні класи. Microsoft VC ++ зробив один крок, щоб визначити це десь внутрішньо:

typedef struct interface;

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

То де б ви це використали? Повернемось до мого прикладу запущеної таблиці об’єктів. Скажімо, базовий клас має ...

віртуальний недійсний друк () = 0;

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

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

Ось ще один приклад. Скажімо, у мене є клас, який реалізує реєстратор, CLog. Цей клас записує у файл на локальному диску. Я починаю використовувати цей клас у своєму спадщині 100 000 рядків коду. Повсюдно. Життя добре, поки хтось не скаже, ей давайте запишемо в базу даних замість файлу. Тепер я створюю новий клас, назвемо його CDbLog і записуємо в базу даних. Чи можете ви уявити клопоту пройти 100 000 ліній і змінити все від CLog до CDbLog? Як варіант, я міг би:

interface ILogger {
    virtual void write( const char* format, ... ) = 0;
};

class CLog : public ILogger { ... };

class CDbLog : public ILogger { ... };

class CLogFactory {
    ILogger* GetLog();
};

Якщо весь код використовував інтерфейс ILogger, все, що мені потрібно було б змінити, це внутрішня реалізація CLogFactory :: GetLog (). Решта коду буде просто автоматично працювати без мене, щоб підняти палець.

Для отримання додаткової інформації про інтерфейси та хороший дизайн OO, я настійно рекомендую спритні принципи, шаблони та практику дядька Боба в C # . Книга заповнена прикладами, що використовують абстракції та дають зрозумілі мовні пояснення всього.


4

КОЛИ вживається однотонний рекомендований / необхідний? Це хороша практика?

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

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


Інтерфейси - це підмножина абстрактних класів. Інтерфейс - абстрактний клас без визначених методів. (Абстрактний клас взагалі без коду.)
Gort the Robot

1
@StevenBurnap: Можливо, якоюсь іншою мовою.
DeadMG

4
"Інтерфейс" - це лише умова в C ++. Коли я бачив, як це використовувався, це абстрактний клас із лише чистими віртуальними методами та відсутністю властивостей. Очевидно, ви можете написати будь-який старий клас і ляпнути "Я" перед іменем.
Gort the Robot

Ось так я очікував, що люди відповідуть на цю посаду. Одне запитання. У будь-якому випадку, дякую багато хлопців за те, що поділилися своїми знаннями. Це співтовариство стоїть час інвестувати в.
appoll

3

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

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

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

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

Примітка: Інтерфейс та абстрактний клас не дуже відрізняються у світі C ++ з багаторазовим успадкуванням тощо, але мають різні значення у Java та ін.


Дуже добре сказано! +1
jmort253

3

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

Скажімо, у нас є така функція, як наступний код Python:

function foo(objs):
    for obj in objs:
        obj.printToScreen()

class HappyWidget:
    def printToScreen(self):
        print "I am a happy widget"

class SadWidget:
    def printToScreen(self):
        print "I am a sad widget"

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

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

Якби ми говорили про динамічну мову, що набрала качок, як Python, ми б в основному вже закінчилися. Однак система статичного типу C ++ вимагає, щоб ми дали клас об'єктам у своїй функції, і він зможе працювати лише з підкласами цього початкового класу.

void foo( Printable *objs[], int n){ //Please correctme if I messed up on the type signature
    for(int i=0; i<n; i++){
        objs[i]->printToScreen();
    }
}

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

У C ++ абстрактні поняття класу та інтерфейсу трохи розмиті. Якщо ви хочете їх краще визначити, абстрактні класи - це те, про що ви думаєте, тоді як інтерфейси зазвичай означають більш загальне, мовне уявлення про набір видимих ​​методів, які об'єкт розкриває. (Хоча деякі мови, як-от Java, використовують термін інтерфейсу для позначення чогось більш прямого типу, наприклад, абстрактного базового класу)

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


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


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


1

Інтерфейси

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

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

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

Якби ви спочатку були розумними, ви б бачили, що це наближається, і не дзвонили, щоб зберегти безпосередньо в csv. Замість цього ви б подумали, що для цього потрібно зробити, незалежно від того, як це було здійснено. Скажімо, store()і retrieve(). Ви робите Persisterінтерфейс з абстрактними методами store()і retrieve()і створити CsvPersisterпідклас , який фактично реалізує ці методи.

Пізніше ви можете створити файл, DbPersisterякий реалізує фактичне зберігання та отримання даних абсолютно інакше, ніж те, як це робив ваш клас csv.

Чудова річ, все, що вам потрібно зробити зараз, - це зміни

Persister* prst = new CsvPersister();

до

Persister* prst = new DbPersister();

і тоді ви закінчите. Ваші дзвінки prst.store()та prst.retrieve()все ще працюватимуть, вони просто розглядаються по-різному «поза кадром».

Тепер вам все одно довелося створювати резюме та db-реалізацію, тож ви ще не відчували розкоші бути шефом. Реальні переваги очевидні, коли ви використовуєте інтерфейси, які створив хтось інший. Якщо хтось ще був добрим, щоб створити його CsvPersister()і DbPersister()вже, тоді вам просто потрібно вибрати один і подзвонити необхідні методи. Якщо ви вирішили використовувати інший пізніше або в іншому проекті, ви вже знаєте, як він працює.

Я справді іржавий на моєму C ++, тому я буду просто використовувати кілька загальних прикладів програмування. Контейнери - прекрасний приклад того, як інтерфейси полегшують ваше життя.

Ви можете Array, LinkedList, BinaryTreeі т.д. все підкласи з Containerяких має такі методи , як insert(), find(), delete().

Тепер, додаючи щось до середини пов’язаного списку, вам навіть не потрібно знати, що таке зв'язаний список. Ви просто зателефонували, myLinkedList->insert(4)і він магічно повторить список і вставить його туди. Навіть якщо ви знаєте, як працює зв'язаний список (який ви насправді повинні), вам не доведеться шукати його конкретні функції, тому що ви, мабуть, уже знаєте, що вони використовують, якщо використовувати інший Containerраніше.

Анотація заняття

Абстрактні класи досить схожі на інтерфейси (ну технічно інтерфейси - абстрактний клас, але тут я маю на увазі базові класи, які мають деякі свої методи.

Скажіть, що ви створюєте гру, і вам потрібно визначити, коли вороги знаходяться на відстані від удару від гравця. Ви можете створити базовий клас, Enemyякий має метод inRange(). Хоча про ворогів багато речей, які відрізняються, метод, що використовується для перевірки їх дальності, є послідовним. Тому у вашому Enemyкласі буде встановлений чіткий метод перевірки дальності, але чисті віртуальні методи для інших речей, які не мають подібності серед типів ворога.

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

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

Однотонні

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

Ось хороша дискусія про глобальну державу з боку більш досвідчених і обережних людей: Чому Глобальна держава така зла?


1

У тваринництві є різні тварини, які є ссавцями. Тут ссавець є базовим класом і з нього походять різні тварини.

Ви коли-небудь бачили ссавця, що гуляє? Так, багато разів , я впевнений , - проте всі вони були види ссавців , були чи вони?

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

Ссавець класу повинен визначати різні характеристики та групи, але він не існує як фізична сутність.

Тому це абстрактний базовий клас.

Як рухаються ссавці? Вони ходять, плавають, літають тощо?

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

Тому MoveAround () - віртуальна функція, тому що кожен ссавець, який походить з цього класу, повинен мати можливість реалізувати її по-різному.

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

Тому MoveAround - це чиста віртуальна функція.

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

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