How to abstract a dependency of subject under test?
The short answer here is to use an interface and set your dependency as the interface type instead of the concrete class that implements said interface.
The reason is that an interface innately allows you to mock its implementation, because you can just create a different implementation of the same interface, one which mocks the behavior however you want it to. You can also look at mock libraries like NSubstitute to dynamically create mocks instead of doing it formally via a mocked class definition.
Your implementation of MenuData
is atypical. Generally, the dependcies go via the constructor, the ID goes via the method. You're doing it the other way around. It's unclear to me whether this is an intentional design decision or an oversight.
I'm keeping the above direct answer short, because I want to instead pivot into discussing the blog post you linked and why I think it's a bad source of advice for you to rely on.
The conflict in your question (i.e. the thing that's driving you to ask it here) seems to be based on the linked blog post and your struggle with implementing what it preaches, so I'm going to focus on the content in the blog post with which I disagree, to justify your struggles and highlight that the advice being given is sometimes misguided or lacking in nuance.
Personal opinion disclaimer: I really dislike the blog post. Not because it's totally wrong (it makes a few good points) but it's written with a tone of authority, engages in blanket statements, selectively curates its content to only include things that agree with the author's intent, and makes several rookie mistakes in the process which completely undercut its authoritative tone.
The rest of this answer will be a response to why the advice given in the blog post is inaccurate, misguided, dead wrong, or in some cases (in my opinion) intentionally doctored to support the content of the blog post.
"Program to an interface, not an implementation"
An interface is just a language construct.
That blog post falls flat on its face right out of the gates, as it conflates "program to an interface" to mean "program to an interface
" (i.e. the language construct). This is not the case, and undermines the entire point that the blog then goes on to make.
For the sake of completeness, in "program to an interface", "interface" refers to any public contract. The interface
construct is one of those, but it's not the only one. Any class' public content (methods, properties, even fields) also constitutes an interface.
When you subtract that undermined argument, what's left is a claim that using an interface alone does not sufficiently provide an abstraction. This is not incorrect, but it is not particularly insightful. Anything can be done badly and/or to a point of defeating its own purpose.
Examples of [thing] being done badly should not be used as argument why [thing] is bad; they're argument of why whoever did [thing] badly did a bad job. Spoiler alert: we will come back to this point several times.
However, as Uncle Bob points out, even an interface as simple as this seemingly innocuous rectangle ‘abstraction' contains potential dangers:
The Rectangle
problem suffers from implicit expectations about how it should operate. It hinges on an assumption that changing one property should never ever affect another, but this is simply not a hard rule in software development.
If you assume that all properties are independent of one another and cannot possibly impact one another, then you're thinking about DTOs, not logical models, which is an oddly (and selectively) restrictive interpretation of what your class (and its abstracted interface) could represent.
Yeah, interfaces don't make a lot of sense on DTOs, because they're dumb data containers that don't have any real logic to them, and LSP is a logic violation, not one of data storage structure.
But even if we humor the author that their implicit inference based on the interface's definition is inevitably the only possible interpretation (which it isn't), then the designer of the interface should be blamed for writing a bad interface. It's nonsensical to use this to argue against the very concept of interface
itself.
The problem stems from the fact that the operations have side effects. Invoking one operation changes the state of a seemingly unrelated piece of data. The more members we have, the greater the risk is
Notice here the introduction of "risk" as a description of this scenario. The author is inherently implying that side effects are universally bad.
If you're that averse to side effects as a blanket measure (which is fine, it's definitely one way to approach your design), then you should be using pure functions and immutable objects; at which point the Rectangle
example is rendered moot, as the entire objection hinges on changing values post-initialization.
So, pick a lane: either side effects are not all bad, or (if you think they are all bad) you should never have designed an interface that enforces mutability in its implementors. In either case, the proposed issue with the Rectangle
problem is not an accurate example.
Header interfaces [..] interfaces mechanically extracted from all members of a concrete class are poor abstractions.
Again we strike on the same issue where doing [thing] badly does not argue against [thing], it argues against the developer who badly did [thing].
For all the harping on about Liskov (the L in SOLID) in the previous chapter, the blog suddenly falls quiet on the Interface Segregation Principle (the I in SOLID), even though it targets this exact problem that the blog post is pointing out.
However, ISP inherently also justifies the use of interfaces (as long as they're propertly segregated), which is why I'm suspecting that the author very much avoided including ISP in the blog post.
Shallow interfaces
This section is egregiously badly written. Not grammatically, but in terms of the technical opinion that it tries to get across. It is so bad that it would be best skipped in its entirety when reading the blog post.
But let's dig in.
[..] because it doesn't recursively extract interfaces from the concrete types exposed by the extracted members.
First of all, that's not a complaint against the language, let alone its interface
construct; that's a complaint about an IDE automation that you think could have worked a bit differently. This is completely unrelated to the actual topic at hand. Judging a language (let alone a language-agnostic OOP paradigm) by one IDE's implementation of a code generation utility is patently moronic, and is one of the main reasons why I personally believe that the author of this blog post did not attempt to make a good faith argument.
Secondly, and more importantly, the vast majority of concrete types that get referenced in public method signatures and public properties are data containers (i.e. DTOs), where extracting an interface
is pointless as it contains no real data.
The assertion that all concrete types should be extracted at the same time is fraught with problems. From a UI perspective, it could cause a massive chain of window popups to extract an entire application's dependency graph. At the same time, a large majority of its windows would be entirely moot as they are DTOs, and the author's underlying argument here is that language paradigms are only as good as the IDE automations that get put on top of them.
At first glance this may look useful, but it isn't. Even though it's an interface, it's still tightly coupled to a specific object context. Not only does ObjectSet reference the Entity Framework, but the Posting class is defined by a very specific, auto-generated Entity context.
Neither of which is the task of the interface-extracting code generation tool. If the author is claiming that the direct consumer of PostingContext
should not have access to EF-dependent types, then PostingContext
(the class) should never have been built in a way to expose these EF types in the first place. The only reason the tool exposes these types in the interface is because the class had already exposed them to begin with, it simply followed suit with the class whose public interface it was explicitly told to copy.
This seems like a very cheap shot to seemingly support the blog post's narrative. The interface extraction tool only derives an interface
from how the class was designed. The author here designed a class' public interface badly, then set the extraction tool to mirror this in the interface
that it generates, and then the author turns around and blames the tool for producing a flawed interface
.
If that's not the pot calling the kettle black, I don't know what is.
Leaky Abstractions - Another way we can create problems for ourselves is when our interfaces leak implementation details.
Leaky interfaces are a design problem. Even without interfaces, your classes would be leaking the same way. This is wholly unrelated to the interface construct.
Exposing a ConnectionString property strongly indicates that the repository is implemented on top of a database
Yeah, again this is just a bad design, not an argument against the interface
construct.
More amusingly, the author seems to have a very restricted understanding of what connectionstring are actually used for. Message brokers/queues, caching services, email clients, IoT devices, microservice frameworks (e.g. Eureka), file storage (e.g. FTP), elastic search, and even the blockchain all make use of connectionstrings.
You shouldn't be exposing connectionstrings regardless of which technology uses them, so I'm questioning the author's argument that this design is somehow a good idea in the first place before we even think of introducing an interface or not.
Conclusion - In short, using interfaces in no way guarantees that we operate with appropriate abstractions. Thus, the proliferation of interfaces that typically follow from TDD or use of DI may not be the pure goodness we tend to believe.
"Conclusion - In short, using a seatbelt in no way guarantees that you will survive every single car crash. Thus, the proliferation of seatbelts that typically follow from legislation or ANCAP recommendations may not be the lifesaver we tend to believe"
I think the flawed logic is obvious here. The author thinks that a counterexample completely dismantles the core concept.
GoogleSheetsService
does GetMenuList actually require? If it calls them all (or maybe 80%), then go ahead, your approach is fine. If GetMenuList requires onlyGetDataFromSheet
, you could save the interface and use just aFunc
parameter instead.MenuData
is atypical. Generally, the dependcies go via the constructor, the ID goes via the method. You're doing it the other way around. It's unclear to me whether this is an intentional design decision or an oversight.