Effctive Java - N0.16 复用优先于继承

 
        继承是实现代码重用的的有力手段,但它未必是最好的方法。对于普通的具体类进行跨越包边界的继承(说的是如果一个包内字段或者方法,私有private[不谈这个最安全];缺省修饰符无[本包里访问问题不大.一般由一个程序员控制且功能集中出了问题也不会扩散到其他包];提升到受保护级别protected[即本包内和其他包内的子类可以进行访问,怕的就是其他开发兄弟直接图省事继承该类功能并加强或者直接复写为自己希望的功能]),则是非常危险的。这里的继承指的是一个类扩展另一个类的继承而非接口继承。
        继承的一大缺点在于打破了封装性。子类依赖于超类中特定的功能实现细节。意味着超类如果发生版本变化,子类有可能受到破坏。除非超类是专门为了扩展而设计的(基础顶层抽象类)
        理由如下:当我们要扩展一个类时,特别是一个别人写好的类,一个类库的类,我们往往关心的仅仅是单个api的功能,而不关心他的实现,但是存在的一个问题就是,同一个类的各个方法直接可能存在联系,可能一个方法的实现依赖于另一个方法,这就意味着,当我们调用一个我们想要操作的方法时,“继承”会隐式的调用另一个方法,这就可能存在问题。
        详细地说,假设我们有一段程序用到了HashSet。为了对程序进行调优,我们需要查询HashSet在创建后添加了多少元素(不能直接查看当前集合的大小,因为在元素被移除时它会递减)。为了提供这种功能,我们创建一个HashSet的变量,并对每一个插入的元素计数,并提供一个查看计数的方法。由于HashSet类有两个增加元素的方法,add和addAll,所以我们需要重写两个方法:
public class InstrumentedHashSet<E> extends HashSet<E> {
     // The number of attempted element insertions
     private int addCount = 0 ;
     public InstrumentedHashSet () {
         super ();
    }
     public InstrumentedHashSet ( int initCap , float loadFactor ) {
         super ( initCap , loadFactor );
    }
     @Override
     public boolean add ( E e ) {
         addCount ++;
         return super . add ( e );
    }
     @Override
     public boolean addAll ( Collection <? extends E > c ) {
         addCount += c . size ();
         return super . addAll ( c );
    }
     public int getAddCount () {
         return addCount ;
    }
     public static void main ( String [] args ) {
         InstrumentedHashSet < String > instrumentedHashSet = new InstrumentedHashSet < String >();
         instrumentedHashSet . addAll ( Arrays . asList ( "旈歆" , "林家豆豆龙" , "妹爷" )); // 3
         System . out . println ( instrumentedHashSet . addCount );  // addCount = 6;
    }
}

我们看上面这个例子:
        该类继承HashSet 为add()以及addAll()增加了一个记录已添加元素数目的功能。程序乍一看没什么问题。
      但事实上HashSet 的addAll是依赖于add方法实现的,所以最后又去迭代调用了InstrumentedHashSet中的add方法(因为InstrumentedHashSet重写了HashSet的add方法)。这就导致得到的结果为正确结果的两倍。
我们只要去掉被覆盖的addAll()方法,就可以修正这个子类。虽然这个类可以正常工作。但是它的功能正确性需要信赖于HashSet的addAll方法是在add方法上实现的这一事实,这种自用性(self-use)是实现细节,而非必须要这么实现,不能保证在java平台的所有实现中都保持不变,不能保证随着上发行版本的不同而不发生变化。假设下个版本addAll()不依赖于add()来实现了,那我们这个子类就彻底错了。(即方法只保证最终正确性)
导致子类脆弱的还有一个原因是:
        ー:它们的超类在后续的发行版本中可以获得新的方法,假设一个程序的安全性信赖于这样的事实:所有被插入到某个集合的元素都满足某个先决条件。下面的做法就可以确保这一点:对集合进行子类化,并覆盖所有能够添加元素的方法以便确保在加入每个元素之前它是满足这个先决条件的。如果在后续的发行版本中,超类中没有增加能插入元素的新方法,那么这种方法可以正常工作。然而,一旦超类增加了这样的新方法,则很可能仅仅由于调用了这个未被子类覆盖的新方法,而将不合法的元素添加到子类的实例中。(因为多态的原因,子类的实现也必须遵守父类的约定)
        二:如果我们仅仅是增加新的方法,而非覆盖现有方法,这样也是有问题的。万一超类增加了一个与我们新增方法同名的方法,仅仅是返回类型不同。那么我们的子类无法通过编译。如果返回类型也相同,相当于我们重写了新增的方法。但我们这个方法是在超类之前就有的,我们无法得知超类这个方法的约定是什么。(可能会造成方法的冲突)
使用“复合(composition)”可以解决上述的问题,不用扩展现有的类,而是在新的类中增加一个私有域。通过“转发”来实现与现有类的交互,这样得到手类将会非常稳固。它不信赖于现有类的实现细节。即使现有的类增加了新方法,也不会影响到新类。请看如下的例子
public class InstrumentedSet < E > extends ForwardingSet < E > {
     private int addCount = 0 ;
     public InstrumentedSet ( Set < E > s ) {
         super ( s );
    }
     @Override
     public boolean add ( E e ) {
         addCount ++;
         return super . add ( e );
    }
     @Override
     public boolean addAll ( Collection <? extends E > c ) {
         addCount += c . size ();
         return super . addAll ( c );
    }
     public int getAddCount () {
         return addCount ;
    }
}
// Reusable forwarding class
class ForwardingSet < E > implements Set < E > {
     private final Set < E > s ;
     public ForwardingSet ( Set < E > s ) {
         this . s = s ;
    }
     public void clear () {
         s . clear ();
    }
     public boolean add ( E e ) {
         return s . add ( e );
    }
     public boolean addAll ( Collection <? extends E > c ) {
         return s . addAll ( c );
    }
     public boolean removeAll ( Collection <?> c ) {
         return s . removeAll ( c );
    }
    ......
}

在上面这个例子里构造了两个类, 一个是用来扩展操作的包裹类 一个是用来与现有类进行交互的转发类 ,可以看到,在现在这个实现中不再直接扩展Set,而是扩展了他的转发类,而在转发类内部,现有Set类是作为它的一个数据域存在的,转发类实现了Set<E>接口,这样他就包括了现有类的基本操作,同时也可能包裹任何Set类型。每个转发动作都直接调用现有类的相应方法并返回相应结果。这样就将信赖于Set的实现细节排除在包裹类之外。有的时候,复合和转发的结合被错误的称为"委托(delegation)"。从技术的角度来说,这不是委托,除非包装对象把自身传递给被包装的对象。
包装类几乎没有什么缺点。需要注意的一点是,包装类不适合用在架设框架上(callback framework),在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为SELF问题。
总结:
(1)复合与继承的含义:
        复合是has a, 继承是is a。
(2)复合与继承的区别:
        我们不单单要看到他们形式上的区别,还要看到他们意义上的区别。这就是引出了以下两个问题:
        1 为什么使用复合?假如B引用了A,那就说明B的职责需要调用A的职责,但是A的职责并不是B的职责;
        2 为什么使用继承?假如B继承了A,那就说明A的职责也是B的职责,只不过B在职责的实现上跟A有所不同。
(3)复合与继承的适用场景:
        1 如果A与B负责不同的业务,但是B在实现其业务逻辑时需要调用A的职责,则建议使用复合;
        2 如果A与B负责同一个业务,但是A与B在业务实现上有所不同,则建议使用继承。
(4)举例说明
        1 预警系统 VS 发送器
                预警系统包含生成预警与发送预警这两部分业务,其中发送预警的业务中需要调用发送器的职责,则预警系统与发送器之间应该使用复合;
        2 不同的发送器
                发送器只有一个发送的职责,但是有不同的实现,如基于邮件和基于短信的,则不同的发送器使用继承。


强化,自己在项目中遇到的问题:
       1.不想使用继承是因为子类(相关类)不需要父类(功能比较齐全的类)的全部功能,并且还想要强化上级类的某些功能.
       2.本来刚看书的时候(菜逼)并不知道为何非要多一层转发类,后来想想应该是子类统一的抽象(设计的时候一定不要依赖具体类,抽象是为了为扩展做缓冲),为的是后方的包装类有一个统一的功能,不过我在项目里一般把这个转发类设为抽象的因为它本身并没有什么具体的意义,只不过把每一个方法包了一层,单独new出来并没有啥卵用(自己瞎扯的别打脸),还有就是添加每个子类都应实现的特定方法.
       3.在项目中的好处我想主要是体现在两点
                (1):确实是增强某些类的功能:让类的功能更加强大,功能实现更加有效率比如io.
                (2):防止父类方法中的错误因子类的中的操作放大,保证原方法逻辑正确.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值