【封装那些事】 未利用封装

未利用封装

客户代码使用显式类型检查(使用一系列if-else或switch语句检查对象的类型),而不利用出层次结构内已封装的类型变化时,将导致这种坏味。

为什么要利用封装?

一种臭名昭著的坏味是,在客户代码中使用条件语句(if-else或switch语句)来显式地检查类型,并根据类型执行相应的操作。我们这里讨论的是:要检查的类型都封装在了层次结构中,但没有利用这一点,即使用显式类型检查,而不依赖于动态多态性。这将导致如下问题:

  • 显式类型检查让客户程序和具体类型紧密耦合,降低了设计的可维护性。例如,引入新类型后,必须修改客户程序,在其中检查新类型以及执行相应操作的代码。
  • 客户程序必须显式地检查层次结构中所有相关的类型。如果未检查一个或多个这样的类型,客户程序在运行阶段可能出现意外的行为。相反,如果利用了运行时多态,完全可以避免这种问题。

未利用封装潜在的原因

以过程型思维使用面向对象语言

开发时的思维是以代码执行过程为导向,自然而然就会使用if-else语句和switch语句。

未应用面向对象原则

无力将面向对象的概念付诸实践。

示例分析一

根为抽象类DataBuffer的层次结构封装了各种基本数据结构型数组,DataBuffer的子类DataBufferByte、DataBufferUShort、DataBufferInt支持相应的基本数据类型数组。DataBuffer定义了常量TYPEBYTE、TYPEUSHORT、TYPEINT。客户程序使用TYPEBYTE、TYPEUSHORT、TYPEINT的DataBuffer来存储数据。

下面是客户程序的示例,演示如何使用switch语句执行针对具体类型的显式类型检查。

switch (transferType)
{
    case DataBuffer.TYPE_BYTE:
        byte[] bdata = (byte[])inData;
        pixel = bdata[0] & 0xff;
        length = bdata.Length;
        break;
    case DataBuffer.TYPE_USHORT:
        short[] sdata = (short[])inData;
        pixel = sdata[0] & 0xffff;
        length = sdata.Length;
        break;
    case DataBuffer.TYPE_INT:
        int[] idata = (int[])inData;
        pixel = idata[0];
        length = idata.Length;
        break;
    default:
        throw new Exception("不支持的transferType");
}

上面代码使用的数据成员transferType定义如下:

protected int transferType;

重构建议:将决定行为的条件语句删除,并在层次结构中引入多态方法。

在客户程序中,提供合适的DataBuffer子类对象。在DataBuffer层次结构类型中,定义方法GetPixel()和GetLengthl()。

public abstract class DataBuffer
{
    public const int TYPE_BYTE = 1;
    public const int TYPE_DOUBLE = 2;
    public const int TYPE_FLOAT = 3;
    public const int TYPE_INT = 4;
    public const int TYPE_USHORT = 5;

    public abstract int GetPixel(object inData);
    public abstract int GetLength(object inData);
}

public class DataBufferInt: DataBuffer
{
    public override int GetPixel(object inData)
    {
        int[] idata = (int[])inData;
        return  idata[0];
    }
    public override int GetLength(object inData)
    {
        int[] idata = (int[])inData;
        return idata.Length;
    }
}

public class DataBufferByte : DataBuffer
{
    public override int GetPixel(object inData)
    {
        byte[] bdata = (byte[])inData;
        return bdata[0] & 0xff;
    }
    public override int GetLength(object inData)
    {
        byte[] bdata = (byte[])inData;
        return bdata.Length;
    }
}

public class DataBufferUShort : DataBuffer
{
    public override int GetPixel(object inData)
    {
        short[] sdata = (short[])inData;
        return sdata[0] & 0xffff;  
    }
    public override int GetLength(object inData)
    {
        short[] sdata = (short[])inData;
        return sdata.Length;
    }
}

并将客户程序switch语句及其case语句简化为:

int pixel = GetPixel(inData);
int length = GetLength(inData);

由于引用dataBuffer指向的是传入的DataBuffer子类对象,因此上述语句将调用相应子类的GetPixel()和GetLength()方法。这里需要注意的是客户程序代码提供特定DataBuffer子类对象,检查输入数据类型和创建DataBuffer子类对象的工作由客户程序负责。可能需要在客户代码或一个工厂类中使用switch-case语句,而只需要使用一次这个switch-case语句。由于客户程序不知道具体是哪个DataBuffer子类,所以它与DataBuffer层次结构耦合更低。这样在DataBuffer层次结构修改既有类型和添加新类型时,不会对客户程序造成影响。即使有影响也是只需要使用一次的这个switch-case语句,修改代码代价极小。

这让我想起,我在看完《重构》后天真幼稚的想消除项目中的switch-case语句,只要项目中存在switch-case语句我就觉得存在坏味道,此后的一段时间我很痛苦,因为项目中总是存在消灭不了的switch-case语句。其实如果项目中需要与外部世界的实体交互,要避免使用条件逻辑很难。例如用户在页面的操作在代码中肯定对应不同的对象来处理,这中间必须使用条件逻辑判断使用哪个对象处理。但是这样的判断应该只有一处,负责日后的代码维护是个灾难。

示例分析二

还是那句话switch-case语句和if-else语句不可怕,可怕的是多个witch-case语句和if-else语句。

对于这样的代码我们要给予充分的关注:

代码1:

if(obj is XXX)
{
    //做事情A
}
if(obj is YYY)
{
    //做事情B
}
if(obj is ZZZ)
{
    //做事情C
}

代码2:

if(obj is XXX)
{
    //做事情A
}
if(obj is YYY)
{
    //做事情B
}
if(obj is ZZZ)
{
    //做事情C
}

代码3:

if(obj is XXX)
{
    //做事情A
}
if(obj is YYY)
{
    //做事情B
}
if(obj is ZZZ)
{
    //做事情C
}

这样的代码是难以扩展的,新增一个类NNN,就需要找到代码1、2、3甚至n进行修改,很容易遗漏。而且遗漏造成的错误只用在代码运行阶段才能发现。

这种情况反映出来的问题就是没有利用封装,已经有了层次结构,却没有予以利用。没有面向接口编程,每个地方面向的都是具体的实现类,每个地方都需要判断实例的类型才可以进行下一步的动作。

进行重构:

代码1:

obj.DoSomething1();

代码2:

obj.DoSomething2();

代码3:

obj.DoSomething3();

obj可以是XXX、YYY、ZZZ。对于现在的代码,新增一个类NNN,代码1、2、3甚至n处根本不需要任何改动。因为它们实现了统一的接口,并且符合开闭原则。

参考:《软件设计重构》

原文发布于微信公众号 - 撸码那些事(lumanxs)

原文发表时间:2018-05-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

《Effective Modern C++》读书笔记

Note:为避免各种侵权问题,本文并没有复制原书任意文字(代码除外,作者已经声明代码可以被使用)。需要原书完整中文翻译的读者请等待官方译本的发布。

952
来自专栏海纳周报

【第二期】一次学透java.io

java.io是新手学习Java的第一个难点。因为这个package中的东西比较多,也比较复杂,另外加上一些接口太过于面向对象了,更加增大了学习的难度。这一期,...

35910
来自专栏Java技术

关于Java代码优化的N条建议!

本文是作者:五月的仓颉 结合自己的工作和平时学习的体验重新谈一下为什么要进行代码优化。在修改之前,作者的说法是这样的:

752
来自专栏HTML5学堂

面向对象系列讲解—面向对象的含义&工厂模式

HTML5学堂:在上一篇文章当中,我们把对象进行了基本的解释,本文当中,我将为大家解释什么是面向对象?为何要使用面向对象,而不用面向过程,面向对象又有什么好处,...

2726
来自专栏ImportSource

JVM中的“同步”到底是怎么实现的?

JVM中的Synchronization是使用monitor entry和exit来实现的。不管是显式的还是隐式的。显式的是通过使用monitorenter和m...

2715
来自专栏编程

Java8新特性第3章

Stream作为Java8的新特性之一,他与Java IO包中的InputStream和OutputStream完全不是一个概念。Java8中的Stream是对...

1897
来自专栏noteless

[三]JavaIO之IO体系类整体设计思路 流的概念以及四大基础分类

从数据库,从文件,从内存,从网络拿数据,你都可以叫做输入,数据写出,都可以叫做输出,这并没有什么好纠结的

642
来自专栏Java面试笔试题

Java中有几种类型的流?

字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主...

592
来自专栏Golang语言社区

go(golang)中的类型转换

在使用 go 这样的强类型语言时,我们常常会遇到类型转换的问题。比如 int 类型转 int64,interface{} 转 struct ,对一种类型取指针、...

47410
来自专栏前端儿

DOM中 property 和 attribute 详解

其实Attribute和Property这两个单词,翻译出来都是“属性”,《js高级程序设计》书中翻译为“特性”和“属性”,以示区别。从而我们也可以顾名思义

812

扫码关注云+社区