trainings/CreatingReliableSoftwareCpp/Presentation/good_practise_solid.md

896 lines
No EOL
37 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SOLID
___
## S - Single resposibility (1)
* <!-- .element: class="fragment fade-in" --> A class (module) should has one and only one reason to change, meaning that a class should has only one job.
* <!-- .element: class="fragment fade-in" --> When we create a class <code>UrlDownloader</code> it shouldn't:
* <!-- .element: class="fragment fade-in" --> Make a connection -> We should have separate object for that like <code>TcpConnection</code>
* <!-- .element: class="fragment fade-in" --> Display it -> We should have separate class to do that like <code>Canvas</code>
* <!-- .element: class="fragment fade-in" --> Modifying the response, like resizing, it should be done by other class like <code>Bitmap</code>
* <!-- .element: class="fragment fade-in" --> Cache it -> We should have other class to store cached images like <code>FileCache</code>
* <!-- .element: class="fragment fade-in" --> Advantages:
* <!-- .element: class="fragment fade-in" -->Testing A class with one responsibility will have far fewer test cases.
* <!-- .element: class="fragment fade-in" -->Lower coupling Less functionality in a single class will have fewer dependencies.
* <!-- .element: class="fragment fade-in" -->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:
* <!-- .element: class="fragment fade-in" --> Download object: Create request, make connection, download raw bits
* <!-- .element: class="fragment fade-in" --> ImageDownloader, TcpConnection class
* <!-- .element: class="fragment fade-in" --> Image representation: Store info about image, bitmap, allow to modify it.
* <!-- .element: class="fragment fade-in" --> Image, Bitmap class
* <!-- .element: class="fragment fade-in" --> User interface: Print object
* <!-- .element: class="fragment fade-in" --> Canvas, Window class
* <!-- .element: class="fragment fade-in" --> Cache: Cache object for future usage and quick access
* <!-- .element: class="fragment fade-in" --> FileStorage, RamStorage class
___
## S - Single resposibility (3)
* <!-- .element: class="fragment fade-in" --> By following of rule one responsibility let's create <code>Image</code> class. This class will be responsible for:
* <!-- .element: class="fragment fade-in" --> Store bitmap
* <!-- .element: class="fragment fade-in" --> Store url of image to allow identification of it
* <!-- .element: class="fragment fade-in" --> This class don't need to know how to display an image or resize it
```C++
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_;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.89em" -->
___
Let's create a second class form this module, a <code>Bitmap</code> class. This class will be responsible for:
* <!-- .element: class="fragment fade-in" --> Store raw bit
* <!-- .element: class="fragment fade-in" --> Store type of image (png, jpg)
* <!-- .element: class="fragment fade-in" --> Store additional info about image like: size, name, url etc...
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
* <!-- .element: class="fragment fade-in" --> <b>Does Bitmap follow the rule of single responsibility?</b>
<!-- .slide: style="font-size: 0.84em" -->
___
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 <code>Canvas</code> class. This class will be responsible for:
* <!-- .element: class="fragment fade-in" --> Knowing how to display bitmap
* <!-- .element: class="fragment fade-in" --> Store additional info about canvas like: size
* <!-- .element: class="fragment fade-in" --> This class don't need to know how to transform image or resize it.
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
* <!-- .element: class="fragment fade-in" --> <b>Does Canvas follow the rule of single responsibility?</b>
<!-- .slide: style="font-size: 0.78em" -->
___
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`.
```C++
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.
<!-- .element: class="fragment fade-in" -->
___
## 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:
* <!-- .element: class="fragment fade-in" --> push_back
* <!-- .element: class="fragment fade-in" --> insert
* <!-- .element: class="fragment fade-in" --> erase
But we don't have methods like:
<!-- .element: class="fragment fade-in" -->
* <!-- .element: class="fragment fade-in" --> sort
* <!-- .element: class="fragment fade-in" --> unique
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.92em" -->
___
## 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:
* <!-- .element: class="fragment fade-in" --> find
* <!-- .element: class="fragment fade-in" --> rfind
* <!-- .element: class="fragment fade-in" --> find_first_of
* <!-- .element: class="fragment fade-in" --> find_first_not_of
* <!-- .element: class="fragment fade-in" --> find_last_of
* <!-- .element: class="fragment fade-in" --> find_last_not_of
* <!-- .element: class="fragment fade-in" --> replace
* <!-- .element: class="fragment fade-in" --> substr
* <!-- .element: class="fragment fade-in" --> contains
* <!-- .element: class="fragment fade-in" --> starts_with
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.84em" -->
___
## std::list
You can argue that `std::list` also has a specific functions like:
* <!-- .element: class="fragment fade-in" --> merge
* <!-- .element: class="fragment fade-in" --> splice
* <!-- .element: class="fragment fade-in" --> remove
* <!-- .element: class="fragment fade-in" --> remove_if
* <!-- .element: class="fragment fade-in" --> reverse
* <!-- .element: class="fragment fade-in" --> unique
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
## 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)
* <!-- .element: class="fragment fade-in" --> Objects or entities should be open for extension but closed for modification.
* <!-- .element: class="fragment fade-in" --> When we add new functionality or a new type our changes should not require modification of existing code
* <!-- .element: class="fragment fade-in" --> When we have a bug in code of course we need to modify it :)
* <!-- .element: class="fragment fade-in" --> Advantages:
* <!-- .element: class="fragment fade-in" --> We stop ourselves from modifying existing code and causing potential new bugs
* <!-- .element: class="fragment fade-in" --> We mainly focus on adding new functionalities rather than modifying code to fit the new version
___
## O - Open-Closed (2)
We implemented an <code>Image</code> class which stores information about `URL` and `bitmap`, now we have a task that we should also have the possibility to store info about:
* <!-- .element: class="fragment fade-in" --> date when the image was downloaded
* <!-- .element: class="fragment fade-in" --> date when the image was created originally
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
* <!-- .element: class="fragment fade-in" --> 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.
* <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.66em" -->
___
## 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?
* <!-- .element: class="fragment fade-in" --> 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.
* <!-- .element: class="fragment fade-in" --> 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.
* <!-- .element: class="fragment fade-in" --> I will describe more these patterns in the section <code>design patterns</code>
___
## Visitor
```C++
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;
};
```
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.68em" -->
___
## Strategy
```C++
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;
};
```
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.71em" -->
___
## 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:
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.90em" -->
___
## Open-close summary
Adding new functionality or types to your code should not require modification of existing code.
___
## L - Liskov Substitution
* <!-- .element: class="fragment fade-in" --> Subtypes must be substitutable for their base types.
* <!-- .element: class="fragment fade-in" --> Preconditions cannot be strengthened in a substitute
* <!-- .element: class="fragment fade-in" --> Postconditions cannot be weakened in a substitute
* <!-- .element: class="fragment fade-in" --> Invariants of the super type must be preserved in a substitute
* <!-- .element: class="fragment fade-in" --> Advantages:
* <!-- .element: class="fragment fade-in" --> We will not be surprised when we use a method from the interface that exists with an exception or will be empty
* <!-- .element: class="fragment fade-in" --> We will not be surprised when a method acts weird in terms of validation of input data or output data
* <!-- .element: class="fragment fade-in" --> 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**?
<div class="multicolumn">
<div class="column">
**A**
```C++
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{};
};
```
</div>
<!-- .element: class="fragment fade-in" -->
<div class="column">
**B**
```C++
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;
};
```
</div>
<!-- .element: class="fragment fade-in" -->
</div>
<!-- .slide: style="font-size: 0.86em" -->
___
The answer is A, because in B we need to modify invariants. Let's check the following code form example B:
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
I hope you see, why this is not a good example of inheritance, and option A is much better.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.86em" -->
___
## 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>`.
```C++
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);
};
```
<!-- .element: class="fragment fade-in" -->
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:
<!-- .element: class="fragment fade-in" -->
```C++
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);
}
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.62em" -->
___
## 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:
<!-- .element: class="fragment fade-in" -->
```C++
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;
}
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.62em" -->
___
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.
```C++
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;
}
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.62em" -->
___
## STL once more
Let's check `std::transform` method. This algorithm satisfies all rules o far: **S**ingle responsibility, **O**pen-close and also a **L**iskov 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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.90em" -->
___
## Liskov Substitution - summary
Make sure that inheritance is about behavior not about data
___
## I - Interface Segregation
* <!-- .element: class="fragment fade-in" --> 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
    * <!-- .element: class="fragment fade-in" --> When we inherit from some interface, we should always implement all methods
    * <!-- .element: class="fragment fade-in" --> 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.
* <!-- .element: class="fragment fade-in" --> Advantages:
    * <!-- .element: class="fragment fade-in" --> We will not be surprised when we use a method from interface which exits with an exception or will be empty
    * <!-- .element: class="fragment fade-in" --> We don't need to worry about how to implement a function which don't make a sense for new class
    * <!-- .element: class="fragment fade-in" --> 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.
<!-- .element: class="fragment fade-in" -->
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
```C++
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;
};
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.72em" -->
___
## 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.
```C++
class ImageDownloader : public Downloader {
public:
void download(const std::string& url, DownloadCallback cb) {
// download an image
}
};
```
```C++
class VideoDownloader : public Downloader {
public:
void download(const std::string& url, DownloadCallback cb) {
// download a video
}
};
```
<!-- .slide: style="font-size: 0.94em" -->
___
## 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.
```C++
class UrlDownloader {
UrlDownloader(ImageDownloader* imageDownloader,
VideoDownloader* videoDownloader,
CertificateDownloader* certificateDownloader,
AudioDownloader* audioDownloader) {}
};
```
<!-- .element: class="fragment fade-in" -->
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**.
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.87em" -->
___
```C++
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;
};
```
```C++
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); });
}
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.67em" -->
___
## 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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.90em" -->
___
## Interface segregation - summary
Make sure interfaces don't include unnecessary dependencies
___
## D - Dependency Inversion
* <!-- .element: class="fragment fade-in" --> High-level modules should not depend on low-level modules. Both should depend on abstraction
* <!-- .element: class="fragment fade-in" --> Abstraction should not depend on details. Details should depend on abstraction
* <!-- .element: class="fragment fade-in" --> In other words this principle allows for decoupling classes
* <!-- .element: class="fragment fade-in" --> Advantages:
* <!-- .element: class="fragment fade-in" --> We can easy substitute object of any other class which inherit from the same interface
* <!-- .element: class="fragment fade-in" --> 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`.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.79em" -->
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!**.
<!-- .element: class="fragment fade-in" -->
___
## D - Dependency Inversion (3)
High level module should not depend on low level module and vice versa. Both should depends on abstraction:
```C++
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);
}
```
<!-- .slide: style="font-size: 0.72em" -->
___
## UML
<img data-src="images/dependency_inversion.png" alt="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.
<img data-src="images/MVC.png" alt="MVC" height="520px">
<!-- .slide: style="font-size: 0.70em" -->
___
## 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.
```C++
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;
}
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.90em" -->
___
## Dependency inversion - summary
Prefer depend on abstraction instead of concrete types.
___
## Exercise
* <!-- .element: class="fragment fade-in" --> Open project streamer
* <!-- .element: class="fragment fade-in" --> Create Mpeg2Streamer class
* <!-- .element: class="fragment fade-in" --> Create MjpegStreamer class
* <!-- .element: class="fragment fade-in" --> Allow to add data to stream
* <!-- .element: class="fragment fade-in" --> Allow to add/remove receivers
* <!-- .element: class="fragment fade-in" --> start/stop stream
* <!-- .element: class="fragment fade-in" --> validate data
* <!-- .element: class="fragment fade-in" --> Make some dummy implementations that will allow to compile code
```C++
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;
```
<!-- .element: class="fragment fade-in" -->
<!-- .slide: style="font-size: 0.85em" -->