从用户界面解耦类


27

关于编写可能必须了解用户界面的类的最佳实践是什么?知道如何绘制自身的类不会因为依赖于用户界面(控制台,GUI等)而打破了一些最佳做法吗?

在许多编程书籍中,我都遇到了显示继承的“ Shape”示例。基类形状具有一个draw()方法,该方法会覆盖每个形状,例如圆形和正方形。这允许多态性。但是draw()方法不是非常依赖于用户界面是什么吗?如果我们将此类写为Win Forms,则不能将其用于控制​​台应用程序或Web应用程序。它是否正确?

这个问题的原因是,我发现自己总是陷入困境,并且挂在如何归纳类上,因此它们是最有用的。这实际上对我不利,我想知道我是否在“努力”。


你为什么要解耦?因为您听说这样做是对的,还是您还有其他原因?
SoylentGray 2011年

2
我整个“知道如何绘画的班级”只是一个我希望消失的可怕的古老例子。特别是在游戏程序员的堆栈上=)
Patrick Hughes

2
@乍得,我在学校外并不是很经验丰富。我读过书,而且我真的很喜欢学习和阅读有关设计模式和最佳实践的新知识。所以是的,您可以说我听说过去耦很好,但这也很有意义。例如,我想编写可用于桌面WinForms应用程序的代码,然后获取该代码并尽可能多地为网站甚至Silverlight应用程序重用。
皮特

@Pete-这是一个很好的答案。
SoylentGray 2011年

2
@Patrick:公平地说,如果您正在编写一个Shape类,那么您可能是在编写图形堆栈本身,而不是在图形堆栈中编写客户端。
肯·布鲁姆

Answers:


13

关于编写可能必须了解用户界面的类的最佳实践是什么?知道如何绘制自身的类不会因为依赖于用户界面(控制台,GUI等)而打破了一些最佳做法吗?

这取决于类和用例。知道如何绘制自己的视觉元素不一定违反单一责任原则。

在许多编程书籍中,我都遇到了显示继承的“ Shape”示例。基类形状具有一个draw()方法,该方法会覆盖每个形状,例如圆形和正方形。这允许多态性。但是draw()方法不是非常依赖于用户界面是什么吗?

同样,不一定。如果可以创建一个接口(drawPoint,drawLine,Set Color等),则几乎可以传递任何上下文以将事物绘制到形状上,例如在形状的构造函数中。这将使形状可以在控制台或给定的任何画布上绘制。

如果我们将此类写为Win Forms,则不能将其用于控制​​台应用程序或Web应用程序。它是否正确?

好吧,那是真的。如果为Windows窗体编写UserControl(通常不是类),则将无法在控制台上使用它。但这不是问题。您为什么希望Windows窗体的UserControl可以与任何类型的演示一起使用?UserControl应该做一件事并且做好。它通过定义绑定到某种形式的演示。最后,用户需要一些具体的东西,而不是抽象的东西。对于框架,这可能仅是部分正确,但对于最终用户应用程序,则是如此。

但是,它背后的逻辑应该分开,以便您可以将其与其他演示技术一起使用。在必要时引入接口,以保持应用程序的正交性。一般规则是:具体事物应该与其他具体事物互换。

这个问题的原因是,我发现自己总是陷入困境,并且挂在如何归纳类上,因此它们是最有用的。这实际上对我不利,我想知道我是否在“努力”。

您知道,极限程序员喜欢他们的YAGNI态度。不要试图通用地编写所有内容,也不要为了使通用性而太费劲。这称为过度工程,最终将导致完全费解的代码。确切地给每个组件一个任务,并确保它做得很好。在需要更改的地方(必要时)放入抽象(例如,用于绘制上下文的接口,如上所述)。

通常,在编写业务应用程序时,应始终尝试使事物脱钩。MVC和MVVM非常适合将逻辑与演示文稿分离,因此您可以将其重新用于Web演示文稿或控制台应用程序。请记住,最后,某些事情必须具体。您的用户无法使用抽象,他们需要一些具体的东西。对于程序员来说,抽象只是帮助您,以保持代码的可扩展性和可维护性。您需要思考在什么地方需要代码变得灵活。最终,所有抽象都必须孕育出具体的东西。

编辑:如果您想阅读更多有关可以提供最佳实践的体系结构和设计技术的信息,建议您阅读@Catchops答案,并阅读有关Wikipedia的SOLID实践的信息。

另外,对于初学者来说,我总是推荐以下书籍:Head First Design Patterns。它不仅可以帮助您了解抽象技术/ OOP设计实践,还可以帮助您理解GoF书(这是很好的书,它不适合初学者)。


1
好的答案,但是极端的程序员不会“在我们期望事物发生变化的地方引入抽象”。我们把在那里的东西抽象变化,干涸的代码。
凯文·克莱恩

1
@kevin cline太棒了,除非您要设计一个公开的库,且该库的API必须符合以前的行为和接口。
Magnus Wolffelt,2011年

@Magnus-是的,用某些语言设计一个库,计划向后二进制兼容性,这很棘手。人们被迫编写各种当前不需要的代码以允许将来扩展。这对于编译成这种语言的语言可能是合理的。对于编译为虚拟机的语言来说,这是愚蠢的。
凯文·克莱恩

19

你绝对是对的。不,您正在“尝试得很好” :)

阅读有关单一责任原则的信息

您在课堂上的内部工作以及应将此信息呈现给用户的方式是两个责任。

不要害怕解开类。很少有问题是过多的抽象和去耦:)

两种非常相关的模式是用于Web应用程序的模型视图控制器和用于Silverlight / WPF的模型视图ViewModel


1
+1您甚至可以将MVC用于非Web应用程序,至少它可以帮助您从明确职责的角度进行思考。
Patrick Hughes

MVVM与MVC无关。MVC主要用于无状态应用程序,例如Web应用程序。
鲍里斯·扬科夫

3
-1以我的经验,最普遍的问题之一是过早的概括和总体工程过度。
Dietbuddha

3
您首先需要知道如何对某些东西进行过度设计。以我的经验,人们更经常“工程不足”软件。
鲍里斯·扬科夫

尽管我赞赏改进软件设计的努力,但是用接口上的调用代替类上的调用不会在语义上分离任何内容。考虑使用动态类型语言时会发生什么。在堆栈交换的程序员部分提供的建议中,过多地强调了表面(语法)解耦,而很少涉及真正的解耦。
Frank Hileman 2014年

7

我经常使用MVVM,我认为业务对象类永远不需要了解用户界面。当然,他们可能需要知道SelectedItem,或IsChecked,或IsVisible等,但是这些值不需要绑定到任何特定的UI中,并且可以是类的通用属性。

如果您需要在后面的代码中对界面做一些事情,例如设置焦点,运行动画,处理热键等,那么代码应该是UI背后的代码的一部分,而不是您的业务逻辑类。

因此,我想说不要停止尝试将UI和类分开。它们之间的解耦越多,维护和测试就越容易。


5

多年来,已经开发出了许多经过尝试的真实设计模式,可以准确满足您的要求。您问题的其他答案都提到了“ 单一责任原则”(这是绝对有效的),并且似乎是导致您提出问题的原因。该原则只是说明,一个班级需要做好一件事情。换句话说,提高内聚性和降低耦合性是优秀的面向对象设计的全部目的-一个类可以很好地完成一件事情,而对其他事情没有太多依赖。

好吧...您是正确的观察到,如果您想在iPhone上画一个圆,它将不同于在运行Windows的PC上画一个圆。您必须(在这种情况下)有一个具体的类,该类在iPhone上绘制一个孔,而另一个在PC上绘制一个孔。这就是所有这些形状示例分解的继承的基本OO原则。您根本无法仅靠继承来做到这一点。

那就是接口进入的地方-正如“ 四人帮”书所述(必须阅读)-始终偏重于实现而不是继承。换句话说,使用接口可以拼凑一个可以以多种方式执行各种功能而又不依赖于硬编码依赖项的体系结构。

我看过参考SOLID原则。那些很棒。“ S”是单一责任原则。但是,“ D”代表依赖倒置。可以在此处使用控制模式的反转(依赖注入)。它非常强大,可以用来回答如何构建一个既可以为iPhone又可以为PC画圈的系统的问题。

可以创建一个包含通用业务规则和数据访问,但使用这些结构具有各种用户界面实现的体系结构。但是,确实有一个团队来实施它并在实际中看到它来真正理解它,这确实有帮助。

这只是对问题的快速高级解答,值得更详细的解答。我鼓励您进一步研究这些模式。可以找到这些模式的一些更具体的实现,例如MVC和MVVM。

祝好运!


当某人对某事投反对票而没有评论其原因时,我会喜欢上它(当然是讽刺)。我说的原则是正确的-下选派请站起来并说出原因。
Catchops,2011年

2

关于编写可能必须了解用户界面的类的最佳实践是什么?

知道如何绘制自身的类不会因为依赖于用户界面(控制台,GUI等)而打破了一些最佳做法吗?

在这种情况下,您仍然可以使用MVC / MVVM并使用通用接口注入不同的UI实现:

public interface IAgnosticChartDrawing
{
   public void Draw(ChartProperties chartProperties);
   event EventHandler ChartPanned;
}

public class GuiChartDrawer : UserControl, IAgnosticChartDrawing
{
    public void Draw(ChartProperties chartProperties)
    {
        //GDI, GTK or something else...
    }

    //Implement event based on mouse actions
}

public class ConsoleChartDrawer : IAgnosticChartDrawing
{
    public void Draw(ChartProperties chartProperties)
    {
        //'Draw' using characters and symbols...
    }

    //Implement event based on keyboard actions
}

IAgnosticChartDrawing guiView = new GuiChartDrawer();
IAgnosticChartDrawing conView = new ConsoleChartDrawer();

Model model = new FinancialModel();

SampleController controllerGUI = new SampleController(model, guiView);
SampleController controllerConsole = new SampleController(model, conView);

这样,您将能够重复使用Controller和Model逻辑,同时能够添加新类型的GUI。


2

有不同的模式可以做到:MVP,MVC,MVVM等。

Martin Fowler(大名)读的一篇不错的文章是GUI Architectures:http : //www.martinfowler.com/eaaDev/uiArchs.html

MVP尚未被提及,但绝对值得一提:看看它。

这是Google Web Toolkit开发人员建议使用的模式,它确实很简洁。

您可以在这里找到真正的代码,真实的示例和理由,以了解为什么使用这种方法:

http://code.google.com/webtoolkit/articles/mvp-architecture.html

http://code.google.com/webtoolkit/articles/mvp-architecture-2.html

遵循这种方法或类似方法的优势之一是可测性!在很多情况下,我会说这是主要优势!


1
+1的链接。我在MSDN上读过一篇关于MVVM的类似文章,尽管这些Google文章的模式略有不同,但它们要好得多。
皮特

1

这是OOP 无法很好地完成抽象的地方之一。OOP多态性对单个变量(“ this”)使用动态分配。如果多态性源于Shape,则不能以多态方式分派渲染器(控制台,GUI等)。

考虑一个可以调度两个或多个变量的编程系统:

poly_draw(Shape s, Renderer r)

并且还假设系统可以为您提供一种表示Shape类型和Renderer类型的各种组合的poly_draw的方法。这样就很容易提出形状和渲染器的分类,对吗?类型检查器将以某种方式帮助您确定是否存在可能错过的形状和渲染器组合。

大多数OOP语言不支持以上任何一种(少数语言支持,但不是主流)。我建议您看看“ 访问者”模式中的一种解决方法。


1

... draw()方法不是非常依赖于用户界面是什么吗?如果我们将此类写为Win Forms,则不能将其用于控制​​台应用程序或Web应用程序。它是否正确?

以上听起来对我来说是正确的。根据我的理解,可以说这意味着ControllerView在MVC设计模式方面相对紧密的耦合。这也意味着要在桌面控制台Web应用程序之间进行切换,必须相应地将控制器和视图同时切换为一对 -仅模型保持不变。

...我发现自己总是被卡住并挂在如何归纳类上,因此它们是最有用的。

好吧,我目前在上面所做的事情是,我们正在谈论的这种View-Controller耦合还可以,甚至更多,在现代设计中它非常流行

不过,在一两年前,我对此也感到不安全。在研究了Sun论坛上有关模式和OO设计的讨论后,我改变了主意。

如果您有兴趣,请亲自尝试此论坛-它现已迁移到Oracle(链接)。如果您到达那里,请尝试ping Saish-当时,他对这些棘手问题的解释对我来说最有帮助。我无法确定他是否仍然参加-我本人已经很长时间没有去过那里了


1

但是draw()方法不是非常依赖于用户界面是什么吗?

从务实的角度来看,Rectangle如果这是用户端的要求,那么系统中的某些代码需要知道如何绘制类似a的东西。这将归结为做真正底层的事情,例如光栅化像素或在控制台中显示某些内容。

从耦合的角度来看,我的问题是,谁/什么应该依赖这种类型的信息,以及在什么程度上详细(例如抽象程度如何)?

抽象绘图/渲染功能

因为如果高层绘图代码仅依赖于非常抽象的事物,那么该抽象也许能够(通过替换具体的实现)在您要定位的所有平台上起作用。作为一个人为的示例,某些非常抽象的IDrawer界面可能能够在控制台API和GUI API中实现,以完成诸如绘制图形的操作(控制台实现可能会将控制台视为具有ASCII艺术的80xN“图像”)。当然,这是一个人为的例子,因为通常这不是您想要执行的,而是将控制台输出视为图像/帧缓冲区。通常,大多数用户端需求都要求在控制台中进行更多基于文本的交互。

另一个考虑因素是设计一个稳定的抽象有多容易?因为如果您所针对的只是现代GUI API,可能会很容易抽象出基本的图形绘制功能,例如绘制线条,矩形,路径,文本,此类东西(仅是有限基元集的简单2D栅格化) ,具有一个抽象接口,可以通过各种子类型轻松地为所有接口实现这些接口,而成本却很少。如果您可以有效地设计这样的抽象并将其实现在所有目标平台上,那么对于形状或GUI控件或任何知道如何使用这样的图形自己绘制图形的人来说,它的危害要小得多,甚至根本没有危害。抽象。

但是要说,您正在尝试抽象出Playstation Portable,iPhone,XBox One和功能强大的游戏PC之间的细节,而您的需求是在每台设备上利用最先进的实时3D渲染/阴影技术。 。在那种情况下,当基础硬件功能和API发生如此巨大的变化时,尝试提出一个抽象接口来提取渲染细节,几乎可以肯定会导致大量的设计和重新设计时间,并且由于意外发生而频繁发生设计更改的可能性很高发现,以及同样是最低公分母解决方案,它无法利用基础硬件的全部唯一性和功能。

使依赖关系流向稳定的“轻松”设计

在我的领域中,我处于最后一种情况。我们针对具有根本不同的基础功能和API的许多不同的硬件,并试图提出一种渲染/绘图抽象来统治它们都是无可救药的(我们可能会举世闻名,只要有效地做到这一点就可以成为游戏)行业中的更换者)。因此,就我而言,我想要的最后一件事就像是类比Shape或,Model或者Particle Emitter知道如何绘制自身,即使它以最高级别和最抽象的方式表示该绘制也是如此...

...因为这些抽象太难于正确设计,并且当一个设计难以正确而又取决于一切时,这就是最昂贵的中央设计变更的秘诀,该变更会波动并破坏所有依赖于它的事物。因此,您想要的最后一件事是使系统中的依赖关系流向抽象设计,以至于很难获得正确的设计(如果不进行侵入性更改,则很难稳定)。

困难取决于容易,不容易取决于困难

因此,我们要做的是使依赖关系流向易于设计的事物。设计一个仅专注于存储多边形和材料之类的抽象“模型”并使其设计正确的设计要比设计一个可以有效地(通过可替换的混凝土子类型)实现以服务于绘图的抽象“渲染器”要容易得多。从PC统一请求与PSP不同的硬件。

在此处输入图片说明

因此,我们将依赖关系从难以设计的事物中分离出来。我们没有让抽象模型知道如何将它们绘制到它们都依赖的抽象渲染器设计上(如果该设计发生更改,就会中断其实现),而是让一个抽象渲染器知道如何绘制场景中的每个抽象对象(模型,粒子发射器等),然后我们可以为PC实施OpenGL渲染器子类型RendererGl,对于PSP 则实现另一种RendererPsp,对于手机等则实现这种情况。从渲染器到场景中的各种类型的实体(模型,粒子,纹理等),而不是相反。

在此处输入图片说明

  • 我所用的“稳定性/不稳定性”与鲍伯叔叔的传入/传出度量标准略有不同,据我所知,度量标准更能衡量变更的难度。我更多地谈论“需要更改的可能性”,尽管他的稳定性指标在那里很有用。当“变化的可能性”与“变化的容易性”成正比时(例如:最有可能需要变化的事物具有最高的不稳定性,并且与鲍勃叔叔的度量有较高的耦合度),那么任何此类可能的变化都是廉价且无干扰的,仅需替换实现,而无需涉及任何中央设计。

如果您发现自己试图在代码库的中央层抽象出一些东西,并且设计起来太困难了,而不是顽固地殴打墙壁,每个月/每年不断对其进行侵入性更改,则需要更新8,000个源文件,因为打破所有依赖它的东西,我的第一建议是考虑反转依赖关系。看看您是否可以以某种方式编写代码,使得难以设计的事物取决于易于设计的其他事物,而没有取决于难以设计的事物而具有易于设计的事物。请注意,我在说的是设计(特别是接口设计),而不是实现:有时候事情很容易设计且难以实现,有时候事情很难设计但容易实现。依赖关系流向设计,因此,重点应仅在于确定某项设计难于确定依赖关系流向的方向。

单一责任原则

对我而言,SRP通常在这里并不那么有趣(尽管取决于上下文)。我的意思是说,在设计目的明确且可维护的Shape东西时会采取钢丝绳平衡的动作,但是例如,如果对象不知道如何绘制自己的对象,则可能必须公开更详细的信息,并且可能没有太多有意义的事情要进行。在特定的使用上下文中处理形状,而不是对其进行构造和绘制。在所有情况下都需要权衡取舍,并且与SRP无关,它可以使我意识到在某些情况下如何使自己成为这样的维护梦night。

它与耦合以及依赖关系在系统中流动的方向有关。如果您试图将所有依赖的抽象渲染接口(因为他们使用它来绘制自己)移植到新的目标API /硬件,并且意识到您必须进行大量更改才能使其在那有效地工作,那么请这是一项非常昂贵的更改,需要替换系统中所有知道如何绘制自己的东西的实现。这是我遇到的最实际的维护问题,这些事情都知道如何绘制自身,如果这会转化为大量依赖流向抽象,而这些依赖又很难正确设计。

开发者骄傲

我提到这一点是因为,根据我的经验,这通常是使依赖关系流向更易于设计的事物的最大障碍。开发人员在这里有点野心很容易地说,“我要设计跨平台渲染抽象来统治所有规则,我要解决其他开发人员花了几个月的时间来移植的过程,然后我得到了正确,它将在我们支持的每个平台上像魔术一样工作,并在每个平台上都使用最先进的渲染技术;我已经在脑海中预想了它。”在这种情况下,他们抵制了避免这样做的实际解决方案,而只是改变了依赖关系的方向,并将可能造成巨大成本和反复出现的中心设计更改转化为对实现的简单廉价更改和本地重复执行。开发人员需要某种“白旗”的本能,以在某些东西难以设计到如此抽象的水平时重新考虑并重新考虑他们的整个策略,否则他们会陷入很多痛苦和痛苦。我建议将这种雄心壮志和战斗精神转移到最容易实现的设计的最新实现中,而不是将这种征服世界的雄心提升到界面设计级别。


1
我似乎无法理解“将依赖关系从难以设计的事物中分离出来”的想法,您是在谈论继承吗?利用PSP / OpenGL的的例子,你的意思,而不是制作OpenGLBillboard,你会成为一个OpenGLRenderer知道如何得出任何类型的IBillBoard?但是它会通过将逻辑委托给它来做到这一点吗IBillBoard,还是会IBillBoard具有条件条件的大型开关或类型呢?这就是我很难理解的原因,因为这似乎根本无法维护!
史蒂夫·查麦拉德

@SteveChamaillard区别在于,PSP(有限的硬件)必须使用老式的屏蔽门透明度和不同的渲染评估顺序来处理体积基元:digitalrune.github.io/DigitalRune-Documentation/media/…。当你有一个中央RendererPsp哪知道你的游戏场景的高层次的抽象,它可以然后做所有的魔法,它需要的方式,看起来在PSP上说服呈现这样的事情后空翻......
龙能源

而如果您有这样的体积原语请求渲染自身,则要么它们的操作如此高级以至于它们基本上是在指定自己进行渲染(除了回旋冗余和进一步耦合之外没有任何区别),或者渲染器抽象无法真正实现以最有效的方式在PSP上执行操作(至少不执行后空翻,如果依赖关系被反转,则不需要这样做)。
Dragon Energy

1
好吧,我想我现在明白了。基本上,您是在说这样的担忧(即硬件)是如此之高,以至于BillBoard很难使诸如此类的低级对象成为现实吗?而IRenderer高水平已经可以依靠这些关注而麻烦更少了?
史蒂夫·查麦拉德

1
由于在不同的平台,不同的硬件功能上都存在尖端的渲染问题,因此很难在我是老板的地方设计某种东西并告诉它要做什么。我说起来容易多了:“嘿,我是多云/有雾的东西,像这样的灰色水​​状颗粒,请尝试使我看起来不错,不要抓住我最近没刮过头的脖子”,并考虑到它的工作局限性,让渲染器找出如何以尽可能美观和实时的方式渲染我。任何告诉它做什么的尝试几乎肯定会导致适得其反的结果。
Dragon Energy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.