我正在编写“必须以特定方式使用”的类(我想所有类都必须...)。例如,我创建了fooManager类,该类需要调用Initialize(string,string)。而且,为使示例更进一步,如果我们不听ThisHappened动作,则该类将无用。
我的意思是,我正在编写的类需要方法调用。但是,如果您不调用这些方法,它将编译得很好,并且最终将得到一个空的新FooManager。在某些时候,根据类及其作用,它要么不起作用,要么崩溃。实现我的类的程序员显然会在其中查找并意识到“哦,我没有调用Initialize!”,这很好。
但我不喜欢那样。我理想的情况是,如果不调用该方法,则不编译该代码。我猜这根本不可能。或立即可见的东西。
我发现自己对这里的当前方法感到困扰,如下所示:
在类中添加一个私有布尔值,并检查是否有必要类被初始化;如果没有,我将抛出一个异常,说“该类尚未初始化,您确定要调用.Initialize(string,string)吗?”。
这种方法我还可以,但它会导致很多代码
此外,当有更多方法要调用的不仅仅是Initiliaze时,有时甚至是更多的代码。我尝试让我的课程没有太多的公共方法/动作,但这并不能解决问题,只是使其合理。
我在这里寻找的是:

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


简而言之,我试图找到一种方法,在以后重用该类或被其他人重用时,永远不会忘记实现调用。
澄清:
在这里要澄清许多问题:


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


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


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



我如何确保他注册了该活动?
我如何确保他不会忘记“在某个时候停止该过程”

在这里,我记得一个广告获取类。只要可见“广告”的视图,该类就会每分钟获取一个新广告。该类在构造时需要一个视图以显示广告,而该视图显然可以包含在参数中。但是一旦视图消失,必须调用StopFetching()。否则,该类将继续为甚至不在该视图中的视图获取广告,这很糟糕。
此外,该类还具有必须监听的事件,例如“ AdClicked”。如果不听,一切都会很好,但是如果未注册水龙头,我们将失去对分析的跟踪。不过,该广告仍然可以正常运行,因此用户和开发人员不会发现任何差异,并且分析将仅包含错误的数据。需要避免这种情况,但是我不确定开发人员如何知道他们必须注册到tao事件。虽然这是一个简化的示例,但是这里的想法是,“确保他使用可用的公共动作”,并且当然在正确的时间!

评论

通常,无法确保以特定顺序进行对类方法的调用。这是许多与停止问题等效的问题之一。尽管在某些情况下有可能使不合规成为编译时错误,但没有通用的解决方案。这大概就是为什么即使数据流分析已经​​相当复杂,很少有语言支持可以让您至少诊断出明显情况的结构的原因。
具有多个参数的单一方法的可能重复与必须按顺序调用的许多方法

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

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

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

#1 楼

在这种情况下,最好使用语言的类型系统来帮助您进行正确的初始化。如何防止未经初始化就使用FooManager?通过防止在没有必要的信息的情况下创建FooManager来正确初始化它。特别地,所有初始化都是构造函数的责任。

但是调用者必须先构造一个FooManager,然后才能初始化它,例如因为FooManager是作为依赖项传递的。

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



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


的问题是,每次访问FooManager时都必须提供初始化信息。

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

我真的想在运行时检查是否调用了initialize()方法,而不是使用类型系统级解决方案。 br />
可以找到一个折衷方案。我们创建一个包装器类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中。

评论


我喜欢前两个解决方案(+1),但是我不认为您最后一个片段重复了FooManager和UninitializedImpl中的方法比重复if(!initialized)throw;更好。

–贝尔吉
16年4月14日在23:30

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

–user253751
16 Apr 15'0:19



MaybeInitializedFoo似乎也不比初始化的Foo好,但是+1给出了一些选项/想法。

–马修·詹姆斯·布里格斯(Matthew James Briggs)
16年4月15日在2:37

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

–内森·库珀(Nathan Cooper)
16年4月15日在7:06

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

–阿蒙
16年4月15日在7:27

#2 楼

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

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

如果需要能够访问未初始化的对象,初始化,然后可以将两个状态实现为单独的类,因此从一个UnitializedFooManager实例开始,该实例具有一个Initialize(...)方法,该方法返回InitializedFooManager。仅在初始化状态下才能调用的方法仅存在于InitializedFooManager上。如果需要,可以将这种方法扩展到多个状态。与运行时异常相比,将状态表示为不同的类需要做更多的工作,但是它还为您提供了编译时的保证,使您不必调用对对象状态无效的方法,并且该方法在代码中更清楚地记录了状态转换。

但是更普遍的是,理想的解决方案是设计类和方法而没有约束,例如要求您在特定时间以特定顺序调用方法。这可能并不总是可能的,但是在许多情况下可以通过使用适当的模式来避免。

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

一个简单的示例:您有一个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();
}


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

评论


此外,我还记得无法从构造函数中删除的情况,但现在没有示例可以显示

–吉尔·桑德(Gil Sand)
16年4月14日在12:23

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

– Ext3h
16年4月14日在12:48

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

– Pieter B
16年4月14日在12:57

@PieterB不,不是,工厂可以确保RAII。但是,只有这样的附加含义,模板/构造函数args(称为UnitializedFooManager)不得与实例(InitializedFooManager)共享状态,因为Initialize(...)实际上具有Instantiate(...)语义。否则,如果开发人员两次使用同一模板,则此示例会导致新问题。如果该语言未明确支持移动语义以可靠地使用该模板,则也无法静态地进行预防/验证。

– Ext3h
16-4-14在13:19



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

–雅克B
16-4-14在14:35



#3 楼

如果您在未初始化的情况下尝试使用它,我通常只会检查它是否初始化,然后抛出(说)一个IllegalStateException

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

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


,这样您就可以控制对象的创建和生命周期,并且客户可以Component作为初始化的结果。实际上,这是一个懒惰的实例。

评论


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

–托马斯W
16年4月15日在3:44

注意:在.NET中,Microsoft将InvalidOperationException升级为您所谓的IllegalStateException。

– Miroxlav
16-4-16在12:52



我并没有否决这个答案,但这并不是一个很好的答案。仅仅将IllegalStateException或InvalidOperationException扔掉,用户将永远无法理解自己做错了什么。这些异常不应成为解决设计缺陷的旁路。

–displayName
16-4-16在15:50



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

–布赖恩·阿格纽(Brian Agnew)
16-4-16在15:52



#4 楼

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

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

另一方面,如果您正在编写C,则您的同事完全有权将您带回去,并纵容您定义支持不同操作的单独的struct FooManagerstruct UninitializedFooManager类型,因为这将导致不必要的复杂代码,而带来的好处很小。

重新编写Python,la中没有任何机制

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

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

评论


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

– A. L. Flanagan
16年4月15日在17:26

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

–帕特里克·柯林斯(Patrick Collins)
16年4月15日在18:03

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

–本杰明·格伦鲍姆(Benjamin Gruenbaum)
16年4月17日在22:19

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

– A. L. Flanagan
16年4月18日在20:50

我喜欢你的风格Patrick。

–吉尔·桑德(Gil Sand)
17年2月6日在22:08

#5 楼

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

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

因此,使用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()抛出的大部分...语法,不需要被捕获,等等。

#6 楼

与当前答案相比,该问题更普遍地看待这个问题,当前的答案主要集中在初始化上。考虑一个具有两种方法的对象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( ) 叫做。诚然,这需要很多工作,所以我通常不会使用这种方法,但是在某些情况下它可能是有益的(尤其是如果您的类在很多情况下会被程序员重用,而程序员可能不会这样做)。熟悉其实现方式,或者对可靠性至关重要的代码)。还要注意,正如许多现有答案所建议的那样,这是构建器模式的概括。它的工作方式几乎相同,唯一真正的区别是数据的存储位置(在原始对象中,而不是在返回的对象中)以及何时打算使用数据(随时使用,仅在初始化期间使用)。 >

#7 楼

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

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

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

#8 楼

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

您描述的某些问题至少在许多情况下很容易解决。

编译时检查肯定比运行时检查更可取,这是最好不要执行任何检查。

RE运行时:

从概念上讲,至少强制使用某种顺序调用函数是很简单的,时间检查。只需放入表明已运行哪个函数的标志,然后让每个函数以类似“如果不是先决条件1或先决条件2则抛出异常”之类的开头即可。如果检查变得复杂,则可以将其推送到私有函数中。

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

RE编译时:

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

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

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

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

无法强制执行

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

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

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

但是还有一点要说,程序员应该阅读函数的文档。

评论


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

–法律
16年4月17日在17:27

@朱尔斯又名“处置者”

–本杰明·格伦鲍姆(Benjamin Gruenbaum)
16年4月17日在22:22

#9 楼

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

如果可以让编译器抓住滥用,这是最好的方法。可用的语言功能取决于语言,您未指定。无论如何,都会在此站点上针对单个问题详细介绍具体技术。

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

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

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

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

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

您可能会认为开销毕竟是微不足道的。如果该类确实进行了I / O文件操作,那么额外的状态字节以及对此类字节的测试就没有了。普通的iostream类检查流是否处于不良状态。

您的直觉是好的。加油!

#10 楼

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

您的情况似乎是您试图获取一个类的对象以可以正常运行,而不必告诉外部用户有关该类的足够信息。您隐藏了比应有的更多的信息。


要么在构造函数中明确要求该必需的信息;要么保留构造函数完整并修改方法,以便顺序进行要调用这些方法,您必须提供丢失的数据。


智慧之语:

*两种方法都必须重新设计类和/或构造函数,然后/或方法。

*如果设计不当并没有从正确的信息中使该课程饿死,您就有可能使您的课程在一个或多个地方中断。

*如果您

*最后,如果应该让局外人知道它必须初始化a,b和c,然后再进行初始化,那么您的课堂就会付出更多有关自己的事情。调用d(),e(),f(int),则抽象中会出现泄漏。

#11 楼

由于您已指定使用C#,因此针对您的情况的可行解决方案是利用Roslyn代码分析器。这将使您可以立即捕获违例,甚至可以提出代码修复建议。
一种实现它的方法是用指定方法需要被调用顺序的属性来修饰时间耦合方法。当分析器在类中找到这些属性时,它将验证方法是否按顺序调用。这会使您的类看起来像以下内容:
 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%合理的。

#12 楼

我之前解决此问题的方法是在类中使用私有构造函数和静态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()

评论


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

– gna
16年4月15日在4:49

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

–萨拉
16年4月15日在10:46

另外,为什么foo和bar成员变量呢?

–帕特里克M
16年4月15日在19:39

#13 楼

有几种方法:


使类易于使用,而又难以使用错误。其他一些答案专门针对这一点,因此,我仅提及RAII和构建器模式。
请注意如何使用注释。如果班级是自我解释的,请不要发表评论。*这样,当班级实际需要它们时,人们更有可能阅读您的评论。并且,如果您遇到此类很难正确使用且需要注释的罕见情况之一,请提供示例。
在调试版本中使用断言。
如果该类的用法无效,则引发异常。

*这些是您不应写的评论类型:

// gets Foo
GetFoo();