枚举并不是恶魔,到处都是条件语句

Avatar
不若风吹尘
2024-06-23T19:56:41
159
0

你是否看到枚举到处都有相同的条件语句(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 调用感到满意吗?也许这会起作用。也许系统性能会很糟糕。

也许你宁愿定义明确的类型并分别处理它们,而不是试图一起处理它们。如果你画一个维恩图,以这个例子中的产品为例,两个概念有多相似?有时我们认为相同的东西实际上完全不同,应该根据它们周围的能力明确地建模为独特的事物。

Last Modification : 9/20/2024 4:26:57 AM


In This Document