Интерфейсы как тип абстрактного класса c
Перейти к содержимому

Интерфейсы как тип абстрактного класса c

  • автор:

Интерфейсы как тип абстрактного класса c

Один из принципов проектирования гласит, что при создании системы классов надо программировать на уровне интерфейсов, а не их конкретных реализаций. Под интерфейсами в данном случае понимаются не только типы C#, определенные с помощью ключевого слова interface , а определение функционала без его конкретной реализации. То есть под данное определение попадают как собственно интерфейсы, так и абстрактные классы, которые могут иметь абстрактные методы без конкретной реализации.

В этом плане у абстрактных классов и интерфейсов много общего. Нередко при проектировании программ в паттернах мы можем заменять абстрактные классы на интерфейсы и наоборот. Однако все же они имеют некоторые отличия.

Когда следует использовать абстрактные классы:

  • Если надо определить общий функционал для родственных объектов
  • Если мы проектируем довольно большую функциональную единицу, которая содержит много базового функционала
  • Если нужно, чтобы все производные классы на всех уровнях наследования имели некоторую общую реализацию. При использовании абстрактных классов, если мы захотим изменить базовый функционал во всех наследниках, то достаточно поменять его в абстрактном базовом классе. Если же нам вдруг надо будет поменять название или параметры метода интерфейса, то придется вносить изменения и также во всех классы, которые данный интерфейс реализуют.

Когда следует использовать интерфейсы:

  • Если нам надо определить функционал для группы разрозненных объектов, которые могут быть никак не связаны между собой.
  • Если мы проектируем небольшой функциональный тип

Ключевыми здесь являются первые пункты, которые можно свести к следующему принципу: если классы относятся к единой системе классификации, то выбирается абстрактный класс. Иначе выбирается интерфейс. Посмотрим на примере.

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

public abstract class Vehicle < public abstract void Move(); >public class Car : Vehicle < public override void Move() < Console.WriteLine("Машина едет"); >> public class Bus : Vehicle < public override void Move() < Console.WriteLine("Автобус едет"); >> public class Tram : Vehicle < public override void Move() < Console.WriteLine("Трамвай едет"); >>

Абстрактный класс Vehicle определяет абстрактный метод перемещения Move() , а классы-наследники его реализуют.

Но, предположим, что наша система транспорта не ограничивается вышеперечисленными транспортными средствами. Например, мы можем добавить самолеты, лодки. Возможно, также мы добавим лошадь — животное, которое может также выполнять роль транспортного средства. Также можно добавить дирижабль. Вобщем получается довольно широкий круг объектов, которые связаны только тем, что являются транспортным средством и должны реализовать некоторый метод Move() , выполняющий перемещение.

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

Возможная реализация интерфейса могла бы выглядеть следующим образом:

public interface IMovable < void Move(); >public abstract class Vehicle : IMovable < public abstract void Move(); >public class Car : Vehicle < public override void Move() =>Console.WriteLine("Машина едет"); > public class Bus : Vehicle < public override void Move() =>Console.WriteLine("Автобус едет"); > public class Hourse : IMovable < public void Move() =>Console.WriteLine("Лошадь скачет"); > public class Aircraft : IMovable < public void Move() =>Console.WriteLine("Самолет летит"); >

Теперь метод Move() определяется в интерфейсе IMovable, а конкретные классы его реализуют.

Говоря об использовании абстрактных классов и интерфейсов можно привести еще такую аналогию, как состояние и действие. Как правило, абстрактные классы фокусируются на общем состоянии классов-наследников. В то время как интерфейсы строятся вокруг какого-либо общего действия.

Например, солнце, костер, батарея отопления и электрический нагреватель выполняют функцию нагревания или излучения тепла. По большому счету выделение тепла — это единственный общий между ними признак. Можно ли для них создать общий абстрактный класс? Можно, но это не будет оптимальным решением, тем более у нас могут быть какие-то родственные сущности, которые мы, возможно, тоже захотим использовать. Поэтому для каждой вышеперечисленной сущности мы можем определить свою систему классификации. Например, в одной системе классов, которые наследуются от общего астрактного класса, были бы звезды, в том числе и солнце, планеты, астероиды и так далее — то есть все те объекты, которые могут иметь какое-то общее с солнцем состояние. В рамках другой системы классов мы могли бы определить электрические приборы, в том числе электронагреатель. И так, для каждой разноплановой сущности можно было бы составить свою систему классов, исходяющую от определенного абстрактного класса. А для общего действия определить интерфейс, например, IHeatable, в котором бы был метод Heat, и этот интерфейс реализовать во всех необходимых классах.

Таким образом, если разноплановые классы обладают каким-то общим действием, то это действие лучше выносить в интерфейс. А для одноплановых классов, которые имеют общее состояние, лучше определять абстрактный класс.

Абстрактный класс или интерфейс?

Чем они отличаются между собой и в чём отличия?
В плане синтаксиса, интерфейс может содержать внутри себя только методы без реализации, свойства, события. В то время как абстрактный класс поддерживает функционал любого другого класса(поля, реализованные методы, делегаты, события, свойства, конструкторы. ), но запрещает создавать экземпляры своего типа. Также нужно помнить, что C# не поддерживает множественное наследование и, соответственно, унаследоваться от нескольких классов не получиться, а вот от нескольких интерфейсов — да.

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

Вот пару советов по определению нужного механизма:

  • Связь потомка с предком. Любой тип может наследовать только одну реализацию. Если производный тип не может ограничиваться отношением типа «является частным случаем» с базовым типом, нужно применять интерфейс, а не базовый тип. Интерфейс подразумевает отношение «поддерживает функциональность». Например, тип может преобразовывать экземпляры самого себя в другой тип (IConvertible), может создать набор экземпляров самого себя (ISerializable) и т. д. Заметьте, что значимые типы должны наследовать от типа System.ValueType и поэтому не могут наследовать от произвольного базового класса. В этом случае нужно определять интерфейс.
  • Простота использования. Разработчику проще определить новый тип, производный от базового, чем создать интерфейс. Базовый тип может предоставлять массу функций, и в производном типе потребуется внести лишь незначительные изменения, чтобы изменить его поведение. При создании интерфейса в новом типе придется реализовывать все члены.
  • Четкая реализация. Как бы хорошо ни был документирован контракт, вряд ли будет реализован абсолютно корректно. По сути, проблемы COM связаны именно с этим — вот почему некоторые COM-объекты нормально работают только с Microsoft Word или Microsoft Internet Explorer. Базовый тип с хорошей реализацией основных функций — прекрасная отправная точка, вам останется изменить лишь отдельные части.
  • Управление версиями. Когда вы добавляете метод к базовому типу, производный тип наследует стандартную реализацию этого метода без всяких затрат. Пользовательский исходный код даже не нужно перекомпилировать. Добавление нового члена к интерфейсу требует изменения пользовательского исходного кода и его перекомпиляции.

Наконец, нужно сказать, что на самом деле можно определить интерфейс и создать базовый класс, который реализует интерфейс. Например, в FCL определен интерфейс IComparer, и любой тип может реализовать этот интерфейс. Кроме того, FCL предоставляет абстрактный базовый класс Comparer, который реализует этот интерфейс (абстрактно) и предлагает реализацию по умолчанию для необобщенного метода Compare интерфейса IComparer. Применение обеих возможностей дает большую гибкость, поскольку разработчики теперь могут выбрать из двух вариантов наиболее предпочтительный.

results matching » «

No results matching » «

Отличия абстрактного класса от интерфейса?

В чем отличие абстрактного класса от интерфейса в Java? И в каких ситуациях лучше применять абстрактный класс, а в каких — интерфейс?

  • Вопрос задан более трёх лет назад
  • 17128 просмотров

Комментировать
Решения вопроса 1
Full-stack developer (Symfony, Angular)

В чем отличие абстрактного класса от интерфейса в Java?

Все упирается в понятие «тип». В былые времена, то есть во времена языка Simula, из которого черпали вдохновение создатели C++, были только классы. И на классах базировалась система типов. Причем механизм наследования был реализован так, как реализован, исключительно для экономии памяти, которая в те времена была очень дорогой.

Для того чтобы достичь полиморфизма, мы должны иметь возможность объявлять абстрактные типы. Мол «любая хрень которая имеет такой тип будет работать как надо». Потому в языках типа C++ появились абстрактные классы. Поскольку иногда нам хочется делать композицию абстрактных типов, в C++ реализовали множественное наследование.

В Java, которая во многом черпала вдохновения из C++ и smalltalk, решили ввести еще одну сущность — интерфейсы. Это был своего рода упрощенный способ задать абстрактный базовый тип. По итогу чтобы не решать проблему бриллианта (или ромба) от множественного наследования было решено отказаться и дать возможность классам имплементить несколько интерфейсов.

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

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

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

Абстрактные классы и интерфейсы. Часть 2

Оптимизация временных и финансовых затрат является существенной частью любого уважающего себя бизнеса. Один из подходов для решения данной задачи состоит в максимальной унификации ресурсов, как людских, так и материальных. На мой взгляд достаточно очевидным является тот факт, что универсальность сотрудников позволяет максимально эффективно использовать их в рамках производственного процесса, “сглаживая” пиковые нагрузки на выполнение той или иной операции. Яркий пример такого подхода — это горячо любимый детьми всей планеты “МакДональдс”. В зависимости от ситуации каждый сотрудник может и на кассу стать, и гамбургер собрать, и столики протереть, и шарики раздать. При этом сотрудник никогда не бывает без дела, и при необходимости помогает своим коллегам если у них “запар”, что обеспечивает максимальную пропускную способность заведения без привлечения дополнительных ресурсов. Конечно, с точки зрения высоких материй можно (и нужно) поставить под сомнение эффективность использования повара с тремя мишленовскими звездами в качестве полотера, но это вполне допустимо в момент когда гамбургеры никому не нужны, но сам ресторан нуждается в серьезной быстрой уборке после отвязного детского мальчишника. Обсуждение границ разумного применения унификации оставим пока за кадром, как и вопрос целесообразности модного нынче в IT подхода “try catch(EpicFailedException) <>”.

В любом случае, для унификации сотрудников важно не то, что ты из себя представляешь как объект, а то, что ты умеешь делать в рамках некоторого “контракта”.

Продолжим тему быстрого и полезного питания, совместив ее с вопросами эффективной доставки его клиентам. Гамбургер сделан и протестирован, и перед менеджером стоит задача обеспечить его доставку по адресу заказчика в установленные временные лимиты и с минимальными финансовыми затратами. К его услугам разношерстный штат курьеров с не менее разнообразными средствами доставки — автомобилем, мопедом, велосипедом, быстрыми ногами, и, наконец, дронами. У каждого из них есть свои достоинства и недостатки в условиях загруженного города. Главное, что ни менеджера, ни клиента не интересует кто (или что) доставит продукт по заданному адресу, важны только сам факт успешной доставки в срок, и соответственно, положительные отзывы в инстаграме о четкой работе заведения. Очевидно, что при этом весь богатый внутренний мир курьера и детали осуществления доставки (количество проездов на красный свет, порванные кроссовки и т.д.) никого не волнуют и не должны волновать — это вредит бизнесу. Детали ничто, контракт — все! А контракт предельно простой: курьер получает заказ на доставку, и, если соглашается с указанным временем, то должен доставить, и все. Это очень упрощает прозрачность и устойчивость работы. У менеджера есть некий безликий пул доставщиков без какой либо детализации, и ему нужно просто нажать кнопку “начать доставку”.

Попробуем теперь создать архитектуру приложения, поддерживающую подобный “контракт”. Предположим, что доставка может быть осуществлена автомобилем, мопедом, пешим курьером либо дроном. Как уже обсуждалось в первой части статьи, использование полиморфизма посредством механизма виртуальных функций и абстрактных классов позволяет возложить ответственность за реализацию метода на конкретный тип. В нашем случае это будут типы Car, Motorbike, Pedestrian, Dron, реализующие общий метод Deliver(/*some params*/). Теперь необходимо их связать неким общим типом для “унификации”, через ссылку на объект которого будет осуществляться связь между менеджером и непосредственно объектом-доставщиком. Пока все красиво, но не безоблачно.

Первая проблема возникает казалась бы из воздуха: а как назвать базовый класс для этих четырех сущностей? Попробуем использовать в качестве базового класса Vehicle, уже описанный ранее. Однако есть неувязка. Сама идея наследования подразумевает, что потомок связан с родительским (базовым) классом отношением “IS” (сокращенно от “ISsoft”, шутка). Car is Vehicle – это правда, Motorbike is Vehicle— тоже правда, а с правдивостью утверждения Pedestrian is Vehicle можно сильно поспорить. Надо четко понимать, что проблема в первую очередь не семантическая, а идеологическая. Между сущностями Car, Motorbike, Pedestrian и Dron просто нет ничего общего ни физиологически, ни физически, а значит наследниками общего класса они быть не могут. Разумеется, скептики могут продолжать упорствовать и искать связь между данными сущностями, аппеллируя к их единому атомному составу и таблице Менделеева, но оставим это на их совести.

Технически связать представленные сущности базовым классом с некоторым вымученным названием DeliveryItem конечно можно, однако это в корне неверно и порождает больше проблем, чем позволяет решить. В первую очередь нужно определиться с функционалом, который обязан обеспечивать класс-наследник. Предположим, это два метода: запрос у объекта GetExpectedDeliveryTime(), и непосредственно команда на выполнение заказа Deliver(). Однако это ограничивает гибкость использования объектов типа Car и Motorbike — их нельзя использовать унифицированным образом, предположим, для использования в качестве такси нет соответствующего метода. Добавление такого метода в DeliveryItem приведет к тому, что и Pedestrian должен выполнять общий контракт, и отработать доставщиком пассажиров, что как минимум нереально. Добавление метода PassengerPickUp() отдельно для Car и Motorbike не позволит их использовать единообразным способом через ссылку на базовый класс, поскольку DeliveryItem ничего не знает о возможности перевозки пассажиров.

Наилучший выход из этой ситуации заключается в объединении объектов по функционалу, а не по схожести внутреннего устройства. С этой целью разработчиками языка C# была выделена отдельная сущность — интерфейс. Реализация в классе некоторого интерфейса накладывает на объект данного класса обязанность по выполнению определенной в нем функциональности (“работы”).

Определим некоторый интерфейс IDeliver и два его метода:

public interface IDeliver

Пусть два класса Car и Dron реализуют этот интерфейс:

public class Car : IDeliver < /* * Some specific fields */ public DateTime GetExpectedTime(string address) < return /* expected delivery time */; >public bool Deliver(string address) < return /* delivery success/not success result */; >> public class Dron : IDeliver < /* * Some specific fields */ public DateTime GetExpectedTime(string address) < return /* expected delivery time */; >public bool Deliver(string address) < return /* delivery success/not success result */; >>

В этом случае работа менеджера по доставке становится тривиальной и сводится к переадресации заказа первому в списке доставщику, способному выполнить заказ в обозначенное время:

public class DeliveryManager < List_deliveryItems = new List ; /* * Some specific fields */ public bool Deliver(string address, DateTime expectedTime) < foreach (var item in _deliveryItems) < if (item.GetExpectedTime(address) > return false; > >

Логику выбора доставщика можно усложнить, введя дополнительно оценку издержек доставки, и осуществлять выбор исходя не только из времени, но и из стоимости доставки. Для этого достаточно будет расширить интерфейс IDelivery соответствующим методом, например, GetCost(string address).

Надо четко понимать, что концепция реализации интерфейсов не заменяет, а дополняет концепцию наследования классов. Можно выделить два основных типичных случая использования интерфейсов:

  1. Объединение (унификация) разнородных сущностей в единую категорию с функциональной точки зрения.
  2. Расширение функциональности однородных объектов.

В рассмотренном выше примере описан первый случай – создание категории объектов, способных осуществить доставку. Хорошей иллюстрацией второго случая (расширение функциональности) является организация бесперебойной и эффективной работы ресторана “МакДональдс”, основная идея которой изложена в начале второй части статьи. Если кратко, то все сотрудники ресторана по мере продвижения по карьерной и зарплатной лестнице осваивают новые специальности и совершенствуют навыки по ранее полученным. Manager, Administrator, Cooker, Cleaner, Animator — это наследники базового абстрактного класса Employee (содержащего персональные данные сотрудника), а их должностные обязанности согласно контракта описываются соответствующими интерфейсами:

public class Cleaner : Employee, ICleaner … public class Cooker : Employee, ICooker, ICleaner … public class Manager : Employee, IManager, IIssueResolver, IAnimator, ICooker …

При таком уровне унификации, имея пул сотрудников, можно в любой момент сделать выборку сотрудников с требуемыми навыками (например, уборки):

public bool EmergencyTableClean() < foreach (var employee in AvailableEmployeePool) < if (employee is ICleaner) < return (employee as ICleaner).CleanTable(); >> return false; >

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

Кратко резюмируя все вышесказанное можно сформулировать следующее формальное правило: для структурно схожих сущностей (имеющих набор общих полей) необходимо использовать наследование, для сущностей, схожих по функциональности – реализацию общих интерфейсов. И да пребудет с вами великая сила ООП!

Автор материала – Игорь Хейдоров, преподаватель Тренинг-центра ISsoft.

Образование: с отличием закончил факультет радиофизики и компьютерных технологий Белорусского государственного университета, кандидат физико-математических наук, доцент.

Опыт работы: C++/C# разработчик с 1997 года.

Обратите внимание! В ISsoft открыты вакансии для C# программистов. Мы ждем ваших откликов!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *