【问题标题】:How can I make sure I'm implementing the Open/Closed principle?如何确保我正在实施开放/封闭原则?
【发布时间】:2014-06-12 22:32:55
【问题描述】:

假设我有一个名为Shape 的基类,我希望每个形状都能够返回它们自己的区域。我可以将Shape 设为接口或抽象类(可能还有其他方式),但最终结果是每个形状都有一个名为Area 的函数。

假设我有一个类似的集合:

List<Shape> bag = loadShapesFromXML(...)

如果我想将所有区域加在一起,我可以这样做:

int total=0;
foreach (var s in bag)
   total += s.Area();

这遵循开/关原则就好了。我遇到的问题是loadShapesFromXML。假设我的 XML 如下所示:

<bag>
  <circle id="1" radius="5" />
  <square id="2" length="3" />
  <rectangle id="3" length="6" width="7"/>
</bag>

我的loadShapesFromXML 方法必须检查“包”中的每个项目以查看它的形状类型。除了使用反射来查看形状的类型是圆形/正方形/矩形之外,我还能做些什么来避免每次决定添加新形状时都修改这个函数?

【问题讨论】:

  • 如果你用序列化器加载它们,这个问题应该会消失。这不是一个选择吗?
  • 如果您正在寻找通用指南(基于标题),向programmers.stackexchange.com 提出更好的问题。否则请查看en.wikipedia.org/wiki/Data_mapper_pattern
  • @BradletDotNET 问题将如何消失?它仍然需要根据派生的Shape 类型的每个特定实现来计算形状,不是吗?
  • 您有什么理由限制自己不使用反射?使用使用反射的第三方库怎么样?
  • @YuvalItzchakov,他担心使用反射来获取类型。通过使用序列化程序,它将自动序列化为正确的类型(可能使用幕后的反射授予)。不过,他不必为新类型修改函数。

标签: c# solid-principles


【解决方案1】:

您的形状对象无法通过设计进行修改 - 因此您无法添加 ReadFromXXXXWriteToXXXX 方法。要实现序列化,您可以使用其他一些组件来了解存储中的数据结构与运行时对象之间的映射 - 由 data mapper pattern 介绍。

Xml 节点的非常基本的“映射器”将是按节点名称索引的阅读器函数字典:

  var creatorsMap = new Dictionary<string, Func<XElement, Shape>>
     {{"rectangle", node => new Rectangle(node.Element(....) ...)}};
  ...
  shapes.Add(creatorsMap[node.Name](node));

如果您关心的是如何发现所有可能的形状 - 而不是反射是查找所有类型的可能方法,那么在代码或配置中手动添加其他类型也是一种选择。如果您使用的是依赖注入容器,它也可能提供一些构建/帮助此类映射方法的方法。

【讨论】:

    【解决方案2】:

    好的,我想我找到了一个很好的方法。我从上面对我的 XML 进行了一些更改。

    <bag>
      <circle id="1" radius="5">Circle 1</circle> />
      <square id="2" length="3">Square 1</square> />
      <rectangle id="3" length="6" width="7">Rectangle 1</rectangle>/>
    </bag>
    

    这是抽象的 shape 类。注意它有两个构造函数。第二种是传入“包”内的 XElement 对象。

    public abstract class shape
    {
        private readonly string _ID;
        public string id
        {
            get
            {
                return _ID;
            }
        }
    
        public string Name { get; set; }
    
        public shape(string id, string name)
        {
            _ID = id;
            this.Name = name;
        }
    
        public shape(XElement element)
        {
            _ID = element.Attribute("id").Value;
            this.Name = element.Value;
        }
    
        public abstract XElement GetXElement();
    
        public abstract double Area();
    
    }
    

    现在这里是一个圆的实例。请注意,构造函数采用该形状的唯一元素并相应地填充属性。另请注意,如果您希望写回 XML 文件,GetXElement 将正确构造 XML 节点。

    public class circle : shape
    {
        public int Radius { get; set; }
    
        public circle(string id, string name, int radius)
            : base(id, name)
        {
            this.Radius = radius;
        }
    
        public circle(XElement element)
            : base(element)
        {
            this.Radius = int.Parse(element.Attribute("radius").Value);
        }
    
        public override XElement GetXElement()
        {
            return new XElement("circle", new XAttribute("id", this.id), new XAttribute("radius", this.Radius), this.Name);
        }
    
        public override double Area()
        {
            return Math.PI * Radius * Radius;
        }
    }
    

    以下是如何使用反射来使用开/闭原则获取区域。请注意,所有形状都是根据 XML 文件的内容构建的,Console.WriteLine(element) 语句会生成 XML 供您使用。在这种情况下,输出将与输入相同,但如果您更改了相关形状的属性,此 XML 输出将更改为匹配。

        public void TestMethod1()
        {
            var doc = XDocument.Load(xmlFile);
            double area=0;
            foreach (var shapeItem in doc.Descendants("bag").Descendants())
            {
                var type = Type.GetType("StackOverflowShapes." + shapeItem.Name + ",StackOverflowShapes");
                var myShape = (shape)Activator.CreateInstance(type, shapeItem);
                area += myShape.Area();
                var element = myShape.GetXElement();
                Console.WriteLine(element);
            }
    
            Assert.Equal(129.5398, area, 4);
        }
    

    因此,现在可以添加新形状,而无需修改计算总面积的例程。此外,每个形状都负责构建自己的 XML 表示,并且可以从 XML 表示中创建自己的实例。

    【讨论】:

      【解决方案3】:

      逻辑必须转移到某个地方,但如果反射消失并且您需要 loadShapesFromXML 不改变,您可以将 Shape 对象类定义为通用而不是特定的,并使其包含所有维度类型。

      class Shape
        var radius;
        var length;
        var width;
        var diagonal; //new ways of defining a shape would go here
        var id;
        var name;
      

      反思是一种更好的方法,但至少这样你可以让你的包 在不修改 loadShapesFromXML 方法的情况下塑造对象。但是,您必须修改您的 Shape 类以处理新的形状类型,并且能够推断它实际上是哪种特定的形状类型(方形、圆形等)。

      if(id == 1)
        bag.add(new Circle(this));
      else if(id == 2)
        bag.add(new Square(this));
      ...
      

      【讨论】:

      • 呃......这两种方法都非常糟糕。现在他必须在需要新数据的任何时候修改基类。这并不比修改 loader 函数好(实际上它更糟)。第二个没问题,只要 Id 一致,但并不比他开始的好多少。
      • 一个形状类不能有一个特定于圆的半径属性。
      • 我问了我 5 岁的孩子,他说是的,圆圈确实是一种形状。我的通用形状想法需要包括半径作为创建/测量形状(可能是圆形)的有效方式。
      猜你喜欢
      • 2021-10-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-17
      • 2010-09-15
      • 1970-01-01
      • 1970-01-01
      • 2021-02-01
      相关资源
      最近更新 更多