简体   繁体   中英

Macro-like structures in C# ==OR== How to factor out a function containing 'return'

I never thought I'd have a need for anything this (since macros are bad, right?), but it turns out I do.

I find myself repeating the following pattern over and over again in my code, dozens if not hundreds of times:

object Function() {
    this.Blah = this.Blah.Resolve(...);
    if(this.Blah == null)
        return null;
    if(this.Blah.SomeFlag)
        return this.OtherFunction();

    // otherwise, continue processing...
}

I'd like to write something like this instead:

object Function() {
    this.Blah = this.Blah.Resolve(...);
    VerifyResolve(this.Blah);

    // continue processing...
}

Except the fact that the pattern contains conditional return s means I can't factor out a function. I've had to subtly change the pattern once already, and since I couldn't do a simple search it was a pain to find all the instances.

How can I avoid repeating myself needlessly, and make any future changes to this pattern easier?

If you often find that you have to check against null you may want to use the Null Object Pattern . Essentially, you create an object that represents the null result. You can then return this object instead of null . You will then have to adapt the rest of your code to include behaviour for the null object. Something along these lines perhaps:

MyClass Function() 
{  
  this.Blah = this.Blah.Resolve(...); 
  VerifyResolve(this.Blah);

  // continue processing...  
}  

MyClass VerifyResolve(MyClass rc) 
{
  // ...
  return rc.Blah.SomeFlag ? rc.OtherFunction() : rc;
} 

NullMyClass : MyClass {
  public override bool SomeFlag { get { return false; } }
}

You could say something like:

object Function()
{ 
    this.Blah = this.Blah.Resolve(...);
    object rc;
    if (!VerifyResolve(out rc)
       return rc;

    // continue processing... 
} 

bool VerifyResolve(out object rc)
{
    if(this.Blah == null)      
    {
        rc = null;
        return true;
    }
    if(this.Blah.SomeFlag)      
    {
        rc = this.OtherFunction();
        return true;
    }
    rc = null;
    return false;
}

Maybe neater:

if (!TryResolve(this.Blah)) return this.Blah;

where TryResolve sets the value of this.Blah to either null or this.OtherFunction and returns an appropriate bool

No offence but there's a couple of code smells in there:

this.Blah = this.Blah.Resolve(...);

This line in particular would cause me to start the process of a rethink.

The main problems I'd have issues with are the object return type and the assignment of a Property to a value returned by calling a method on that property. These both smell like you've messed up the inheritance somewhere and ended up with a heavily stateful system, which is both a bugger to test and a pain to maintain.

Perhaps it would be better to rethink instead of trying to use hacks and tricks to sidestep this issue: I normally find that if I'm trying to abuse the language with a feature like Macros then my design needs work !

EDIT

Ok, so with added info perhaps this isn't so much of a smell, but I'd still suggest the following:

class ExampleExpr{
    StatefulData data ... // some variables that contain the state data
    BladhyBlah Blah { get; set; }

    object Function(params) {
        this.Blah = this.Blah.Resolve(params);
        ....
    }
}

This code is worrying because it forces a fully state-based approach, the output depends on what has happened beforehand, and thus needs specific steps to replicate. This is a pain to test. Also, if you call Function() twice, there's no guarantee what will happen to Blah without knowing what state it was in in the first place.

class ExampleExpr{
    StatefulData data ... // some variables that contain the state data

    object Function(params) {
        BlahdyBlah blah = BlahdyBlah.Resolve(params, statefulData);
    }
}

if we use a factory-style method instead, returning a new instance with specific information whenever we are presented with a certain set of parameters, we have eliminated one place where stateful data is used (ie the BladhyBlah instance is now re-created on each call with a specific parameter set).

This means we can replicate any functionality in testing by simply calling Function(params) with a specific Setup() to create the statefulData and a specific set of params.

In theory this is less efficient (as a new BlahdyBlah is being created on each factory call) but it's possible to cache instances of BlahdyBlah with specific data and share them between factory calls (assuming they do not have other methods that effect their internal state). However, it's MUCH more maintanence-friendly and from a testing standpoint totally kicks stateful data's bum.

This also helps remove your original problem, as when we don't rely on an internal instance variable we can all Resolve(params, statefulData) externally from Function(params), and simply not call Function(params) if either blah == null or blah.SomeFlag == SomeFlag.Whatever. Thus, by moving this outside the method we no longer need to worry about the returns.

Hope that's somewhere in the right ballpark, it's hard to know exactly what to recommend given a small example, as is usually the case with difficult/abstract questions on here.

It is better to extand the Blah class with VerifyResolve extantion method. If you own the Blah source then may be better to create some IVerifyResolveble Interface and make Blah implement it.

object Function() { 
    this.Blah = this.Blah.Resolve(...);
    object result;
    if (VerifyResolve(this.Blah, out result))
        return result;

    // continue processing... 
} 

It's messy, annoying, and verbose, but this is what I had to go with. There's probably no better way.

Another way is to make a struct, whose nullability is significant.

struct Rc
{
  internal object object;
  internal Rc(object object) { this.object = object; }
}

object Function() 
{  
    this.Blah = this.Blah.Resolve(...); 
    Rc? rc = VerifyResolve();
    if (rc.HasValue)
       return rc.Value.object; 

    // continue processing...  
}  

Rc? VerifyResolve() 
{ 
    if(this.Blah == null)       
    { 
        return new Rc(null); 
    } 
    if(this.Blah.SomeFlag)       
    { 
        return new Rc(this.OtherFunction()); 
    } 
    return null; 
} 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM