|
3 | 3 | <!-- Appendix: Programming Guidelines --> |
4 | 4 | # 附录:编程指南 |
5 | 5 |
|
| 6 | +> 本附录包含了有助于指导你进行低级程序设计和编写代码的建议。 |
| 7 | +
|
| 8 | +当然,这些只是指导方针,而不是规则。我们的想法是将它们用作灵感,并记住偶尔会违反这些指导方针的特殊情况。 |
6 | 9 |
|
7 | 10 | <!-- Design --> |
8 | 11 | ## 设计 |
9 | 12 |
|
| 13 | +1. **优雅总是会有回报**。从短期来看,似乎需要更长的时间才能找到一个真正优雅的问题解决方案,但是当该解决方案第一次应用并能轻松适应新情况,而不需要数小时,数天或数月的挣扎时,你会看到奖励(即使没有人可以测量它们)。它不仅为你提供了一个更容易构建和调试的程序,而且它也更容易理解和维护,这也正是经济价值所在。这一点可以通过一些经验来理解,因为当你想要使一段代码变得优雅时,你可能看起来效率不是很高。抵制急于求成的冲动,它只会减慢你的速度。 |
| 14 | + |
| 15 | +2. **先让它工作,然后再让它变快**。即使你确定一段代码非常重要并且它是你系统中的主要瓶颈**,也是如此。不要这样做。使用尽可能简单的设计使系统首先运行。然后如果速度不够快,请对其进行分析。你几乎总会发现“你的”瓶颈不是问题。节省时间,才是真正重要的东西。 |
| 16 | + |
| 17 | +3. **记住“分而治之”的原则**。如果所面临的问题太过混乱**,就去想象一下程序的基本操作,因为存在一个处理困难部分的神奇“片段”(piece)。该“片段”是一个对象,编写使用该对象的代码,然后查看该对象并将其困难部分封装到其他对象中,等等。 |
| 18 | + |
| 19 | +4. **将类创建者与类用户(客户端程序员)分开**。类用户是“客户”,不需要也不想知道类幕后发生了什么。类创建者必须是设计类的专家,他们编写类,以便新手程序员都可以使用它,并仍然可以在应用程序中稳健地工作。将该类视为其他类的*服务提供者*(service provider)。只有对其它类透明,才能很容易地使用这个类。 |
| 20 | + |
| 21 | +5. **创建类时,给类起个清晰的名字,就算不需要注释也能理解这个类**。你的目标应该是使客户端程序员的接口在概念上变得简单。为此,在适当时使用方法重载来创建直观,易用的接口。 |
| 22 | + |
| 23 | +6. **你的分析和设计必须至少能够产生系统中的类、它们的公共接口以及它们与其他类的关系,尤其是基类**。 如果你的设计方法产生的不止于此,就该问问自己,该方法生成的所有部分是否在程序的生命周期内都具有价值。如果不是,那么维护它们会很耗费精力。对于那些不会影响他们生产力的东西,开发团队的成员往往不会去维护,这是许多设计方法都没有考虑的生活现实。 |
| 24 | + |
| 25 | +7. **让一切自动化**。首先在编写类之前,编写测试代码,并将其与类保持一致。通过构建工具自动运行测试。你可能会使用事实上的标准Java构建工具Gradle。这样,通过运行测试代码可以自动验证任何更改,将能够立即发现错误。因为你知道自己拥有测试框架的安全网,所以当发现需要时,可以更大胆地进行彻底的更改。请记住,语言的巨大改进来自内置的测试,包括类型检查,异常处理等,但这些内置功能很有限,你必须完成剩下的工作,针对具体的类或程序,去完善这些测试内容,从而创建一个强大的系统。 |
| 26 | + |
| 27 | +8. **在编写类之前,先编写测试代码,以验证类的设计是完善的**。如果不编写测试代码,那么就不知道类是什么样的。此外,通过编写测试代码,往往能够激发出类中所需的其他功能或约束。而这些功能或约束并不总是出现在分析和设计过程中。测试还会提供示例代码,显示了如何使用这个类。 |
| 28 | + |
| 29 | +9. **所有的软件设计问题,都可以通过引入一个额外的间接概念层次(extra level of conceptual indirection)来解决**。这个软件工程的基本规则是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。” |
| 30 | + |
| 31 | +10. **间接(indirection)应具有意义(与准则9一致)**。这个含义可以简单到“将常用代码放在单个方法中。”如果添加没有意义的间接(抽象,封装等)级别,那么它就像没有足够的间接性那样糟糕。 |
| 32 | + |
| 33 | +11. **使类尽可能原子化**。 为每个类提供一个明确的目的,它为其他类提供一致的服务。如果你的类或系统设计变得过于复杂,请将复杂类分解为更简单的类。最直观的指标是尺寸大小,如果一个类很大,那么它可能是做的事太多了,应该被拆分。建议重新设计类的线索是: |
| 34 | + - 一个复杂的*switch*语句:考虑使用多态。 |
| 35 | + - 大量方法涵盖了很多不同类型的操作:考虑使用多个类。 |
| 36 | + - 大量成员变量涉及很多不同的特征:考虑使用多个类。 |
| 37 | + - 其他建议可以参见Martin Fowler的*Refactoring: Improving the Design of Existing Code*(重构:改善既有代码的设计)(Addison-Wesley 1999)。 |
| 38 | + |
| 39 | +12. **注意长参数列表**。那样方法调用会变得难以编写,读取和维护。相反,尝试将方法移动到更合适的类,并且(或者)将对象作为参数传递。 |
| 40 | + |
| 41 | +13. **不要重复自己**。如果一段代码出现在派生类的许多方法中,则将该代码放入基类中的单个方法中,并从派生类方法中调用它。这样不仅可以节省代码空间,而且可以轻松地传播更改。有时,发现这个通用代码会为接口添加有价值的功能。此指南的更简单版本也可以在没有继承的情况下发生:如果类具有重复代码的方法,则将该重复代码放入一个公共方,法并在其他方法中调用它。 |
| 42 | + |
| 43 | +14. **注意*switch*语句或链式*if-else*子句**。一个*类型检查编码*(type-check coding)的指示器意味着需要根据某种类型信息选择要执行的代码(确切的类型最初可能不明显)。很多时候可以用继承和多态替换这种代码,多态方法调用将会执行类型检查,并提供了更可靠和更容易的可扩展性。 |
| 44 | + |
| 45 | +15. **从设计的角度,寻找和分离那些因不变的事物而改变的事物**。也就是说,在不强制重新设计的情况下搜索可能想要更改的系统中的元素,然后将这些元素封装在类中。 |
| 46 | + |
| 47 | +16. **不要通过子类扩展基本功能**。如果一个接口元素对于类来说是必不可少的,则它应该在基类中,而不是在派生期间添加。如果要在继承期间添加方法,请考虑重新设计。 |
| 48 | + |
| 49 | +17. **少即是多**。从一个类的最小接口开始,尽可能小而简单,以解决手头的问题,但不要试图预测类的所有使用方式。在使用该类时,就将会了解如何扩展接口。但是,一旦这个类已经在使用了,就无法在不破坏客户端代码的情况下缩小接口。如果必须添加更多方法,那很好,它不会破坏代码。但即使新方法取代旧方法的功能,也只能是保留现有接口(如果需要,可以结合底层实现中的功能)。如果必须通过添加更多参数来扩展现有方法的接口,请使用新参数创建重载方法,这样,就不会影响到对现有方法的任何调用。 |
| 50 | + |
| 51 | +18. **大声读出你的类以确保它们合乎逻辑**。将基类和派生类之间的关系称为“is-a”,将成员对象称为“has-a”。 |
| 52 | + |
| 53 | +19. **在需要在继承和组合之间作决定时,问一下自己是否必须向上转换为基类型**。如果不是,则使用组合(成员对象)更好。这可以消除对多种基类型的感知需求(perceived need)。如果使用继承,则用户会认为他们应该向上转型。 |
| 54 | + |
| 55 | +20. **注意重载**。方法不应该基于参数的值而有条件地执行代码。在这里,应该创建两个或多个重载方法。 |
| 56 | + |
| 57 | +21. **使用异常层次结构**,最好是从标准Java异常层次结构中的特定适当类派生。然后,捕获异常的人可以为特定类型的异常编写处理程序,然后为基类型编写处理程序。如果添加新的派生异常,现有客户端代码仍将通过基类型捕获异常。 |
| 58 | + |
| 59 | +22. **有时简单的聚合可以完成工作**。航空公司的“乘客舒适系统”由独立的元素组成:座位,空调,影视等,但必须在飞机上创建许多这样的元素。你创建私有成员并建立一个全新的接口了吗?如果不是,在这种情况下,组件也应该是公共接口的一部分,因此应该创建公共成员对象。这些对象有自己的私有实现,这些实现仍然是安全的。请注意,简单聚合不是经常使用的解决方案,但确实会有时候会用到。 |
| 60 | + |
| 61 | +23. **考虑客户程序员和维护代码的人的观点**。设计类以便尽可能直观地被使用。预测要进行的更改,并精心设计类,以便轻松地进行更改。 |
| 62 | + |
| 63 | +24. **注意“巨型对象综合症”**(giant object syndrome)。这通常是程序员的痛苦,他们是面向对象编程的新手,总是编写面向过程程序并将其粘贴在一个或两个巨型对象中。除应用程序框架外,对象代表应用程序中的概念,而不是应用程序本身。 |
| 64 | + |
| 65 | +25. **如果你必须做一些丑陋的事情,至少要把类内的丑陋本地化**。 |
| 66 | + |
| 67 | +26. **如果必须做一些不可移植的事情,那就对这个事情做一个抽象,并在一个类中进行本地化**。这种额外的间接级别可防止在整个程序中扩散这种不可移植性。 (这个原则也体现在*桥接*模式中,等等)。 |
| 68 | + |
| 69 | +27. **对象不应该仅仅只是持有一些数据**。它们也应该有明确的行为。有时候,“数据传输对象”(data transfer objects)是合适的,但只有在泛型集合不合适时,才被明确用于打包和传输一组元素。 |
| 70 | + |
| 71 | +28. **在从现有类创建新类时首先选择组合**。仅在设计需要时才使用继承。如果在可以使用组合的地方使用继承,那么设计将会变得很复杂,这是没必要的。 |
| 72 | + |
| 73 | +29. **使用继承和覆盖方法来表达行为的差异,而不是使用字段来表示状态的变化**。如果发现一个类使用了状态变量,并且有一些方法是基于这些变量切换行为的,那么请重新设计它,以表示子类和覆盖方法中的行为差异。一个极端的反例是继承不同的类来表示颜色,而不是使用“颜色”字段。 |
| 74 | + |
| 75 | +30. **注意*协变*(variance)**。两个语义不同的对象可能具有相同的操作或职责。为了从继承中受益,会试图让其中一个成为另一个的子类,这是一种很自然的诱惑。这称为协变,但没有真正的理由去强制声明一个并不存在的父子类关系。更好的解决方案是创建一个通用基类,并为两者生成一个接口,使其成为这个通用基类的派生类。这仍然可以从继承中受益,并且这可能是关于设计的一个重要发现。 |
| 76 | + |
| 77 | +31. **在继承期间注意*限定*(limitation)**。最明确的设计为继承的类增加了新的功能。含糊的设计在继承期间删除旧功能而不添加新功能。但是规则是用来打破的,如果是通过调用一个旧的类库来工作,那么将一个现有类限制在其子类型中,可能比重构层次结构更有效,因此新类适合在旧类的上层。 |
| 78 | + |
| 79 | +32. **使用设计模式来消除“裸功能”(naked functionality)**。也就是说,如果类只需要创建一个对象,请不要推进应用程序并写下注释“只生成一个。”应该将其包装成一个单例(singleton)。如果主程序中有很多乱七八糟的代码去创建对象,那么找一个像工厂方法一样的创建模式,可以在其中封装创建过程。消除“裸功能”不仅会使代码更易于理解和维护,而且还会使其能够更加防范应对后面的善意维护者(well-intentioned maintainers)。 |
| 80 | + |
| 81 | +33. **注意“分析瘫痪”(analysis paralysis)**。记住,不得不经常在不了解整个项目的情况下推进项目,并且通常了解那些未知因素的最好、最快的方式是进入下一步而不是尝试在脑海中弄清楚。在获得解决方案之前,往往无法知道解决方案。Java有内置的防火墙,让它们为你工作。你在一个类或一组类中的错误不会破坏整个系统的完整性。 |
| 82 | + |
| 83 | +34. **如果认为自己有很好的分析,设计或实施,请做一个演练**。从团队外部带来一些人,不一定是顾问,但可以是公司内其他团体的人。用一双新眼睛评审你的工作,可以在一个更容易修复它们的阶段发现问题,而不仅仅是把大量时间和金钱全扔到演练过程中。 |
10 | 84 |
|
11 | 85 | <!-- Implementation --> |
12 | 86 | ## 实现 |
|
0 commit comments