【问题标题】:Abstract Factory Pattern - dependency between concrete products抽象工厂模式——具体产品之间的依赖关系
【发布时间】:2013-08-01 20:46:15
【问题描述】:

请阅读下面的代码,问题在最后。

using System;
using System.Collections.Generic;

namespace Graphics
{
    public interface IGraphicsFactory
    {
        ICanvas CreateCanvas();
        Square CreateSquare();
        ComposedShape CreateComposedShape();
    }

    public class SimpleGraphicsFactory : IGraphicsFactory
    {
        public Square CreateSquare()
        {
            return new SimpleImpl.SimpleSquare();
        }

        public ComposedShape CreateComposedShape()
        {
            return new SimpleImpl.SimpleComposedShape();
        }

        public ICanvas CreateCanvas()
        {
            return new SimpleImpl.SimpleCanvas();
        }
    }

    public interface ICanvas
    {
        void AddShape(ShapeBase shape);
        void Render();
    }

    public abstract class ShapeBase
    {
        public abstract void Paint(ICanvas canvas);
    }

    public abstract class Square : ShapeBase
    {
        public int size;
    }

    public abstract class ComposedShape : ShapeBase
    {
        public int size;
        public ShapeBase InternalShape1 { get; set; }
        public ShapeBase InternalShape2 { get; set; }
    }
}


namespace Graphics.SimpleImpl
{
    internal class SimpleSquare : Graphics.Square
    {
        public void Init()
        {
            // do something really important
        }

        public override void Paint(ICanvas canvas)
        {
            Init();

            //?? how to avoid the type cast? (and I want to keep the DrawLine out of the ICanvas interface)
            SimpleCanvas scanvas = (canvas as SimpleCanvas);
            scanvas.DrawLine();
            scanvas.DrawLine();
            scanvas.DrawLine();
            scanvas.DrawLine();
        }
    }

    internal class SimpleComposedShape : Graphics.ComposedShape
    {
        public void Init()
        {
            //?? how can I call `InternalShape1.Init', preferably without type casts? (and I want to keep `Init` out of the `ShapeBase` class)
            // this.InternalShape1.Init();
            // this.InternalShape2.Init();
        }

        public override void Paint(ICanvas canvas)
        {
            Init();
            // TODO: draw the thing
        }
    }

    internal class SimpleCanvas : Graphics.ICanvas
    {
        List<ShapeBase> shapes = new List<ShapeBase>();

        public void AddShape(ShapeBase shape)
        {
            shapes.Add(shape);
        }

        public void Render()
        {
            foreach (ShapeBase s in shapes)
            {
                s.Paint(this);
            }
        }


        public void DrawLine()
        {
        }
    }
}


namespace Test
{
    using Graphics;
    class TestSimpleGraphics
    {
        static void Test1()
        {
            IGraphicsFactory fact = new SimpleGraphicsFactory();
            ICanvas canvas = fact.CreateCanvas();

            Square sq1 = fact.CreateSquare();
            Square sq2 = fact.CreateSquare();
            ComposedShape cs = fact.CreateComposedShape();
            cs.InternalShape1 = sq1;
            cs.InternalShape2 = sq2;

            canvas.AddShape(cs);
            canvas.Paint();
        }
    }
}
  1. 我的抽象工厂模式实现是否正确?
  2. 内部SimpleSquare.Paint:可以避免类型转换吗? (并且我想将DrawLine 排除在ICanvas 接口之外)
  3. SimpleComposedShape.Init 内部:我怎样才能调用InternalShape.Init,最好没有类型转换? (并且我想将Init 排除在ShapeBase 类之外)

【问题讨论】:

  • 这是太多的代码来解决#1,但对于#2:如果不将其放入该接口(或不同的接口)或可能在某处使用泛型,就无法避免强制转换。此外,您需要在演员表之后检查 null 。对于#3,我只需将它添加到基类或使用通用 ComposedShape where T : ShapeBase.
  • 如果你有 ICanvas 实现的工厂,我将其命名为 CanvasFactory。另外,您的ICanvas 应该代表或抽象什么?我不明白它的目的。什么时候应该调用AddShape?为什么画布包含形状列表? ShapeBase.Paint 内部应该发生什么? ICanvas 实现是否负责绘制图形基元?在那种情况下,为什么不应该为此提供公共方法呢?
  • @Groo:我的图形模型类似于“保留模式”图形库,请参阅en.wikipedia.org/wiki/Retained_mode。所以,把画布想象成类似于浏览器的 DOM 的东西。它不提供绘制线条或矩形的原语,只是将对象添加到其中,并且对象在内部呈现。 ShapeBase.Paint 是抽象的,所以它不能有实体。而这整个事情只是我正在研究的其他东西的等效模型,它具有完全不同的语义,可能这就是示例代码不是最好的原因。
  • 是的,我知道您想要存储 DOM,但关键是具体的 ICanvas 应该存储 知道其实际实现的原始形状(对于例如,SimpleSquare 应该有 Paint(SimpleCanvas) 方法,而不是 Paint(ICanvas) 方法)。
  • 为了 KISS 的利益,您真的有多个实现系列吗?你真的会有一组 SimpleXXX 类,还有一组 ComplexXxx 类和一个 MediocreXxx 类吗?如果你不需要它,不要过于复杂。

标签: c# oop design-patterns language-agnostic factory-pattern


【解决方案1】:

1 - 我认为您的 SimpleGraphicsFactory 确实是抽象工厂的一个很好的例子。

2 - SimpleSquare 转换为 SimpleCanvas 是完全合适的,因为它们都是同一个“家族”的一部分,由同一个具体工厂创建。回忆一下抽象工厂的定义(重点是我的):

提供一个接口来创建相关或依赖的家庭 没有指定具体类的对象。

这种设计模式的含义是它创建的类可以假设/要求它们与来自同一家族的类一起使用。

使用 .NET 世界中的另一个示例,System.Data 命名空间的行为方式类似。 System.Data.Sql 命名空间中的对象不能与 System.Data.Oracle 中的对象一起使用。您不能在需要 OracleParameter 的地方传递 SqlParameter。您选择家庭并留在家庭中。

3 - 我不知道你想做什么,你需要评论细节,我会修改我的答案来解决。我希望ComposedShape 有一个方法Add(Shape s),它可以让调用者将多个形状添加到复合(容器)中。但也许我理解错了。

【讨论】:

  • 感谢您的回答。由于具体类中需要强制转换,我不确定我的实现是否正确。这些演员表感觉很自然,但最好避免它们,我想知道这是否也是可能的。我修改了代码示例,使其更加清晰;在我的示例中,ComposedShape 比您预期的要简单,它只包含两个不同的形状,它们是在运行时指定的。就我而言,要求是在调用 Paint 之前必须完全初始化两个内部形状。
  • 我不同意这些演员表是“完全合适的”。我很少看到在 OOP 实现中没有表示问题的强制转换,在这种情况下,解决方案很明显:您要么希望在 SimpleSquare.Paint 中有一个具体的 SimpleCanvas 参数,要么您需要制作此方法公开。
  • 当 OP 在一篇文章中提出多个问题时,这很困难。我不想过多地剖析这些方法。问题的主题(和标题)是关于抽象工厂模式中创建的对象之间的依赖关系。对此我说是的你可以。这就是它的设计目的。说到细节,我不明白为什么Paint 会是一种特定于家庭的方法,所以我怀疑设计中还有其他问题。
  • 从对原始问题的编辑和 cmets 来看,代码示例只是一个用于讨论的示例,可能不值得过多剖析。
【解决方案2】:

如果我的意图是正确的,那么您就是在尝试模仿 System.Drawing.Graphics 类(或 HTML &lt;canvas&gt;,例如)的功能。

如果正确,我会给出以下建议:

  1. IGraphicsFactory 重命名为 ICanvasFactory 并让它只创建具体的 ICanvas 实现。删除 CreateSquare 和其他方法,因为您不必通过工厂创建形状(只有通过具体的 ICanvas 实现来传递形状很重要)。

  2. ICanvas 接口表示可以绘制原始形状(线条、圆形、填充区域等)的画布。这意味着您应该公开允许调用者创建这些原语的公共方法。 Graphics 还提供了各种转换功能,但现在这可能有点矫枉过正:

    interface ICanvas
    {
         void Clear();
         void DrawLine(Point from, Point to, Pen pen);
         void DrawCircle(Point center, Double radius, Pen pen);
         void Paint();
         /* and stuff like that */
    }
    
  3. Canvas 不应包含Shapes 的列表,而应包含原语列表。例如,当您调用 ICanvas.DrawLine(...) 方法时,它应该创建一个 Line 原语的实例并将其存储在内部列表中。

    这些原始类的功能将取决于您的ICanvas 的实际实现(它是绘制到位图还是打印到打印机等)。您还将拥有一些隐藏的、依赖于实现的数据(我将使用 byte[],假装它存储某种 8 位位图):

    class BitmapCanvas : ICanvas
    {
        private readonly byte[] _bitmapData;
        private readonly List<IBitmapShape> _primitives;
    
        public BitmapCanvas(int width, int height)
        {
            _bitmapData = new byte[width * height];
            _primitives = new List<IPrimitiveShape>();
        }
    
        public void DrawLine(...) 
        {
            // different implementations will handle this part differently.
            _primitives.Add(new BitmapLine(_bitmapData, from, to, pen));
        }
    
        public void Paint()
        {
            Clear(_bitmapData);
            foreach (var shape in _primitives)
                shape.Draw();
        }
    }
    
  4. 具体的原始类将处理这个内部逻辑:

    class BitmapLine : IBitmapShape
    {
        public void Draw()
        {
            // write to the underlying byte array
        }
    }
    
  5. 由于ICanvas 实现不会绘制实际形状,因此您不需要ShapeBase 类。但是,您将需要一个用于绘制图形基元的模拟类(上面称为 IBitmapShape)。

【讨论】:

  • 对不起,但这根本不能解决我的问题。该示例只是我正在研究的其他东西的等效模型,它具有完全不同的语义,所以可能这就是你觉得我想要其他东西。
  • 我以为我含蓄地回答了它们,但是如果您想要明确的答案,那就去吧: 1. 正确的工厂实现?是和不是。需要根据 .NET 命名约定重命名,并且需要丢失不属于那里的方法。 2. Cast inside SimpleSquare.Paint:是的,您需要公开该方法,因为画布抽象应该为其他类抽象这些方法(并允许其他类调用它们 - 检查System.Drawing.Graphics 和 HTML canvas 是如何工作的. 3. Init 方法应该被移除并在Shape 构造函数中完成。
  • 问题是我根本不想实现图形系统,而是别的。所以再举一个例子,假设我要实现一个数据库访问库。那么ICanvas 类似于ISqlConnectionICanvas.Render 类似于ISqlConnection.ExecuteShapeBase 类似于SqlCommandSquareShape 类似于SelectCommandSimpleCanvas 可以是MySqlConnection。所以,即使我的示例模型在现实世界中没有意义,我也不能像你提议的那样做这么大的改变。
  • 首先,没有ISqlConnection这样的东西。有一个IDbConnection 和一个具体的SqlConnection。而这正是我所说的:SqlCommandtakes a concrete SqlConnection in its constructor,而不仅仅是任意的IDbConnection。这就是为什么您的SimpleSquare 需要采用具体的SimpleCanvas(注意Simple - Sql 前缀类比)并丢弃ICanvas 接口,从而无需强制转换。
  • ShapeBase.Paint(ICanvas) 方法是多余的,整个抽象类也是如此。对于所有简单形状,我会将ShapeBase 替换为interface ISimpleShape { void Paint(ISimpleCanvas); },并完全摆脱ShapeBase 类(这根本不需要强加于所有ICanvas 实现)。跨度>
猜你喜欢
  • 1970-01-01
  • 2015-09-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多