Kirill_Cyber
Kirill_Cyber

Reputation: 55

What design pattern should use in this case?

I have big model, that aggragates data for buisness entity.

class BigObject   
{  
    TypeA DataA { get;set; }    
    TypeB DataB { get;set; }     
    TypeC DataC { get;set; }
}   

and have service, which fill fields of model from differents sources. Some data depends from another data

class DataService   
{    
    public BigObject GetModel() 
    {     
        var model = new BigObject();     

        model.DataA = sourceServiceA.GetData();     
        model.DataB = sourceServiceB.GetData(model.DataA.Id);     
        model.DataC = sourceServiceC.GetData();   
    }  
}  

In method GetModel() I need to configure, which fields should be filled, which should not. For example, I want to fill DataA property, but don't want fill others. First idea is pass in method object BigObjectFilter

public BigObject GetModel(BigObjectFilter filter)
class BigObjectFilter   
{       
    bool FillDataA { get; set; }       
    bool FillDataB { get; set; }       
    bool FillDataC { get; set; }  
} 

and initialize this object in DataService clients. In GetObject method I was going to add conditions like

if (filter.FillDataA) 
{ 
    model.DataA = sourceServiceA.GetData(); 
} 
if (filter.FillDataC) 
{ 
    model.DataC = sourceServiceC.GetData(); 
}

I see, that this solution looks like bad practice. I would like to improve this construction. How can i improve it? I can't see, how to use builder pattern in this case, because i have requeired and optional data, one depends on the other.

Upvotes: 2

Views: 180

Answers (2)

StepUp
StepUp

Reputation: 38209

It looks like you have at least two choices here:

  • use some collection which stores value to handle
  • an approach inspired by Chain-of-responsibility pattern

Let's start from collection which stores value to handle

At first, we need our class with properties to be filled:

public class BigObject
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
}

Then this is our class which will handle all your properties:

public class BigObjectHandler
{
    Dictionary<string, Action> _handlerByproperty = new ();
    BigObject _bigObject;

    public BigObjectHandler(BigObject bigObject)
    {
        _bigObject = bigObject;
        _handlerByproperty.Add("A", GetDataA);
        _handlerByproperty.Add("B", GetDataB);
        _handlerByproperty.Add("C", GetDataC);
    }

    public void Handle(string propertyName) => 
        _handlerByproperty[propertyName].Invoke();


    private void GetDataA()
    {
        _bigObject.A = 1; // sourceServiceA.GetData();
    } 

    private void GetDataB()
    { 
        _bigObject.B = 1; // sourceServiceA.GetData();
    }

    private void GetDataC() 
    {
        _bigObject.C = 1; // sourceServiceA.GetData();
    }
}

And then you can call the above code like this:

IEnumerable<string> propertiesToFill = new List<string> { "A", "B" };
BigObject bigObject = new ();
BigObjectHandler bigObjectMapHandler = new (bigObject);

foreach (var propertyToFill in propertiesToFill)
{
    bigObjectMapHandler.Handle(propertyToFill);
}

OUTPUT:

A = 1
B = 1

Chain-of-responsibility pattern

If you have many if else statements, then you can try to use "Chain-of-responsibility pattern". As wiki says:

the chain-of-responsibility pattern is a behavioral design pattern consisting of a source of command objects and a series of processing objects. Each processing object contains logic that defines the types of command objects that it can handle; the rest are passed to the next processing object in the chain. A mechanism also exists for adding new processing objects to the end of this chain

However, we will not stop execution if some of condition is met. Let me show an example.

At first, we need some abstraction of handler:

public abstract class BigObjectHandler
{
    private BigObjectHandler _nextBigObjectHandler;

    public void SetSuccessor(BigObjectHandler bigObjectHandler)
    {
        _nextBigObjectHandler = bigObjectHandler;
    }

    public virtual BigObject Execute(BigObject bigObject,
        BigObjectFilter parameter)
    {
        if (_nextBigObjectHandler != null)
            return _nextBigObjectHandler.Execute(bigObject, parameter);

        return bigObject;
    }
}

Then we need concrete implemenatation of these handlers for your properties. This properties will be filled by your sourceServiceX.GetData():

public class BigObjectAHandler : BigObjectHandler
{
    public override BigObject Execute(BigObject bigObject, BigObjectFilter filter)
    {
        if (filter.FillA)
        {
            bigObject.A = 1; // sourceServiceA.GetData();
        }

        return base.Execute(bigObject, filter);
    }
}

And:

public class BigObjectBHandler : BigObjectHandler
{
    public override BigObject Execute(BigObject bigObject, BigObjectFilter filter)
    {
        if (filter.FillB)
        {
            bigObject.B = 2; // sourceServiceB.GetData();
        }

        return base.Execute(bigObject, filter);
    }
}

And:

public class BigObjectCHandler : BigObjectHandler
{
    public override BigObject Execute(BigObject bigObject, BigObjectFilter filter)
    {
        if (filter.FillC)
        {
            bigObject.C = 3; // sourceServiceC.GetData();
        }

        return base.Execute(bigObject, filter);
    }
}

And these are object with data:

public class BigObject
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
}

And some filter which will contain settings of what property should be filled:

public class BigObjectFilter
{
    public bool FillA { get; set; } = true;
    public bool FillB { get; set; }
    public bool FillC { get; set; }
}

And then we can call the above code like this:

BigObjectHandler chain = new BigObjectAHandler();
BigObjectHandler objectBHandler = new BigObjectBHandler();
BigObjectHandler objectCHandler = new BigObjectCHandler();

chain.SetSuccessor(objectBHandler);
objectBHandler.SetSuccessor(objectCHandler);

BigObjectFilter bigObjectFilter = new BigObjectFilter();
bigObjectFilter.FillA = true;

BigObject vehicle = chain.Execute(new BigObject(), bigObjectFilter); // A = 1   

It can be seen after code execution that onle property A is handled. Output is:

A = 1
B = 1

Upvotes: 2

Peter Csala
Peter Csala

Reputation: 22849

For the sake of simplicity let's assume that TypeA, TypeB and TypeC are int?.

We can define a command class for the BigObject with the following constructors:

class BigObjectCommand
{
    private readonly Func<BigObjectFilter, bool> canExecute;
    private readonly Action<BigObject>? executeWithoutParam;
    private readonly Action<int, BigObject>? executeWithParam;
    private readonly Expression<Func<BigObject, int?>>? dependsOn;

    public BigObjectCommand(Func<BigObjectFilter, bool> canExecute, Action<BigObject> execute)
    {
        this.canExecute = canExecute;
        this.executeWithoutParam = execute;
    }

    public BigObjectCommand(Func<BigObjectFilter, bool> canExecute, Action<int, BigObject> execute, Expression<Func<BigObject, int?>> dependsOn)
    {
        this.canExecute = canExecute;
        this.executeWithParam = execute;
        this.dependsOn = dependsOn;
    }
}
  • The first constructor will be used to cover the DataA and DataC properties' initialization
  • The second constructor will be used to cover the initialization of DataB property

Now, we can define an Evaluate method to implement the core logic

public void Evaluate(BigObjectFilter filter, BigObject context)
{
    if (!canExecute(filter))
        return; //or throw exception

    if (executeWithoutParam is not null)
    {
        executeWithoutParam(context);
        return;
    }

    var input = dependsOn!.Compile()(context);
    if (!input.HasValue)
        return; //or throw exception

    executeWithParam!(input.Value, context);
}
  • If the condition fails we don't do the assignment
  • If the assignment does not require any input then we simply execute it
  • If the assignment depends on an input then we check whether it is populated or not and depending on the result we may or may not execute the assignment

With these in our hand the GetModel can be implemented like this:

private readonly List<BigObjectCommand> Commands;

public DataService()
{
    Commands = new()
    {
        new (filter => filter.FillDataA, (m) => m.DataA = sourceServiceA.GetData()),
        new (filter => filter.FillDataB, (i, m) => m.DataB = sourceServiceB.GetData(i), m => m.DataA),
        new (filter => filter.FillDataC, (m) => m.DataC = sourceServiceC.GetData()),
    };
}

public BigObject GetModel(BigObjectFilter filter)
{
    var model = new BigObject();

    foreach (var command in Commands)
    {
        command.Evaluate(filter, model);
    }

    return model;
}

This solution is far from perfect I just wanted to share with you the basic idea how to apply the Command pattern for your problem.


Here you can a find a working example. By changing the FillDataXYZ values you can see how the Evaluate works in practice.

Upvotes: 2

Related Questions