如何警告其他程序员类的实现


96

我正在编写“必须以特定方式使用”的类(我想所有类都必须...)。

例如,我创建了一个fooManager类,该类需要调用Initialize(string,string)。而且,为了进一步推动该示例,如果我们不听其ThisHappened行为,则该类将无用。

我的意思是,我正在编写的类需要方法调用。但是,如果您不调用这些方法,它将编译得很好,并且最终将得到一个空的新FooManager。在某些时候,根据类及其作用,它要么不起作用,要么崩溃。实现我的类的程序员显然会在其中查找并意识到“哦,我没有调用Initialize!”,这很好。

但是我不喜欢那样。我理想的情况是,如果不调用该方法,则不编译该代码。我猜这根本不可能。或立即可见清晰的事物。

我发现自己对这里的当前方法感到困扰,具体方法如下:

在类中添加一个私有布尔值,并检查类是否已初始化;如果不是,我将抛出一个异常,说“该类尚未初始化,您确定要调用.Initialize(string,string)吗?”。

我对这种方法还可以,但是它会导致很多代码被编译,最终对于最终用户来说是不必要的。

另外,当有更多方法Initiliaze要调用时,有时甚至会有更多的代码。我试图让我的课程没有太多的公共方法/动作,但这并不能解决问题,只是使其合理。

我在这里寻找的是:

  • 我的方法正确吗?
  • 有更好的吗?
  • 你们做什么/建议?
  • 我是否要解决非问题?同事们告诉我,程序员在尝试使用该类之前应先检查该类。我谨对此表示不同意见,但这是我相信的另一件事。

简而言之,我试图找到一种永远不会忘记在以后重用该类或被其他人重用时实现调用的方法。

澄清说明:

要在此处澄清许多问题:

  • 我绝对不仅在谈论类的初始化部分,而且还涉及整个生命周期。防止同事两次调用一个方法,确保他们在Y之前先调用X,以此类推。任何最终成为强制性的要求和文档中的东西,但我希望在代码中尽可能地简化和缩小。我确实很喜欢Asserts的想法,尽管我很确定我将需要混合其他一些想法,因为Asserts并非总是可能的。

  • 我正在使用C#语言!我怎么没提到呢?我在Xamarin环境中,通常在解决方案中使用大约6到9个项目来构建移动应用程序,包括PCL,iOS,Android和Windows项目。我已经从事开发工作大约一年半(包括学校和工作在内),因此有时我会提出荒谬的陈述\问题。所有这一切可能都无关紧要,但是太多的信息并不总是一件坏事。

  • 由于平台的限制和依赖注入的使用,我不能总是将所有必需的东西都放在构造函数中,因为除了Interfaces之外的其他参数都没有了。也许我的知识不足,这很有可能。在大多数情况下,这不是初始化问题,而是更多

我如何确定他注册了该活动?

我如何确保他不会忘记“在某个时候停止该过程”

在这里,我记得一个广告获取类。只要可见“广告”的视图,该类就会每分钟获取一个新广告。该类在构造时需要一个可以显示广告的视图,该视图显然可以包含在参数中。但是,一旦视图消失,必须调用StopFetching()。否则,该类将继续为甚至不在该视图中的视图获取广告,这很糟糕。

同样,该类具有必须被监听的事件,例如“ AdClicked”。如果不听,一切都会很好,但是如果未注册水龙头,我们将失去对分析的跟踪。不过,该广告仍然可以正常运行,因此用户和开发人员不会发现任何差异,并且分析将仅包含错误的数据。需要避免这种情况,但是我不确定开发人员如何知道他们必须注册到tao事件。虽然这是一个简化的示例,但是这里有一个想法,“确保他使用可用的公共动作”,而且当然在正确的时间!


13
通常,无法确保以特定顺序发生对类的方法的调用。这是许多与停止问题等效的问题之一。尽管在某些情况下可以使不合规成为编译时错误,但尚无通用解决方案。推测这就是为什么即使数据流分析已经​​相当复杂,很少有语言支持可以让您至少诊断出明显情况的结构的原因。
Kilian Foth '16


5
另一个问题的答案无关紧要,问题不一样,因此它们是不同的问题。如果有人在找那个讨论,他不会输入另一个问题的标题。
吉尔·桑德

6
是什么防止在ctor中合并 initialize的内容?必须在initialize对象创建之后进行调用吗?在可能引发异常并破坏创建链的意义上,ctor是否会过于“冒险”?
斑点

7
这个问题称为时间耦合。如果可以的话,请尝试通过在遇到无效输入时在构造函数中引发异常来避免将对象置于无效状态,这样一来,new如果对象尚未准备好使用,您将永远不会超出调用范围。依靠初始化方法注定会再次困扰您,除非绝对必要,否则应避免使用它,至少这是我的经验。
2016年

Answers:


161

在这种情况下,最好使用语言的类型系统来帮助您进行正确的初始化。我们怎样才能防止FooManager使用而不被初始化?通过防止FooManager创建没有必要的信息来正确初始化。特别地,所有初始化都是构造函数的责任。您绝对不应让构造函数以非法状态创建对象。

但是调用者需要先构造一个a,FooManager然后才能对其进行初始化,例如,因为FooManagera作为依赖项进行传递。

FooManager如果您没有,则不要创建一个。相反,您可以做的是传递一个对象,使您可以检索完全构造的对象FooManager,但仅包含初始化信息。(就函数式编程而言,我建议您对构造函数使用部分应用程序。)例如:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

问题在于,每次访问时都必须提供init信息FooManager

如果需要使用您的语言,则可以将getFooManager()操作包装在类工厂或类构建器中。

我真的想对initialize()方法是否被调用进行运行时检查,而不是使用类型系统级的解决方案。

可以找到一个折衷方案。我们创建了一个包装器类MaybeInitializedFooManager,该包装器类的get()方法返回FooManager,但是如果FooManager尚未完全初始化则抛出该方法。这仅在通过包装器完成初始化或存在 FooManager#isInitialized()方法的情况下有效。

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

我不想更改类的API。

在这种情况下,您将要避免if (!initialized) throw;每种方法中的条件。幸运的是,有一个简单的模式可以解决这个问题。

您提供给用户的对象只是一个空外壳,它将所有调用委派给实现对象。默认情况下,实现对象会为未初始化的每个方法引发错误。但是,该initialize()方法用完全构造的对象替换实现对象。

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

这会将类的主要行为提取到中MainImpl


9
我喜欢前两个解决方案(+1),但我不认为您最后一个重复执行in FooManager和in 方法的代码片段UninitialisedImpl比重复更好if (!initialized) throw;
Bergi '16

3
@Bergi有些人喜欢上课。尽管如果FooManager很多方法,它可能比潜在地忘记某些if (!initialized)检查要容易得多。但是在那种情况下,您可能应该更喜欢分班。
immibis '16

1
MaybeInitializedFoo似乎也不比初始化的Foo好,但是+1给出了一些选项/想法。
马修·詹姆斯·布里格斯

7
+1工厂是正确的选择。您不能不对其进行更改就无法改进设计,因此恕我直言,答案的最后一部分是如何将其减少一半不会对OP有所帮助。
内森·库珀

6
@Bergi我发现上一节介绍的技术非常有用(另请参见“通过多态替换条件”重构技术和状态模式)。如果我们使用if / else,则很容易忘记一个方法的签入,而忘记实现接口所需的方法则要困难得多。最重要的是,MainImpl完全对应于一个将initialize方法与构造方法合并的对象。这意味着MainImpl的实现可以享受到此提供的更强大的保证,从而简化了主代码。…
amon

79

防止客户“滥用”对象的最有效和有用的方法是使其变为不可能。

最简单的解决方案是Initialize与构造函数合并。这样,该对象将永远不会在未初始化状态下供客户端使用,因此不会发生错误。如果您无法在构造函数本身中进行初始化,则可以创建一个工厂方法。例如,如果您的类要求注册某些事件,则可以要求事件侦听器作为构造函数或工厂方法签名中的参数。

如果您需要能够在初始化之前访问未初始化的对象,则可以将这两种状态实现为单独的类,因此可以从一个UnitializedFooManager实例开始,该实例的Initialize(...)方法返回InitializedFooManager。只能在初始化状态下调用的方法仅存在于上InitializedFooManager。如果需要,可以将该方法扩展到多个状态。

与运行时异常相比,将状态表示为不同的类需要更多的工作,但是它还为您提供了编译时保证,即您不会调用对对象状态无效的方法,并且可以在代码中更清楚地记录状态转换。

但更一般而言,理想的解决方案是设计类和方法时不受限制,例如要求您在特定时间和特定顺序调用方法。这可能并不总是可能的,但是在很多情况下可以通过使用适当的模式来避免。

如果您具有复杂的时间耦合(必须按一定顺序调用许多方法),则一种解决方案可能是使用control的反转,因此您可以创建一个类,以适当的顺序调用方法,但使用模板方法或事件以允许客户端在流程的适当阶段执行自定义操作。这样,将按正确顺序执行操作的责任从客户端推到了类本身。

一个简单的示例:您有一个File-object,它使您可以从文件中读取。但是,客户端需要在调用方法Open之前先ReadLine进行调用,并且需要记住始终调用Close(即使发生异常),之后ReadLine再也不能调用该方法。这是时间耦合。可以通过使用一个采用回调或委托作为参数的方法来避免这种情况。该方法管理打开文件,调用回调然后关闭文件。它可以将带有Read方法的独特接口传递给回调。这样,客户端就不可能忘记以正确的顺序调用方法。

与时间耦合的接口:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

没有时间耦合:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

一个更重量级的解决方案是将其定义File为一个抽象类,该类要求您实现一个(受保护的)方法,该方法将在打开的文件上执行。这称为模板方法模式。

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

优点是相同的:客户端不必记住一定顺序调用方法。根本不可能以错误的顺序调用方法。


2
我还记得有些情况,那就是根本不可能从构造函数中删除,但是我现在没有一个例子可以显示
Gil Sand

2
现在,该示例描述了工厂模式-但第一句话实际上描述了RAII范例。那么这个答案应该推荐哪一个呢?
Ext3h '16

11
@ Ext3h我不认为工厂模式和RAII是互斥的。
Pieter B

@PieterB不,不是,工厂可以确保RAII。但这仅意味着模板/构造函数args(称为UnitializedFooManager)不得与实例(InitializedFooManager)共享状态,这Initialize(...)实际上具有Instantiate(...)语义。否则,如果开发人员两次使用同一模板,则此示例会导致新问题。如果该语言不明确支持move语义以可靠地使用该模板,则不可能静态地进行预防/验证。
Ext3h '16

2
@ Ext3h:我推荐第一个解决方案,因为它是最简单的。但是,如果客户端需要在调用Initialize之前能够访问该对象,则不能使用它,但是可以使用第二种解决方案。
JacquesB '16

39

我通常会检查IllegalStateException是否初始化,并且在未初始化的情况下尝试使用它。

但是,如果您希望编译时安全(值得称赞,并且值得推荐),为什么不将初始化视为返回已构造和初始化对象的工厂方法,例如

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

这样您就可以控制对象的创建和生命周期,并且您的客户端可以通过Component初始化来获得目标。实际上,这是一个懒惰的实例。


1
好的答案-简洁的答案中有2个好的选择。上面的其他答案提出了(理论上)有效的类型系统体操;但在大多数情况下,这两个简单的选项是理想的实际选择。
Thomas W

1
注意:在.NET中,Microsoft将InvalidOperationException升级为您所说的IllegalStateException
miroxlav '16

1
我并没有否决这个答案,但这并不是一个很好的答案。仅仅投掷IllegalStateExceptionInvalidOperationException突然丢掉,用户将永远无法理解自己做错了什么。这些异常不应成为解决设计缺陷的旁路。
displayName 2016年

您会注意到,抛出异常是一个可能的选项,我将继续详细说明我的首选选项-使此编译时安全。我已编辑答案以强调这一点
Brian Agnew

14

我要从其他答案中打破一点,不同意:如果不知道您使用的是哪种语言,就不可能回答这个问题。这是否是一个有价值的计划,以及应该给予的正确的“警告”您的用户完全取决于您的语言提供的机制以及该语言的其他开发人员所遵循的约定。

如果您FooManager在Haskell中拥有一个,则允许您的用户构建一个无法管理s的行为犯罪Foo的,因为类型系统使它变得如此简单,而这正是Haskell开发人员所期望的约定。

在另一方面,如果你正在写C,你的同事会完全在自己的权利带你出去回来,拍你的定义独立struct FooManagerstruct UninitializedFooManager类型,支持不同的操作,因为它会导致不必要的复杂代码收效甚微。

如果您正在编写Python,则该语言中没有机制可让您做到这一点。

您可能没有在编写Haskell,Python或C,但是它们是说明类型系统预期能完成多少工作的示例。

遵循开发人员对您的语言的合理期望,并抵制过度设计没有自然惯用实现的解决方案的冲动(除非错误很容易制造且难以发现,因此值得将其极端化)长度以使其变为不可能)。如果您没有足够的语言经验来判断什么是合理的,请听从比您更了解的人的建议。


我对“如果您正在编写Python,语言中没有机制可以让您做到这一点”感到困惑。Python具有构造函数,创建工厂方法非常简单。所以也许我不明白您所说的“这个”是什么意思?
AL Flanagan

3
“这个”是指编译时错误,而不是运行时错误。
Patrick Collins

只是提到了Python 3.5,它是当前版本的可插拔类型系统。绝对有可能仅在运行代码之前就将其导入,从而使Python出错。直到3.5,这是完全正确的。
本杰明·格伦鲍姆

哦好的。迟来的+1。甚至感到困惑,这是非常有见地的。
AL Flanagan

我喜欢你的风格Patrick。
吉尔·桑德

4

由于您似乎不想将代码检查交付给客户(但对于程序员来说似乎很好),因此可以使用断言函数(如果它们在您的编程语言中可用)。

这样,您就可以在开发环境中进行检查了(其他开发人员称之为WILL的测试将可预测地失败),但是您不会将代码交付给客户,因为断言(至少在Java中是这样),只有选择性地进行编译。

因此,使用该类的Java类如下所示:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

断言是检查所需程序运行时状态的好工具,但实际上不要期望任何人在真实环境中做错事。

而且它们非常苗条,不会占用if()throw ...语法的大部分,也不需要被捕获等等。


3

与当前的答案相比,该问题更笼统地看待这个问题,当前的答案主要集中在初始化上。考虑的对象,将会有两个方法,a()b()。要求是a()始终被调用过b()。您可以通过从中返回一个新对象a()并将其移至b()新对象而不是原始对象来创建编译时检查,以确认是否发生了这种情况。示例实现:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

现在,如果不先调用a()就无法调用b(),因为它是在一个隐藏的接口中实现的,直到调用a()为止。诚然,这需要很多工作,所以我通常不会使用这种方法,但是在某些情况下它可能是有益的(尤其是如果您的类在很多情况下会被程序员重用,而程序员可能不会这样做)。熟悉其实现方式,或者对可靠性至关重要的代码)。还要注意,这是许多现有答案所建议的构建器模式的概括。它的工作方式几乎相同,唯一真正的区别是数据的存储位置(在原始对象中,而不是在返回的对象中)以及打算何时使用数据(在任何时候都仅在初始化期间使用)。


2

当我实现一个需要额外的初始化信息或链接到其他对象才能使用的基类时,我倾向于将该基类抽象化,然后在该类中定义在基类流程中使用的几个抽象方法(例如abstract Collection<Information> get_extra_information(args);并且abstract Collection<OtherObjects> get_other_objects(args);必须通过继承协议由具体类来实现,从而迫使该基类的用户提供基类所需的所有东西。

因此,当我实现基类时,我立即明确地知道了为使基类正确运行而必须编写的内容,因为我只需要实现抽象方法即可。

编辑:澄清一下,这几乎与向基类的构造函数提供参数相同,但是抽象方法实现允许实现处理传递给抽象方法调用的参数。甚至在不使用任何参数的情况下,如果抽象方法的返回值取决于您可以在方法主体中定义的状态,这仍然很有用,而将变量作为参数传递给它时是不可能的构造函数。当然,如果您宁愿使用合成而不是继承,您仍然可以传递基于相同原理具有动态行为的参数。


2

答案是:是的,不是,有时。:-)

您描述的一些问题很容易解决,至少在许多情况下是这样。

编译时检查肯定比运行时检查更好,后者比根本没有检查更好。

可再生能源运行时间:

至少从概念上讲,通过运行时检查强制按特定顺序调用函数是容易的。只需放入表明已运行哪个函数的标志,然后让每个函数以类似“如果不是先决条件1或先决条件2则抛出异常”之类的开头即可。如果检查变得复杂,则可以将其推入私有功能。

您可以使某些事情自动发生。举一个简单的例子,程序员经常谈论对象的“惰性填充”。创建实例时,将is-populated标志设置为false,或将某些关键对象引用设置为null。然后,当调用需要相关数据的函数时,它将检查标志。如果为假,则填充数据并将标志设置为true。如果为真,则继续假设数据在那里。您可以在其他前提条件下做同样的事情。在构造函数中设置标志或将其设置为默认值。然后,当您到达应该调用某些前提条件函数的位置时,如果尚未调用该条件函数,请调用它。当然,只有在那时有要调用的数据时,这才起作用,但是在许多情况下,您可以在“

RE编译时间:

正如其他人所说,如果需要在使用对象之前进行初始化,则将初始化放入构造函数中。对于OOP,这是非常典型的建议。如果可能的话,应该使构造函数创建对象的有效可用实例。

我看到一些有关在初始化之前需要传递对对象的引用该怎么办的讨论。我想不出一个真正的例子,但我想这可能发生。已经提出了解决方案,但是我在这里看到的解决方案以及我能想到的任何解决方案都会使代码变得混乱和复杂。在某些时候,您必须问:是否值得制作难看的,难以理解的代码,以便我们可以进行编译时检查而不是运行时检查?还是我为自己和他人创造了很多工作,以实现一个不错的目标,但这不是必需的?

如果应始终同时运行两个功能,则简单的解决方案是使它们成为一个功能。就像每次将广告添加到页面一样,您也应该为其添加指标处理程序,然后将代码添加到指标处理程序中,并将其添加到“添加广告”功能内。

有些事情在编译时几乎是不可能检查的。就像某个功能不能被多次调用的要求一样。(我已经写了很多类似的函数。最近我写了一个函数,可以在魔术目录中查找文件,处理找到的文件,然后将其删除。当然,一旦删除了文件,就无法重新运行。等等。我不知道任何一种语言具有允许您阻止在编译时在同一实例上两次调用该函数的功能。我不知道编译器如何确定这是正在发生的事情。您所能做的就是进行运行时检查。特别是运行时检查很明显,不是吗?“如果已经在这里= true,则抛出异常,否则设置已经在这里= true”。

RE无法执行

完成一个类后,类需要进行某种清理的情况并不少见:关闭文件或连接,释放内存,将最终结果写入数据库等。没有一种简单的方法可以强制在编译时进行或运行时间。我认为大多数OOP语言都有某种“ finalizer”功能的规定,该功能在实例被垃圾回收时被调用,但我认为大多数人还说他们不能保证将永远运行该功能。OOP语言通常包括带有某些规定的“ dispose”功能,以设置实例的使用范围,“ using”或“ with”子句,并且当程序退出该范围时,将运行dispose。但这要求程序员使用适当的范围说明符。并不是强迫程序员正确地做,

在无法强迫程序员正确使用该类的情况下,我会尝试使它出错,以便如果他做错了,程序就会崩溃,而不是给出错误的结果。一个简单的例子:我见过很多程序将一堆数据初始化为虚拟值,这样,即使用户未能调用函数正确填充数据,用户也永远不会得到空指针异常。我一直想知道为什么。您正在竭尽全力使错误更难以发现。如果当程序员未能调用“加载数据”功能然后又巧妙地尝试使用数据时,他得到了空指针异常,则该异常将迅速向他显示问题所在。但是,如果在这种情况下通过将数据空白或为零来隐藏它,则该程序可能会运行到完成但产生不正确的结果。在某些情况下,程序员甚至可能没有意识到存在问题。如果他确实注意到了,他可能必须走很长一段路才能找到问题出在哪里。尽早失败比尝试勇敢地坚持下去更好。

通常,可以肯定的是,当您无法正确使用对象时,总是很好的。如果函数的构造方式使得程序员根本没有办法进行无效调用,或者无效调用会产生编译时错误,那么这是很好的。

但是,您还必须指出,程序员应阅读该函数的文档。


1
关于强制清除,有一种模式可以使用,其中只能通过调用特定方法来分配资源(例如,在典型的OO语言中,它可以具有私有构造函数)。此方法分配资源,使用对资源的引用调用传递给它的函数(或接口的方法),然后在此函数返回后销毁资源。除了突然终止线程之外,此模式还可以确保始终销毁资源。这是功能语言中的一种常见方法,但是我也已经看到它在OO系统中使用效果很好。
Jules


2

是的,您的直觉是偶然的。许多年前,我在杂志上写过文章(有点像这个地方,但在枯树上,每月放出一次),讨论DebuggingAbuggingAntibugging

如果您可以使编译器抓住错误,那是最好的方法。可用的语言功能取决于语言,您未指定。无论如何,将针对此站点上的单个问题详细介绍特定的技术。

但是,进行测试以在编译时而非运行时检测使用情况实际上是同一类型的测试:您仍然需要知道首先调用适当的函数并以正确的方式使用它们。

设计该组件以避免一开始就成为一个问题就更加微妙了,我喜欢Buffer就像Element示例。第4部分(发布于20年前!哇!)包含一个与您的案例非常相似的讨论示例:必须谨慎使用此类,因为在开始使用read()之后可能不会调用buffer()。相反,它只能在构造后立即使用一次。让类自动和不可见地管理调用者的缓冲区将在概念上避免该问题。

您问,是否可以在运行时进行测试以确保正确使用,您是否应该这样做?

我会说是的。当代码得以维护且特定用法受到干扰时,有一天可以为您的团队节省大量调试工作。可以将测试设置为可以测试的一组正式约束不变式。这并不意味着每次调用都会留有额外的空间来跟踪状态并进行检查。它在类中,因此可以被调用,并且代码记录了实际的约束和假设。

也许仅在调试版本中进行检查。

也许检查很复杂(例如,考虑进行堆检查),但是它存在并且可以偶尔执行一次,或者在处理代码并且出现一些问题时在这里和那里添加调用,或者对它进行特定的测试。确保链接到同一类的单元测试集成测试程序中没有任何此类问题。

您可能最终会认为开销很小。如果该类确实提供文件I / O,则没有额外的状态字节和对此的测试。通用iostream类检查流是否处于不良状态。

你的直觉是好的。保持!


0

信息隐藏(封装)的原理是,该类外部的实体不应该知道超过正确使用该类所需的知识。

您的案例似乎是您试图使一个类的对象正常运行,而又不告诉外部用户有关该类的足够信息。您隐藏了比应有的更多信息。

  • 要么在构造函数中明确要求该必需信息;要么
  • 或保持构造函数完整无缺并修改方法,以便调用这些方法,您必须提供缺少的数据。

至理名言:

*无论哪种方式,您都必须重新设计类和/或构造函数和/或方法。

*如果设计不当并没有从正确的信息中饿死课堂,您可能会冒险上课,而不是在一个地方中断,而可能在多个地方中断。

*如果您编写的异常和错误消息不佳,那么您的课堂将会更多地介绍自己。

*最后,如果应该使局外人知道必须先初始化a,b和c,然后调用d(),e(),f(int),那么抽象中就存在泄漏。


0

由于您已指定使用C#,因此针对您的情况的可行解决方案是利用Roslyn代码分析器。这将使您可以立即捕获违规,甚至可以提出建议的代码修复

实现它的一种方法是装饰与指定顺序的方法需要被称为属性的时间再加方法1。当分析器在类中找到这些属性时,它将验证这些方法是按顺序调用的。这会使您的课程看起来像以下内容:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1:我从未编写过Roslyn代码分析器,因此这可能不是最佳实现。但是,使用Roslyn代码分析器来验证您的API是否正确使用的想法是100%合理的。


-1

我之前解决此问题的MakeFooManager()方法是在类中使用私有构造函数和静态方法。例:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

由于实现了构造函数,因此任何人都无法FooManager不通过而创建的实例MakeFooManager()


1
这似乎只是重复之前在几个小时前发布的先前答案中提出和解释的观点
gna

1
与仅在ctor中进行空检查相比,这有什么优势吗?似乎您刚刚制作了一个只包装另一个方法就什么也不做的方法。为什么?
萨拉

另外,为什么foo和bar成员变量?
帕特里克·中号

-1

有几种方法:

  1. 使类易于使用,对则难以使用。其他一些答案专门针对这一点,因此我仅提及RAII构建器模式

  2. 注意如何使用注释。如果班级是自我解释,请不要发表评论。*这样,当班级实际需要它们时,人们更有可能阅读您的评论。并且,如果您遇到以下几种罕见情况之一,其中的类很难正确使用并且需要注释,请提供示例。

  3. 在调试版本中使用断言

  4. 如果该类的用法无效,则引发异常。

*以下是您不应该写的评论:

// gets Foo
GetFoo();
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.