4

I have a class that inherits from TFileStream and a class that inherits from TMemoryStream. Both implement exactly the same functions to do with reading data eg:

TCustomFileStream = class (TFileStream)
  function ReadByte: byte;
  function ReadWord: word;
  function ReadWordBE: word;
  function ReadDWord: longword;
  function ReadDWordBE: longword;
  function ReadString(Length: integer): string;
  function ReadBlockName: string;
  etc

When I want to write a function that could take either type of stream as a parameter I have to use TStream:

function DoStuff(SourceStream: TStream);

This of course means that I cant use my custom functions. Whats the best way of dealing with this? Ideally I'd like to be able to have a Tstream compatible class that works on either a FileStream or MemoryStream so I can do something like this and it wont matter if the stream is a FileStream or MemoryStream:

function DoStuff(SourceStream: TMyCustomStream);
begin
    data := SourceStream.ReadDWord;
    otherData := SourceStream.Read(Buffer, 20);

end;
Bennyboy
  • 75
  • 3

5 Answers5

7

To answer the question in the actual question title: You can't. :)

But if we take a step back and look at the problem that you were trying solve:

I have a class that inherits from TFileStream and a class that inherits from TMemoryStream. Both implement exactly the same functions to do with reading data

I think you have mis-stated your problem and re-stating it correctly points to the answer you need. :)

I have some structured data that I need to read from different sources (different stream classes).

A stream is just a bunch of bytes. Any structure in those bytes is determined by how you read/write the stream. i.e. in this case, that "how" is embodied in your functions. The fact that the concrete stream classes involved are a TFileStream and TMemoryStream is not fundamentally a part of the problem. Solve the problem for TStream and you solve it for all TStream derived classes, including the ones you are dealing with right now.

Stream classes should be specialised based on how they need to read/write bytes to and from specific locations (memory, file, strings etc) not any particular structure in those bytes.

Instead of creating specialised stream classes which have to duplicate the knowledge of the structure, what you really need is a class that encapsulates the knowledge of the structure of the data involved and is able to apply it to any stream.

A Reader Class

One approach to this (the best ?) is to implement a class that encapsulates the behaviour you require. For example in a "reader" class.

TStuffReader = class
private
  fStream: TStream;
public
  constructor Create(aStream: TStream);
  function ReadByte: byte;
  function ReadWord: word;
  function ReadWordBE: word;
  function ReadDWord: longword;
  function ReadDWordBE: longword;
  function ReadString(Length: integer): string;
  function ReadBlockName: string;
end;

This class might also provide the functions for writing to a stream as well (in which case you might call it a TStuffFiler, for example, since it would not be just a reader) or you might have a separate class for writing called TStuffWriter (for example).

However you choose to implement it, this reader class will be able to read (and/or write) that structured data from/to any TStream derived class. It should not matter to these functions what specific class of stream is involved.

If your problem includes a need to pass references to a stream around to various functions etc then instead you pass around a reference to the reader class. The reader necessarily carries with it a reference to the stream involved, but the code that previously you thought needed to call the functions on the specialised stream classes instead simply uses the reader function. If that code also needs access to the stream itself then the reader can expose it if necessary.

Then in the future if you ever found yourself needing to read such data from other stream classes (for example a TBLOBStream if you find yourself retrieving data from a database) your TStuffReader (or whatever you choose to call it) can step right in and do the job for you without any further work on your part.

The Class Helper Non-Alternative

Class helpers might appear to provide a mechanism to approximate "multiple inheritance" but should be avoided. They were never intended for use in application code.

The presence of class helpers in the VCL is exactly as you would expect since they are intended for use in frameworks and libraries and the VCL is a framework library so it's use there is entirely consistent with that usage.

But this is not an endorsement that they are suitable for use in application code and the documentation continues to enforce this point.

Class and record helpers provide a way to extend a type, but they should not be viewed as a design tool to be used when developing new code. For new code you should always rely on normal class inheritance and interface implementations.

The documentation is also quite clear on the limitations that apply to class helpers but do not clearly explain why these can lead to problems, which is perhaps why some people still insist that they are suitable for use.

I covered these problems in a blog post not long after they were introduced (and the same problems still apply today). In fact, it is such an issue that I wrote a number of posts covering the topic.

There seems to be a reluctance to let go of the notion that as long as you are careful with your helpers then you won't run into problems, which is to ignore the fact that no matter how careful you are, your use of helpers can be broken by somebody else's equally careful use, if you end up sharing code with them.

And there is no more shared code in Delphi than the VCL itself.

The use of (additional) helpers in the VCL makes the prospect of running into trouble with your own helpers more likely, not less. Even if your code works perfectly well with your own helpers in one version of the VCL, the next version could break things.

Far from being a recommendation for more use of helpers, their proliferation in the VCL is just one very good reason that you should avoid them.

Deltics
  • 22,162
  • 2
  • 42
  • 70
  • 1
    .net extension methods are a good and useful feature. Helpers are analogous. The problem is their implementation in Delphi. – David Heffernan Aug 31 '16 at 06:12
  • 1
    Both extension methods and helpers are useful features, but they are only superficially similar in that they extend a type. Beyond that: one allows multiple extensions in scope. The other does not. One has a mechanism for resolving ambiguous calls (*arising* from having multiple in scope). The other does not. If helper classes were to suddenly allow multiple in scope this would be potentially breaking change in itself (albeit that may be fixed with a resolution mechanism). Note that even the .net extension methods implementation is *not recommended for general use either*. – Deltics Aug 31 '16 at 21:46
  • 1
    Bottom line: regardless of the merits or otherwise of helper methods, they are not the most appropriate solution in this case anyway. Speciailising a stream - whether by subclassing or extension - is an anti-pattern in this case, which is about separation of concerns. – Deltics Aug 31 '16 at 21:48
  • That's my point in the first comment you made. The intent has promise but the implementation is flawed. – David Heffernan Aug 31 '16 at 21:51
6

First of all: Multi-inheritance is not possible in Delphi.

You say the methods of your custom stream classes are implemented the same for both of them? You could either use the decorator pattern in form of a stream reader class.

On the other hand, you could extend TStream by writing a class helper for it:

TCustomStreamHelper = class helper for TStream
  function ReadByte: byte;
  function ReadWord: word;
  function ReadWordBE: word;
  function ReadDWord: longword;
  function ReadDWordBE: longword;
  function ReadString(Length: integer): string;
  function ReadBlockName: string;
  // etc.
end;

Thus, in every unit where TCustomStreamHelper is known by the compiler (because you added its unit to the uses clause), you can use TStream like it had those additional methods for centuries.

René Hoffmann
  • 2,766
  • 2
  • 20
  • 43
  • A class helper looks like the simplest solution. Thank you! – Bennyboy Aug 30 '16 at 14:22
  • 1
    @Bennyboy For now perhaps. ;-) In the long term, it might pay off to refactor and avoid the class helper. The class helper feature exists as a "quick and dirty" fix, not a design tool. – Ondrej Kelle Aug 30 '16 at 16:20
  • @OndrejKelle: Apparently not such a *quick and dirty fix* anymore, now that they're all over the RTL and VCL (TStringHelper, TIntegerHelper, TExtendedHelper, etc.) in recent versions. – Ken White Aug 30 '16 at 16:55
  • @KenWhite Hopefully they are planned to be removed/refactored in the future. (Also because of the limitation of only one helper per class this can turn into a maintenance nightmare.) Their own [documentation](http://docwiki.embarcadero.com/RADStudio/Berlin/en/Class_and_Record_Helpers_(Delphi)) says: _"Class and record helpers provide a way to extend a type, but they should not be viewed as a design tool to be used when developing new code. For new code you should always rely on normal class inheritance and interface implementations."_ – Ondrej Kelle Aug 30 '16 at 17:01
  • @OndrejKelle: They've just added a whole bunch of them (in particular, the ones I listed above), so I highly doubt they'll be refactored out. (They've also recently extended it from just class helpers to also support records.) I'd suspect that the documentation is old (that portion was written circa D2006 or so) and hasn't been updated to reflect changes since. – Ken White Aug 30 '16 at 17:04
  • @KenWhite That might be a case of "quick and dirty" for reasons like tight schedule, lack of developers ;-) etc. I hope they don't plan to keep them around in the next release. – Ondrej Kelle Aug 30 '16 at 17:11
  • @OndrejKelle: They've added more each version, and updated the documentation (at least in Berlin) to describe the extension to include records. Doesn't sound like anything *quick and dirty* when they continue to add and document to me. Anyway, we're off-track here. – Ken White Aug 30 '16 at 17:12
  • 1
    @Ken - unless and until those quick and dirty helper types are given proper status as a fully robust language feature for *general purpose use*, you are hanging yourself out to dry if you build code that relies on them. As you say, they are proliferating in the VCL (which is **not** evidence for "best practice") and if this proliferation includes a TStream helper in the future then any helper you come up with in the meantime could easily fall out of scope and fixing that could be a real headache (since you can't simply cast your way out of such helper mess). – Deltics Aug 31 '16 at 04:06
4

You could have a separate reader class operating on an (abstract) stream. Have a look e.g. at TBinaryReader in Classes for inspiration.

Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
0

Delphi doesn't support multiple inheritance and it doesn't make sense in this case.

Possible Solution

What you can do is to write a class implementing TStream and accepting an internal stream which might be TFileStream or TMemoryStream. Something like this:

class TMyStream = class(TStream)
    private
        InternalStream: TStream;
    public
        constructor Create(InternalStream:TStream);

        /// implement all TStream abstract read, write, and seek methods and call InternalStream methods inside them.

        /// implement your custom methods here
end;

constructor TMyStream.Create(InternalStream:TStream)
begin
    Self.InternalStream=InternalStream;
end;

This way you have the exact class that you need; supporting both stream default methods and your custom methods.

Optional

If you must have two different classes for your custom TFileStream and TMemoryStream then you can do something like this:

class TMyFileStream : TMyStream
public
    constructor Create(const Path:String); reintroduce;
end

constructor TMyFileStream.Create(const Path:String);
begin
    inherited Create(TFileStream.Create(Path));
end;

These workarounds are just some ideas to help you get close to what you want. Modify them to make them satisfy your needs.

Javid
  • 2,755
  • 2
  • 33
  • 60
0

You can put your common methods in an interface and implement the QueryInterface, _AddRef and _Release methods in each descendant classes.

See Delphi interfaces without reference counting.

type

  IStreamInterface = interface
    function ReadByte: byte;
    function ReadWord: word;
    function ReadWordBE: word;
    function ReadDWord: longword;
    function ReadDWordBE: longword;
    function ReadString(Length: integer): string;
    function ReadBlockName: string;
  end;

  TCustomFileStream = class (TFileStream, IStreamInterface)
    function ReadByte: byte;
    function ReadWord: word;
    function ReadWordBE: word;
    function ReadDWord: longword;
    function ReadDWordBE: longword;
    function ReadString(Length: integer): string;
    function ReadBlockName: string;

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;

  end;

  TCustomMemoryStream = class (TMemoryStream, IStreamInterface)
    function ReadByte: byte;
    function ReadWord: word;
    function ReadWordBE: word;
    function ReadDWord: longword;
    function ReadDWordBE: longword;
    function ReadString(Length: integer): string;
    function ReadBlockName: string;

    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;

  end;

... and use an argument of type IStreamInterface for the procedure:

procedure DoStuff(SourceStream: IStreamInterface);
var
  data: Word;
begin
  data := SourceStream.ReadDWord;
end;
fantaghirocco
  • 4,761
  • 6
  • 38
  • 48
  • This would still have the drawback of completely redundant methods in both classes, which the OP does want to get rid of. – René Hoffmann Aug 30 '16 at 14:17
  • 1
    @René it seems to me that the OP says that both classes implement "the same functions", not that the functions share the same code – fantaghirocco Aug 30 '16 at 14:22
  • If the functions used different code then the question would make no sense. – David Heffernan Aug 30 '16 at 14:58
  • @David as far as I can understand, the key point of the question is the `DoStuff`argument. According to the question itself, I see no evidence of your assertion. – fantaghirocco Aug 30 '16 at 15:33
  • 1
    You are reading it wrong. Reading a DWORD from a stream is the same no matter how the stream is implemented. – David Heffernan Aug 30 '16 at 15:46
  • No. Reading a `DWORD` is the same for all streams. Quite what you think `TStream.Read` and `TStream.Write` are if not methods for reading and writing, I have no idea. – David Heffernan Aug 30 '16 at 15:53
  • Would be close on useless to have a stream class that offered no means of accessing the stream of bytes! – David Heffernan Aug 30 '16 at 16:01
  • 3
    I honestly don't know why this was downvoted... My understanding of the question is the same as fantaghirocco, that is : OP already implemented identical function in descendants of TFileStream and TMemoryStream and would like to call those methods in function DoStuff without having to bother with the actual type. While interface aren't arguably the best solution in that specific cases, it's still a valid way to solve similar issues. – Ken Bourassa Aug 30 '16 at 18:54
  • @KenBourassa What about all the other stream types. Add and implement the same interface to all of those types? No. It's a bad solution. Hence the voting. – David Heffernan Aug 31 '16 at 10:04
  • @DavidHeffernan But since the scope of the question is about 2 specific TStream descendant and not all of them, I think this answer is perfectly fine. It could also be useful to other visitors coming with the same issue (New methods in descending class) where the class helper or reader could not be applied as easily (or elegantly). For these reasons, I think the downvoting was overzealous. – Ken Bourassa Aug 31 '16 at 14:39
  • @KenBourassa Three upvotes for an answer that promotes duplication of indentical code is what I regard overzealous. – David Heffernan Aug 31 '16 at 14:44