英文:
Java class inheriting from multiple interfaces without code duplication
问题
假设有5个接口:
IAnimal
ICanivore
继承自IAnimal
IPet
继承自IAnimal
IDog
继承自IPet
,ICarnivore
ICat
继承自IPet
,ICarnivore
现在我想为狗和猫分别创建两个新类,class MyDog
和 class MyCat
。如果我简单地这样做:class MyDog implements IDog
和 class MyCat implements ICat
,那么这两种动物之间会有很多代码重复,这是不必要的,因为IAnimal
、ICarnivore
等的实现在这两种动物之间将是相同的。
有没有一种方法可以只实现每个接口的方法一次?
附注:接口和依赖/层次结构可能不能更改。
英文:
Imagine 5 interfaces:
IAnimal
ICanivore
extendsIAnimal
IPet
extendsIAnimal
IDog
extendsIPet
,ICarnivore
ICat
extendsIPet
,ICarnivore
I now want to create 2 new classes for dogs and cats, class MyDog
and class MyCat
. If I simply do class MyDog implements IDog
and class MyCat implements ICat
, then I will have a lot of code duplication between the two animals, which is unnecessary since the implementations for IAnimal
, ICarnivore
etc. will be identical between the two animals.
Is there a way to implement the methods of every interface only once?
PS: Interfaces and dependecies/hierarchies may not be changed.
答案1
得分: 1
让我们澄清一下代码重复的问题,确保我们谈论的是同一件事,并解释为什么代码重复是不好的:
- 接口中的代码重复。
这两个类都将使用许多相似的接口。接口只需要编写一次,然后多次使用。有时这是可取的,因为修改接口会触发所有子类的修改。但有时这是不可取的,因为修改可能意味着向不希望修改的类添加代码(但由于与不相关的类所需的接口更改而不得不修改)。
- 实现中的代码重复。
这两个类都将实现许多相似的方法。具体方法的实现经常会重复编写。这通常是不可取的,因为需要进行软件维护。如果在一个常被复制的方法的实现中发现了一个错误,就需要在每个副本中实现修复,漏掉的副本会使错误在代码中存在的时间超过预期。另一方面,如果这些副本的生命周期是不同的,那么实际上可能需要这种重复,因为对一个模块的修复(一个具有自己更新计划的较大代码集合)不会强制发布所有模块(其中该错误可能影响很小或根本不影响)。
因此,代码重复通常是不好的,但并不总是不好的。代码重复是不好的情况远远超过合理的情况。
为了消除代码重复,传统的Java方法是使用抽象类。您找出共同的重复部分,然后创建一个抽象类。命名约定通常为“AbstractDog”或“DefaultDog”,这两种命名约定都不是很好(当您最终深入研究命名时)。这个抽象类将为其所属类别应使用的所有方法提供共同的实现。
在您的情况下,“AbstractPet”可能是一个选择,或者“AbstractMammal”。注意,“Mammal”已经是一个抽象的概念,所以也许我们可以去掉多余的“Abstract”。
public abstract class Mammal extends IAnimal {
private float heartbeatsPerMinute;
public Mammal(float heartbeatsPerMinute) {
this.heartbeatsPerMinute = heartbeatsPerMinute;
}
public float getPulse() {
return heartbeatsPerMinute;
}
public abstract getCommonName();
}
现在,您的所有哺乳动物都不需要实现getHeartbeatsPerMinute()
,这可能(或可能不)是接口所需方法之一。
这种建模的主要问题不在于语言,而在于人们经常低估类、抽象类和接口,并提出无法划分为数学集合的组合(我指的不是Java集合,而是离散数学的“集合论”规则)。
使用“类/抽象类/接口”方法很容易入门;但是,如果选择的抽象方法实际上不是其预期领域的共同分母,您必须为某些领域“覆盖”它。这意味着您有一个同时“包含”某些行为但又不包含的集合。这种不清晰的思维会导致随着时间的推移,代码变得难以扩展、维护和理解,因为您不断收集“异常情况”。
一个异常情况可能很容易处理,但是组合两个异常情况通常意味着评估4种情况(两者都存在、两者都不存在,以及一个其他存在的情况和一个其他不存在的情况)。添加另一个异常情况会将当前存在的情况数量乘以2。很快,您会看到六个异常情况会导致64种情况,其中您永远不会真正测试它们的所有正确代码功能。
因此,可以尝试使用抽象类,但请注意每个类都像其实例集合一样。同时请注意,与实际集合理论不同,Java(和其他语言)没有“不在此集合中”的表达式。请注意,虽然您可以自由组合接口,但不能自由组合抽象类(这限制了类型划分的种类,这是集合理论的一个子集,易于实现(对大多数问题来说足够了)。同时要记住,设计一个(集合)类层次结构,无法根据实际世界划分概念,除此之外还要设计一个在Java范围内无法编译的类层次结构,都是有可能的。
实际上,使用类/抽象类/接口并不难;但是,如果您没有看到如何使用这些工具使您的程序易于维护的一瞥,您的程序(以及您自己)可能会遭受自我造成的伤害。如果您的简单方法某天无法奏效,请拿出一些纸张,开始绘制Venn图。在几分钟内,您可能会意识到程序模型的缺陷,以一种告诉您所需的方式(其余的技巧是使程序的其余部分进入该状态)。
祝您好运!
PS. 对不守代码重复的部分表示抱歉。基本上,我们都接受了有些时候您无法使Java的“抽象类”层次结构折叠成一个在任何情况下都易于使用的链条。当发生这种情况时,将发生代码重
英文:
Let's clarify about the code duplication, to make certain we are talking about the same things, and why code duplication is bad:
- Code duplication in the interfaces.
Both of these classes will use a number of similar interfaces. The interface is written once and used many times. This is desirable sometimes; because, modifying the interface triggers modifications in all the child classes. This is also sometimes undesirable, as a modification might mean adding code to classes that you would rather not modify (but have to be modified due to the interface change being needed by an unrelated class).
- Code duplication in the implementation.
Both of these classes will implement a number of similar methods. The implementation of specific methods is often written many times. This is typically undesirable due to the need for software maintenance. If a bug is found in one implementation of a commonly copied method, one would need to implement the fix in every copy, where missed copies would keep the bug in the code longer than desired. On the other hand, if these copies are meant to have different lifecycles, then one might actually desire this duplication, as a fix to one module (a larger collection of code with its own update schedule) wouldn't mandate releasing all modules (where the bug may have little or no impact).
So Code Duplication is generally bad; but, it is not always bad. The scenarios where it is bad are far greater than those scenarios where it makes sense.
To remove code duplication, the traditional Java approach is to use abstract classes. You find the duplicates that are in common, and create an abstract class. The naming convention is often 'AbstractDog' or 'DefaultDog'; both of which are pretty bad naming conventions (When you finally do the deep dive into naming) This abstract class will have the common implementations for all of the methods that it's category should use.
In your case, 'AbstractPet' might be a choice or 'AbstractMammal'. Note that 'Mammal' is already an abstract idea, so maybe let's just drop the redundant 'Abstract'
public abstract class Mammal extends IAnimal {
private float heartbeatsPerMinute;
public Mammal(float heartbeatsPerMinute) {
this.heartbeatsPerMinute = heartbeatsPerMinute;
}
public float getPulse() {
return heartbeatsPerMinute;
}
public abstract getCommonName();
}
Now all of your mammals don't need to implement getHeartbeatsPerMinute()
and that may (or may not) be one of the interface's required methods.
The main problem with this kind of modeling isn't the language. The main problem is that people often under-evaulate the classes, abstract classes, and interfaces and come up with combinations that fail to subdivide into mathematical Sets (I'm not talking the Java Sets, but the discrete math "Set theory" rules).
It's easy to get started with the "Class / Abstract Class / Interface" approach; but, if you choose an abstract method that isn't really a common denominator across it's intended domain, you have to "override" it for some of the domain. This means you have a Set that both "includes" some behavior and at the same time doesn't. This kind of unclear thinking leads to code that becomes hard to extend, maintain, and reason about over time; because, you keep collecting "exception cases".
One exception case might be easy to deal with, but combining two exception cases, typically means evaluating 4 scenrios (both present, both absent, and two where one other is present). Adding in another exception case multiplies the scenarios currently in existance by two. Quickly one can see that six exceptions will lead to 64 scenarios, of which you will never really test them all for correct code functionality.
So, go ahead and experiment with abstract classes; but, be aware that each class acts like a set of it's instances. Also be aware that unlike real set theory, Java (and other languages) don't have a "not in this set" expression. Be aware that while you can combine Interfaces freely, you can't combine abstract classes (this limits the kinds of divisions of types to a subset of set theory, which is easy to implement (and sufficient for most problems). And keep in mind that it is both possible to design a (set) class hierarchy that fails to divide up the concepts according to the real worls in addition to designing a class hierarcy that fails to compile within the scope of Java's rules.
In practice it really isn't hard to use classes / abstract classes / interfaces; however, if you aren't given a glimpse of what you need to do to make your programs easily maintainable with these tools, you programs (and you) will probably suffer from self-inflicted harm. If your simple approaches stop working one day, break out some paper and start drawing Venn diagrams. In a few minutes you'll probably realize the flaws in the program's model in a way that tells you what you need (the rest of the trick is getting the rest of the program into that state).
Good luck!
PS. Sorry about the deviation from the code duplication bits. Basically, we all accept that there are some times when you can't make Java's 'abstract class' hierarcy collapse to a chain that is easily usable in ever scenario. When that happens, code duplication will occur. That's a weakness of the Java language; but, every programming language has weaknesses. Sometime those weaknesses aren't bad enough to even matter, sometimes they are irrelevant to the problem being solved, and sometimes you should switch programming languages.
The decision to switch has to do with accepting that this problem needs features where a weakness directly hurts, but keep in mind that with the switch, you get an entirely different set of weaknesses, often overlapping where teh previous language had strengths.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论