【问题标题】:Refactoring Code to avoid Type Casting重构代码以避免类型转换
【发布时间】:2014-01-31 14:47:02
【问题描述】:

我在 .Net 4.0 中有以下 C# 代码。它需要将 IBusiness 类型转换为 IRetailBusiness。

//Type checking
if (bus is IRetailBusiness)
{
       //Type casting
       investmentReturns.Add(new RetailInvestmentReturn((IRetailBusiness)bus));
}

if (bus is IIntellectualRights)
{
       investmentReturns.Add(new IntellectualRightsInvestmentReturn((IIntellectualRights)bus));
}

业务场景:

我正在为投资控股公司设计一个软件系统。公司拥有零售业务和知识产权业务。 BookShop 和 AudioCDShop 是零售业务的例子。 EngineDesignPatent 和BenzolMedicinePatent 是知识产权业务的例子。这两种业务类型完全不相关。

投资公司有一个概念叫InvestmentReturn(但每个个体企业对这个概念完全一无所知)。 InvestmentReturn 是从每项业务中获得的利润,使用ProfitElement 计算。对于每种“业务类型”(零售、知识产权),使用的 ProfitElement 是不同的。

问题

如何重构此类设计以避免type castingtype checking

抽象投资

public abstract class InvestmentReturn
{
    public double ProfitElement { get; set; }
    public IBusiness Business{ get;  set; }

    public abstract double GetInvestmentProfit();

    public double CalculateBaseProfit()
    {
       double profit = 0;

       if (ProfitElement < 5)
       {
           profit = ProfitElement * 5 / 100;
       }
       else if (ProfitElement < 20)
       {
           profit = ProfitElement * 7 / 100;
       }
       else
       {
           profit = ProfitElement * 10 / 100;
       }

       return profit;
    }
}

扩展

public class RetailInvestmentReturn : InvestmentReturn
{
    public RetailInvestmentReturn(IRetailBusiness retail)
    {
        Business = retail;
    }

    public override  double GetInvestmentProfit()
    {
        //GrossRevenue is the ProfitElement for RetailBusiness
        ProfitElement = ((IRetailBusiness)Business).GrossRevenue;
        return base.CalculateBaseProfit();
    }  
}

public class IntellectualRightsInvestmentReturn : InvestmentReturn
{

    public IntellectualRightsInvestmentReturn(IIntellectualRights intellectual)
    {
        Business = intellectual;
    }

    public override double GetInvestmentProfit()
    {
        //Royalty is the ProfitElement for IntellectualRights Business
        ProfitElement = ((IIntellectualRights)Business).Royalty;
        return base.CalculateBaseProfit();
    }
}

客户

class Program
{

    static void Main(string[] args)
    {

        #region MyBusines

        List<IBusiness> allMyProfitableBusiness = new List<IBusiness>();

        BookShop bookShop1 = new BookShop(75);
        AudioCDShop cd1Shop = new AudioCDShop(80);
        EngineDesignPatent enginePatent = new EngineDesignPatent(1200);
        BenzolMedicinePatent medicinePatent = new BenzolMedicinePatent(1450);

        allMyProfitableBusiness.Add(bookShop1);
        allMyProfitableBusiness.Add(cd1Shop);
        allMyProfitableBusiness.Add(enginePatent);
        allMyProfitableBusiness.Add(medicinePatent);

        #endregion

        List<InvestmentReturn> investmentReturns = new List<InvestmentReturn>();

        foreach (IBusiness bus in allMyProfitableBusiness)
        {
            //Type checking
            if (bus is IRetailBusiness)
            {
                //Type casting
                investmentReturns.Add(new RetailInvestmentReturn((IRetailBusiness)bus));
            }

            if (bus is IIntellectualRights)
            {
                investmentReturns.Add(new IntellectualRightsInvestmentReturn((IIntellectualRights)bus));
            }
        }

        double totalProfit = 0;
        foreach (var profitelement in investmentReturns)
        {
            totalProfit = totalProfit + profitelement.GetInvestmentProfit();
            Console.WriteLine("Profit: {0:c}", profitelement.GetInvestmentProfit());
        }

        Console.ReadKey();
    }
}

业务领域实体

public interface IBusiness
{

}

public abstract class EntityBaseClass
{

}

public interface IRetailBusiness : IBusiness
{
    double GrossRevenue { get; set; }
}

public interface IIntellectualRights : IBusiness
{
    double Royalty { get; set; }
}



#region Intellectuals
public class EngineDesignPatent : EntityBaseClass, IIntellectualRights
{
    public double Royalty { get; set; }
    public EngineDesignPatent(double royalty)
    {
        Royalty = royalty;
    }
}

public class BenzolMedicinePatent : EntityBaseClass, IIntellectualRights
{
    public double Royalty { get; set; }
    public BenzolMedicinePatent(double royalty)
    {
        Royalty = royalty;
    }
}
#endregion

#region Retails
public class BookShop : EntityBaseClass, IRetailBusiness
{
    public double GrossRevenue { get; set; }
    public BookShop(double grossRevenue)
    {
        GrossRevenue = grossRevenue;
    }
}

public class AudioCDShop : EntityBaseClass, IRetailBusiness
{
    public double GrossRevenue { get; set; }
    public AudioCDShop(double grossRevenue)
    {
        GrossRevenue = grossRevenue;
    }
}
#endregion

参考文献

  1. Refactor my code : Avoiding casting in derived class
  2. Cast to generic type in C#
  3. How a Visitor implementation can handle unknown nodes
  4. Open Closed Principle and Visitor pattern implementation in C#

【问题讨论】:

标签: c# generics design-patterns domain-driven-design visitor-pattern


【解决方案1】:

此解决方案使用的概念是业务接口知道它们必须创建返回,并且它们的具体实现知道要创建的具体返回是什么种类

第一步InvestmentReturn拆分成两个接口;原来的减去Business 属性和一个新的通用子类:

public abstract class InvestmentReturn
{
    public double ProfitElement { get; set; }
    public abstract double GetInvestmentProfit();

    public double CalculateBaseProfit()
    {
        // ...
    }
}

public abstract class InvestmentReturn<T>: InvestmentReturn where T : IBusiness
{
    public T Business { get; set; }        
}

第 2 步继承通用的,这样您就可以使用 Business 而无需强制转换:

public class RetailInvestmentReturn : InvestmentReturn<IRetailBusiness>
{
    // this won't compile; see **Variation** below for resolution to this problem...
    public RetailInvestmentReturn(IRetailBusiness retail)
    {
        Business = retail;
    }

    public override double GetInvestmentProfit()
    {
        ProfitElement = Business.GrossRevenue;
        return CalculateBaseProfit();
    }
}

第 3 步IBusiness 添加一个返回InvestmentReturn 的方法:

public interface IBusiness
{
    InvestmentReturn GetReturn();
}

第 4 步 引入EntityBaseClass 的通用子类以提供上述方法的默认实现。如果您不这样做,您将不得不为所有企业实施它。如果您这样做 这样做,则意味着您不想重复 GetReturn() 实现的所有类必须从下面的类继承,这反过来意味着它们必须从 EntityBaseClass 继承。

public abstract class BusinessBaseClass<T> : EntityBaseClass, IBusiness where T : InvestmentReturn, new()
{
    public virtual InvestmentReturn GetReturn()
    {
        return new T();
    }
}

第 5 步如有必要,为您的每个子类实现该方法。以下是BookShop 的示例:

public class BookShop : BusinessBaseClass<RetailInvestment>, IRetailBusiness
{
    public double GrossRevenue { get; set; }
    public BookShop(double grossRevenue)
    {
        GrossRevenue = grossRevenue;
    }

    // commented because not inheriting from EntityBaseClass directly
    // public InvestmentReturn GetReturn()
    // {
    //     return new RetailInvestmentReturn(this);
    // }
}

第 6 步 修改您的 Main 以添加 InvestmentReturn 的实例。您不必进行类型转换或类型检查,因为前面已经以类型安全的方式完成了:

    static void Main(string[] args)
    {
        var allMyProfitableBusiness = new List<IBusiness>();
        // ...
        var investmentReturns = allMyProfitableBusiness.Select(bus => bus.GetReturn()).ToList();
        // ...
    }

如果您不希望您的具体企业知道任何关于创建InvestmentReturn 的信息(只知道他们必须在被问到时创建一个),那么您可能希望将此模式修改为合并一个创建返回给定输入的工厂(例如IBusiness 实现和InvestmentReturn 子类型之间的映射)。

变体

以上所有内容都可以正常工作,并且将编译如果您删除了设置Business 属性的投资回报构造函数。这样做意味着在别处设置Business。这可能是不可取的。

另一种方法是在GetReturn 中设置Business 属性。我找到了一种方法来做到这一点,但它确实开始让课程看起来很乱。值不值得你来评价一下。

RetailInvestmentReturn中移除非默认构造函数:

public class RetailInvestmentReturn : InvestmentReturn<IRetailBusiness>
{
   public override double GetInvestmentProfit()
   {
       ProfitElement = Business.GrossRevenue;
       return CalculateBaseProfit();
   }
}

更改BusinessBaseClass。这就是双重施法变得混乱的地方,但至少它仅限于一个地方。

public abstract class BusinessBaseClass<T, U> : EntityBaseClass, IBusiness
    where T : InvestmentReturn<U>, new()
    where U : IBusiness
{
    public double GrossRevenue { get; set; }

    public virtual InvestmentReturn GetReturn()
    {
        return new T { Business = (U)(object)this };
    }
}

最终改变您的业务。这是BookShop 的示例:

public class BookShop : BusinessBaseClass<RetailInvestmentReturn, IRetailBusiness>, IRetailBusiness
{
    // ...
}

【讨论】:

  • 非常感谢您的想法 - Inherit from the generic subclass。该解决方案巧妙地克服了类型转换问题。这个pattern叫什么名字?
  • 我更新了我的答案。请看第 4 步,我在其中引入了一个通用业务基类来消除冗余。
  • @Lijo,不确定该模式是否有名称。当我想提供默认行为和引入广泛行为行为的简洁方式时,我之前使用过它,但没有所有的强制转换。
  • 我做了具体的业务来继承BusinessBaseClass如下。 public class BookShop : BusinessBaseClass&lt;RetailInvestmentReturn&gt;, IRetailBusiness。但我收到编译错误:RetailInvestmentReturn' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'BusinessBaseClass&lt;T&gt;
  • 无需在混音中注入工厂,您就可以应用我所做的编辑。但不确定是否值得在每项业务中避免新事物。如果你有很多,那可能是值得的。
【解决方案2】:

使 IBusiness 的具体实现可通过访问者模式访问。然后在投资回报域中创建一个可以访问每个业务的访问者,如下所示:

public interface IVisitor<T>
{
    T Visit(IIntellectualRights bus);
    T Visit(IRetailBusiness bus);
}

public interface IBusiness
{
    T Accept<T>(IVisitor<T> visitor);
}

public class AudioCDShop : EntityBaseClass, IRetailBusiness
{
     public void Accept(IVisitor visitor)
     {
          return visitor.Visit(this);
     }

//do the same for each IBusiness implementor.

那么在你的投资回报领域:

 public class InvestmentVisitor : IVisitor<InvestmentReturn>
 {
     public InvestmentReturn GetInvestment(IBusiness bus)
     {
          return bus.Accept(this);
     }

     public InvestmentReturn Visit(IIntellectualRights bus)
     {
          return new IntellectualRightsInvestmentReturn(bus)
     }

     public InvestmentReturn  Visit(IRetailBusiness bus)
     {
          return new RetailInvestmentReturn(bus);
     }
 }

用法

 var investmentReturn = new InvestmentVisitor().GetInvestment(bus);

完全未经测试和验证.. 但这个概念有效。 如果您只有两种不同类型的节点需要访问,这也可能有点过头了。

【讨论】:

    【解决方案3】:

    基本上,如果我理解正确的话,你想避免类型转换。

    您可以简单地通过更改构造函数签名来实现:

    public RetailInvestmentReturn(IRetailBusiness retail) {...}
    public IntellectualRightsInvestmentReturn(IIntellectualRights intellectual) {...}
    

    到:

    public RetailInvestmentReturn(IBusiness retail) {...}
    public IntellectualRightsInvestmentReturn(IBusiness intellectual) {...}
    

    如果由于设计限制而无法选择,那么您可以尝试使用策略模式。那仍然需要类型转换,但是您会摆脱可怕的“如果”和“其他”,我相信这是您真正的问题,对吗?为此,您需要:

    1. 使用“类型”和“运行方法”创建字典
    2. 使用它!

    在代码中,它看起来像这样:

    1. (创建,...)

          Dictionary<Type,Func<IBusiness,InvestmentReturn>> dict = new Dictionary<Type, Func<IBusiness, InvestmentReturn>>
          {
              {typeof (BookShop), business => new RetailInvestmentReturn((IRetailBusiness) business)},
              {typeof (AudioCDShop), business => new IntellectualRightsInvestmentReturn((IIntellectualRights) business)}
          };
      
    2. (使用它!)

    // 试试这个:

    foreach (IBusiness bus in allMyProfitableBusiness)
    {
        investmentReturns.Add(dict[bus.GetType()](bus));
    }
    

    【讨论】:

      【解决方案4】:

      让IBusiness接口处理InvestmentReturn类型,返回利润金额。

      对 foreach 循环的更改:

      foreach (IBusiness bus in allMyProfitableBusiness)
      {
          // No type checking or casting.  It is scalable to new business types.
          investmentReturns.Add(bus.GetReturnInvestment());
      }
      

      界面更新:

      public interface IBusiness
      {
          IReturnInvestment GetReturnInvestment();
      
          double GetProfit();
      }
      
      public abstract class EntityBaseClass
      {
      
      }
      
      public interface IRetailBusiness : IBusiness
      {
          ...
      }
      
      public interface IIntellectualRights : IBusiness
      {
          ...
      }
      

      具有另一个基类的业务类:

      #region Intellectuals
      public abstract IntellectualRightsBaseClass : EntityBaseClass, IIntellectualRights
      {
          ...
          public IReturnInvestment GetReturnInvestment()
          {
              return new IntellectualRightsInvestmentReturn(this);
          }
      
          public double GetProfit()
          {
              return this.Royalty;
          }
      }
      
      public class EngineDesignPatent : IntellectualRightsBaseClass
      {
          ...
      
      }
      
      public class BenzolMedicinePatent : IntellectualRightsBaseClass
      {
          ...
      }
      #endregion
      
      #region Retails
      public abstract RetailBusinessBaseClass : IRetailBusiness
      {
          ...
          public IReturnInvestment GetReturnInvestment()
          {
              return new RetailInvestmentReturn(this);
          }
      
          public double GetProfit()
          {
              return this.GrossRevenue;
          }
      }
      
      public class BookShop : RetailBusinessBaseClass 
      {
          ...
      }
      
      public class AudioCDShop : RetailBusinessBaseClass 
      {
          ...
      }
      #endregion
      

      扩展

      public class RetailInvestmentReturn : InvestmentReturn
      {
          public RetailInvestmentReturn(IRetailBusiness retail)
              : base(retail)
          {
          }
      
          /* Don't need anymore
          public override  double GetInvestmentProfit()
          {
              //GrossRevenue is the ProfitElement for RetailBusiness
              ProfitElement = Business.GetProfit();
              return base.CalculateBaseProfit();
          } */
      }
      
      public class IntellectualRightsInvestmentReturn : InvestmentReturn
      {
      
          public IntellectualRightsInvestmentReturn(IIntellectualRights intellectual)
              : base(intellectual)
          {
          }
      
          /* Don't need anymore
          public override double GetInvestmentProfit()
          {
              //Royalty is the ProfitElement for IntellectualRights Business
              ProfitElement = Business.GetProfit();
              return base.CalculateBaseProfit();
          } */
      }
      

      抽象投资回报

      public abstract class InvestmentReturn
      {
          protected InvestmentReturn(IBusiness business)
          {
              this.Business = business;
          }
      
          public double ProfitElement { get; set; }
          public IBusiness Business{ get;  set; }
      
          public override double GetInvestmentProfit()
          {
              ProfitElement = this.Business.GetProfit();
              return this.CalculateBaseProfit();
          }
      
          public double CalculateBaseProfit()
          {
             double profit = 0;
      
             if (ProfitElement < 5)
             {
                 profit = ProfitElement * 5 / 100;
             }
             else if (ProfitElement < 20)
             {
                 profit = ProfitElement * 7 / 100;
             }
             else
             {
                 profit = ProfitElement * 10 / 100;
             }
      
             return profit;
          }
      }
      

      【讨论】:

      • IBusiness 在域中没有关于 GetReturnInvestment() 的信息。因此这是行不通的。
      • 是的,这是我建议您添加的内容,除非您无法更改 IBusiness?
      • 即使他可以改变IBusiness,他也不应该,你不应该强迫一个域知道另一个域..域B依赖于域A,域A不知道域B。 .
      【解决方案5】:

      让 InvestmentReturn 成为一个具体的类并将 ProfitElement 逻辑转移到 IBusiness 怎么样?

      public class InvestmentReturn
      {
          public double ProfitElement { get; set; }
      
          public double InvestmentProfit{ get; set; }
      
          public RetailInvestmentReturn(IRetailBusiness bus)
          {
              ProfitElement = bus.GetProfitElement();
          }
      
          public double CalculateBaseProfit()
          {
              ......
          }
      }
      
      public class BenzolMedicinePatent : EntityBaseClass, IIntellectualRights
      {
          public double Royalty { get; set; }
          public BenzolMedicinePatent(double royalty)
          {
              Royalty = royalty;
          }
      
          public override double GetProfitElement() 
          {
              return royalty;
          }    
      }
      
      
      
      public class BookShop : EntityBaseClass, IRetailBusiness
      {
          public double GrossRevenue { get; set; }
          public BookShop(double grossRevenue)
          {
              GrossRevenue = grossRevenue;
          }
          public override double GetProfitElement() 
          {
              return royalty;
          } 
      }
      

      现在客户端变成了:

      class Program
      {
      
          static void Main(string[] args)
          {
      
              #region MyBusines
      
              List<IBusiness> allMyProfitableBusiness = new List<IBusiness>();
      
              BookShop bookShop1 = new BookShop(75);
              AudioCDShop cd1Shop = new AudioCDShop(80);
              EngineDesignPatent enginePatent = new EngineDesignPatent(1200);
              BenzolMedicinePatent medicinePatent = new BenzolMedicinePatent(1450);
      
              allMyProfitableBusiness.Add(bookShop1);
              allMyProfitableBusiness.Add(cd1Shop);
              allMyProfitableBusiness.Add(enginePatent);
              allMyProfitableBusiness.Add(medicinePatent);
      
              #endregion
      
              List<InvestmentReturn> investmentReturns = new List<InvestmentReturn>();
      
              foreach (IBusiness bus in allMyProfitableBusiness)
              {
                  investmentReturns.Add(new InvestmentReturn(bus));
              }
      
              double totalProfit = 0;
              foreach (var profitelement in investmentReturns)
              {
                  totalProfit = totalProfit + profitelement.GetInvestmentProfit();
                  Console.WriteLine("Profit: {0:c}", profitelement.GetInvestmentProfit());
              }
      
              Console.ReadKey();
          }
      }
      

      【讨论】:

      • BenzolMedicinePatent 在域中没有关于 GetProfitElement() 的信息。因此这是行不通的。
      • @Lijo 版税是知识产权业务的利润要素吗?
      • 是的,确实如此。然而,版税是知识产权业务的利润要素这一事实是知识产权业务领域之外的信息。这是投资公司决定的事情
      • @Lijo 对不起,我对投资控股领域不熟悉。你能详细说明一下吗?比如为什么“特许权使用费是知识产权业务领域之外的利润要素”使得这种方法不可用?
      【解决方案6】:

      以上所有答案似乎都比必要的复杂,但您并没有真正说明您需要保留哪些信息,因此很难知道重新设计您的课程能走多远。如果您只想产生上述程序产生的结果并仍然使用对象,您可以这样做:

      class Program2
      {
          static void Main(string[] args)
          {
              List<Business> allMyProfitableBusiness = new List<Business>();
      
              Business bookShop1 = new Business(75);
              Business cd1Shop = new Business(80);
              Business enginePatent = new Business(1200);
              Business medicinePatent = new Business(1450);
      
              allMyProfitableBusiness.Add(bookShop1);
              allMyProfitableBusiness.Add(cd1Shop);
              allMyProfitableBusiness.Add(enginePatent);
              allMyProfitableBusiness.Add(medicinePatent);
      
              foreach (var business in allMyProfitableBusiness)
              {
                  Console.WriteLine("Profit: {0:c}", business.GetInvestmentProfit());
              }
      
              Console.ReadKey();
          }
      }
      
      
      public class Business
      {
          private double ProfitElement;
      
          public Business(double profitElement)
          {
              ProfitElement = profitElement;
          }
      
          public double GetInvestmentProfit()
          {
              double profit = 0;
      
              if (ProfitElement < 5)
              {
                  profit = ProfitElement * 5 / 100;
              }
              else if (ProfitElement < 20)
              {
                  profit = ProfitElement * 7 / 100;
              }
              else
              {
                  profit = ProfitElement * 10 / 100;
              }
      
              return profit;
          }
      }
      

      如果出于某种原因,您确实需要保留不同类型的业务(零售和知识分子),您仍然可以使用基类大大简化它。您不需要泛型或您的InvestmentReturnEntityBaseClass(虽然您计算totalProfit,但它并未在任何地方使用):

      class Program
      {
          static void Main(string[] args)
          {
              List<Business> allMyProfitableBusiness = new List<Business>();
      
              BookShop bookShop1 = new BookShop(75);
              AudioCDShop cd1Shop = new AudioCDShop(80);
              EngineDesignPatent enginePatent = new EngineDesignPatent(1200);
              BenzolMedicinePatent medicinePatent = new BenzolMedicinePatent(1450);
      
              allMyProfitableBusiness.Add(bookShop1);
              allMyProfitableBusiness.Add(cd1Shop);
              allMyProfitableBusiness.Add(enginePatent);
              allMyProfitableBusiness.Add(medicinePatent);
      
              double totalProfit = 0;
              foreach (var business in allMyProfitableBusiness)
              {
                  totalProfit = totalProfit + business.GetInvestmentProfit();
                  Console.WriteLine("Profit: {0:c}", business.GetInvestmentProfit());
              }
      
              Console.ReadKey();
          }
      }
      
      
      public abstract class Business
      {
          public abstract double Profit { get; }
      
          public double GetInvestmentProfit()
          {
              double profit = 0;
      
              if (Profit < 5)
              {
                  profit = Profit * 5 / 100;
              }
              else if (Profit < 20)
              {
                  profit = Profit * 7 / 100;
              }
              else
              {
                  profit = Profit * 10 / 100;
              }
      
              return profit;
          }
      }
      
      
      public abstract class RetailBusiness : Business
      {
          public double GrossRevenue { get; set; }
          public override double Profit {get {return GrossRevenue; } }
      }
      
      public abstract class IntellectualRights : Business
      {
          public double Royalty { get; set; }
          public override double Profit {get {return Royalty; } }
      }
      
      
      #region Intellectuals
      public class EngineDesignPatent : IntellectualRights
      {
          public EngineDesignPatent(double royalty)
          {
              Royalty = royalty;
          }
      }
      
      public class BenzolMedicinePatent : IntellectualRights
      {
          public BenzolMedicinePatent(double royalty)
          {
              Royalty = royalty;
          }
      }
      #endregion
      
      
      #region Retails
      public class BookShop : RetailBusiness
      {
          public BookShop(double grossRevenue)
          {
              GrossRevenue = grossRevenue;
          }
      }
      
      public class AudioCDShop : RetailBusiness
      {
          public AudioCDShop(double grossRevenue)
          {
              GrossRevenue = grossRevenue;
          }
      }
      #endregion
      

      当然,任何人读到这篇文章的机会都非常非常小:-)

      【讨论】:

        猜你喜欢
        • 2012-06-28
        • 2019-09-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多