12/08/2018, 14:17

The Quality of Software Design ~Kỳ 4~

Part 4. Play in the Utopia of reuse Chúng ta hãy cùng nhìn lại bài tập tôi đã đưa ra vào cuối bài viết kỳ trước. Đó là “Chương trình hiển thị số tờ theo từng loại mệnh giá tiền cho số tiền trong ví của bạn”. Hơn nữa, chương trình đó không chỉ có chức năng hiển thị số tờ theo từng loạị ...

Part 4. Play in the Utopia of reuse

Chúng ta hãy cùng nhìn lại bài tập tôi đã đưa ra vào cuối bài viết kỳ trước. Đó là “Chương trình hiển thị số tờ theo từng loại mệnh giá tiền cho số tiền trong ví của bạn”. Hơn nữa, chương trình đó không chỉ có chức năng hiển thị số tờ theo từng loạị tiền mà chức năng hiển thị tổng số tiền cũng được thêm vào. Sau đó, bạn đã biết được trong ví mình có bao nhiêu đô la Mỹ rồi. Đó là bài tập về việc chúng ta phải đếm cả loại tiền này nữa. Trong mệnh giá tiền đô, có loại mệnh giá nhỏ hơn 1 USD là Cent nên đối với loại mệnh giá này nên xử lý thế nào thì được? Đây chính là vấn đề. Hình như trong số báo trước tôi đang dừng lại ở chỗ: nếu tạo class riêng cho từng loại mệnh giá tiền thì không tốt lắm. Đến đây các bạn đã nhớ ra chưa?

Có lẽ các bạn cũng hiểu rằng “nếu ít nhất cần phải thiết kế class riêng cho đồng USD và đồng Cent thì cần phải code sao cho chương trình có thể “biết” được việc đồng cent là mệnh giá tiền nhỏ hơn đô la” đúng không? Nếu có 100 đồng xu cent thì cũng không thể biến thành tờ 1 đô la được nhưng nếu có đủ 100 cent thì có thể sử dụng như 1 đô la. Nói cách khác thì nếu có 100 xu 1 cent thì tổng số tiền sẽ bằng 1 đô la. Vì vậy, sẽ phát sinh yêu cần cần phải luôn luôn suy nghĩ về class đồng cent và class đồng đô la trong quan hệ liên kết với nhau.

Đây là cấu trúc class ở thời điểm tháng trước nhưng nếu phải xem xét cả điều kiện ở phần trên “ Nếu có 100 đồng 1 cent thì tổng số tiền sẽ là 1 đô la” thì chắc trong đầu các bạn sẽ hiện lên cấu trúc class như sau.

Nếu thiết kế cho đồng đô la và cent theo cấu trúc này thì sẽ là USDollar Class và DollarBill class. Vậy, chúng ta hãy cùng suy nghĩ xem, điểm khác biệt của CentCoin class là gì? Tôi cũng xin xác nhận lại là quan hệ trên dưới này là quan hệ của Supper class/Sub class.

Nếu generate ra object gọi là “có 2 tờ 10 đô” thì sẽ thành:
    bill1 = new DollarBill(10, 2);
Thêm nữa, nếu generate object “có 3 đồng xu 10 cent” thì sẽ là:
    bill2 = new CentCoin(10, 3);

Thêm nữa, trong cấu trúc này thì điểm khác biệt giữa DollarBill và CentCoin là USDollar class sẽ thực hiện xử lý, tính ra được tổng số tiền thì cần phải có xử lý để thay đổi phương thức count theo class của object bằng method của USDollar class hay method phần static v.v… Đối với những bạn đang nghĩ “Nếu là bạn thì sẽ làm như thế nào?” thì có các phương pháp sau.

 1. Set riêng array cho DollarBill và CentCoin
 2. Phân biệt Runtime class
 3. Sử dụng flag
 4. set add thành method.
 5. Others??

 Về cách thứ nhất thì như tôi đã viết trong số báo trước, cần phải viết code phụ thuộc vào context đối tượng là tiền tệ nên cách này không ổn.

Về cách 2 và cách 3 thì khi tính toán tổng số cần phải phán đoán loại object rồi phân nhánh xử lý cho add vào dollar hay add vào cent nên xét về độ trừu tượng thì không khác gì so với cách 1. Trường hợp phán đoán Runtime Class thì như sau (Do cách sử dụng flag cũng gần như tương tự nên tôi không trình bày ở đây)

13:    unsigned long getsumDollar() {
14:           unsigned long dollarsum = 0;
15:           unsigned long centsum = 0;
16:           for (USdollar d: dollararray) {
17:                 if (RuntimeClass(d) == DollarBill) {
18:                       dollarsum += d.value*d.count;
19:                 else if (RuntimeClass(d) == CentCoin) {
20:                        centsum += d.value*d.count;
21:                 else {
22:                        Assert(“Unexpected runtime class!”);
23:                 }
24:           }
25:           return dollarsum + centsum/100;
26:    }

Vậy, về cách “4. set add thành method” thì sao? Nếu chỉ nhìn câu chữ thì có lẽ sẽ khó hiểu nhưng tôi nghĩ các bạn xem đoạn code bên dưới xong thì sẽ hiểu.  Trong trường hợp này, tạo pure virtual method tên là Add(unsigned long &dollar, unsigned long &cent), thực hiện code thực tế cho từng class DollarBill, CentCoin

13:    void DollarBill::Add(unsigned long &dollar, unsigned long &cent) {
14:               dollar += this.value * this.count;
15:    }
16:    void CentCoin::Add(unsigned long &dollar, unsigned long &cent) {
17:               cent += this.value * this.count;
18:    }

Nếu làm như vậy thì phía gọi sẽ không phân biệt đồng cent và chỉ gọi method Add là xong.

19:    unsigned long getsumDollar() {
20:           unsigned long dollarsum = 0;
21:           unsigned long centsum = 0;
22:           for (USdollar d: dollararray) {
23:                d.Add(dollarsum, centsum);
24:           }
25:           return dollarsum + centsum/100;
26:    }

Như vậy, độ trừu tượng đã tăng lên một chút rồi. Bởi vì phía gọi không cần phân biệt loại object mà chỉ cần gọi cùng 1 method là xong.  Ở đây, chúng ta hãy cùng nhớ lại requirement ban đầu. Cần phải hiển thị loại tiền: tiền giấy, tiền đồng và số tờ tiền. Trong đoạn source code tháng trước có method get số tiền và số tờ từ từng object.

13: void printbill() { 14: for (Bill b: billarray) { 15: printf(“%d vnd : %d”, b.value, b.count); 16: }

Tôi muốn các bạn nhớ được ra source code như trên. “b” là object thể hiện loại mệnh giá tiền giấy. “b.value” là số tiền. “b.count” là số tờ. “%d vnd : %d” là format hiển thị, sẽ thay đổi theo loại tiền. Nếu là đồng USD thì sẽ là “$$%d : %d”, đồng cent thì hiển thị “%d cent : %d” là được nên nếu làm như vậy thì chẳng phải chúng ta đang giao xử lý Print cho các object thực hiện hay sao?

 4:     void VndBill::Print() {
 5:           printf(“%d vnd : %d”, this value, this count);
 6:     }
 7:     void DollarBill::Print() {
 8:           printf(“$ %d : %d”, this.value, this.count);
 9:     }
10:     void CentCoin::Print() {
11:           printf(“%d cent : %d”, this.value, this.count);
12:    }

Nếu bố trí method này vào từng class VndBill, DollarBill, CentCoin thì phía gọi chỉ việc gọi Print trong object mỗi class là xong. Ví dụ như đoạn code sau:

27:    void printbill() {
28:           for (Bill d: billarray) {
29:                d.Print();
30:           }
31:    }

Bản thân Bill d là VND hay là USD hay Cent thì không biết nhưng chỉ cần gọi method Print mà mỗi object đang chứa là có thể hiển thị được theo mong muốn!

Tuy nhiên, thực tế vẫn còn xa để có thể vui mừng vì đã “code đẹp, code đơn giản”. Chúng ta mới chỉ đặt chân đến cổng “thiên đường reusability và extendibility” mà thôi. Nếu làm như vậy mà có thể chạy được thì việc quay về xem xét lại requirement ban đầu là vô cùng quan trọng.

 (1) Hiển thị số tiền và số tờ của cả tiền giấy và tiền xu trong ví

 (2) Hiển thị tổng số tiền

Yêu cầu ban đầu là như trên. Vậy, nếu như trong ví có lẫn cả VND và USD thì sẽ hiển thị tổng số tiền như thế nào?  Thực ra, trong yêu cầu này cũng không nêu rõ cần phải hiển thị như thế nào. Nếu chưa rõ thì quan trọng là cần làm rõ yêu cầu. Tất nhiên nếu đây là công việc trên thực tế thì phải xác nhận với khách hàng. Ở đây chúng ta hãy coi như yêu cầu là “ Hiển thị tổng số tiền theo đơn vị tiền tệ của từng nước”. Tuy nhiên, yêu cầu của khách hàng thì không chỉ có như vậy.

Nếu như có thể nói được là “À, thì ra là như vậy. Thế thì dù có thêm tiền của nước nào vào thì cũng đều hiển thị được nhỉ? Nếu là đồng USD thì sẽ hiển thị tổng tiền kiểu như $$0.25 nhỉ?” thì công việc sẽ tốt hơn nhỉ. Nếu có thể đặt câu hỏi về những spec chưa rõ ràng thì bạn sẽ làm gì nếu những requirement được thêm vào trong lúc bạn chưa biết?

Trong những lúc như thế này thì việc quan trọng là “ Không nhìn chằm chằm vào design, source code hiện tại mà suy nghĩ mà phân tích requirement và nghĩ xem nên làm thế nào”. Nếu không bắt đầu từ chỗ suy nghĩ xem Requirement đang yêu cầu gì và nên làm như thế nào mới tốt thì tiếp theo sẽ tạo nên một “căn phòng tạm bợ chắp vá” mà thôi!

 (1) Hiển thị số tiền và số tờ của tiền giấy, tiền đồng có trong ví
 (2) Sử dụng dấu thập phân để hiển thị đơn vị tiền tệ nhỏ.
 (3) Hiển thị tổng số tiền theo đơn vị tiền tệ của từng nước.

Thứ tự hiển thị thì như thế nào là được? Các bạn hãy xác nhận với khách hàng cả nội dung này nữa. Khi đó bạn có thể nhận được câu trả lời là “Vậy thì hiển thị theo set số tờ cho từng loại tiền rồi đến tổng số tiền”. Khách hàng lúc nào cũng là là những người ích kỷ.

 (4) Hiển thị số tờ theo từng loại tiền, hiển thị gộp tổng số tiền

Đến đây lại có Requirement được thêm vào rồi. Nhưng các bạn không cần phải vội. Hãy bình tĩnh và thử suy nghĩ xem “Đã làm được cái gì rồi” và “Chưa làm được gì”

  • Những việc đã làm được: (OK-1) Hiển thị số tiền và số tờ cho cả loại tiền xu và tiền giấy của VND, USD.
  • Việc chưa làm được: (NG-1) Hiển thị tổng số tiền có sử dụng dấu thập phân (NG-2) Hiển thị đơn vị tiền của quốc gia tùy ý (NG-3) Control thứ tự hiển thị

Đầu tiên, hãy nhìn kỹ 3 mục NG. Nếu các bạn thử nghĩ về đối tượng “Cần phải xử lý cái gì” thì đối tượng của NG-1 là việc hiển thị tổng số tiền, NG-2 là đơn vị tiền tệ của tiền xu và tiền giấy, NG-3 là tập hợp các loại tiền. Từ NG-1, NG-2 đến NG-3, độ lớn của đối tượng lớn dần. Trường hợp những vấn đề như lần này: độ lớn của đối tượng là khác nhau thì về cơ bản nên xử lý từ vấn đề nhỏ trước thì sẽ dễ giải quyết hơn. Cũng có ngoại lệ nên không thể khẳng định được nhưng ít nhất thì tôi nghĩ trong trường hợp này thì nên giải quyết từ vấn đề nhỏ đến vấn đề lớn thì sẽ dễ hiểu hơn.

Hãy cùng giải quyết nhanh NG-1 nào. Việc sử dụng floating point mà tôi đã nêu trong số báo tháng trước là điều tuyệt đối không được làm trong chương trình xử lý tiền. Việc tính toán sử dụng floating point về nguyên lý có tiềm ẩn error nên không phù hợp khi dùng để tính toán tiền. Nếu vậy thì phải làm sao? Cũng có cách để chia phần thập phân và phần số nguyên như chúng ta đã bàn từ đầu tới giờ nhưng với trường hợp tính toán “Số tiền có trong ví” như lần này thì cần suy nghĩ phương pháp để hiển thị tất cả bằng số nguyên. Tóm lại là hiển thị số tiền bằng đơn vị 1 cent. Ở đây cũng có trường hợp sử dụng số nguyên như kiểu fixed-point. Nếu làm như vậy thì sẽ hiển thị 1 USD = 100, 10 USD = 1000, 100 USD = 10000.

Nếu hiển thị bằng kiểu unsigned long thì giá trị lớn nhất là 4294967295. Nếu là đồng USD thì có thể hiển thị đến 42,949,672 đô và 95 cent. Có lẽ với trường hợp số tiền trong ví thì hiển thị như vậy là đủ rồi chăng?Nếu có ai thấy chưa đủ thì hãy kết bạn với người đó.

Nếu thể hiện bằng fixed-point như thế này thì không cần phải phân biệt DollarBill và CentCoin trong class diagram ở phần trước. Chỉ cần 1 USDollar class là đã đủ cover hết.

 Nếu generate object “có 2 tờ 10 USD” ra thì sẽ thành:
    bill1 = new USDollar(1000, 2);
 Nếu generate object “có 3 xu 10 cent” thì sẽ thành:
    bill2 = new USDollar(10, 3);
 Chỉ như thế này đã được chưa? Đương nhiên là chưa được rồi. Nếu dùng cách hiển thị số tờ 10 USD
    > 1000 cent : 2
 Thì sẽ khác với giá trị mong muốn.

 7:     void USDollar::Print() {
 8:           if (this.value >= 100) {
 9:               printf(“$ %d : %d”, this.value/100, this.count);
10:          } else {
11:              printf(“%d cent : %d”, this.value, this.count);
12:          }
13:     }

Code như vậy thì chưa “đẹp” lắm nhưng tạm thời đã giải quyết được vấn đề. Nếu xét về điểm nào chưa “đẹp” thì là chỗ [if (this.value >= 100)]. Value trong this là giá trị được quyết định khi generate object nên không phải là nội dung được thay đổi giữa chừng. Vì đây là do mỗi khi chạy chương trình thì lại phân nhánh điều kiện.  Nếu như vậy thì nên quyết định ngay lúc generate object thì tốt hơn nhỉ?

Nhưng phải làm thế nào?

Trong method Print() ở phần trên thì điểm khác nhau giữa line 9 và line 11 là format string của phần printf và mẫu số của value. Vì vậy, chỉ cần setting là biến số member của object khi generate object ra là được.

 1: class USDollar : public Bill {
 2:       string printformat;
 3:       unsigned long printdenom;
 4:       USDollar(unsigned long val, unsigned long cnt
 5:                                string format, unsigned long denom)
 6:               this.value = val;
 7:               this.count = cnt;
 8:               this.printformat = format;
 9:               this.printdenom = denom;
10:       }
11:       void print() {
12:              printf(this.printformat, this.value/this.denom, this.count);
13:       }

Trong trường hợp này thì phương pháp generate object “có 2 tờ 10 USD” là:

bill1 = new USDollar(1000, 2, “$ %d : %d”, 100);

Hơn nữa, nếu generate object “Có 3 xu 10 cent” ra thì sẽ là

bill2 = new USDollar(10, 3, “%d cent : %d”, 1);

Trường hợp đồng xu cent thì hãy chú ý việc mẫu số của giá trị hiển thị là 1. Nó không phải là 0 mà nếu như có bị set thành 0 thì cũng sẽ thành “phép chia cho 0”

Khi hoàn tất thì hãy tạo method tính toán tổng số tiền USD và method hiển thị.

20:    unsigned long getsumDollar() {
21:           unsigned long sum = 0;
22:           for (USdollar d: dollararray) {
23:                sum += d.value*d.count;
24:           }
25:           return sum;
26:    }
27:    void printDollarSum(unsigned long sum) {
28:           printf(“USD $ %d.%d”, sum / 100, sum %100);
29:    }

Nào, bây giờ chúng ta đã có thể cho hiển thị cả đồng USD và tiền xu trong 1 class rồi. Việc hiển thị sau dấu thập phân của cent cũng đã giải quyết xong!

0