... | ... | @@ -20,7 +20,9 @@ The ideal is for a source file to contain one, and only one, language. Realistic |
|
|
|
|
|
Following "[The Principle of Least Surprise](http://en.wikipedia.org/wiki/ Principle_of_least_astonishment)," any function or class should implement the behaviors that another programmer could reasonably expect. For example, consider a function that translates the name of a day to an `enum` that represents the day.
|
|
|
|
|
|
```java
|
|
|
Day day = DayDate.StringToDay(String dayName);
|
|
|
```
|
|
|
|
|
|
We would expect the string "Monday" to be translated to `Day.MONDAY`. We would also expect the common abbreviations to be translated, and we would expect the function to ignore case.
|
|
|
|
... | ... | @@ -34,9 +36,9 @@ There is no replacement for due diligence. Every boundary condition, every corne |
|
|
|
|
|
#### G4: Overridden Safeties
|
|
|
|
|
|
Chernobyl melted down because the plant manager overrode each of the safety mecha- nisms one by one. The safeties were making it inconvenient to run an experiment. The result was that the experiment did not get run, and the world saw it's first major civilian nuclear catastrophe.
|
|
|
Chernobyl melted down because the plant manager overrode each of the safety mechanisms one by one. The safeties were making it inconvenient to run an experiment. The result was that the experiment did not get run, and the world saw it's first major civilian nuclear catastrophe.
|
|
|
|
|
|
It is risky to override safeties. Exerting manual control over serialVersionUID may be necessary, but it is always risky. Turning off certain compiler warnings (or all warnings!) may help you get the build to succeed, but at the risk of endless debugging sessions. Turn- ing off failing tests and telling yourself you'll get them to pass later is as bad as pretending your credit cards are free money.
|
|
|
It is risky to override safeties. Exerting manual control over `serialVersionUID` may be necessary, but it is always risky. Turning off certain compiler warnings (or all warnings!) may help you get the build to succeed, but at the risk of endless debugging sessions. Turning off failing tests and telling yourself you'll get them to pass later is as bad as pretending your credit cards are free money.
|
|
|
|
|
|
#### G5: Duplication
|
|
|
|
... | ... | @@ -47,11 +49,11 @@ abstraction level. |
|
|
|
|
|
The most obvious form of duplication is when you have clumps of identical code that look like some programmers went wild with the mouse, pasting the same code over and over again. These should be replaced with simple methods.
|
|
|
|
|
|
A more subtle form is the switch/case or if/else chain that appears again and again in various modules, always testing for the same set of conditions. These should be replaced with polymorphism.
|
|
|
A more subtle form is the `switch/case` or `if/else` chain that appears again and again in various modules, always testing for the same set of conditions. These should be replaced with polymorphism.
|
|
|
|
|
|
Still more subtle are the modules that have similar algorithms, but that don’t share similar lines of code. This is still duplication and should be addressed by using the TEM- PLATE METHOD,4 or STRATEGY5 pattern.
|
|
|
Still more subtle are the modules that have similar algorithms, but that don't share similar lines of code. This is still duplication and should be addressed by using the [TEMPLATE METHOD](http://en.wikipedia.org/wiki/Template_method_pattern), or [STRATEGY PATTERN](http://en.wikipedia.org/wiki/Strategy_pattern).
|
|
|
|
|
|
Indeed, most of the design patterns that have appeared in the last fifteen years are sim- ply well-known ways to eliminate duplication. So too the Codd Normal Forms are a strat- egy for eliminating duplication in database schemae. OO itself is a strategy for organizing modules and eliminating duplication. Not surprisingly, so is structured programming.
|
|
|
Indeed, most of the design patterns that have appeared in the last fifteen years are simply well-known ways to eliminate duplication. So too the Codd Normal Forms are a strategy for eliminating duplication in database schemae. OO itself is a strategy for organizing modules and eliminating duplication. Not surprisingly, so is structured programming.
|
|
|
|
|
|
I think the point has been made. Find and eliminate duplication wherever you can.
|
|
|
|
... | ... | @@ -59,17 +61,296 @@ I think the point has been made. Find and eliminate duplication wherever you can |
|
|
|
|
|
It is important to create abstractions that separate higher level general concepts from lower level detailed concepts. Sometimes we do this by creating abstract classes to hold the higher level concepts and derivatives to hold the lower level concepts. When we do this, we need to make sure that the separation is complete. We want all the lower level concepts to be in the derivatives and all the higher level concepts to be in the base class.
|
|
|
|
|
|
For example, constants, variables, or utility functions that pertain only to the detailed implementation should not be present in the base class. The base class should know noth- ing about them.
|
|
|
For example, constants, variables, or utility functions that pertain only to the detailed implementation should not be present in the base class. The base class should know nothing about them.
|
|
|
|
|
|
This rule also pertains to source files, components, and modules. Good software design requires that we separate concepts at different levels and place them in different containers. Sometimes these containers are base classes or derivatives and sometimes they are source files, modules, or components. Whatever the case may be, the separation needs to be complete. We don’t want lower and higher level concepts mixed together.
|
|
|
This rule also pertains to source files, components, and modules. Good software design requires that we separate concepts at different levels and place them in different containers. Sometimes these containers are base classes or derivatives and sometimes they are source files, modules, or components. Whatever the case may be, the separation needs to be complete. We don't want lower and higher level concepts mixed together.
|
|
|
|
|
|
Consider the following code:
|
|
|
|
|
|
public interface Stack {
|
|
|
Object pop() throws EmptyException;
|
|
|
void push(Object o) throws FullException; double percentFull();
|
|
|
class EmptyException extends Exception {}
|
|
|
class FullException extends Exception {} }
|
|
|
|
|
|
The percentFull function is at the wrong level of abstraction. Although there are many implementations of Stack where the concept of fullness is reasonable, there are other implementations that simply could not know how full they are. So the function would be better placed in a derivative interface such as BoundedStack.
|
|
|
|
|
|
Perhaps you are thinking that the implementation could just return zero if the stack were boundless. The problem with that is that no stack is truly boundless. You cannot really prevent an OutOfMemoryException by checking for
|
|
|
stack.percentFull() < 50.0.
|
|
|
|
|
|
Implementing the function to return 0 would be telling a lie.
|
|
|
|
|
|
The point is that you cannot lie or fake your way out of a misplaced abstraction. Isolating abstractions is one of the hardest things that software developers do, and there is no quick fix when you get it wrong.
|
|
|
|
|
|
#### G7: Base Classes Depending on Their Derivatives
|
|
|
|
|
|
The most common reason for partitioning concepts into base and derivative classes is so that the higher level base class concepts can be independent of the lower level derivative class concepts. Therefore, when we see base classes mentioning the names of their derivatives, we suspect a problem. In general, base classes should know nothing about their derivatives.
|
|
|
|
|
|
There are exceptions to this rule, of course. Sometimes the number of derivatives is strictly fixed, and the base class has code that selects between the derivatives. We see this a lot in finite state machine implementations. However, in that case the derivatives and base class are strongly coupled and always deploy together in the same jar file. In the general case we want to be able to deploy derivatives and bases in different jar files.
|
|
|
|
|
|
Deploying derivatives and bases in different jar files and making sure the base jar files know nothing about the contents of the derivative jar files allow us to deploy our systems in discrete and independent components. When such components are modified, they can be redeployed without having to redeploy the base components. This means that the impact of a change is greatly lessened, and maintaining systems in the field is made much simpler.
|
|
|
|
|
|
#### G8: Too Much Information
|
|
|
|
|
|
Well-defined modules have very small interfaces that allow you to do a lot with a little. Poorly defined modules have wide and deep interfaces that force you to use many different gestures to get simple things done. A well-defined interface does not offer very many functions to depend upon, so coupling is low. A poorly defined interface provides lots of functions that you must call, so coupling is high.
|
|
|
|
|
|
Good software developers learn to limit what they expose at the interfaces of their classes and modules. The fewer methods a class has, the better. The fewer variables a function knows about, the better. The fewer instance variables a class has, the better.
|
|
|
Hide your data. Hide your utility functions. Hide your constants and your temporaries. Don't create classes with lots of methods or lots of instance variables. Don't create lots of protected variables and functions for your subclasses. Concentrate on keeping interfaces very tight and very small. Help keep coupling low by limiting information.
|
|
|
|
|
|
#### G9: Dead Code
|
|
|
|
|
|
Dead code is code that isn't executed. You find it in the body of an if statement that checks for a condition that can't happen. You find it in the catch block of a try that never throws. You find it in little utility methods that are never called or `switch/case` conditions that never occur.
|
|
|
|
|
|
The problem with dead code is that after awhile it starts to smell. The older it is, the stronger and sourer the odor becomes. This is because dead code is not completely updated when designs change. It still compiles, but it does not follow newer conventions or rules. It was written at a time when the system was different. When you find dead code, do the right thing. Give it a decent burial. Delete it from the system.
|
|
|
|
|
|
#### G10: Vertical Separation
|
|
|
|
|
|
Variables and function should be defined close to where they are used. Local variables should be declared just above their first usage and should have a small vertical scope. We don't want local variables declared hundreds of lines distant from their usages.
|
|
|
|
|
|
Private functions should be defined just below their first usage. Private functions belong to the scope of the whole class, but we'd still like to limit the vertical distance between the invocations and definitions. Finding a private function should just be a matter of scanning downward from the first usage.
|
|
|
|
|
|
#### G11: Inconsistency
|
|
|
|
|
|
If you do something a certain way, do all similar things in the same way. This goes back to the principle of least surprise. Be careful with the conventions you choose, and once chosen, be careful to continue to follow them.
|
|
|
|
|
|
If within a particular function you use a variable named `response` to hold an `HttpServletResponse`, then use the same variable name consistently in the other functions that use `HttpServletResponse` objects. If you name a method `processVerificationRequest`, then use a similar name, such as `processDeletionRequest`, for the methods that process other kinds of requests.
|
|
|
|
|
|
Simple consistency like this, when reliably applied, can make code much easier to read and modify.
|
|
|
|
|
|
|
|
|
#### G12: Clutter
|
|
|
|
|
|
Of what use is a default constructor with no implementation? All it serves to do is clutter up the code with meaningless artifacts. Variables that aren't used, functions that are never called, comments that add no information, and so forth. All these things are clutter and should be removed. Keep your source files clean, well organized, and free of clutter.
|
|
|
|
|
|
#### G13: Artificial Coupling
|
|
|
|
|
|
Things that don't depend upon each other should not be artificially coupled. For example, general `enums` should not be contained within more specific classes because this forces the whole application to know about these more specific classes. The same goes for general purpose `static` functions being declared in specific classes.
|
|
|
|
|
|
In general an artificial coupling is a coupling between two modules that serves no direct purpose. It is a result of putting a variable, constant, or function in a temporarily convenient, though inappropriate, location. This is lazy and careless.
|
|
|
|
|
|
Take the time to figure out where functions, constants, and variables ought to be declared. Don't just toss them in the most convenient place at hand and then leave them there.
|
|
|
|
|
|
#### G14: Feature Envy
|
|
|
|
|
|
This is one of Martin Fowler's code smells. The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes. When a method uses accessors and mutators of some other object to manipulate the data within that object, then it envies the scope of the class of that other object. It wishes that it were inside that other class so that it could have direct access to the variables it is manipulating. For example:
|
|
|
|
|
|
```java
|
|
|
public class HourlyPayCalculator {
|
|
|
public Money calculateWeeklyPay(HourlyEmployee e) {
|
|
|
int tenthRate = e.getTenthRate().getPennies();
|
|
|
int tenthsWorked = e.getTenthsWorked();
|
|
|
int straightTime = Math.min(400, tenthsWorked);
|
|
|
int overTime = Math.max(0, tenthsWorked - straightTime); int straightPay = straightTime * tenthRate;
|
|
|
int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
|
|
|
return new Money(straightPay + overtimePay);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
The `calculateWeeklyPay` method reaches into the `HourlyEmployee` object to get the data on which it operates. The `calculateWeeklyPay` method envies the scope of `HourlyEmployee`. It "wishes" that it could be inside `HourlyEmployee`.
|
|
|
|
|
|
All else being equal, we want to eliminate Feature Envy because it exposes the internals of one class to another. Sometimes, however, Feature Envy is a necessary evil. Consider the following:
|
|
|
|
|
|
```java
|
|
|
public class HourlyEmployeeReport {
|
|
|
private HourlyEmployee employee ;
|
|
|
|
|
|
public HourlyEmployeeReport(HourlyEmployee e) {
|
|
|
this.employee = e;
|
|
|
}
|
|
|
String reportHours() {
|
|
|
return String.format(
|
|
|
"Name: %s\tHours:%d.%1d\n",
|
|
|
employee.getName(),
|
|
|
employee.getTenthsWorked()/10,
|
|
|
employee.getTenthsWorked()%10);
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Clearly, the `reportHours` method envies the `HourlyEmployee` class. On the other hand, we don't want `HourlyEmployee` to have to know about the format of the report. Moving that format string into the `HourlyEmployee` class would violate several principles of object oriented design (Specifically, the [Single Responsibility Principle](http://en.wikipedia.org/wiki/Single_responsibility_principle), the [Open Closed Principle](http://en.wikipedia.org/wiki/Open/closed_principle), and the [Common Closure Principle](http://en.wikipedia.org/wiki/Package_principles).). It would couple `HourlyEmployee` to the format of the report, exposing it to changes in that format.
|
|
|
|
|
|
#### G15: Selector Arguments
|
|
|
|
|
|
There is hardly anything more abominable than a dangling `false` argument at the end of a function call. What does it mean? What would it change if it were `true`? Not only is the purpose of a selector argument difficult to remember, each selector argument combines many functions into one. Selector arguments are just a lazy way to avoid splitting a large function into several smaller functions. Consider:
|
|
|
|
|
|
```java
|
|
|
public int calculateWeeklyPay(boolean overtime) {
|
|
|
int tenthRate = getTenthRate();
|
|
|
int tenthsWorked = getTenthsWorked();
|
|
|
int straightTime = Math.min(400, tenthsWorked);
|
|
|
int overTime = Math.max(0, tenthsWorked - straightTime);
|
|
|
int straightPay = straightTime * tenthRate;
|
|
|
double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
|
|
|
int overtimePay = (int)Math.round(overTime*overtimeRate);
|
|
|
return straightPay + overtimePay;
|
|
|
}
|
|
|
```
|
|
|
|
|
|
|
|
|
You call this function with a `true` if overtime is paid as time and a half, and with a `false` if overtime is paid as straight time. It's bad enough that you must remember what `calculateWeeklyPay(false)` means whenever you happen to stumble across it. But the real shame of a function like this is that the author missed the opportunity to write the
|
|
|
following:
|
|
|
|
|
|
```java
|
|
|
public int straightPay() {
|
|
|
return getTenthsWorked() * getTenthRate();
|
|
|
}
|
|
|
public int overTimePay() {
|
|
|
int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
|
|
|
int overTimePay = overTimeBonus(overTimeTenths);
|
|
|
return straightPay() + overTimePay;
|
|
|
}
|
|
|
private int overTimeBonus(int overTimeTenths) {
|
|
|
double bonus = 0.5 * getTenthRate() * overTimeTenths;
|
|
|
return (int) Math.round(bonus);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
|
|
|
Of course, selectors need not be `boolean`. They can be enums, integers, or any other type of argument that is used to select the behavior of the function. In general it is better to have many functions than to pass some code into a function to select the behavior.
|
|
|
|
|
|
#### G16: Obscured Intent
|
|
|
|
|
|
We want code to be as expressive as possible. Run-on expressions, Hungarian notation, and magic numbers all obscure the author's intent. For example, here is the `overTimePay` function as it might have appeared:
|
|
|
|
|
|
```java
|
|
|
public int m_otCalc() {
|
|
|
return iThsWkd * iThsRte +
|
|
|
(int) Math.round(0.5 * iThsRte *
|
|
|
Math.max(0, iThsWkd - 400)
|
|
|
);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
Small and dense as this might appear, it's also virtually impenetrable. It is worth taking the time to make the intent of our code visible to our readers.
|
|
|
|
|
|
|
|
|
#### G17: Misplaced Responsibility
|
|
|
|
|
|
One of the most important decisions a software developer can make is where to put code. For example, where should the PI constant go? Should it be in the `Math` class? Perhaps it belongs in the `Trigonometry` class? Or maybe in the `Circle` class?
|
|
|
|
|
|
The principle of least surprise comes into play here. Code should be placed where a reader would naturally expect it to be. The `PI` constant should go where the trig functions are declared. The OVERTIME_RATE constant should be declared in the `HourlyPayCalculator` class.
|
|
|
|
|
|
Sometimes we get "clever" about where to put certain functionality. We'll put it in a function that's convenient for us, but not necessarily intuitive to the reader. For example, perhaps we need to print a report with the total of hours that an employee worked. We could sum up those hours in the code that prints the report, or we could try to keep a running total in the code that accepts time cards.
|
|
|
|
|
|
One way to make this decision is to look at the names of the functions. Let's say that our report module has a function named `getTotalHours`. Let's also say that the module that accepts time cards has a `saveTimeCard` function. Which of these two functions, by it's name, implies that it calculates the total? The answer should be obvious.
|
|
|
|
|
|
Clearly, there are sometimes performance reasons why the total should be calculated as time cards are accepted rather than when the report is printed. That's fine, but the names of the functions ought to reflect this. For example, there should be a `computeRunningTotalOfHours` function in the timecard module.
|
|
|
|
|
|
#### G18: Inappropriate Static
|
|
|
|
|
|
`Math.max(double a, double b)` is a good static method. It does not operate on a single instance; indeed, it would be silly to have to say `new Math().max(a,b)` or even `a.max(b)`. All the data that max uses comes from its two arguments, and not from any "owning" object. More to the point, there is almost no chance that we'd want `Math.max` to be polymorphic.
|
|
|
|
|
|
Sometimes, however, we write static functions that should not be static. For example, consider:
|
|
|
|
|
|
```java
|
|
|
HourlyPayCalculator.calculatePay(employee, overtimeRate).
|
|
|
```
|
|
|
|
|
|
Again, this seems like a reasonable `static` function. It doesn't operate on any particular object and gets all it's data from it's arguments. However, there is a reasonable chance that we'll want this function to be polymorphic. We may wish to implement several different algorithms for calculating hourly pay, for example, `OvertimeHourlyPayCalculator` and `StraightTimeHourlyPayCalculator`. So in this case the function should not be static. It should be a nonstatic member function of `Employee`.
|
|
|
|
|
|
In general you should prefer nonstatic methods to static methods. When in doubt, make the function nonstatic. If you really want a function to be static, make sure that there is no chance that you'll want it to behave polymorphically.
|
|
|
|
|
|
#### G19: Use Explanatory Variables
|
|
|
|
|
|
Kent Beck wrote about this in his great book Smalltalk Best Practice Patterns and again more recently in his equally great book Implementation Patterns. One of the more powerful ways to make a program readable is to break the calculations up into intermediate values that are held in variables with meaningful names.
|
|
|
|
|
|
|
|
|
Consider this example from FitNesse:
|
|
|
|
|
|
```java
|
|
|
Matcher match = headerPattern.matcher(line);
|
|
|
if(match.find())
|
|
|
{
|
|
|
String key = match.group(1);
|
|
|
String value = match.group(2);
|
|
|
headers.put(key.toLowerCase(), value);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
The simple use of explanatory variables makes it clear that the first matched group is the key, and the second matched group is the value.
|
|
|
|
|
|
It is hard to overdo this. More explanatory variables are generally better than fewer. It is remarkable how an opaque module can suddenly become transparent simply by breaking the calculations up into well-named intermediate values.
|
|
|
|
|
|
#### G20: Function Names Should Say What They Do
|
|
|
|
|
|
Look at this code:
|
|
|
|
|
|
```java
|
|
|
Date newDate = date.add(5);
|
|
|
```
|
|
|
|
|
|
Would you expect this to add five days to the date? Or is it weeks, or hours? Is the `date` instance changed or does the function just return a new `Date` without changing the old one? You can't tell from the call what the function does.
|
|
|
|
|
|
If the function adds five days to the date and changes the date, then it should be called `addDaysTo` or `increaseByDays`. If, on the other hand, the function returns a new date that is five days later but does not change the date instance, it should be called `daysLater` or `daysSince`.
|
|
|
|
|
|
If you have to look at the implementation (or documentation) of the function to know what it does, then you should work to find a better name or rearrange the functionality so that it can be placed in functions with better names.
|
|
|
|
|
|
#### G21: Understand the Algorithm
|
|
|
|
|
|
Lots of very funny code is written because people don't take the time to understand the algorithm. They get something to work by plugging in enough `if` statements and flags, without really stopping to consider what is really going on.
|
|
|
|
|
|
Programming is often an exploration. You think you know the right algorithm for something, but then you wind up fiddling with it, prodding and poking at it, until you get it to "work." How do you know it "works"? Because it passes the test cases you can think of.
|
|
|
|
|
|
There is nothing wrong with this approach. Indeed, often it is the only way to get a function to do what you think it should. However, it is not sufficient to leave the quotation marks around the word "work."
|
|
|
|
|
|
Before you consider yourself to be done with a function, make sure you understand how it works. It is not good enough that it passes all the tests. You must know that the solution is correct.
|
|
|
|
|
|
Often the best way to gain this knowledge and understanding is to refactor the func- tion into something that is so clean and expressive that it is obvious how it works.
|
|
|
|
|
|
G22: Make Logical Dependencies Physical
|
|
|
|
|
|
If one module depends upon another, that dependency should be physical, not just logical. The dependent module should not make assumptions (in other words, logical dependencies) about the module it depends upon. Rather it should explicitly ask that module for all the information it depends upon.
|
|
|
|
|
|
For example, imagine that you are writing a function that prints a plain text report of hours worked by employees. One class named `HourlyReporter` gathers all the data into a convenient form and then passes it to `HourlyReportFormatter` to print it.
|
|
|
|
|
|
```java
|
|
|
public class HourlyReporter {
|
|
|
private HourlyReportFormatter formatter;
|
|
|
private List<LineItem> page;
|
|
|
private final int PAGE_SIZE = 55;
|
|
|
|
|
|
public HourlyReporter(HourlyReportFormatter formatter) {
|
|
|
this.formatter = formatter;
|
|
|
page = new ArrayList<LineItem>();
|
|
|
}
|
|
|
|
|
|
public void generateReport(List<HourlyEmployee> employees) {
|
|
|
for (HourlyEmployee e : employees) {
|
|
|
addLineItemToPage(e);
|
|
|
if (page.size() == PAGE_SIZE)
|
|
|
printAndClearItemList();
|
|
|
}
|
|
|
if (page.size() > 0)
|
|
|
printAndClearItemList();
|
|
|
}
|
|
|
|
|
|
private void printAndClearItemList() {
|
|
|
formatter.format(page);
|
|
|
page.clear();
|
|
|
}
|
|
|
|
|
|
private void addLineItemToPage(HourlyEmployee e) {
|
|
|
LineItem item = new LineItem();
|
|
|
item.name = e.getName();
|
|
|
item.hours = e.getTenthsWorked() / 10;
|
|
|
item.tenths = e.getTenthsWorked() % 10;
|
|
|
page.add(item);
|
|
|
}
|
|
|
|
|
|
public class LineItem {
|
|
|
public String name;
|
|
|
public int hours;
|
|
|
public int tenths;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
This code has a logical dependency that has not been physicalized. Can you spot it? It is the constant `PAGE_SIZE`. Why should the `HourlyReporter` know the size of the page? Page size should be the responsibility of the `HourlyReportFormatter`.
|
|
|
|
|
|
The fact that PAGE_SIZE is declared in `HourlyReporter` represents a misplaced responsibility [G17] that causes `HourlyReporter` to assume that it knows what the page size ought to be. Such an assumption is a logical dependency. `HourlyReporter` depends on the fact that `HourlyReportFormatter` can deal with page sizes of 55. If some implementation of `HourlyReportFormatter` could not deal with such sizes, then there would be an error.
|
|
|
|
|
|
We can physicalize this dependency by creating a new method in `HourlyReportFormatter` named `getMaxPageSize()`. `HourlyReporter` will then call that function rather than using the `PAGE_SIZE` constant.
|
|
|
|
|
|
|
|
|
|
... | ... | |