在进行单个类的设计时,设定的类接口及其承载的职责就是其外部表现,成员变量和成员方法是其内部结构。
1.内部结构
如果忽视了内部结构的复杂性和坚固性需要,就很容易产生“ N 个接口→ N 个方法,围绕方法组织成员变量”的错误设计思路。
如果认识到内部结构还要考虑坚固性和美感,那么就需要将不同方法间的代码重复抽取出来建立新的私有方法,需要将复杂方法分解为多个更简单的私有方法。
例如,有电商系统的购物车类如下:
其中,processNormalUser方法的代码如下:
如果不考虑内部结构质量,上述代码是可以接受的。但是如果考虑内部结构质量,上述代码就存在瑕疵了:方法内部执行了多个相对独立的业务逻辑(各个空行分割的代码片段),可以将它们分别定为独立的私有方法,然后在processNormalUser方法中调用它们,如下代码所示(省略各个私有方法的定义)。
展开processVIPUser和processInternalUser两个方法,它们的其他代码都一样,只是在处理运费和商品优惠时有所区别。
这种情况下,建立各个私有方法的类实现就明显有更高的质量:更易于理解、容易复用(复用重复代码)、易于修改(一处修改多处有效)。
关注内部结构还可以根据职责需要建立更易用的抽象数据类型,并重构类的结构和方法。例如商品Item使用成员变量int ID作为商品标识。
相比较之下,可以将ID构建为抽象数据类型Key:
可以设计Key拥有“合法验证”“顺序递增”等方法职责,那么很明显Key的方案在正确性表现上会高于使用int的方案。
2.外部表现
如果忽视了外部表现的抽象和简洁性需要,就会把一些类的内部结构不必要地暴露给外部,带来额外风险。
如果认识到外部表现需要简洁和抽象,就会仔细斟酌类的职责,把不需要暴露给外部的内容都保护起来,实现真正的封装和信息隐藏。
最典型的场景是JavaBean的读取器getter和修改器setter,不加区分地在getter中返回成员变量值、在setter中修改成员变量值,就可能会导致不谨慎的内部结构暴露。如下代码所示,它将speed属性间接完全暴露给了外部,如果setSpeed时将newSpeed设置为不合法的数字(例如负数),就可能引入错误。
要考虑到,getter中可以返回加工的值,setter中可以检查后再修改,如下面所示。
上述代码对setSpeed方法的修正提高了代码的可靠性。
又如,如下代码暴露了成员变量tracks是List结构。如果Album将tracks修改为Map类型,就会影响到使用getTracks的Client。
相比较之下,下面的代码实现了对tracks结构类型的隐藏,得到Iterator的Client并不知道Album使用了怎样的数据类型,Album可以任意地调整tracks的类型。
再例如,在图1-27的设计方案中,Account在方法getHolder返回值中暴露了自己拥有的Customer成员变量。FundsTransfer得到并操作Customer会导致出现新的耦合。如果修改Customer,从类图上看只会影响Account,但事实上因为getHolder方法的暴露,FundsTransfer也会受到影响,可修改性和可复用性都有瑕疵。
图1-27 成员变量暴露示例
一个改进的方案如图1-28所示。通过使用委托方法isHolderMonitored,Account保护了成员变量Customer,FundsTransfer与Customer之间是完全解耦的,可复用性和可修改性都得到了保障。
图1-28 成员变量保护示例