Using relational model (keys) to make references to other objects, good or bad idea?
Question: In my previous job, most of program processing relied on persistent data
stored on a DB.

So the DB data model leaded the runtime programs data structures. Thus it was
very convenient for us to use primary keys values as references to others
objects.

**For example :**

1 - Considering we have a company that sell goods such as books to customers
via Internet. We have three classes : Book, Order and Customer.

  * The Book class contains different information about the book and also a unique identifier such as ISBN number.

  * Customer class contains all data (and generally much more) the company needs to know in order to ship the books to theirs customers such as the email address. Customer objects have also a unique persistent id that identify them.

  * Thus the Order class contains two relational references `int isbn;` (the book id) and `int customer_id;`

In this example case, order class methods do not need to access customer data
or book data, so order class don't need to depend from them.

2 - If we now consider another class that is used to write and send the email
confirmation of an order :

    
    
    class OrderMailer
    {
       // Customer index
       std::map<int, Customer *> customers;
    ...
       // we have a function that send email with low level parameters
       void sendEmail(const std::string& mailAddress, const std::string& body);
    
       // and we have another method that simply sends the email for a given order
       void sendEmail(const Order& order);
    };
    

The sendEmail(const Order& order) method will need to get the customer email
adress, thus it will need to get the object from its identifier.

Thats why we have a map and then the address will be accessed like that

    
    
    const std::string& target = customers[order.customer_id]->emailAddress; // not found test omitted for reading.
    

Thats the idea.

**The question part :**

I used this way of referencing during several year without asking myself "Is
it really a good idea" because :

  * object/record ids were the way to identify object in the company

  * such persistent ids where used everywhere (code, logs, discussion with other ITs)

  * runtime data structures always reflected DB data model (it may not be a solid argument but that was very helpful when switching between DB world and runtime world(c++, python, js))

I'm not anymore in this company but I kept this way of programming when
dealing with persistent records and I'm not sure I'm doing the things right.

What have we done?

We used a logical way of referencing objects instead of using those provided
by language : pointers or c++ references. It sounds really bad to me when said
like that.

Here is a list of pro and cons for using this "method" from my point of view :

  * Pros :

    * If there is an underlying relational data model, the runtime data structures reflect the data model and things are easier to understand
    * This avoid useless coupling of classes (In the example, Order does not depends on Customer and Book classes)
    * Unique identifiers can be strings that are very human friendly to read
  * Cons :

    * Why not using the basic fundamental features of the language that are pointers and references to do this? thats sounds a bad approach.
    * We need to use dictionaries/index (maps) every-time we want to access data of logically referenced objects

As said, i'm not sure this is a good thing to use logical/relational
references. Is there some rules that can be applied in order to decide using
or not this approach? I will be happy to have your opinion about this.


Answer: Well this is how I would do it. Since this is C++ and if we use standard
containers. Why not use iterators. I added some printing code for easy testing
also.

If you want iterator stability change the vector to a list. For better lookup
operations have a look at `boost::multi_index`
<http://www.boost.org/doc/libs/1_53_0/libs/multi_index/doc/index.html>.
`boost::multi_index` is also good to use if iterator stability is important.

It would probably also be easy to use `boost::serialization` if you want to
write your data structures to file.

    
    
    #include <string>
    #include <iostream>
    #include <vector>
    
    struct Streamable
    {
      virtual ~Streamable() = default;
      virtual std::ostream& print(std::ostream& ost) const = 0;
    };
    
    std::ostream& operator<<(std::ostream& ost, const Streamable& str)
    {
      return str.print(ost);
    }
    
    struct Customer : public Streamable
    {
      std::string name_m;
      Customer(const std::string& name_):name_m(name_)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        return ost<<name_m;
      }
    };
    
    struct CustomerColl : public std::vector<Customer>,
                  public Streamable
    {
      CustomerColl(std::initializer_list<Customer> customers)
        :std::vector<Customer>(customers)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        ost<<"Customers:"<<std::endl;
        for(const auto& c: *this)
        {
          ost<<c<<std::endl;
        }
        return ost;
      }
    };
    
    struct Book : public Streamable
    {
      std::string title_m;
      unsigned int isbn_m;
    
      Book(const std::string& title_, const unsigned int& isbn_)
        :title_m(title_),isbn_m(isbn_)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        return ost<<title_m<<":{"<<isbn_m<<"}";
      }
    };
    
    struct BookColl : public std::vector<Book>,
              public Streamable
    {
      BookColl(std::initializer_list<Book> books)
        :std::vector<Book>(books)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        ost<<"Books:"<<std::endl;
        for(const auto& b: *this)
        {
          ost<<b<<std::endl;
        }
        return ost;
      }
    };
    
    struct Order : public Streamable
    {
      BookColl::const_iterator book_m;
      CustomerColl::const_iterator customer_m;
    
      Order(const BookColl::const_iterator& book_,
        const CustomerColl::const_iterator& customer_)
        :book_m(book_),customer_m(customer_)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        return ost<<"["<<*customer_m<<"->"<<*book_m<<"]";
      }
    };
    
    struct OrderColl : public std::vector<Order>,
               public Streamable
    {
      OrderColl(std::initializer_list<Order> orders)
        :std::vector<Order>(orders)
      {}
      std::ostream& print(std::ostream& ost) const
      {
        ost<<"Orders:"<<std::endl;
        for(const auto& o: *this)
        {
          ost<<o<<std::endl;
        }
        return ost;
      }
    };
    
    int main()
    {
      CustomerColl customers{{"Anna"},{"David"},{"Lisa"}};
      BookColl books{{"C++",123},{"Java",234},{"Lisp",345}};
    
      OrderColl orders{{books.begin(),customers.begin()},{books.begin(),customers.begin()+1},{books.end()-1,customers.begin()+2}};
    
      std::cout<<customers<<std::endl;
      std::cout<<books<<std::endl;
      std::cout<<orders<<std::endl;
    
      return 0;
    }
    

