【问题标题】:Factory Pattern, open closed principle, interfaces and generics工厂模式、开闭原则、接口和泛型
【发布时间】:2015-09-14 17:16:08
【问题描述】:

我尝试根据开闭原则重构一些代码,但在应用设计模式时,我似乎无法正确获取以下类。 (对于下面列出的许多类,我深表歉意 - 我已尽可能减少它们,但其余部分需要向您展示我的设计)。

设置由以下类组成:

public interface IPortFactory
{
    IPort CreatePort(int id, PortDetails details);
}

public class PtpPortFactory : IPortFactory
{
    public IPort CreatePort(int id, PortDetails details)
    {
        var ptpPortDetails = details as PtpPortDetails;
        if (ptpPortDetails == null)
        {
            throw new ArgumentException("Port details does not match ptp ports", "details");
        }

        return new PtpPort(id, FiberCapability.FromValue(ptpPortDetails.Capability));
    }
}

public interface IPort
{
    int Id { get; }
}

public interface IInternetPort : IPort
{
    bool OfCapability(FiberCapability capability);
}

public class PtpPort : IInternetPort
{
    private readonly FiberCapability _capability;

    public PtpPort(int id, FiberCapability capability)
    {
        _capability = capability;
        Id = id;
    }

    public int Id { get; private set; }

    public bool OfCapability(FiberCapability capability)
    {
        return capability.Equals(_capability);
    }
}

除了PtpPort,我还有PonPort,它也实现了IInternetPort,而CatvPort只是实现了IPort

在这段代码中,我认为有代码异味的迹象。在PtpPortFactory 中的CreatePort 中,漂亮的事情是它接受PtpPortDetails(继承自PortDetails)而不是强制转换。但是,如果这样做,我将无法创建同样实现IPortFactoryPonPortFactory,因为这些端口需要PonPortDetails。或CatvPortFactory 就此事。

当我使用端口工厂时出现另一种代码气味

PortType portType = command.PortType;
IPortFactory portFactory = portType.GetPortFactory();
var portsToSelectFrom = ports.Select(port => (IInternetPort) portFactory.CreatePort(port.Id, port.PortDetails)).ToList();

我真的不想从IPortIInternetPort 进行向下转换,而只需让CreatePort 返回IInternetPort

理解上面需要的最后一点信息大概就是下面这个类(基于Jimmy BogardsEnumeration类):

public abstract class PortType : Enumeration<PortType, int>
{
    public static readonly PortType Ptp = new PtpPortType();
    public static readonly PortType Pon = new PonPortType();
    public static readonly PortType Catv = new CatvPortType();

    protected PortType(int value, string description)
        : base(value, description) { }

    public abstract IPortFactory GetPortFactory();

    private class CatvPortType : PortType
    {
        public CatvPortType() : base(2, "catv") { }

        public override IPortFactory GetPortFactory()
        {
            return new CatvPortFactory();
        }
    }

    private class PonPortType : PortType
    {
        public PonPortType() : base(1, "pon") { }

        public override IPortFactory GetPortFactory()
        {
            throw new NotImplementedException("Pon ports are not supported");
        }
    }

    private class PtpPortType : PortType
    {
        public PtpPortType() : base(0, "ptp") { }

        public override IPortFactory GetPortFactory()
        {
            return new PtpPortFactory();
        }
    }
}

我真的希望有人能一路帮助我(我尝试过引入泛型,但似乎总是遇到 C# 不支持返回类型协变的障碍)。

此外,任何其他有助于我编写更好代码的提示和技巧将不胜感激。

更新

由于有评论要求,我在下面添加了更多代码。

public Port Handle(TakeInternetPortCommand command)
{
    var portLocatorService = new PortLocatorService();
    IList<Port> availablePorts = portLocatorService.FindAvailablePorts(command.Pop, command.PortType);

    PortType portType = command.PortType;
    IPortFactory portFactory = portType.GetPortFactory();
    var portsToSelectFrom = ports.Select(port => (IInternetPort) portFactory.CreatePort(port.Id, port.PortDetails)).ToList();

    IPort port = _algorithm.RunOn(portsToSelectFrom);
    Port chosenPort = availablePorts.First(p => p.Id == port.Id);
    chosenPort.Take(command.Spir);
    _portRepository.Add(chosenPort);
    return chosenPort;
}

不要被突然之间还有Port 类型这一事实搞糊涂了。这是另一个有界上下文中的聚合(在 DDD 的意义上)。 该算法需要将IInternetPort 列表作为输入,因为它在内部使用OfCapability 方法来选择端口。然而,当算法选择了正确的端口时,我们只对Id感兴趣,因此返回类型只是IPort

【问题讨论】:

  • 当您有工作代码并寻求帮助以重构或获得建议时,Code Review Stack Exchange 最适合这项工作。
  • IPortIInternetPortIPortFactory 的消费者/客户是谁?您如何/何时决定使用PtpPortPonPortCatvPort?它们是否都在同一个应用程序实例中使用?还是您决定根据某些应用程序配置只使用其中一个?
  • @Pierre-LucPineault,抱歉在错误的论坛发帖。下次我会记得 Code Review Stack Exchange!
  • @YacoubMassad,感谢您的深入提问。 IPortIInternetPortIPortFactory 的消费者/客户端,即我原帖中“使用端口工厂时出现另一种代码气味”正下方的三行的地方,是同一个命令处理程序应用。端口类型在命令中作为字符串发送(最终是客户端将其作为发布请求正文的一部分提交)。
  • IPort作为接口有什么具体原因吗?

标签: c# design-patterns factory-pattern solid-principles open-closed-principle


【解决方案1】:

以下是我对您要解决的问题的理解(业务问题,而不是设计问题):

你有一个端口列表,你想在列表上执行一些算法,从列表中选择一个端口(基于特定于算法的一些标准)。

在这里,我将建议一种对此进行建模的方法。我将假设您有以下输入类:

public class PortInput
{
    public int Id { get; set; }

    public PortDetails PortDetails { get; set; }
}

这对应于您问题中 Port 类的一部分。

这里有算法的接口:

public interface IPortSelectionAlgorithm
{
    int SelectPort(PortInput[] port_inputs);
}

请注意,此接口模拟了我们要解决的问题。即,给定一个端口列表 -> 我们需要选择一个

以下是此类算法接口的实现:

public class PortSelectionAlgorithm : IPortSelectionAlgorithm
{
    private readonly ICapabilityService<PortDetails> m_CapabilityService;

    public PortSelectionAlgorithm(ICapabilityService<PortDetails> capability_service)
    {
        m_CapabilityService = capability_service;
    }

    public int SelectPort(PortInput[] port_inputs)
    {
        //Here you can use m_CapabilityService to know if a specific port has specific capability

        ...
    }
}

这个实现声明的是它需要一些知道如何根据端口详细信息获取端口功能的服务。以下是此类服务合同的定义:

public interface ICapabilityService<TDetails> where TDetails : PortDetails
{
    bool OfCapability(TDetails port_details, FiberCapability capability);
}

对于每种端口类型,我们都可以创建此类服务的实现,如下所示:

public class PtpPortCapabilityService: ICapabilityService<PtpPortDetails>
{
    public bool OfCapability(PtpPortDetails port_details, FiberCapability capability)
    {
        ...
    }
}

public class CatvPortCapabilityService : ICapabilityService<CatvPortDetails>
{
    public bool OfCapability(CatvPortDetails port_details, FiberCapability capability)
    {
        ...
    }
}

public class PonPortCapabilityService : ICapabilityService<PonPortDetails>
{
    public bool OfCapability(PonPortDetails port_details, FiberCapability capability)
    {
        //If such kind of port does not have any capability, simply return false
        ...
    }
}

然后,我们可以创建另一个实现,使用这些单独的服务来判断任何端口的功能:

public class PortCapabilityService : ICapabilityService<PortDetails>
{
    private readonly ICapabilityService<PtpPortDetails> m_PtpPortCapabilityService;
    private readonly ICapabilityService<CatvPortDetails> m_CatvPortCapabilityService;
    private readonly ICapabilityService<PonPortDetails> m_PonPortCapabilityService;

    public PortCapabilityService(ICapabilityService<PtpPortDetails> ptp_port_capability_service, ICapabilityService<CatvPortDetails> catv_port_capability_service, ICapabilityService<PonPortDetails> pon_port_capability_service)
    {
        m_PtpPortCapabilityService = ptp_port_capability_service;
        m_CatvPortCapabilityService = catv_port_capability_service;
        m_PonPortCapabilityService = pon_port_capability_service;
    }

    public bool OfCapability(PortDetails port_details, FiberCapability capability)
    {
        PtpPortDetails ptp_port_details = port_details as PtpPortDetails;

        if (ptp_port_details != null)
            return m_PtpPortCapabilityService.OfCapability(ptp_port_details, capability);

        CatvPortDetails catv_port_details = port_details as CatvPortDetails;

        if (catv_port_details != null)
            return m_CatvPortCapabilityService.OfCapability(catv_port_details, capability);

        PonPortDetails pon_port_details = port_details as PonPortDetails;

        if (pon_port_details != null)
            return m_PonPortCapabilityService.OfCapability(pon_port_details, capability);

        throw new Exception("Unknown port type");
    }
}

如您所见,除了算法类之外,没有任何类知道端口 ID。确定功能的类不知道端口 ID,因为它们可以在没有它们的情况下完成工作。

另外需要注意的是,您不需要在每次运行算法时都实例化新的能力服务。这与您的问题中描述的 IInternetPort 实现形成对比,您每次想要执行算法时都会创建一个新实例。我猜你这样做是因为每个实例都绑定到不同的 ID。

这些类使用依赖注入。您应该编写它们以便能够使用它们。您应该在Composition Root 中执行此操作。

以下是您如何使用Pure DI 进行此类组合:

IPortSelectionAlgorithm algorithm =
    new PortSelectionAlgorithm(
        new PortCapabilityService(
            new PtpPortCapabilityService(),
            new CatvPortCapabilityService(),
            new PonPortCapabilityService()));

【讨论】:

  • 哇!多么令人印象深刻的答案。我按照你的想法,但是OfCapability的方法是不是违背了开闭原则?如果这三种端口类型要具有更多类似OfCapability 的方法,我将不得不在添加新端口时修改每个服务。此外,当我尝试应用 DDD 时,我非常希望我的域类包含业务逻辑 - 这里它被存放在一个服务中,即PortCapabilityService。这条评论绝不是负面的——我只是想看看你的建议是否可以朝着这个方向发展:)
  • 如果您需要添加其他方法,例如 OfCapability(例如 OfAttribute),您可以创建另一个服务合约并创建其他服务来处理此类方法。我同意你的观点,PortCapabilityService 违反了开闭原则。当您添加更多端口类型时,您需要对其进行修改。我不确定这是否可以以干净的方式解决。关于DDD,我知之甚少。您的问题中的端口类(例如 PtpPort)是否被视为 DDD 实体?我猜不是,因为您每次运行算法时都创建它们只是为了能够计算能力。
  • PtpPort 等端口类不是 DDD 实体,而是值对象。但是,对于放置在对象本身上的这些对象,我仍然希望有尽可能多的逻辑。再次感谢您的努力!!令我惊讶的是,人们愿意花费自己的时间来尝试帮助他人:)
【解决方案2】:

我认为有代码异味的迹象。在 PtpPortFactory 的 CreatePort 中,漂亮的事情是它接受 PtpPortDetails(继承自 PortDetails)而不是强制转换。

不,这很好,因为依赖项应该是抽象而不是实现。所以在这种情况下,传递 PortDetails 就可以了。

我看到的气味在这里:

public interface IPort
{
    int Id { get; }
}

public interface IInternetPort : IPort
{
    bool OfCapability(FiberCapability capability);
}

接口基本上用于定义行为。你在接口中使用属性对我来说看起来很可疑。

  • 继承描述了 is-a 关系。
  • 实现接口 描述了一种可以做的关系。

你在这里处理的是 AbstractFactory 模式。假设您可以拥有抽象工厂BasePortFactory,它可以执行IPortFactory 声明的操作。 所以你应该从工厂方法返回BasePortFactory。但在设计解决方案时,这又是一种选择。

类似地,CreatePort 方法应该根据您是否要使用 is-a 而不是 can-do 东西来公开返回类型是基类还是接口。

更新

此示例不适合您的场景,但这是为了描述我分享的想法:

public interface IInternetPort
{
    bool OfCapability(FiberCapability capability);
}

/// <summary>
/// This class can be a replacement of (IPort) interface. Each port is enabled for query via IInternetPort.
/// As a default behavior every port is not Internet enabled so OfCapability would return false.
/// Note: If you want you can still keep the IPort interface as Marker interface. 
/// /// </summary>
public abstract class Port : IInternetPort
{
    public int Id { get; private set; }

    public Port(int Id)
    {
        this.Id = Id;
    }

    public virtual bool OfCapability(FiberCapability capability)
    {
        // Default port is not internet capable
        return false; 
    }
}

/// <summary>
/// This class is-a <see cref="Port"/> and can provide capability checker.
/// Overiding the behavior of base for "OfCapability" would enable this port for internet.
/// </summary>
public class PtpPort : Port
{
    private readonly FiberCapability _capability;

    public PtpPort(int id, FiberCapability capability) : base(id)
    {
        _capability = capability;
    }

    public override bool OfCapability(FiberCapability capability)
    {
        return capability.Equals(_capability);
    }
}

/// <summary>
/// this test class doesn't need to implement or override OfCapability method
/// still it will be act like any other port.
/// | TestPort port = new TestPort(22);
/// | port.OfCapability(capability);
/// </summary>
public class TestPort : Port
{
    public TestPort(int id): base(id) { }
}

这是需要更改方法签名以返回 Port 而不是 IPort 的工厂。

public interface IPortFactory
{
    Port CreatePort(int id, PortDetails details);
}

public class PtpPortFactory : IPortFactory
{
    public Port CreatePort(int id, PortDetails details)
    {
        var ptpPortDetails = details as PtpPortDetails;
        if (ptpPortDetails == null)
        {
            throw new ArgumentException("Port details does not match ptp ports", "details");
        }

        return new PtpPort(id, FiberCapability.FromValue(ptpPortDetails.Capability));
    }
}

现在这条线不需要任何外部演员。

var portsToSelectFrom = ports.Select(port => portFactory.CreatePort(port.Id, port.PortDetails)).ToList();

附: - 此类问题应在code reviewprogrammer 提出。

【讨论】:

  • “你在接口中使用属性对我来说看起来很可疑”。你为什么这样说 ?你能解释一下吗?
  • 可能并不意味着它是最佳实践。如果他们在实现中做了一些不重要的事情,这可能是一个不好的做法。如果您了解 Can-do 行为,您将避免这种做法。这都是基于意见的。我没有说这是错误,这是我自己对怀疑的看法。我看不出得到 -1 的理由。
  • 我的意见,你的信息不是答案,这就是为什么你可能得到-1(不是我)
  • 因为这不是一个问题,它只是要求最佳实践和代码审查的东西。 :)
  • @vendettamit,我将接口视为合同,而不仅仅是可以做的行为,您的这种说法似乎误导了我。我建议通过这篇文章 - stackoverflow.com/questions/972392/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-12-25
  • 1970-01-01
  • 2016-12-26
  • 2020-07-24
  • 2021-08-24
  • 2023-03-31
  • 2011-01-23
相关资源
最近更新 更多