You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- The constructor for `Simulation` calls the constructor of `Data` directly; if the constructor of `Data` changes (because we have changed something about our data representation) then the class `Simulation` must also be changed.
272
272
- The class `Data` may develop and contain functionality that is irrelevant to what `Simulation` needs.
273
273
274
-
Dependency injection is generally achieved by using an abstract class in place of a concrete type for a component of a class. The abstract class defines a interface that must be met by any class that you want to use, but does not enforce what exactly that class should be. This allows you to design a class which can be reused with different components which fulfil the same functionality depending on what you need it for.
274
+
Dependency injection breaks the coupling between these classes by contructing the component (`Data`) _separately_, and passing it into the constructor for the class of object that will hold it (`Simulation`). This means that if we change the way that the `Data`class is constructed, we don't have to make any changes to the `Simulation` class.
275
275
276
276
```cpp
277
-
classAbstractSimData
277
+
classSimulation
278
278
{
279
-
public:
280
-
virtual void print() = 0;
279
+
Simulation(unique_ptr<Data> &inData)
280
+
{
281
+
data = std::move(inData);
282
+
}
283
+
284
+
void printData()
285
+
{
286
+
data->print();
287
+
}
288
+
289
+
private:
290
+
std::unique_ptr<Data> data;
281
291
};
292
+
```
282
293
283
-
classData : publicAbstractSimData
294
+
- Note that the `Simulation` class now does not call the constructor for the `data` object: the `Data` implementation can change completely as long as it still implements the `print` method, which is the only thing that we need from it in this example. The `Simulation` class is now _decoupled_ from any elements of the `Data` class that it does not directly need to know about and use.
295
+
296
+
We can also add a setter function using a similar appraoch. With this kind of structure we can create classes that allow components to be swapped out during the lifetime of the object, something that will be very important for the next pattern that we look at.
297
+
298
+
```cpp
299
+
class Simulation
284
300
{
285
301
public:
286
-
void print()
302
+
Simulation(unique_ptr<Data> &inData)
287
303
{
288
-
for(auto x: data)
289
-
{
290
-
std::cout << x << " ";
291
-
}
292
-
std::cout << std::endl;
304
+
data = std::move(inData);
305
+
}
306
+
307
+
void setData(unique_ptr<Data> &inData)
308
+
{
309
+
data = std::move(inData);
310
+
}
311
+
312
+
void printData()
313
+
{
314
+
data->print();
293
315
}
294
316
295
317
private:
296
-
vector<int> data;
318
+
std::unique_ptr<Data> data;
297
319
};
298
320
```
299
-
-`AbstractSimData` is an abstract class, because its function `print` is not implemented. It defines the interface that any data class that wants to be used with the `Simulation` class would need to implement.
300
-
-`print` is _pure_ and _virtual_ which means that it will always be overridden by a derived class. This defines a "contract": a set of functionality that anything which inherits from this abstract class _must_ implement. We can use such abstract classes to define minimal functionality required by other classes: this is sometimes referred to as an "interface".
301
-
- Interfaces are a core language feature of some other languages like Java and C#, but are not explicitly implemented in C++.
302
-
- In C++ we generally implement interfaces using abstract classes containing only pure virtual functions and variables.
321
+
- If we have two data sets `dataSet1` and `dataSet2` we can now change the data that the `Simulation` object looks at runtime without creating a new `Simulation` object.
322
+
323
+
Using setters to implement dependency injection should not lead you to neglect your constructors! It's generally best if you always **make sure that objects are constructed in a valid state**. In this example, we might consider any object where the `data` pointer is null to be invalid. In order to avoid a partially constructed object, we need to make sure the dependency injection is implemented in the constructor (and optionally the setter), and always checks for null pointers.
303
324
304
-
The trick with dependency injection is to then pass (a.k.a. "inject") the component you want to use to a constructor or setter function. This is done at runtime rather than compile time, and means that different instances of the class can be instantiated with different components based on run-time considerations.
305
325
```cpp
306
326
classSimulation
307
327
{
308
-
Simulation(unique_ptr<AbstractSimData> &inData)
328
+
public:
329
+
Simulation(unique_ptr<Data> &inData)
309
330
{
331
+
if(!inData)
332
+
{
333
+
throw std::runtime_error("Simulation error: data set pointer cannot be null.")
334
+
}
335
+
data = std::move(inData);
336
+
}
337
+
338
+
void setData(unique_ptr<Data> &inData)
339
+
{
340
+
if(!inData)
341
+
{
342
+
throw std::runtime_error("Simulation error: data set pointer cannot be null.")
343
+
}
310
344
data = std::move(inData);
311
345
}
312
346
@@ -316,14 +350,47 @@ class Simulation
316
350
}
317
351
318
352
private:
319
-
std::unique_ptr<AbstractSimData> data;
353
+
std::unique_ptr<Data> data;
320
354
};
321
355
```
322
356
323
-
- Now `Simulation` works with an abstract class `AbstractSimData`, which does not itself contain an implementation of `print`. It doesn't care _how_ it gets done, just that it _can_ be done.
324
-
- Note that the `Simulation` class now does not call the constructor for the `data` object: the `Data` implementation can change completely as long as it still implements the `print` method, which is the only thing that we need from it in this example. The `Simulation` class is now _decoupled_ from any elements of the `Data` class that it does not directly need to know about and use.
357
+
## Example: Strategy Pattern
358
+
359
+
The _strategy pattern_ takes advantage of polymorphic behaviour to provide different solutions or representations for problems at runtime.
360
+
361
+
Often the best solution to use for a particular problem will depend on the details of that problem: some sorting algorithms are faster on shorter lists while other are faster on longer lists, or some integrators might be more accurate and efficient when integrating slowly changing functions but others work better for oscillating functions. If we have a class which needs to have a component which achieves a task like this, then rather than having multiple classes with different concrete implementations of these algorithms built in, we can have a single class with a pointer to an abstract base class e.g. a sorter, or an integrator. We can then have different sub-classes of our abstract class that implement the different solutions to our problem, and we can pass different solutions into our main class, or even change solutions throughout the runtime based on considerations that we can't know at compile time (e.g. the length of a list that needs to be sorted). Let's continue our example from dependency injection, but now let's add an abstract base class for the `Data`.
362
+
363
+
364
+
```cpp
365
+
class AbstractSimData
366
+
{
367
+
public:
368
+
virtual void print() = 0;
369
+
};
370
+
371
+
class Data : public AbstractSimData
372
+
{
373
+
public:
374
+
void print()
375
+
{
376
+
for(auto x: data)
377
+
{
378
+
std::cout << x << " ";
379
+
}
380
+
std::cout << std::endl;
381
+
}
382
+
383
+
private:
384
+
vector<int> data;
385
+
};
386
+
```
387
+
-`AbstractSimData` is an abstract class, because its function `print` is not implemented. It defines the interface that any data class that wants to be used with the `Simulation` class would need to implement. This interface should be kept to the minimum required.
388
+
-`print` is _pure_ and _virtual_ which means that it will always be overridden by a derived class. This defines a "contract": a set of functionality that anything which inherits from this abstract class _must_ implement. We can use such abstract classes to define minimal functionality required by other classes: this is sometimes referred to as an "interface".
389
+
- Interfaces are a core language feature of some other languages like Java and C#, but are not explicitly implemented in C++.
390
+
- In C++ we generally implement interfaces using abstract classes containing only pure virtual functions and variables.
391
+
392
+
With this in place, we can define multiple classes which inherit from `AbstractSimData`, and which can then store data in different ways (e.g. maps instead of vectors and so on), or implement functions differently.
325
393
326
-
We gain even more flexibility by using a setter function. With this kind of structure we can also create classes that allow components to be swapped out during the lifetime of the object, meaning that the functionality of the object can be changed during runtime.
327
394
```cpp
328
395
classSimulation
329
396
{
@@ -347,22 +414,39 @@ class Simulation
347
414
std::unique_ptr<AbstractSimData> data;
348
415
};
349
416
```
350
-
- If we have two data sets `dataSet1` and `dataSet2` we can now change the data that the `Simulation` object looks at runtime without creating a new `Simulation` object.
351
-
-`dataSet1` and `dataSet2` don't even need to be the same type, as long as they are both of a type which inherits from `AbstractSimData`!
352
417
353
-
## Example: Strategy Pattern
354
-
355
-
One way that we can make use of this kind of class structure is to be able to select different solutions for the same problem at runtime. Often the best solution to use for a particular problem will depend on the details of that problem: some sorting algorithms are faster on shorter lists while other are faster on longer lists, or some integrators might be more accurate and efficient when integrating slowly changing functions but others work better for oscillating functions. If we have a class which needs to have a component which achieves a task like sorting or integrating, then rather than having multiple versions of that class with different concrete implementations of these algorithms built in, we can have a class which contains an abstract sorter or integrator class. We can then have different sub-classes of our abstract class that implement the different solutions to our problem, and we can pass different solutions into our main class, or even change solutions throughout the runtime based on considerations that we can't know at compile time (e.g. the length of a list that needs to be sorted). This is commonly known as the _Strategy Pattern_ (a "pattern" is just a term for a general purpose solution which is commonly applied in programming).
418
+
- `Simulation` doesn't care _how_ the `data` component implements `print`, it only needs to know that it _can_. The abstract base class has captured everything that is required by the `Simulation` class.
419
+
- With the setter in place we can change the approach that we are taking whenever we need to.
356
420
357
421
## Example: Factory Pattern
358
422
359
-
When dealing with abstract classes it is sometimes useful to be able to make objects of different sub-classes depending on runtime considerations. In this case, we can define another class or method, sometimes known as a "factory", which returns something of the base type. Let's say we have a system that allows a person to register with the University as either a `Student` or an `Employee`, both of which inherit from a generic `Person` class. Whether or not we create `Student` or `Employee` object will depend on the input that the person gives us, which we cannot know before run time. We can then create a class or function which returns a `Person` type, but which, depending on the information input, may create a `Student` or `Employee` object and return a pointer to that.
423
+
When dealing with abstract classes it is sometimes useful to be able to make objects of different sub-classes depending on runtime considerations. In this case, we can define another class or method, sometimes known as a "factory", which returns something of the base type. Common examples might be selecting an approach using a runtime flag, or changing approaches based on the size of the problem.
424
+
425
+
A factory can be implemented as a class, but often the simplest approach (in C++) is to just have a factory function like the one below.
-`ShortDataManager` and `LargeDataManager` are two concrete classes which inherit from `AbstractDataManager`, and are optimised for dealing with data on different scales.
443
+
- The factory uses the size of the data being passed in to make a decision about the approach that is going to be taken. The size of the vector is only known at runtime.
360
444
361
445
## Implementing Multiple Interfaces
362
446
363
447
When working with interfaces which define minimal behaviour, it is possible that useful objects will need to implement more than one set of behaviour. This is easy to do when the sets of behaviour are nested i.e. we can model it with a chain of inheritance. (An `Undergraduate` is a type of `Student` which is a type of `UniversityMember`.) Things can be slightly more complicated when an object implements two different functionalities which are independent of one another, for example a `StudentTeachingAssistant` is a `Student` and an `Employee`, but a `Student` is not a type of `Employee` (and vice versa).
364
448
365
-
In this case we would need to implement two interfaces independently, which can be done using multiple inheritance. Multiple inheritance is a complex topic in C++ that goes beyond implementing multiple abstract interfaces, so you should think carefully about whether, and how, you use it.
449
+
In this case we would need to implement two interfaces independently, which can be done using multiple inheritance. Multiple inheritance is a complex topic in C++ that goes beyond implementing multiple abstract interfaces, so you should think carefully about whether, and how, you use it. A good rule of thumb is to avoid it where you can (and limit inheritance to tree-like structures), and if you _do_ need to use it then limit yourself to inheritance from abstract classes with no explicit implementation or data members.
366
450
367
451
As an example, let's say we have a computer system that models people in a university and it has two classes `Student` and `Employee` to represent roles which can be taken by people in the university. We'll have some systems that process employees, and some that deal with students. There will likely be multiple types of students and employees, which we can fit seamlessly into these systems by having derived classes which inherit from `Student` or `Employee`. Now say we want a class to model a student teaching assistant - these are both students _and_ employees, and should be able to function in parts of the system that deal with either. In this case we need a class that is recognisable as both a `Student` and an `Employee` in our program. We can do this by declaring multiple inheritance:
0 commit comments