Replies: 15 comments
-
This proposal makes me uncomfortable, primarily because it appears to primarily assist in writing The interface is defined so tests can use a mock implementationThis scenario should be avoided. Mock implementations are difficult to write and debug and result in tests that are both fragile and do not reflect the behavior of production code. Use instances of the concrete object instead. If the concrete object is not able to run in a test environment, a new concrete implementation of the interface can be defined in test code to replace the original. Where mocking frameworks result in code that looks like a use of the interface, this approach results in code that looks like an implementation of the interface, which is much easier to locate and debug. The proposal doesn't appear to address the latter strategy, and could lead to confusion regarding it. In addition, the latter strategy is fairly well supported by the use of virtual methods in the original implementation. The interface is defined as part of an API boundaryThis scenario (and interface with one implementation) is acceptable, but for the sake of providing a stable API and communicating it is preferable to define the interface separately. One advantage provided by interfaces at API boundaries is external consumers should not need to be exposed to the implementation details in order to understand the interface. The proposal described here necessarily interleaves the two, adding complexity to a design intended to avoid the complexity. |
Beta Was this translation helpful? Give feedback.
-
Agreed. Having the interface separate forces thinking about the contract distinctly from the implementation. The interface is significantly more important than any concrete class that implements it and should be separate. Trying to modify that contract should require additional effort, not be as simple as just modifying the signature of some method on the class. |
Beta Was this translation helpful? Give feedback.
-
Thanks for the response @sharwell I agree that this wouldn't be as appropriate for interfaces defined as part of an API boundary - it's clearer if external consumers use But about your mock implementation point, I'm not sure I understand. I'm not suggesting manually writing mock implementations - isn't it common to unit test using a mocking framework like Moq? Moq, Rhinomoq, and other similar frameworks can't mock concrete classes. So currently we have two choices:
Are you suggesting that we should be doing the first one? I also feel that the fact that Resharper/Visual Studio's |
Beta Was this translation helpful? Give feedback.
-
@HaloFour you are absolutely right, for many interfaces - and those should continue to be written as they are right now. I'm not in any way suggesting that we move away from interfaces in general. They are invaluable for defining an externally-facing contract, when that's what you want to do. But very often, possibly even in the majority of .NET projects, classes are written for something like an ASP.NET stack, and are not open sourced, put on NuGet or otherwise externally consumed. Many of these classes will never be referenced outside of the solution they live in. Why would you want modifying the contract of those classes to require more effort than is necessary? Any "breaking" changes to the contract would be immediately caught by the compiler/intellisense, and quickly fixed by the developer. |
Beta Was this translation helpful? Give feedback.
-
@mmkal About 18 months ago the team I'm on faced a similar challenge to what you describe as the first option. The motivating factor was the use of mocks was causing us major problems:
We started by challenging the assertion that writing tests without mocks was difficult. Our sprints were already proving that mocks were a major barrier to maintaining code, so we just needed to find an approach that wasn't as hard. The approach we went with involved the following:
Our findings were overwhelmingly positive. We not only addressed the maintainability problem we were facing with mocks, but we also overcame other things:
Today we've essentially abandoned the use of mocks (while reserving the ability to use them if we ever find a case where they really are required). On the way to this decision, all of our coverage metrics increased, along with our agility and confidence in the test suite to reveal bugs before shipping. In addition our tests run notably faster (more than 20% improvement) and are now deterministic. |
Beta Was this translation helpful? Give feedback.
-
I recently performed an analysis on a codebase I'm working in; 70% of the interfaces had only 1 implementation, and ~20% had only 2 implementations. The redundant interfaces only complicate maintenance, while creating a false illusion of "extensibility". A bad abstraction wrapped in an interface is still a bad abstraction. |
Beta Was this translation helpful? Give feedback.
-
@YaakovDavis - interesting, I suspect this is pretty common. Do you have a shareable tool for getting these stats? @sharwell isn't what you're describing integration testing, not unit testing? If that philosophy works for your team that's great. But unit testing is still a thing. Plus, with Moq, for example, "finding and updating the mock implementation" would be as simple as finding/changing this line, in the test that is newly failing since the behaviour change: Mock<interface<GreetingPrinter>>()
.Setup(g => g.Greet())
.Throws<InvalidOperationException>(); I do understand your point, but this proposal, to my mind, would deal with a real problem that most .NET projects I've worked on suffer from: codebases bloated with interfaces that provide very little value. I completely respect your viewpoint, but my problem is "interfaces are taking up too much of my filesystem, time and mental energy". It'd be a little disappointing if the reason for this feature not going ahead was "you shouldn't be unit testing/using Moq in the first place" - even if that's a valid opinion. It's just unrealistic to expect unit testing to go away any time soon. |
Beta Was this translation helpful? Give feedback.
-
@mmkal |
Beta Was this translation helpful? Give feedback.
-
I'd add that beyond not adding value, they often take away value, by hiding information. Often times, the proper functioning of a system relies on a specific, singular, concrete implementation. In such cases, there's no point in hiding the implementation behind an interface. Hiding a singular artifact, is not abstraction, but obfuscation. |
Beta Was this translation helpful? Give feedback.
-
@YaakovDavis That depends heavily on your viewpoint. If you are on the team responsible for the singular concrete implementation, then that's correct. However, if you are on a team that simply consumes the implementation then the implementation details hurt in two ways:
|
Beta Was this translation helpful? Give feedback.
-
@sharwell Class/member visibility is a basic facility in enabling this. Interfaces are helpful when you have multiple implementations, or expect to have such multiplicity in the future for obvious reasons. In many codebases, they're used merely as pointless facades over concrete, singular implementations. |
Beta Was this translation helpful? Give feedback.
-
@mmkal I consider our tests (at least the ones I'm talking about here) to be unit tests. It helped to describe desired characteristics of individual tests, such as the following:
In order to meet these in our unit tests, we found it particularly valuable to structure the tests to mimic user actions as opposed to being focused on calling specific methods. Edit: If tests are structured in a way that related to behavior of a user or the system¹, it's much easier to communicate the testing strategy to a QA team, and it's much easier to write and verify regression tests that match reports made by users. ¹ File system permissions is one example of system behavior.
I agree. These days we only start with an interface if we're working on an API layer intended to expose stable functionality to unknown code in different components. Inside of the implementation of some functional component, we avoid interfaces. One premise of our design is any component can use the functionality exposed in the designated API of any other component at any point, and the implementation is not allowed to make assumptions about how the API is used aside from the explicitly-documented restrictions for the API methods (e.g. pre- and post-conditions). |
Beta Was this translation helpful? Give feedback.
-
@sharwell maybe it would be helpful to separate out the reasons interfaces are currently used:
For 1 and 2 - the current setup is pretty much fine. Here's one other useful real-world scenario this could be useful: When consuming an external library, the API is not always designed the way your team would like. As an example which has hit me IRL: the Microsoft Cognitive Services Emotion API. For whatever reason, the MS development team decided to make the |
Beta Was this translation helpful? Give feedback.
-
This could be easily accomplished by a source generator: [AutoInterface]
public partial class GreetingPrinter {
public void Greet(string person) => Console.WriteLine("Hello " + person);
} The generator could then generate the following: public interface IGreetingPrinter {
void Greet(string person);
}
public partial class GreetingPrinter : IGreetingPrinter { } |
Beta Was this translation helpful? Give feedback.
-
@HaloFour that is true, and something I have looked into - but the same could be said of many language features. I suppose I'll have to settle for that unless others' opinions somehow change. |
Beta Was this translation helpful? Give feedback.
-
Interfaces are fine when they represent a contract that could be implemented in many ways. But very often, they will be used to wrap a single concrete class, simply because interfaces work better with Dependency Injection and mocking frameworks.
You'll often see code like this:
This works, but having to create the
IGreetingPrinter
interface is an unnecessary overhead. People rely on their IDE to do it (extract interface), and it's not always simple to change the signature of a public method without tools like Resharper. Sometimes parameter names can get out of sync, or methods are added to the class but not the interface. In cases like this, it would be preferable to bypass theIGreetingPrinter
definition completely, and use theinterface
keyword to ask the compiler to generate us an interface with the concrete class's public properties and methods:The compiler does the boring, easily automate-able work for us of creating the interface. It doesn't clutter up our file system or the class definition file. It could even be used to define new classes implementing the same contract:
The
interface
keyword is already reserved, so this addition should be backwards compatible.Thoughts? Could this work?
Beta Was this translation helpful? Give feedback.
All reactions