design patterns in modern c++
TRANSCRIPT
Design Patterns inModern C++, Part I
Dmitri Nesterukdmitrinеstеruk@gmаil.соm
@dnesteruk
What’s In This Talk?
• Examples of patterns and approaches in OOP design• Adapter• Composite• Specification pattern/OCP• Fluent and Groovy-style builders• Maybe monad
Adapter
STL String Complaints
• Making a string is easystring s{“hello world”};
• Getting its constituent parts is notvector<strings> words;boost::split(words, s, boost::is_any_of(“ “));
• Instead I would preferauto parts = s.split(“ “);
• It should work with “hello world”• Maybe some other goodies, e.g.
• Hide size()• Have length() as a property, not a function
Basic Adapter
class String {string s;
public:String(const string &s) : s{ s } { }
};
Implement Splitclass String {
string s;public:
String(const string &s) : s{ s } { }vector<string> split(string input){
vector<string> result;boost::split(result, s,
boost::is_any_of(input), boost::token_compress_on);return result;
}};
Length Proxying
class String {string s;
public:String(const string &s) : s{ s } { }vector<string> split(string input);size_t get_length() const { return s.length(); }
};
Length Proxying
class String {string s;
public:String(const string &s) : s{ s } { }vector<string> split(string input);size_t get_length() const { return s.length(); }
// non-standard!__declspec(property(get = get_length)) size_t length;
};
String Wrapper Usage
String s{ "hello world" };cout << "string has " << s.length << " characters" << endl;
auto words = s.split(" ");for (auto& word : words)cout << word << endl;
Adapter Summary
• Aggregate objects (or keep a reference)• Can aggregate more than one
• E.g., string and formatting
• Replicate the APIs you want (e.g., length)• Miss out on the APIs you don’t need• Add your own features :)
Composite
Scenario
• Neurons connect to other neurons
• Neuron layers are collections of neurons
• These two need to be connectable
Scenariostruct Neuron{
vector<Neuron*> in, out;unsigned int id;
Neuron(){
static int id = 1;this->id = id++;
}}
Scenario
struct NeuronLayer : vector<Neuron>{NeuronLayer(int count){while (count-- > 0)emplace_back(Neuron{});
}}
State Space Explosition
• void connect_to(Neuron& other){out.push_back(&other);other.in.push_back(this);
}• Unfortunately, we need 4 functions
• Neuron to Neuron• Neuron to NeuronLayer• NeuronLayer to Neuron• NeuronLayer to NeuronLayer
One Function Solution?
• Simple: treat Neuron as NeuronLayer of size 1• Not strictly correct• Does not take into account other concepts (e.g., NeuronRing)
• Better: expose a single Neuron in an iterable fashion• Other programming languages have interfaces for iteration
• E.g., C# IEnumerable<T>• yield keyword
• C++ does duck typing• Expects begin/end pair
• One function solution not possible, but…
Generic Connection Functionstruct Neuron{
...template <typename T> void connect_to(T& other){
for (Neuron& to : other)connect_to(to);
}template<> void connect_to<Neuron>(Neuron& other){
out.push_back(&other);other.in.push_back(this);
}};
Generic Connection Function
struct NeuronLayer : vector<Neuron>{…template <typename T> void connect_to(T& other){for (Neuron& from : *this)for (Neuron& to : other)from.connect_to(to);
}};
How to Iterate on a Single Value?
struct Neuron{…Neuron* begin() { return this; }Neuron* end() { return this + 1; }
};
API Usage
Neuron n, n2;NeuronLayer nl, nl2;
n.connect_to(n2);n.connect_to(nl);nl.connect_to(n);nl.connect_to(nl2);
Specification Pattern and the OCP
Open-Closed Principle
• Open for extension, closed for modification• Bad: jumping into old code to change a stable, functioning system• Good: making things generic enough to be externally extensible• Example: product filtering
Scenario
enum class Color { Red, Green, Blue };enum class Size { Small, Medium, Large };
struct Product{std::string name;Color color;Size size;
};
Filtering Productsstruct ProductFilter{
typedef std::vector<Product*> Items;static Items by_color(Items items, Color color){
Items result;for (auto& i : items)
if (i->color == color)result.push_back(i);
return result;}
}
Filtering Productsstruct ProductFilter{
typedef std::vector<Product*> Items;static Items by_color(Items items, Color color) { … }static Items by_size(Items items, Size size){
Items result;for (auto& i : items)
if (i->size == size)result.push_back(i);
return result;}
}
Filtering Productsstruct ProductFilter{
typedef std::vector<Product*> Items;static Items by_color(Items items, Color color) { … }static Items by_size(Items items, Size size) { … }static Items by_color_and_size(Items items, Size size, Color color){Items result;for (auto& i : items)
if (i->size == size && i->color == color)result.push_back(i);
return result;}
}
Violating OCP
• Keep having to rewrite existing code• Assumes it is even possible (i.e. you have access to source code)
• Not flexible enough (what about other criteria?)• Filtering by X or Y or X&Y requires 3 functions
• More complexity -> state space explosion
• Specification pattern to the rescue!
ISpecification and IFilter
template <typename T> struct ISpecification{virtual bool is_satisfied(T* item) = 0;
};template <typename T> struct IFilter{virtual std::vector<T*> filter(std::vector<T*> items, ISpecification<T>& spec) = 0;
};
A Better Filterstruct ProductFilter : IFilter<Product>{
typedef std::vector<Product*> Products;Products filter(
Products items, ISpecification<Product>& spec) override
{Products result;for (auto& p : items)
if (spec.is_satisfied(p))result.push_back(p);
return result;}
};
Making Specifications
struct ColorSpecification : ISpecification<Product>{Color color;explicit ColorSpecification(const Color color): color{color} { }
bool is_satisfied(Product* item) override {return item->color == color;
}}; // same for SizeSpecification
Improved Filter Usage
Product apple{ "Apple", Color::Green, Size::Small };Product tree { "Tree", Color::Green, Size::Large };Product house{ "House", Color::Blue, Size::Large };
std::vector<Product*> all{ &apple, &tree, &house };
ProductFilter pf;ColorSpecification green(Color::Green);
auto green_things = pf.filter(all, green);for (auto& x : green_things)std::cout << x->name << " is green" << std::endl;
Filtering on 2..N criteria
• How to filter by size and color?• We don’t want a SizeAndColorSpecification
• State space explosion
• Create combinators• A specification which combines two other specifications• E.g., AndSpecification
AndSpecification Combinatortemplate <typename T> struct AndSpecification : ISpecification<T>{
ISpecification<T>& first;ISpecification<T>& second;
AndSpecification(ISpecification<T>& first, ISpecification<T>& second)
: first{first}, second{second} { }
bool is_satisfied(T* item) override{
return first.is_satisfied(item) && second.is_satisfied(item);}
};
Filtering by Size AND Color
ProductFilter pf;ColorSpecification green(Color::Green);SizeSpecification big(Size::Large);AndSpecification<Product> green_and_big{ big, green };
auto big_green_things = pf.filter(all, green_and_big);for (auto& x : big_green_things)
std::cout << x->name << " is big and green" << std::endl;
Specification Summary
• Simple filtering solution is• Too difficult to maintain, violates OCP• Not flexible enough
• Abstract away the specification interface• bool is_satisfied_by(T something)
• Abstract away the idea of filtering• Input items + specification set of filtered items
• Create combinators (e.g., AndSpecification) for combining multiple specifications
Fluent and Groovy-Style Builders
Scenario
• Consider the construction of structured data• E.g., an HTML web page
• Stuctured and formalized• Rules (e.g., P cannot contain another P)• Can we provide an API for building these?
Building a Simple HTML List
// <ul><li>hello</li><li>world</li></ul>string words[] = { "hello", "world" };ostringstream oss;oss << "<ul>";for (auto w : words)
oss << " <li>" << w << "</li>";oss << "</ul>";printf(oss.str().c_str());
HtmlElement
struct HtmlElement{
string name;string text;vector<HtmlElement> elements;const size_t indent_size = 2;
string str(int indent = 0) const; // pretty-print}
Html Builder (non-fluent)
struct HtmlBuilder{
HtmlElement root;HtmlBuilder(string root_name) { root.name = root_name; }void add_child(string child_name, string child_text){
HtmlElement e{ child_name, child_text };root.elements.emplace_back(e);
}string str() { return root.str(); }
}
Html Builder (non-fluent)
HtmlBuilder builder{"ul"};builder.add_child("li", "hello")builder.add_child("li", "world");cout << builder.str() << endl;
Making It Fluent
struct HtmlBuilder{
HtmlElement root;HtmlBuilder(string root_name) { root.name = root_name; }HtmlBuilder& add_child(string child_name, string child_text){
HtmlElement e{ child_name, child_text };root.elements.emplace_back(e);return *this;
}string str() { return root.str(); }
}
Html Builder
HtmlBuilder builder{"ul"};builder.add_child("li", "hello").add_child("li", "world");cout << builder.str() << endl;
Associate Builder & Object Being Built
struct HtmlElement{
static HtmlBuilder build(string root_name){
return HtmlBuilder{root_name};}
};
// usage:HtmlElement::build("ul")
.add_child_2("li", "hello").add_child_2("li", "world");
Groovy-Style Builders
• Express the structure of the HTML in code• No visible function calls• UL {
LI {“hello”},LI {“world”}
}• Possible in C++ using uniform initialization
Tag (= HTML Element)
struct Tag{
string name;string text;vector<Tag> children;vector<pair<string, string>> attributes;
protected:Tag(const std::string& name, const std::string& text)
: name{name}, text{text} { }Tag(const std::string& name, const std::vector<Tag>& children)
: name{name}, children{children} { }}
Paragraph
struct P : Tag{
explicit P(const std::string& text): Tag{"p", text}
{}P(std::initializer_list<Tag> children): Tag("p", children)
{}
};
Image
struct IMG : Tag{
explicit IMG(const std::string& url): Tag{"img", ""}
{attributes.emplace_back(make_pair("src", url));
}};
Example Usage
std::cout <<
P {IMG {"http://pokemon.com/pikachu.png"}
}
<< std::endl;
Facet Builders
• An HTML element has different facets• Attributes, inner elements, CSS definitions, etc.
• A Person class might have different facets• Address• Employment information
• Thus, an object might necessitate several builders
Personal/Work Information
class Person{
// addressstd::string street_address, post_code, city;
// employmentstd::string company_name, position;int annual_income = 0;
Person() {} // private!}
Person Builder (Exposes Facet Builders)class PersonBuilder{
Person p;protected:
Person& person;explicit PersonBuilder(Person& person)
: person{ person } { }public:
PersonBuilder() : person{p} { }operator Person() { return std::move(person); }
// builder facetsPersonAddressBuilder lives();PersonJobBuilder works();
}
Person Builder (Exposes Facet Builders)class PersonBuilder{
Person p;protected:
Person& person;explicit PersonBuilder(Person& person)
: person{ person } { }public:
PersonBuilder() : person{p} { }operator Person() { return std::move(person); }
// builder facetsPersonAddressBuilder lives();PersonJobBuilder works();
}
Person Builder Facet Functions
PersonAddressBuilder PersonBuilder::lives(){
return PersonAddressBuilder{ person };}
PersonJobBuilder PersonBuilder::works(){
return PersonJobBuilder{ person };}
Person Address Builderclass PersonAddressBuilder : public PersonBuilder{
typedef PersonAddressBuilder Self;public:
explicit PersonAddressBuilder(Person& person) : PersonBuilder{ person } { }
Self& at(std::string street_address){
person.street_address = street_address;return *this;
}Self& with_postcode(std::string post_code);Self& in(std::string city);
};
Person Job Builder
class PersonJobBuilder : public PersonBuilder{
typedef PersonJobBuilder Self;public:
explicit PersonJobBuilder(Person& person) : PersonBuilder{ person } { }
Self& at(std::string company_name);Self& as_a(std::string position);Self& earning(int annual_income);
};
Back to Person
class Person{
// fieldspublic:
static PersonBuilder create();
friend class PersonBuilder;friend class PersonAddressBuilder;friend class PersonJobBuilder;
};
Final Person Builder Usage
Person p = Person::create().lives().at("123 London Road")
.with_postcode("SW1 1GB")
.in("London").works().at("PragmaSoft")
.as_a("Consultant")
.earning(10e6);
Maybe Monad
Presence or Absence
• Different ways of expressing absence of value• Default-initialized value
• string s; // there is no ‘null string’
• Null value• Address* address;
• Not-yet-initialized smart pointer• shared_ptr<Address> address
• Idiomatic• boost::optional
Monads
• Design patterns in functional programming• First-class function support• Related concepts
• Algebraic data types• Pattern matching
• Implementable to some degree in C++• Functional objects/lambdas
Scenario
struct Address{char* house_name; // why not string?
}
struct Person{Address* address;
}
Print House Name, If Any
void print_house_name(Person* p){if (p != nullptr &&
p->address != nullptr && p->address->house_name != nullptr)
{cout << p->address->house_name << endl;
}}
Maybe Monad
• Encapsulate the ‘drill down’ aspect of code• Construct a Maybe<T> which keeps context• Context: pointer to evaluated element
• person -> address -> name
• While context is non-null, we drill down• If context is nullptr, propagation does not happen• All instrumented using lambdas
Maybe<T>template <typename T>struct Maybe {
T* context;Maybe(T *context) : context(context) { }
};
// but, given Person* p, we cannot make a ‘new Maybe(p)’
template <typename T> Maybe<T> maybe(T* context){
return Maybe<T>(context);}
Usage So Far
void print_house_name(Person* p){maybe(p). // now drill down :)
}
Maybe::Withtemplate <typename T> struct Maybe{
...template <typename TFunc>auto With(TFunc evaluator){
if (context == nullptr)return ??? // cannot return maybe(nullptr) :(
return maybe(evaluator(context));};
}
What is ???
• In case of failure, we need to return Maybe<U>• But the type of U should be the return type of evaluator• But evaluator returns U* and we need U• Therefore…• return Maybe<
typename remove_pointer<decltype(evaluator(context))
>::type>(nullptr);
Maybe::With Finished
template <typename TFunc> auto With(TFunc evaluator){
if (context == nullptr)return Maybe<typename remove_pointer<
decltype(evaluator(context))>::type>(nullptr);return maybe(evaluator(context));
};
Usage So Far
void print_house_name(Person* p){maybe(p) // now drill down :).With([](auto x) { return x->address; }).With([](auto x) { return x->house_name; }). // print here (if context is not null)
}
Maybe::Do
template <typename TFunc>auto Do(TFunc action){// if context is OK, perform action on it if (context != nullptr) action(context);
// no context transition, so...return *this;
}
How It Works
• print_house_name(nullptr)• Context is null from the outset and continues to be null• Nothing happens in the entire evaluation chain
• Person p; print_house_name(&p);• Context is Person, but since Address is null, it becomes null henceforth
• Person p;p->Address = new Address;p->Address->HouseName = “My Castle”;print_house_name(&p);
• Everything works and we get our printout
Maybe Monad Summary
• Example is not specific to nullptr• E.g., replace pointers with boost::optional
• Default-initialized types are harder• If s.length() == 0, has it been initialized?
• Monads are difficult due to lack of functional support• [](auto x) { return f(x); } instead ofx => f(x) as in C#
• No implicits (e.g. Kotlin’s ‘it’)
That’s It!
• Questions?• Design Patterns in C++ courses on Pluralsight• dmitrinеsteruk /at/ gmail.com• @dnesteruk
Design Patterns in Modern C++, Part II
Dmitri Nesterukdmitrinеstеruk@gmаil.соm
@dnesteruk
What’s In This Talk?
• Part II of my Design Patterns talks• Part I of the talk available online in English and Russian
• Examples of design patterns implemented in C++• Disclaimer: C++ hasn’t internalized any patterns (yet)
• Patterns• Memento• Visitor• Observer• Interpreter
MementoHelping implement undo/redo
Bank Account
• Bank account has a balance• Balance changed via
• Withdrawals• Deposits
• Want to be able to undo an erroneous transaction
• Want to navigate to an arbitrary point in the account’s changeset
class BankAccount{
int balance{0};public:
void deposit(int x) {balance += x;
}void withdraw(int x) {
if (balance >= x)balance -= x;
}};
Memento (a.k.a. Token, Cookie)
class Memento{int balance;
public:Memento(int balance): balance(balance)
{}friend class BankAccount;
};
• Keeps the state of the balance at a particular point in time• State is typically private
• Can also keep reason for latest change, amount, etc.
• Returned during bank account changes
• Memento can be used to restore object to a particular state
Deposit and Restore
Memento deposit(int amount){balance += amount;return { balance };
}void restore(const Memento& m){balance = m.balance;
}
BankAccount ba{ 100 };auto m1 = ba.deposit(50); // 150auto m2 = ba.deposit(25); // 175// undo to m1ba.restore(m1); // 150// redoba.restore(m2); // 175
Storing Changes
class BankAccount {int balance{0}, current;vector<shared_ptr<Memento>> changes;
public:BankAccount(const int balance) : balance(balance){
changes.emplace_back(make_shared<Memento>(balance));current = 0;
}};
Undo and Redo
shared_ptr<Memento> deposit(intamount){balance += amount;auto m = make_shared<Memento>(balance);
changes.push_back(m);++current;return m;
}
shared_ptr<Memento> undo(){if (current > 0){--current;auto m = changes[current];balance = m->balance;return m;
}return{};
}
Undo/Redo Problems
• Storage excess• Need to store only changes, but still…
• Need to be able to overwrite the Redo step• Undo/redo is typically an aggregate operation
Cookie Monster!
• Why doesn’t push_back() return a reference?
• Well, it could, but…• As long as you’re not resizing due
to addition• How to refer to an element of
vector<int>?• Dangerous unless append-only• Change to vector< shared_ptr<int>>• Change to list<int>• Some range-tracking magic token
solution
• Return a magic_cookie that• Lets you access an element provided
it exists• Is safe to use if element has been
deleted• Keeps pointing to the correct element
even when container is reordered• Requires tracking all mutating
operations• Is it worth it?
ObserverThis has been done to death, but…
Simple Model
class Person{int age;
public:void set_age(int age){this->age = age;
}int get_age() const{return age;
}};
• Person has an age: private field with accessor/mutator
• We want to be informedwhenever age changes
• Need to modify the setter!
Person Listener
struct PersonListener{virtual ~PersonListener() = default;virtual void person_changed(Person& p,
const string& property_name, const any new_value) = 0;};
Person Implementation
class Person{vector<PersonListener*> listeners;
public:void subscribe(PersonListener* pl) {listeners.push_back(pl);
}void notify(const string& property_name, const any new_value){for (const auto listener : listeners)listener->person_changed(*this, property_name, new_value);
}};
Setter Change
void set_age(const int age){if (this->age == age) return;this->age = age;notify("age", this->age);
}
Consumption
struct ConsoleListener : PersonListener{void person_changed(Person& p,
const string& property_name, const any new_value) override
{cout << "person's " << property_name
<< " has been changed to ";if (property_name == "age"){
cout << any_cast<int>(new_value);}cout << "\n";
}};
Person p{14};ConsoleListener cl;p.subscribe(&cl);p.set_age(15);p.set_age(16);
Dependent Property
bool get_can_vote() const{return age >= 16;
}
• Where to notify?• How to detect that any
affecting property has changed?
• Can we generalize?
Notifying on Dependent Property
void set_age(const int age){if (this->age == age) return;
auto old_c_v = get_can_vote();
this->age = age;notify("age", this->age);
auto new_c_v = get_can_vote();if (old_c_v != new_c_v){
notify("can_vote", new_c_v);}
}
save old value
get new value compare and notify only if
changed
Observer Problems
• Multiple subscriptions by a single listener• Are they allowed? If not, use std::set
• Un-subscription• Is it supported?• Behavior if many subscriptions/one listener?
• Thread safety• Reentrancy
Thread Safetystatic mutex mtx;class Person {⋮void subscribe(PersonListener* pl){lock_guard<mutex> guard{mtx};⋮
}void unsubscribe(PersonListener* pl){lock_guard<mutex> guard{mtx};for (auto it : listeners){if (*it == pl) *it = nullptr;// erase-remove in notify()
}}
};
• Anything that touches the list of subscribers is locked• Reader-writer locks better (shared_lock for
reading, unique_lock for writing)
• Unsubscription simply nulls the listener• Must check for nullptr• Remove at the end of notify()
• Alternative: use concurrent_vector• Guaranteed thread-safe addition• No easy removal
Reentrancy
struct TrafficAdministration : Observer<Person>{void field_changed(Person& source,
const string& field_name){
if (field_name == "age"){if (source.get_age() < 17)
cout << "Not old enough to drive!\n";else{
// let's not monitor them anymorecout << "We no longer care!\n";source.unsubscribe(this);
}}}};
Age changes (1617):• notify() called• Lock taken
field_changed• unsubscribe()
unsubscribe()• Tries to take a lock• But it’s already taken
Observer Problems
• Move from mutex to recursive_mutex• Doesn’t solve all problems• See Thread-safe Observer Pattern – You’re doing it Wrong (Tony Van
Eerd)
Boost.Signals2• signal<T>
• A signal that can be sent to anyone willing to listen
• T is the type of the slot function• A slot is the function that receives the
signal• Ordinary function• Functor, std::function• Lambda
• Connection• signal<void()> s;
creates a signal• auto c = s.connect([](){
cout << “test” << endl;});connects the signal to the slot
• More than one slot can be connected to a signal
• Disconnection• c.disconnect();• Disconnects all slots
• Slots can be blocked• Temporarily disabled• Used to prevent infinite recursion• shared_connection_block(c)• Unblocked when block is destroyed, or
explicitly viablock.unblock();
INotifyPropertyChanged<T>
template <typename T>struct INotifyPropertyChanged{virtual ~INotifyPropertyChanged() = default;signal<void(T&, const string&)> property_changed;
};struct Person : INotifyPropertyChanged<Person>{void set_age(const int age){if (this->age == age) return;
this->age = age;property_changed(*this, "age");}
};
Consuming INPC Model
Person p{19};p.property_changed.connect(
[](Person&, const string& prop_name){
cout << prop_name << " has been changed" << endl;});
p.set_age(20);
InterpreterMake your own programming language!
Interpreter
• Interpret textual input• A branch of computer science
• Single item: atoi, lexical_cast, etc.• Custom file format: XML, CSV• Embedded DSL: regex• Own programming language
Interpreting Numeric Expressions
(13-4)-(12+1)
Lex: [(] [13] [-] [4] [)] [-] …
Parse: Op(-, Op(-, 13, 4), Op(+,12,1))
Token
struct Token{enum Type { integer, plus, minus, lparen, rparen
} type;
string text;
explicit Token(Type type, const string& text) :type{type}, text{text} {}
};
Lexingvector<Token> lex(const string& input){
vector<Token> result;for (int i = 0; i < input.size(); ++i){
switch (input[i]){case '+':
result.push_back(Token{ Token::plus, "+" });break;
case '-':result.push_back(Token{ Token::minus, "-" });break;
case '(':result.push_back(Token{ Token::lparen, "(" });break;
case ')':result.push_back(Token{ Token::rparen, ")" });break;
default:ostringstream buffer;buffer << input[i];for (int j = i + 1; j < input.size(); ++j){
if (isdigit(input[j])){buffer << input[j];++i;
}else{result.push_back(Token{ Token::integer, buffer.str() });
break;}
}…
Parsing Structuresstruct Element{virtual ~Element() = default;virtual int eval() const = 0;
};
struct Integer : Element{int value;explicit Integer(const int value): value(value)
{}int eval() const override { return value;
}};
struct BinaryOperation : Element{enum Type { addition, subtraction } type;shared_ptr<Element> lhs, rhs;
int eval() const override{if (type == addition) return lhs->eval() + rhs->eval();
return lhs->eval() - rhs->eval();}
};shared_ptr<Element> parse(const vector<Token>& tokens){⋮
}
Parsing Numeric Expressions
string input{ "(13-4)-(12+1)" };auto tokens = lex(input);try {auto parsed = parse(tokens);cout << input << " = " << parsed->eval() << endl;
} catch (const exception& e){cout << e.what() << endl;
}
Boost.Sprit
• A Boost library for interpreting text• Uses a de facto DSL for defining the parser• Defining a separate lexer not mandatory• Favors Boost.Variant for polymorphic types• Structurally matches definitions to OOP structures
Tlön Programming Language
• Proof-of-concept language transpiled into C++• Parser + pretty printer + notepadlike app• Some language features
• Shorter principal integral types• Primary constructors• Tuples• Non-keyboard characters (keyboard-unfriendly)
Visitor
Adding Side Functionality to Classes
• C++ Committee not liking UCS• Or any other “extension function” kind of deal• Possible extensions on hierarchies end up being intrusive
• Need to modify the entire hierarchy
That’s It!
• Design Patterns in C++ courses on Pluralsight• Leanpub book (work in progress!)• Tlön Programming Language• dmitrinеsteruk /at/ gmail.com• @dnesteruk