Money (Деньги)

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

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

Описание Money

Огромное количество компьютеров в мире обрабатывают данные о деньгах. Удивительно, что класс Деньги до сих пор не является базовым в любом языке программирования. Недостаток такого рода типа данных приводит к проблемам, наиболее заметные из которых - работа с валютой. Если все вычисления в программе проделываются в одной валюте, никаких особых проблем нет, но как только вводится многовалютность - надо думать о том, чтобы не сложить 10 долларов с 10 йенами без перевода курсов валют. Менее заметна проблема с округлением. Денежные вычисления часто округляют до наименьшей из существующих мер. При этом легко не учесть копейки из-за ошибок округления.

Что действительно хорошо в ООП, так это то, что вы можете исправить эти проблемы, созданием класса Money (Деньги), чтобы работать с денежными величинами и избегать общих ошибок.

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

// Money Pattern in JavaScript
class Money {
    constructor(amount, currency) {
        // Store amount in cents to avoid floating point errors
        this.cents = Math.round(amount * 100);
        this.currency = currency;
    }
    
    get amount() {
        return this.cents / 100;
    }
    
    equals(other) {
        return other instanceof Money && 
               this.cents === other.cents && 
               this.currency === other.currency;
    }
    
    add(other) {
        if (this.currency !== other.currency) {
            throw new Error(`Cannot add ${this.currency} and ${other.currency}`);
        }
        return new Money((this.cents + other.cents) / 100, this.currency);
    }
    
    subtract(other) {
        if (this.currency !== other.currency) {
            throw new Error(`Cannot subtract ${this.currency} and ${other.currency}`);
        }
        return new Money((this.cents - other.cents) / 100, this.currency);
    }
    
    multiply(factor) {
        return new Money((this.cents * factor) / 100, this.currency);
    }
    
    divide(divisor) {
        if (divisor === 0) {
            throw new Error('Cannot divide by zero');
        }
        return new Money((this.cents / divisor) / 100, this.currency);
    }
    
    isZero() {
        return this.cents === 0;
    }
    
    isNegative() {
        return this.cents < 0;
    }
    
    toString() {
        return `${this.amount.toFixed(2)} ${this.currency}`;
    }
    
    static zero(currency) {
        return new Money(0, currency);
    }
}

// Usage
const price1 = new Money(19.99, 'USD');
const price2 = new Money(5.01, 'USD');
const total = price1.add(price2);
console.log(total.toString()); // 25.00 USD

const discount = total.multiply(0.1);
const finalPrice = total.subtract(discount);
console.log(finalPrice.toString()); // 22.50 USD
// Money Pattern in C++
#include <iostream>
#include <string>
#include <stdexcept>

class Money {
private:
    long cents;
    std::string currency;
    
public:
    Money(double amount, const std::string& currency) 
        : cents(static_cast<long>(amount * 100)), currency(currency) {}
    
    double getAmount() const {
        return static_cast<double>(cents) / 100.0;
    }
    
    std::string getCurrency() const {
        return currency;
    }
    
    bool equals(const Money& other) const {
        return cents == other.cents && currency == other.currency;
    }
    
    Money add(const Money& other) const {
        if (currency != other.currency) {
            throw std::runtime_error("Cannot add different currencies");
        }
        return Money((cents + other.cents) / 100.0, currency);
    }
    
    Money subtract(const Money& other) const {
        if (currency != other.currency) {
            throw std::runtime_error("Cannot subtract different currencies");
        }
        return Money((cents - other.cents) / 100.0, currency);
    }
    
    Money multiply(double factor) const {
        return Money((cents * factor) / 100.0, currency);
    }
    
    Money divide(double divisor) const {
        if (divisor == 0) {
            throw std::runtime_error("Cannot divide by zero");
        }
        return Money((cents / divisor) / 100.0, currency);
    }
    
    bool isZero() const {
        return cents == 0;
    }
    
    bool isNegative() const {
        return cents < 0;
    }
    
    std::string toString() const {
        return std::to_string(getAmount()) + " " + currency;
    }
    
    static Money zero(const std::string& currency) {
        return Money(0.0, currency);
    }
};

// Usage
int main() {
    Money price1(19.99, "USD");
    Money price2(5.01, "USD");
    Money total = price1.add(price2);
    std::cout << total.toString() << std::endl; // 25.00 USD
    
    Money discount = total.multiply(0.1);
    Money finalPrice = total.subtract(discount);
    std::cout << finalPrice.toString() << std::endl; // 22.50 USD
    
    return 0;
}
// Money Pattern in Go
package main

import (
    "fmt"
    "math"
)

type Money struct {
    cents    int64
    currency string
}

func NewMoney(amount float64, currency string) Money {
    return Money{
        cents:    int64(math.Round(amount * 100)),
        currency: currency,
    }
}

func (m Money) Amount() float64 {
    return float64(m.cents) / 100.0
}

func (m Money) Currency() string {
    return m.currency
}

func (m Money) Equals(other Money) bool {
    return m.cents == other.cents && m.currency == other.currency
}

func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, fmt.Errorf("cannot add different currencies")
    }
    return Money{
        cents:    m.cents + other.cents,
        currency: m.currency,
    }, nil
}

func (m Money) Subtract(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, fmt.Errorf("cannot subtract different currencies")
    }
    return Money{
        cents:    m.cents - other.cents,
        currency: m.currency,
    }, nil
}

func (m Money) Multiply(factor float64) Money {
    return Money{
        cents:    int64(math.Round(float64(m.cents) * factor)),
        currency: m.currency,
    }
}

func (m Money) Divide(divisor float64) (Money, error) {
    if divisor == 0 {
        return Money{}, fmt.Errorf("cannot divide by zero")
    }
    return Money{
        cents:    int64(math.Round(float64(m.cents) / divisor)),
        currency: m.currency,
    }, nil
}

func (m Money) IsZero() bool {
    return m.cents == 0
}

func (m Money) IsNegative() bool {
    return m.cents < 0
}

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

func ZeroMoney(currency string) Money {
    return Money{cents: 0, currency: currency}
}

// Usage
func main() {
    price1 := NewMoney(19.99, "USD")
    price2 := NewMoney(5.01, "USD")
    total, _ := price1.Add(price2)
    fmt.Println(total.String()) // 25.00 USD
    
    discount := total.Multiply(0.1)
    finalPrice, _ := total.Subtract(discount)
    fmt.Println(finalPrice.String()) // 22.50 USD
}
# Money Pattern in Python
from decimal import Decimal, ROUND_HALF_UP
from typing import Union

class Money:
    def __init__(self, amount: Union[float, Decimal], currency: str):
        # Use Decimal for precise decimal arithmetic
        self.amount = Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
        self.currency = currency
    
    def __add__(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError(f"Cannot add {self.currency} and {other.currency}")
        return Money(self.amount + other.amount, self.currency)
    
    def __sub__(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError(f"Cannot subtract {self.currency} and {other.currency}")
        return Money(self.amount - other.amount, self.currency)
    
    def __mul__(self, factor: Union[float, Decimal]) -> 'Money':
        return Money(self.amount * Decimal(str(factor)), self.currency)
    
    def __truediv__(self, divisor: Union[float, Decimal]) -> 'Money':
        if divisor == 0:
            raise ValueError("Cannot divide by zero")
        return Money(self.amount / Decimal(str(divisor)), self.currency)
    
    def __eq__(self, other: 'Money') -> bool:
        return self.amount == other.amount and self.currency == other.currency
    
    def __str__(self) -> str:
        return f"{self.amount} {self.currency}"
    
    def is_zero(self) -> bool:
        return self.amount == 0
    
    def is_negative(self) -> bool:
        return self.amount < 0
    
    @classmethod
    def zero(cls, currency: str) -> 'Money':
        return cls(0, currency)

# Usage
if __name__ == "__main__":
    price1 = Money(19.99, "USD")
    price2 = Money(5.01, "USD")
    total = price1 + price2
    print(total)  # 25.00 USD
    
    discount = total * 0.1
    final_price = total - discount
    print(final_price)  # 22.50 USD
<?php
// Money Pattern in PHP
class Money {
    private $amount;
    private $currency;
    
    public function __construct($amount, $currency) {
        // Use BC Math for precise decimal arithmetic
        $this->amount = bcadd($amount, '0', 2);
        $this->currency = $currency;
    }
    
    public function getAmount() {
        return $this->amount;
    }
    
    public function getCurrency() {
        return $this->currency;
    }
    
    public function equals(Money $other) {
        return bccomp($this->amount, $other->amount, 2) === 0 && 
               $this->currency === $other->currency;
    }
    
    public function add(Money $other) {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException("Cannot add different currencies");
        }
        return new Money(bcadd($this->amount, $other->amount, 2), $this->currency);
    }
    
    public function subtract(Money $other) {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException("Cannot subtract different currencies");
        }
        return new Money(bcsub($this->amount, $other->amount, 2), $this->currency);
    }
    
    public function multiply($factor) {
        return new Money(bcmul($this->amount, $factor, 2), $this->currency);
    }
    
    public function divide($divisor) {
        if ($divisor == 0) {
            throw new InvalidArgumentException("Cannot divide by zero");
        }
        return new Money(bcdiv($this->amount, $divisor, 2), $this->currency);
    }
    
    public function isZero() {
        return bccomp($this->amount, '0', 2) === 0;
    }
    
    public function isNegative() {
        return bccomp($this->amount, '0', 2) < 0;
    }
    
    public function __toString() {
        return $this->amount . " " . $this->currency;
    }
    
    public static function zero($currency) {
        return new Money('0', $currency);
    }
}

// Usage
$price1 = new Money('19.99', 'USD');
$price2 = new Money('5.01', 'USD');
$total = $price1->add($price2);
echo $total . "\n"; // 25.00 USD

$discount = $total->multiply('0.1');
$finalPrice = $total->subtract($discount);
echo $finalPrice . "\n"; // 22.50 USD
?>

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

Источник