The beauty of design patterns learning (VIII): Why is said to be multi-purpose combination of less inheritance? How do you decide which combination or by inheritance?

In object-oriented programming, there is a very classic design principles, that is: a combination of superior inheritance, with a combination of less inheritance. Why is not recommended to use inheritance? What are the advantages compared to the combination of inheritance? How to determine the inheritance or with a combination?

Why is not recommended to use inheritance?

Inheritance is one of the four major characteristics of object-oriented, to represent between class is-arelations, can solve the problem of code reuse. Although there are a lot of role inheritance, but inheritance hierarchy too deep, too complex, can also affect the maintainability of the code. So, whether you should use inheritance in the project, there are many online controversy. Many people think that inheritance is an anti-pattern, should be used sparingly, even without. Why the controversy?

Suppose we want to design a class about birds. We will "bird" such a thing abstract concept, defined as an abstract class AbstractBird. All the more segments of the birds, such as sparrows, pigeons, crows, etc., inherit the abstract class.

We know that most birds can fly, then can we in the AbstractBirdabstract class, define a fly()way to do that? the answer is negative. Although most birds can fly, but there are exceptions, such as the ostrich does not fly. Ostrich has inherited fly()parent class method, it would have ostrich "fly" such behavior, which is clearly not in line with our understanding of the real world things. Of course, you might say, ostrich rewritten in this subclass ( override) fly()method, it throws UnSupportedMethodExceptionan exception can not it do? Specific code implementation is as follows:

public class AbstractBird {
  //...省略其他属性和方法...
  public void fly() { //... }
}

public class Ostrich extends AbstractBird { //鸵鸟
  //...省略其他属性和方法...
  public void fly() {
    throw new UnSupportedMethodException("I can't fly.'");
  }
}

While this design idea can solve the problem, but less beautiful. Because in addition to the ostrich, flightless bird, there are many, such as penguins. For these flightless birds, the need of rewriting fly()method throws an exception. This design, on the one hand, and inviting the workload of encoding; on the other hand, is also contrary to the principle of minimum knowledge ( Least Knowledge Principlealso called the principle of least knowledge or Demeter), exposure should not be exposed to external interfaces, increased class probability misused during use.

That through the AbstractBirdderived class two more segments of the abstract class: birds flying AbstractFlyableBirdand flightless birds AbstractUnFlyableBird, so sparrows, crows fly these birds are inherited AbstractFlyableBird, let ostriches, penguins these flightless bird , inherited AbstractUnFlyableBirdclass, can not you do? Specific inheritance as shown below:
image.png
As can be seen from the figure, into a three-inheritance. However, on the whole, the current inheritance is relatively simple, relatively shallow level, can be considered an acceptable design ideas. And then continue to add a little more difficult. In just this scenario, we are only concerned about the "bird will not fly," but if we are also concerned about the "birds will be called," that this time, how to design the inheritance relationship between classes it?

Whether or not fly? You will call? Two acts with them will have four cases: fly will be called, will call not fly, fly will not be called, will not fly will not be called. If we just continue to follow the design ideas, it would need to define four abstract classes ( AbstractFlyableTweetableBird, , AbstractFlyableUnTweetableBird, ).AbstractUnFlyableTweetableBird If you also need to consider "whether it will lay eggs" such a behavior, it is estimated that the combination would explode. Class inheritance hierarchy will be deeper and deeper, inheritance relationships will become increasingly complex. And this level is very deep, very complex inheritance, on the one hand, will lead to less readable code. Because we have to figure out which methods have a class, property, you must read the code of the parent, the parent class code of the parent class ...... the way back to the code topmost parent class. On the other hand, it also destroy the package characteristics such, implementation details will be exposed to a parent class subclasses. Implementation subclass of the parent class implementation-dependent, both the height of the coupling, once the parent code changes will affect all the logic subclasses.AbstractUnFlyableUnTweetableBird
image.png

In short, the biggest problem is that inheritance: inheritance hierarchy too deep, too complex inheritance affects the readability and maintainability of the code. This is why inheritance is not recommended. That just examples of inherited problems, how to solve it?

What are the advantages compared to the combination of inheritance?

In fact, you can use a combination of ( composition), interfaces, delegates ( delegation) three technical means to solve together just inherited problems.

前面讲到接口的时候说过,接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。将这个设计思路翻译成 Java 代码的话,就是下面这个样子:

public interface Flyable {
  void fly();
}
public interface Tweetable {
  void tweet();
}
public interface EggLayable {
  void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鸵鸟
  //... 省略其他属性和方法...
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}
public class Sparrow impelents Flayable, Tweetable, EggLayable {//麻雀
  //... 省略其他属性和方法...
  @Override
  public void fly() { //... }
  @Override
  public void tweet() { //... }
  @Override
  public void layEgg() { //... }
}

不过,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鸵鸟
  private TweetAbility tweetAbility = new TweetAbility(); //组合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}

继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。

如何判断该用组合还是继承?

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。从上面的例子来看,继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合。

如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

前面讲到继承可以实现代码复用。利用继承特性,我们把相同的属性和方法,抽取出来,定义到父类中。子类复用父类中的属性和方法,达到代码复用的目的。但是,有的时候,从业务含义上,A 类和 B 类并不一定具有继承关系。比如,Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,但并不具有继承关系(既不是父子关系,也不是兄弟关系)。仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。如果不熟悉背后设计思路的同事,发现 Crawler 类和 PageAnalyzer 类继承同一个父类,而父类中定义的却只是 URL 相关的操作,会觉得这个代码写得莫名其妙,理解不了。这个时候,使用组合就更加合理、更加灵活。具体的代码实现如下所示:

public class Url {
  //...省略属性和方法
}

public class Crawler {
  private Url url; // 组合
  public Crawler() {
    this.url = new Url();
  }
  //...
}

public class PageAnalyzer {
  private Url url; // 组合
  public PageAnalyzer() {
    this.url = new Url();
  }
  //..
}

还有一些特殊的场景要求我们必须使用继承。如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。比如下面这样一段代码,其中 FeignClient 是一个外部类,我们没有权限去修改这部分代码,但是我们希望能重写这个类在运行时执行的 encode() 函数。这个时候,我们只能采用继承来实现了。

public class FeignClient { // feign client框架代码
  //...省略其他代码...
  public void encode(String url) { //... }
}

public void demofunction(FeignClient feignClient) {
  //...
  feignClient.encode(url);
  //...
}

public class CustomizedFeignClient extends FeignClient {
  @Override
  public void encode(String url) { //...重写encode的实现...}
}

// 调用
FeignClient client = new CustomizedFeignClient();
demofunction(client);

尽管有些人说,要杜绝继承,100% 用组合代替继承,但是这里的观点没那么极端!之所以“多用组合少用继承”这个口号喊得这么响,只是因为,长期以来,过度使用继承。还是那句话,组合并不完美,继承也不是一无是处。只要我们控制好它们的副作用、发挥它们各自的优势,在不同的场合下,恰当地选择使用继承还是组合,这才是我们所追求的境界。

重点回顾

1. 为什么不推荐使用继承?

继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。虽然继承有诸多作用,但继承层次过深、过复杂,也会影响到代码的可维护性。在这种情况下,我们应该尽量少用,甚至不用继承。

2. 组合相比继承有哪些优势?

继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过组合、接口、委托三个技术手段来达成。除此之外,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

3. 如何判断该用组合还是继承?

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。反之,我们就尽量使用组合来替代继承。除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。

思考

  • Based MVCarchitecture development Webtime of applications, often defined at the database level Entity, in Servicethe definition of the business layer BO( Business Object), in Controllerthe definition of the interface layer VO( View Object). In most cases, Entity, BO, VOthe code greatly between the three repeats, but not identical. How to deal with Entity, BO, VOcode duplication problem?

Reference: Why is said to be multi-purpose combination of less inheritance? How do you decide which combination or by inheritance?

This article from the blog article multiple platforms OpenWrite release!

Guess you like

Origin www.cnblogs.com/muchen-li/p/11940523.html