1

I have an object that describes a two-dimensional Graph:

class Graph {
  var points; // Array of points
}

Now I need to find some complex patterns in this graph, like (but more complex):

  • Find two consecutive points with the biggest differences.
  • Calculate for each point the average of the X previous points.

I'm wondering where this logic should be:

  1. In Graph class to connect between the data to its logic.
  2. The graph contains only the data and function to manage it, and the logic will be in other class(es) with static functions that will get the Graph as an argument.
  3. Graph function will provide an API that get instance of business logic class.

    class Graph {
        var points; // Array of points
        public function findHighestDifferent(instance:HighestDiffFinder) {
            var filteredPoints = points.filter(...);
            instance.find(filteredPoints);
        }
    }
    
  4. A better way :)

1
  • Thank you for the clarification, THe graph is a two-dimensional graph, each point has X and Y.
    – Michael
    Jun 24, 2018 at 14:44

4 Answers 4

13

"Graph" is an ambigous term, refering to different kinds of mathematical objects.

But whatever kind of graph you have in mind, each of these objects is an abstraction on its own, with certain operations which are essential to define the abstraction. For example, if you think of a graph in the sense of a line chart, the typical operations could be to

  • add some new points which are automatically inserted in a way to keep the points ordered according to their X coordinates

  • get out the points in X coordinate order

  • maybe delete or replace some points

  • maybe operations to calculate the intermediate lines between two neighboured points.

These are the public operations which should be part of the Graph class itself - a graph which is just a container for a list of points, as shown in your example, without a public API which forms the abstraction, is quite pointless. On the other hand, every operation which is not part of that abstraction is better placed elsewhere.

Where exactly, depends. It depends on how the graph will be used in your system, which other places in your system are or could be available, and on the individual operation. Sometimes a class on its own is fine. Sometimes a static function is ok. Sometimes a function inside another class is sufficient, if that other class is the one-and-only place in the system where the operation is needed. And sometimes it is ok to make an exception from the rule, stay pragmatic and place the function directly in the Graph class, just because you currently don't have a better place.

Note there is no hard-and-fast rule saying "this or that place is better", this is to some degree opinionated. Try to follow the SOLID principle, try to create coherent abstractions with disjoint responsibilities, and refactor as the program evolves. Often, a very simple operation, simple enough to be just one functions, starts to become more complex over time, which makes it necessary to refactor the logic of the function into many smaller functions which will then become refactored to a class on their own.

0
2

Separation of concern

The key design principle here is the separation of concerns:

  • On one side you have graphs, which by definition should provide for nodes, edges and the navigation between them.
  • On the other side you have complex algorithms that work on graphs, for example for exploring or annotating them or building paths.

The separation of concerns suggests to implement the complex algorithms independently of the graph (more precisely, it's a separation between different layers, so it would be a "horizontal separation"). Of course, this requires to first define a proper interface for Graph. Today, it's a collection of Points. But tomorrow, you may well let it evolve as a 2D adjacency matrix, or a table of adjacency lists, or any other suitable data structure:

  • If your algorithms would be embedded in Graph, they might use knowledge about Graph's internal structure. So changing Graph, might require change of the algorithms. This would make your system difficult to maintain.
  • Keeping your algorithms independent of the Graph internals (i.e. using only the public interface of Graph) would on contrary create a much more robust architecture. You may change the algorithm independently of the graph, and vis-versa.
  • Independence has another obvious advantage: If tomorrow one of your complex algorithm could help you when developing the next generation GPS software, it would be a piece of cake for you to implement a Graph adapter for the GPS map and reuse your code.

Best option

Your option 2 seems to implement separation of concerns. So this is the way to follow.

You could also think to encapsulate the algorithms in some class. This would for example have the advantage of allowing you to derive algorithms if some of them have common parts.

You may consider using the strategy pattern to make some variations with one implementation using several different run-time strategies (e.g. findHighestDifference and findLowestDifference).

I don't know your language, but you could be interested in having a look at the design of the boost graph library, which is very elegant and comprehensive in all these regards.

6
  • 1
    +1 This should be the answer. Keep the Graph and the statistics for that graph separate. Jun 24, 2018 at 18:59
  • Thanks. I'd even add that this answer should remain valid even if OP would use the term Graph to mean Polygon: the many ways and purposes of doing statistics and analysis of the point sequence call for separation of concerns for exactly the same reasons.
    – Christophe
    Jun 24, 2018 at 20:36
  • It depends on the use-case. Separating logic from its data is usually not a good idea though, and it is not what SoC is about. If you have an algorithm on the graph data, then it is an algorithm concerning that data. It is not "another" concern, therefore you should not separate them, barring other architectural constraints or requirements. Jun 25, 2018 at 10:42
  • @RobertBräutigam I do not recommend to separate the logic from the data. I recommend to separate a higher level logic from a lower level logic, and not to mix very different levels of abstraction in the same class.
    – Christophe
    Jun 25, 2018 at 10:51
  • @Christophe Ok, I agree with that. However, whether the said functionality is "higher level" depends on what the application is about. You don't have to build a generic Graph library in your application, if the only requirement is to find two points with the biggest difference, because in the this case, the whole purpose of the class Graph is to find these points. In this case it should not be separated. Jun 25, 2018 at 11:30
1

You have a hard time answering your question because it is lacking context. All your suggestions provide a way to place functionality into a function/method. Merely having the necessity for this functionality doesn't help finding a good way to place it.

  1. In Graph class to connect between the data to its logic.

    • yes so you can operate on private data in the Graph object
    • no because you don't want to turn this thing into a god object
  2. the logic will be in other class(es) with static functions that will get the Graph as an argument.

    • yes so the functionality is more modular, can be developped/tested individually
    • no because both functions you mentioned

      Find two consecutive points with the biggest differences.

      Calculate for each point the average of the X previous points.

      have a state of their own which you might want to save/load or make user-configurable, so those functions shouldn't be static.

Stop thinking bottom-up from your data class when it comes to implementing functionality. You will find the right place to implement your functionality when you look at it from a top-down perspective.

  • What data in what scope does the function operate on? When you search for the consecutive points for example, at that place in your code, do you know that it should look for two (and not three or four) consecutive points? Or ist that some information provided by the user in a TextBox of a GUI, which you rather not want to or cannot evaluate at that place in your code?
  • How complex is the functionality? Should it run asynchronously? On a different thread? On a different machine?

Plus the aspects of writing/deploying/maintaining the code.

  • Will several developpers implement the functions? Managing changes to a single file by multiple users is possible, but can lead to problems.
  • Should each function be tested individually?
  • Do you want to add more functions later on to an already deployed earlier version of the software?
0
1

The examples you gave are not "complex business logic". They are fairly basic questions about a "Graph" (which most people would call a list of points, graphs have nodes and arcs). The implementation of these methods (e.g. "closest consecutive pair") would not change due to changing business requirements. So you could reasonably put that code in the Graph class. That would be my first choice. This is good enough for a class used by a single application. If there were some desire to reuse that code in multiple artifacts, then I would think harder.

"Complex business logic" usually comes from real-world constraints that must be respected by the solution, and that are subject to change. For example, a program that schedules commercial drivers or aircraft crew must follow regulations that specify a maximum duty time.

This is a very common sort of question on SE. About 99% of the time the answer is "It doesn't matter.", because the code in question has a single caller and the decision is easily changed. Put the logic wherever it's convenient and does not create a circular dependency between classes (because classes with circular dependencies cannot be unit-tested). When it becomes inconvenient, then refactor. You can do that because you have a good set of unit tests and integration tests, right?

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.