Value vs. Entity Objects
Entities
- have a unique identity
- even if they have the same data, so long as the objects are distinct, they are considered different entities
- have lifespans / continuity; can be changed over time and over implementations
- generally rely on references or pointers
- Examples:
- physical objects: airplane, runway, taxiway, ...
- people: passenger, booking agent, ...
- records: customer information, boarding pass, flight schedule, ...
- transactions: reservations, cancellations, receipts, ...
Values
- are simply data, and are immutable (eg: Java strings)
- immutability is faked in C++
- return a new object by value and over-write (or
std::move
) of contents to over-write rater than modifythis
.
- return a new object by value and over-write (or
- immutability is faked in C++
- if the data is the same, we consider two distinct values to be the same
- Examples:
- mathematical types: rational numbers, polynomials, matrices, ...
- measurements: size, distance, weight, mass, energy, duration, ...
- other quantities: money
- other properties: colour, location, date, time, ...
- restricted value sets: names, addresses, postal codes, number ranges, ...
Quick example:
You ware implementing a videogame version of a card game. Which classes are Entity-based ADTs, and which are Value-based ADTs?**
Class | Entitiy | Value |
---|---|---|
Score | X | |
Player | X | |
Hand | X | |
Deck | X | |
Card | ? | X |
Design of Entity ADTs
Operations on Entity ADTs should reflect a real-world event:
- Copying an entity is usually no meaningful, since programs no longer reflect reality. Operations on copies are uncoordinated and can be lost (when copies disappear)
- prohibit copy constructor*
- prohibit assignment*
- prohibit type conversions*
- avoid equality
- clone operation may be useful
- Computations on entities are generally not meaningful
- think twice before overloading any operators aside from
new
anddelete
operator<
might be useful to overload as a way to apply to an entity's name or unique ID
- think twice before overloading any operators aside from
* by prohibit we actually mean using the new delete
keyword to remove these operations from the class. Eg:
class X {
public:
X(const X&) = delete;
X&operator = (const X&) = delete;
} // valid C++11 and up
Design of Value ADTs
Equality is important in value types:
- quality and other comparison operators are valid and useful
- copy constructor should exist
- assignment operator should exist
Computations involving values might make sense
- consider overloading arithmetic operators
Virtual function and inheritance are generally uncommon (tend to use final
on the class)
Mutable Objects
Mutable Value-based ADTs (e.g: Date) are problematic when they can be referenced from two variables.
You can run into errors like this:
Person myPerson ( "David O'Leary", new Date(1, "May", 1990) );
cout << myPerson.DOB() << endl; // assume implemented properly
Date myDate = myPerson.DOB();
myDate.monthIs( myDate.month() + 1 );
cout << myPerson.DOB() << endl; // oof!
Q: How do we make something immutable?
- remove all mutators
- make data private
- make sure class cannot be derived from (using
final
), or make some methods on the classfinal
- Whenever revieving or returning a ref/ptr, just make a copy
In C++, fake immutability via deep copy + overwriting original object data via operator=
Singleton Design Pattern
Ensures that exactly one object of our ADT exists.
class Egg {
static Egg e; // singleton instance
int i; // data member
Egg(int ii) : i(ii) {} // private constructor
public:
static Egg* instance() { return &e; }
int val() const { return i; }
Egg(const Egg&) = delete; // prevent copy
Egg& operator= (const Egg&) = delete; // prevent assign
};
Egg Egg::e(42); // initialization of singleton
Exposed Implementation and the PImpl Idiom
Generally, it's not ideal to have a implementation exposed to client. So, the code below is less than ideal.
class Rational {
public:
Rational (int numer = 0, int denom = 1);
int numerator() const;
int denominator() const;
private:
int numerator_;
int denominator_;
};
Using the PImpl Idiom let's us hide the data-implementation of a class from client code.
We simply encapsulate the data representation in a nested private structure (which can be in a separate file).
class Rational {
public:
Rational (int numer = 0, int denom = 1);
int numerator() const;
int denominator() const;
private:
struct Impl;
Impl* rat_;
public:
~Rational();
Rational ( const Rational& );
Rational& operator= ( const Rational& );
};
// later
struct Rational::Impl {
int numerator_;
int denominator_;
public:
Impl(int n, int d): numerator_{n}, denominator_{d} {}
};
// what the Rational constructor might then look like
Rational::Rational(int n, int d):
rat_{new Rational::Impl{n, d}} {
if (d == 0) throw "Panic! denominator == 0";
reduce();
// ...
}
// must also implement a destructor for Rational that deletes it's Impl
Note: Rational
must have pointer to Impl
, as we want the precise structure of Impl
to be separate from the Rational
class. By using a pointer, the compiler doesn't need to know Impl
's precise structure, just that we want a pointer to it.