你是否看到枚举到处都有相同的条件语句(if/switch)?有多种处理方法,但很大程度上取决于你的上下文。类型检查周围的条件语句、扩展方法、继承和多态性都是选择。
枚举
在下面的例子中,产品的 OfferingType 用于确定它是否应具有可下载的 URL 和文件名。然而,由于只有模板、电子书和离线课程支持此功能,其他类型,如书籍或课程,将返回 null。
public enum OfferingType
{
Course,
Ebook,
Book,
Template,
OfflineCourse,
}
public sealed record Product(int Id, OfferingType Type);
public sealed class ProductHandler(ResourcesHelper _resourcesHelper)
{
public void DoStuff(Product product)
{
if (product.Type == OfferingType.Template ||
product.Type == OfferingType.Ebook ||
product.Type == OfferingType.OfflineCourse)
{
var fileName = _resourcesHelper.GetDefaultDownloadFileName(product.Type);
var downloadUrl = _resourcesHelper.GetDownloadUrl(product.Type);
}
}
}
public sealed class ResourcesHelper
{
public string? GetDownloadUrl(OfferingType offeringType)
{
if (offeringType == OfferingType.Template ||
offeringType == OfferingType.Ebook ||
offeringType == OfferingType.OfflineCourse)
{
// some code to do this...
return "TODO: get the download URL";
}
return null;
}
public string? GetDefaultDownloadFileName(OfferingType offeringType)
{
if (offeringType == OfferingType.Template ||
offeringType == OfferingType.Ebook ||
offeringType == OfferingType.OfflineCourse)
{
// some code to do this...
return "TODO: get the file name";
}
return null;
}
}
如前所述,OfferingType 作为参数被传递给其他方法,而这些方法最终都会进行完全相同的条件检查。
public sealed record DownloadableResource(
int Id,
int ProductId,
string DownloadUrl,
string DefaultDownloadFilename);
public sealed class ProductHandler2(ResourcesHelper2 _resourcesHelper)
{
public void DoStuff(Product product)
{
var downloadableResource = _resourcesHelper.GetDownloadable(product.Id);
// do stuff with downloadable resource
}
}
public sealed class ResourcesHelper2
{
public DownloadableResource? GetDownloadable(int productId)
{
// TODO: go fetch this from the DB or return null...
}
}
如果我们有成千上万的产品,但只有少数产品拥有下载链接,那么我们将进行大量无用的数据库调用。
类型、继承、多态性和扩展
另一种选择是使用不同的类型来表示特定的产品类型,而不是枚举。所以我们有一个抽象的 Product 类和其他继承它的类,如 Template、Ebook 和 OfflineCourse。
我对此的问题是,这实际上与枚举没有太大区别,因为我们仍然需要进行条件检查,只是不是针对枚举,而是针对类型检查。
public abstract class Product(int id)
{
public int Id { get; } = id;
}
public class Template(int id) : Product(id) { }
public class Ebook(int id) : Product(id) { }
public class OfflineCourse(int id) : Product(id) { }
public sealed class ProductHandler(ResourcesHelper resourcesHelper)
{
public void DoStuff(Product product)
{
if (product is Template ||
product is Ebook||
product is OfflineCourse)
{
var fileName = resourcesHelper.GetDefaultDownloadFileName(product);
var downloadUrl = resourcesHelper.GetDownloadUrl(product);
}
}
}
public sealed class ResourcesHelper
{
public string? GetDownloadUrl(Product product)
{
if (product is Template ||
product is Ebook||
product is OfflineCourse)
{
// some code to do this...
return "TODO: get the download URL";
}
return null;
}
public string? GetDefaultDownloadFileName(Product product)
{
if (product is Template ||
product is Ebook||
product is OfflineCourse)
{
// some code to do this...
return "TODO: get the file name";
}
return null;
}
}
我们可以更进一步,定义一个基类,它包含几个我们可以重写的虚方法。更重要的是,我们可以使用选项类型(Option type)作为返回值,或者直接返回 none 来表示不存在的值。这种方式允许我们通过多态来处理不同类型的产品逻辑,同时利用选项类型优雅地处理可能缺失的数据情况。
public class Product(int id, OfferingType type)
{
public virtual Option<string> GetDownloadUrl()
{
return Option.None<string>();
}
public virtual Option<string> GetDefaultDownloadFileName()
{
return Option.None<string>();
}
}
public class DownloadProduct(int id, OfferingType type) : Product(id, type)
{
public override Option<string> GetDefaultDownloadFileName()
{
return Option.Some("Some Value");
}
public override Option<string> GetDownloadUrl()
{
return Option.Some("Some Value");
}
}
public class Course(int id, OfferingType type) : Product(id, type)
{
public override Option<string> GetDefaultDownloadFileName()
{
return Option.None<string>();
}
public override Option<string> GetDownloadUrl()
{
return Option.None<string>();
}
}
public sealed class ProductHandler()
{
public void DoStuff(Product product)
{
product.GetDefaultDownloadFileName().MatchSome(
filename =>
{
// Do something with the filename.
});
product.GetDownloadUrl().MatchSome(
url =>
{
// Do something with the url
});
}
}
我们已经删除了条件语句,并在有值时通过调用 Match()
处理 Option
类型。虽然不完全相同,但你可以使返回类型为可空字符串并处理可空值。
我们可以继续深入,像之前一样创建一个产品抽象类,但我们不需要重写任何内容;相反,我们明确地有一个实现下载产品的类型。
public abstract class Product(int id, OfferingType type) { }
public class DownloadableProduct(int id, OfferingType type) : Product(id, type)
{
public Option<string> GetDefaultDownloadFileName()
{
return Option.Some("Some Value");
}
public Option<string> GetDownloadUrl()
{
return Option.Some("Some Value");
}
}
public sealed class ProductHandler()
{
public void DoStuff(DownloadableProduct product)
{
product.GetDefaultDownloadFileName().MatchSome(
filename =>
{
// Do something with the filename.
});
product.GetDownloadUrl().MatchSome(
url =>
{
// Do something with the url
});
}
}
我们没有处理基类型,而是明确处理 DownloadableProduct
。我们将有其他处理器,或者任何其他代码将有不同的代码路径来处理不同的产品。
或者,我们可以回到最初的条件语句,但只需在枚举上使用扩展方法,将所有有效的提供类型分组,简化我们的条件检查。
public static class Extensions
{
public static bool IsDownloadable(this OfferingType offeringType)
{
var validOfferings = new List<OfferingType>()
{
OfferingType.Template,
OfferingType.Ebook,
OfferingType.OfflineCourse
};
return validOfferings.Contains(offeringType);
}
}
public sealed class ProductHandler(ResourcesHelper resourcesHelper)
{
public void DoStuff(Product product)
{
if (product.Type.IsDownloadable())
{
var fileName = resourcesHelper.GetDefaultDownloadFileName(product.Type);
var downloadUrl = resourcesHelper.GetDownloadUrl(product.Type);
}
}
}
条件语句
你不必对枚举有相同的重复条件语句。有许多不同的替代方法来处理它们。哪种解决方案最有效取决于你的具体情况。你可以委托给运行时,并且对额外的 I/O DB 调用感到满意吗?也许这会起作用。也许系统性能会很糟糕。
也许你宁愿定义明确的类型并分别处理它们,而不是试图一起处理它们。如果你画一个维恩图,以这个例子中的产品为例,两个概念有多相似?有时我们认为相同的东西实际上完全不同,应该根据它们周围的能力明确地建模为独特的事物。
Comments