51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2465|回复: 0
打印 上一主题 下一主题

[转贴] 面向对象缺陷分析

[复制链接]
  • TA的每日心情
    无聊
    4 天前
  • 签到天数: 530 天

    连续签到: 2 天

    [LV.9]测试副司令

    跳转到指定楼层
    1#
    发表于 2018-12-18 16:13:37 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    本帖最后由 测试积点老人 于 2018-12-18 16:15 编辑

    教科书上说,面向对象三大特性是封装、继承、多态。其中封装和多态并非面向对象原创,比如说USB口可以插入U盘、手机、ipod等设备,这就是多态。插入之后电脑内部怎么运转不用管,看显示器操作就行,这就是封装。在软件开发领域,面向对象出现之前,封装和多态虽然没有具体概念,但是其思想却已经广为应用。我认为,最能代表面向对象的,也是最值得喷的特性,就是继承。



    将世界看成对象的集合

    据说,面向对象是比较适合人的思维的,因为人在看世界的时候就是一个个对象,因此面向对象有利于程序的构建。这个观点是非常形而上(美其名曰哲学)的,其正确性先不去讨论。为了模拟对象,class关键字引入各种面向对象语言,把属性与操作(方法)封装于其中。在现实世界中对象似乎有一定层次关系,比如鸡鸭牛羊属于动物,动物属于生物,生物属于对象……以此为据,面向对象先驱们设计了继承这一思想,让class之间也形成继承关系,这样既有助于理解类的概念,又让代码有层次性,而且还提高了代码的复用性——少写了不少代码,提高生产力。自此,以class、继承为基础的面向对象思想、语言、设计方法大行其道,直至今日。



    继承的问题

    直觉上看,继承确实符合人认识事物的思维。但是在实践中,我发现继承有时不那么好用。比如番茄属于植物,属于食物,然而植物和食物之间的关系不是继承也不是并列,而是有交集。在食物类中,番茄同时具有水果和蔬菜的特性,即便现实世界也很难区分。这就是继承的问题所在:面向对象语言中的继承体系是比较严格的树形关系,而现实世界或者软件需求却不一定是树形,很可能关系错综复杂。

    我在“设计模式的本质思想”一文中提到一个例子,现在将这个例子简化一下。有个Bird类,类内有Sing(鸣叫)和Move(移动)两个方法,具体操作什么由子类实现。先设计基类,如下

    1. //纯面向对象语言java登场
    2. abstract class Bird {
    3.         abstract void Sing();
    4.         abstract void Move();
    5. }
    复制代码

    Java号称纯面向对象语言,一切都是对象。看看又是abstract又是class,真有面向对象风格,基类设计的没太大问题。然后我们实现(泛化)一个麻雀类,叫声是"Jijijiji",移动方式是"Fly"(不要联想JJFly),一个鸽子类,叫声是"gugugu",移动方式是"Fly"。如下

    1. class Sparrow extends Bird{
    2.         @Override
    3.         void Sing() {
    4.                 System.out.println("jiji");
    5.         }
    6.         @Override
    7.         void Move() {
    8.                 System.out.println("Fly");
    9.         }
    10.         
    11. }
    12. class Dove extends Bird{
    13.         @Override
    14.         void Sing() {
    15.                 System.out.println("gugugu");
    16.         }
    17.         @Override
    18.         void Move() {
    19.                 System.out.println("Fly");
    20.         }
    21. }
    复制代码

    至此,一个简单的鸟继承体系完成。在应用层使用Bird对象的时候不必管具体子类会怎么实现,似乎很好地实现了模块化。

    不过仔细一看有重复代码,Fly那一部分似乎是重复的。那我们提取一个FlyBird类,移动方式是"Fly",叫声由子类实现,麻雀和鸽子都继承自FlyBird。看起来不错。

    现在我们在加入小鸡类,叫声是"jijiji"而移动方式是"walk",叫声那部分代码可能也会重复。有兴趣的同志可以试试,麻雀、鸽子、小鸡这三个类只用单继承,很难避免代码重复。出现这一问题的原因是,类之间的关系很复杂,只靠简单的树形继承无法表达这些关系。


    解决方案——设计模式诞生

    设计模式远远没有大家想的和他自己鼓吹的那么高大上,其出现主要还是为了解决上面这个问题。看看如何用策略模式解决这一问题

    1. //策略模式的思想是组合代替继承
    2. //先声明两个接口
    3. interface Move{
    4.         void move();
    5. }
    6. interface Sing{
    7.         void sing();
    8. }
    9. //基类的行为不直接定义,而通过接口实现
    10. class Bird {
    11.         Bird(Move m, Sing s){
    12.                 this.m = m;
    13.                 this.s = s;
    14.         }
    15.         private Move m;
    16.         private Sing s;
    17.         
    18.         public void Sing(){
    19.                 s.sing();
    20.         }
    21.         public void Move(){
    22.                 m.move();
    23.         }
    24. }
    25. //实现飞行动作
    26. class Fly implements Move{
    27.         public void move(){
    28.                 System.out.println("Fly");
    29.         }
    30. }
    31. //实现Jiji叫声
    32. class Jiji implements Sing{
    33.         public void sing() {
    34.                 System.out.println("jijiji");
    35.         }
    36. }
    37. //麻雀类
    38. class Sparrow extends Bird{
    39.         Sparrow(){
    40.                 super(new Fly(), new Jiji());
    41.         }
    42. }
    复制代码

    按照这种方式,用组合代替继承,合理的避免了重复代码,软件再扩大时也比较容易管理。确实是个不错的解决方案。


    其他面向对象解决方式

    多重继承

    单一继承的树形关系不是难以模拟需求吗?我用多重继承,实现FlyBird/WalkBird,以及JijiBird/GuguBird,麻雀继承自FlyBird and JijiBird(别想歪了),鸽子继承自GuguBird and FlyBird。理论上在这个问题中,多重继承也能合理的组织类的关系,但是多重继承问题很多,最主要的,继承自两个类,两个类的属性和方法保存一份还是两份?保存谁的?如果问题再复杂点,多重继承恐怕也很难满足需求。

    方法2,将所有操作,比如Fly/Walk/Jiji/Gugu,都放在基类中实现,子类只负责调用。当子类没有新属性的时候这不失为一种解决办法。看起来很扯淡,其实是有应用的,比如基类是单链表,子类封装起来调用插入删除方法可以形成栈与队列。

    其他语言的解决方式


    C语言

    1. #include <stdio.h>
    2. struct Bird{
    3.         void *property;
    4. };
    5. //函数指针模拟虚函数
    6. typedef void (*MoveFun)(Bird*);
    7. typedef void (*SingFun)(Bird*);
    8. void Fly(Bird*){
    9.         printf("Fly\n");
    10. }

    11. void Gugu(Bird*){
    12.         printf("Gugu\n");
    13. }

    14. int main(){
    15.         //下面三行初始化一个鸽子
    16.         Bird b;
    17.         MoveFun doveMove = Fly;
    18.         SingFun doveSing = Gugu;
    19.         
    20.         doveMove(&b);
    21.         doveSing(&b);
    22. }
    复制代码

    我学C++面向对象经历了这么个过程:class不就是struct加函数么。。。原来面向对象虚函数可以实现应用实现分离,不错。。。C语言加函数指针也能实现虚函数。。。有些东西不太好设计,还好我有设计模式。。。这TM不就是struct加函数指针么!!!

    在此稍微总结一下,通过策略模式和C语言的两个例子,我们发现组合通常比继承来的爽快,能够更简单更直接地解决问题。然而面向对象语言通常不把类内方法当成可以赋值的变量(函数不是first-class),而是写了之后就固定了,和类型严格绑定,这可能是造成继承不灵活的根本原因。在python语言中,类内的方法强制带self参数,可以调用类外的函数模拟对类内函数的赋值,实为面向对象史上一大进步(此前对python有误解,现已修改)。

    即使应用如上的策略模式,运行时如果不允许替换方法,面向对象的缺陷还是很大,比如小鸡长成大鸡,叫声从Jijiji变成Gugugu,恐怕要重新设计了(简单的处理方式是有的,比如把两个接口对象改成public随意改动,不过这肯定会因为太不OO而被否决)。然而用C语言就不需要所谓的设计。



    Lua语言

    1. function Fly()
    2.         print("Fly")
    3. end
    4. function Gugu()
    5.         print("Gugu")
    6. end

    7. function GetDove()
    8.         dove = {}
    9.         dove.Move = Fly
    10.         dove.Sing = Gugu
    11.         return dove
    12. end

    13. dove = GetDove()
    14. dove.Move()
    15. dove.Sing()
    复制代码

    lua不支持面向对象,然而用table可以完美的模拟面向对象做得到和做不到的事。table只是一个key-value的集合,不去刻意区分属性和方法,不管属性还是方法都能随意替换。同时Lua是弱类型语言,如果把dove对象的Sing改成Jiji,那么dove就变成了一个麻雀,这就是传说中的“鸭子类型”。



    继承的适用范围

    实践来看,如果你想通过继承实现开闭原则——新需求只需要在已有类的基础上添加而不需要改动已有类——简直是痴人说梦。如果在设计之初就考虑应对变化,那么往往也在浪费时间,你设想的变化通常不会出现,出现的都是你没想到的。继承为主的面向对象思想,不仅不适合变化快速繁多的场合,反而对需求变化不太大的模块更能胜任:GUI中的控件(同类控件除了绘制和鼠标消息基本没什么变化的需求,一个控件写好了到处通用N个版本也不会改)、基本数据结构和算法(基本没有需求变化)、编译原理的词法分析模块(至多对内部算法优化,对外接口不会有变动) 。



    总结

    为了弥补继承的缺陷,编程语言引入各种概念,多重继承(c++、python)、Mixin(模拟多重继承)、delegate(C#,不过delegate作为lambda比C的函数指针要强)、functor(C++)、interface(Java and others)以及各种设计模式。C语言和lua的table有更简单更直接的实现方式,不过人家不支持面向对象,请读者不要在这扯皮说C也有面向对象思想精髓之类。


    在此引用宏哥一句话:OO就是把屁股朝天拉屎, 不拉在裤子上的就叫做"OO设计师"。整个OO的设计精髓就在于不要把屎拉裤子上。问题是, 为什么要朝天拉屎呢 --  为了OO。


    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-25 03:13 , Processed in 0.064198 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表