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


13

Я изучаю ООП на C ++ и, хотя мне известны определения этих трех концепций, я не могу понять, когда и как их использовать.

Давайте использовать этот класс для примера:

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. Синглтон

КАК работает ограничение класса иметь только один объект?

Можете ли вы разработать класс, который будет иметь только 2 экземпляра? Или, может быть, 3?

КОГДА использовать синглтон рекомендуется / необходимо? Это хорошая практика?

2. Абстрактный класс

Насколько я знаю, если есть только одна чисто виртуальная функция, класс становится абстрактным. Итак, добавляя

virtual void print ()=0;

сделал бы это, верно?

ПОЧЕМУ вам нужен класс, объект которого не требуется?

3.Interface

Если интерфейс является абстрактным классом, в котором все методы являются чисто виртуальными функциями, то

В чем главное отличие двух из них?

Заранее спасибо!


2
Синглтон является спорным, сделайте поиск его на этом сайте, чтобы получить различные мнения.
Уинстон Эверт

2
Стоит также отметить, что хотя абстрактные классы являются частью языка, ни синглтоны, ни интерфейсы не являются. Это модели, которые люди реализуют. В частности, Singleton - это то, что требует умного взлома, чтобы заставить его работать. (Хотя, конечно, вы можете создать синглтон только по соглашению.)
Gort the Robot

1
По одному, пожалуйста.
JeffO

Ответы:


17

1. Синглтон

Вы ограничиваете количество экземпляров, потому что конструктор будет закрытым, то есть только статические методы могут создавать экземпляры этого класса (на самом деле для этого есть и другие грязные приемы, но давайте не будем увлекаться).

Создание класса, который будет иметь только 2 или 3 экземпляра, вполне выполнимо. Вы должны использовать singleton всякий раз, когда чувствуете необходимость иметь только один экземпляр этого класса во всей системе. Это обычно происходит с классами, которые имеют поведение «менеджера».

Если вы хотите узнать больше о Одиночке вы можете начать в Википедии и особенно для C ++ в этом сообщении .

В этом паттерне, безусловно, есть что-то хорошее и плохое, но это обсуждение относится к другому месту.

2. Абстрактные классы

Да все верно. Только один виртуальный метод помечает класс как абстрактный.

Вы будете использовать такие классы, когда у вас есть большая иерархия классов, в которой не нужно создавать экземпляры верхних классов.

Предположим, вы определяете класс Mammal, а затем наследуете его Dog и Cat. Если вы подумаете об этом, нет особого смысла иметь чистый экземпляр млекопитающего, поскольку сначала вам нужно узнать, что это за млекопитающее на самом деле.

Потенциально есть метод MakeSound (), который будет иметь смысл только в унаследованных классах, но нет общего звука, который могут издавать все млекопитающие (это просто пример, не пытающийся обосновать звуки млекопитающих здесь).

Таким образом, это означает, что Mammal должен быть абстрактным классом, так как он будет иметь некоторое общее поведение, внедренное для всех млекопитающих, но его на самом деле не предполагается создавать. Это основная концепция абстрактных классов, но определенно есть что еще вы должны изучить.

3. Интерфейсы

В C ++ нет чистых интерфейсов в том же смысле, в каком вы используете Java или C #. Единственный способ создать его - иметь чистый абстрактный класс, который имитирует большую часть поведения, которое вы хотите от интерфейса.

В основном, поведение, которое вы ищете, заключается в определении контракта, с которым другие объекты могут взаимодействовать, не заботясь о базовой реализации. Когда вы делаете класс чисто абстрактным, это означает, что вся реализация принадлежит где-то еще, поэтому целью этого класса является только контракт, который он определяет. Это очень мощная концепция в ОО, и вам определенно стоит больше в нее разобраться.

Вы можете прочитать о спецификации интерфейса для 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 - если вы хотите, чтобы весь вызывающий код использовал один экземпляр переменных, по каким-либо причинам у вас есть следующие опции:

  • Глобальные переменные - очевидно, без инкапсуляции, большая часть кода связана с глобальными ... плохо
  • Класс со всеми статическими функциями - немного лучше, чем простые глобальные переменные, но это конструктивное решение все еще ведет вас к пути, где код опирается на глобальные данные и может быть очень трудно изменить позже. Также вы не можете воспользоваться преимуществами ООП, такими как полиморфизм, если у вас есть только статические функции
  • Singleton - Несмотря на то, что существует только один экземпляр класса, фактическая реализация класса не должна ничего знать о том, что он является глобальным. Итак, сегодня у вас может быть класс-одиночка, завтра вы можете просто сделать его конструктор общедоступным и позволить клиентам создавать несколько экземпляров. Большая часть клиентского кода, который ссылается на синглтон, не должна была бы изменяться, и реализация самого синглтона не должна была бы изменяться. Единственное изменение заключается в том, как клиентский код получает одноэлементную ссылку в первую очередь.

Из всех злых и плохих вариантов, если вам нужны глобальные данные, синглтон - НАМНОГО лучший подход, чем любой из двух предыдущих. Это также позволяет вам держать свои варианты открытыми, если завтра вы передумаете и решите использовать инверсию управления вместо глобальных данных.

Так где бы вы использовали синглтон? Вот несколько примеров:

  • Регистрация - если вы хотите, чтобы у всего процесса был один журнал, вы можете создать объект журнала и передавать его везде. Но что, если у вас есть 100 000 тысяч строк устаревшего кода приложения? изменить их все? Или вы можете просто ввести следующее и начать использовать его где угодно:

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

  • Для отладки мы часто находим глобальную таблицу запущенных объектов очень полезной. У нас есть несколько классов, которые мы хотели бы отслеживать. Все они происходят из одного базового класса. Во время создания экземпляра они вызывают одноэлементную таблицу объектов и регистрируют себя. Когда они уничтожены, они не регистрируются. Я могу подойти к любой машине, присоединиться к процессу и создать список запущенных объектов. Я был в продукте более полувека, и я никогда не чувствовал, что нам когда-нибудь понадобились 2 «глобальные» таблицы объектов.

  • У нас есть несколько относительно сложных служебных классов анализатора строк, которые полагаются на регулярные выражения. Классы регулярных выражений должны быть инициализированы перед выполнением сопоставлений. Инициализация несколько дорогая, потому что именно тогда FSM генерируется на основе строки разбора. Однако после этого к классу регулярных выражений могут безопасно обращаться 100 потоков, потому что однажды созданный FSM никогда не меняется. Эти классы синтаксического анализатора внутренне используют синглтоны, чтобы эта инициализация происходила только один раз. Это значительно улучшило производительность и никогда не вызывало проблем из-за «злых синглетонов».

Сказав все это, вы должны помнить, когда и где использовать синглтоны. 9 из 10 раз есть лучшее решение, и вы должны использовать его вместо этого. Однако бывают случаи, когда синглтон является абсолютно правильным выбором дизайна.

Следующая тема ... интерфейсы и абстрактные классы. Во-первых, как уже упоминали другие, интерфейс - это абстрактный класс, но он выходит за рамки этого, обеспечивая, что он не имеет абсолютно никакой реализации. В некоторых языках ключевое слово интерфейса является частью языка. В C ++ мы просто используем абстрактные классы. Microsoft VC ++ сделала один шаг, чтобы определить это где-то внутри:

typedef struct interface;

... так что вы все равно можете использовать ключевое слово интерфейса (оно даже будет выделено как «реальное» ключевое слово), но для реального компилятора это просто структура.

Так где бы вы использовали это? Давайте вернемся к моему примеру с таблицей запущенных объектов. Допустим, базовый класс имеет ...

виртуальный void print () = 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 (). Остальная часть кода будет работать автоматически, без необходимости поднимать палец.

Для получения дополнительной информации об интерфейсах и хорошем дизайне ОО, я настоятельно рекомендую Agile Принципы, Шаблоны и Практики Дяди Боба в C # . Книга заполнена примерами, в которых используются абстракции, и предоставляет понятные объяснения всего.


4

КОГДА использовать синглтон рекомендуется / необходимо? Это хорошая практика?

Никогда. Хуже того, они абсолютная сука, от которой нужно избавиться, поэтому однажды совершив эту ошибку, вы будете преследовать вас много-много лет.

Разница между абстрактными классами и интерфейсами абсолютно отсутствует в C ++. Обычно у вас есть интерфейсы для указания некоторого поведения производного класса, но без необходимости указывать все это. Это делает ваш код более гибким, потому что вы можете заменить любой класс, который соответствует более ограниченной спецификации. Интерфейсы времени выполнения используются, когда вам нужна абстракция времени выполнения.


Интерфейсы являются подмножеством абстрактных классов. Интерфейс - это абстрактный класс без определенных методов. (Абстрактный класс без кода вообще.)
Gort the Robot

1
@ StevenBurnap: Может быть, на каком-то другом языке.
DeadMG

4
«Интерфейс» - это просто соглашение в C ++. Когда я увидел, что он используется, это абстрактный класс с чисто виртуальными методами и без свойств. Очевидно, что вы можете написать любой старый класс и поставить «Я» перед именем.
Gort the Robot

Я ожидал, что люди ответят на этот пост. Один вопрос за один раз. В любом случае, большое спасибо, ребята, за то, что поделились своими знаниями. В этом сообществе стоит
потратить

3

Синглтон полезен, когда вам не нужны несколько копий определенного объекта, должен быть только один экземпляр этого класса - он используется для объектов, которые поддерживают глобальное состояние, должны каким-то образом иметь дело с не-входящим кодом и т. Д.

Синглтон с фиксированным числом 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 ++ понятия класса и интерфейса absctract немного размыты. Если вы хотите определить их лучше, абстрактные классы - это то, о чем вы думаете, в то время как интерфейсы обычно означают более общую междисциплинарную идею набора видимых методов, предоставляемых объектом. (Хотя некоторые языки, такие как 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()все еще будут работать, они просто обрабатываются по-разному "за кадром".

Теперь вам все еще нужно было создать реализации cvs и 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.