Шаблон C# для избежания негерметичной абстракции, когда одна из реализаций требует дополнительного шага
Я реализую ITracker
интерфейс, который выглядит примерно так:
public interface ITracker
{
void Track(ITrackerEvent trackerEvent);
}
Я изначально создал реализацию этого интерфейса, упаковывающего Mixpanel.NET. Затем я создал еще один, который оборачивает Application Insights. Тем не менее, Application Insights один требует Flush()
отправить данные на сервер.
Я не хочу загрязнять ITracker
интерфейс с Flush()
метод только потому, что одна из реализаций нуждается в ней. Это было бы похоже на дырявую абстракцию.
Тем не менее, мне нужно вызвать этот метод в какой-то момент (возможно, при закрытии приложения) и не хочу делать это каждый раз Track
называется.
Можно ли вызвать метод, когда Tracker собирает мусор в конце сеанса? Это даже хороший подход?
Я чувствую, что мне не хватает трюк здесь!
4 ответа
Я бы сделал ITracker
использовать шаблон транзакции одинаково, потому что могут быть случаи, когда вы можете отменить события отслеживания, выполнить откат или изменить их:
public interface ITracker
{
void Track(ITrackerEvent trackerEvent);
void Commit();
}
И затем для реализации:
- Я бы позвонил
Flush
внутриCommit
для применения идеи. - Я бы записал события трекера в коллекцию в памяти (
List<ITrackerEvent>
или жеBlockingCollection<ITrackerEvent>
если параллелизм включен) внутриTrack
метод, а затем использовать вашу текущую логику для вызоваMixpanel.NET
API внутриCommit
реализация метода дляMixpanel.NET
,
Рекомендация: ITracker
следует также реализовать IDisposable
так как трекеры обычно используют ресурсы, которые нужно утилизировать.
Опираясь на Лери, я бы подумал о том, что может понадобиться трекеру.
Я был бы склонен сделать что-то вроде этого:
public interface ITracker {
void BeginTracking();
void Track(ITrackerEvent trackerEvent);
void EndTracking();
}
Тогда у всех трекеров есть чувство, когда они начинают и когда они заканчивают. Это важно, потому что трекер может удерживать ресурсы, которые не должны удерживаться дольше, чем необходимо. Если трекер не нужно использовать ни BeginTracking
или же EndTracking
Реализация тривиальна. Тривиальная реализация не является утечкой абстракции. Утечка абстракции не подходит для всех реализаций.
Теперь предположим, что вы категорически против использования двух дополнительных методов в каждом трекере (почему?). Вместо этого вы могли бы иметь ITrackerEvents, которые являются внешними и охватывают семантическое значение Begin и End. Мне это не нравится Требуется, чтобы у каждого трекера был специальный код для обработки внешних событий.
Вы также можете иметь отдельный интерфейс
public interface IDemarcatedTracker : ITracker {
void BeginTracking();
void EndTracking();
}
который требует, чтобы в вашем коде вызова был специальный код, чтобы проверить, является ли ITracker также IDemarcatedTracker:
public void BeginTracking(ITracker tracker)
{
IDemarcatedTracker demarcatedTracker = tracker as IDemarcatedTracker;
if (demarcatedTracker != null)
demarcatedTracker.BeginTracking();
}
И не слишком сильно, но я также хотел бы знать, что должно произойти, когда трекер выйдет из строя? Просто слепо бросить исключение? И вот тут абстракция на самом деле негерметична. У трекера нет процесса, чтобы сообщить, что он не может отследить.
В вашем случае вы можете захотеть вернуть логическое значение (ограниченная информация), код ошибки (несколько больше информации) или класс / структуру ошибок. Или вы можете захотеть иметь стандартное исключение, которое выдается. Или вы можете захотеть, чтобы метод Begin() включал делегат для вызова при возникновении ошибки в отслеживании.
Я бы просто получил ITracker
от IDisposable
, Классы, которые реализуют этот интерфейс, могут выбрать выполнение некоторых действий в Dispose
метод, например, ваш Flush
или ничего не делать.
public interface ITracker : IDisposable
{
void Track(ITrackerEvent trackerEvent);
}
Кроме того, посмотрите на наблюдаемую модель, ITracker
name предполагает, что вы можете захотеть выполнить какое-либо действие при изменении состояния объекта.
Похоже, вы что-то буферизируете - вещи, которые отслеживаются, нужно время от времени сбрасывать. Ваш интерфейс скрывает это поведение, и это хорошо - вы не сможете узнать по этому интерфейсу, когда он очищается или даже буферизируется.
Если это большой объем, я хотел бы установить два параметра - максимальный интервал очистки и максимальный размер буфера. Первый использует таймер для регулярной очистки. Вторая вызывает сброс при достижении емкости. И затем я снова вспыхиваю, когда объект утилизируется или собирается.
Я разделил буфер на его собственный класс, чтобы я мог повторно использовать его и тестировать его независимо. Я посмотрю, смогу ли я найти его, но написать не так много кода.