目录
介绍
面向对象编程的世界有点令人困惑。它需要掌握很多东西:SOLID原则、设计模式等等。这引发了很多讨论:设计模式是否仍然相关,SOLID是否仅用于面向对象的代码?有人说,一个人应该更喜欢组合而不是继承,但是当一个人应该选择一个或另一个时,确切的经验法则是什么?
由于对这个问题发表了许多意见,我不认为我的意见会是最终的,但是,在本文中,我将介绍帮助我使用C#进行日常编程的系统。但在我们跳到这个之前,让我们看一下另一个问题。考虑代码。
public class A
{
public virtual void Foo()
{
Console.WriteLine("A");
}
}
public class B : A
{
public override void Foo()
{
Console.WriteLine("B");
}
}
public class C : A
{
public void Foo()
{
Console.WriteLine("C");
}
}
var b = new B();
var c = new C();
b.Foo();
c.Foo();
你能说出代码在每种情况下会输出什么吗?如果你答对了,各自的输出将是“B”和“C”,那么为什么override关键词很重要?
输入动态多态性
多态性被认为是面向对象编程的支柱之一。但这究竟意味着什么?维基百科告诉我们,多态性是为不同类型的实体提供单个接口或使用单个符号来表示多个不同的类型。
我不希望你从第一次就掌握这个定义,所以让我们看一些例子。
string Add(string input1, string input2) => string.Concat(input1, input2);
int Add(int input1, int input2) => input1 + input2;
上面是临时多态性的示例,它指的是可以应用于不同类型的参数的多态函数,但根据应用它们的参数的类型,其行为也不同。那么,为什么多态性对面向对象代码如此重要呢?此代码段没有为此问题提供明确的答案。让我们看一下更多的例子。
class List<T> {
class Node<T> {
T elem;
Node<T> next;
}
Node<T> head;
int length() { ... }
}
这是参数多态的一个例子,老实说,它看起来比面向对象更实用。让我们看一下最后一个例子。
interface IDiscountCalculator
{
decimal CalculateDiscount(Item item);
}
class ThanksgivingDayDiscountCalculator : IDiscountCalculator
{
public decimal CalculateDiscount(Item discount)
{
//omitted
}
}
class RegularCustomerDiscountCalculator : IDiscountCalculator
{
public decimal CalculateDiscount(Item discount)
{
//omitted
}
}
这是动态多态性的一个示例,它是在运行时应用多态性的术语(主要通过子类型化)。如果你在面试前试图记住所有这些设计模式或一些SOLID原则,你可能会注意到熟悉的东西的形状。让我们看看动态多态性如何在这些概念中表现出来。
动态多态性和设计模式
大多数设计模式(策略、命令、装饰器等)都依赖于注入抽象类或接口,并在运行时选择它的实现。让我们看一下一些类图,以确保情况确实如此。
上面是策略模式的图,其中Client使用抽象,并在运行时选择其具体实现。
这是装饰器。
在这种情况下,包装器接受wrappee,它是抽象的实例,其实现在运行时可能会有所不同。
动态多态性和SOLID
在面试中问到SOLID时,我经常听到的回答是“S代表单一责任,O代表呃......”。相反,我认为这个首字母缩略词的后四个字母更重要,因为它们代表了动态多态性顺利运行的一组先决条件。
例如,开闭原则代表了一种思维方式,在这种思维方式中,您将每个新问题都作为抽象的子类型进行处理。回想一下IDiscountCalculator例子。想象一下,当您必须添加另一个折扣(例如父亲节)时。为了满足开闭原则,您必须添加另一个执行计算的FathersDayDiscountCalculator子类。
让我们继续讨论Liskov替换原理。想象一下打破它的情况:我们必须检查用户是否真的是父亲,并且日期是否匹配。因此,我们添加了检查用户是否符合条件的public方法。
class FathersDayDiscountCalculator : IDiscountCalculator
{
public decimal CalculateDiscount(Item discount)
{
//omitted
}
public bool IsEligible(User user, DateTime date)
{
//omitted
}
}
现在调用代码将面临一些复杂情况:
private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
decimal result = 0;
foreach (var discountCalculator in _discountCalculators)
{
if (discountCalculator is FathersDayDiscountCalculator)
{
var fathersDayDiscountCalculator =
discountCalculator as FathersDayDiscountCalculator;
if (fathersDayDiscountCalculator.IsEligible(user, DateTime.UtcNow))
{
result += fathersDayDiscountCalculator.CalculateDiscount(item);
}
}
else
{
result += discountCalculator.CalculateDiscount(item);
}
}
return result;
}
很啰嗦,不是吗?因此,为了满足Liskov替换原则,我们必须强制我们所有的实现都表现出抽象提供的相同公共契约。否则,会使动态多态性的应用复杂化。
使动态多态性应用复杂化的另一件事是抽象过于宽泛。想象一下,我们已经将IsEligible作为接口的一部分,现在所有具体的类都实现了它。调用代码大大简化。
private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
decimal result = 0;
foreach (var discountCalculator in _discountCalculators)
{
result += discountCalculator.CalculateDiscount(item);
}
return result;
}
但是现在想象一下(我知道这个例子有点做作,但只是为了论证!)其中一个实现抛出NotImplementedException,因为它对这种特定类型的折扣没有意义。此时,您可以预见到问题CalculateDiscountForItem会失败,并出现运行时异常。
这就是接口隔离原则的意义所在:不要把抽象做得太宽泛,这样具体类型就不会在实现它们时遇到麻烦,从而使你的动态多态性复杂化,产生不必要的NotImplementedException。
到这个时候,你可能会观察到依赖反转原则在起作用。在上面的示例中,我们处理抽象的集合,并且不知道它们的运行时类型。
更喜欢组合而不是继承
我不会深入探讨为什么组合图更可取。有很多例子说明继承如何使事情复杂化。但是现在,当你对什么是合法的继承情况有疑问时,这里有一个答案给你:当它促进动态多态性时。
Virtual和Override
在这一点上,那些在文章开头没有正确知道问题答案的人可能会怀疑这是一个棘手的问题。事实上,虽然当我们使用var关键字时行为是相似的,但当我们应用动态多态性时,差异开始出现。就此而言,让我们将两个实例都转换为父类型。
A b = new B();
A c = new C();
现在输出将分别为“B”和“A”。记住这个吗?override关键字的目标是促进动态多态性。所以可以这样想:当我们注入抽象时,我们期望使用具体的类型实现,因为override有助于实现这个目标,所以将调用B实现。
为什么这很重要?
所以现在,你知道如何记住所有这些讨厌的面试问题了。但最好奇的人可能会问:这种编程风格有什么好处?为什么我们努力在面向对象的代码库中应用动态多态性?
想象一下,我们的代码库中有两个方法。
public string GetCurrencySign(string currencyCode)
{
return currencyCode switch
{
"US" => "$",
"JP" => "¥",
_ => throw new ArgumentOutOfRangeException(nameof(currencyCode)),
};
}
public decimal GetRoundUpAmount(decimal amount, string currencyCode)
{
return currencyCode switch
{
"US" => Math.Floor(amount + 1),
"JP" => Math.Floor(amount / 100 + 1) * 100,
_ => throw new ArgumentOutOfRangeException(nameof(currencyCode))
};
}
现在想象一下,我们必须增加另一个国家的支持。看起来没什么大不了的,但想象一下这两种方法隐藏在那些具有数千个类和数十万行代码的“真实世界”代码库之一中。最有可能的是,您会忘记所有应该添加国家/地区支持的地方。这正是霰弹枪手术代码的味道。
我们如何解决它?让我们在一个地方提取与国家/地区代码相关的所有信息。
public interface IPaymentStrategy
{
string CurrencySign { get; }
decimal GetRoundUpAmount(decimal amount);
}
现在,当我们必须添加新的国家/地区代码时,我们被迫实现上面的接口,因此我们绝对不会忘记任何内容。我们使用factory返回IPaymentStrategy的实例。
public string GetCurrencySign(string currencyCode)
{
var strategy = _strategyFactory.CreateStrategy(currencyCode);
return strategy.CurrencySign;
}
在上面的示例中,我们通过应用动态多态性修复了代码异味。有时,我们设法满足一些SOLID原则(即通过扩展而不是修改来制作新功能,从而实现Open-Closed)并应用设计模式。通过只应用一个OOD原则,为你的简历提供一堆很酷的企业东西!
结论
软件工程师,就像我们大多数人一样,倾向于遵循很多原则,而不质疑它们的基本原理。当这样做时,原则往往会被扭曲并偏离其最初的目标。因此,通过质疑最初的目标是什么,我们可以应用这些原则,因为它们打算应用。
在这篇文章中,我认为OOD之外的核心原则之一是动态多态性的应用,而许多原则(SOLID、设计模式)只是围绕它构建的助记符。
https://www.codeproject.com/Articles/5363685/Dynamic-Polymorphism-Key-Concept-to-Master-OOP