Idioms and Patterns Story
Generic DIP
Home  Repo
Top, Bottom

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.