Интерфейсы и абстрактные классы, default interface methods
↑ 1.4 ООП в C♯ · ← Предыдущая · → Следующая
Зачем это на собесе
интерфейсы — основа DI, тестируемости и слабой связанности. Вопрос «интерфейс или абстрактный класс» гарантирован. В проекте:
ISagaContextAccessor,ISagaFinalizer, сервисы терминологии с реализациямиgrpcиpg.
Объяснение
Интерфейс — контракт
Описывает, что умеет тип, без того как. Класс или struct может реализовать много интерфейсов.
public interface ITerminologyService
{
Task<Concept?> LookupAsync(string system, string code, CancellationToken ct);
Task<IReadOnlyList<Concept>> ExpandAsync(string valueSetUrl, CancellationToken ct);
}
// две реализации одного контракта (как в проекте: services_terminology_implementation_grpc / _pg)
public sealed class PgTerminologyService(TerminologyDbContext db) : ITerminologyService { ... }
public sealed class GrpcTerminologyService(Terminology.TerminologyClient client) : ITerminologyService { ... }
// потребитель зависит от абстракции — реализацию выбирает DI
public sealed class DiagnosisValidator(ITerminologyService terminology) { ... }Default interface methods (C# 8)
Интерфейс может содержать реализацию по умолчанию — чтобы добавить метод, не ломая существующие реализации.
public interface IReport
{
string Title { get; }
byte[] Render(ReportData data);
string FileName => $"{Title}_{DateTime.UtcNow:yyyyMMdd}.pdf"; // реализация по умолчанию
}Нюанс: default-метод доступен только через ссылку типа интерфейса, не через класс.
Ещё в интерфейсах можно: статические члены, static abstract (generic math, C# 11), приватные методы-хелперы.
Явная реализация интерфейса
public class AuditLog : IDisposable, IAsyncDisposable
{
void IDisposable.Dispose() { ... } // доступно только через IDisposable
public ValueTask DisposeAsync() { ... }
}Для разрешения конфликта одинаковых сигнатур из разных интерфейсов или чтобы скрыть член из публичного API класса.
Интерфейс vs абстрактный класс
- Множественность: интерфейсов — много, базовый класс — один.
- Состояние: интерфейс — без полей экземпляра, абстрактный класс — может хранить состояние и иметь конструктор.
- Назначение: интерфейс — «умеет делать» (роль, контракт между модулями), абстрактный класс — «является» + общая реализация (шаблон алгоритма).
- Эволюция: добавить член в интерфейс без default-реализации ломает все реализации, в абстрактный класс — можно с виртуальной реализацией.
- Struct: может реализовать интерфейс, но не наследовать класс.
Практика: публичные контракты сервисов — интерфейсы. Абстрактный базовый класс — когда много общей реализации (Template Method), и часто вместе:
IReport+ReportBase : IReport.
Маркерные и ролевые интерфейсы
- Маркерный (пустой) интерфейс — лучше заменить атрибутом.
- Interface Segregation: много мелких ролевых интерфейсов лучше одного «толстого» (
IReadRepositoryиIWriteRepository).
Нюансы и подводные камни
- «Интерфейс на каждый класс» (
IPatientServiceс единственной реализацией и без тестов) — церемония. Интерфейс оправдан, когда есть несколько реализаций, граница модуля или нужна подмена в тестах. - Приведение struct к интерфейсу вызывает boxing. В generic-коде используйте ограничения
where T : IFoo— без боксинга. - Default interface methods — не замена базовых классов, у них нет состояния.
- Интерфейс, который протекает деталями реализации (
IUserRepository.GetDbContext()), теряет смысл абстракции.
Тестирование
Интерфейсы позволяют подменять зависимости:
[Fact]
public async Task Validator_rejects_unknown_code()
{
var terminology = Substitute.For<ITerminologyService>();
terminology.LookupAsync("icd10", "XXX", Arg.Any<CancellationToken>()).Returns((Concept?)null);
var sut = new DiagnosisValidator(terminology);
(await sut.IsValidAsync("icd10", "XXX", default)).Should().BeFalse();
}Вопросы с ответами
Когда использовать интерфейс, а когда абстрактный класс?
Интерфейс — для контракта или роли, когда нужны несколько независимых реализаций, множественная реализация и подмена в тестах. Абстрактный класс — когда у наследников много общей реализации и состояния (Template Method). Часто сочетают.
Что такое default interface methods и зачем они?
Реализация метода прямо в интерфейсе (C# 8+). Позволяет развивать интерфейс, не ломая существующие реализации. Доступны через ссылку на интерфейс.
Что такое явная реализация интерфейса?
Реализация члена с указанием имени интерфейса (void IFoo.Bar()). Член доступен только через ссылку на интерфейс. Нужна при конфликте сигнатур или чтобы скрыть член из API класса.
Зачем интерфейсы для DI?
Потребитель зависит от абстракции, а не от конкретного класса. Реализацию можно заменить (другая БД, gRPC-клиент, мок в тестах) без изменения потребителя — принцип инверсии зависимостей.
Связанные темы
- Предыдущая: Наследование, virtual, override, new · Следующая: Полиморфизм и инкапсуляция на практике
- DI и IoC: DI и IoC: зачем и как работает контейнер
- SOLID (ISP, DIP): [[be-8-1-1-solid-v-csharp-s-primerami|SOLID в C# с примерами]]
- Moq и NSubstitute: Тестовые двойники: Moq и NSubstitute
- Generics и static abstract: Generics глубоко: constraints, вариантность, static abstract
- Interface и type в TS: Interface и type: различия