只做一件事的类的模式


24

假设我有一个程序可以执行以下操作

void doStuff(initalParams) {
    ...
}

现在,我发现“做事”是相当复杂的操作。该过程变得很大,我将其拆分为多个较小的过程,很快我意识到在进行填充时具有某种状态会很有用,因此我需要在较小的过程之间传递较少的参数。因此,我将其纳入自己的类中:

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

然后我这样称呼它:

new StuffDoer().Start(initialParams);

或像这样:

new StuffDoer(initialParams).Start();

这就是感觉不对的地方。使用.NET或Java API时,我始终不会调用new SomeApiClass().Start(...);,这使我怀疑自己做错了。当然,我可以将StuffDoer的构造函数设为私有,并添加一个静态辅助方法:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

但是然后我有了一个其外部接口仅包含一个静态方法的类,这也感觉很奇怪。

因此,我的问题是:这种类型的类是否有公认的模式?

  • 只有一个入口
  • 是否没有“外部可识别”状态,即实例状态仅执行该入口点时需要?

众所周知,我做的事情就像bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");我只关心特定信息,而LINQ扩展方法不可用。可以正常工作,并且适合if()状况内,而无需跳过箍。当然,如果要编写这样的代码,则需要使用垃圾回收语言。
CVn 2012年

如果与语言无关,为什么还要同时添加javac#?:)
史蒂芬·杰里斯

我很好奇,“一种方法”的功能是什么?这将有助于确定特定情况下的最佳方法,但这仍然是一个有趣的问题!
史蒂文·杰里斯

@StevenJeuris:在各种情况下我都偶然发现了这个问题,但是在当前情况下,这是一种将数据导入本地数据库的方法(从Web服务器加载数据,进行一些操作并将其存储在数据库中)。
Heinzi

1
如果在创建实例时始终调用该方法,为什么不使它成为构造函数内部的初始化逻辑呢?
NoChance 2012年

Answers:


29

有一种称为“ 方法对象”的模式,您可以在其中将一个带有多个临时变量/参数的大型方法分解为一个单独的类。您执行此操作,而不只是将方法的一部分提取到单独的方法中,因为它们需要访问局部状态(参数和临时变量),并且您不能使用实例变量共享局部状态,因为它们对于该方法而言是局部的(以及从中提取的方法),其余对象将不再使用。

因此,方法改为成为自己的类,参数和临时变量成为该新类的实例变量,然后将该方法分解为新类的较小方法。生成的类通常只有一个公共实例方法,该方法执行该类封装的任务。


1
这听起来很像我在做什么。谢谢,至少我现在有个名字。:-)
Heinzi

2
非常相关的链接!对我来说,它闻起来像是一种反模式。如果您的方法那么长,那么也许其中的一部分可以在可重用的类中提取出来,或者通常以更好的方式进行重构?我以前也没有听说过这种模式,也找不到很多其他资源,这使我相信它通常没有被使用太多。
史蒂文·杰里斯


1
@StevenJeuris我认为这不是反模式。一种反模式是将具有批参数的重构方法弄乱,而不是在实例变量中存储这些方法之间常见的状态。
Alfredo Osorio

@AlfredoOsorio:嗯,它确实杂乱了很多参数的重构方法。它们存在于对象中,所以它比将它们全部传递更好,但比重构为仅需要有限参数子集的可重用组件要糟糕。
Jan Hudec 2012年

13

我要说的是,所讨论的类(使用单个入口点)经过精心设计。易于使用和扩展。相当扎实。

同样的事情no "externally recognizable" state。这是很好的封装。

我看不到类似代码的任何问题:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);

1
当然,如果您不需要doer再次使用它,则将减少为var result = new StuffDoer(initialParams).Calculate(extraParams);
CVn 2012年

计算应重命名为doS​​tuff!
shabunc 2012年

1
我要补充一点,如果您不想在使用它的地方初始化(新)您的类(可能要使用Factory),则可以选择在业务逻辑中的其他位置创建对象,而不是直接传递参数到您拥有的单个公共方法。另一种选择是使用Factory并在其上使用make方法,该方法获取参数并通过其构造函数创建具有所有参数的对象。比您从任何地方调用没有参数的公共方法都可以。
Patkos Csaba

2
这个答案过于简单。StringComparer您使用的课程new StringComparer().Compare( "bleh", "blah" )设计得好吗?完全不需要时如何创建状态呢?好的,行为被很好地封装了(至少对类的用户来说,更多有关我的回答),但这在默认情况下并没有使其成为“好的设计”。至于“行动成果”的例子。仅当您使用doer与分开使用时,这才有意义result
史蒂文·杰里斯

1
这不是存在的理由,而是one reason to change。差异似乎微妙。您必须了解这意味着什么。它将永远不会创建一个方法类
jgauffin 2012年

8

SOLID原则的角度来看,jgauffin的答案很有意义。但是,您不应忘记一般的设计原则,例如信息隐藏

我发现了使用给定方法的几个问题:

  • 正如您指出的那样,当创建的对象不管理任何状态时,人们不会期望使用'new'关键字。您的设计反映了它的意图。使用类的人可能会困惑于它管理的是什么状态,以及随后对该方法的调用是否会导致不同的行为。
  • 从使用类的人的角度来看,内部状态被很好地隐藏了,但是当您想对类进行修改或仅仅理解它时,您会使事情变得更加复杂。我已经写了很多关于拆分方法(使它们变小)的问题,尤其是在将状态转移到类范围时。您正在修改API的使用方式,以便具有更小的功能!我认为这绝对是太过分了。

一些相关参考

争论的主要点可能在于“ 单一责任原则”的延伸范围“如果将它推到极致并建立有一个理由存在的类,则每个类可能只会得到一个方法。这将导致即使是最简单的过程也需要大量的类,从而导致系统成为难以理解且难以改变。”

与此主题相关的另一个相关参考:“将程序划分为执行一个可识别任务的方法。使方法中所有操作都处于相同的抽象级别。” - 肯特·贝克这里的关键是“抽象的同一水平。” 这并不意味着“一件事”,因为它经常被解释。此抽象级别完全取决于您要设计的上下文

那么正确的方法是什么?

在不知道您的具体用例的情况下,很难分辨。在某些情况下,我有时(不经常)使用类似的方法。当我想处理数据集时,不想让整个类范围都可以使用此功能。我写了一篇关于它的文章,文章介绍lambda如何进一步改善封装性。我也在程序员这个话题上提出了一个问题。以下是我使用此技术的最新示例。

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

由于内部的“本地”代码ForEach不会在其他任何地方重用,因此我可以将其保留在与之相关的确切位置。在我看来,以这样一种方式对代码进行概述:将相互依赖的代码紧密地组合在一起,使其更具可读性。

可能的选择

  • 在C#中,您可以改用扩展方法。因此,您直接传递给该“一件事”方法就可以对参数进行操作。
  • 查看此函数是否实际上不属于另一个类。
  • 使它成为静态类中的静态函数。这很可能是最合适的方法,正如您所引用的常规API中所反映的那样。

在另一个答案Poltergeists中找到了这种反模式的可能名称。
史蒂文·杰里斯

4

我会说这很好,但是您的API仍应该是静态类上的静态方法,因为这正是用户所期望的。静态方法用于new创建您的帮助对象并完成工作的事实是一个实现细节,无论调用该对象的人都应该隐藏该实现细节。


哇!您设法比我更简洁地掌握了它。:) 谢谢。(当然我走得更远一点,我们可能不同意,但我与一般的前提下同意)
史蒂芬Jeuris

2
new StuffDoer().Start(initialParams);

无能的开发人员可能会使用共享实例并使用它,这是一个问题

  1. 多次(如果以前(可能是部分)执行不会干扰以后的执行,则可以)
  2. 从多个线程(不是很好,您明确地说它具有内部状态)。

因此,这需要明确的文档,它不是线程安全的,并且如果它是轻量级的(创建速度很快,不会占用外部资源或大量内存),则可以即时实例化。

将其隐藏在静态方法中将对此有所帮助,因为这样就永远不会重复使用该实例。

如果它具有一些昂贵的初始化,则可以(并非总是)准备一次初始化并多次使用它,这是有益的,方法是创建另一个仅包含状态并使其可克隆的类。初始化将创建将存储在中的状态doerstart()将其克隆并将其传递给内部方法。

这也将允许其他事情,例如持久执行部分执行的状态。(如果花费很长时间并且外部因素(例如电力供应故障)可能会中断执行。)但是通常不需要这些额外的花哨的东西,因此不值得。


好点。希望您不介意清理。
史蒂文·杰里斯

2

除了我更详尽,个性化的其他答案外,我觉得以下观察值得单独回答。

有迹象表明您可能正在遵循Poltergeists反模式

Poltergeists是具有非常有限的角色和有效生命周期的课程。他们经常启动其他对象的处理。重构的解决方案包括将责任重新分配给寿命更长的对象,从而消除了Poltergeists。

症状和后果

  • 冗余导航路径。
  • 瞬态关联。
  • 无状态类。
  • 临时的,短期的对象和类。
  • 仅用于通过临时关联“播种”或“调用”其他类的单操作类。
  • 具有“类似控件”操作名称的类,例如start_process_alpha。

重构解决方案

捉鬼敢死队通过将Poltergeists完全从类层次结构中删除来解决他们。但是,在删除它们之后,必须替换poltergeist“提供”的功能。只需进行简单的调整即可校正体系结构,这很容易。


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.