ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

多态

2020-04-02 10:01:36  阅读:225  来源: 互联网

标签:子类 代码 多态 耦合 apply public


——“面向对象的三大特性是什么?”

——“封装、继承、多态。”

这大概是最容易回答的面试题了。但是,封装、继承、多态到底是什么?它们在面向对象中起到了什么样的作用呢?


多态

多态(Polymorphic)其实也是一个顾名思义的词:“多态”就是一种事物的“多”种形“态”。

↑ 例如《超能勇士》中的猩猩将军就有三种形态:原始形态、金属变体形态、无敌金刚形态。

“多态”这个概念在面向对象中同样有多种形态:类的多态、方法的多态、以及实例的多态。

类的多态

类的多态最常见、也最好理解。子类继承了父类(包括实现某个接口)后,父类就有了多种形态:它既可以是父类本身,也可以是它的子类A,还可以是它的子类B、甚至可以是子类A的子类A1……在《继承》一文中,这种多态的例子比比皆是,这里就不赘述了。

方法的多态

绝大多数情况下,子类继承了父类之后都会重写父类的方法实现。这时,同一个方法就有了多种不同的实现,这就是一种方法的多态。当然,除了重写之外,还有一种方法的多态叫做“重载”,即通过修改方法的参数列表(参数类型、参数个数等)来为同一个方法提供多种实现。

不过我个人不太喜欢重载,也不太愿意把归入多态之中。Java以方法名+参数列表作为一个方法的“签名”,而重载修改了参数列表,也就修改了方法签名。这时,重载后的这些方法还能称为“同一个方法”的不同实现吗?此外,Java会在编译期就确定使用哪个重载方法,但只有到了运行时才能知道使用的是哪个类重写的方法。也就是说,重载并不具备多态所应有的“在运行时改变程序功能”的作用。套用继承章节中的一句话来说:如果重载甚至不能“like-a”多态,那还能说它“is-a”多态吗?

例如,在下面这段代码中,虽然i_am_a_set实际上是一个Set,并且test(Set<String>)方法也能更精确地匹配到它,但是调用test(i_am_a_set)时,还是执行了test(Collection<String>)方法。

public class OverLoadTest{
    private static void test(Set<String> set){
        System.out.println("set:"+set);
    }
    private static void test(Collection<String> collection){
        System.out.println("collection:"+collection);
    }
    public static void main(String[] args){
        Collection<String> i_am_a_set = new HashSet<>();
        // 这里还是会调用test(Collection<String>)方法
        test(i_am_a_set);
    }
}

当然,重载不算多态只是我的一家之言,姑妄言之姑妄听之吧。

实例的多态

实例的多态同样是一家之言:同一个类拥有多个实例,并且每个实例中的数据各不相同,那么这就是一种实例的多态。考虑到“对象”不仅指编译期间的静态的类、也包括了运行期间的动态的实例,其实实例的多态和类的多态一样,也是同一个对象的多种形态。从这个角度来看,实例的多态也可以理解为一种更广义的多态。

这么一看,我们每次new一个对象、并对它设置不同的数据,都是一种实例的多态。但是实例的多态还有更强大的作用。虽然“绝大多数情况下,子类继承了父类之后都会重写父类的方法实现”,但是在个别情况下,子类并不重写父类的方法,而只是给父类中的某些字段设置一些不一样的值。

例如在下面的例子中,为了让开发和QA看到不同的仪表盘,我们创建了两个不同的子类DashBoardServcie4Dev和DashBoardServcie4Qa。它们并没有重写父类ServiceDashBoardAsChain的任何方法,只是通过构造函数为父类字段list设置了不同的值:

class ServiceDashBoardAsChain implements DashBoardServcie{
    protected List<DashBoardServcie> list;
    @Override
    public void fill(DashBoard dashBoard){
        list.foreach(s->s.fill(dashBoard));
    }
}
@Service
class DashBoardServcie4Dev extends ServiceDashBoardAsChain {
    public DashBoardServcie4Dev(@Autowired DashBoardServcie service4Cpu,
                    @Autowired DashBoardServcie service4Memory){
        super();
        list = Arrays.asList(service4Cpu,service4Memory);
    }
}
@Service
class DashBoardServcie4Qa extends ServiceDashBoardAsChain {
    public DashBoardServcie4Qa(@Autowired DashBoardServcie service4Cover,
                    @Autowired DashBoardServcie service4Tests,
                    @Autowired DashBoardServcie service4Sonar){
        super();
        list = Arrays.asList(service4Tests,service4Cover,
            service4Sonar);
    }
}

把对象单纯的理解为编译期间的静态的类、而把运行期间的动态的实例抛诸脑后,是出现这种类的主要原因。把这种类的多态转化成实例的多态,既能减少冗余的类定义、降低类爆炸的风险,又能提高程序功能的灵活性和扩展性:

class ServiceDashBoardAsChain implements DashBoardServcie{
    @Setter
    private DashBoardServcie list;
    @Override
    public void fill(DashBoard dashBoard){
        list.foreach(s->s.fill(dashBoard));
    }
}
/** 有了这一个类,就可以生成DashBoardServcie4Dev、DashBoardServcie4QA、
 *  DashBoardServcie4Ops、DashBoardServcie4Pm等多个类所需的实例。
 *  用户还能根据自己的关注点来自定义不同的面板,更加灵活。* */
@Component
class DashBoardServcieFactory{
    /** 单独注入。其中:
     * CPU --> service4Cpu
     * MEMORY --> service4Memory
     * TEST --> service4Tests
     * COVER --> service4Cover
     * SONAR --> service4Sonar
     * */
    @Resource
    private Map<DashBoardType, DashBoardServcie> dashBoardMap;
    public DashBoardServcie build(DashBoardType... types){
        ServiceDashBoardAsChain chain = new ServiceDashBoardAsChain();
        chain.setList(Stream.of(types)
                .map(dashBoardMap::get)
                .collect(Collector.toList()));
        return chain;
    }
}

显然,实例的多态都可以转化为类的多态;再把重载排除在多态之外的话,出现了方法的多态就一定会出现类的多态。所以,我们后面的讨论都围绕类的多态展开。


多态与面向对象

与封装、继承不同,多态不是构成面向对象的要素,而是面向对象自身的特征。如果说封装是父亲、继承是母亲、面向对象是他们的孩子,那么多态就是这个孩子身上无穷的生命力和无尽的可能性。

面向对象模拟着现实世界的类型体系创建了对象体系,这套对象体系也就与类型体系有着相同之处。现实世界中的“多态”俯拾皆是,我们所熟知的“生物多样性”就是一个绝佳的例子。

地球上的生物自诞生之初,就呈现出千人千面的形态。在寒武纪生命大爆炸中出现的三叶虫,就有诸如莱德利基虫、球接子、褶颊虫、镜眼虫、小油栉虫,大卫奇异虫、齿肋虫、裂肋虫等不同种类;植物登上陆地之后,在石炭纪就演化出了石松类、节蕨植物、真蕨类、种子蕨和裸子植物的参天绿意;时至今日,哪怕是小小的加拉帕戈斯雀,也因身体大小、鸟喙形态的不同而分了十几种之多。生命就是以这样的千姿百态,适应了地球上千奇百怪的环境,占据了从赤道到两极、从天空到深渊、从雨林到沙漠的每一个角落;更是在历经五次大灭绝之后,从(最残酷时)残存不到5%的物种开始,复苏、生发、壮大,最终形成了现在这个千娇百媚的世界。

↑ 左上:部分三叶虫;右上:部分加拉帕戈斯雀;下:部分石松。

如果用计算机的语言来描述,“地球Online”的这套生态系统,就是靠着生物物种的“多态”特性,满足了形形色色的“产品需求”。即使是在五次删库跑路之后,借助着“多态”特性,这个系统也能在残存不到5%的代码和数据的基础上,演化出了今天这套“地球Online 6.0”:不仅仍能满足所有环境和生态位的需求,更是开发出了智能生物,不得不令人啧啧称奇。

面向对象的对象体系在通过继承来模拟现实中的类型体系的时候,也毫不客气地把多态特性“顺手牵羊”了。实际上,只要有了继承、只要允许多个子类继承同一个父类,那么就自然而然地有了多态特性。由于有了多态特性,对象体系也拥有了“演化”能力,也就是在保持系统和抽象基本稳定的前提下,引入新的功能、结构以满足新的需求的能力。而这种演化能力,正是系统为满足不同的业务需求而必须具备的生命力和可能性。

例如下图就是我们系统中某个功能模块的“演化”过程。

↑ 一个功能模块的演化之路

最初,这个模块只提供一种功能,只需要ServiceA这一个类就足够了。后来,我们需要在原有功能的基础上增加一项新的功能。新功能与原功能大同小异,而且原功能还要保留——调用ServiceA的地方非常多,无论开发还是测试,都不能保证覆盖到每一个改动点。这时,多态特性就发挥了作用:我们在ServiceA的基础上扩展出了一个子类ServiceB。所有沿用原功能、调用了ServiceA的地方无需做任何改动;所有需要使用新功能的地方调用ServiceB即可。然而,随着对业务的深入理解,我们发现ServiceA和ServiceB看似一脉相承、实则南辕北辙。这一点在随后的需求中就体现了出来:我们需要从ServiceB的功能中细分出一种更新的功能;它与ServiceB还是异曲同工,但与ServiceA已相去甚远。为了更好的描述对应这些功能的类之间的关系,我们通过抽取出BaseService类,把ServiceA和ServiceB由父子关系转变为兄弟关系。同时再次借助继承与多态,在ServiceB的基础上扩展出子类ServiceC,用以满足新的业务功能。

人们常说:系统架构不是设计出来的、而是演化出来的。然而,如果没有多态特性,我们的系统只能一次又一次地“重做”,根本无法演化。


多态与抽象

从抽象的角度来看,多态是什么呢?首先,抽象的作用在于“隐藏细节”。多态就是它要隐藏的一种细节。例如,我们系统需要根据用户的位置信息定位到用户所在的省、市、区县。显然的,这个功能可以抽象为这样一个LocationService接口:

public interface LocationServcie{
    /** 
     * 根据Location中的经纬度或者ip地址,定位City中的省、市、区县代码及名称。
     *
     * @param loc 经度和维度要么都有值、要么都没有值;不能一个有值、一个没有值。
     *            经纬度和IP地址至少有一个有值。    
     * @return 如果定位成功,将返回省、市、区县三级代码及相应的名称。    
     *         三级代码保证非空;如果是在直辖市,三级代码相同;如果在市区,市、区县代码相同。    
     *         如果定位失败,将返回null。    
     * */
    City locate(Location loc);
}

不过,具体要怎样定位呢?如果用户授权我们使用定位信息,那么我们就可以根据经纬度信息,调用某地图API来查到地址;然后将地址转换为所需的代码。如果某地图API出了问题——无论是服务自身还是运营商网络出了问题——我们都可以更换另一家地图API来查询地址。如果所有地图API都调用失败、或者用户压根就不让我们使用定位信息,我们还可以使用IP地址进行定位——尽管IP定位并不准确,但是些许聊胜无,至少它可以“尽可能”地保证业务继续处理下去:

↑ 定位功能模块的对象体系

但是,对调用方来说,它只需要知道LocationService接口的相关约束:入参要传入哪些值、出参会返回哪些值,这就够了。至于这个模块到底是通过经纬度定位的、还是通过IP地址定位的呢?这个模块到底是用哪一家的地图API定位的呢?调用方不需要知道。这就像我们去银行取钱时,只要输入正确的密码、能拿到所需的钞票就可以了。至于柜台后面坐着的是男是女、是老是少、是机器人还是外星人,这不重要。

↑ 不过,如果你不仅想取银行的钱,还想娶银行的人,那就另说了。

我们还可以换一个角度来看多态与抽象:多态不仅仅是抽象内部的细节,同时也是实现细节的“最佳实践”。我们不妨设想一下:如果面向对象不支持多态——例如,一个接口只能有一个实现类、一个抽象类只能有一个子类、不允许非抽象类拥有子类,我们要怎样实现一个接口呢?

仍以上面的定位功能为例。如果LocationService接口下只允许有一个实现类,那么,为了提供ByMapApiXxx、ByMapApiYyy和ByIp这三种服务,这个硕果仅存的实现类只会有两种可能的编码方式:要么,它对外暴露三个方法、分别提供三种服务,由调用方自己选择和处理方法调用逻辑;要么,它仍然只提供一个方法,但是方法内部用if-else等方式把三种调用逻辑“一网打尽”。

我们网上购物凑优惠时常常遇到极其复杂的规则:又要组战队、又要每日签到、又要分享集赞……不仅又啰嗦又麻烦,而且稍不留神就会算错折扣;好不容易算好了账,活动方一个规则补丁,又要全部从头再来。跟直接撒币发红包相比,这种所谓的“优惠”实在是费时费力又不讨好。

↑ 优惠规则这么复杂,无怪乎连“你真的会网购吗”这种文章都一搜一大把。

如果接口使用三个方法来提供三种服务——就像下面这段代码这样,那就跟这种毫无诚意的优惠活动差不多:本来简单明了的一件事情,由于接口把内部细节都暴露了出来,使得调用方代码又啰嗦又容易重复,并且调用方很容易对接口逻辑产生误解和误用。不仅如此,接口方法一旦发生变化——尤其是新增或者下线一个定位服务——那么所有的调用方都要修改代码。这种“发散变化”是任何一个开发人员都不能接受的。

/** 接口和实现类定义了三个不同的方法 */
public class LcationServiceImpl implements LocationService{
    public City locateByApiXxx(Location loc){...}
    public City locateByApiYyy(Location loc){...}
    public City locateByIp(Location loc){...}
}
public class CityService{
    public void userCity(UserInfo user, Location loc){
        /* 调用方使用时就要这样写代码 */
        City city = locationServcie.locateByApiXxx(loc);
        if(city == null){
            city = locationServcie.locateByApiYyy(loc);
        }
        if(city == null){
            city = locationService.locateByIp(loc);
        }
        if(city != null){
            // 略
        }
    }
}

那么,在一个方法内用if-else等方式把多种服务“一网打尽”呢?相比提供多个方法,这种方式的确可以更好地保持接口的抽象性和稳定性。但是,这种方式就像是使用了二向箔一样:它抹平了抽象的层级,把本可以逐层分解的业务复杂性全部堆叠到一层,人为地推高了代码复杂性,不仅把代码变得难以维护,而且把原本简单的业务也变成了水中花、雾中月,捉摸不透、脆弱不堪。

↑ “等高线图”就是一种现实中的“二向箔”,它把三维空间中的地形压缩成了二维平面上的线条,同时抹掉了太多细节。例如,如果要修一条从左到右贯穿上图的铁路,只看等高线图,谁能说出来要钻几个隧道、要架几座桥?

在我们某个系统中,所有的业务功能都是通过if-else来区分处理的。当if-else累积到一定程度之后,出现了一件非常诡异的事情:所有人都说这个系统的业务逻辑很简单;但所有人都说不出系统中的业务逻辑是怎样的——哪怕只是一个产品、一种用户的完整业务都说不出来。为什么?因为这些流程全都散落在系统的if-else里:这个if里有一段、那个else里有一段;这种产品跟那种产品的逻辑纠缠在一起,这类用户和那类用户的流程混合在一起。要想把它们挑拣出来、拼凑完整,简直比从肯德基全家桶里拼凑出一只完整的小公鸡还要困难。

↑ 要不要挑战一下,看能拼出几只小鸡来?

这还只是问题的开始。由于没有人能说清楚完整的业务逻辑,所以每当产品提出新需求的时候,也就没有人能说清楚到底要怎么改,只能通过“扒代码”来估计改动范围和开发工作量。但是,就如用归纳法永远也找不出真理一样,“扒代码”永远也无法明确地告诉你“改动范围就这么大”、“工作量就这么点”。事实上,在开发过程中发现新的改动点、在测试时发现其它功能受到影响,对这个系统来说是家常便饭;相应的,延期、加班、线上bug……也就纷至沓来了。

如果使用多态呢?我曾经用多态的方式,重构过一个类似的系统。重构完成之后,只用一张表格就可以把完整的业务流程、以及不同产品不同用户所做的特殊操作全部展示出来。在这张表格中,一个新的需求要改什么、加什么、删什么,全都一目了然;改动范围和工作量也都变得清晰明确了。延期?不存在;加班?没必要;bug?我们有枪手——“枪手,走遍天下,蚊虫无忧”哈哈哈。

↑ 业务流程表格大概就是这个样子的。产品丙是后来新增的需求。从这张表格里,我们就能看出来系统需要改哪些东西了。

为什么使用了多态就能达到这样的效果呢?因为多态能够充分利用抽象的层级特性,从而把纷繁复杂的实现细节分散在不同的抽象层级中。通过这样的层层分解,我们一定能找到这样两个抽象层级:一个既能够完整的描述业务流程,又不会陷入底层细节中、绕行“山路十八弯”后仍然“云深不知处”;另一个则把某一类业务的细节描述得纤毫毕现,但对其它类型的业务则“事不关己高高挂起”。

有了第一个抽象层级,我们对业务流程就有了一个清晰而明确的总体认识,对产品需求有哪些改动点、有多少工作量、有哪些潜在风险,自然也就一目了然了。这就像遇到了张松的刘备一样,掌握了蜀中的道路、地形、布防、民情等整体情报后,对如何施行“跨有荆益”这一战略、入蜀要途径哪些城池关隘、哪里可以募兵哪里可以筹粮等问题自然也就胸有成竹了。有了这样清晰的战略部署,成都还不是手到擒来。

↑ 刘备入蜀路线图。自古“蜀道之难,难于上青天”,没有清晰的战略部署,谁敢拿性命去“快速试错”?

而有了第二个抽象层级,我们的业务流程和代码模块就可以充分地解耦合,从而降低彼此之间的牵制和掣肘,从而减少不必要的bug和开发测试工作量。


多态与高内聚低耦合

无论使用多态还是if-else,都可以把抽象下的多种服务聚合到同一个模块内。但是,多态所提供的低耦合是其它任何方式都无法比拟的。

例如,在我们的系统中,有这样一段代码:

public class AuditServiceImpl implements AuditService{
    @Override
    public void audit(Apply apply){
        // 一堆公共逻辑
        // 然后根据产品类型做不同的必填项校验
        if(apply.getProduct() == ProductA) {
            // 执行ProductA对应的校验,略
        }else if(apply.getProduct() == ProductB)){
            // 执行ProductB对应的校验,略
        }else{
            // 执行ProductC对应的校验,略
        }
        // 又一堆公共逻辑
        // 又根据产品类型组装不同的数据
        if(apply.getProduct() == ProductB) {
            // 略
        }// else-if,略
        // 还有一堆公共逻辑
        // 再次根据产品类型按不同的逻辑处理返回数据
        if(apply.getProduct() == ProductC) {
            // 略
        }// else-if,略
    }
}

这好像是我们写业务代码时最常见的方式:一开始只有ProductA;然后业务上增加了大同小异的ProductB,于是代码中也在差异化的地方加上if(ProductB);接着又有了ProudctC/ProductD/ProudctE,于是代码中这个地方加一个if(ProductC),那个地方加一个if(ProductD || ProductE),久而久之,代码就变成了上面这个样子。我们经常吐槽说自己系统里的代码是“Shit Hill”,其实很多时候,“Shit Hill”就是这么来的。

“Shit Hill”的问题可谓罄竹难书,模块间的强耦合就是罪魁祸首之一:从ProductA到ProductE,相关的功能代码全都杂糅在一起,使得这几个本应相互独立的产品和业务之间产生了耦合性最强、也最另令人深恶痛绝的内容耦合。

Content coupling is said to occur when one module uses the code of other module, for instance a branch. This violates information hiding - a basic design concept.内容耦合是指一个模块直接使用另一个模块的代码。这种耦合违反了信息隐藏这一基本的设计概念。

花园的景昕,公众号:景昕的花园细说几种耦合

内容耦合使得我们在为一个产品修改代码的时候,总会感到“战战兢兢,如履薄冰,如临深渊”,因为谁也不知道自己改的代码会不会影响到其它产品的业务。我就曾经在这样一个方法的第十几行处加了一行代码;没想到在一百多行开外,这行代码引发了另一个bug。

这种抓狂的感觉……谁写bug谁知道啊。

要怎样化解这些问题呢?多态就是一种非常好的方案。例如,我们可以用多态把上面这段代码改写成这样:

abstract class AuditServcieAsSkeleton implements AuditService{
    @Override
    public void audit(Apply apply){
        // 一堆公共逻辑
        // 校验入参
        doValid(apply);
        // 又一堆公共逻辑
        // 构建请求数据
        Request request = buildRequst(apply);
       // 再来一堆公共逻辑
       // 处理返回数据
       dealResponse(response);
    }
    protected abstract void doValid(Apply apply);
    protected abstract Request buildRequest(Apply apply);
    protected abstract void dealResopsne(Response resp);
}
class AuditServiceAsDispatcher implements AuditService{
    private Map<ProductType, AuditService> dispatcher;
    @Override
    public void audit(Apply apply){
        dispatcher.get(apply.getProductType())
                  .audit(apply);
    }
}
class AuditService4ProductA extends AuditServiceAsSkeleton{
    @Override
    protected void doValid(Apply apply){
        // 产品A的校验逻辑
    }
    @Override
    protected Request buildRequest(Apply apply){
        // 组装产品A所需的请求数据
    }
    @Override
    protected void dealResopsne(Response resp){
        // 按产品A的逻辑处理返回结果
    }
}
class AuditService4ProductB extends AuditServiceAsSkeleton{
    // 按产品B的需求处理;略。// 产品C、D、E的类也略。
}

借助多态方案,ProductA/B/C/D/E的相关代码被分散到完全独立的几个类中,从而把产品功能之间的内容耦合降低为特征耦合甚至数据耦合。这样,无论是哪个产品要修改自己的功能、或者我们要再新增一套新的产品,都可以做到与其它产品毫无瓜葛;从而做到“代码耦合少,bug远离我”。

Stamp coupling occurs when modules share a composite data structure and use only parts of it, possibly different parts .特征耦合是指多个模块共享一个数据结构、但是只使用了这个数据结构的一部分——可能各自使用了不同的部分。

花园的景昕,公众号:景昕的花园细说几种耦合

Data coupling occurs when modules share data through, for example, parameters. Each datum is an elementary piece, and these are the only data shared (e.g., passing an integer to a function that computes a square root).数据耦合是指模块间通过传递数值来共享数据。传递的每个值都是基本数据,而且传递的值是就是要共享的值。

花园的景昕,公众号:景昕的花园细说几种耦合

不过,使用多态就难免要使用继承;因而也难免会遇到困扰继承的子类耦合。但这并不是多态带来的问题,而是使用继承所需要特别注意的。

多态

标签:子类,代码,多态,耦合,apply,public
来源: https://blog.51cto.com/winters1224/2484126

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有