Identity Map (Карта присутствия / Карта соответствия)

Паттерн проектирования Identity Map
Описание Identity Map
Обеспечивает однократную загрузку объекта, сохраняя данные об объекте в карте соответствия. При обращении к объектам, ищет их в карте соответсвия.
Одна старая пословица постулирует, что человек с двумя часами никогда не знает, сколько сейчас времени. И если уж двое часов вносят путаницу, то с загрузкой объектов из БД может получиться гораздо большая путаница. Если разработчик не достаточно аккуратен, может получиться, что он загрузит данные из БД в два объекта. Потом, когда он сохранит их, получится путаница и конкуренция различных данных.
Более того, с этим связаны проблемы производительности. Когда дважды загружается одна и та же информация, увеличиваются затраты на передачу данных. Таким образом, отказ от загрузки одних и тех же данных дважды не только обеспечивает корректность информации, но и ускоряет работу приложения.
Паттерн Identity Map (Карта присутствия / Карта соответствия) хранит записи о всех объектах, которые были считаны из БД за время выполнения одного действия. Когда происходит обращение к объекту, проверяется карта соответствия (присутствия), чтобы узнать, загружен ли объект.
Примеры реализации
// Identity Map Pattern in JavaScript
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
}
class IdentityMap {
constructor() {
this.map = new Map();
}
get(id) {
return this.map.get(id);
}
put(id, obj) {
this.map.set(id, obj);
}
has(id) {
return this.map.has(id);
}
remove(id) {
return this.map.delete(id);
}
clear() {
this.map.clear();
}
}
class UserRepository {
constructor() {
this.identityMap = new IdentityMap();
}
findById(id) {
// Check identity map first
if (this.identityMap.has(id)) {
console.log(`User ${id} found in identity map`);
return this.identityMap.get(id);
}
// Load from database
console.log(`Loading user ${id} from database`);
const user = this.loadFromDatabase(id);
// Store in identity map
this.identityMap.put(id, user);
return user;
}
loadFromDatabase(id) {
// Simulate database load
return new User(id, `User ${id}`, `user${id}@example.com`);
}
}
// Usage
const repo = new UserRepository();
const user1 = repo.findById(1); // Loads from DB
const user2 = repo.findById(1); // Loads from identity map
console.log(user1 === user2); // true
// Identity Map Pattern in C++
#include <iostream>
#include <unordered_map>
#include <memory>
class User {
private:
int id;
std::string name;
std::string email;
public:
User(int id, const std::string& name, const std::string& email)
: id(id), name(name), email(email) {}
int getId() const { return id; }
std::string getName() const { return name; }
std::string getEmail() const { return email; }
};
class IdentityMap {
private:
std::unordered_map<int, std::shared_ptr<User>> map;
public:
std::shared_ptr<User> get(int id) {
auto it = map.find(id);
return (it != map.end()) ? it->second : nullptr;
}
void put(int id, std::shared_ptr<User> user) {
map[id] = user;
}
bool has(int id) const {
return map.find(id) != map.end();
}
void remove(int id) {
map.erase(id);
}
void clear() {
map.clear();
}
};
class UserRepository {
private:
IdentityMap identityMap;
public:
std::shared_ptr<User> findById(int id) {
// Check identity map first
if (identityMap.has(id)) {
std::cout << "User " << id << " found in identity map" << std::endl;
return identityMap.get(id);
}
// Load from database
std::cout << "Loading user " << id << " from database" << std::endl;
auto user = loadFromDatabase(id);
// Store in identity map
identityMap.put(id, user);
return user;
}
private:
std::shared_ptr<User> loadFromDatabase(int id) {
return std::make_shared<User>(id, "User " + std::to_string(id),
"user" + std::to_string(id) + "@example.com");
}
};
// Identity Map Pattern in Go
package main
import "fmt"
type User struct {
ID int
Name string
Email string
}
type IdentityMap struct {
map map[int]*User
}
func NewIdentityMap() *IdentityMap {
return &IdentityMap{
map: make(map[int]*User),
}
}
func (im *IdentityMap) Get(id int) *User {
return im.map[id]
}
func (im *IdentityMap) Put(id int, user *User) {
im.map[id] = user
}
func (im *IdentityMap) Has(id int) bool {
_, exists := im.map[id]
return exists
}
func (im *IdentityMap) Remove(id int) {
delete(im.map, id)
}
func (im *IdentityMap) Clear() {
im.map = make(map[int]*User)
}
type UserRepository struct {
identityMap *IdentityMap
}
func NewUserRepository() *UserRepository {
return &UserRepository{
identityMap: NewIdentityMap(),
}
}
func (r *UserRepository) FindByID(id int) *User {
// Check identity map first
if r.identityMap.Has(id) {
fmt.Printf("User %d found in identity map\n", id)
return r.identityMap.Get(id)
}
// Load from database
fmt.Printf("Loading user %d from database\n", id)
user := r.loadFromDatabase(id)
// Store in identity map
r.identityMap.Put(id, user)
return user
}
func (r *UserRepository) loadFromDatabase(id int) *User {
return &User{
ID: id,
Name: fmt.Sprintf("User %d", id),
Email: fmt.Sprintf("user%d@example.com", id),
}
}
// Usage
func main() {
repo := NewUserRepository()
user1 := repo.FindByID(1) // Loads from DB
user2 := repo.FindByID(1) // Loads from identity map
fmt.Println(user1 == user2) // true
}
# Identity Map Pattern in Python
class User:
def __init__(self, user_id, name, email):
self.id = user_id
self.name = name
self.email = email
class IdentityMap:
def __init__(self):
self._map = {}
def get(self, id):
return self._map.get(id)
def put(self, id, obj):
self._map[id] = obj
def has(self, id):
return id in self._map
def remove(self, id):
return self._map.pop(id, None)
def clear(self):
self._map.clear()
class UserRepository:
def __init__(self):
self.identity_map = IdentityMap()
def find_by_id(self, id):
# Check identity map first
if self.identity_map.has(id):
print(f"User {id} found in identity map")
return self.identity_map.get(id)
# Load from database
print(f"Loading user {id} from database")
user = self._load_from_database(id)
# Store in identity map
self.identity_map.put(id, user)
return user
def _load_from_database(self, id):
return User(id, f"User {id}", f"user{id}@example.com")
# Usage
if __name__ == "__main__":
repo = UserRepository()
user1 = repo.find_by_id(1) # Loads from DB
user2 = repo.find_by_id(1) # Loads from identity map
print(user1 is user2) # True
<?php
// Identity Map Pattern in PHP
class User {
private $id;
private $name;
private $email;
public function __construct($id, $name, $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
public function getId() { return $this->id; }
public function getName() { return $this->name; }
public function getEmail() { return $this->email; }
}
class IdentityMap {
private $map = [];
public function get($id) {
return $this->map[$id] ?? null;
}
public function put($id, $obj) {
$this->map[$id] = $obj;
}
public function has($id) {
return isset($this->map[$id]);
}
public function remove($id) {
unset($this->map[$id]);
}
public function clear() {
$this->map = [];
}
}
class UserRepository {
private $identityMap;
public function __construct() {
$this->identityMap = new IdentityMap();
}
public function findById($id) {
// Check identity map first
if ($this->identityMap->has($id)) {
echo "User $id found in identity map\n";
return $this->identityMap->get($id);
}
// Load from database
echo "Loading user $id from database\n";
$user = $this->loadFromDatabase($id);
// Store in identity map
$this->identityMap->put($id, $user);
return $user;
}
private function loadFromDatabase($id) {
return new User($id, "User $id", "user$id@example.com");
}
}
// Usage
$repo = new UserRepository();
$user1 = $repo->findById(1); // Loads from DB
$user2 = $repo->findById(1); // Loads from identity map
var_dump($user1 === $user2); // true
?>
Использована иллюстрация с сайта Мартина Фаулера из статьи