Vietnamese (Tiếng Việt) translation by Thai An (you can also view the original English article)
Money Pattern, do Martin Fowler định nghĩa và được xu tất bản trong mô hình kiến trúc ứng dụng doanh nghiệp, là một cách tuyệt vời để thể hiện các cặp đơn vị -giá trị. Nó được gọi là Money Pattern (mô hình tiền tệ) vì nó nổi bật lên trong bối cảnh tài chính và chúng tôi sẽ minh họa việc sử dụng mô hình trong bằng PHP.
Một tài khoản kiểu như PayPal
Tôi không hiểu PayPal được triển khai như thế nào, nhưng tôi nghĩ nên lấy chức năng của nó làm ví dụ. Hãy để tôi cho bạn biết ý tôi là gì, tài khoản PayPal của tôi có hai loại tiền: US đô và Euro. PayPal tách biệt hai giá trị này, nhưng tôi có thể nhận bằng bất kỳ loại tiền nào, tôi có thể thấy tổng số tiền của mình ở bất kỳ loại tiền nào trong hai loại tiền và tôi có thể trích xuất một trong hai loại. Cho mục đích của ví dụ này, hãy tưởng tượng rằng chúng tôi trích xuất bằng bất kỳ loại tiền tệ nào và chuyển đổi tự động được thực hiện nếu số dư của loại tiền cụ thể đó thấp hơn số tiền chúng tôi muốn chuyển nhưng vẫn có đủ tiền bằng loại tiền khác. Ngoài ra, chúng tôi sẽ giới hạn ví dụ chỉ có hai loại tiền tệ.
Lấy một tài khoản
Nếu tôi đã tạo và sử dụng một đối tượng Account, tôi muốn khởi tạo nó bằng số tài khoản.
function testItCanCrateANewAccount() { $this->assertInstanceOf("Account", new Account(123)); }
Điều này rõ ràng sẽ thất bại vì chúng tôi chưa có class Account
class Account { }
Chà, viết nó trong một file "Account.php
" mới và yêu cầu nó trong test, đã giúp nó vượt qua. Tuy nhiên, tất cả điều này được thực hiện chỉ để khiến chúng ta thoải mái với ý tưởng. Tiếp theo, tôi đang nghĩ đến việc lấy id
của tài khoản.
function testItCanCrateANewAccountWithId() { $this->assertEquals(123, (new Account(123))->getId()); }
Thực sự tôi đã thay đổi test trước đó thành test này. Không cần phải giữ test đầu tiên. Cứ để mặc nó, có nghĩa là nó thúc ép tôi phải suy nghĩ về class Account
và thực sự phải tạo ra nó. Giờ chúng ta có thể tiếp tục.
class Account { private $id; function __construct($id) { $this->id = $id; } public function getId() { return $this->id; } }
Test đã vượt qua và Account bắt đầu trông giống như một class thực sự.
Tiền tệ
Dựa trên sự tương đồng PayPal của chúng tôi, chúng tôi có thể muốn xác định loại tiền chính và tiền phụ cho tài khoản của mình.
private $account; protected function setUp() { $this->account = new Account(123); } [...] function testItCanHavePrimaryAndSecondaryCurrencies() { $this->account->setPrimaryCurrency("EUR"); $this->account->setSecondaryCurrency('USD'); $this->assertEquals(array('primary' => 'EUR', 'secondary' => 'USD'), $this->account->getCurrencies()); }
Bây giờ test trên sẽ buộc chúng ta viết đoạn code sau.
class Account { private $id; private $primaryCurrency; private $secondaryCurrency; [...] function setPrimaryCurrency($currency) { $this->primaryCurrency = $currency; } function setSecondaryCurrency($currency) { $this->secondaryCurrency = $currency; } function getCurrencies() { return array('primary' => $this->primaryCurrency, 'secondary' => $this->secondaryCurrency); } }
Hiện tại, chúng tôi đang biến tiền tệ trở thành một string đơn giản. Có thể trong tương lai điều này sẽ thay đổi, nhưng hiện giờ thì chưa.
Đưa tiền cho tôi
Có vô số lý do tại sao không biểu đạt tiền tệ hư một giá trị đơn giản. Các phép tính cho float? Bất kỳ ai? Còn phân số tiền tệ thì sao? Chúng ta có nên có 10, 100 hoặc 1000 xu bằng những ngoại tệ? Chà, đây là một vấn đề khác chúng ta sẽ phải tránh. Còn việc phân bổ những xu không thể chia được?
Có quá nhiều vấn đề về ngoại tệ khi làm việc với tiền khi chuyển thành code, vì vậy chúng ta sẽ đi thẳng vào giải pháp, Money Pattern. Đây là một pattern (mô hình) khá đơn giản, với những lợi thế lớn và nhiều trường hợp sử dụng, vượt xa lĩnh vực tài chính. Bất cứ khi nào bạn phải biểu đạt cho một cặp đơn vị giá trị, có lẽ bạn nên sử dụng pattern này.

Money Pattern về cơ bản là một class gói gọn một số tiền và kiểu tiền tệ. Sau đó, nó định nghĩa tất cả các phép toán dựa trên giá trị liên quan đến tiền tệ. "allocate()
" là một hàm đặc biệt để phân phối một lượng tiền cụ thể giữa hai hoặc nhiều người nhận.
Vì vậy, với tư cách là người sử dụng của Money
, tôi muốn có thể thực hiện việc này trong một test:
class MoneyTest extends PHPUnit_Framework_TestCase { function testWeCanCreateAMoneyObject() { $money = new Money(100, Currency::USD()); } }
Nhưng điều đó sẽ không hiệu quả. Chúng tôi cần cả Money
và Currency
. Thậm chí nhiều hơn, chúng ta cần Currency
trước cả Money
. Đây sẽ là một class đơn giản, vì vậy tôi sẽ bỏ qua việc test nó ngay bây giờ. Tôi khá chắc chắn rằng IDE có thể tạo ra gần như toàn bộ code cho tôi.
class Currency { private $centFactor; private $stringRepresentation; private function __construct($centFactor, $stringRepresentation) { $this->centFactor = $centFactor; $this->stringRepresentation = $stringRepresentation; } public function getCentFactor() { return $this->centFactor; } function getStringRepresentation() { return $this->stringRepresentation; } static function USD() { return new self(100, 'USD'); } static function EUR() { return new self(100, 'EUR'); } }
Đó là đủ cho ví dụ của chúng tôi. Có hai hàm static cho tiền USD và EUR. Trong một ứng dụng thực tế, có lẽ chúng ta sẽ có một constructor chung có tham số và tải tất cả loại tiền từ bảng cơ sở dữ liệu hoặc, thậm chí tốt hơn, từ file text.
Tiếp theo, bao gồm hai file mới trong test:
require_once '../Currency.php'; require_once '../Money.php'; class MoneyTest extends PHPUnit_Framework_TestCase { function testWeCanCreateAMoneyObject() { $money = new Money(100, Currency::USD()); } }
Test này vẫn thất bại, nhưng ít nhất hiện giờ có thể tìm thấy Currency
. Chúng tôi tiếp tục với triển khai Money
tối thiểu. Nhiều hơn một chút so với test yêu cầu, lần nữa là code tự động sinh ra.
class Money { private $amount; private $currency; function __construct($amount, Currency $currency) { $this->amount = $amount; $this->currency = $currency; } }
Xin lưu ý, chúng tôi ép buộc kiểu Currency
cho tham số thứ hai trong hàm constructor của chúng tôi. Đây là cách hay để tránh khách hàng của chúng tôi gửi rác dưới dạng tiền tệ.
So sánh tiền
Điều đầu tiên xuất hiện trong đầu tôi sau khi đối tượng tối thiểu khởi chạy là tôi sẽ phải so sánh các đối tượng money bằng cách nào đó. Sau đó, tôi nhớ rằng PHP khá thông minh khi so sánh các đối tượng, vì vậy tôi đã tạo ra test này.
function testItCanTellTwoMoneyObjectAreEqual() { $m1 = new Money(100, Currency::USD()); $m2 = new Money(100, Currency::USD()); $this->assertEquals($m1,$m2); $this->assertTrue($m1 == $m2); }
Vâng, test đó thực sự đã vượt qua. Hàm "assertEquals
" có thể so sánh hai đối tượng và thậm chí cả điều kiện đẳng thức tích hợp từ PHP "==
" đang cho tôi biết điều tôi mong đợi. Tốt.
Nhưng, nếu chúng ta quan tâm đến một đối tượng lớn hơn cái kia thì sao? Trước sự ngạc nhiên của tôi, test sau đây cũng thành công mà không gặp vấn đề gì.
function testOneMoneyIsBiggerThanTheOther() { $m1 = new Money(200, Currency::USD()); $m2 = new Money(100, Currency::USD()); $this->assertGreaterThan($m2, $m1); $this->assertTrue($m1 > $m2); }
Điều này dẫn chúng ta đến ...
function testOneMoneyIsLessThanTheOther() { $m1 = new Money(100, Currency::USD()); $m2 = new Money(200, Currency::USD()); $this->assertLessThan($m2, $m1); $this->assertTrue($m1 < $m2); }
... một test và nó vượt qua ngay lập tức.
Cộng, trừ, nhân
Thấy rất nhiều phép màu của PHP thực sự hiệu quả khi so sánh, tôi không thể cưỡng lại việc thử cái này.
function testTwoMoneyObjectsCanBeAdded() { $m1 = new Money(100, Currency::USD()); $m2 = new Money(200, Currency::USD()); $sum = new Money(300, Currency::USD()); $this->assertEquals($sum, $m1 + $m2); }
Test đã thất bại và cho tôi biết rằng:
Object of class Money could not be converted to int
Hừm. Điều này khá rõ. Tại thời điểm này, chúng tôi phải đưa ra quyết định. Có thể tiếp tục bài tập này với PHP, nhưng cách tiếp cận này ở một số thời điểm chuyển bài hướng dẫn thành PHP cheatsheet thay vì một design patter (mô hình thiết kế). Vì vậy, hãy đưa ra quyết định để thực hiện các phương thức thực tế để thêm, trừ và nhân các đối tượng tiền.
function testTwoMoneyObjectsCanBeAdded() { $m1 = new Money(100, Currency::USD()); $m2 = new Money(200, Currency::USD()); $sum = new Money(300, Currency::USD()); $this->assertEquals($sum, $m1->add($m2)); }
Test này cũng thất bại, nhưng với lỗi cho chúng tôi biết không có phương thức "add
" trong Money
.
public function getAmount() { return $this->amount; } function add($other) { return new Money($this->amount + $other->getAmount(), $this->currency); }
Để cộng hai đối tượng Money
, chúng ta cần một cách để lấy số lượng đối tượng chúng ta truyền vào làm đối số. Tôi thích viết một getter, nhưng đặt biến class thành public cũng sẽ là một giải pháp chấp nhận được. Nhưng nếu chúng ta muốn cộng Đô vào Euro thì sao?
/** * @expectedException Exception * @expectedExceptionMessage Both Moneys must be of same currency */ function testItThrowsExceptionIfWeTryToAddTwoMoneysWithDifferentCurrency() { $m1 = new Money(100, Currency::USD()); $m2 = new Money(100, Currency::EUR()); $m1->add($m2); }
Có một số cách để xử lý các phép tính trên các đối tượng Money
với các loại tiền tệ khác nhau. Chúng tôi sẽ đề ra một ngoại lệ và chờ kết quả test từ nó. Ngoài ra, chúng tôi có thể triển khai cơ chế chuyển đổi tiền tệ trong ứng dụng của mình, gọi nó, chuyển đổi cả hai đối tượng Money
thành một số loại tiền tệ mặc định và so sánh chúng. Hoặc, nếu chúng ta có một thuật toán chuyển đổi tiền tệ phức tạp hơn, chúng ta luôn có thể chuyển đổi từ loại này sang loại khác và so sánh với loại tiền được chuyển đổi đó. Vấn đề là khi chuyển đổi xảy ra, phí chuyển đổi phải được xem xét và mọi thứ sẽ trở nên khá phức tạp. Vì vậy, hãy bỏ ngoại lệ đó đi và tiếp tục.
public function getCurrency() { return $this->currency; } function add(Money $other) { $this->ensureSameCurrencyWith($other); return new Money($this->amount + $other->getAmount(), $this->currency); } private function ensureSameCurrencyWith(Money $other) { if ($this->currency != $other->getCurrency()) throw new Exception("Both Moneys must be of same currency"); }
Điều đó tốt hơn. Chúng tôi kiểm tra xem các loại tiền tệ có khác nhau không và đưa ra một ngoại lệ. Tôi đã viết nó như một phương thức private, bởi vì tôi biết chúng ta cũng sẽ cần nó trong các phép toán khác.
Phép trừ và phép nhân rất giống với phép cộng, vì vậy đây là code và bạn có thể tìm thấy các test trong code nguồn đính kèm.
function subtract(Money $other) { $this->ensureSameCurrencyWith($other); if ($other > $this) throw new Exception("Subtracted money is more than what we have"); return new Money($this->amount - $other->getAmount(), $this->currency); } function multiplyBy($multiplier, $roundMethod = PHP_ROUND_HALF_UP) { $product = round($this->amount * $multiplier, 0, $roundMethod); return new Money($product, $this->currency); }
Với phép trừ, chúng ta phải đảm bảo có đủ tiền và với phép nhân, chúng ta phải thực hiện các hành động để làm tròn (lên hoặc xuống) để phép chia (nhân với số nhỏ hơn một) sẽ không cho ra kết quả "nửa xu". Chúng tôi để số tiền của mình theo xu, đơn vị thấp nhất của tiền tệ. Chúng tôi không thể chia nó nhiều hơn.
Giới thiệu tiền tệ vào tài khoản của chúng tôi
Chúng tôi gần như có Money
và Currency
. Đã đến lúc giới thiệu những đối tượng này vào Account
. Chúng tôi sẽ bắt đầu với Money
và thay đổi test của chúng tôi cho phù hợp.
function testItCanHavePrimaryAndSecondaryCurrencies() { $this->account->setPrimaryCurrency(Currency::EUR()); $this->account->setSecondaryCurrency(Currency::USD()); $this->assertEquals(array('primary' => Currency::EUR(), 'secondary' => Currency::USD()), $this->account->getCurrencies()); }
Do tính chất dynamic typing của PHP, test này vượt qua mà không gặp vấn đề gì. Tuy nhiên, tôi muốn thúc ép các phương thức trong Account
sử dụng các đối tượng Money
và không chấp nhận đối tượng nào khác. Điều này không bắt buộc, nhưng tôi thấy những kiểu hinting cực kỳ hữu dụng khi người khác cần hiểu code của chúng tôi.
function setPrimaryCurrency(Currency $currency) { $this->primaryCurrency = $currency; } function setSecondaryCurrency(Currency $currency) { $this->secondaryCurrency = $currency; }
Bây giờ, rõ ràng với bất kỳ ai đọc code này lần đầu tiên thì dĩ nhiên Account
hiệu quả với Currency
.
Giới thiệu Money với Account của chúng tôi
Hai hành động cơ bản mà bất kỳ tài khoản nào cũng phải cung cấp là: deposit (tiền gửi) - nghĩa là bổ sung tiền vào tài khoản - và withdraw (rút tiền) - nghĩa là xóa tiền khỏi tài khoản. Tiền gửi có một nguồn và rút tiền có một đích đến khác, ngoài tài khoản hiện tại của chúng tôi. Chúng tôi sẽ không đi vào chi tiết về cách triển khai các giao dịch này, chúng tôi sẽ chỉ tập trung vào việc triển khai các hiệu ứng này có trên tài khoản của chúng tôi. Vì vậy, chúng ta có thể hình dung một test thế này cho việc gửi tiền.
function testAccountCanDepositMoney() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); $this->assertEquals($money, $this->account->getPrimaryBalance()); }
Điều này sẽ yêu cầu chúng ta phải tạo ra khá nhiều code triển khai.
class Account { private $id; private $primaryCurrency; private $secondaryCurrency; private $secondaryBalance; private $primaryBalance; function getSecondaryBalance() { return $this->secondaryBalance; } function getPrimaryBalance() { return $this->primaryBalance; } function __construct($id) { $this->id = $id; } [...] function deposit(Money $money) { $this->primaryCurrency == $money->getCurrency() ? $this->primaryBalance = $money : $this->secondaryBalance = $money; } }
OK. Tôi biết, tôi đã viết nhiều hơn những điều thực sự cần thiết. Nhưng tôi không muốn làm bạn phát chán với những bước quá đơn giản và tôi cũng khá chắc chắn rằng code cho juniorBalance
sẽ hoạt động chính xác. Gần như hoàn toàn được tạo ra bởi IDE. Tôi thậm chí sẽ bỏ qua việc test nó. Trong khi code này làm cho test của chúng tôi vượt qua, chúng tôi phải tự hỏi điều gì xảy ra khi chúng tôi gửi tiền tiếp theo? Chúng tôi muốn tiền của chúng tôi được thêm vào số dư trước đó.
function testSubsequentDepositsAddUpTheMoney() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); //One euro in the account $this->account->deposit($money); //Two euros in the account $this->assertEquals($money->multiplyBy(2), $this->account->getPrimaryBalance()); }
Vâng, thất bại. Vì vậy, chúng tôi phải cập nhật code của chúng tôi.
function deposit(Money $money) { if ($this->primaryCurrency == $money->getCurrency()){ $this->primaryBalance = $this->primaryBalance ? : new Money(0, $this->primaryCurrency); $this->primaryBalance = $this->primaryBalance->add($money); }else { $this->secondaryBalance = $this->secondaryBalance ? : new Money(0, $this->secondaryCurrency); $this->secondaryBalance = $this->secondaryBalance->add($money); } }
Điều này tốt hơn nhiều. Chúng tôi có thể hoàn thành với phương thức deposit và chúng tôi có thể tiếp tục withdraw.
function testAccountCanWithdrawMoneyOfSameCurrency() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); $this->account->withdraw(new Money(70, Currency::EUR())); $this->assertEquals(new Money(30, Currency::EUR()), $this->account->getPrimaryBalance()); }
Đây là một test đơn giản. Giải pháp cũng rất đơn giản.
function withdraw(Money $money) { $this->primaryCurrency == $money->getCurrency() ? $this->primaryBalance = $this->primaryBalance->subtract($money) : $this->secondaryBalance = $this->secondaryBalance->subtract($money); }
Chà, nó hiệu quả, nhưng nếu chúng ta muốn sử dụng loại Currency không có trong tài khoản thì sao? Chúng ta nên gửi một Excpetion cho vấn đề đó.
/** * @expectedException Exception * @expectedExceptionMessage This account has no currency USD */ function testThrowsExceptionForInexistentCurrencyOnWithdraw() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); $this->account->withdraw(new Money(70, Currency::USD())); }
Điều đó cũng sẽ yêu cầu chúng tôi kiểm tra các tiền tệ của chúng tôi.
function withdraw(Money $money) { $this->validateCurrencyFor($money); $this->primaryCurrency == $money->getCurrency() ? $this->primaryBalance = $this->primaryBalance->subtract($money) : $this->secondaryBalance = $this->secondaryBalance->subtract($money); } private function validateCurrencyFor(Money $money) { if (!in_array($money->getCurrency(), $this->getCurrencies())) throw new Exception( sprintf( 'This account has no currency %s', $money->getCurrency()->getStringRepresentation() ) ); }
Nhưng nếu chúng ta muốn rút nhiều hơn số chúng ta có thì sao? Trường hợp đó đã được giải quyết khi chúng tôi thực hiện phép trừ trên Money
. Dưới đây là test chứng minh điều đó.
/** * @expectedException Exception * @expectedExceptionMessage Subtracted money is more than what we have */ function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); $this->account->withdraw(new Money(150, Currency::EUR())); }
Xử lý rút tiền và chuyển đổi
Một trong những điều khó giải quyết hơn khi chúng ta làm việc với nhiều loại tiền tệ là chuyển đổi giữa chúng. Cái đẹp của pattern thiết kế này là cho phép chúng ta đơn giản hóa phần nào vấn đề này bằng cách cô lập và gói gọn nó trong class riêng của nó. Mặc dù logic trong một class Exchange
có thể rất phức tạp, nhưng việc sử dụng nó trở nên dễ dàng hơn nhiều. Với mục đích của hướng dẫn này, hãy hình dung rằng chúng ta chỉ có một số logic Exchange
rất cơ bản. 1 EUR = 1,5 USD.
class Exchange { function convert(Money $money, Currency $toCurrency) { if ($toCurrency == Currency::EUR() && $money->getCurrency() == Currency::USD()) return new Money($money->multiplyBy(0.67)->getAmount(), $toCurrency); if ($toCurrency == Currency::USD() && $money->getCurrency() == Currency::EUR()) return new Money($money->multiplyBy(1.5)->getAmount(), $toCurrency); return $money; } }
Nếu chúng tôi chuyển đổi từ EUR sang USD, chúng tôi nhân giá trị lên 1,5, nếu chúng tôi chuyển đổi từ USD sang EUR, chúng tôi chia giá trị cho 1,5, nếu không, nghĩa là chúng tôi đang chuyển đổi hai loại tiền tệ cùng loại, vì vậy chúng tôi không làm gì và trả về số tiền. Tất nhiên, trong thực tế sẽ là một class phức tạp hơn nhiều.
Giờ đây, khi có class Exchange
, Account
có thể đưa ra các quyết định khác nhau khi chúng tôi muốn rút Money
bằng một loại tiền tệ, nhưng chúng tôi không đưa ra một loại tiền cụ thể. Đây là một test nhằm minh họa tốt hơn.
function testItConvertsMoneyFromTheOtherCurrencyWhenWeDoNotHaveEnoughInTheCurrentOne() { $this->account->setPrimaryCurrency(Currency::USD()); $money = new Money(100, Currency::USD()); //That's 1 USD $this->account->deposit($money); $this->account->setSecondaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO = 1.5 USD $this->account->deposit($money); $this->account->withdraw(new Money(200, Currency::USD())); //That's 2 USD $this->assertEquals(new Money(0, Currency::USD()), $this->account->getPrimaryBalance()); $this->assertEquals(new Money(34, Currency::EUR()), $this->account->getSecondaryBalance()); }
Chúng tôi thiết lập loại tiền chính của tài khoản của chúng tôi thành USD và gửi đi 1 đô. Sau đó, chúng tôi thiết lập tiền tệ thứ cấp thành EUR và gửi 1 Euro. Sau đó, chúng tôi rút ra 2 đô. Cuối cùng, chúng tôi hy vọng sẽ vẫn còn 0 đô và 0,34 Euro. Tất nhiên test này đưa ra một exception, vì vậy chúng tôi phải triển khai một giải pháp cho vấn đề nan giải này.
function withdraw(Money $money) { $this->validateCurrencyFor($money); if ($this->primaryCurrency == $money->getCurrency()) { if( $this->primaryBalance >= $money ) { $this->primaryBalance = $this->primaryBalance->subtract($money); }else{ $ourMoney = $this->primaryBalance->add($this->secondaryToPrimary()); $remainingMoney = $ourMoney->subtract($money); $this->primaryBalance = new Money(0, $this->primaryCurrency); $this->secondaryBalance = (new Exchange())->convert($remainingMoney, $this->secondaryCurrency); } } else { $this->secondaryBalance = $this->secondaryBalance->subtract($money); } } private function secondaryToPrimary() { return (new Exchange())->convert($this->secondaryBalance, $this->primaryCurrency); }
Ồ, rất nhiều thay đổi đã được triển khai để hỗ trợ việc chuyển đổi tự động này. Điều đang xảy ra là nếu chúng ta trong trường hợp trích xuất từ tiền tệ chính của mình và chúng ta không có đủ tiền, chúng ta sẽ chuyển đổi số dư của tiền tệ thứ cấp thành tiền chính và thử lại phép trừ. Nếu chúng ta vẫn không có đủ tiền, đối tượng $ourMoney
sẽ đưa ra exception phù hợp. Mặt khác, chúng tôi sẽ xét số dư chính của mình thành 0 và chúng tôi sẽ chuyển đổi số tiền còn lại thành tiền tệ thứ cấp và đặt số dư thứ cấp của chúng tôi thành giá trị đó.
Vẫn theo logic tài khoản của chúng tôi để khai triển chuyển đổi tự động tương tự cho tiền tệ thứ cấp. Chúng tôi sẽ không thực hiện một logic đối xứng như vậy. Nếu bạn thích ý tưởng, hãy coi nó như một bài tập cho bạn. Ngoài ra, hãy nghĩ đến một phương thức private cơ bản hơn sẽ làm thực hiện chuyển đổi tự động trong cả hai trường hợp.
Sự thay đổi phức tạp này đối với logic của chúng tôi cũng buộc phải cập nhật một trong những test khác. Bất cứ khi nào chúng tôi muốn tự động chuyển đổi, chúng tôi phải có số dư, ngay cả khi nó chỉ bằng không.
/** * @expectedException Exception * @expectedExceptionMessage Subtracted money is more than what we have */ function testItThrowsExceptionIfWeTryToSubtractMoreMoneyThanWeHave() { $this->account->setPrimaryCurrency(Currency::EUR()); $money = new Money(100, Currency::EUR()); //That's 1 EURO $this->account->deposit($money); $this->account->setSecondaryCurrency(Currency::USD()); $money = new Money(0, Currency::USD()); $this->account->deposit($money); $this->account->withdraw(new Money(150, Currency::EUR())); }
Phân bổ tiền giữa các tài khoản
Phương thức cuối cùng chúng ta cần thực hiện trên Money
là allocate
. Đây là logic quyết định những việc cần làm khi phân chia tiền giữa các tài khoản khác nhau mà không thể thực hiện chính xác. Ví dụ: nếu chúng tôi có 0,10 xu và chúng tôi muốn phân bổ chúng giữa hai tài khoản theo tỷ lệ phần trăm 30-70, điều đó thật dễ dàng. Một tài khoản sẽ nhận được ba xu và bảy xu còn lại. Tuy nhiên, nếu chúng tôi muốn thực hiện phân bổ tỷ lệ 30-70 tương tự cho năm xu, có vấn đề. Phân bổ chính xác sẽ là 1,5 xu trong một tài khoản và 3,5 trong tài khoản khác. Nhưng chúng tôi không thể chia xu, vì vậy chúng tôi phải thực hiện thuật toán của riêng mình để phân bổ tiền.
Có thể có một số giải pháp cho vấn đề này, một thuật toán phổ biến là thêm một xu liên tục vào mỗi tài khoản. Nếu một tài khoản có nhiều xu hơn giá trị toán học chính xác của nó, tài khoản đó sẽ bị loại khỏi danh sách phân bổ và không nhận thêm tiền. Đây là biểu diễn đồ họa.

Và một test để chứng minh quan điểm của chúng tôi như bên dưới.
function testItCanAllocateMoneyBetween2Accounts() { $a1 = $this->anAccount(); $a2 = $this->anAccount(); $money = new Money(5, Currency::USD()); $money->allocate($a1, $a2, 30, 70); $this->assertEquals(new Money(2, Currency::USD()), $a1->getPrimaryBalance()); $this->assertEquals(new Money(3, Currency::USD()), $a2->getPrimaryBalance()); } private function anAccount() { $account = new Account(1); $account->setPrimaryCurrency(Currency::USD()); $account->deposit(new Money(0, Currency::USD())); return $account; }
Chúng tôi chỉ tạo một đối tượng Money với năm xu và hai tài khoản. Chúng tôi gọi hàm allocate
và hy vọng sẽ có 2 đến 3 giá trị trong hai tài khoản. Chúng tôi cũng đã tạo một phương thức helper để nhanh chóng tạo ra các tài khoản. Test thất bại, như mong đợi, nhưng chúng ta có thể giúp nó thành công khá dễ dàng.
function allocate(Account $a1, Account $a2, $a1Percent, $a2Percent) { $exactA1Balance = $this->amount * $a1Percent / 100; $exactA2Balance = $this->amount * $a2Percent / 100; $oneCent = new Money(1, $this->currency); while ($this->amount > 0) { if ($a1->getPrimaryBalance()->getAmount() < $exactA1Balance) { $a1->deposit($oneCent); $this->amount--; } if ($this->amount <= 0) break; if ($a2->getPrimaryBalance()->getAmount() < $exactA2Balance) { $a2->deposit($oneCent); $this->amount--; } } }
Không phải là code đơn giản nhất, nhưng nó hiệu quả khi việc test thành công vượt qua của chúng tôi đã chứng minh điều đó. Điều duy nhất chúng ta vẫn có thể làm với code này là giảm sự trùng lặp nhỏ bên trong vòng lặp while
.
function allocate(Account $a1, Account $a2, $a1Percent, $a2Percent) { $exactA1Balance = $this->amount * $a1Percent / 100; $exactA2Balance = $this->amount * $a2Percent / 100; while ($this->amount > 0) { $this->allocateTo($a1, $exactA1Balance); if ($this->amount <= 0) break; $this->allocateTo($a2, $exactA2Balance); } } private function allocateTo($account, $exactBalance) { if ($account->getPrimaryBalance()->getAmount() < $exactBalance) { $account->deposit(new Money(1, $this->currency)); $this->amount--; } }
Tổng kết
Điều tôi thấy tuyệt vời với pattern nhỏ này là một loạt các trường hợp để chúng ta có thể áp dụng.
Chúng tôi đã thực hiện với money pattern. Chúng tôi thấy rằng đó là một pattern khá đơn giản, nó gói gọn các chi tiết cụ thể của khái niệm về tiền. Chúng tôi cũng thấy rằng việc đóng gói này làm giảm bớt gánh nặng tính toán từ Account. Account có thể tập trung vào việc đại diện cho khái niệm ở cấp độ cao hơn, từ góc nhìn của ngân hàng. Account có thể thực hiện các phương thức như kết nối với chủ tài khoản, ID, giao dịch và tiền. Nó sẽ như một dàn nhạc không phải là một máy tính. Tiền sẽ đảm nhiệm việc tính toán.
Điều tôi thấy tuyệt vời với pattern nhỏ này là một loạt các trường hợp chúng ta có thể áp dụng nó. Về cơ bản, mỗi khi bạn có một cặp đơn vị - giá trị, bạn có thể dùng nó. Hãy tưởng tượng bạn có một ứng dụng về thời tiết và bạn muốn triển khai một biễu diễn nhiệt độ. Đó sẽ là tương đương với đối tượng Money của chúng tôi. Bạn có thể dùng Fahrenheit hoặc Celsius làm tiền tệ.
Một trường hợp sử dụng khác là khi bạn có một ứng dụng bản đồ và bạn muốn diễn tả khoảng cách giữa các địa điểm. Bạn có thể dễ dàng sử dụng pattern này để chuyển đổi giữa các phép đo Metric hoặc Imperial. Khi bạn làm việc với các đơn vị đơn giản, bạn có thể bỏ đối tượng Exchange và triển khai logic chuyển đổi đơn giản bên trong đối tượng "Money" của mình.
Vậy tôi hy vọng bạn thích hướng dẫn này và tôi rất muốn nghe bạn sử dụng khái niệm này theo những cách khác nhau. Cảm ơn bạn đã đọc bài viết.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post