Generic DIP
Interesting demonstration of Dependency Inversion Principle
with generic trait.
The Dependency Inversion Prinicple states:
High-level components should not depend on low-level components. They should both depend on
abstractions.
The principle tells us that a software component should depend on abstract interfaces of components it uses
instead of directly binding to the used compenents implementations. To completely sever the using component
from a used component, the abstract interface must provide a factory function that removes user creational
dependencies.
The code below shows how to do that.
C++
hide
/////////////////////////////////////////////////
// GenericDIP.cpp //
// - demonstrates Dep Inv Principle //
// with calculator //
// Jim Fawcett, 23 Jan 2021 //
/////////////////////////////////////////////////
/*
Demonstrates Dependency Inversion Principle:
"High level modules should not depend on
low level modules. Both should depend
on abstractions."
This demonstration builds a basic demo with
self-annunciating low level components.
- High level part: Demo<T>
- Low level parts: First, Second
- Abstraction defined in this package:
- trait Say
The definitons of First and Second could be
changed in any way that is compatible with
trait Say without affecting compilation of
Demo<T>.
*/
#include <iostream>
#include <memory>
using Byte = unsigned short;
/*-------------------------------------------
All calc classes must have these functions.
*/
template<typename T>
struct Calc {
static std::unique_ptr<Calc<T>> create();
T calc(T arg1, T arg2);
};
/*-------------------------------------------
Adds two T types
*/
template<typename T>
class Plus : public Calc<T> {
public:
static std::unique_ptr<Calc<T>> create() {
return
std::unique_ptr<Calc<T>>(new Plus);
}
T calc(T arg1, T arg2)
{
return arg1 + arg2;
}
};
/*-------------------------------------------
Multiplies two T types
*/
template<typename T>
class Times : public Calc<T> {
public:
static std::unique_ptr<Calc<T>> create() {
return
std::unique_ptr<Calc<T>>(new Times);
}
T calc(T arg1, T arg2)
{
return arg1 * arg2;
}
};
/*---------------------------------------
Demo uses any Calc type without need to
know which one. Only needs to know that
it's Calc.
Uses Calc's factory function create() to
generate an instance of calculator
function without knowing what type it's
using.
Demo is isolated from calculator's impl,
just depending on Calc interface.
U is the type of a calculator function.
T is the type of the calculation data.
*/
template<typename U, typename T>
class Demo {
public:
Demo() {
std::unique_ptr<Calc<T>> oper =
U::create();
}
T do_calc(T arg1, T arg2) {
T rslt = oper->calc(arg1, arg2);
result = rslt;
return rslt;
}
T get_result() {
return result;
}
private:
std::unique_ptr<U> oper;
T result;
};
int main() {
std::cout << "\n -- generic DIP demo --\n";
Demo<Plus<int>, int> demo1;
int tmou = demo1.do_calc(40, 2);
std::cout
<< "\n The meaning of the universe is "
<< tmou;
std::cout
<< "\n saved result: "
<< demo1.get_result();
std::cout << "\n";
Demo<Times<double>, double> demo2;
double some_number =
demo2.do_calc(42.5, 2.0);
std::cout
<< "\n some-number = "
<< some_number;
std::cout
<< "\n saved result: "
<< demo2.get_result();
std::cout << "\n\n That's all Folks!\n\n";
}
Output
-- generic DIP demo --
The meaning of the universe is 42
saved result: 42
some-number = 85
saved result: 85
That's all Folks!
Rust
unhide
/////////////////////////////////////////
// generic_dip::main.rs //
// - demonstrates Dep Inv Principle //
// with calculator //
// Jim Fawcett, 19 Jan 2021 //
/////////////////////////////////////////
/*
Demos Dependency Inversion Principle:
"High level modules should not depend
on low level modules. Both should
depend on abstractions."
This demo builds a calculator for
adding and multiplying two copy types.
- High level part: Demo<U,T>
- Low level parts: Plust<T>, Times<T>
- Abstraction defined in this package:
- Calc<T> defined here
- Abstractions defined by Rust std::libs
https://doc.rust-lang.org/beta/std/:
- std::marker::Copy
- std::ops::Add
- std::ops::Mul
- std::default::Default
The definitons of Plus<T> and Times<T>
could be changed in ways compatible with
these abstractions without affecting
compilation of Demo<U,T>.
*/
#![allow(dead_code)]
use std::ops::{Add, Mul};
use std::default::Default;
use std::marker::{Copy, PhantomData};
/*-------------------------------------------
All calc classes must have these functions.
*/
pub trait Calc<T> {
fn new() -> Self;
fn calc(arg1:T, arg2:T) -> T;
}
/*-------------------------------------------
Adds two Copy types
*/
struct Plus<T>
where T: Copy + Add + Add<Output = T> {
phantom: PhantomData<T>
}
impl<T> Calc<T> for Plus<T>
where T: Copy + Add + Add<Output = T> {
fn new() -> Self {
Plus {
phantom: PhantomData
}
}
fn calc(arg1:T, arg2:T) -> T {
arg1 + arg2
}
}
/*-------------------------------------------
Multiplies two Copy types
*/
struct Times<T>
where T: Copy + Mul + Mul<Output = T> {
phantom: PhantomData<T>
}
impl<T> Calc<T> for Times<T>
where T: Copy + Mul + Mul<Output = T> {
fn new() -> Self {
Times {
phantom: PhantomData
}
}
fn calc(arg1:T, arg2:T) -> T {
arg1 * arg2
}
}
/*---------------------------------------
Demo uses any Calc type without need to
know which one. Only needs to know that
it's Calc.
Uses Calc's factory function new() to
generate an instance of calculator
function without knowing what type its
using.
Demo is isolated from calculator's impl,
just depending on Calc and Copy traits.
U is the type of a calculator function.
T is the type of the calculation data.
*/
struct Demo<U,T>
where U: Calc<T>, T: Copy + Default {
oper: U,
result: T
}
impl<U,T> Demo<U,T>
where U: Calc<T>, T: Copy + Default {
fn new() -> Demo<U, T> {
Demo {
oper: U::new(),
result: T::default()
}
}
fn do_calc(&mut self, arg1:T, arg2:T)
-> T {
self.result = U::calc(arg1, arg2);
self.result
}
fn get_result(&self) -> T {
self.result
}
}
/*-------------------------------------------
Executive, main(), needs to have all type
information.
*/
fn main() {
let mut demo =
Demo::<Times<i32>, i32>::new();
let result = demo.do_calc(21, 2);
print!("\n Times(21, 2) = {:?}", result);
print!(
"\n saved result = {:?}",
demo.get_result()
);
println!();
let mut demo =
Demo::<Plus<f64>, f64>::new();
let result = demo.do_calc(21.0, 2.0);
print!(
"\n Plus(21.0, 2.0) = {:?}",
result
);
print!(
"\n saved result = {:?}",
demo.get_result()
);
println!("\n\n That's all Folks!\n\n");
}
Output
C:\...\DepInvPrinciple\CalcDemo>
cargo run -q
Times(21, 2) = 42
saved result = 42
Plus(21.0, 2.0) = 23.0
saved result = 23.0
That's all Folks!
C#
hide
/////////////////////////////////////////
// generic_dip::Program.cs //
// - demonstrates Dep Inv Principle //
// with calculator //
// Jim Fawcett, 19 Jan 2021 //
/////////////////////////////////////////
/*
C# is not expressive enough to implement
this demo, as defined here:
-----------------------------------------
Demos Dependency Inversion Principle:
"High level modules should not depend
on low level modules. Both should
depend on abstractions."
This demo builds a calculator for
adding and multiplying two copy types.
- High level part: Demo
- Low level parts: Plus, Times
- Abstraction to be defined in this
package:
- Calc
-----------------------------------------
The issue is that C# does an eager
syntax check for generic arguments.
It will not compile a generic
application unless the generic
parameter(s) are constrained to support
all operations needed. Calc needs
add and multiply operators, but
operator+ and operator- cannot be
overloaded for generic types.
This would not be needed if there was
some way to constrain the types to be
numeric, but C# doesn't support that.
C++ can implement the demo because it
does a lazy syntax check of the generic
code. It only fails to compile if we
instantiate the demo with a type that
does not implement operator+ and
operator-.
Rust can implement the demo because,
even though it does an eager syntax
check, it provides constraints in the
form of the Add and Mul traits, so
compilation succeeds as long as the
instantiated type supports those
constraints.
C# does not have equivalent constraints
so the generic code fails to compile
before we get a chance to instantiate
with a type that would otherwise work.
*/
using System;
class Program
{
static void Main(string[] args)
{
Console.Write(
"\n\n Out of luck!\n\n"
);
}
}
}
4. Epilogue
The following pages provide sequences of code examples for idioms and principles in each of the three languages
cited here, e.g. C#, C++, and Rust. Object model differences will often be pointed out in comments within the
code blocks.