Relationship between Base Classes and Derived Classes

Một phần của tài liệu Visual C 2012 How to Program _ www.bit.ly/taiho123 (Trang 451 - 474)

2. When the app executes, another compiler (known as the just-in-time compiler

11.4 Relationship between Base Classes and Derived Classes

In this section, we use an inheritance hierarchy containing types of employees in a com- pany’s payroll app to discuss the relationship between a base class and its derived classes.

In this company,commission employees(who will be represented as objects of a base class) are paid a percentage of their sales, whilebase-salaried commission employees(who will be represented as objects of a derived class) receive a base salaryplusa percentage of their sales.

We divide our discussion of the relationship between commission employees and base-salaried commission employees into five examples:

1. The first creates classCommissionEmployee, which directly inherits from classob- jectand declares asprivateinstance variables a first name, last name, social se- curity number, commission rate and gross (i.e., total) sales amount.

2. The second declares classBasePlusCommissionEmployee, which also directly in- herits from objectand declares asprivate instance variables a first name, last name, social security number, commission rate, gross sales amountandbase salary.

We create the class by writingeveryline of code the class requires—we’ll soon see that it’s more efficient to create this class by inheriting fromCommissionEmployee. 3. The third declares a separateBasePlusCommissionEmployeeclass that extends class

CommissionEmployee (i.e., a BasePlusCommissionEmployee is a Commission- Employeewho also has a base salary). We show that base-class methods must be ex- plicitly declaredvirtualif they’re to be overridden by methods in derived classes.

BasePlusCommissionEmployee attempts to access class CommissionEmployee’s

privatemembers, but this results in compilation errors because a derived classcan- notaccess its base class’sprivateinstance variables.

4. The fourth shows that if base classCommissionEmployee’s instance variables are declared asprotected, aBasePlusCommissionEmployeeclass that inherits from classCommissionEmployeecan access that data directly. For this purpose, we de- clare classCommissionEmployeewithprotectedinstance variables.

5. The fifth example demonstratesbest practiceby setting theCommissionEmployee

instance variables back toprivatein classCommissionEmployeeto enforce good software engineering. Then we show how a separate BasePlusCommissionEm- ployeeclass, which inherits from classCommissionEmployee, can useCommis-

sionEmployee’spublicmethods to manipulateCommissionEmployee’sprivate instance variables.

11.4.1 Creating and Using aCommissionEmployeeClass

We begin by declaring classCommissionEmployee(Fig. 11.4). Line 5 begins the class decla- ration. The colon (:) followed by class nameobjectat the end of the declaration header in- dicates that class CommissionEmployee extends (i.e., inherits from) class object (System.Objectin the Framework Class Library). C# programmers use inheritance to cre- ate classes from existing classes. In fact,everyclass in C# (exceptobject) extends an existing class. Because classCommissionEmployeeextends classobject, classCommissionEmployee

inherits the methods of classobject—classobjecthas no fields. Every C# class directly or indirectly inheritsobject’s methods. If a class does not specify that it inherits from another

11.4 Relationship between Base Classes and Derived Classes 411

class, the new classimplicitlyinherits fromobject. For this reason, you typically donotin- clude “: object” in your code—we do so in this example for demonstration purposes.

1 // Fig. 11.4: CommissionEmployee.cs

2 // CommissionEmployee class represents a commission employee.

3 using System;

4

5 public class CommissionEmployee : object 6 {

7 8 9 10 11 12

13 // five-parameter constructor 14

15 16 17 18 19 20 21 22 23 24

25 // read-only property that gets commission employee's first name 26 public string FirstName

27 {

28 get

29 {

30 return firstName;

31 } // end get

32 } // end property FirstName 33

34 // read-only property that gets commission employee's last name 35 public string LastName

36 {

37 get

38 {

39 return lastName;

40 } // end get

41 } // end property LastName 42

43 // read-only property that gets

44 // commission employee's social security number 45 public string SocialSecurityNumber

46 {

47 get

48 {

49 return socialSecurityNumber;

Fig. 11.4 | CommissionEmployeeclass represents a commission employee. (Part 1 of 2.)

private string firstName;

private string lastName;

private string socialSecurityNumber;

private decimal grossSales; // gross weekly sales private decimal commissionRate; // commission percentage

public CommissionEmployee( string first, string last, string ssn, decimal sales, decimal rate )

{

// implicit call to object constructor occurs here firstName = first;

lastName = last;

socialSecurityNumber = ssn;

GrossSales = sales; // validate gross sales via property CommissionRate = rate; // validate commission rate via property } // end five-parameter CommissionEmployee constructor

50 } // end get

51 } // end property SocialSecurityNumber 52

53 // property that gets and sets commission employee's gross sales 54 public decimal GrossSales

55 {

56 get

57 {

58 return grossSales;

59 } // end get

60 set

61 {

62 if ( value >= 0 )

63 grossSales = value;

64 else

65 throw new ArgumentOutOfRangeException(

66 "GrossSales", value, "GrossSales must be >= 0" );

67 } // end set

68 } // end property GrossSales 69

70 // property that gets and sets commission employee's commission rate 71 public decimal CommissionRate

72 {

73 get

74 {

75 return commissionRate;

76 } // end get

77 set

78 {

79 if ( value > 0 && value < 1 ) 80 commissionRate = value;

81 else

82 throw new ArgumentOutOfRangeException( "CommissionRate", 83 value, "CommissionRate must be > 0 and < 1" );

84 } // end set

85 } // end property CommissionRate 86

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

102 } // end class CommissionEmployee

Fig. 11.4 | CommissionEmployeeclass represents a commission employee. (Part 2 of 2.)

// calculate commission employee's pay public decimal Earnings()

{

return commissionRate * grossSales;

} // end method Earnings

// return string representation of CommissionEmployee object public override string ToString()

{

return string.Format(

"{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}",

"commission employee", FirstName, LastName,

"social security number", SocialSecurityNumber,

"gross sales", GrossSales, "commission rate", CommissionRate );

} // end method ToString

11.4 Relationship between Base Classes and Derived Classes 413

CommissionEmployeeClass Overview

CommissionEmployee’s public services include a constructor (lines 14–23), methods

Earnings(lines 88–91) andToString(lines 94–101), and thepublicproperties (lines 26–85) for manipulating the class’s instance variablesfirstName,lastName,socialSecu- rityNumber,grossSales andcommissionRate (declared in lines 7–11). Each of its in- stance variable isprivate, so objects of other classes cannot directly access these variables.

Declaring instance variables asprivateand providingpublicproperties to manipulate and validate them helps enforce good software engineering. Thesetaccessors of proper- tiesGrossSalesandCommissionRate, for example,validatetheir arguments before assign- ing the values to instance variablesgrossSalesandcommissionRate, respectively.

CommissionEmployeeConstructor

Constructors arenotinherited, so classCommissionEmployeedoes not inherit classobject’s constructor. However, classCommissionEmployee’s constructor calls classobject’s construc- torimplicitly. In fact, before executing the code in its own body, the derived class’s construc- tor calls its direct base class’s constructor, either explicitly or implicitly (if no constructor call is specified), to ensure that the instance variables inherited from the base class are initialized properly. The syntax for calling a base-class constructor explicitly is discussed in Section 11.4.3. If the code does not include anexplicitcall to the base-class constructor, the compiler generates animplicitcall to the base class’sdefaultorparameterlessconstructor. The comment in line 17 indicates where the implicit call to the base classobject’s default con- structor is made (you do not write the code for this call). Classobject’s default (empty) con- structor does nothing. Even if a class does not have constructors, the default constructor that the compiler implicitly declares for the class will call the base class’s default or parameterless constructor. Classobjectis theonlyclass that does not have a base class.

After the implicit call toobject’s constructor occurs, lines 18–22 in the constructor assign values to the class’s instance variables. We donotvalidate the values of arguments

first,lastandssnbefore assigning them to the corresponding instance variables. We certainly could validate the first and last names—perhaps by ensuring that they’re of a rea- sonable length. Similarly, a social security number could be validated to ensure that it con- tains nine digits, with or without dashes (e.g.,123-45-6789or123456789).

CommissionEmployeeMethodEarnings

Method Earnings (lines 88–91) calculates a CommissionEmployee’s earnings. Line 90 multiplies thecommissionRateby thegrossSalesand returns the result.

CommissionEmployeeMethodToString

MethodToString(lines 94–101) is special—it’s one of the methods thateveryclass inherits directly or indirectly from class object, which is the root of the C# class hierarchy.

Section 11.7 summarizes classobject’s methods. MethodToStringreturns astringrepre- senting an object. It’s calledimplicitlyby an app whenever an object must be converted to a

stringrepresentation, such as inConsole’sWritemethod orstringmethodFormatusing a format item. Classobject’sToStringmethod returns astringthat includes the name of the object’s class. It’s primarily a placeholder that can be (and typically should be)overridden by a derived class to specify an appropriatestringrepresentation of the data in a derived class object. MethodToStringof classCommissionEmployeeoverrides (redefines) classob-

ject’sToStringmethod. When invoked, CommissionEmployee’sToStringmethod uses

stringmethodFormatto return astringcontaining information about theCommission- Employee. Line 97 uses the format specifierC(in"{6:C}") to formatgrossSalesas currency and the format specifierF2(in"{8:F2}") to format thecommissionRatewith two digits of precision to the right of the decimal point. To override a base-class method, a derived class must declare a method with keywordoverrideand with thesame signature(method name, number of parameters and parameter types)and return type as the base-class method—

object’sToStringmethod takesnoparameters and returns typestring, soCommissionEm- ployeedeclaresToStringwith the same parameter list and return type.

ClassCommissionEmployeeTest

Figure 11.5 tests classCommissionEmployee. Lines 10–11 create aCommissionEmployee

object and invoke its constructor (lines 14–23 of Fig. 11.4) to initialize it. We append the

Msuffix to the gross sales amount and the commission rate to indicate that the compiler

should treate these asdecimalliterals, rather thandoubles. Lines 16–22 useCommission- Employee’s properties to retrieve the object’s instance-variable values for output. Line 23 outputs the amount calculated by theEarningsmethod. Lines 25–26 invoke thesetac- cessors of the object’sGrossSalesandCommissionRateproperties to change the values of instance variablesgrossSalesandcommissionRate. Lines 28–29 output thestringrep- resentation of the updatedCommissionEmployee. When an object is output using a format item, the object’sToStringmethod is invokedimplicitlyto obtain the object’sstringrep- resentation. Line 30 outputs the earnings again.

Common Programming Error 11.1

It’s a compilation error to override a method with one that has adifferentaccess modifier.

Overriding a method with amore restrictiveaccess modifier would break theis-arela- tionship. If apublicmethod could be overridden as aprotectedorprivatemethod, the derived-class objects would not be able to respond to the same method calls as base-class objects. Once a method is declared in a base class, the methodmusthave thesameaccess modifier for all that class’s direct and indirect derived classes.

1 // Fig. 11.5: CommissionEmployeeTest.cs 2 // Testing class CommissionEmployee.

3 using System;

4

5 public class CommissionEmployeeTest 6 {

7 public static void Main( string[] args )

8 {

9 // instantiate CommissionEmployee object 10

11 12

13 // display CommissionEmployee data 14 Console.WriteLine(

15 "Employee information obtained by properties and methods: \n" );

16 Console.WriteLine( "First name is {0}", );

17 Console.WriteLine( "Last name is {0}", );

Fig. 11.5 | Testing classCommissionEmployee. (Part 1 of 2.)

CommissionEmployee employee = new CommissionEmployee( "Sue",

"Jones", "222-22-2222", 10000.00M, .06M );

employee.FirstName employee.LastName

11.4 Relationship between Base Classes and Derived Classes 415

11.4.2 Creating aBasePlusCommissionEmployeeClass without Using Inheritance

We now discuss the second part of our introduction to inheritance by declaring and testing the (completely new and independent) classBasePlusCommissionEmployee(Fig. 11.6), which contains a first name, last name, social security number, gross sales amount, com- mission rateandbase salary—“Base” in the class name stands for “base salary” not base class. ClassBasePlusCommissionEmployee’spublicservices include aBasePlusCommis- sionEmployeeconstructor (lines 16–26), methods Earnings (lines 113–116) and To-

String (lines 119–127), and public properties (lines 30–110) for the class’s private instance variablesfirstName, lastName, socialSecurityNumber,grossSales, commis-

sionRateandbaseSalary(declared in lines 8–13). These variables, properties and meth- ods encapsulate all the necessary features of a base-salaried commission employee. Note the similarity between this class and classCommissionEmployee(Fig. 11.4)—in this exam- ple,we do not yet exploit that similarity.

ClassBasePlusCommissionEmployeedoesnotspecify that it extendsobjectwith the syntax “: object” in line 6, so the classimplicitlyextendsobject. Also, like classCommis-

sionEmployee’s constructor (lines 14–23 of Fig. 11.4), class BasePlusCommissionEm- 18 Console.WriteLine( "Social security number is {0}",

19 );

20 Console.WriteLine( "Gross sales are {0:C}", );

21 Console.WriteLine( "Commission rate is {0:F2}",

22 );

23 Console.WriteLine( "Earnings are {0:C}", );

24 25 26 27 28 29 30

31 } // end Main

32 } // end class CommissionEmployeeTest

Employee information obtained by properties and methods:

First name is Sue Last name is Jones

Social security number is 222-22-2222 Gross sales are $10,000.00

Commission rate is 0.06 Earnings are $600.00

Updated employee information obtained by ToString:

commission employee: Sue Jones social security number: 222-22-2222 gross sales: $5,000.00

commission rate: 0.10 earnings: $500.00

Fig. 11.5 | Testing classCommissionEmployee. (Part 2 of 2.)

employee.SocialSecurityNumber

employee.GrossSales employee.CommissionRate

employee.Earnings() employee.GrossSales = 5000.00M; // set gross sales

employee.CommissionRate = .1M; // set commission rate Console.WriteLine( "\n{0}:\n\n{1}",

"Updated employee information obtained by ToString", employee );

Console.WriteLine( "earnings: {0:C}", employee.Earnings() );

ployee’s constructor invokes classobject’s default constructorimplicitly, as noted in the comment in line 19 of Fig. 11.6.

1 // Fig. 11.6: BasePlusCommissionEmployee.cs

2 // BasePlusCommissionEmployee class represents an employee that receives 3 // a base salary in addition to a commission.

4 using System;

5

6 public class BasePlusCommissionEmployee 7 {

8 private string firstName;

9 private string lastName;

10 private string socialSecurityNumber;

11 private decimal grossSales; // gross weekly sales 12 private decimal commissionRate; // commission percentage 13

14

15 // six-parameter constructor

16 public BasePlusCommissionEmployee( string first, string last, 17 string ssn, decimal sales, decimal rate, decimal salary )

18 {

19 // implicit call to object constructor occurs here 20 firstName = first;

21 lastName = last;

22 socialSecurityNumber = ssn;

23 GrossSales = sales; // validate gross sales via property 24 CommissionRate = rate; // validate commission rate via property 25

26 } // end six-parameter BasePlusCommissionEmployee constructor 27

28 // read-only property that gets

29 // BasePlusCommissionEmployee's first name 30 public string FirstName

31 {

32 get

33 {

34 return firstName;

35 } // end get

36 } // end property FirstName 37

38 // read-only property that gets

39 // BasePlusCommissionEmployee's last name 40 public string LastName

41 {

42 get

43 {

44 return lastName;

45 } // end get

46 } // end property LastName 47

Fig. 11.6 | BasePlusCommissionEmployeeclass represents an employee that receives a base salary in addition to a commission. (Part 1 of 3.)

private decimal baseSalary; // base salary per week

BaseSalary = salary; // validate base salary via property

11.4 Relationship between Base Classes and Derived Classes 417

48 // read-only property that gets

49 // BasePlusCommissionEmployee's social security number 50 public string SocialSecurityNumber

51 {

52 get

53 {

54 return socialSecurityNumber;

55 } // end get

56 } // end property SocialSecurityNumber 57

58 // property that gets and sets

59 // BasePlusCommissionEmployee's gross sales 60 public decimal GrossSales

61 {

62 get

63 {

64 return grossSales;

65 } // end get

66 set

67 {

68 if ( value >= 0 )

69 grossSales = value;

70 else

71 throw new ArgumentOutOfRangeException(

72 "GrossSales", value, "GrossSales must be >= 0" );

73 } // end set

74 } // end property GrossSales 75

76 // property that gets and sets

77 // BasePlusCommissionEmployee's commission rate 78 public decimal CommissionRate

79 {

80 get

81 {

82 return commissionRate;

83 } // end get

84 set

85 {

86 if ( value > 0 && value < 1 ) 87 commissionRate = value;

88 else

89 throw new ArgumentOutOfRangeException( "CommissionRate", 90 value, "CommissionRate must be > 0 and < 1" );

91 } // end set

92 } // end property CommissionRate 93

94 // property that gets and sets

95 // BasePlusCommissionEmployee's base salary 96

97 98 99

Fig. 11.6 | BasePlusCommissionEmployeeclass represents an employee that receives a base salary in addition to a commission. (Part 2 of 3.)

public decimal BaseSalary {

get {

ClassBasePlusCommissionEmployee’sEarningsmethod (lines 113–116) computes the earnings of a base-salaried commission employee. Line 115 adds the employee’s base salary to the product of the commission rate and the gross sales, and returns the result.

ClassBasePlusCommissionEmployeeoverridesobjectmethodToStringto return a

string containing the BasePlusCommissionEmployee’s information (lines 119–127).

Once again, we use format specifierCto format the gross sales and base salary as currency and format specifierF2to format the commission rate with two digits of precision to the right of the decimal point (line 122).

ClassBasePlusCommissionEmployeeTest

Figure 11.7 tests class BasePlusCommissionEmployee. Lines 10–12 instantiate a Base-

PlusCommissionEmployee object and pass "Bob", "Lewis", "333-33-3333", 5000.00M,

.04Mand300.00Mto the constructor as the first name, last name, social security number, gross sales, commission rate and base salary, respectively. Lines 17–25 useBasePlusCom- missionEmployee’s properties and methods to retrieve the values of the object’s instance variables and calculate the earnings for output. Line 27 invokes the object’sBaseSalary property to changethe base salary. PropertyBaseSalary’s setaccessor (Fig. 11.6, lines 102–109) ensures that instance variablebaseSalaryis not assigned a negative value, be- 100

101 102 103 104 105 106 107 108 109 110 111

112 // calculate earnings 113 public decimal Earnings()

114 {

115

116 } // end method Earnings 117

118 // return string representation of BasePlusCommissionEmployee 119 public override string ToString()

120 {

121 return string.Format(

122 "{0}: {1} {2}\n{3}: {4}\n{5}: {6:C}\n{7}: {8:F2}\n{9}: {10:C}", 123 "base-salaried commission employee", firstName, lastName, 124 "social security number", socialSecurityNumber,

125 "gross sales", grossSales, "commission rate", commissionRate,

126 );

127 } // end method ToString

128 } // end class BasePlusCommissionEmployee

Fig. 11.6 | BasePlusCommissionEmployeeclass represents an employee that receives a base salary in addition to a commission. (Part 3 of 3.)

return baseSalary;

} // end get set

{

if ( value >= 0 ) baseSalary = value;

else

throw new ArgumentOutOfRangeException( "BaseSalary", value, "BaseSalary must be >= 0" );

} // end set

} // end property BaseSalary

return baseSalary + ( commissionRate * grossSales );

"base salary", baseSalary

11.4 Relationship between Base Classes and Derived Classes 419

cause an employee’s base salary cannot be negative. Lines 29–30 of Fig. 11.7 invoke the ob- ject’sToStringmethodimplicitlyto get the object’sstringrepresentation.

1 // Fig. 11.7: BasePlusCommissionEmployeeTest.cs 2 // Testing class BasePlusCommissionEmployee.

3 using System;

4

5 public class BasePlusCommissionEmployeeTest 6 {

7 public static void Main( string[] args )

8 {

9 // instantiate BasePlusCommissionEmployee object 10

11 12 13

14 // display BasePlusCommissionEmployee's data 15 Console.WriteLine(

16 "Employee information obtained by properties and methods: \n" );

17 Console.WriteLine( "First name is {0}", );

18 Console.WriteLine( "Last name is {0}", );

19 Console.WriteLine( "Social security number is {0}",

20 );

21 Console.WriteLine( "Gross sales are {0:C}", );

22 Console.WriteLine( "Commission rate is {0:F2}",

23 );

24 Console.WriteLine( "Earnings are {0:C}", );

25 Console.WriteLine( "Base salary is {0:C}", );

26 27 28

29 Console.WriteLine( "\n{0}:\n\n{1}",

30 "Updated employee information obtained by ToString", );

31 Console.WriteLine( "earnings: {0:C}", employee.Earnings() );

32 } // end Main

33 } // end class BasePlusCommissionEmployeeTest

Employee information obtained by properties and methods:

First name is Bob Last name is Lewis

Social security number is 333-33-3333 Gross sales are $5,000.00

Commission rate is 0.04 Earnings are $500.00 Base salary is $300.00

Updated employee information obtained by ToString:

base-salaried commission employee: Bob Lewis social security number: 333-33-3333

gross sales: $5,000.00 commission rate: 0.04 base salary: $1,000.00 earnings: $1,200.00

Fig. 11.7 | Testing classBasePlusCommissionEmployee.

BasePlusCommissionEmployee employee =

new BasePlusCommissionEmployee( "Bob", "Lewis",

"333-33-3333", 5000.00M, .04M, 300.00M );

employee.FirstName employee.LastName employee.SocialSecurityNumber

employee.GrossSales employee.CommissionRate

employee.Earnings() employee.BaseSalary employee.BaseSalary = 1000.00M; // set base salary

employee

Much of the code for classBasePlusCommissionEmployee(Fig. 11.6) is similar, if not identical, to the code for classCommissionEmployee(Fig. 11.4). For example, in classBase-

PlusCommissionEmployee,privateinstance variablesfirstNameandlastNameand prop- ertiesFirstNameandLastNameare identical to those of classCommissionEmployee. Classes

CommissionEmployee and BasePlusCommissionEmployee also both contain private instance variablessocialSecurityNumber, commissionRate and grossSales, as well as properties to manipulate these variables. In addition, theBasePlusCommissionEmployee

constructor is almost identical to that of classCommissionEmployee, except that Base-

PlusCommissionEmployee’s constructor also sets thebaseSalary. The other additions to classBasePlusCommissionEmployeeareprivateinstance variablebaseSalaryand prop- ertyBaseSalary. ClassBasePlusCommissionEmployee’sEarningsmethod is nearly iden- tical to that of classCommissionEmployee, except thatBasePlusCommissionEmployee’s also adds thebaseSalary. Similarly, classBasePlusCommissionEmployee’sToStringmethod is nearly identical to that of classCommissionEmployee, except thatBasePlusCommissionEm- ployee’sToStringalso formats the value of instance variablebaseSalaryas currency.

We literallycopiedthe code from classCommissionEmployeeandpastedit into class

BasePlusCommissionEmployee, then modified class BasePlusCommissionEmployee to include a base salary and methods and properties that manipulate the base salary. This

“copy-and-paste” approach is often error prone and time consuming. Worse yet, it can spread many physical copies of the same code throughout a system, creating a code-main- tenance nightmare. Is there a way to “absorb” the members of one class in a way that makes them part of other classes without copying code? In the next several examples we answer this question, using a more elegant approach to building classes—namely,inheritance.

11.4.3 Creating aCommissionEmployee–

BasePlusCommissionEmployeeInheritance Hierarchy

Now we declare classBasePlusCommissionEmployee(Fig. 11.8), which extends classCom-

missionEmployee(Fig. 11.4). ABasePlusCommissionEmployeeobjectis aCommissionEm- ployee(because inheritance passes on the capabilities of classCommissionEmployee), but classBasePlusCommissionEmployeealso has instance variablebaseSalary(Fig. 11.8, line 7). The colon (:) in line 5 of the class declaration indicates inheritance. As a derived class,

BasePlusCommissionEmployeeinherits the members of classCommissionEmployeeand can access those members that are non-private. The constructor of classCommissionEmployee

Error-Prevention Tip 11.1

Copying and pasting code from one class to another can spread errors across multiple source-code files. To avoid duplicating code (and possibly errors) in situations where you want one class to “absorb” the members of another class, use inheritance rather than the

“copy-and-paste” approach.

Software Engineering Observation 11.3

With inheritance, the common members of all the classes in the hierarchy are declared in a base class. When changes are required for these common features, you need to make the changes only in the base class—derived classes then inherit the changes. Without inheritance, changes would need to be made to all the source-code files that contain a copy of the code in question.

Một phần của tài liệu Visual C 2012 How to Program _ www.bit.ly/taiho123 (Trang 451 - 474)

Tải bản đầy đủ (PDF)

(1.020 trang)