3

I have a data structure which has an add function. When the user instantiates a new data structure object, she can specify an algorithm which will be executed each time the add function is called and alters the newly inserted value based on the previous values in the data structure. The algorithm needs to have access to all the data in the data structure.

Thus the data structure needs to store an algorithm object (since this is called each time the add is executed). On the other hand the algorithm needs access the data structure, and thus needs to store the data structure object.

I think situations where class A has an instance of class B and class B has an instance of class A are usually undesirable.

Does anybody have an idea how to solve this recurrence here?

2
  • 1
    Don't make the datastructure responsible for executing the algorithm on itself. Create a container that stores the datastructure and the algorithm and makes sure the algorithm is executed everytime the container is told to add something to the datastructure.
    – valenterry
    Jul 9, 2015 at 14:59
  • Recommended reading: strategy pattern.
    – user22815
    Jul 9, 2015 at 20:39

5 Answers 5

2

One way to approach this problem without extending the tiers of your system is to implement the Command and Strategy patterns together in your design. Encapsulate the actionable method (in your case, "add") as a Command and decorate it with a Strategy that encapsulates your algorithm.

Inside the Execute of your AbstractCommand, execute a virtual OnAfterExecute() method, which is where you state that you want your Algorithm to process. To tie the patterns together, conceptualize the responsibilities of your Context and of your Invoker as being the same.

Untested code. Typed by hand from memory.

public interface ICommand
{
    void OnBeforeExecute();
    void OnExecute();
    void OnAfterExecute();
    void Execute();
}
public abstract class Command : ICommand
{
    protected IStrategy Algorithm{ get;set; }
    public Command(IStrategy algorithm)
    {
      this.Algorithm = algorithm;
    }
    public virtual void OnBeforeExecute(){};
    public virtual void OnExecute(){};
    public virtual void OnAfterExecute(){};
    public virtual void Execute()
    {
       OnBeforeExecute();
       OnExecute();
       OnAfterExecute();    
    };
}

public class AddCommand : Command
{
    private IPersistenceService PersistenceService {get;set;}
    public AddCommand(IStrategy algorithm, IPersistenceService persistenceService) :base(algorithm)
    {
       this.PersistenceService = persistenceService;
    }
    public virtual override OnExecute()
    {
       this.PersistenceService.ExecuteAddStuffs();
    }
    public virtual override OnAfterExecute()
    {
       this.Algorithm.Process();
    }
}

public interface IStrategy
{
    void Process();
}

public class HighPassFilterStrategy : IStrategy
{
    private IPersistenceService PersistenceService { get;set; }
    public void Process()
    {
       //Execute your HighPassFilter logic.
    }
}

//DomainProcess is your InvokerContext
//As an alternative design consideration, you could move the virtual 'On' methods
//up into DomainProcess and decouple the strategy from the command.
public class DomainProcess
{
    public ICommand Command{ get; protected set;}
    public DomainProcess(IStrategy strategy, ICommand command)
    {
       command.Algorithm = strategy;
       this.Command = command;
    }

    public void Run()
    {
       Command.Execute();
    }
}

public class YourDataStructure
{
   public string Foo {get;set;}
   public string Bar {get;set;}

   //List of DomainProcess. Won't accept angle brackets.
   //List"left angle bracket"DomainProcess"right angle bracket"
    private List DomainProcesses { get;set; }

    public DataStructure(List workflows)
    {
       this.DomainProcesses = workflows;

       //alternative, but indicative of poor design. **Objects should not
       //construct their own dependencies.**
       //--------------------------------------------------------- 
       //I'm including this so that I don't have to design all the way up to
       //Composition Root for this illustration.
       //---------------------------------------------------------
       if(!this.DomainProcesses.Any(process => process.Name.Equals("Add"))
       {
          var algorithm = new HighPassFilterStrategy();
          var command = new AddCommand();
          var add = new DomainProcess(algorithm, command);
          this.DomainProcesses.Add(add);
       }
    }

    public void AddRuntimeOperation(string name, ICommand command, IStrategy algorithm)
    {
       DomainProcess process = new DomainProcess(algorithm, command);
       process.Name = name;

       DomainProcesses.Add(process);
    }
    public void Add()
    {
       var add = this.DomainProcesses.Where(operation => operation.Name.Equals("Add'")).SingleOrDefault();

       add.Run();
    }
}

Your structure that currently has an Add method on it could instead encapsulate a DomainProcess called Add

  public void Add()
  {
    DomainProcess.Run();
  }

The OnBeforeExecute, OnExecute and OnAfterExecute methods are colloquially known as aspects.

If you are going to use an object hierarchy in your design, aspects should serve as your primary extension points of that hierarchy.

By creating the default implementation of Execute as an aggregation of these 3 aspects, you can very easily specify inside your Algorithms that you want them to be processed Before or After your main operation is executed.

More specific information about your requirements might change the recommendation.

3
  • Too much jargon IMO, and some of it wrong. I'm familiar with " object relational impedance mismatch", famous Ted Neward Vietnam analogy here and this is not it. Nowhere does OP talks about any relational database, let alone any database at all.
    – user949300
    Jul 23, 2015 at 15:04
  • @user949300 re:[errors] If you feel that my answer contains factual errors, please enumerate them and explain your concerns; I will evaluate them and produce corrections where necessary. The design I have detailed will solve the problem that was posed; is extensible; does not require double dispatch; should be very easy to understand; and honors interface segregation,dependency inversion, and the single responsibility principle Jul 23, 2015 at 18:01
  • @user949300 re:[jargon]. The mention of the ORIM was parenthetical. The design that I posed does not rely on any particular data structure. Persistence concerns are abstracted away and data is assumed to be accessible through PersistenceService. At any rate, the storage mathematics of the data are irrelevant to the problem at hand. I'm not quite sure what other terms you are considering jargon for your "too much" attribution so I can't really respond. I feel that I provided context for everything I said (except for the ORIM remark, which I have taken out) Jul 23, 2015 at 18:16
3

The data structure should contain a link to the algorithm, since it has exactly one algorithm. Right? (as I understand the requirements)

But the same algorithm, say, "normalize to the average" could work on many different instances of the data structure. So it should not contain/store an instance of the data structure. Instead, its method should take the datastructure as an additional argument. e.g.

calculate(newValue, existingDataStructure);

Then, your datastructure's add() method calls

tweakedValue = this.algorithm.calculate(newValue, this);
this.add(tweakedValue);
1

If you have a cyclic dependency like this, this hints at one of two problems:

  • the two objects should really be one object
  • the two objects should really be three objects (with the inter-related parts extracted into the third)

What you have here, is highly unusual. Adding an element to a data structure is one of the primary responsibilities of a data structure, outsourcing that to the client, especially in a way that requires privileged access to the data structure's private internals, just seems wrong somehow.

This is very different from, say, a SortedSet which needs both an Ordering and an Equality algorithm, but neither of those two algorithms needs to know anything about the Set, it only needs to know how to compare two elements. Or a HashMap which needs a Hash algorithm that can hash a single element. That's the typical case for a data structure parameterized by an algorithm.

1

I have a data structure which has an add function. When the user instantiates a new data structure object, she can specify an algorithm which will be executed each time the add function is called and alters the newly inserted value based on the previous values in the data structure. The algorithm needs to have access to all the data in the data structure

This is not a good idea and besides makes not really sense.

If I get you correctly, you want to create an object which contains some elements e1, e2, e3, .... At a given point in time, you want to do a transformation of the elements so far. At a later point, you want to do another transformation on the collection as a whole.

There is no need to do a transformation in between, since it makes no difference whether the collection stores the values plain vanilla or transformed. At a given point, you need to read or change the transformed values: this is the right point to do a transformation of the colletion (so far) as a whole.

If that is, what you want, this would be a perfect use-case for the visitor pattern: In case, you have have Python at hand or do not mind installing it, here is a nice example how to set up the visitor pattern

#!/usr/bin/env python3


class Container:
    def __init__(self, values):
        self._values = values

    def transform(self, transformator):
        self._values = transformator.transform(self._values)

    def accept(self, visitor):
        visitor.visit(self)


class Visitor:
    pass


class Transformator(Visitor):
    pass


class DoublingVisitor(Transformator):
    def visit(self, container):
        container.transform(self)

    def transform(self, values):
        return list(map(lambda x: 2*x, values))


def main():
    container = Container([1, 2, 3])
    doublingVisitor = DoublingVisitor()
    container.accept(doublingVisitor)
    print(container._values)

if __name__ == '__main__':
    main()

Anyways, the python code is easy to understand. The point is, you need one class which I called (lacking of a better name) Container, which accepts a Visitor. On the other hand, you need an object derived from Visitor, which holds as a Transformator the algorithm (which was the term you used). The Visitor needs one method visit, which has one parameter (the object, which it visits). That is the whole magic.

While visiting, the visitor calls the transform method on the object to visit, which in turn receives the Visitor / Transformator who implements a transform method. That's it.

Every time, you want to do something with your collection, you could accept another Visitor.

0
0

Classes A and B should not have-a instance of each other; that way lies madness.

You make no mention of what the post-add algorithms do, but since they must have access to the instance of A, they need to be methods of A, ignoring troublesome constructions like friend functions.

class A:
  member x
  member y

  constructor(x, y, post_add_func(pointer or reference or name)

  method add()
      do_add_stuff
      call_post_add_func

  method post_add_1()
  method post_add_2()

This allows an "algorithm" to be bound to an instance of A, thus giving it full access to the members and methods of A.

I don't understand what the expected purpose of class B is, I suspect it was just a container for "algorithms" and will add unnecessary complexity. B probably doesn't need to exist.

1
  • "You make no mention of what the post-add algorithms do". You are right, sorry, the algorithm is a filter (lowPass / Highpass...) it basically alters the value of the element to be inserted based on the previous values in the data structure
    – user695652
    Jul 9, 2015 at 15:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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