Now what happens if the dependency is not with respect to a regular API, but with a COM interface ?

Suppose a COM interface IControl is implemented by CControl, in a dll DLL1. Now DLL2 obtains this CControl indirectly (for example, calling IControl* m_control = CoCreateInstance(...) with clsid==CLSID_CControl and riid==IID_IControl), therefore using it as an instance of IControl. We want to add some new behavior to CControl. However, we can't change IControl since that would break the rules of COM - once you release an interface, it's sealed for life.

The sensible thing to do is to add a new interface, call it IControl2, with the new operations. IControl2 should extend IControl for simplicity (otherwise we need multiple inheritance), and the new CControl can implement IControl2. In DLL2, we have two options:

  1. Making m_control be an IControl2* and use the new methods directly
  2. Continue using m_control as an IControl*.

If we choose option 1, we have a servicing dependency: IControl2 is only implemented in the new version of DLL1. That's bad news.

If we go with option 2, we don't get the new methods...or do we?

Enter QueryInterface

QueryInterface, the basis of all of COM, can be used to save us from unnecessary dependencies. Instead of writing:

IControl2* m_control;
HRESULT hr = CoCreateInstance(CLSID_CControl, NULL, CLSCTX_FROM_DEFAULT_CONTEXT, IID_PPV_ARGS(&m_control));
...
m_control->MethodInIControl2(...);
We should write:
IControl* m_control;
HRESULT hr = CoCreateInstance(CLSID_CControl, NULL, CLSCTX_FROM_DEFAULT_CONTEXT, IID_PPV_ARGS(&m_control));
...
IControl2* pControl2;
hr = m_control->QueryInterface(IID_PPV_ARGS(&pControl2))
if (SUCCEEDED(hr))
{
    pControl2->MethodInIControl2(...);
}
else
{
    // do previous behavior
}

This way, if the new DLL1 is deployed, then CControl will implement IControl2 and we will be able to get the new behavior. Otherwise, nothing changes.