C # to avoid leaky abstraction when one of the implementations requires an extra step

I am implementing an ITracker interface that looks something like this:

 public interface ITracker { void Track(ITrackerEvent trackerEvent); } 

I originally created an implementation of this interface by wrapping Mixpanel.NET. Then I created another instance of the Insights application. However, Application Insights requires Flush() to send data to the server.

I don’t want to pollute the ITracker interface with the Flush() method just because it needs one of the implementations. He will feel like skipping an abstraction.

However, I need to call this method at some point (possibly when the application is finished) and I do not want to do this every time I call Track .

Is it possible to call a method when the Tracker is garbage collected at the end of the session? Is this even a good approach?

It seems to me that I missed the trick here!

+5
source share
4 answers

I would use ITracker to use a similar transaction template, because there may be times when you can refuse tracker events, roll back or change them:

 public interface ITracker { void Track(ITrackerEvent trackerEvent); void Commit(); } 

And then for each implementation:

  • I would call Flush inside Commit for applications.
  • I would write tracking events in a collection in memory ( List<ITrackerEvent> or BlockingCollection<ITrackerEvent> if concurrency) inside the Track method, and then use your current logic to call the Mixpanel.NET api method inside Commit for Mixpanel.NET .

Recommendation: ITracker should also implement IDisposable , as trackers typically use resources that need to be removed.

+5
source

Based on Leri, I would think more about what the tracker can do.

I would be inclined to do something like this:

 public interface ITracker { void BeginTracking(); void Track(ITrackerEvent trackerEvent); void EndTracking(); } 

Then all trackers have a sense of when they start and when they finish. This is important because the tracker can hold resources that should not be held longer than necessary. If the tracker does not need to use either BeginTracking or EndTracking , the implementation is trivial. The trivial implementation is not an leaky abstraction. A flowing abstraction is one that does not work for all implementations.

Now suppose you are flat against two additional methods in each tracker (why?). Instead, you could have ITrackerEvents that are out of range and encompass the semantic meaning of Begin and End. I do not like it. This requires that each tracker has a special code to handle out-of-band events.

You may also have a separate interface.

 public interface IDemarcatedTracker : ITracker { void BeginTracking(); void EndTracking(); } 

which requires that you have a special code code in the call code to check if ITracker is also an IDemarcatedTracker:

 public void BeginTracking(ITracker tracker) { IDemarcatedTracker demarcatedTracker = tracker as IDemarcatedTracker; if (demarcatedTracker != null) demarcatedTracker.BeginTracking(); } 

And so as not to overdo it too much, but I also wonder what should happen when the tracker fails? Just blindly throw an exception? And here the abstraction actually proceeds. The tracker does not know that he cannot track.

In your case, you may want to return a logical (limited information), error code (a bit more information) or an error class / struct. Or you might want to get a standard exception. Or you might want the Begin () method to include a delegate to call when a tracking error occurs.

+5
source

I would just get an ITracker from IDisposable . Classes that implement this interface can choose to perform some action in the Dispose method, for example. your Flush or do nothing.

 public interface ITracker : IDisposable { void Track(ITrackerEvent trackerEvent); } 

Also, see Observable Pattern , the name ITracker suggests that you can take some action when the state of the object changes.

+3
source

It seems that you are buffering something - things that need to be tracked need to redden from time to time. Your interface hides this behavior, and it’s good - you should not point from this interface when it becomes reddened or even if it receives buffering.

If this is a large amount, I like to set two parameters - the maximum flash interval and the maximum buffer size. The first uses a timer for regular cleaning. The second is triggered when bandwidth is reached. And then I wash off again when the object is located or assembled.

I allocated a buffer to my class so that I can reuse it, and unit test independently. I will see if I can find him, but there is nothing to write.

+1
source

Source: https://habr.com/ru/post/1244735/


All Articles