这可能是一个通用的OOP问题。我想在接口和抽象类的使用基础上做一个通用的比较。
什么时候需要使用接口,什么时候需要使用抽象类?
这可能是一个通用的OOP问题。我想在接口和抽象类的使用基础上做一个通用的比较。
什么时候需要使用接口,什么时候需要使用抽象类?
当前回答
我认为最简洁的说法是:
共享属性=>抽象类。 共享功能=>接口。
更简单地说……
抽象类示例:
public abstract class BaseAnimal
{
public int NumberOfLegs { get; set; }
protected BaseAnimal(int numberOfLegs)
{
NumberOfLegs = numberOfLegs;
}
}
public class Dog : BaseAnimal
{
public Dog() : base(4) { }
}
public class Human : BaseAnimal
{
public Human() : base(2) { }
}
由于动物有一个共同的属性——在这种情况下是腿的数量——创建一个包含这个共同属性的抽象类是有意义的。这也允许我们编写操作该属性的通用代码。例如:
public static int CountAllLegs(List<BaseAnimal> animals)
{
int legCount = 0;
foreach (BaseAnimal animal in animals)
{
legCount += animal.NumberOfLegs;
}
return legCount;
}
接口的例子:
public interface IMakeSound
{
void MakeSound();
}
public class Car : IMakeSound
{
public void MakeSound() => Console.WriteLine("Vroom!");
}
public class Vuvuzela : IMakeSound
{
public void MakeSound() => Console.WriteLine("VZZZZZZZZZZZZZ!");
}
这里要注意的是,呜呜祖拉和汽车是完全不同的东西,但它们有共同的功能:发出声音。因此,接口在这里是有意义的。此外,它将允许程序员将发出声音的东西分组在一个公共界面下——在本例中是IMakeSound。通过这种设计,你可以编写以下代码:
List<IMakeSound> soundMakers = new List<ImakeSound>();
soundMakers.Add(new Car());
soundMakers.Add(new Vuvuzela());
soundMakers.Add(new Car());
soundMakers.Add(new Vuvuzela());
soundMakers.Add(new Vuvuzela());
foreach (IMakeSound soundMaker in soundMakers)
{
soundMaker.MakeSound();
}
你知道那会输出什么吗?
最后,您可以将两者结合起来。
结合例子:
public interface IMakeSound
{
void MakeSound();
}
public abstract class BaseAnimal : IMakeSound
{
public int NumberOfLegs { get; set; }
protected BaseAnimal(int numberOfLegs)
{
NumberOfLegs = numberOfLegs;
}
public abstract void MakeSound();
}
public class Cat : BaseAnimal
{
public Cat() : base(4) { }
public override void MakeSound() => Console.WriteLine("Meow!");
}
public class Human : BaseAnimal
{
public Human() : base(2) { }
public override void MakeSound() => Console.WriteLine("Hello, world!");
}
这里,我们要求所有的BaseAnimals发出声音,但是我们还不知道它的实现。在这种情况下,我们可以抽象接口实现并将其实现委托给它的子类。
最后一点,还记得在抽象类示例中我们如何操作不同对象的共享属性,以及在接口示例中我们如何调用不同对象的共享功能吗?在最后一个例子中,我们可以两者都做。
其他回答
车辆包括汽车、坦克、飞机、手推车等。
抽象类Vehicle可以有子类,如car、tank、plane、cart等。
public abstract Vehicle {...}
public Car extends Vehicle {...}
public Tank extends Vehicle {...}
那么,什么是可移动的?几乎一切! 然后
石头,蛋糕,汽车,行星,星系,甚至你都是可移动的!
它们中的大多数也是可观察的!
什么是可吃的?这是一款名为《美味星球》的游戏。 然后
石头,汽车,行星,星系,甚至时间!
public interface Movable {...}
public interface Observable {...}
public interface Eatable {...}
public class Stone implements Movable, Eatable, Observable {...}
public class Time implements Eatable, Observable {...}
public class Stupidity implements Observable {...}
终于!
public class ChocolateCar extends Vehicle implements Eatable {...}
纯粹在继承的基础上,你可以使用一个抽象的定义清晰的后代,抽象的关系(例如动物->猫)和/或需要继承虚拟或非公共属性,特别是共享状态(接口不支持)。
你应该尝试使用组合(通过依赖注入)而不是继承,并且注意接口作为契约支持单元测试、关注点分离和(语言变化)多重继承,而抽象则不能。
就我个人而言,我几乎从不需要编写抽象类。
大多数时候,我看到抽象类被(错误地)使用,这是因为抽象类的作者使用了“模板方法”模式。
“Template方法”的问题在于它几乎总是某种程度上是可重入的——“派生”类不仅知道它正在实现的基类的“抽象”方法,还知道基类的公共方法,即使大多数时候它不需要调用它们。
(过于简化的)例子:
abstract class QuickSorter
{
public void Sort(object[] items)
{
// implementation code that somewhere along the way calls:
bool less = compare(x,y);
// ... more implementation code
}
abstract bool compare(object lhs, object rhs);
}
因此,在这里,该类的作者编写了一个泛型算法,并打算通过提供自己的“钩子”(在本例中是一个“比较”方法)来“专门化”它,以供人们使用。
所以预期的用法是这样的:
class NameSorter : QuickSorter
{
public bool compare(object lhs, object rhs)
{
// etc.
}
}
这样做的问题在于,你将两个概念过度地耦合在了一起:
比较两个项目的一种方法(哪个项目应该放在前面) 排序项目的方法(即快速排序vs归并排序等)
在上面的代码中,从理论上讲,“compare”方法的作者可以重新调用超类“Sort”方法…即使在实践中,他们永远不会想要或需要这样做。
为这种不必要的耦合付出的代价是,很难更改超类,而且在大多数OO语言中,不可能在运行时更改它。
另一种方法是使用“策略”设计模式:
interface IComparator
{
bool compare(object lhs, object rhs);
}
class QuickSorter
{
private readonly IComparator comparator;
public QuickSorter(IComparator comparator)
{
this.comparator = comparator;
}
public void Sort(object[] items)
{
// usual code but call comparator.Compare();
}
}
class NameComparator : IComparator
{
bool compare(object lhs, object rhs)
{
// same code as before;
}
}
现在请注意:我们所拥有的只是接口,以及这些接口的具体实现。在实践中,您实际上不需要任何其他东西来进行高级OO设计。
为了“隐藏”我们已经通过使用“QuickSort”类和“NameComparator”实现了“名称排序”的事实,我们仍然可以在某个地方写一个工厂方法:
ISorter CreateNameSorter()
{
return new QuickSorter(new NameComparator());
}
任何时候你有一个抽象类,你都可以这样做…即使基类和派生类之间存在自然的可重入关系,将它们显式化通常也是值得的。
最后一个想法:我们上面所做的一切都是通过使用“QuickSort”函数和“NameComparison”函数来“组合”一个“NameSorting”函数……在函数式编程语言中,这种编程风格变得更加自然,代码更少。
基本的经验法则是:对于“名词”使用抽象类,对于“动词”使用接口
例如:car是一个抽象类和驱动器,我们可以让它成为一个接口。
答案因语言而异。例如,在Java中,一个类可以实现(继承)多个接口,但只能继承一个抽象类。所以接口给了你更多的灵活性。但在c++中却不是这样。