2

How do I have to design my classes and their methods, so that it is clear, that certain method calls have to be made before others are useful/valid?

Stage1 firstStage = StageProcessor( userOriginal )

Stage2 secondStage = firstStage.ApplyRequiredTransformationBeforeSecondStage()

UserResult result = StageProcessor( firstStage, secondStage )

My approach is to use types to enforce dependencies. Are there other ways?

Please rename and refactor everything in this example to make it more readable and increase encapsulation.

I'd like to note, that method ApplyRequiredTransformationBeforeSecondStage() needs to be called between Stage1 and Stage2. But I am allowed to move it into an extra class.

Additionally UserResult really depends on Stage1 and Stage2 at the same time.

  • Is there something that prevents you from encapsulating this sequence inside of one method? The client client code would call the method without needing to know the sequence. The call would [obviously] look something like UserResult result = Processor.Process(userOriginal). – Nick Alexeev Dec 10 '18 at 23:20
  • @NickAlexeev Valid point. Stage2is actually a result required by the user. I forgot to make that clear in my example. So as long as UserResult returns result and secondStage, I could reduce all steps to one. – VisorZ Dec 11 '18 at 9:11
4

You can do some fancy "Fluent" style interfaces

public interface IProcess
{
    IStage2 = RunStage1(UserInput input);
}

public interface IStage2 
{
    IStage3 = RunStage3();
}

public interface IStage3 
{
    FinalResult FinalResult();
}

So when the user put its in their code they get..

var result = Process
        .RunStage1(myinput)
        .RunStage2()
        .RunStage3()
        .FinalResult();
3

The fancy term for what you're dealing with is temporal coupling.

My approach is to use types to enforce dependencies. Are there other ways?

There are many ways to do that.

Methods that mask their dependency on object state or global variables often cause this problem. The solution is to make those dependencies explicit.

For example you might have

ProcessOne();
ProcessTwo();
ProcessThree();

and the only hint that they must be called in order is their names. This is less that ideal since that's not enforced and leaves us stuck with names that are not very descriptive.

One way to make their dependencies explicit is to take the state they require as arguments.

ProcessThree(ProcessTwo(ProcessOne()));

This makes it impossible to call them out of order. This works inside objects and in functional languages.

For an object oriented API that will be accessed from outside you can use what's most properly called a internal Domain Specific Language (iDSL). This should not be confused with other fluent interfaces that allow calling methods in any order by returning this. iDSLs, such as Java 8 streams and JOOQ, return an intermediate object that dictates what methods are now legal to be called. This is what you're hinting at when you talk about controlling call order with types.

Returning the intermediate type and this can be mixed to form a complex language full of both required and optional steps. But if you're in a language that offers named parameters with default arguments this might seem needless. Almost like it's a lot of fuss to get around a language limitation.

  • Isn't this what @Ewan posted in their answer? Although it just deals with interfaces, not concrete types... – Greg Burghardt Dec 10 '18 at 19:29
2

Types are a pretty good way to enforce constraints statically which is what you are doing here. As such, the way you've written this has the compiler doing most of the leg work for you which is a good way to be.

An alternative would be to call methods on a single object that, internally, updated a state machine on that object. If a particular operation didn't see the correct state then it would produce an error.

This is worse in the sense that the constraints are checked at runtime instead of compile time. If you had a particularly complex set of constraints, and a type system that couldn't describe those constraints adequately, then it may be a useful alternative. However, personally, I prefer the type checked approach where possible.

1

I like Ewan's answer because it is an elegant solution to the problem you bringing to the table, yet I suspect the question itself signals a problem with your design. The way you present it, it seems rather pointless to let the client call the shots to perform steps that can be performed sequentially in one go by the serving object itself. Then there would be no issue in the first place.

Of course you would have though of that and it didn't work for you. So I figure there must be something you are not telling us. The client needs to mess with intermediate results or something. If that were the case you may have three different responsibilities on your hand that should not be handled by the same class.

What is it that makes you want to have this three-stage processor?

  • In detail; the user wants a duplicate of her object and I provide the duplicate method. Additionally she requests a dictionary that links elements of her object and the duplicate. That dictionary can only be created, if some tags have been added to her original object before(!) it was duplicated. – VisorZ Dec 11 '18 at 9:06
  • 2
    @VisorZ It seems you are facilitating the user's workflow by adding methods to your class. With every next step she comes up with, she has you write another method to support her scenario. Instead you should provide the tools and she should be able to apply them to solve her problem. Now your API is solving a scenario-specific problem for a user. You are going to have a series of methods for every individual user problem. That is not a an API, that is a monolithic application. it will be like "these methods are for John's November problem and those are for Jane's December problem". – Martin Maat Dec 11 '18 at 10:13
0

I think that's a fine way you have there for cases where there could be branching paths (ex: Stage1->[Stage2A or Stage2B]->Final Result) and for cases where you might want to memoize and reuse one of those stages to evaluate a different result. You've got the compiler and type system to prevent any possible misuse there and eliminated the temporal coupling that way and I can't tell for sure but it looks like it might have also eliminated the need to mutate state in favor of returning new states absent side effects.

Another way in cases where you don't need that kind of flexibility and there's an underlying order dependency in the interfaces is just have one function which accepts a function object/delegate/pointer as a parameter. Example:

do_something(SomeFunction func, ...)
{
     do_first_required_thing(...);
     do_second_required_thing(...);

     func(...);

     do_final_cleanup(...);
}

This kind of approach can be useful sometimes when you need like some things to be done before executing the user's code and possibly some things after (I tend to use it in cases where some kind of lock is required before invoking the specified function or there's a startup/shutdown process before and after what the user wants to do).

0

If you wish to enforce the order in the code, then Ewan's fluent example is an excellent way of doing this.

Alternatively, you could use a builder to ensure things happen in the correct order, without having to worry about the order specified in the client code:

var processor = new StageProcessor(userOriginal);
processor.AddToSecondStage(someOtherData);
processor.AddToPreStageTwoTransformations(someTransformation);
processor.AddToFirstStage(someData);

var result = processor.Resolve();

In the above, I've deliberately specified the actions in reverse order to highlight the disconnect between recoding the actions and resolving them. Because each call simply stores the action required until Resolve is called, the order then doesn't matter.

Both approaches have their place. So whether using contextual interfaces or builder is a better approach for you really depends on your specific application.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.