Embedded Value (Объединённое свойство)

Паттерн проектирования Embedded Value

Паттерн проектирования Embedded Value

Описание Embedded Value

Записывает объект в несколько полей таблицы другого объекта.

Множество небольших объектов играют важную роль в объектно-ориентированной системе, но не подходят для хранения в отдельной таблице. Это, к примеру, объекты-деньги, зависящие от валюты и диапазоны дат. Не смотря на то, что обычно для хранения отдельного объекта используется отдельная таблица, ни один человек в трезвом уме не станет делать отдельную таблицу для денег.

Паттерн Embedded Value (Объединённое свойство) распределяет значения полей объекта в поля таблицы объекта-владельца.

Примеры реализации

// Embedded Value Pattern in JavaScript
class Money {
    constructor(amount, currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    getAmount() {
        return this.amount;
    }
    
    getCurrency() {
        return this.currency;
    }
    
    toString() {
        return `${this.amount} ${this.currency}`;
    }
}

class DateRange {
    constructor(startDate, endDate) {
        this.startDate = startDate;
        this.endDate = endDate;
    }
    
    getStartDate() {
        return this.startDate;
    }
    
    getEndDate() {
        return this.endDate;
    }
    
    getDuration() {
        return Math.ceil((this.endDate - this.startDate) / (1000 * 60 * 60 * 24));
    }
}

class Employment {
    constructor(company, position, salary, startDate, endDate) {
        this.company = company;
        this.position = position;
        this.salary = salary; // Embedded Money object
        this.period = new DateRange(startDate, endDate); // Embedded DateRange object
    }
    
    getCompany() {
        return this.company;
    }
    
    getPosition() {
        return this.position;
    }
    
    getSalary() {
        return this.salary;
    }
    
    getPeriod() {
        return this.period;
    }
    
    // Method to get salary as separate fields (for database storage)
    getSalaryFields() {
        return {
            salaryAmount: this.salary.getAmount(),
            salaryCurrency: this.salary.getCurrency()
        };
    }
    
    // Method to get period as separate fields (for database storage)
    getPeriodFields() {
        return {
            startDate: this.period.getStartDate(),
            endDate: this.period.getEndDate()
        };
    }
    
    // Method to create from database fields
    static fromFields(company, position, salaryAmount, salaryCurrency, startDate, endDate) {
        const salary = new Money(salaryAmount, salaryCurrency);
        return new Employment(company, position, salary, startDate, endDate);
    }
}

class Address {
    constructor(street, city, state, zipCode, country) {
        this.street = street;
        this.city = city;
        this.state = state;
        this.zipCode = zipCode;
        this.country = country;
    }
    
    getFullAddress() {
        return `${this.street}, ${this.city}, ${this.state} ${this.zipCode}, ${this.country}`;
    }
    
    // Method to get address as separate fields (for database storage)
    getAddressFields() {
        return {
            street: this.street,
            city: this.city,
            state: this.state,
            zipCode: this.zipCode,
            country: this.country
        };
    }
    
    // Method to create from database fields
    static fromFields(street, city, state, zipCode, country) {
        return new Address(street, city, state, zipCode, country);
    }
}

class Person {
    constructor(name, email, address) {
        this.name = name;
        this.email = email;
        this.address = address; // Embedded Address object
    }
    
    getName() {
        return this.name;
    }
    
    getEmail() {
        return this.email;
    }
    
    getAddress() {
        return this.address;
    }
    
    // Method to get address as separate fields (for database storage)
    getAddressFields() {
        return this.address.getAddressFields();
    }
    
    // Method to create from database fields
    static fromFields(name, email, street, city, state, zipCode, country) {
        const address = Address.fromFields(street, city, state, zipCode, country);
        return new Person(name, email, address);
    }
}

// Usage
const salary = new Money(75000, 'USD');
const period = new DateRange(new Date('2020-01-01'), new Date('2023-12-31'));
const employment = new Employment('Tech Corp', 'Software Engineer', salary, new Date('2020-01-01'), new Date('2023-12-31'));

console.log('Employment:', employment.getCompany(), employment.getPosition());
console.log('Salary:', employment.getSalary().toString());
console.log('Period:', employment.getPeriod().getDuration(), 'days');

// Get fields for database storage
const salaryFields = employment.getSalaryFields();
const periodFields = employment.getPeriodFields();
console.log('Salary fields:', salaryFields);
console.log('Period fields:', periodFields);

// Create from database fields
const employmentFromDB = Employment.fromFields(
    'Tech Corp',
    'Software Engineer',
    75000,
    'USD',
    new Date('2020-01-01'),
    new Date('2023-12-31')
);

console.log('Employment from DB:', employmentFromDB.getCompany());

// Address example
const address = new Address('123 Main St', 'New York', 'NY', '10001', 'USA');
const person = new Person('John Doe', 'john@example.com', address);

console.log('Person:', person.getName());
console.log('Address:', person.getAddress().getFullAddress());

// Get address fields for database storage
const addressFields = person.getAddressFields();
console.log('Address fields:', addressFields);

// Create person from database fields
const personFromDB = Person.fromFields('John Doe', 'john@example.com', '123 Main St', 'New York', 'NY', '10001', 'USA');
console.log('Person from DB:', personFromDB.getName());
<?php
// Embedded Value Pattern in PHP
class Money {
    private $amount;
    private $currency;
    
    public function __construct($amount, $currency) {
        $this->amount = $amount;
        $this->currency = $currency;
    }
    
    public function getAmount() {
        return $this->amount;
    }
    
    public function getCurrency() {
        return $this->currency;
    }
    
    public function __toString() {
        return $this->amount . ' ' . $this->currency;
    }
}

class DateRange {
    private $startDate;
    private $endDate;
    
    public function __construct($startDate, $endDate) {
        $this->startDate = $startDate;
        $this->endDate = $endDate;
    }
    
    public function getStartDate() {
        return $this->startDate;
    }
    
    public function getEndDate() {
        return $this->endDate;
    }
    
    public function getDuration() {
        return ceil(($this->endDate->getTimestamp() - $this->startDate->getTimestamp()) / (60 * 60 * 24));
    }
}

class Employment {
    private $company;
    private $position;
    private $salary;
    private $period;
    
    public function __construct($company, $position, $salary, $startDate, $endDate) {
        $this->company = $company;
        $this->position = $position;
        $this->salary = $salary; // Embedded Money object
        $this->period = new DateRange($startDate, $endDate); // Embedded DateRange object
    }
    
    public function getCompany() {
        return $this->company;
    }
    
    public function getPosition() {
        return $this->position;
    }
    
    public function getSalary() {
        return $this->salary;
    }
    
    public function getPeriod() {
        return $this->period;
    }
    
    // Method to get salary as separate fields (for database storage)
    public function getSalaryFields() {
        return [
            'salaryAmount' => $this->salary->getAmount(),
            'salaryCurrency' => $this->salary->getCurrency()
        ];
    }
    
    // Method to get period as separate fields (for database storage)
    public function getPeriodFields() {
        return [
            'startDate' => $this->period->getStartDate(),
            'endDate' => $this->period->getEndDate()
        ];
    }
    
    // Method to create from database fields
    public static function fromFields($company, $position, $salaryAmount, $salaryCurrency, $startDate, $endDate) {
        $salary = new Money($salaryAmount, $salaryCurrency);
        return new Employment($company, $position, $salary, $startDate, $endDate);
    }
}

class Address {
    private $street;
    private $city;
    private $state;
    private $zipCode;
    private $country;
    
    public function __construct($street, $city, $state, $zipCode, $country) {
        $this->street = $street;
        $this->city = $city;
        $this->state = $state;
        $this->zipCode = $zipCode;
        $this->country = $country;
    }
    
    public function getFullAddress() {
        return $this->street . ', ' . $this->city . ', ' . $this->state . ' ' . $this->zipCode . ', ' . $this->country;
    }
    
    // Method to get address as separate fields (for database storage)
    public function getAddressFields() {
        return [
            'street' => $this->street,
            'city' => $this->city,
            'state' => $this->state,
            'zipCode' => $this->zipCode,
            'country' => $this->country
        ];
    }
    
    // Method to create from database fields
    public static function fromFields($street, $city, $state, $zipCode, $country) {
        return new Address($street, $city, $state, $zipCode, $country);
    }
}

class Person {
    private $name;
    private $email;
    private $address;
    
    public function __construct($name, $email, $address) {
        $this->name = $name;
        $this->email = $email;
        $this->address = $address; // Embedded Address object
    }
    
    public function getName() {
        return $this->name;
    }
    
    public function getEmail() {
        return $this->email;
    }
    
    public function getAddress() {
        return $this->address;
    }
    
    // Method to get address as separate fields (for database storage)
    public function getAddressFields() {
        return $this->address->getAddressFields();
    }
    
    // Method to create from database fields
    public static function fromFields($name, $email, $street, $city, $state, $zipCode, $country) {
        $address = Address::fromFields($street, $city, $state, $zipCode, $country);
        return new Person($name, $email, $address);
    }
}

// Usage
$salary = new Money(75000, 'USD');
$period = new DateRange(new DateTime('2020-01-01'), new DateTime('2023-12-31'));
$employment = new Employment('Tech Corp', 'Software Engineer', $salary, new DateTime('2020-01-01'), new DateTime('2023-12-31'));

echo "Employment: " . $employment->getCompany() . " - " . $employment->getPosition() . "\n";
echo "Salary: " . $employment->getSalary() . "\n";
echo "Period: " . $employment->getPeriod()->getDuration() . " days\n";

// Get fields for database storage
$salaryFields = $employment->getSalaryFields();
$periodFields = $employment->getPeriodFields();
echo "Salary fields: " . json_encode($salaryFields) . "\n";
echo "Period fields: " . json_encode($periodFields) . "\n";

// Create from database fields
$employmentFromDB = Employment::fromFields(
    'Tech Corp',
    'Software Engineer',
    75000,
    'USD',
    new DateTime('2020-01-01'),
    new DateTime('2023-12-31')
);

echo "Employment from DB: " . $employmentFromDB->getCompany() . "\n";

// Address example
$address = new Address('123 Main St', 'New York', 'NY', '10001', 'USA');
$person = new Person('John Doe', 'john@example.com', $address);

echo "Person: " . $person->getName() . "\n";
echo "Address: " . $person->getAddress()->getFullAddress() . "\n";

// Get address fields for database storage
$addressFields = $person->getAddressFields();
echo "Address fields: " . json_encode($addressFields) . "\n";

// Create person from database fields
$personFromDB = Person::fromFields('John Doe', 'john@example.com', '123 Main St', 'New York', 'NY', '10001', 'USA');
echo "Person from DB: " . $personFromDB->getName() . "\n";
?>
// Embedded Value Pattern in Go
package main

import (
    "fmt"
    "time"
)

type Money struct {
    Amount   float64
    Currency string
}

func (m *Money) GetAmount() float64 {
    return m.Amount
}

func (m *Money) GetCurrency() string {
    return m.Currency
}

func (m *Money) String() string {
    return fmt.Sprintf("%.2f %s", m.Amount, m.Currency)
}

type DateRange struct {
    StartDate time.Time
    EndDate   time.Time
}

func (dr *DateRange) GetStartDate() time.Time {
    return dr.StartDate
}

func (dr *DateRange) GetEndDate() time.Time {
    return dr.EndDate
}

func (dr *DateRange) GetDuration() int {
    return int(dr.EndDate.Sub(dr.StartDate).Hours() / 24)
}

type Employment struct {
    Company  string
    Position string
    Salary   Money     // Embedded Money object
    Period   DateRange // Embedded DateRange object
}

func (e *Employment) GetCompany() string {
    return e.Company
}

func (e *Employment) GetPosition() string {
    return e.Position
}

func (e *Employment) GetSalary() Money {
    return e.Salary
}

func (e *Employment) GetPeriod() DateRange {
    return e.Period
}

// Method to get salary as separate fields (for database storage)
func (e *Employment) GetSalaryFields() map[string]interface{} {
    return map[string]interface{}{
        "salaryAmount":   e.Salary.GetAmount(),
        "salaryCurrency": e.Salary.GetCurrency(),
    }
}

// Method to get period as separate fields (for database storage)
func (e *Employment) GetPeriodFields() map[string]interface{} {
    return map[string]interface{}{
        "startDate": e.Period.GetStartDate(),
        "endDate":   e.Period.GetEndDate(),
    }
}

// Method to create from database fields
func NewEmploymentFromFields(company, position string, salaryAmount float64, salaryCurrency string, startDate, endDate time.Time) *Employment {
    salary := Money{Amount: salaryAmount, Currency: salaryCurrency}
    period := DateRange{StartDate: startDate, EndDate: endDate}
    return &Employment{
        Company:  company,
        Position: position,
        Salary:   salary,
        Period:   period,
    }
}

type Address struct {
    Street  string
    City    string
    State   string
    ZipCode string
    Country string
}

func (a *Address) GetFullAddress() string {
    return fmt.Sprintf("%s, %s, %s %s, %s", a.Street, a.City, a.State, a.ZipCode, a.Country)
}

// Method to get address as separate fields (for database storage)
func (a *Address) GetAddressFields() map[string]string {
    return map[string]string{
        "street":  a.Street,
        "city":    a.City,
        "state":   a.State,
        "zipCode": a.ZipCode,
        "country": a.Country,
    }
}

// Method to create from database fields
func NewAddressFromFields(street, city, state, zipCode, country string) *Address {
    return &Address{
        Street:  street,
        City:    city,
        State:   state,
        ZipCode: zipCode,
        Country: country,
    }
}

type Person struct {
    Name    string
    Email   string
    Address Address // Embedded Address object
}

func (p *Person) GetName() string {
    return p.Name
}

func (p *Person) GetEmail() string {
    return p.Email
}

func (p *Person) GetAddress() Address {
    return p.Address
}

// Method to get address as separate fields (for database storage)
func (p *Person) GetAddressFields() map[string]string {
    return p.Address.GetAddressFields()
}

// Method to create from database fields
func NewPersonFromFields(name, email, street, city, state, zipCode, country string) *Person {
    address := NewAddressFromFields(street, city, state, zipCode, country)
    return &Person{
        Name:    name,
        Email:   email,
        Address: *address,
    }
}

func main() {
    // Usage
    salary := Money{Amount: 75000, Currency: "USD"}
    period := DateRange{
        StartDate: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
        EndDate:   time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC),
    }
    employment := Employment{
        Company:  "Tech Corp",
        Position: "Software Engineer",
        Salary:   salary,
        Period:   period,
    }

    fmt.Printf("Employment: %s - %s\n", employment.GetCompany(), employment.GetPosition())
    fmt.Printf("Salary: %s\n", employment.GetSalary().String())
    fmt.Printf("Period: %d days\n", employment.GetPeriod().GetDuration())

    // Get fields for database storage
    salaryFields := employment.GetSalaryFields()
    periodFields := employment.GetPeriodFields()
    fmt.Printf("Salary fields: %+v\n", salaryFields)
    fmt.Printf("Period fields: %+v\n", periodFields)

    // Create from database fields
    employmentFromDB := NewEmploymentFromFields(
        "Tech Corp",
        "Software Engineer",
        75000,
        "USD",
        time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
        time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC),
    )

    fmt.Printf("Employment from DB: %s\n", employmentFromDB.GetCompany())

    // Address example
    address := Address{
        Street:  "123 Main St",
        City:    "New York",
        State:   "NY",
        ZipCode: "10001",
        Country: "USA",
    }
    person := Person{
        Name:    "John Doe",
        Email:   "john@example.com",
        Address: address,
    }

    fmt.Printf("Person: %s\n", person.GetName())
    fmt.Printf("Address: %s\n", person.GetAddress().GetFullAddress())

    // Get address fields for database storage
    addressFields := person.GetAddressFields()
    fmt.Printf("Address fields: %+v\n", addressFields)

    // Create person from database fields
    personFromDB := NewPersonFromFields("John Doe", "john@example.com", "123 Main St", "New York", "NY", "10001", "USA")
    fmt.Printf("Person from DB: %s\n", personFromDB.GetName())
}

На схеме представлен объект "Место работы" (Employment) со ссылками на объекты диапазона дат и денег. В таблице, хранящей объект "место работы", объекты дат и денег раскладываются на отдельные поля, но не хранятся в отдельных таблицах.

Использована иллюстрация с сайта Мартина Фаулера.

Источник