trainings/CreatingReliableSoftwareCpp/Presentation/good_practise_solid.md

37 KiB
Raw Permalink Blame History

SOLID


S - Single resposibility (1)

  • A class (module) should has one and only one reason to change, meaning that a class should has only one job.
  • When we create a class UrlDownloader it shouldn't:
    • Make a connection -> We should have separate object for that like TcpConnection
    • Display it -> We should have separate class to do that like Canvas
    • Modifying the response, like resizing, it should be done by other class like Bitmap
    • Cache it -> We should have other class to store cached images like FileCache
  • Advantages:
    • Testing A class with one responsibility will have far fewer test cases.
    • Lower coupling Less functionality in a single class will have fewer dependencies.
    • Organization Smaller, well-organized classes are easier to search than monolithic ones.

S - Single resposibility (2)

Let's say we want to create a program that downloads images and allows us to display them. In such cases we need to implement a few classes that are responsible for:

  • Download object: Create request, make connection, download raw bits
    • ImageDownloader, TcpConnection class
  • Image representation: Store info about image, bitmap, allow to modify it.
    • Image, Bitmap class
  • User interface: Print object
    • Canvas, Window class
  • Cache: Cache object for future usage and quick access
    • FileStorage, RamStorage class

S - Single resposibility (3)

  • By following of rule one responsibility let's create Image class. This class will be responsible for:
    • Store bitmap
    • Store url of image to allow identification of it
    • This class don't need to know how to display an image or resize it
class Image {
public:
    Image(const std::string& url, const Bitmap& bitmap)
        : url_(url), bitmap_(bitmap) {}
    Image(const std::string& url, Bitmap&& bitmap)
        : url_(url), bitmap_(std::move(bitmap)) {}

    const Bitmap& bitmap() const { return bitmap_; }
    const std::string& url() const { return url_; }

private:
    std::string url_;
    Bitmap bitmap_;
};

Let's create a second class form this module, a Bitmap class. This class will be responsible for:

  • Store raw bit
  • Store type of image (png, jpg)
  • Store additional info about image like: size, name, url etc...
class Bitmap {
public:
    struct Pixel { uint8_t r; uint8_t g; uint8_t b; }
    enum class Type { PNG, JPG };

    Image(size_t width, size_t height, Type type, std::vector<Pixel> bits)
        : _width(width), _height(height), _type(type), _bits(std::move(bits)) {}

    void resize(size_t newWidth, size_t newHeight) { ... }
    void mirrorReflection() { ... }
    // getter like, witdh(), height() etc...

private:
    size_t _width; 
    size_t _height; 
    Type _type; 
    std::vector<Pixel> _bits;
};
  • Does Bitmap follow the rule of single responsibility?

Bitmap keeps info about types of images. So when we want to support a new type of image we need to modify the Image class and also modify the methods, like resize, mirror reflection, etc... because each type of image may be formatted differently. That's why we should separate this class. I will show it later when we will talk about other rules from SOLID.


Let's create a second module, which will be resposible for printing image. We starts from a Canvas class. This class will be responsible for:

  • Knowing how to display bitmap
  • Store additional info about canvas like: size
  • This class don't need to know how to transform image or resize it.
class Canvas {
public:
    Canvas(size_t width, size_t height) : _width(width), _height(height) {}

    void print(const Image& img) const {
        const auto& bitmap = img.bitmap();
        const size_t imgWidth = bitmap.width();
        const size_t imgHeight = bitmap.height();
        if (imgWidth > _width || imgHeight > _height) {
            bitmap.resize(_width, _height);
        }
        ....
    }

private:
    size_t _width; 
    size_t _height; 
};

  • Does Canvas follow the rule of single responsibility?

Once again no. Canvas is tightly coupled with types of images, so whenever we add a new type of image we need to rewrite methods. As you can see, one change (adding a new type of image) demands a user to reimplement the Canvas class and Bitmap class and probably even more classes. This is not a single responsibility. The class should have one reason to change and now class has a lot of reasons to change. Once again I will show you later how we can resolve it.


Single resposibility in STL

SOLID is also valid for static polymorphism. As an example, we can check the STL algorithm like std::stransform.

template<class InputIt, class OutputIt, class UnaryOp>
OutputIt transform(InputIt first1, InputIt last1,
                   OutputIt d_first, UnaryOp unary_op)
{
    while (first1 != last1)
        *d_first++ = unary_op(*first1++);
 
    return d_first;
}

Transform performs one operation e2e: iterate through the input range, call unary_op, and store the result in an output range.


Single resposibility in STL (2)

Another example will be a std::vector class. The whole interface is strongly connected with a dynamically allocated array, we can:

  • push_back
  • insert
  • erase

But we don't have methods like:

  • sort
  • unique
  • transform

Now you can understand, why std::remove is a separate method, because std::vector should not have logic, on how to rearrange an array, because this is not a part of its responsibility. std::vector gives us an interface only for adding, modifying, and removing elements form a dynamic array and STL algorithm give us a possibility to rearrange the container.


Breaking single resposibility in STL

Two years after the STL library was introduced we get also a std::string which is not a good example of a single responsibility. You may ask why. Let's check std::string interface:

  • find
  • rfind
  • find_first_of
  • find_first_not_of
  • find_last_of
  • find_last_not_of
  • replace
  • substr
  • contains
  • starts_with
  • ends_with

I know that this is handy, to have such a method I use them a lot. But this is not exactly a doo design. Because every standard when std::string grows, the authors of libraries need to modify the class and can introduce new bugs, so development of this class is hard.


std::list

You can argue that std::list also has a specific functions like:

  • merge
  • splice
  • remove
  • remove_if
  • reverse
  • unique
  • sort

yes, I agree, but these functions are specified to the type. std::list can perform these operations differently, because we may manipulate the pointers. This is an implementation of detail for std::list. STL algorithm should not know that this specific iterator should be treated differently.


Single resposibility - summary

In short words, we can say that a single responsibility means that a function, class, or module should not implement details of two orthogonal issues.


O - Open-Closed (1)

  • Objects or entities should be open for extension but closed for modification.
    • When we add new functionality or a new type our changes should not require modification of existing code
    • When we have a bug in code of course we need to modify it :)
  • Advantages:
    • We stop ourselves from modifying existing code and causing potential new bugs
    • We mainly focus on adding new functionalities rather than modifying code to fit the new version

O - Open-Closed (2)

We implemented an Image class which stores information about URL and bitmap, now we have a task that we should also have the possibility to store info about:

  • date when the image was downloaded
  • date when the image was created originally
  • a place where the image was taken

We need to ask ourselves whether these pieces of information are required for the existing code or not.

  • If required, we probably need to modify the class `Image` extend an interface, and add new fields. But still, this is done by extension of already implemented code. Other classes still compile, and we decide which class should use a new functionality.
  • If not required, we probably need to add a derived class that will have this information and use it in a new part of the code that we implement. So we don't modify anything from existing code, we just add a new functionality.
class DescribedImage : public Image {
public:
    DescribedImage(const std::string& url, const Bitmap& bitmap, DateTime downloadTime, DateTime imageTime, Coordinate place):
        Image(url, bitmap), _downloadTime(downloadTime), _imageTime(imageTime), _place(place) {}

    const DateTime& downloadTime() const { return _downloadTime; }
    const DateTime& imageTime() const { return _imageTime; }
    const Coordinate& place() const { return _place; }

private:
    DateTime _downloadTime;
    DateTime _imageTime;
    Coordinate _place;
};

O - Open-Closed (3)

When we depend on interfaces, we can also add easily new functionality by extending them. Unfortunately, if other classes inherit from these interfaces we need to write the implementation for each class, which requires modification of existing code and we break the rule open-close in such a way we need to think about what will change often: types or methods?

  • If types we can use for instance a strategy design pattern, because when we create a new type, we need to create a new strategy only for this type and we don't modify anything in existing code.
  • If methods we can use for instance a visitor design pattern, because when we add a new method, we need to implement a visit method only for this new one, but the existing code doesn't change.
  • I will describe more these patterns in the section design patterns

Visitor

class BitmapVisitor {
public:
    virtual ~BitmapVisitor() = default;

    virtual void visit(JPGImage& image, /* other args */) = 0;
    virtual void visit(PNGImage& image, /* other args */) = 0;
    virtual void visit(GIFImage& image, /* other args */) = 0;
    virtual void visit(SVGImage& image, /* other args */) = 0;
};
class ResizeVisitor : public BitmapVisitor {
public:
    void visit(JPGImage& image, /* other args */) override;
    void visit(PNGImage& image, /* other args */) override;
    void visit(GIFImage& image, /* other args */) override;
    void visit(SVGImage& image, /* other args */) override;
};

class RotateVisitor : public BitmapVisitor {
public:
    void visit(JPGImage& image, /* other args */) override;
    void visit(PNGImage& image, /* other args */) override;
    void visit(GIFImage& image, /* other args */) override;
    void visit(SVGImage& image, /* other args */) override;
};

Strategy

class DownloadStrategy {
public:
    virtual ~DownloadStrategy() = default;
    using DownloadToVectorCallback = std::function<void(std::vector<uint8_t>&&)>;
    using DownloadToStringCallback = std::function<void(std::string&&)>;
    using DownloadToFileCallback = std::function<void()>;

    virtual void downloadToVector(const std::string& url, DownloadToVectorCallback callback) = 0;
    virtual void downloadToString(const std::string& url, DownloadToStringCallback callback) = 0;
    virtual void downloadToFile(const std::string& url, const std::string& path, DownloadToFileCallback callback) = 0;
};
class ImageDownloadStrategy : public DownloadStrategy {
public:
    void downloadToVector(const std::string& url, DownloadToVectorCallback callback) override;
    void downloadToString(const std::string& url, DownloadToStringCallback callback) override;
    void downloadToFile(const std::string& url, const std::string& path, DownloadToFileCallback callback) override;
};

class VideoDownloadStrategy : public DownloadStrategy {
public:
    void downloadToVector(const std::string& url, DownloadToVectorCallback callback) override;
    void downloadToString(const std::string& url, DownloadToStringCallback callback) override;
    void downloadToFile(const std::string& url, const std::string& path, DownloadToFileCallback callback) override;
};

STL

As previously for static polymorphism, we also should apply SOLID principles. Once more look at the STL library. It was designed for the open-close principle. because if we want to add new behavior like a new algorithm we don't need to modify anything from the STL library. So every user can do that in its code. For instance, I want to implement the tranform_if method:

template <typename IN, typename OUT, typename PRED, typename FUN>
OUT transform_if(IN first, IN last, OUT out, PRED pred, FUN fun) {
    while (first != last) {
        if (pred(*first)) {
            *out = fun(*first);
        } else {
            *out = *first;
        }
        ++out;
        ++first;
    }

    return out;
}

Open-close summary

Adding new functionality or types to your code should not require modification of existing code.


L - Liskov Substitution

  • Subtypes must be substitutable for their base types.
    • Preconditions cannot be strengthened in a substitute
    • Postconditions cannot be weakened in a substitute
    • Invariants of the super type must be preserved in a substitute
  • Advantages:
    • We will not be surprised when we use a method from the interface that exists with an exception or will be empty
    • We will not be surprised when a method acts weird in terms of validation of input data or output data
    • Our code will be readable and reliable

Shapes

Let's put aside the topic of downloading for a moment. I want to show you an another example. The problem with Shapes. This is common example, so you can saw it before on the internet. I have one question for you, which example A or B is the correct one?

A

class Square {
public:
    virtual void setWidth(int);
    virtual int getArea() const;
protected:
    int width{};
};

class Rectangle : public Square {         
public:
    virtual void setHeight(int);
    int getArea() const override;
private:
    int height{};
};

B

class Rectangle {
public:
    virtual void setWidth(int);
    virtual void setHeight(int);
    virtual int getArea() const;
private:
    int width{};
    int height{};
};

class Square : public Rectangle {        
public:
    int getArea() const override;
};

The answer is A, because in B we need to modify invariants. Let's check the following code form example B:

class Square : public Rectangle {        
public:
    void setWidth(int width) override { 
        // This shape needs to be a square, so both width and height need to be the same
        _width = width;
        _height = width;
    }
    void setHeight(int height) override {
        // This shape needs to be a square, so both width and height need to be the same
        _width = height;
        _height = height;
    }
    int getArea() const override;
};

I hope you see, why this is not a good example of inheritance, and option A is much better.


L - Liskov Substitution

Backing to previous example. Now we want to implement a downloader class, that allow to fetch any type of data to std::vector<uint8_t>.

class Downloader {
public:
    using Callback = std::function<void(std::vector<uint8_t>&&)>;

    virtual void downloadToVector(const std::string& url, Callback callback) {
        if (!validateUrl(url)) {
            throw InvalidUrlException(url);
        }
        startDownloading(url, callback);
    }

private:
    bool validateUrl(const std::string& url);
    void startDownloading(const std::string& url, Callback callback);
};

Now we want to add also a downloader that loads data not from the network, but from the database. We use the same virtual function, because URL we can treat as the destination of the table (eg: "databaseName.tableName"). So we implement a new class:

class DatabaseDownloader : public Downloader {
public:
    virtual void downloadToVector(const std::string& tableName, Callback callback) {
        // Check if user provide string: databaseName.tableName
        if (!validatePath(url)) {
            throw InvalidTableNameException(tableName);
        }
        startDownloading(tableName, callback);
    }  
}

L - Liskov Substitution (2)

In the previous example, we once again modify invariants. A programmer who has used the Downloader class for a long time, will be sure, that he needs to provide a valid URL address. He will be very surprised when he will get an exception InvalidTableNameException. This is a dangerous situation because sometimes we may not see such an exception and the program will continue with invalid data. The wrong output will show later, so finding a root cause may be hard and for a few days, we will greet with debugger.

Another bad example will be when we change the preconditions:

class Ship {
public:
    virtual StatusCode sail() {
        if (_numberOfCrew < 20) {
            return StatusCode::NotEnoughSailor;
        }
        if (_cargoWeight > _maxLoad) {
            return StatusCode::ShipOverloaded;
        }
        return StatusCode::OK;
    }
};

class WarShip : public Ship {
    StatusCode sail() override {
        // Weakened precondition (previously was 20)
        if (_numberOfCrew < 15) {
            return StatusCode::NotEnoughSailor;
        }
        // Strengthened precondition (now we need also add the weight of cannons)
        if (_cargoWeight + _cannonsWeight > _maxLoad) {s
            return StatusCode::ShipOverloaded;
        }
        return StatusCode::OK; 
    }
};

You may ask me: Why our ship can't behave differently? How can I develop a few types of ships? The answer is easy, just create an interface for the ship, and then in implementation, you will provide a different behavior, but if you inherit from an already implemented class you should not break the Liskov substitution, because we don't know which class will be under the pointer or reference. We have assumption that a base class has some prediction, so we need to satisfy them ant everything should work.

class Ship {
public:
    virtual ~Ship();
    virtual StatusCode sail() = 0
};

class CargoShip : public Ship {
public:
    StatusCode sail() override {
        if (_numberOfCrew < 20) {
            return StatusCode::NotEnoughSailor;
        }
        if (_cargoWeight > _maxLoad) {
            return StatusCode::ShipOverloaded;
        }
        return StatusCode::OK;
    }
};

class WarShip : public Ship {
    StatusCode sail() override {
        if (_numberOfCrew < 15) {
            return StatusCode::NotEnoughSailor;
        }
        if (_cargoWeight + _cannonsWeight > _maxLoad) {s
            return StatusCode::ShipOverloaded;
        }
        return StatusCode::OK; 
    }
};

STL once more

Let's check std::transform method. This algorithm satisfies all rules o far: Single responsibility, Open-close and also a Liskov substitution. The L letter is supported, because we have a class InputIt and OutputIt which need to provide interface like: operator++, operator*, operator= and operator!=. We demand from a provided class that these 4 methods need to be implemented. On the other case, the code will not compile. Still, we may provide the wrong implementation, but this is our bug, not a bug from transform method.

template<class InputIt, class OutputIt, class UnaryOp>
OutputIt transform(InputIt first1, InputIt last1,
                   OutputIt d_first, UnaryOp unary_op)
{
    while (first1 != last1)
        *d_first++ = unary_op(*first1++);
 
    return d_first;
}

Liskov Substitution - summary

Make sure that inheritance is about behavior not about data


I - Interface Segregation

  • A client should never be forced to implement an interface that it doesnt use, or clients shouldnt be forced to depend on methods they do not use

    * When we inherit from some interface, we should always implement all methods     * There is an exception. This may be hard to achieve this for the observer class, because sometimes we need to know only about one event, but other classes could need more events, That is why we do not always implement all of them.

  • Advantages:

    * We will not be surprised when we use a method from interface which exits with an exception or will be empty     * We don't need to worry about how to implement a function which don't make a sense for new class     * We will follow a rule: Single responsibility. Actually, this is a special case of single responsibility.


I - Interface Segregation (2)

Let's rewrite the interface for the Download class to have only one function download, and create classes that will be responsible for downloading a specific type of info, like Image.

class Downloader {
public:
    using DownloadImageFinishedCallback = std::function<void(Result, const Image*)>;
    using DownloadCertificateFinishedCallback = std::function<void(Result, const Certificate*)>;
    using DownloadAudioFinishedCallback = std::function<void(Result, const Audio*)>;
    using DownloadVideoFinishedCallback = std::function<void(Result, const Video*)>;

    virtual ~Downloader() = default;

    virtual void downloadImage(const std::string& url, DownloadImageFinishedCallback callback) = 0;
    virtual void downloadCertificate(const std::string& url, DownloadCertificateFinishedCallback callback) = 0;
    virtual void downloadAudio(const std::string& url, DownloadAudioFinishedCallback callback) = 0;
    virtual void downloadVideo(const std::string& url, DownloadVideoFinishedCallback callback) = 0;
};
class Downloader {
public:
    using DownloadCallback = std::function<void(Result, const std::vector<uint8_t>&)>;

    virtual ~Downloader() = default;

    virtual void download(const std::string& url, DownloadCallback cb) = 0;
};

I - Interface Segregation (3)

Now depending on what we want to download we can use one of the implementations. Of course, we lose a multitasking object which knows how to download everything, but do we need it? Based on the first rule single responsibility is better to have a simpler class.

class ImageDownloader : public Downloader  {
public:
    void download(const std::string& url, DownloadCallback cb) {
        // download an image
    }
};
class VideoDownloader : public Downloader  {
public:
    void download(const std::string& url, DownloadCallback cb) {
        // download a video
    }
};

I - Interface Segregation (4)

Ok, but what if I need all of these 4 methods. In such case I need to provide 4 object co C'tor of class, to be able to download everything.

class UrlDownloader {
    UrlDownloader(ImageDownloader* imageDownloader,
                  VideoDownloader* videoDownloader,
                  CertificateDownloader* certificateDownloader,
                  AudioDownloader* audioDownloader) {} 
};

Yes, but this is ok. Usual, classes will not need to all downloader objects. For instance, a Player will need to have only 2 objects: AudioDownloader and VideoDownloader. Another class like Canvas will need to have only ImageDownloader and SecurityManager will need to download only a certificate. Having smaller classes allows us to provide only the functionality that we need. We can also achieve the same by splitting the interface of Downloader into 4 smaller classes, instead of having one function download. This depends on what will be more useful in our case. Noticed that this is what I show you earlier → a strategy design pattern.


class ImageDownloader {
public:
    using DownloadImageFinishedCallback = std::function<void(Result, const Image*)>;
    virtual void downloadImage(const std::string& url, DownloadImageFinishedCallback callback) = 0;
};
class VideoDownloader {
public:
    using DownloadVideoFinishedCallback = std::function<void(Result, const Video*)>;
    virtual void downloadVideo(const std::string& url, DownloadVideoFinishedCallback callback) = 0;
};
class CertificateDownloader {
public:
    using DownloadCertificateFinishedCallback = std::function<void(Result, const Certificate*)>;
    virtual void downloadCertificate(const std::string& url, DownloadCertificateFinishedCallback callback) = 0;
};
class AudioDownloader {
public:
    using DownloadAudioFinishedCallback = std::function<void(Result, const Audio*)>;
    virtual void downloadAudio(const std::string& url, DownloadAudioFinishedCallback callback) = 0;
};
class Player {
public:
    Player(AudioDownloader* audioDownloader, VideoDownloader* videoDownloader): 
        _audioDownloader(audioDownloader), _videoDownloader(videoDownloader) {}

    void playAudio(const std::string& url) {
        _audioDownloader->downloadAudio(url, [](Result res, const Audio* audio){ OnAutioDownloaded(res, autio); });
    }

    void playAudio(const std::string& url) {
        _videoDownloader->downloadVideo(url, [](Result res, const Audio* audio){ OnAutioDownloaded(res, autio); });
    }
}

STL example

Of course, I can't leave this principle without show you that STL library also follow this rule. Once more, check std::transform class. We have two types of interface: InputIt and OutputIt. Each iterator require something else. Input is use for reading and output for store values. That's why we can for instance read form a file or write to the file. If we use only one interface, read-write we will lose this possibility, because input_iterator can't store a value the same as output-iterator can't read a value. Keeping 2 interfaces, we allow to read and write from anything.

template<class InputIt, class OutputIt, class UnaryOp>
OutputIt transform(InputIt first1, InputIt last1,
                   OutputIt d_first, UnaryOp unary_op)
{
    while (first1 != last1)
        *d_first++ = unary_op(*first1++);
 
    return d_first;
}

Interface segregation - summary

Make sure interfaces don't include unnecessary dependencies


D - Dependency Inversion

  • High-level modules should not depend on low-level modules. Both should depend on abstraction
  • Abstraction should not depend on details. Details should depend on abstraction
  • In other words this principle allows for decoupling classes
  • Advantages:
    • We can easy substitute object of any other class which inherit from the same interface
    • This also makes testing easy, because we can mock each object!

D - Dependency Inversion (2)

Take a look for a Player class which has two functions and two members and allow to playAudio or palyVideo.

class Player {
public:
    void playAudio(const std::string& url) {
        _audioDownloader.downloadAudio(url, [](Result res, const Audio* audio) { 
            OnAudioDownloaded(res, autio); });
    }

    void playVideo(const std::string& url) {
        _videoDownloader.downloadVideo(url, [](Result res, const Video* video) { 
            OnVideoDownloaded(res, video); });
    }

private:
    Mp3AudioDownloader _audioDownloader;
    Mp4VideoDownloader _videoDownloader;
}

At first sight, everything still works correctly. We can download everything as previously. But what happens when we want to test this class by using mock? Or instead of Mp3AudioDownloader, we want to provide WAVAudioDownloader, Creating objects inside class tightly coupled these classes together!.


D - Dependency Inversion (3)

High level module should not depend on low level module and vice versa. Both should depends on abstraction:

class Player {
public:
    Player(AudioDownloader* audioDownloader, VideoDownloader* videoDownloader)
        : _audioDownloader(audioDownloader), _videoDownloader(videoDownloader) {}

    void playAudio(const std::string& url) {
        _audioDownloader.downloadAudio(url, [](Result res, const Audio* audio) { OnAudioDownloaded(res, autio); });
    }

    void playVideo(const std::string& url) {
        _videoDownloader.downloadVideo(url, [](Result res, const Video* video) { OnVideoDownloaded(res, video); });
    }

private:
    AudioDownloader* _audioDownloader;
    VideoDownloader* _videoDownloader;
};

int main() {
    WAVAudioDownloader wavDownloader;
    MP4VideoDownloader mp4Downloader;
    Player(&wavDownloader, &mp4Downloader);
}

UML

Dependency inversion ___

MVC

The good example of dependency inversion is model-view-controller. Model should be a high level because it will rarely be modified, but view and controller should be on low level because we will continuously add new functionality there. Additionally, we should create two interfaces in the module, one for communication with Controller and the second for communication with view.

MVC ___

STL for the fifth time

And let's back to the std::transform method. We represent a set of requirements for each iterator. We demand comparison, dereference etc… if we provide such implementation it will work. Furthermore, we don't need to know what exactly is passed here until it satisfies interface. So transform don't depend on low level modules we need to satisfy interface to compile the code.

template<class InputIt, class OutputIt, class UnaryOp>
OutputIt transform(InputIt first1, InputIt last1,
                   OutputIt d_first, UnaryOp unary_op)
{
    while (first1 != last1)
        *d_first++ = unary_op(*first1++);
 
    return d_first;
}

Dependency inversion - summary

Prefer depend on abstraction instead of concrete types.


Exercise

  • Open project streamer
  • Create Mpeg2Streamer class
  • Create MjpegStreamer class
  • Allow to add data to stream
  • Allow to add/remove receivers
  • start/stop stream
  • validate data
  • Make some dummy implementations that will allow to compile code
virtual bool addReceiver(const StreamInfo& info) = 0;
virtual bool removeReceiver(const StreamInfo& info) = 0;
virtual bool addData(const std::vector<uint8_t>& data) = 0;
virtual bool validateData(const std::vector<uint8_t>& data) const = 0;
virtual bool startStream() = 0;
virtual bool stopStream() = 0;
virtual bool streamInProgress() const = 0;