Matching objects of different types without violating SOLID/minimizing dependencies

I have two separate inheritance hierarchies that represent objects that can be “matched” to each other to consitute a FooBarMatch (which contains references to the matched FooBase and Bar partners):

                          _____________
                         | FooBarMatch |
                         |-------------|
                    ---<>|FooBase      |<>---
                   |     |Bar          |     |
                   |     |_____________|     |
                   |                         |
                   |                         |
             ______|_______          ________|_____              
            | <<abstract>> |        |      Bar     |
            |    FooBase   |        |______________|
            |______________|
                   ^
                   |
         --------------------
 _______|______       _______|______
|     FooA     |     |      FooB    |
|______________|     |______________|

The actual logic for determining whether a FooBase is a match for a Bar depends on what subtype FooBase ends up being (if FooBase is a FooA we need to execute different logic than if it’s a FooB). But all FooBase objects can be compared to a Bar to see if they are a match.

I am currently handling this as follows: by creating an IBarMatchable interface with an IsMatch(Bar) : bool method, which is implemented by an abstract method in FooBase. FooA and FooB then contain different implementations for IsMatch(Bar). It looks like this:

                          _____________
                         | FooBarMatch |
                         |-------------|
 _______________    ---<>|FooBase      |<>---
| IBarMatchable |  |     |Bar          |     |
|---------------|  |     |_____________|     |
| IsMatch(Bar)  |  |                         |
|_______________|  |                         |
        ^    ______|_______          ________|_____              
        |   | <<abstract>> |        |      Bar     |
         ---|    FooBase   |        |______________|
            |--------------|
            |*IsMatch(Bar)*|
            |______________|
                   ^
                   |
         --------------------
 _______|______       _______|______
|     FooA     |     |      FooB    |
|--------------|     |--------------|
|IsMatch(Bar)  |     |IsMatch(Bar)  |
|______________|     |______________|

Now to determine matches we can only concern ourselves with IBarMatchable. This seems to accomplish what I want, however I am curious if this is “good design” or not– I’m most concerned that it now seems to

  1. Introduces a dependency on Bar to FooBase to support functionality that is ultimately outside of FooBase (FooBase itself doesn’t care about matching at all). FooBase otherwise has nothing to do with Bar.

  2. Introduces a second “reason to change” (i.e. the way we define a match changes) to FooBase— so potentially a SRP violation.

Does this violate SOLID? Is there a better way to design this that does not require FooBase to depend on Bar?

9

There are various ways to do this:

  • As mentioned in the comments: an extension method could accomplish the same thing
  • If you want to continue with the interface approach but are concerned about FooBase having a dependency on Bar, you could implement the interface in FooA and FooB so that only they have a dependency on Bar (which is an actual dependency). Then you just work on the interface instead of on the base-class.

Either way, a lot of it depends on your actual model. In a case like this, it’s better to mention the actual model rather than use foo and bar

Put the dependency in FooBarMatch

Very similar to IEnumerable.GetEnumerator, implement a IMatcher in Foo and Bar that returns a matcher object – with a IMatchable.GetMatcher method (and others as needed).

IMatcher.MatchMeUP will take a counterpart object. An IMatcher object will necessarily need to know intimate details to make matches. The FooBarMatch object needs to know what, when, how to call IMatcher API to make matches and do something with the results. Foo and Bar don’t even need to be aware of the existence of any other classes.

However the devil is in the details and there will be design decisions as to how the Foo & Bar “native” public API and their IMatcher public API is manipulated in FooBarMatch to make the match-making happen.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *