0

I have recently encountered this problematic method (minimal reproducible sample in C++ but this question aims to be language agnostic past the syntax):

void MyObject::twice_bind_cycle() {
    _bind1();
    _bind2();
    _activate();
    _unbind2();
    _unbind1();
}

This entire method interacts with an API as do the binding/unbinding methods themselves. It is OpenGL, but since this question is more about design decisions, I think it belongs on here at Software Engineering SE. I have generalized the problem for those unfamiliar with its usage.

I find there is tight coupling between the pairs of binding and unbinding methods. In order to perform activate() correctly, _bind1() must run before _bind2() and _unbind2() before _unbind1(), which forms an implicit interdependence of state within twice_bind_cycle() between the binds and unbinds. This would be acceptable in many cases---in fact it appears I could merge the functions if I were not reusing them elsewhere in say, bind_cycle_1() and bind_cycle_2().

A problem arises when I want to move the inner _bind2() and _unbind2() functions from the MyObject class and into their own. Their implementations, which aren't shown here for generalization, strongly suggest to me they ought to belong to their own object. They do not rely on any object state, but they do control a critical part of the API and their semantics with MyObject are becoming increasingly incompatible as the project grows. Further I would like to write several new methods that call _bind2() and _unbind2() that should not belong to MyObject. Finally, I predict several issues that could occur if I do not decouple them soon and continue work as normal.

I am now concerned about how the twice_bind_cycle() is supposed to be re-implemented and still function correctly in the provided order, while attempting to preserve its approach to encapsulation.

Some approaches I have considered:

  1. Create the other object as proposed and use an intermediate class that contains both objects via composition. Move twice_bind_cycle() to this class. Okay, but activate() requires access to private variables inside MyObject and the binding/unbinding methods are private. I am willing to break encapsulation here and provide the getters necessary for activate() because I find the attributes it requires to be insignificant to other objects. But making the binding functions public is a no-go because they are a major part of the objects' internal implementation.

  2. Use a command pattern. From the brief research I made, it appears like making the methods public just with extra steps. I have little experience with this pattern so I understand this is a limited view.

  3. Make the binding/unbinding methods protected and inherit from both of the new classes (C++). Inheritance has its own problems and especially multiple inheritance. Although I am open to suggestions, I'd prefer to avoid that route in favor of composition. I am fine with interfaces, though.

  4. Use the friend keyword (C++ only AFAIK). Make an intermediate class and make it a friend in the MyObject and MyOtherObject classes. However, there is still transitive coupling via the intermediary and if I wanted to use either object independently, that association with it will be there.

What should I do? Are there any other options? It seems inevitable that a compromise must be made somewhere. Also, am I being pedantic about the level of encapsulation and coupling within my program? Do I have enough reason to be?

Thanks

youngson
  • 17
  • 4
  • 1
    What about a solution that lets you write like this: `bond1(bond2(activateMe))`? – candied_orange Apr 20 '23 at 14:51
  • I can't. These are API calls. Although the method does behave like a stack. – youngson Apr 20 '23 at 15:05
  • Unless you mean wrapping a function around a bind pair and accepting an arbitrary function as an argument and calling it in the middle? Interesting approach – youngson Apr 20 '23 at 15:13
  • 1
    @soung I do. It's called the "hole in the middle" pattern. It's a specific version of the decorator pattern. It lets you keep your binds and unbinds paired while applying them where ever. – candied_orange Apr 20 '23 at 15:21
  • Can be done with objects or first order functions. – candied_orange Apr 20 '23 at 15:27
  • I've used first order functions before. Didn't know they were called that. Objects might be more suitable because I would need some polymorphism to make the first and second binding pair work together because they do functionally different things – youngson Apr 20 '23 at 15:32
  • 1
    Your unbind operation sounds like a perfect fit for RAII/context managers/try-with-resource/defer or whatever it's called in your language. – amon Apr 20 '23 at 15:33
  • @amon right, destructors, close, finally, etc. can call the paired unbind without `activate` needing to know about them. – candied_orange Apr 20 '23 at 15:46
  • the question seems too general. As it looks, this could be sound functional decomposition, or even on purpose a template method design pattern if _xxx() are virtual. Moreover there is no single best approach: extracting some pairs could require exposing internal details or increasing complexity (e.g. injecting a factory for RAII objects). More focus is needed here – Christophe Apr 20 '23 at 16:12
  • Switch to OpenGL Direct State Access and there is no more binding. Alternatively, don't unbind things because it's a waste of time. – user253751 Apr 21 '23 at 13:42
  • Don't think about encapsulation as "I have this function that is a complete black box and when I call it a whole bunch of magic happens and there are some desirable side effects". That's not a useful form of encapsulation. Such a function has no externally visible behavior (in terms of what you can infer in isolation, just looking at parameters and return types). It's OK to break it down, and establish a more explicit contract between different parts (the "behavior" is in the contract, it's not about what the function does line by line). Encapsulate the internals of each part. – Filip Milovanović Apr 21 '23 at 17:19
  • @FilipMilovanović I know what you mean and I do always try to return something and act apon the return value. I find void returns contradictory to the OO philosophy. But here I am working with what I am given. OpenGL has pushed me into black box thinking because the functions don't return anything. – youngson Apr 21 '23 at 22:41
  • @syoung void returns are [not contradictory to OO](https://swe.stackexchange.com/q/365829). They are contradictory to functional programming's referential transparency. But if you insist on using return rather than an output port you have to send control back to where it came from. You can't send it on to the next thing, whatever that was configured to be. You can't face an interface from the outside, you have to sneak back into whatever called you, having no idea what that is, or whatever protocol it prefers to speak. So you just send back your internal state since you don't know better. – candied_orange Apr 27 '23 at 18:21

2 Answers2

1

You are using C++. You can use lambdas like so:

template<typename Middle>
void MyObject::bind1(Middle m) {
    _bind1();
    m();
    _unbind1();
}

template<typename Middle>
void MyObject::bind2(Middle m) {
    _bind2();
    m();
    _unbind2();
}

void MyObject::twice_bind_cycle() {
    bind1([&]() {
        bind2([&]() {
            _activate();
        });
    });
}

It is expected that the compiler inlines everything and reduces it to the you have shown in the question. This is a reasonable expectation, especially since the function templates must be defined in the header file so the compiler can see their definitions everywhere they are called.

user253751
  • 4,864
  • 3
  • 20
  • 27
  • I would accept this answer as it achieves the refactoring I was looking for but I asked for additional discussion that doesn't make this a StackOverflow question – youngson Apr 22 '23 at 08:57
1

Specifically for OpenGL 4.5 (2014) or later, you can use Direct State Access functions which do not require binding.

Note: by now this should be supported on all graphics cards anyone cares about. It is also available as an extension which is supported on some very old cards such as GTX 260 and GeForce 8300 (with updated drivers), a card from 2006 that I'm sure you don't care about by now.

user253751
  • 4,864
  • 3
  • 20
  • 27