From 0c109d90c8d8d9c72794f377760435d882a54f4d Mon Sep 17 00:00:00 2001 From: IanWold Date: Sun, 21 Jan 2024 17:12:04 -0600 Subject: [PATCH 1/2] Add annoying c# habbits --- ..._habbits_found_in_every_csharp_codebase.md | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md diff --git a/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md b/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md new file mode 100644 index 0000000..b148d06 --- /dev/null +++ b/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md @@ -0,0 +1,108 @@ +# Interfaces + +It is rare to find a legacy C# codebase where interfaces aren't critically misused, yet interfaces are almost entirely necessary when developing even relatively simple production applications. Interfaces should be used to represent meaningful, coherent abstractions or capabilities relevant to your domain, and only used when there is a clear need for abstraction and/or polymorphism. One benefit gained by interfaces is to allow for multiple class implementations, facilitating class swapping, refactor testing, decorator pattern, or testing fakes. A (maybe better) benefit comes from the ability to assign multiple abstract concepts to a single concrete class, a la the Liskov principle. + +Often though we find a one-interface-per-class rule in C# codebases, which eliminates or outright ignores a fair set of benefits. How can any of us claim our C# code is SOLID if we restrict ourselves away from even being able to use the Liskov principle? Worse yet is when we tack arbitrary interfaces on to represent abstract-concrete concepts, such as with the ever-present, ever-empty, and ever-reflected `IModel` interface. + +## Each Class has One Interface + +This sort of code is so common in a C# codebase that it's almost expected of every C# engineer to produce by default: + +```csharp +public interface IMailChimpFacadeEmailService { ... } + +public class MailChimpFacadeEmailService : IMailChimpFacadeEmailService { ... } +``` + +And there is almost never an `IEmailService` to be found! Interfaces are a specific tool with a specific purpose. If they're only mirroring the class structure, they're unnecessary. In these cases the code is better without these interfaces. If you do need to swap the class in a future refactor, you can create an interface then; it's dead weight to live in the code in the meantime. If you need to rely on the interface for mocks or fakes for unit tests, there's a good chance you're testing wrong and you should think about that too. + +A legitimate use - and one that will support legitimate testing strategies - is to create an interface for the properly abstract `IEmailService` concept, then implement it with the one which calls into the MailChimp facade. + +## There's Too Many Interfaces + +Even in the above example, I might question the need for an `IEmailService`. Are we really using the interface to properly segregate concepts in our codebase? Are we using them to achieve a beneficial pattern, such as the decorator pattern? 9 times out of 10 the answer will be "no". + +In these cases, I fall back on the Liskov principle not just as a way to think about abstractions, but also as a guide for when to create interfaces. Are these interfaces really describing meaningful, coherent, single, and abstract concepts that are relevatnt to my domain? Could multiple classes fulfil this contract, even if the classes are unrelated? These questions are useful heuristics when trying to determine this. More often than not, I think the interfaces can be thrown away. It's okay for multiple classes to not have interfaces. + + + +# Abstractions + +Misusing abstractinos itsn't a sin that's endemic to C# codebases specifically, it's across the entire industry. There's an interesting observation to be made that the incidence rate of overused abstractions tends to directly correspond with the number of software engineers involved in the team or company which owns that code - when we have time to kill at work, we tend to _work_. Abstractinos are inherently complex, and we should strive to create simple code. The trouble is that abstractions can increase the flexibility of code, so they're also essential tools. The prevailing wisdom is that abstractions should be used sparingly and when necessary, and nobody (not even me) can define when it's necessary to abstract. + +## There's (Sometimes Multiple) Passthrough layers + +Architectural abstractions can kill an entire codebase, especially when too many layers are introduced. One common find in C# codebases is the passthrough layer - often the repository layer. Indeed, gone are the days of the Data Access Layer, we now have the Repository Layer, named after the pattern. This layer is often a passthrough to Entity Framework (EF, like most ORMs, already implement repository pattern), to service layers which contact external services, or both. Whether it's a repository layer or not, passthrough layers are, I think universally agreed, incredibly annoying. + +These layers add no value, they don't procide modulatiry, and often hinder scalability. Demonstrably, they add to development time. Kill these layers, even if they cause you to have to rethink your architectural approach. + +## Abstractions are not Refactored Out + +Again, the solace here is that every software engineer - not just those of us writing C# - is guilty of creating needless abstractions. If I'm trying to focus on the C# community though, I might accuse us of being reticent to refactor abstractions. When I open a new C# codebase, it feel expected that even simple, imperative concepts are broken into complex, unclear, and often too-restrictive abstractions. [insert example here] + +Overall, I would say that an abstraction should clearly and demonstrably add distinct value or functionality. If you can't explain the value-add in one simple sentence, you've probably missed the mark. Architectural abstractions should specifically enhance modularity, scalability, or separation of concerns in a beneficial and articulable way. + +When refactoring (which ought be a constant practice), pay particular attention to abstractions. Celebrate when you can remove an abstraction, and always prefer clear, readable, imperative/procedural code over abstractions where the benefit of the latter is tenuous. + + + +# Exceptions + (Overarching point: What is the purpose of an exception? Use exceptions only for exceptional, error conditions.) + + Employ regular control structures (like if-else, loops) for standard flow control. + Consider alternative error handling strategies like return codes or validation methods for expected conditions. + Combine exception handling with proactive measures like input validation, fail-safes, and default values. + Use logging to track unexpected states or errors. + Design your application to gracefully handle errors and maintain a consistent state. + +## Exceptions are Used for Control Flow + * Reasons: Exceptions are meant for exceptional, unexpected events. Using them for regular control flow (like in a loop or conditionals) is misleading and can significantly impact performance due to the cost of raising and handling exceptions. + *Detrimental Effects: Poor performance, reduced code readability, and potential misuse of exception handling mechanisms. + +## Handling an Error means Catching an Exception + Using exceptions for error handling instead of Result pattern or other preventative strategies. + Often leads to swallowing errors or allowing exceptions to interrupt user experience. + * Reasons: While exceptions are a key part of error handling, relying solely on them can overlook other robust error-handling techniques like input validation or fail-safe mechanisms. + * Detrimental Effects: Potential for unhandled exceptions, reduced application stability, and missed opportunities for preventive error handling. + + + +# Testing + (Overarching point: Testing needs to be approached more thoughtfully, both from a conceptual and technical standpoint) + + Focus unit tests on the expected behavior of the code rather than its internal implementation. + Write tests that are resilient to changes in the implementation as long as the behavior remains consistent. + Use mock objects and test doubles to isolate the unit of work and avoid dependencies on external systems or complex setups. + Regularly conduct integration and end-to-end tests to ensure different parts of the application work together correctly. + Use a combination of testing types (unit, integration, end-to-end) to cover different aspects and layers of the application. + Ensure tests mimic real-world scenarios and user interactions as closely as possible. + +## Unit Tests are Coupled to Implementation + overly reliant on mocks; writing hacky unit tests to test untestable code + * Reasons: Tests should focus on behavior rather than implementation details. Coupling tests to implementation can lead to brittle tests that break with any change in the code, even if the behavior remains correct. + * Detrimental Effects: Increased maintenance of tests, reduced effectiveness in catching regressions, and potential for false negatives. + +## Behavior Testing Isn't used + * Reasons: Focusing solely on unit tests can miss larger issues that only appear when components interact. Behavior testing ensures that the system works as a whole. + * Detrimental Effects: Potential for integration bugs, reduced confidence in the system’s overall reliability, and overlooked user experience issues. + + + +# Language Features + (Overarching point: With so many features in a language/framework, choosing which to use when requires diligence. There's no "one right way" but a lot of undocumented bad ways.) + + Use LINQ judiciously, where it improves readability and efficiency. + Avoid using LINQ in performance-critical paths if it introduces significant overhead. + Stay updated on LINQ best practices and performance implications to make informed decisions. + Keep up-to-date with new C# features and understand their intended use cases. + Experiment with new features in non-critical parts of your codebase to gain familiarity. + Adopt new features where they improve code clarity, performance, and maintainability, while being cautious about using them inappropriately. + +## Over- or Under-Utilizing LINQ + either for too simple operations causing performance hits or for too complex operations causing unreadable/undebuggable code (and often performance hits) + * Reasons: LINQ is powerful but can be overused, leading to less readable, less performant code. Conversely, underusing it can result in more verbose and less efficient code than necessary. + * Detrimental Effects: Performance issues, decreased readability, and potentially missing out on concise, expressive code patterns. + +## Over- or Under-Looking New Features + * Reasons: Ignoring new features can result in missing out on improvements and more efficient ways of coding. Misusing them can lead to convoluted solutions that don’t align with the feature’s intended use. + * Detrimental Effects: Reduced code efficiency and quality, potential for using inappropriate solutions, and missing out on language evolution benefits. \ No newline at end of file From 578e3c79342bf570f4d52597b0c1461492c09b53 Mon Sep 17 00:00:00 2001 From: IanWold Date: Sun, 4 Feb 2024 08:31:28 -0600 Subject: [PATCH 2/2] Add four deeply ingrained csharp cliches --- ..._habbits_found_in_every_csharp_codebase.md | 108 --------------- .../four_deeply_ingrained_csharp_cliches.md | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+), 108 deletions(-) delete mode 100644 Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md create mode 100644 Site/Posts/four_deeply_ingrained_csharp_cliches.md diff --git a/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md b/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md deleted file mode 100644 index b148d06..0000000 --- a/Site/Posts/annoying_habbits_found_in_every_csharp_codebase.md +++ /dev/null @@ -1,108 +0,0 @@ -# Interfaces - -It is rare to find a legacy C# codebase where interfaces aren't critically misused, yet interfaces are almost entirely necessary when developing even relatively simple production applications. Interfaces should be used to represent meaningful, coherent abstractions or capabilities relevant to your domain, and only used when there is a clear need for abstraction and/or polymorphism. One benefit gained by interfaces is to allow for multiple class implementations, facilitating class swapping, refactor testing, decorator pattern, or testing fakes. A (maybe better) benefit comes from the ability to assign multiple abstract concepts to a single concrete class, a la the Liskov principle. - -Often though we find a one-interface-per-class rule in C# codebases, which eliminates or outright ignores a fair set of benefits. How can any of us claim our C# code is SOLID if we restrict ourselves away from even being able to use the Liskov principle? Worse yet is when we tack arbitrary interfaces on to represent abstract-concrete concepts, such as with the ever-present, ever-empty, and ever-reflected `IModel` interface. - -## Each Class has One Interface - -This sort of code is so common in a C# codebase that it's almost expected of every C# engineer to produce by default: - -```csharp -public interface IMailChimpFacadeEmailService { ... } - -public class MailChimpFacadeEmailService : IMailChimpFacadeEmailService { ... } -``` - -And there is almost never an `IEmailService` to be found! Interfaces are a specific tool with a specific purpose. If they're only mirroring the class structure, they're unnecessary. In these cases the code is better without these interfaces. If you do need to swap the class in a future refactor, you can create an interface then; it's dead weight to live in the code in the meantime. If you need to rely on the interface for mocks or fakes for unit tests, there's a good chance you're testing wrong and you should think about that too. - -A legitimate use - and one that will support legitimate testing strategies - is to create an interface for the properly abstract `IEmailService` concept, then implement it with the one which calls into the MailChimp facade. - -## There's Too Many Interfaces - -Even in the above example, I might question the need for an `IEmailService`. Are we really using the interface to properly segregate concepts in our codebase? Are we using them to achieve a beneficial pattern, such as the decorator pattern? 9 times out of 10 the answer will be "no". - -In these cases, I fall back on the Liskov principle not just as a way to think about abstractions, but also as a guide for when to create interfaces. Are these interfaces really describing meaningful, coherent, single, and abstract concepts that are relevatnt to my domain? Could multiple classes fulfil this contract, even if the classes are unrelated? These questions are useful heuristics when trying to determine this. More often than not, I think the interfaces can be thrown away. It's okay for multiple classes to not have interfaces. - - - -# Abstractions - -Misusing abstractinos itsn't a sin that's endemic to C# codebases specifically, it's across the entire industry. There's an interesting observation to be made that the incidence rate of overused abstractions tends to directly correspond with the number of software engineers involved in the team or company which owns that code - when we have time to kill at work, we tend to _work_. Abstractinos are inherently complex, and we should strive to create simple code. The trouble is that abstractions can increase the flexibility of code, so they're also essential tools. The prevailing wisdom is that abstractions should be used sparingly and when necessary, and nobody (not even me) can define when it's necessary to abstract. - -## There's (Sometimes Multiple) Passthrough layers - -Architectural abstractions can kill an entire codebase, especially when too many layers are introduced. One common find in C# codebases is the passthrough layer - often the repository layer. Indeed, gone are the days of the Data Access Layer, we now have the Repository Layer, named after the pattern. This layer is often a passthrough to Entity Framework (EF, like most ORMs, already implement repository pattern), to service layers which contact external services, or both. Whether it's a repository layer or not, passthrough layers are, I think universally agreed, incredibly annoying. - -These layers add no value, they don't procide modulatiry, and often hinder scalability. Demonstrably, they add to development time. Kill these layers, even if they cause you to have to rethink your architectural approach. - -## Abstractions are not Refactored Out - -Again, the solace here is that every software engineer - not just those of us writing C# - is guilty of creating needless abstractions. If I'm trying to focus on the C# community though, I might accuse us of being reticent to refactor abstractions. When I open a new C# codebase, it feel expected that even simple, imperative concepts are broken into complex, unclear, and often too-restrictive abstractions. [insert example here] - -Overall, I would say that an abstraction should clearly and demonstrably add distinct value or functionality. If you can't explain the value-add in one simple sentence, you've probably missed the mark. Architectural abstractions should specifically enhance modularity, scalability, or separation of concerns in a beneficial and articulable way. - -When refactoring (which ought be a constant practice), pay particular attention to abstractions. Celebrate when you can remove an abstraction, and always prefer clear, readable, imperative/procedural code over abstractions where the benefit of the latter is tenuous. - - - -# Exceptions - (Overarching point: What is the purpose of an exception? Use exceptions only for exceptional, error conditions.) - - Employ regular control structures (like if-else, loops) for standard flow control. - Consider alternative error handling strategies like return codes or validation methods for expected conditions. - Combine exception handling with proactive measures like input validation, fail-safes, and default values. - Use logging to track unexpected states or errors. - Design your application to gracefully handle errors and maintain a consistent state. - -## Exceptions are Used for Control Flow - * Reasons: Exceptions are meant for exceptional, unexpected events. Using them for regular control flow (like in a loop or conditionals) is misleading and can significantly impact performance due to the cost of raising and handling exceptions. - *Detrimental Effects: Poor performance, reduced code readability, and potential misuse of exception handling mechanisms. - -## Handling an Error means Catching an Exception - Using exceptions for error handling instead of Result pattern or other preventative strategies. - Often leads to swallowing errors or allowing exceptions to interrupt user experience. - * Reasons: While exceptions are a key part of error handling, relying solely on them can overlook other robust error-handling techniques like input validation or fail-safe mechanisms. - * Detrimental Effects: Potential for unhandled exceptions, reduced application stability, and missed opportunities for preventive error handling. - - - -# Testing - (Overarching point: Testing needs to be approached more thoughtfully, both from a conceptual and technical standpoint) - - Focus unit tests on the expected behavior of the code rather than its internal implementation. - Write tests that are resilient to changes in the implementation as long as the behavior remains consistent. - Use mock objects and test doubles to isolate the unit of work and avoid dependencies on external systems or complex setups. - Regularly conduct integration and end-to-end tests to ensure different parts of the application work together correctly. - Use a combination of testing types (unit, integration, end-to-end) to cover different aspects and layers of the application. - Ensure tests mimic real-world scenarios and user interactions as closely as possible. - -## Unit Tests are Coupled to Implementation - overly reliant on mocks; writing hacky unit tests to test untestable code - * Reasons: Tests should focus on behavior rather than implementation details. Coupling tests to implementation can lead to brittle tests that break with any change in the code, even if the behavior remains correct. - * Detrimental Effects: Increased maintenance of tests, reduced effectiveness in catching regressions, and potential for false negatives. - -## Behavior Testing Isn't used - * Reasons: Focusing solely on unit tests can miss larger issues that only appear when components interact. Behavior testing ensures that the system works as a whole. - * Detrimental Effects: Potential for integration bugs, reduced confidence in the system’s overall reliability, and overlooked user experience issues. - - - -# Language Features - (Overarching point: With so many features in a language/framework, choosing which to use when requires diligence. There's no "one right way" but a lot of undocumented bad ways.) - - Use LINQ judiciously, where it improves readability and efficiency. - Avoid using LINQ in performance-critical paths if it introduces significant overhead. - Stay updated on LINQ best practices and performance implications to make informed decisions. - Keep up-to-date with new C# features and understand their intended use cases. - Experiment with new features in non-critical parts of your codebase to gain familiarity. - Adopt new features where they improve code clarity, performance, and maintainability, while being cautious about using them inappropriately. - -## Over- or Under-Utilizing LINQ - either for too simple operations causing performance hits or for too complex operations causing unreadable/undebuggable code (and often performance hits) - * Reasons: LINQ is powerful but can be overused, leading to less readable, less performant code. Conversely, underusing it can result in more verbose and less efficient code than necessary. - * Detrimental Effects: Performance issues, decreased readability, and potentially missing out on concise, expressive code patterns. - -## Over- or Under-Looking New Features - * Reasons: Ignoring new features can result in missing out on improvements and more efficient ways of coding. Misusing them can lead to convoluted solutions that don’t align with the feature’s intended use. - * Detrimental Effects: Reduced code efficiency and quality, potential for using inappropriate solutions, and missing out on language evolution benefits. \ No newline at end of file diff --git a/Site/Posts/four_deeply_ingrained_csharp_cliches.md b/Site/Posts/four_deeply_ingrained_csharp_cliches.md new file mode 100644 index 0000000..dbcba45 --- /dev/null +++ b/Site/Posts/four_deeply_ingrained_csharp_cliches.md @@ -0,0 +1,125 @@ +;;; +{ + "title": "Four Deeply-Ingrained C# Cliches", + "description": "There's a lot to love about C# and .NET, and there are some things that I don't love as much. Then there are four bad habbits that are so deeply ingrained they've become cliches within our codebases.", + "date": "4 February 2024", + "contents": false, + "hero": "photo-1619468654139-877048c5a71f", + "related": [ + { "title": "Eight Maxims", "description": "A few principles for thoughtful software engineering.", "fileName": "eight_maxims" }, + { "title": "My (Continuing) Descent Into Madness", "description": "It started simply enough, when I asked myself if I should try an IDE other than Visual Studio. Mere months later, I'm now using a tiling window manager. This is the story of my (continuing) descent into madness.", "fileName": "my_continuing_descent_into_madness" }, + { "title": "Reclaim Your Agile: The One Clever Trick Agile Coaches Don't Want You to Know", "description": "What if I told you there's one trick to being able to reshape your team's development process without your company knowing it? What if I told you that you can achieve actual Agile even though you work in a Scrum firm?", "fileName": "reclaim_your_agile" } + ] +} +;;; + +There's a lot to love about C# and .NET, and there are some things that I don't love as much. Then there are four bad habbits that are so deeply ingrained they've become cliches within our codebases. Despite being obviously bad practices, their ubiquity seems to force them into our codebases in spite of our knowing better. When fighting the battle against [complexity spirit demon](https://grugbrain.dev/) it's easy to tire and allow these to slip through, perhaps justified by their disarming commonality. + +It's not as though these subjects haven't been written about before, but that they are still practiced in spite of our best efforts to craft well-written code. I think it's useful to call these four out, and I want to propose alternative solutions with which I have found success in the past. Take my solutions with a grain of salt if you will, but do have a think about a better alternative yourself. These cliches have plagued too many codebases, creating spaghettiable (is that a word?) code and exposing too many bug vectors. + +# Interfaces + +Interfaces are critically overused _and_ misused. When I open a C# codebase I can guarantee I'll be treated to a whole host of interfaces that have exactly one implementation. Maybe they'll also be mocked in 200 different unit tests. I know I'll be treated to files that have classes so simple they haven't changed in five years, yet there's an interface right at the top for this class exposing the single public method. I know I'll find at least one (usually many) empty interfaces, and I'm always on the edge of my seat to find how much runtime reflection there is referencing the empty `IModel`. + +What's going wrong here? I think we've forgotten the utility that interfaces have for us, yet interfaces are almost entirely necessary when developing even relatively simple production applications. Interfaces should be used to represent meaningful, coherent abstractions or capabilities relevant to your domain, and only used when there is a clear need for abstraction and/or polymorphism. One benefit gained by interfaces is to allow for multiple class implementations, facilitating class swapping, refactor testing, decorator pattern, or testing fakes. A (maybe better) benefit comes from the ability to assign multiple abstract concepts to a single concrete class, a la the Liskov principle. + +Often though we find a one-interface-per-class rule in C# codebases, which eliminates or outright ignores a fair set of benefits. How can any of us claim our C# code is SOLID if we restrict ourselves away from even being able to use the Liskov principle? Sure, we don't need to adhere to Liskov religiously, but it's a good heuristic to help judge when to use an interface. If they're only mirroring the class structure, they're unnecessary. In these cases the code is better without these interfaces. If you do need to swap the class in a future refactor, you can create an interface then; it's dead weight to live in the code in the meantime. If you need to rely on the interface for mocks or fakes for unit tests, there's a good chance you're testing wrong and you should think about that too. + +As an example, consider the following example which we have each encountered several thousand times: + +```csharp +public interface IMailChimpFacadeEmailService { ... } + +public class MailChimpFacadeEmailService : IMailChimpFacadeEmailService { ... } +``` + +And there is almost never an `IEmailService` to be found! In this case we're usually better just deleting `IMailChimpFacadeEmailService`, but in the cases where the interface abstraction is necessary then replacing `IMailChimpFacadeEmailService` with `IEmailService` is entirely appropriate. + +Empty interfaces should be deleted outright. But how will we reflect on our models without an empty `IModel` interface? Well, we probably shouldn't reflect on them, especially not after startup time. A lot of legitimate uses can be replaced by source generators here. These empty interfaces really grind my gears; they pollute my code and make me sad. + +# Abstractions + +Overusing abstractions isn't a sin that's endemic to C# codebases specifically; it's across the entire industry. There's an interesting observation to be made that the incidence rate of overused abstractions tends to directly correspond with the number of software engineers involved in the team or company which owns that code - when we have time to kill at work, we tend to _work_. Abstractions are inherently complex, and we should strive to create simple code. The trouble is that abstractions can increase the flexibility of code, so they're also essential tools. The prevailing wisdom is that abstractions should be used sparingly and when necessary, and nobody (not even me) can define when it's necessary to abstract. + +One common find in C# codebases is the passthrough layer - often the repository layer. Indeed, gone are the days of the Data Access Layer, we now have the Repository Layer, named after the pattern. This layer is often a passthrough to Entity Framework (EF, like most ORMs, already implements the repository pattern), to service layers which contact external services, or both. Whether it's a repository layer or not, passthrough layers are, I think universally agreed, incredibly annoying. + +These layers add no value, they don't provide modularity, and often hinder scalability. Demonstrably, they add to development time. Kill these layers, even if they cause you to have to rethink your architectural approach. + +Overall, I would say that an abstraction should clearly and demonstrably add distinct value or functionality. If you can't explain the value-add in one simple sentence, you've probably missed the mark. Architectural abstractions should specifically enhance modularity, scalability, or separation of concerns in a beneficial and articulable way. When refactoring (which ought be a constant practice), pay particular attention to abstractions. Celebrate when you can remove an abstraction, and always prefer clear, readable, imperative or procedural code over abstractions where the benefit of the latter is tenuous. + +# Exceptions + +There's a lot that's been written about the ills of exceptions, and I think the negativity associated with them is maybe a bit exaggerated. One concept which I think is overlooked is the [two-pronged error model](https://joeduffyblog.com/2016/02/07/the-error-model/); that there are some errors which need to be handled immediately, and then others which require an interruption to the control flow. The fact of the matter is that exceptions satisfy the use cases for the latter - the truly _exceptional_ scenarios. We get into trouble however when we start relying on exceptions for all of our error handling. + +The blame for this one definitely falls first on the C# language - it really only natively supports exceptions and there's still not a great way to create and consume a robust result or option monad (discriminated unions when). That said, one of the variants of the result pattern are better for the former sort of error. If you can encapsulate the result of your operations - particularly your data access and business operations - in such a way that the resulting value can't be accessed without a success check, then you'll be guarding the consuming code against abusing your method - you'll be making your contract more explicit. + +Here's a simple result object for C# that uses the try pattern to do this: + +```csharp +public class Result +{ +    private bool isSuccess; +    private T? value; + +    private Result(bool isSuccess, T? value) +    { +        _isSuccess = isSuccess +        _value = value; +    } + +    public bool IsSuccess([NotNullWhen(true)]out T? result) +    { +        result = value; +        return isSuccess; +    } + + public static Result Success(T value) => new(true, value); + + public static Result Failure() => new(false, default); +} +``` + +I can't consume the value unless I make a check now: + + +```csharp +var itemResult = GetItem(); +if (!itemResult.IsSuccess(out var item)) +{ +    // handle error case +    return; +} + +item.DoSomething(); +``` + +The other bad aspect of exceptions comes when we start using them for situations that aren't even errors. It takes very little ingenuity - disappointingly little, even - to adapt exceptions as Malbolge's new `goto`. It isn't uncommon to see this in codebases, especially in library or utility logic, where some non-happy-path, yet non-error, scenario will throw, expecting to be caught _somewhere_. Sometimes this is code that merely lacks the most basic consideration (say, an email validator that throws if the string is empty instead of returning the validation error). + +Other times this is entirely intentional, given very careful consideration in the entirely wrong direction. A great example is the `IDataReader` from `Microsoft.SqlClient` - if I want to access a column that doesn't exist (i.e. `dataReader["some_column"]`) it throws! Maybe that would made sense if they implemented a `TryGetValue` or `ContainsColumn`, but they did neither. The problem is so onerous because the contract for this object is that I am _supposed_ to use try/catch here to poll for maybe-extant columns. + +# Unit Tests + +I'll admit up front here that I'm a unit test hater. Always have been, and aggressively moreso every single time that I have to rewrite 20 unit tests because I just did a minor, non-behavior-altering refactor. Unit tests, as they're currently practiced, are in their ideal form a way to isolate a single method (or "unit") of code to ensure its contract is respected. There's a lot wrong with this. + +First of all, our unit tests (almost) never live up to this. And it's not a question here of letting perfect be the enemy of good enough, they're so completely far away from this supposed ideal that I don't think it's fair to say they're even _trying_ to aspire to it. We find unit tests heavily reliant on mocks (or fakes if we're lucky) where methods that don't even make sense to test in isolation are stood up in an almost Frankensteinian manner to prod it with ill-figured test scenarios. + +On top of that, it's rare to find a test that even tests the properties of these methods appropriately. Often times, these tests don't even seem to know what the properties of the methods are they need to test for (a stark contrast to the much more focused discipline of [property testing](https://matthewtolman.com/article/what-is-property-testing)). + +More troubling than either of those is that isolating methods is usually not the best way to test an application. The majority of methods in a business application do not need isolated testing. Rather, the system as a whole needs testing. This ideal of isolating methods to ensure that they adhere to the contracts they establish loses the forest for the trees; contracts can and ought be enforced in the code itself. + +This all culminates in the extremely common deception that our codebases are robust because we have 90% code coverage with thousands of tests that are severely abusing mocks and checking arbitrary input and output values, or call patterns worse yet! This leads to our codebases being so fragile that even small changes require several unnecessary changes across dozens of useless unit tests. And it's entirely missing the point to suggest that this is because unit tests aren't being used correctly. The definition I provided earlier is inherently flawed and leads towards code being developed this way. Mocks or fakes are necessary to set up most classes for testing, and this practice leads to huge swathes of tests which are coupled to the implementations they test. + +You'll be interested to learn then that this definition of a unit test that is so ubiquitous across our industry is entirely wrong. It's a great misunderstanding of the original intent of the "unit test". I'll link [this brilliant talk by Ian Cooper](https://www.youtube.com/watch?v=EZ05e7EMOLM) as an in-depth explanation, but it suffices to say that the original intent of the "unit" test was that a "unit" represented some portion of the system which was behaviorally isolated. Perhaps we should be easy on ourselves for having mistaken that to mean "method" but after enduring how many unit test refactors, my charitability is thin. + +Proper behavior tests are sorely underused, often not at all. It's quite typical that codebases will have maybe 95% unit testing and 5% proper behavior testing. This is backwards - when we're developing LOB or product software, the behavior is the most important, not the implementation. The tests _need_ to be divorced from the implementation in order to properly isolate and test behavior. + +# Conclusion + +tl;dr: + +* Interfaces are overused and have become almost like header files, mindlessley mirroring the class structures in our codebases. Consider the Liskov principle when creating interfaces, and best yet only use interfaces when they're helping you fulfil a pattern or a meaningful abstraction. +* On the topic of abstractions, they tend to be used too frequently for cases which don't require abstraction. Sometimes it's difficult to see these abstractions enter the codebase over several PRs, so refactor your code frequently and aim to reduce abstractions every time. Prefer writing straightforward imperative or procedural code when possible. +* Exceptions are often used for any kind of error handling and sometimes as extra special `goto`s. Only use exceptions for exceptional conditions that require you to break control flow, and consider using some form of the result pattern for error handling. +* Unit tests are the work of the devil; they tend to lead to tests coupled to the implementations they test. Most applications can (and should) be entirely tested with behavioral (integration, e2e) tests instead of unit tests. When you do need to isolate discrete methods or classes for testing, consider whether patterns like property testing would better test the behavior of the method. + +These cliches lead to code of a lower quality. Sometimes they just make it a bit more tedious to add a feature or change a bit of business logic. Other times, often over time, they create mangled codebases where making progress is like swimming through molasses. Though these are common practices in C# codebases, they don't need to be. I've suggested several alternatives that I've found to be very well worth considering, but you might have your own alternatives instead. What's most important is that we can stop letting these four cliches into our code, and start doing _something_ more robust.