82

At which point should YAGNI take precedence against good coding practices and vice versa? I'm working on a project at work and want to slowly introduce good code standards to my co-workers (currently there are none and everything is just kind of hacked together without rhyme or reason), but after creating a series of classes (we don't do TDD, or sadly any kind of unit testing at all) I took a step back and thought it's violating YAGNI because I pretty much know with certainty that we don't require the need to extend some of these classes.

Here's a concrete example of what I mean: I have a data access layer wrapping a set of stored procedures, which uses a rudimentary Repository-style pattern with basic CRUD functions. Since there are a handful of methods that all my repository classes need, I created a generic interface for my repositories, called IRepository. However, I then created a "marker" interface (i.e. interface that doesn't add any new functionality) for each type of repository (e.g. ICustomerRepository) and the concrete class implements that. I've done the same thing with a Factory implementation to build the business objects from DataReaders/DataSets returned by the Stored Procedure; the signature of my repository class tends to look something like this:

public class CustomerRepository : ICustomerRepository
{
    ICustomerFactory factory = null;

    public CustomerRepository() : this(new CustomerFactory() { }

    public CustomerRepository(ICustomerFactory factory) {
        this.factory = factory;
    }      

    public Customer Find(int customerID)
    {
        // data access stuff here
        return factory.Build(ds.Tables[0].Rows[0]);
    }
}

My concern here is that I'm violating YAGNI because I know with 99% certainty that there is never going to be a reason to give anything other than a concrete CustomerFactory to this repository; since we don't have unit tests I don't need a MockCustomerFactory or similar things, and having so many interfaces might confuse my co-workers. On the other hand, using a concrete implementation of the factory seems like a design smell.

Is there a good way to come to a compromise between proper software design and not overarchitecting the solution? I'm questioning if I need to have all of the "single implemenation interfaces" or if I could sacrifice a bit of good design and just have, for example, the base interface and then the single concrete, and not worry about programming to the interface if the implementation is that will ever be used.

8
  • 19
    You say "since we don't have unit tests I don't need MockX" which naturally leads to "I don't need IX, I only need X". I flip that around, the fact that you don't have unit tests highlights the fact that you need IX and MockX, because these things will help you have unit tests. Don't accept the reality of having no tests, treat that as a temporary problem that will be remedied over (probably a good, long) time. Commented Sep 16, 2011 at 15:31
  • 10
    Even though it's trivial to google it, someone should mention that YAGNI stands for "You ain't gonna need it"
    – thedaian
    Commented Sep 16, 2011 at 17:32
  • 1
    I'd think if you were writing new classes like this, you'd want to add some unit tests. Even if your coworkers won't run them. At least later you can say, "Look! my unit tests caught it when you broke my code! See how great unit tests are!" In that case, making it mockable might be worthwhile here. (Although, I'd prefer it if the object could be mocked without defining an interface) Commented Sep 16, 2011 at 18:36
  • Wouldn't the unit tests force me to create (or use) a mock framework so I don't hit live stored procedures? That's the main reason I tend to not add tests - we each have a local copy of the production database that we test and write code against. Commented Sep 17, 2011 at 4:20
  • 3
    @Anthony And does mocking always justify the extra complexity overhead that it entails? Mocking is a fine tool but its usefulness too must be weighted against the costs and sometimes the scale is tipped the other way by an excess of indirection. Sure, there are tools to help with the extra complexity but they won’t make the complexity go away. There seems to be a growing trend of treating “testing at all cost” as a given. I believe this is false. Commented Sep 17, 2011 at 8:04

14 Answers 14

76

Is there a good way to come to a compromise between proper software design and not overarchitecting the solution?

YAGNI.

I could sacrifice a bit of good design

False assumption.

and the base interface and then the single concrete,

That's not a "sacrifice". That is good design.

9
  • 79
    Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. Antoine De St-Exupery
    – Newtopian
    Commented Sep 16, 2011 at 14:23
  • 6
    @Newtopian: people have mixed feelings about that regarding Apple's latest products. :)
    – Roy Tinker
    Commented Sep 16, 2011 at 17:17
  • 20
    I have a huge problem with this kind of answers. Is "X is true because Y says it, and Y is well backed by the community" valid reasoning? If someone in real life ranted against YAGNI, would you show him this answer as an argument?
    – deprecated
    Commented Sep 17, 2011 at 11:05
  • 9
    My point isn't about YAGNI - it's about answer quality. I dind't say someone in particular was ranting, it was part of my reasoning. Please read it again.
    – deprecated
    Commented Sep 17, 2011 at 18:55
  • 7
    Agree with vemv that this is a poor answer, with no actual information. And the 69 upvoters should be ashamed. Its like a comment on a sports blog "Go YAGNI, they rock!". Not a good answer for here.
    – user949300
    Commented Oct 2, 2014 at 21:09
83

In most cases, avoiding code that you aren't going to need leads to better design. The most maintainable and future-proof design is the one that uses the smallest amount of well-named, simple code that satisfies the requirements.

The most simple designs are the most easy to evolve. Nothing kills maintainability like useless, overengineered abstraction layers.

9
  • 8
    I find 'avoiding YAGNI code' unnervingly ambiguous. It could mean code you're not gonna need or code that adheres to the YAGNI principle. (Cf. 'KISS code')
    – sehe
    Commented Sep 16, 2011 at 16:52
  • 2
    @sehe: It's not ambiguous at all (while "adhers to the YAGNI principle" is outright self-contradicting) if you spell it out: what could "You-aint't-gonna-need-it code" possibly mean but code that you ain't gonna need? Commented Sep 16, 2011 at 22:19
  • 1
    I'm going to print this answer in a big font and hang it where all the architecture astronauts can read it. +1! Commented Sep 16, 2011 at 22:20
  • 1
    +1. I can still remember my first corporate project I was given to support and add some new features. First thing I did was remove 32,000 lines of useless code from a 40,000 line program without losing any functionality. The original programmer was fired shortly thereafter. Commented Sep 17, 2011 at 13:28
  • 4
    @vemv: read "useless" as "not currently being used, except trivially", i.e. a case of YAGNI. And "overengineered" is quite a bit more specific than "bad". Specifically it means "complexity caused by theoretical concepts or imagined possible requirements, rather than concrete, current requirements". Commented Sep 17, 2011 at 19:16
65

YAGNI and SOLID (or any other design methodology) are not mutually exclusive. However, they are near-polar opposites. You do not have to adhere 100% to either, but there will be some give-and-take; the more you look at a highly-abstracted pattern used by one class in one place, and say YAGNI and simplify it, the less SOLID the design becomes. The reverse can be true as well; many times in development, a design is implemented SOLIDly "on faith"; you don't see how you will need it, but you just have a hunch. This could be true (and is more and more likely to be true the more experience you gain), but it could also put you in as much technical debt as a slap-dash "do it light" approach; instead of a DIL "spaghetti code" codebase, you may end up with "baklava code", having so many layers that simply adding a method or a new data field becomes a days-long process of wading through service proxys and loosely-coupled dependencies with only one implementation. Or you could end up with "spaghetti-Os code", which comes in such small, loosely-structured pieces that moving up, down, left or right in the architecture takes you through 50 methods with 3 lines each.

I've said it in other answers, but here it is: On the first pass, make it work. On the second pass, make it elegant. On the third pass, make it SOLID.

Breaking that down:

When you first write a line of code, it simply has to work. At this point, for all you know, it's a one-off. So, you don't get any style points for building an "ivory-tower" architecture to add 2 and 2. Do what you gotta do, and assume you'll never see it again.

The next time your cursor goes in that line of code, you have now disproved your hypothesis from when you first wrote it. You are revisiting that code, likely either to extend it or to use it elsewhere, so it's not a one-off. Now, some basic principles like DRY (don't repeat yourself) and other simple rules for code design should be implemented; extract methods and/or form loops for repeated code, extract variables for common literals or expressions, maybe add some comments, but overall your code should self-document. Now, your code is well-organized, if still maybe tightly-coupled, and anyone else looking at it can easily learn what you're doing by reading the code, instead of tracing it line-by-line.

The third time your cursor enters that code, it's probably kind of a big deal; you're either extending it yet again or it's become useful in at least three different other places in the codebase. At this point, it's a key, if not core, element of your system, and should be architected as such. At this point, you usually also have the knowledge of how it's been used so far, which will allow you to make good design decisions regarding how to architect the design to streamline those usages and any new ones. Now the SOLID rules should enter the equation; extract classes containing code with specific purposes, define common interfaces for any classes that have similar purposes or functionality, set up loosely-coupled dependencies between classes, and design the dependencies such that you can easily add, remove, or swap them.

From this point on, should you need to further extend, reimplement or re-use this code, it's all nicely packaged up and abstracted in the "black box" format we all know and love; plug it in wherever else you need it, or add a new variation on the theme as a new implementation of the interface without having to change the usage of said interface.

5
  • I second the awesomeness. Commented Sep 18, 2011 at 10:05
  • 2
    Yes. When I used to design-for-reuse/extension upfront, I'd find that when I wanted to reuse or extend it, it would be in a different way than I had anticipated. Prediction is hard, especially concerning the future. So I support your 3-strikes rule - by that time, you have a reasonable idea of how it will be reused/extended. NB: an exception is if you already know how it will be (e.g. from previous projects, domain knowledge, or already specified).
    – 13ren
    Commented Sep 19, 2011 at 23:15
  • On the fourth pass, make it AWESOME. Commented Sep 20, 2011 at 20:31
  • @KeithS: It seems John Carmack did something similar: "source code of Quake II ... unify Quake 1, Quake World and QuakeGL into one beautiful code architecture." fabiensanglard.net/quake2/index.php
    – 13ren
    Commented Sep 21, 2011 at 3:13
  • +1 I think I'll name ur rule: "Practical refactoring" :)
    – Songo
    Commented Nov 20, 2012 at 18:01
31

Instead of either of those, I prefer WTSTWCDTUAWCROT?

(What's the simplest thing we can do that's useful and we can release on Thursday?)

Simpler acronyms are on my list of things to do, but they're not a priority.

6
  • 9
    That acronym violates the YAGNI principle :)
    – riwalk
    Commented Sep 16, 2011 at 16:06
  • 4
    I used exactly the letters I needed--no more and no less. In that way, I'm kind of like Mozart. Yep. I'm the Mozart of acronyms. Commented Sep 20, 2011 at 11:54
  • 4
    I never knew Mozart was terrible at making acronyms. I learn something new every day on an SE site. :P Commented Oct 7, 2011 at 9:14
  • 3
    @Mike Sherrill 'CatRecall': Maybe one should extend that to WTSTWCDTUWCROTAWBOF = "What's the simplest thing we can do that's useful, we can release on Thursday and won't break on Friday?" ;-)
    – Giorgio
    Commented Jan 6, 2015 at 9:25
  • 1
    @Stargazer712 no :) it violates POLA.
    – v.oddou
    Commented Jun 30, 2016 at 7:10
26

YAGNI and good design are not conflicting. YAGNI is about (not) supporting future needs. Good design is about making transparent what your software does right now and how it does this.

Will introducing a factory make your existing code simpler? If not, don't add it. If it does, for example when you are adding tests (which you should do!), add it.

YAGNI is about not adding complexity to support future functions.
Good Design is about removing complexity while still supporting all current functions.

18

They aren't in conflict, your goals are wrong.

What are you attempting to accomplish?

You want to write quality software, and to do that you want to keep your code base small and not have issues.

Now we reach a conflict, how do we cover off all cases if we don't write cases we aren't going to use?

Here's what your problem looks like.

enter image description here (anyone interested, this is called evaporating clouds)

So, what's driving this?

  1. You don't know what you're not going to need
  2. You don't want to waste time and bloat your code

Which one of these can we solve? Well, it looks like not wanting to waste time and bloat code is a great goal, and makes sense. What about that first one? Can we find out what we're going to need to code?

I'm working on a project at work and want to slowly introduce good code standards to my co-workers (currently there are none and everything is just kind of hacked together without rhyme or reason) [...] I took a step back and thought it's violating YAGNI because I pretty much know with certainty that we don't require the need to extend some of these classes.

Let's re-phrase all of that

  • No code standards
  • No project planning going on
  • Cowboys everywhere doing their own damn thing (and you're trying to play sheriff in the wild wild west), yee haw.

Is there a good way to come to a compromise between proper software design and not overarchitecting the solution?

You don't need a compromise, you need someone to manage the team who is competent and has vision of the whole project. You need someone who can plan what you ARE going to need, instead of each of you throwing in things you ARE NOT going to need because you're so uncertain of the future because... why? I'll tell you why, it's because nobody has a damn plan among all of you. You're trying to bring in code standards to fix an entirely separate issue. Your PRIMARY problem you need to solve is a clear roadmap and project. Once you have that, you can say "code standards help us reach this goal more effectively as a team," which is the absolute truth but outside the scope of this question.

Get a project/team manager who can do these things. If you have one, you need to ask them for a map and explain the YAGNI problem that not having a map is presenting. If they're grossly incompetent, write the plan yourself and say "here's my report for you on things we need, please review it and let us know your decision."

5
  • We don't have one, sadly. The Development Manager either is more concerned about getting things done fast than about standards/quality, or would make the standard practices like using raw DataSets everywhere returned directly from classes, VB6 style coding and everything in code-behind with duplicate logic copied and pasted all over. Commented Sep 16, 2011 at 14:56
  • That's fine, it's going to be a bit slower, but you need to speak to their worries then. Explain that this YAGNI/Useless-code problem is wasting time, suggest the roadmap, give him a roadmap, explain it will make work faster. When he's bought in, come in with the problems that poor standards have to the speed of development, suggest the better ones. They want the project done, they just don't know how to manage the thing, you'll either need to replace him or baby him... or quit.
    – Incognito
    Commented Sep 16, 2011 at 14:59
  • Erm..... I think the driving goal would be 'you want to have a good, valuable product'?
    – sehe
    Commented Sep 16, 2011 at 16:56
  • @sehe that's not his decision :p.
    – Incognito
    Commented Sep 16, 2011 at 17:59
  • 3
    Great answer. He is facing an uphill battle. Getting it done will be difficult if there is no management buy in. And you are right that question is premature and does not address the real problem. Commented Sep 17, 2011 at 15:32
11

Allowing your code to be extended with unit tests is never going to be covered under YAGNI, because you will need it. However, I'm not convinced that your design changes to a single-implementation interface are actually increasing the testability of the code because CustomerFactory already inherits from an interface and can be swapped out for a MockCustomerFactory at any time anyway.

1
  • 1
    +1 for helpful comment about actual problem rather than just about cosmic battle and/or love-in between Good Design and YAGNI.
    – psr
    Commented Sep 16, 2011 at 18:07
11

The question presents a false dilemma. Proper application of the YAGNI principle isn't some unrelated thing. It's one aspect of good design. Each of the SOLID principles are aspects of good design as well. You can't always fully apply every principle in any discipline. Real-world problems put a lot of forces on your code, and some of those push in opposing directions. Principles of design have to account for all of those, but no handful of principles can fit all situations.

Now, let's take a look at each principle with the understanding that while they may sometimes pull in different directions, they are by no means inherently in conflict.

YAGNI was conceived to help developers avoid a particular kind of rework: that which comes from building the wrong thing. It does this by guiding us to avoid making errant decisions too early based on assumptions or predictions about what we think will change or be needed in the future. Collective experience tells us that when we do this, we are usually wrong. For example, YAGNI would tell you not to create an interface for the purpose of reusability, unless you know right now that you need multiple implementers. Similarly YAGNI would say don't create a "ScreenManager" to manage the single form in an application unless you know right now that you're going to have more than one screen.

Contrary to what many people think, SOLID is not about reusability, genericity, or even abstraction. SOLID is intended to help you write code that is prepared for change, without saying anything about what that specific change might be. The five principles of SOLID create a strategy for building code that is flexible without being overly generic, and simple without being naive. Proper application of SOLID code produces small, focused classes with well-defined roles and boundaries. The practical result is that for any needed requirements change, a minimum number of classes need to be touched. And similarly, for any code change, there is a minimized amount of "ripple" through to other classes.

Looking at the example situation you have, let's see what YAGNI and SOLID might have to say. You are considering a common repository interface due to the fact that all the repositories look the same from the outside. But the value of a common, generic interface is the ability to use any of the implementers without needing to know which one it is in particular. Unless there is somewhere in your app where this would be necessary or useful, YAGNI says don't do it.

There are 5 SOLID principles to look at. S is Single Responsibility. This says nothing about the interface, but it might say something about your concrete classes. It could be argued that handling the data access itself might best be made a responsibility of one or more other classes, while the repositories' responsibility is to translate from an implicit context (CustomerRepository is a repository implicitly for Customer entities) into explicit calls to the generalized data access API specifying the Customer entity type.

O is Open-Closed. This is mostly about inheritance. It would apply if you were trying to derive your repositories from a common base implementing common functionality, or if you expected to derive further from the different repositories. But you're not, so it doesn't.

L is Liskov Substitutability. This applies if you intended to use the repositories through the common repository interface. It places restrictions on the interface and implementations to ensure consistency and avoid special handling for different impelementers. The reason for this is that such special handling undermines the purpose of an interface. It might be useful to consider this principle, because it may warn you away from using the common repository interface. This coincides with YAGNI's guidance.

I is Interface Segregation. This may apply if you start to add different query operations to your repositories. Interface segregation applies where you can divide the members of a class into two subsets where one will be used by certain consumers and the other by others, but no consumer will likely use both subsets. The guidance is to create two separate interfaces, rather than one common one. In your case, it's unlikely that fetching and saving individual instances would be consumed by the same code that would do general querying, so it might be useful to separate those into two interfaces.

D is Dependency Injection. Here we come back to the same point as the S. If you separated your consumption of the data access API into a separate object, this principle says that rather than just newing up an instance of that object, you should pass it in when you create a repository. This makes it easier to control the lifetime of the data access component, opening up the possibility of sharing references to it between your repositories, without having to go the route of making it a singleton.

It's important to note that most of the SOLID principles don't necessarily apply at this particular stage of your app's development. For example, whether you should break out data access depends on how complicated it is, and whether you want to test your repository logic without hitting the database. It sounds like this is unlikely (unfortunately, in my opinion), so it's probably not necessary.

So after all that consideration, we find that YAGNI and SOLID actually do provide one common piece of solid, immediately-relevant advice: It's probably not necessary to create a common generic repository interface.

All this careful thought is extremely useful as a learning exercise. It's time consuming as you learn, but over time you develop intuition and becomes very quick. You'll know the right thing to do, but don't need to think all these words unless someone asks you to explain why.

1
  • I think most the discussion in this page leaves out 2 big contenders "low coupling" and "high cohesion" of the GRASPrinciples. The costlier design decisions stems from "low coupling" principle. Like when to "activate" SRP+ISP+DIP for the sake of low coupling. Example: one class -> 3 classes in MVC pattern. Or even more expensive: split in .dll/.so modules/assemblies. That's very costly because of build implications, projects, makelists, build server, source-versioned files additions...
    – v.oddou
    Commented Jun 30, 2016 at 7:22
8

You seem to believe that 'good design' means following some sort of idealogy and formal set of rules that must always be applied, even when useless.

IMO that's bad design. YAGNI is a component of good design, never a contradiction to it.

2

In your example, I would say that YAGNI should prevail. It won't cost you that much if you need to add interfaces later. By the way, is it really good design to have one interface by class if it serves no goal at all ?

One more thought, maybe sometimes what you need is not Good Design but Sufficient Design. Here are is very interesting sequence of posts on the topic :

1
  • Your first and third links go to the same place. Commented Sep 16, 2011 at 14:18
2

Some people argue, that interface names should not be starting with I. Specifically one reason is, that you're actually leaking the dependency on whether the given type is a class or an interface.

What prohibits you from CustomerFactory being a class at first and later changing it into an interface, that will either be implemented by DefaultCustormerFactory or UberMegaHappyCustomerPowerFactory3000? The only thing you should be having to change is the place, where the implementation gets instantiates. And if you have a more less good design, then this is a handful of places at most.

Refactoring is a part of development. Better have little code, that is easy to refactor, than have an interface and a class declared for every single class, forcing you to change every method name in at least two places at the same time.

The real point in using interfaces is achieving modularity, which is possibly the most important pillar of good design. Note however, that a module is not only defined by its decoupling from the outside world (even though that is how we perceive it from the outside perspective), but equally by its inner working.
What I mean to point out is, that decoupling things, that inherently belong together doesn't make much sense. In a way it is like having a cupboard with an individual shelf per cup.

The importance lies in devising a big, complex problem into smaller, simpler subproblems. And you must stop at the point, where they become simple enough without further subdivisions, or else they will in fact become more complicated. This could be seen as a corollary of YAGNI. And it definitely means good design.

The goal is not to somehow solve a local problem with a single repository and a single factory. The goal is, that this decision has no effect on the rest of your application. That's what modularity is about.
You want your coworkers to look at your module, see a facade with a handful of self-explanatory calls and feel confident, that they can use them, without having to worry about all the potentially sophisticated inner plumbings.

0

You're creating an interface anticipating multiple implementations in the future. You might as well have an I<class> for every class in your codebase. Don't.

Just use the single concrete class according to YAGNI. If you find you need to have a "mock" object for testing purposes, turn the original class into an abstract class with two implementations, one with the original concrete class and the other with the mock implementation.

You will obviously have to update all the instantiations of the original class to instantiate the new concrete class. You can get around that by using a static constructor up front.

YAGNI says not to write code before it needs to be written.

Good design says to use abstraction.

You can have both. A class is an abstraction.

0

Why the marker interfaces? It strikes me that it's doing nothing more than "tagging". With a different "tag" for every factory-type, what's the point?

The purpose of an interface is to give a class "acts like" behavior, to give them "repository-ability" so to speak. So if all your concrete repository-types behave like the same IRepository (they all implement IRepository) then they all can be handled in the same way by other code - by the exact same code. At this point your design is extensible. Adding more concrete repository-types, all handled as generic IRepository(ies) - the same code handles all concrete types as "generic" repositories.

Interfaces are for handling things based on commonalities. But custom marker interfaces a) add no behavior. and b) force you to handle their uniqueness.

To the extent you design useful interfaces you get the OO goodness of not having to write specialized code to handle specialized classes, types, or custom marker interfaces that have a 1:1 correlation to concrete classes. That's pointless redundancy.

Off hand I can see a marker interface if, for example, you needed a strongly typed collection of lots of different classes. In the collection they are all "ImarkerInterface"s but as you pull them out you're having to cast them to their proper types.

0

Can you, right now, write down a vaguely reasonable ICustomerRepository? E.g., (really reaching here, probably a bad example) in the past your customers have always used PayPal? Or everybody in the company is abuzz about hooking up with Alibaba? If so, you might want to use the more complex design now, and appear farsighted to your bosses. :-)

Otherwise, wait. Guessing at a interface before you have one or two actual implementations usually fails. In other words, don't generalize / abstract / use a fancy design pattern until you have a couple of examples to generalize.

Not the answer you're looking for? Browse other questions tagged or ask your own question.