5CCYB041
We have now covered all the material necessary to finish the robot arm project
You can find the most up to date version in the project's solution/
folder
Modify your code to implement the remaining steps:
Refer to the online solution for the final design
Object-Oriented Programming is based on 4 core principles:
Object-Oriented Programming is based on 4 core principles:
We have already been using all 4 of these principles throughout the course
⇒ it's time to formalise what they mean
Abstraction aims to provide a simplified interface to otherwise complex systems.
Abstraction aims to provide a simplified interface to otherwise complex systems.
We use abstractions all the time:
Abstraction aims to provide a simplified interface to otherwise complex systems.
We use abstractions all the time:
Abstraction aims to provide a simplified interface to otherwise complex systems.
We use abstractions all the time:
Abstraction aims to provide a simplified interface to otherwise complex systems.
We use abstractions all the time:
These are all examples of abstraction
Abstraction can be achieved in C++ by providing intuitive interfaces to potentially complex objects
The most common manifestation is the use of classes with methods
Abstraction can be achieved in C++ by providing intuitive interfaces to potentially complex objects
The most common manifestation is the use of classes with methods
Example, the Image
class that we set up for the fMRI project:
std::vector
internallyAbstraction can be achieved in C++ by providing intuitive interfaces to potentially complex objects
The most common manifestation is the use of classes with methods
Example, the Image
class that we set up for the fMRI project:
std::vector
internallyThe various segments in the robot arm projects also provide a level of abstraction
tip_position()
method provides the position without the need to worry
about how it was computedAbstraction can be achieved in C++ by providing intuitive interfaces to potentially complex objects
The most common manifestation is the use of classes with methods
Example, the Image
class that we set up for the fMRI project:
std::vector
internallyThe various segments in the robot arm projects also provide a level of abstraction
tip_position()
method provides the position without the need to worry
about how it was computedBut functions can also provide abstractions:
load_pgm()
functionEncapsulation aims to maintain the integrity of a system, by protecting its internal components and preventing direct manipulation other than through the interface provided
Encapsulation aims to maintain the integrity of a system, by protecting its internal components and preventing direct manipulation other than through the interface provided
Again, there are plenty of everyday examples of encapsulation:
Encapsulation aims to maintain the integrity of a system, by protecting its internal components and preventing direct manipulation other than through the interface provided
Again, there are plenty of everyday examples of encapsulation:
All of these are examples of encapsulation:
Encapsulation can be achieved in C++ by protecting the internal state of objects
The most common way is to use classes, storing the state of the object as private or protected attributes, along with suitably designed public methods to interact with these attributes safely
Encapsulation can be achieved in C++ by protecting the internal state of objects
The most common way is to use classes, storing the state of the object as private or protected attributes, along with suitably designed public methods to interact with these attributes safely
For example, consider our Image class:
Encapsulation can be achieved in C++ by protecting the internal state of objects
The most common way is to use classes, storing the state of the object as private or protected attributes, along with suitably designed public methods to interact with these attributes safely
For example, consider our Image class:
⇒ no, since changing the image dimensions changes the number of pixels
Encapsulation can be achieved in C++ by protecting the internal state of objects
The most common way is to use classes, storing the state of the object as private or protected attributes, along with suitably designed public methods to interact with these attributes safely
For example, consider our Image class:
⇒ no, since changing the image dimensions changes the number of pixels
The current implementation protects these attributes, ensuring they cannot be modified other than through the methods provided.
Inheritance allows one class to derive from and extent another class, allowing strongly related objects to share common attributes and methods
Inheritance allows one class to derive from and extent another class, allowing strongly related objects to share common attributes and methods
Examples of inheritance include:
Inheritance allows one class to derive from and extent another class, allowing strongly related objects to share common attributes and methods
Examples of inheritance include:
Inheritance allows one class to derive from and extent another class, allowing strongly related objects to share common attributes and methods
Examples of inheritance include:
These are examples of making use of a common base, which can then be extended in different ways for specific products
Inheritance in achieved in OOP by allowing a base class to be inherited by one or more derived classes
The data members of the base class are also present in any derived class
The methods of the base class are also available to the derived class and external code
Derived classes can extend the base class by adding more data members and providing additional functionality
Derived class can also provide their own implementation of some of the methods of the base class
Inheritance in achieved in OOP by allowing a base class to be inherited by one or more derived classes
The data members of the base class are also present in any derived class
The methods of the base class are also available to the derived class and external code
Derived classes can extend the base class by adding more data members and providing additional functionality
Derived class can also provide their own implementation of some of the methods of the base class
We have seen this in action in our robot arm project
The word comes from Greek, and means 'many forms'
Polymorphism refers to the ability to define different objects that provide the same interface
The word comes from Greek, and means 'many forms'
Polymorphism refers to the ability to define different objects that provide the same interface
There are many day-to-day examples of polymorphism:
The word comes from Greek, and means 'many forms'
Polymorphism refers to the ability to define different objects that provide the same interface
There are many day-to-day examples of polymorphism:
The word comes from Greek, and means 'many forms'
Polymorphism refers to the ability to define different objects that provide the same interface
There are many day-to-day examples of polymorphism:
The word comes from Greek, and means 'many forms'
Polymorphism refers to the ability to define different objects that provide the same interface
There are many day-to-day examples of polymorphism:
These all involve objects with a known, common interface, but different implementations
C++ supports different types of polymorphism:
In this form, a common interface is defined in the (human-readable) C++ code, and the compiler deduces which implementation to use when generating the binary code
C++ supports different types of polymorphism:
In this form, a common interface is defined in the (human-readable) C++ code, and the compiler deduces which implementation to use when generating the binary code
In this form, a common interface is defined in the C++ code, and also in the generated binary code. The compiler implements the necessary machinery to ensure that the program can work with any object that provides the expected interface, but cannot make assumptions regarding which specific subtype might be provided at any point in time while running.
The decision as to which specific implementation is to be used is therefore made while the program is running: dynamically, at run-time.
C++ provides two main ways to achieve compile-time polymorphism:
We can provide multiple functions with the same name, but different arguments
void display (int x) { std::cout << x << "\n"; }void display (double d) { std::cout << d << "\n"; }void display (const std::string& s) { std::cout << s << "\n"; }...double val = 10.2;display (val); // <= the compiler can deduce which version to invoke here...std::string mesg = "not good!";display (mesg); // <= the compiler can deduce which different version to use here
C++ provides two main ways to achieve compile-time polymorphism:
We can specify a function as taking one or more generic argument types
template <typename X> void display (const X& x) { std::cout << x << "\n"; }...double val = 10.2;display (val); // <= the compiler can substitute 'double' instead of 'X' here...std::string mesg = "not good!";display (mesg); // <= the compiler can substitute 'std::string' instead of 'X' here
C++ provides two main ways to achieve compile-time polymorphism:
We can also create template classes in this way:
template <typename X> class Complex { public: void display () const { std::cout << "(" << real << "," << imag << ")\n"; ... private: X real, imag; };Complex<int> ci { 3, 2 };Complex<double> cd { 1.0, 0.5 };
In C++, runtime polymorphism is typically achieved using inheritance and virtual functions.
In C++, runtime polymorphism is typically achieved using inheritance and virtual functions.
We define a base class to specify the expected interface
virtual
keywordIn C++, runtime polymorphism is typically achieved using inheritance and virtual functions.
We define a base class to specify the expected interface
virtual
keywordWe can then define derived classes that inherit from the base class
virtual
and provide their own implementationIn C++, runtime polymorphism is typically achieved using inheritance and virtual functions.
We define a base class to specify the expected interface
virtual
keywordWe can then define derived classes that inherit from the base class
virtual
and provide their own implementationWhich implementation to invoke will be determined at runtime, at the point of use, based on the specific type of the object being handled at that time
In C++, runtime polymorphism is typically achieved using inheritance and virtual functions.
We define a base class to specify the expected interface
virtual
keywordWe can then define derived classes that inherit from the base class
virtual
and provide their own implementationWhich implementation to invoke will be determined at runtime, at the point of use, based on the specific type of the object being handled at that time
For example, let's design a class to represent a generic numerical problem
class ProblemBase { public: virtual int size () const = 0; virtual double eval (const std::vector<double>& x) const = 0;};
For example, let's design a class to represent a generic numerical problem
class ProblemBase { public: virtual int size () const = 0; virtual double eval (const std::vector<double>& x) const = 0;};
We can then write a function to minimise any problem defined in this way:
std::vector<double> minimise (const ProblemBase& problem);
which could use some suitable optimisation algorithm to identify and return the optimal parameter vector.
One such problem might be to fit an exponential:
The task is to identify the parameters of the curve that minimise the sum of squared differences between the fitted curve and the measurements.
We can then represent our exponential fitting problem by deriving from ProblemBase
:
class Exponential : public ProblemBase { public: Exponential (const std::vector<double>& measurements, const std::vector<double>& timepoints) : m (measurements), t (timepoints) { } int size () const override { return 2; } double eval (const std::vector<double>& x) const override { double cost = 0.0; for (int n = 0; n < m.size(); n++) { double diff = x[0] * std::exp (x[1]*t[n]) - m[n]; cost += diff*diff; } return cost; } private: std::vector<double> m, t;};
We can then represent our exponential fitting problem by deriving from ProblemBase
:
class Exponential : public ProblemBase { public: Exponential (const std::vector<double>& measurements, const std::vector<double>& timepoints) : m (measurements), t (timepoints) { } int size () const override { return 2; } double eval (const std::vector<double>& x) const override { double cost = 0.0; for (int n = 0; n < m.size(); n++) { double diff = x[0] * std::exp (x[1]*t[n]) - m[n]; cost += diff*diff; } return cost; } private: std::vector<double> m, t;};
The problem is characterised by 2 parameters: the amplitude and decay rate
We can then represent our exponential fitting problem by deriving from ProblemBase
:
class Exponential : public ProblemBase { public: Exponential (const std::vector<double>& measurements, const std::vector<double>& timepoints) : m (measurements), t (timepoints) { } int size () const override { return 2; } double eval (const std::vector<double>& x) const override { double cost = 0.0; for (int n = 0; n < m.size(); n++) { double diff = x[0] * std::exp (x[1]*t[n]) - m[n]; cost += diff*diff; } return cost; } private: std::vector<double> m, t;};
The eval()
call evaluates the goodness of fit:
We can then represent our exponential fitting problem by deriving from ProblemBase
:
class Exponential : public ProblemBase { public: Exponential (const std::vector<double>& measurements, const std::vector<double>& timepoints) : m (measurements), t (timepoints) { } int size () const override { return 2; } double eval (const std::vector<double>& x) const override { double cost = 0.0; for (int n = 0; n < m.size(); n++) { double diff = x[0] * std::exp (x[1]*t[n]) - m[n]; cost += diff*diff; } return cost; } double eval (const std::vector<double>& x) const override { private: std::vector<double> m, t;};
We can provide the measurements and their corresponding time points in the constructor
Similarly, we could use this approach to solve any other problem of interest that can be expressed as a cost that depends on a vector of parameters.
Similarly, we could use this approach to solve any other problem of interest that can be expressed as a cost that depends on a vector of parameters.
Runtime polymorphism is useful when the system needs to interact with a generic type of object, but the specific type cannot be known at runtime, or may change at runtime.
A fundamental aspect of OOP design is to:
A fundamental aspect of OOP design is to:
When designing an OOP solution, the first challenge is to identify which aspects of the problem should be represented as distinct classes
A fundamental aspect of OOP design is to:
When designing an OOP solution, the first challenge is to identify which aspects of the problem should be represented as distinct classes
The next challenge is to specify how these classes and objects relate to each other. There are different types of relationships in OOP, including notably:
Dependency is the most 'loose' form of relationship
Objects may 'know' about each other, but are otherwise independent.
Dependency is the most 'loose' form of relationship
Objects may 'know' about each other, but are otherwise independent.
For example, a class to load and store a matrix of data would depend on
the std::string
and the std::ifstream
classes
std::string
is used to provide the filenamestd::ifstream
might be used internally in the class' load()
method Dependency is the most 'loose' form of relationship
Objects may 'know' about each other, but are otherwise independent.
For example, a class to load and store a matrix of data would depend on
the std::string
and the std::ifstream
classes
std::string
is used to provide the filenamestd::ifstream
might be used internally in the class' load()
method More generally, classes A
& B
can be said to be in a dependency
relationship if class A
is only used in class B
's method(s) (whether as
arguments or local variables), but not in a more persistent manner.
Aggregation implies a weak association between two classes
This applies in situations where one class holds a reference to another, but does not otherwise 'own' it
Aggregation implies a weak association between two classes
This applies in situations where one class holds a reference to another, but does not otherwise 'own' it
For example, a student has-a university, and vice-versa, but neither 'own' each other
More examples of aggregation:
Company
has-a Employee
(in fact, has-many Employee
s) Employee
s exist even if Company
closesEmployee
can be employed in multiple Company
sMore examples of aggregation:
Company
has-a Employee
(in fact, has-many Employee
s) Employee
s exist even if Company
closesEmployee
can be employed in multiple Company
sAirplane
has-a Passenger
(again, has-many Passenger
s)Airplane
does not imply Passenger
s need to be terminatedPassenger
can only be on one Airplane
at a timeMore examples of aggregation:
Company
has-a Employee
(in fact, has-many Employee
s) Employee
s exist even if Company
closesEmployee
can be employed in multiple Company
sAirplane
has-a Passenger
(again, has-many Passenger
s)Airplane
does not imply Passenger
s need to be terminatedPassenger
can only be on one Airplane
at a timeComputer
has-a Keyboard
Computer
can exist without a keyboard
Keyboard
can be removed, replaced, etc.More examples of aggregation:
Company
has-a Employee
(in fact, has-many Employee
s) Employee
s exist even if Company
closesEmployee
can be employed in multiple Company
sAirplane
has-a Passenger
(again, has-many Passenger
s)Airplane
does not imply Passenger
s need to be terminatedPassenger
can only be on one Airplane
at a timeComputer
has-a Keyboard
Computer
can exist without a keyboard
Keyboard
can be removed, replaced, etc.
Aggregation can be unidirectional or bidirectional
Company
has-a Employee
, but it's also possible that an Employee
has-a Company
Computer
has-a Keyboard
, but there is no need to say that
the Keyboard
has-a Computer
...Composition is implies a stronger association between two classes
This applies in situations where one class is made up of other objects, including the other class under consideration
Composition is implies a stronger association between two classes
This applies in situations where one class is made up of other objects, including the other class under consideration
For example, a car is-made-of a chassis, engine, 4 wheels, etc.
Composition is implies a stronger association between two classes
This applies in situations where one class is made up of other objects, including the other class under consideration
For example, a car is-made-of a chassis, engine, 4 wheels, etc.
Composition can only be unidirectional
More examples of composition:
Building
is-made-of (multiple) Room
sRoom
cannot exist outside of a Building
More examples of composition:
Building
is-made-of (multiple) Room
sRoom
cannot exist outside of a Building
Book
is-made-of (many) Page
s Book
is destroyed, so are its Page
sMore examples of composition:
Building
is-made-of (multiple) Room
sRoom
cannot exist outside of a Building
Book
is-made-of (many) Page
s Book
is destroyed, so are its Page
sCar
is-made-of Motor
(amongst other parts!)Motor
can be taken out of Car
, replaced, etcCar
is not complete without Motor
, and Motor
is (usually!) destroyed if Car
is destroyedCar
shall be made-of a Motor
Inheritance (also known as generalisation) is the strongest form of relationship
This applies in situations where one class is a specialisation of another, more general class
Example of inheritance include:
Example of inheritance include:
Dog
is-a Animal
Cat
, Mouse
, Pangolin
, ...Example of inheritance include:
Dog
is-a Animal
Cat
, Mouse
, Pangolin
, ...4WheelDrive
is-a Car
FrontWheelDrive
, RearWheelDrive
, Formula1
, ...Example of inheritance include:
Dog
is-a Animal
Cat
, Mouse
, Pangolin
, ...4WheelDrive
is-a Car
FrontWheelDrive
, RearWheelDrive
, Formula1
, ...BluetoothMouse
is-a Mouse
USBMouse
, PS/2Mouse
, TouchPad
, TouchScreen
, ...Inheritance implies that derived classes are full instances of the class they inherit from
Inheritance implies that derived classes are full instances of the class they inherit from
Derived classes extend the functionality of the base class
Inheritance implies that derived classes are full instances of the class they inherit from
Derived classes extend the functionality of the base class
The Base class will have no 'knowledge' of or dependence on the derived classes
Inheritance implies that derived classes are full instances of the class they inherit from
Derived classes extend the functionality of the base class
The Base class will have no 'knowledge' of or dependence on the derived classes
Objects of a derived type can be used anywhere the Base type is expected
Inheritance implies that derived classes are full instances of the class they inherit from
Derived classes extend the functionality of the base class
The Base class will have no 'knowledge' of or dependence on the derived classes
Objects of a derived type can be used anywhere the Base type is expected
... but objects of the Base type cannot be used where a specific derived type is expected
Object-Oriented Design (OOD) is a method of designing software by conceptualizing it as a group of interacting objects, each representing an instance of a class.
Object-Oriented Design (OOD) is a method of designing software by conceptualizing it as a group of interacting objects, each representing an instance of a class.
These objects encapsulate both data (attributes) and behavior (methods).
Object-Oriented Design (OOD) is a method of designing software by conceptualizing it as a group of interacting objects, each representing an instance of a class.
These objects encapsulate both data (attributes) and behavior (methods).
The aim of Object-Oriented Design is to create a system with the following properties:
Object-Oriented Design (OOD) is a method of designing software by conceptualizing it as a group of interacting objects, each representing an instance of a class.
These objects encapsulate both data (attributes) and behavior (methods).
The aim of Object-Oriented Design is to create a system with the following properties:
The OOP design process involves:
When writing out an OOP design, we need to state:
When writing out an OOP design, we need to state:
When writing out an OOP design, we need to state:
When writing out an OOP design, we need to state:
When writing out an OOP design, we need to state:
When writing out an OOP design, we need to state:
⇒ Let's look at a few illustrative examples
We are designing a system to handle mathematics on numbers provided in different forms: real, rational, and complex
We need to support display and basic operations on these numbers
We also need to provide:
Our design might involves:
display()
methodnegate()
methodOur design might involves:
display()
methodnegate()
methodReal
class Rational
class, with an additional simplify()
methodComplex
class, with an additional conjugate()
methodOur design might involves:
display()
methodnegate()
methodReal
class Rational
class, with an additional simplify()
methodComplex
class, with an additional conjugate()
methodComplex
class may also be composed of two Real
numbersOur design might involves:
display()
methodnegate()
methodReal
class Rational
class, with an additional simplify()
methodComplex
class, with an additional conjugate()
methodComplex
class may also be composed of two Real
numbersHere what this might look like in C++ (ignoring constructors and getter/setter methods)
class NumberBase { public: virtual void display () = 0; virtual void negate () = 0; virtual ~NumberBase() { }};
class Rational : public NumberBase { public: void display () override; void negate () override; void simplify (); private: int m_numerator, m_denominator;};
class Real : public NumberBase { public: void display () override; void negate () override; private: double m_value;};
class Complex : public NumberBase { public: void display () override; void negate () override; void conjugate (); private: Real m_real, m_imag;};
A medical catheter manufacturer wishes to store information about all of the catheter types it produces. The following statements summarise the characteristics of catheters:
All catheters should have a stiffness (nm-1), a cost (in pounds/pence) and a diameter. Diameters are represented in the French scale, in which 1Fr = 0.33mm. All catheter diameters will be an integer between 3Fr and 34Fr
All catheters produced by the manufacturer are either ablation catheters or balloon catheters. Ablation catheters have a power in Watts. Balloon catheters have a balloon size in mm
The company manufactures both MR-compatible and non-MR-compatible ablation catheters. MR-compatible ablation catheters consist of a number of segments, although the precise number of segments varies
In the software application, the classes for all types of catheter should be able to display their parameters to the screen
This project brief calls for:
An abstract base
class to represent a generic Catheter
, with:
This project brief calls for:
An abstract base
class to represent a generic Catheter
, with:
A BallonCatheter
class derived from Catheter
, with:
This project also calls for:
Another abstract class derived from Catheter
, to represent an AblationCatheter
, with:
This project also calls for:
Another abstract class derived from Catheter
, to represent an AblationCatheter
, with:
A NonMRCompatibleAblationCatheter
class:
This project also calls for:
Another abstract class derived from Catheter
, to represent an AblationCatheter
, with:
A NonMRCompatibleAblationCatheter
class:
A MRCompatibleAblationCatheter
class:
What this might look like in C++ (ignoring constructors and getter/setter methods):
class Catheter { public: virtual void display () const = 0; virtual ~Catheter () { } protected: double m_stiffness; double m_cost; int m_diameter;};
class BallonCatheter : public Catheter { public: void display () const override; private: double m_balloon_size;};
class AblationCatheter : public Catheter { public: virtual ~AblationCatheter () { } protected: double m_power;};
class MRCompatibleAblationCatheter : public AblationCatheter { public: void display () const override; protected: int m_num_segments;};
class NonMRCompatibleAblationCatheter : public AblationCatheter { public: void display () const override;};
We are developing software to perform numerical optimisation of any well-behaved continuous function or problem. Problems to be solved will all compute a cost as a function of the parameter vector of a size dependent on the problem. The software will provide implementations of multiple optimisation algorithms, allowing the user to choose the most appropriate approach for their particular problem.
A problem will need to report its number of parameters, and provide a method to compute the cost for a given parameter vector of the stated size.
Optimisation algorithms will all provide methods to:
To start with, the software will provide implementations for 3 derivative-free optimisation algorithms: the Nelder-Mead method; Particle Swarm Optimisation; and the Genetic algorithm
This project calls for:
ProblemBase
OptimiserBase
This project calls for:
ProblemBase
OptimiserBase
The OptimiserBase
class will be inherited by (at least) 3 derived classes:
NelderMeadOptimiser
ParticleSwarmOptimiser
GeneticOptimiser
This project calls for:
ProblemBase
OptimiserBase
The OptimiserBase
class will be inherited by (at least) 3 derived classes:
NelderMeadOptimiser
ParticleSwarmOptimiser
GeneticOptimiser
The ProblemBase
class will be inherited by any problem that software users
wish to optimise
DemoProblem
to illustrate
the use of the software!The ProblemBase
class:
size()
method to return its number of parameterscompute()
method to calculate the cost,
given a parameter vector as its argumentThe ProblemBase
class:
size()
method to return its number of parameterscompute()
method to calculate the cost,
given a parameter vector as its argumentThe OptimiserBase
class:
init()
method to initialise the algorithm, providing a
reference to the problem to be solvediterate()
method to perform the next iteration of the
algorithm and update the current best estimate of the parameter vectorconverged()
method to check for convergenceget_solution()
method to retrieve the current best estimateThe relationship between ProblemBase
and OptimiserBase
is one of
aggregation:
OptimiserBase
'has-a' ProblemBase
The relationship between ProblemBase
and OptimiserBase
is one of
aggregation:
OptimiserBase
'has-a' ProblemBase
The relationship between OptimiserBase
and the 3 specific implementations (NelderMeadOptimiser
,
ParticleSwarmOptimiser
, GeneticOptimiser
) is one of inheritance:
NelderMeadOptimiser
'is-a' OptimiserBase
The relationship between ProblemBase
and OptimiserBase
is one of
aggregation:
OptimiserBase
'has-a' ProblemBase
The relationship between OptimiserBase
and the 3 specific implementations (NelderMeadOptimiser
,
ParticleSwarmOptimiser
, GeneticOptimiser
) is one of inheritance:
NelderMeadOptimiser
'is-a' OptimiserBase
Likewise, the relationship between ProblemBase
and any specific problem (notably DemoProblem
)
is also one of inheritance:
DemoProblem
'is-a' ProblemBase
The NelderMeadOptimiser
class:
ProblemBase
that implements
the Nelder-Mead methodinit()
methoditerate()
methodconverged()
methodThe NelderMeadOptimiser
class:
ProblemBase
that implements
the Nelder-Mead methodinit()
methoditerate()
methodconverged()
methodThe other optimisers will loop very similar, though the attributes will necessarily differ according to the needs of the specific optimisation algorithm
The DemoProblem
class:
ProblemBase
in the expected format to demonstrate the
use of the softwaresize()
methodcompute()
methodThe DemoProblem
class:
ProblemBase
in the expected format to demonstrate the
use of the softwaresize()
methodcompute()
methodOther problems can be set up by copying DemoProblem
and implementing the
specific calculations required for their problem
class ProblemBase { public: virtual int size () const = 0; virtual double compute (const std::vector<double>& x) const = 0;};
class OptimiserBase { public: OptimiserBase (const ProblemBase& problem) : m_problem (problem) { } virtual void init (const std::vector<double>& initial_guess) = 0; virtual void iterate () = 0; virtual bool converged () = 0; const std::vector<double>& get_solution () const { return m_estimate; } protected: ProblemBase& m_problem; std::vector<double> m_estimate;};
class DemoProblem : public ProblemBase { public: int size () override { return 5; } // for example... double compute (const std::vector<double>& x) const override;};
class NelderMeadOptimiser : public OptimiserBase { public: NelderMeadOptimiser (const ProblemBase& problem) : OptimiserBase (problem) { } void init (const std::vector<double>& initial_guess) override; void iterate () override; bool converged () override; private: std::vector<std::vector<double>> m_simplex; double m_reflection, m_expansion, m_contraction, m_shrink;};
and similarly for the other optimisers
It is difficult to provide general advice about how to engineer an OOP design for a given problem
It is difficult to provide general advice about how to engineer an OOP design for a given problem
Nonetheless, there are some broad principles that can be used to design robust, maintainable, and scalable software.
It is difficult to provide general advice about how to engineer an OOP design for a given problem
Nonetheless, there are some broad principles that can be used to design robust, maintainable, and scalable software.
The best known such principles are known as the SOLID principles
It is difficult to provide general advice about how to engineer an OOP design for a given problem
Nonetheless, there are some broad principles that can be used to design robust, maintainable, and scalable software.
The best known such principles are known as the SOLID principles
We'll go over a few such principles over The next few slides to give you an idea of what to have in mind when faced with an Object-Oriented Design problem
The SOLID principles are five widely-used guidelines
The five principles spell out the acronym SOLID:
A class should have one and only one reason to change, meaning that a class should have only one job.
If you find that your class is responsible for distinct aspects of your code, then you should consider splitting the functionality across multiple classes.
A class should have one and only one reason to change, meaning that a class should have only one job.
If you find that your class is responsible for distinct aspects of your code, then you should consider splitting the functionality across multiple classes.
For example, if you have a class to manage patient records, but it also has functionality to manage patient appointments, you should consider having separate classes to manage each aspects separately
Objects or entities should be open for extension but closed for modification
The idea here is that we should create a design that allows for the addition of functionality, without requiring modification of any code that might be used elsewhere.
Objects or entities should be open for extension but closed for modification
The idea here is that we should create a design that allows for the addition of functionality, without requiring modification of any code that might be used elsewhere.
For example, imagine our robot arm was written assuming a fixed configuration, with the calculation of the tip position all done as a single function
A better approach is to use inheritance:
Subtypes must be substitutable for their base types without altering the correctness of the program.
Essentially, this means that derived classes should not behave in unexpected ways
Subtypes must be substitutable for their base types without altering the correctness of the program.
Essentially, this means that derived classes should not behave in unexpected ways
For example, imagine we have a base class representing a general 3D shape, with a
virtual method get_volume()
to obtain its volume
get_volume()
method of any derived type should always return the correct volumeget_volume()
method of any derived type should always succeed (for a properly initialised shape)Subtypes must be substitutable for their base types without altering the correctness of the program.
Essentially, this means that derived classes should not behave in unexpected ways
For example, imagine we have a base class representing a general 3D shape, with a
virtual method get_volume()
to obtain its volume
get_volume()
method of any derived type should always return the correct volumeget_volume()
method of any derived type should always succeed (for a properly initialised shape)More explicitly, if the base class provides certain functionality that is expected to work in a certain way, it should work the same way for all derived classes as well.
Clients should not be forced to depend on interfaces they do not use
This means that interfaces should be kept minimal and specific to a single task. If a class provides functionality for many loosely related tasks, you should consider splitting this off into distinct interfaces.
The main advantage of this principle is to avoid bloated interfaces that become difficult to maintain.
Clients should not be forced to depend on interfaces they do not use
This means that interfaces should be kept minimal and specific to a single task. If a class provides functionality for many loosely related tasks, you should consider splitting this off into distinct interfaces.
The main advantage of this principle is to avoid bloated interfaces that become difficult to maintain.
A good example is the addition of functionality such as stapling and faxing to Xerox printers (this is actually where this principle originates!).
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
The idea behind this principle is to avoid introducing dependencies between specific sub-systems that would be hard to untangle later.
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
The idea behind this principle is to avoid introducing dependencies between specific sub-systems that would be hard to untangle later.
For example, a patient record system might need to interface with a database where the records are stored. If we write the code for the database we currently use, it may be very difficult to modify this code to swap to a different (presumably better) database at a later stage.
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
The idea behind this principle is to avoid introducing dependencies between specific sub-systems that would be hard to untangle later.
For example, a patient record system might need to interface with a database where the records are stored. If we write the code for the database we currently use, it may be very difficult to modify this code to swap to a different (presumably better) database at a later stage.
A better approach is to introduce another level of abstraction to represent a database and all the generic interactions required, so that switching to a different database can be done by modifying that interface, without affecting any other parts of the code.
While inheritance is a powerful tool, it has its downsides, introducing very tight coupling between the different components, and making it harder to modify.
While inheritance is a powerful tool, it has its downsides, introducing very tight coupling between the different components, and making it harder to modify.
For example, if we had designed a base class for a car that assumed manual transmission, introducing automatic transmission into the class hierarchy would require extensive changes in the base and derived classes.
While inheritance is a powerful tool, it has its downsides, introducing very tight coupling between the different components, and making it harder to modify.
For example, if we had designed a base class for a car that assumed manual transmission, introducing automatic transmission into the class hierarchy would require extensive changes in the base and derived classes.
Rather than trying to identify common features between components and forming a hierarchy, it is often easier to compose complex components out of simpler ones.
While inheritance is a powerful tool, it has its downsides, introducing very tight coupling between the different components, and making it harder to modify.
For example, if we had designed a base class for a car that assumed manual transmission, introducing automatic transmission into the class hierarchy would require extensive changes in the base and derived classes.
Rather than trying to identify common features between components and forming a hierarchy, it is often easier to compose complex components out of simpler ones.
Inheritance is best reserved for cases that truly require runtime polymorphism.
One of the aims of Object-Oriented design to maximise code re-use: don't repeat yourself!
If you have the same block of code in more than two places, consider making it a separate method.
Similarly, if you use a hard-coded value more than once, consider defining them as a constant at global scope.
One of the aims of Object-Oriented design to maximise code re-use: don't repeat yourself!
If you have the same block of code in more than two places, consider making it a separate method.
Similarly, if you use a hard-coded value more than once, consider defining them as a constant at global scope.
The benefit of this principle is reduced maintenance:
This principle suggests that you should only implement the features you need – not the features you think you might need in the future
This principle suggests that you should only implement the features you need – not the features you think you might need in the future
The point is to avoid unnecessary code bloat, maintenance burden, source of bugs – and wasting your own time!
This principle suggests that you should only implement the features you need – not the features you think you might need in the future
The point is to avoid unnecessary code bloat, maintenance burden, source of bugs – and wasting your own time!
This is closely related to the widely used Keep It Simple Stupid (KISS)
The KISS principle suggests that designs (in software and more broadly!) should aim for simplicity
Simple solutions are often better in practice
The KISS principle suggests that designs (in software and more broadly!) should aim for simplicity
Simple solutions are often better in practice
... and simple designs are often more elegant!
There are many design principles that can help to guide the design process
Some of these can at times be contradictory
There are many design principles that can help to guide the design process
Some of these can at times be contradictory
Remember these are only guidelines
Ultimately, the best design is one that is sufficiently complex to perform the task at hand, while remaining as simple as it can be.
A medical device manufacturer specialises in designing and producing stent grafts, which are tubular devices typically composed of a special fabric supported by a rigid structure (the stent). They are commonly employed in endovascular surgery to repair aneurysms, which are balloon-like bulges in arteries (e.g. the aorta) caused by weakness in the arterial wall. The manufacturer produces two different types of stent graft: bifurcated and complex (see images below). In a bifurcated stent graft the tubular structure splits into two, and each branch is typically placed in a separate branch of the artery undergoing repair. In a complex stent grant, the tubular structure contains a number of holes which should be located over the smaller arteries that branch off of the main artery. Complex stent grafts come in two types: branched (in which the holes have tubes attached to them which fit into the smaller arteries) or fenestrated (in which they are just holes).
The extra information in the following slides may be useful.
The rigid structure of all stent grafts is made of either nitinol or stainless steel. Some stent grafts have a fabric cover whilst others are uncovered. All stent grafts should have a diameter and a length, both stored in millimetres. For a bifurcated stent graft it is also necessary to store the diameter of each of the bifurcations. For a complex stent graft it is necessary to store the number of holes it has. For each hole the following extra information is required: the angular position (an integer between 1 and 12 which represents the position of the hole on a 'clock face' viewed from above the stent graft; and the height of the hole in millimetres.
There will not normally be more than 20 holes in a stent graft.
You are tasked with creating an object-oriented design to represent this problem domain. In your final design, the classes for each type of stent graft should be able to display their information to the screen.
Stent grafts: (left) bifurcated, and (right) complex.
We have now covered all the material necessary to finish the robot arm project
You can find the most up to date version in the project's solution/
folder
Modify your code to implement the remaining steps:
Refer to the online solution for the final design
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
s | Start & Stop the presentation timer |
t | Reset the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |