gpt4 book ai didi

c# - 如何在此接口(interface)与继承 C# 示例中最大化代码重用

转载 作者:行者123 更新时间:2023-12-04 02:18:35 28 4
gpt4 key购买 nike

灵感来自 a great video关于使用 JavaScript 示例的主题“优先对象组合而不是继承”;我想在 C# 中试用它来测试我对这个概念的理解,但它并没有像我希望的那样顺利。

/// PREMISE
// Animal base class, Animal can eat
public class Animal
{
public void Eat() { }
}
// Dog inherits from Animal and can eat and bark
public class Dog : Animal
{
public void Bark() { Console.WriteLine("Bark"); }
}
// Cat inherits from Animal and can eat and meow
public class Cat : Animal
{
public void Meow() { Console.WriteLine("Meow"); }
}
// Robot base class, Robot can drive
public class Robot
{
public void Drive() { }
}

问题是我想添加可以吠叫和驾驶但不能吃东西的RobotDog类。

第一个解决方案是创建 RobotDog 作为 Robot 的子类,
public class RobotDog : Robot
{
public void Bark() { Console.WriteLine("Bark"); }
}

但是为了给它一个 Bark 函数,我们必须复制并粘贴 Dog's Bark 函数,所以现在我们有重复的代码。

第二种解决方案是创建一个带有 Bark 方法的通用父类(super class),然后 Animal 和 Robot 类都继承自
public class WorldObject
{
public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }

但是现在每只动物和每一个机器人都会有一种吠叫方法,而它们中的大多数都不需要。继续这种模式,子类将充满它们不需要的方法。

第三种解决方案是为可以 Bark 的类创建一个 IBarkable 接口(interface)
public interface IBarkable
{
void Bark();
}

并在 Dog 和 RobotDog 类中实现它
public class Dog : Animal, IBarkable
{
public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

但是我们又一次出现了重复的代码!

第四种方法是再次使用 IBarkable 接口(interface),但创建一个 Bark 助手类,然后每个 Dog 和 RobotDog 接口(interface)实现都会调用该类。

这感觉是最好的方法(以及视频似乎推荐的方法),但我也可以从项目中看到一个问题,因为它被助手弄得杂乱无章。

第五个建议的(hacky?)解决方案是在空的 IBarkable 接口(interface)上挂起一个扩展方法,这样如果你实现了 IBarkable,你就可以 Bark
public interface IBarker {    }
public static class ExtensionMethods
{
public static void Bark(this IBarker barker) {
Console.WriteLine("Woof!");
}
}

这个网站上很多类似的回答问题,以及我读过的文章,似乎都推荐使用 abstract但是,类不会与解决方案 2 有相同的问题吗?

将 RobotDog 类添加到此示例中的最佳面向对象方法是什么?

最佳答案

起初,如果您想遵循“组合优于继承”,那么超过一半的解决方案不适合,因为您仍然在这些解决方案中使用继承。

实际上用“组合优于继承”来实现它有多种不同的方式,可能每一种都有自己的优点和缺点。首先,一种可能的方式,但目前在 C# 中不可用。至少不是一些重写 IL 代码的扩展。一种想法通常是使用mixin。所以你有接口(interface)和一个 Mixin 类。 Mixin 基本上只包含“注入(inject)”到类中的方法。他们不是从中派生的。所以你可以有一个这样的类(所有代码都是伪代码)

class RobotDog 
implements interface IEat, IBark
implements mixin MEat, MBark
IEatIBark提供接口(interface),而 MEatMBark将是具有一些您可以注入(inject)的默认实现的 mixin。这样的设计在 JavaScript 中是可能的,但目前在 C# 中是不可能的。它的优点是最后你有一个 RobotDog具有 IEat 的所有方法的类和 IBark具有共享实现。这同时也是一个缺点,因为您创建了具有很多方法的大类。最重要的是可能存在方法冲突。例如,当您想注入(inject)具有相同名称/签名的两个不同接口(interface)时。尽管这种方法首先看起来不错,但我认为缺点很大,我不鼓励这样的设计。

由于 C# 不直接支持 Mixins,您可以使用扩展方法以某种方式重建上面的设计。所以你还有 IEatIBark接口(interface)。并且您为接口(interface)提供扩展方法。但它与 mixin 实现具有相同的缺点。所有方法都出现在对象上,方法名称冲突问题。同样最重要的是,组合的想法也是您可以提供不同的实现。对于同一个界面,你也可以有不同的 Mixin。最重要的是,mixin 只是用于某种默认实现,其想法仍然是您可以覆盖或更改方法。

用扩展方法做那种事情是可能的,但我不会使用这样的设计。理论上,您可以创建多个不同的命名空间,因此根据加载的命名空间,您将获得具有不同实现的不同扩展方法。但是这样的设计我觉得比较别扭。所以我不会使用这样的设计。

我如何解决它的典型方法是期待您想要的每种行为的字段。所以你的 RobotDog 看起来像这样
class RobotDog(ieat, ibark)
IEat Eat = ieat
IBark Bark = ibark

所以这意味着。您有一个包含两个属性的类 EatBark .这些属性的类型是 IEatIBark .如果你想创建一个 RobotDog实例然后你必须传入一个特定的 IEatIBark您要使用的实现。
let eat  = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)

现在 RobotDog 会像猫一样吃东西,像狗一样吠叫。您只需调用 RobotDog 应该执行的操作即可。
robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()

现在,您的 RobotDog 的行为完全取决于您在构建对象时提供的注入(inject)对象。您还可以在运行时使用另一个类切换行为。如果您的 RobotDog 在游戏中并且 Barking 升级了,您只需在运行时用另一个对象和您想要的行为替换 Bark
robotdog.Bark <- new DeadlyScreamBarking()

无论是通过变异它,还是创建一个新对象。您可以使用可变或不可变设计,这取决于您。所以你有代码共享。至少我更喜欢这种风格,因为不是拥有一个包含数百种方法的对象,你基本上拥有一个包含不同对象的第一层,这些对象将每个能力完全分开。例如,如果您将移动添加到您的 RobotDog 类,您只需添加一个“Imovable”属性,该接口(interface)可以包含多个方法,如 MoveTo , CalculatePath , Forward , SetSpeed等等。它们将在 robotdog.Move.XYZ 下完全可用.碰撞方法也没有问题。例如,每个类上可能有相同名称的方法,没有任何问题。并且在上面。你也可以有多个相同类型的行为!例如 Health 和 Shield 可以使用相同的类型。例如一个简单的“MinMax”类型,它包含一个最小值/最大值和当前值以及对它们进行操作的方法。 Health/Shield 基本上具有相同的行为,您可以使用这种方法轻松地在同一个类中使用其中的两个,因为没有方法/属性或事件发生冲突。
robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)

以前的设计可能会略有改变,但我不认为它会变得更好。但是很多人无脑地采用每一种设计模式或法则,希望它能自动让一切变得更好。我想在这里指的是通常称为 Law-of-Demeter我认为这很糟糕,尤其是在这个例子中。其实好与不好的讨论很多。我认为这不是一个好的规则,在这种情况下它也变得显而易见。如果你遵循它,你必须为你拥有的每个对象实现一个方法。所以代替
robotdog.Eat.Fruit()
robotdog.Eat.Drink()

你在 RobotDog 上实现了在 Eat 字段上调用一些方法的方法,那么你最终得到了什么?
robotdog.EatFruit()
robotdog.EatDrink()

您还需要再次解决碰撞,例如
robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)

实际上,您只是编写了许多仅委托(delegate)给某个字段上的其他方法的方法。但是你赢了什么?基本上什么都没有。你只是无脑地遵循了一条规则。理论上可以说。但是 EatFruit()在调用 Eat.Fruit() 之前可以做一些不同的事情或做一些额外的事情. Weel 是的,可能是。但是如果你想要其他不同的 Eat 行为,那么你只需创建另一个实现 IEat 的类。并且在实例化它时将该类分配给机器人狗。

从这个意义上说,得墨忒耳定律不是点数练习。

http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/

作为结论。如果您遵循该设计,我会考虑使用第三个版本。使用包含您的 Behavior 对象的属性,您可以直接使用这些行为。

关于c# - 如何在此接口(interface)与继承 C# 示例中最大化代码重用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35368684/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com