与字符串耦合比与类方法耦合更“松散”吗?


18

我正在使用Swing在Java中启动一个学校小组项目。这是数据库桌面应用程序上的简单GUI。

教授给了我们去年项目的代码,以便我们了解他的工作方式。我最初的印象是代码要复杂得多,但是我想程序员在查看他们不仅编写的代码时经常会想到这一点。

我希望找到他的系统好坏的原因。(我问教授,他说我稍后再看为什么会更好,这让我不满意。)

基本上,为了避免他的可持久对象,模型(业务逻辑)和视图之间的任何耦合,所有操作都由字符串完成。存储在数据库中的可持久对象是字符串的哈希表,模型和视图彼此“订阅”,为它们所订阅的“事件”提供字符串键。

触发事件后,视图或模型会向所有订阅者发送字符串,这些订阅者将决定对该事件采取何种措施。例如,在一种视图动作侦听器方法中(我相信这只是在可持久对象上设置了bikeMakeField):

    else if(evt.getSource() == bicycleMakeField)
    {
        myRegistry.updateSubscribers("BicycleMake", bicycleMakeField.getText());
    }

该调用最终到达Vehicle模型中的此方法:

public void stateChangeRequest(String key, Object value) {

            ... bunch of else ifs ...

    else
    if (key.equals("BicycleMake") == true)
    {
                ... do stuff ...

这位教授说,这种处理方式比让视图简单地在业务逻辑对象上调用方法更具可扩展性和可维护性。他说,视图和模型之间没有耦合,因为它们不知道彼此的存在。

我认为这是一种较差的耦合,因为视图和模型必须使用相同的字符串才能工作。如果删除视图或模型,或在字符串中输入错误,则不会出现编译错误。这也使代码比我认为的要长得多。

我想和他讨论这个问题,但是他利用他的行业经验来反驳我这个经验不足的学生可能提出的任何论点。我缺少他的方法有什么优势吗?


为了明确起见,我想比较上面的方法,同时将视图与模型明显耦合。例如,您可以将车辆模型对象传递到视图,然后更改车辆的“制造”,请执行以下操作:

vehicle.make = bicycleMakeField.getText();

这将把目前仅用于将车辆的品牌设置在一个地方的15条代码减少为一条可读代码。(由于这种操作在整个应用程序中进行了数百次,因此我认为这将是可读性和安全性的巨大胜利。)


更新资料

我的团队负责人和我重组了框架,采用了我们希望使用静态类型进行编码的方式,并告知了教授,最后给了他一个演示。只要我们不向他求助,他是否足够慷慨地允许我们使用我们的框架,以及是否可以让我们其余的团队保持最新状态-这对我们来说似乎很公平。


12
加时。我认为您的教授应该更新自己,而不使用旧代码。在学术界,没有理由使用2006年的2012
。– Farmor

3
当您最终得到他的答复时,请通知我们。
JeffO 2012年

4
不管代码的质量或技术的智慧如何,都应该从对用作标识符的字符串的描述中删除“ magic”一词。“魔术字符串”表示该系统不合逻辑,并且将使您的视图变色并毒害您与讲师的任何讨论。诸如“键”,“名称”或“标识符”之类的词比较中性,也许更具描述性,并且更有可能导致有益的对话。
卡勒布(Caleb)2012年

1
真正。与他交谈时我没有称它们为魔术绳,但是您说的对,没有必要使它听起来比实际更糟。
菲利普(Philip)

2
教授描述的方法(通过名为字符串的事件通知侦听器)可能很好。实际上,枚举可能更有意义,但这不是最大的问题。我在这里看到的真正问题是,接收通知的对象是模型本身,然后您必须根据事件的类型手动进行调度。在函数是第一类的语言中,您应该注册一个函数,而不是事件的对象。在Java中,我猜想应该使用某种包装单个函数的对象
Andrea

Answers:


32

您的教授提出的方法最好用字符串类型来描述,并且几乎在每个级别上都是错误的。

最好通过依赖性反转来减少耦合,依赖性反转是通过多态调度完成的,而不是硬接线许多情况。


3
您编写了“几乎每个级别”,但尚未写出为什么(在您看来)在此示例中不合适的情况。知道会很有趣。
hakre'2

1
@hakre:没有合适的情况,至少在Java中没有。我说过,“几乎在每个级别上都是错误的”,因为它比根本不使用任何间接方法更好,而只是将所有代码都放在一个地方。但是,使用(全局)魔术字符串常量作为手动调度的基础只是最终使用全局对象的一种复杂方式。
back2dos 2012年

因此,您的观点是:从来没有合适的案例,所以这不是一个案例吗?这几乎不是一个论点,并且实际上不是很有帮助。
hakre'2

1
@hakre:我的观点是,这种方法由于多种原因(第1段-避免这种情况的足够原因在字符串类型的解释中给出)是不好的,因为有更好的方法(第2段),因此不应使用)。
back2dos 2012年

3
@hakre我可以想象有一种情况可以证明采用这种方法,那就是如果一个连环杀手用导管将您绑在椅子上,并用电锯在头顶上,狂笑着,命令您到处使用字符串文字进行调度逻辑。除此之外,依靠语言功能来执行此类操作是可取的。实际上,我还可以想到另一起涉及鳄鱼和忍者的案件,但这涉及的程度更多。
罗布

19

你的教授做错了。他试图通过强制通信在两个方向上通过字符串传播来完全分离视图和模型(视图不依赖于模型,模型不依赖于视图),这完全违背了面向对象设计和MVC的目的。听起来好像他不是在编写类并设计基于OO的体系结构,而是在编写完全响应基于文本的消息的各种模块

为了对教授有点公平,他正在尝试用Java创建与高度通用的.NET接口非常相似的接口INotifyPropertyChanged,该接口通过传递指定哪个属性已更改的字符串来通知订户更改事件。至少在.NET中,此接口主要用于从数据模型进行通信以查看对象和GUI元素(绑定)。

如果做对了,采用这种方法可能会带来一些好处¹:

  1. 视图依赖于模型,但是模型不需要依赖于视图。这是适当的控制反转
  2. 您在View代码中只有一个位置可以管理对模型中的更改通知的响应,而只有一个位置可以进行订阅/取消订阅,从而减少了挂起的参考内存泄漏的可能性。
  3. 要从事件发布者添加或删除事件时,可以减少编码开销。在.NET中,有一个称为的内置发布/订阅机制,events但是在Java中,您将必须创建类或对象,并为每个事件编写订阅/取消订阅代码。
  4. 这种方法最大的缺点是,订阅代码可能与发布代码不同步。但是,这在某些开发环境中也是一个好处。如果在模型中开发了一个新事件,并且尚未更新视图以订阅该事件,则该视图仍可能有效。同样,如果事件被删除,则您会有一些无效的代码,但是它仍然可以编译并运行。
  5. 至少在.NET中,反射在这里非常有效地用于根据传递给订阅者的字符串自动调用getter和setter。

因此,简而言之(可以回答您的问题),可以做到,但是您的教授所采用的方法是错误的,因为它不允许视图和模型之间至少存在一种单向依赖。这只是不切实际,过于学术化,也不是如何使用现有工具的良好示例。


¹在Google上进行搜索,INotifyPropertyChanged以找到人们发现的一百万种方法,以尝试在实施魔术字符串避免使用魔术字符串


听起来您在这里描述了SOAP。:D…(但是的,我明白你的意思,你是对的)
康拉德·鲁道夫

11

enumpublic static final String应该使用s(或s)代替魔术字符串。

你的教授不懂OO。例如有具体的Vehicle类?
并让这个班的学生了解自行车的制造方法?

车辆应该是抽象的,并且应该有一个名为Bike的具体子类。我会按照他的规则打球,以取得良好的成绩,然后忘记他的教学。


3
除非本练习的目的是使用更好的OO原理来改进代码。在这种情况下,请重构。
joshin4colours 2012年

2
@ joshin4colours如果StackExchange在给它评分,那将是正确的。但是由于评分员是提出这个想法的人,因此他会给我们所有人打坏分数,因为他知道自己的实施会更好。
丹·尼利

7

我认为您的观点很不错,魔术弦很不好。我们团队中的某个人几周前不得不调试由魔术弦引起的问题(但是他们写的是他们自己的代码,所以我闭上了嘴)。它发生在工业界

他的方法的优点是,编码可能会更快,尤其是对于小型演示项目。缺点是容易产生错字,字符串使用得越频繁,错字代码搞砸的机会就越大。而如果你想改变一个神奇的字符串,字符串重构是难度比重构变量,因为重构工具也可以轻触琴弦含有神奇的字符串(如一个子),但没有自己的魔法字符串,应被感动。另外,您可能需要保留魔术字符串的字典或索引,以便新开发人员不会出于相同的目的而开始发明新的魔术字符串,也不会浪费时间查找现有的魔术字符串。

在这种情况下,看起来枚举可能比魔术字符串甚至全局字符串常量更好。同样,当您使用常量字符串或枚举时,IDE经常会为您提供某种形式的代码辅助(自动完成,重构,查找所有引用等)。如果使用魔术字符串,IDE不会提供太多帮助。


我同意枚举比字符串文字更好。但是即使那样,用枚举键调用“ stateChangeRequest”还是直接在模型上调用方法真的更好吗?无论哪种方式,模型和视图都在那个位置耦合。
菲利普(Philip)

@Philip:那么,我想在那时解耦模型和视图,您可能需要某种控制器类才能做到这一点……
FrustratedWithFormsDesigner 2012年

@FrustratedWithFormsDesigner-是的。MVC架构是最不耦合的
cdeszaq 2012年

是的,如果另一层有助于将它们分离,我不会反对。但是,该层仍可以是静态类型的,并且可以使用实际方法而不是字符串或枚举消息来连接它们。
菲利普(Philip)

6

使用“行业经验”是一个糟糕的论据。您的教授需要提出一个比那更好的论点:-)。

就像其他人所说的,使用魔术字符串无疑是不受欢迎的,使用枚举被认为更安全。枚举具有含义范围,魔术字符串则没有。除此之外,您的教授将关注点分离的方式被认为是一种比当今更古老,更脆弱的技术。

我在回答此问题时看到@backtodos涵盖了这一点,因此我只想补充一点,即使用控制反转(依赖注入是一种形式),您很快就会在整个行业中使用Spring框架。 ..)被认为是处理这种类型的去耦的更现代的方法。


2
完全同意,学术界应该比行业提出更高的要求。学术界==最佳理论方法;业界==最佳实践方法
农民2012年

3
不幸的是,一些在学术界教授这些东西的人之所以这样做,是因为他们无法在行业中削减它。从积极的一面来看,学术界的一些人很才华横溢,不应在某些公司办公室中藏身之地。
FrustratedWithFormsDesigner 2012年

4

让我们说清楚一点;有不可避免地在那里某些耦合。存在这样一个可订阅的主题这一事实是耦合的重点,而消息其余部分(例如“单个字符串”)的性质也是如此。鉴于此,问题就变成了通过字符串或类型完成耦合时是否更大的问题之一。答案取决于您是否担心随时间或空间分布的耦合:是要在以后读取消息之前将消息保存在文件中,还是将消息发送到另一个进程(特别是如果(不是用相同的语言编写的),使用字符串可以使事情变得更加简单。缺点是没有类型检查。直到运行时(有时甚至直到那时),才会检测到错误。

Java的缓解/妥协策略可以是使用类型化接口,但是该类型化接口位于单独的程序包中。它应该仅由Java interface和基本值类型(例如,枚举,异常)组成。应该有没有在该封装接口的实现。然后每个人都对此进行实现,如果有必要以复杂的方式传达消息,则Java委托的特定实现可以在必要时使用魔术字符串。它还使将代码迁移到更专业的环境(例如JEE或OSGi)变得更加容易,并且极大地帮助了诸如测试之类的小事情。


4

您的教授建议使用的是“魔术弦”。虽然它确实在类之间“松散耦合”,允许更轻松地进行更改,但它是一种反模式。字符串应该非常非常少地用于包含或控制代码指令,并且在绝对必须发生时,用于控制逻辑的字符串的值应该集中且不断地定义,因此对的期望值只有一个权限任何特定的字符串。

原因很简单;不会对字符串进行编译器检查,以检查其语法或与其他值的一致性(最多只能检查它们是否与转义字符和字符串中其他特定于语言的格式有关的格式正确)。您可以将任何想要的内容放入字符串中。这样,您就可以编译出一个毫无希望的破损算法/程序,直到运行时才发生错误。考虑以下代码(C#):

private Dictionary<string, Func<string>> methods;

private void InitDictionary() //called elsewhere
{
   methods = new Dictionary<string, Func<string>> 
      {{"Method1", ()=>doSomething()},
       {"MEthod2", ()=>doSomethingElse()}} //oops, fat-fingered it
}

public string RunMethod(string methodName)
{
   //very naive, but even a check for the key would generate a runtime error, 
   //not a compile-time one.
   return methods[methodName](); 
}

...

//this is what we thought we entered back in InitDictionary for this method...
var result = RunMethod("Method2"); //error; no such key

...所有这些代码都可以编译,请先尝试一下,但是再看一下该错误就很明显了。此类编程有很多示例,而且都容易出错,即使您将字符串定义为常量也是如此(因为.NET中的常量已写入使用它们的每个库的清单中,因此必须然后在更改定义的值时重新编译所有内容,以使该值“全局”更改)。由于错误不是在编译时捕获的,因此必须在运行时测试中捕获,并且只有通过足够的代码练习才能保证单元测试中100%的代码覆盖率,通常只能在绝对故障保护中找到它实时系统。


2

实际上,这些类别仍然紧密相连。除了现在,它们以某种方式紧密耦合,编译器无法告诉您何时发生故障!

如果有人将“ BicycleMake”更改为“ BicyclesMake”,那么直到运行时,没人会发现问题已经解决。

与编译时错误相比,运行时错误的修复成本更高-这只是种种种弊病。

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.