Adding functionality to any TextReader

I have a Location class that represents a place somewhere in the stream. (The class is not associated with any particular stream.) The location information will be used to match tokens with the input location in my parser to provide the user with more pleasant reporting of errors.

I want to add location tracking to an instance of TextReader . Thus, when reading tokens, I can capture the location (which is updated using TextReader as the data is read) and transfer it to the token during the tokenization process.

I am looking for a good approach to achieve this. I came up with several projects.

Manual location tracking

Every time I need to read from TextReader , I call the AdvanceString in the Location AdvanceString object with the read data.

Benefits

  • Very simple.
  • No bloating class.
  • No need to rewrite TextReader methods.

disadvantages

  • The logic of tracking the location of pairs in the tokenization process.
  • It's easy to forget to keep track of something (although unit testing helps with this).
  • Blurs existing code.

Plain TextReader

Create the LocatedTextReaderWrapper class that surrounds each method call, tracking the Location property. Example:

 public class LocatedTextReaderWrapper : TextReader { private TextReader source; public Location Location { get; set; } public LocatedTextReaderWrapper(TextReader source) : this(source, new Location()) { } public LocatedTextReaderWrapper(TextReader source, Location location) { this.Location = location; this.source = source; } public override int Read(char[] buffer, int index, int count) { int ret = this.source.Read(buffer, index, count); if(ret >= 0) { this.location.AdvanceString(string.Concat(buffer.Skip(index).Take(count))); } return ret; } // etc. } 

Benefits

  • Tokenization is not aware of location tracking.

disadvantages

  • The user needs to create an instance of the LocatedTextReaderWrapper and add it in addition to the TextReader instance.
  • It does not allow you to add different types of tracking or different location trackers without layers of wrappers.

TextReader -Based TextReader Wrap

Like LocatedTextReaderWrapper , but separates it from the Location object, raising an event whenever data is read.

Benefits

  • Can be reused for other types of tracking.
  • Tokenization is not aware of location tracking or other tracking.
  • May have several independent independent Location objects (or other tracking methods) at once.

disadvantages

  • Template code is required to enable location tracking.
  • The user must create an instance of the wrapper and delete it in addition to the instance of TextReader .

Aspect Oriented Approach

Use AOP to execute as an event-based wrapper method.

Benefits

  • Can be reused for other types of tracking.
  • Tokenization is not aware of location tracking or other tracking.
  • No need to rewrite TextReader methods.

disadvantages

  • External dependencies are required, which I want to avoid.

I am looking for the best approach in my situation. I would like to:

  • Do not inflate location tracking tokenizer methods.
  • Strong initialization in user code is not required.
  • You don't have / big code / duplicated code.
  • (Possibly) do not connect the TextReader to the Location class.

Any understanding of this problem and possible solutions or adjustments are welcome. Thanks!

(For those who want to ask a specific question: what is the best way to port the functionality of TextReader ?)

I have applied the β€œ TextReader wrapper” and β€œ TextReader event-based TextReader ” and TextReader unhappy with both, for the reasons mentioned in their flaws.

+4
source share
3 answers

I agree that creating a wrapper that implements TextReader delegates the implementation to the underlying TextReader and adds some additional support for location tracking is a good approach. You can think of it as a Decorator pattern because you decorate the TextReader class TextReader some extra features.

Improved packaging:. You can write it in an easier way to separate the TextReader from the Location type - this way you can easily change the Location yourself or even provide other functions that are based on tracking reading progress.

 interface ITracker { void AdvanceString(string s); } class TrackingReader : TextReader { private TextReader source; private ITracker tracker; public TrackingReader(TextReader source, ITracker tracker) { this.source = source; this.tracker = tracker; } public override int Read(char[] buffer, int index, int count) { int res = base.Read(buffer, index, count); tracker.AdvanceString(buffer.Skip(index).Take(res); return res; } } 

I would also encapsulate the creation of the tracker in some kind of factory method (so that you have one place in the application that deals with construction). Note that you can use this simple TextReader creation, which also reports progress to several Location objects:

 static TextReader CreateReader(TextReader source, params ITracker[] trackers) { return trackers.Aggregate(source, (reader, tracker) => new TrackingReader(reader, tracker)); } 

This creates a chain of TrackingReader objects, and each of the objects reports the reading progress to one of the trackers passed as arguments.

As for the events . I think that using standard .NET / C # events for this kind of thing is not performed so often in .NET libraries, but I think this approach can be quite interesting as well - especially in C # 3, where you can use such functions like lambda expressions or even the Reactive Framework.

Simple use does not add as much boiler plate:

 using(ReaderWithEvents reader = new ReaderWithEvents(source)) { reader.Advance += str => loc.AdvanceString(str); // .. } 

However, you can also use Reactive Extensions to handle events. To count the number of new lines in the source, you can write something like this:

 var res = (from e in Observable.FromEvent(reader, "Advance") select e.Count(ch => ch == '\n')).Scan(0, (sum, current) => sum + current); 

This will give you a res event that fires every time you read a line from TextReader , and the value carried by the event will be the current number of lines (in the text).

+5
source

I would like to use the usual TextReader shell because this seems like the most natural approach. Also, I don’t think that the flaws you talked about are really flaws:

  • The user needs to create an instance of BasedTextReaderWrapper and place it in addition to the TextReader instance.

Well, if the user needs to track, he will still have to manipulate objects related to tracking, and creating a wrapper is not such a big problem ... To simplify the task, you can create an extension method that creates a tracking wrapper for any TextReader .

  • It does not allow you to add different types of tracking or different location trackers without layers of wrappers.

You can always use a collection of Location objects, rather than one:

 public class LocatedTextReaderWrapper : TextReader { private TextReader source; public IList<Location> Locations { get; private set; } public LocatedTextReaderWrapper(TextReader source, params Location[] locations) { this.Locations = new List<Location>(locations); this.source = source; } public override int Read(char[] buffer, int index, int count) { int ret = this.source.Read(buffer, index, count); if(ret >= 0) { var s = string.Concat(buffer.Skip(index).Take(count)); foreach(Location loc in this.Locations) { loc.AdvanceString(s); } } return ret; } // etc. } 
+1
source

AFAIK, if you use PostSharp, it is injected into the compilation process and rewrites the code as needed, so you don’t have any perpetual dependencies to send (other than API user requirements so that PostSharp can compile your source).

0
source

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


All Articles