DEV Community

Discussion on: Stop trying to be so DRY, instead Write Everything Twice (WET)

Collapse
 
wuz profile image
Conlin Durbin

Nothing you've written here disregards the idea of writing everything twice. With your sum example, you've taken an exceptionally trite example - adding numbers - to support the idea. What I was talking about with this article was writing real code that handles real business concerns. Often times those functions and components involve complex logic that often time seems duplicated on a surface level, but contains a number of nuances.

As far as refactoring complex functions into smaller functions, once again this only really works if you can adequately determine that the functionality of that complex function - otherwise you might break those out into small functions that don't work together the same way as the original complex function. Side effects, database calls, etc exist and breaking down functions doesn't always just mean breaking them into component parts.

As far as documentation and testing, its a rather bold assumption that most companies have the capacity/foresight to do those things. The idea that consultants should document the developers work is just wrong by the way. Introducing another layer of complexity, this time on an organizational level is a horrible idea.

Funnily enough, you've highlighted the exact problem with trying to DRY out your code too early:

"Should I use numberAdder method or maybe sumNumbers... oh there's another one called sumNums!"
"is it ok to use this one that receives an array or this other one that works with objects?"
"Maybe this one that returns an array of lists would be ok in comparison with this other that returns a matrix..."
"what they do different from one to another apart of having different data structures for inputs and outputs?"

These are all examples of implementation and logic changes that were probably made for a specific reason at a specific time. Assuming you know best how you write an abstraction that handles all of these things, before seeing 2 or 3 use cases of all of these things is a recipe for disaster and a perfect example of premature abstraction.

Finally, telling me I "need to take in mind" that hello worlds and side projects aren't good representations of enterprise software is, at best, rude and, at worst, exceptionally arrogant and assumptive. I've been writing enterprise software for companies with large eng teams for years. I've seen the dangers of premature abstraction far more than I've seen code with too much repetition.

The moral of this whole thing is that following anything to the point of dogma is dangerous and leads to negative impact on your team. WET tries to give you a good level of ambiguity to decide a point at which code can be abstracted without being to dogmatic. WET doesn't really care if you write it twice, thrice, or four times. Just know that you need a good sample size before abstracting out a complex bit of business logic.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

How it works

First of all, the sum example works well because it does a single thing, that's what your functions/methods are mant to do, a single thing, so it can be applied equally to any code because a complex process is just a group of many little things put one after another.

A scientific method works the same when adding 1 test case than when adding 93247829.

Is this the issue?

What I was talking about with this article was writing real code that handles real business concerns. Often times those functions and components involve complex logic that often time seems duplicated on a surface level, but contains a number of nuances. As far as refactoring complex functions into smaller functions, once again this only really works if you can adequately determine that the functionality of that complex function - otherwise you might break those out into small functions that don't work together the same way as the original complex function. Side effects, database calls, etc exist and breaking down functions doesn't always just mean breaking them into component parts.

This is the most stupid thing I've heard in a long time. Try to remember some subjects from the college such logic and then let's apply it to an agnostic example:

// "complex function to deal with business needs"
function featureX(arg) {
  const data = getThings(arg);
  let cookedData;
  let useCase;

  if (BusinessLogicCondition) {
    cookedData = data.split(";");
    useCase = 1;
  } else {
    cookedData = data.split("#");
    useCase = 2;
  }

  let relatedData = [];

  if (useCase == 1) {
    this.Obj.IdProp = cookedData[cookedData.findIndex("userId") + 1];
    let extraData = DBQuery(`select * from table1 t1 where t1.userId like ${this.Obj.IdProp}`);
  } else if (useCase == 2) {
    this.Obj.IdProp = cookedData[cookedData.findIndex("companyId") + 1];
    let extraData = DBQuery(`select * from table2 t2 where t2.companyId like ${this.Obj.IdProp}`);
  }

  relatedData.push(extraData);

  cookedData.forEach((el) => {
    doSomethingWith(el);
  });

  render(relatedData);
}

let's break things down:

const { render } = require("node-sass");

BusinessLogicCondition = true;

function featureX(arg) {
  data = getArrayFromString(args);
  setCurrentIdProps(data);
  relatedData = getRelatedData();
  doSomethingWithData(data);

  render(relatedData);
}

function getArrayFromString(str) {
  return checkBusinessLogic() ? data.split(";") : data.split("#");
}

function checkBusinessLogic() {
  return bussinessLogicCondition ? true : false;
}

setCurrentIdProps(data) {
    checkBusinessLogic() 
    ? this.Obj.IdProp = data[data.findIndex("userId") + 1] 
    : this.Obj.IdProp = data[data.findIndex("companyId") + 1];
}

function getRelatedData() {
  return checkBusinessLogic() ? getRelatedUserData(this.Obj.IdProp) : getRelatedCompanyData(this.Obj.IdProp);
}

function doSomethingWithData(data) {
    data.forEach((el) => {
        doThing(el);
    });
}

I've had a hard time coding the first example tbh and I don't want to loose much time with that, i could show you real complex (really complex actually) business code from a software that more than 100 multi-billionaire companies are using, each with it's own use-cases and config, but a confidentiality contract forbids me so we will have to settle for this example.

Now imagine you need another feature or use case, in the first dirty/shitty example you'll need to put some conditions and dirty behaviour in between the current feature code or repeat the entire feature just for this.
On the other hand, in the second case you can add whatever you need as single responsibility function and then call it in the middle of your stuff, whenever it's needed.
If this new function edit things that other following function needs you can see it easily by checking the current code as it can be read like prose, you read this and you think:

"ok, this is the feature I need to edit, let's see what's up in there, oh ok, it gets the string, convert it into an array splitting by one char or another, ok it sets the props using this received data ID depending on it's company or user, then it gets related data from DB using the same condition, then it does that with this received data (such filters or whatever) and finally it renders the data".

function featureX(arg) {
  data = getArrayFromString(args);
  setCurrentIdProps(data);
  relatedData = getRelatedData();
  doSomethingWithData(data);

  render(relatedData);
}

It will be same readable even when growing because your main function/method has well semantically defined function/method calls that you can read and understand easily one following another. When you read something like that you don't want or need further knowledge that what the main function is telling you.

If you have a function called getArrayFromString but you need to do another function to get an array from a string with different conditions, you can either extend this current one (never exceeding 3/4 args, because if you are exceeding 3/4 args, much probably you can separate this into two different functions/methods) or refactor it like getArrayFromString / getArrayFromQueryString or maybe getArrayFromStringFeature or even getArrayFromCSVString / getArrayFromTaggedString.


The real issue: Our mind in development process

Out of examples, let's go deep in this to address the real issue. We -in our head- code features, most of them conditioned to a state.

Our mind works like this when trying to express ourselves on a way or another, getting considerable chunks of information and understanding a surface overall to find a quick solution to a given problem, while we have a hard time going deep in each information chunk, where we need to strive at the time of talking/writing.

But when reading information it's just the opposite, when we read surface information our brain begin to ask questions and beg for details because we feel the information incomplete.

This is why it's easier for us to code like the first example but refactor it to look more like the second one needs some effort.

It's not bad to code like the first example, it's easier to us, the half of our job is to get things done, the other half is refactor the things that we get working to be more readable and maintainable, thus if you "code, struggle, code, get things done and jump into another thing", you can seem efficient -today- but the entire team will struggle to get things done, release new features and fix bugs in a not-so-far future.

Once you talked about Try to DRY your code too early and premature abstraction I assumed you either have few or little experience with complex projects or get stuck in a bad-driven one for years... If you abstract something taking in mind the current situation and something new needs to be done that interacts with this abstraction... REFACTOR THE ABSTRACTION! and if you refactor your code once you have the things done to "DRY it" you'll end up with a good yet extensive API that, if well documented, the entire team can consume to keep the things ordered and in peace.

Of course the most important thing in this is to keep your tests at the top of the entire process, because if you don't test your software or you have some tests but are outdated, you can't trust your tests, and if you can't trust your tests, you will apply the "if it works, don't touch it" rule, which means that you are afraid of the code, you are afraid of breaking some business process, cause bugs and so on. If you can trust the tests, you'll be able to refactor your code and if the test says it's ok, it's ok.

Our MAIN job as developers is to automate things, not just for clients but also for ourselves thus if you need to code things twice or thrice or... because you want to avoid issues, your project had very bad decisions regarding the underlying architecture, design patterns, methodology and so on.


How to work

There are few ways to achieve a good software product, I'll set one here, you can search for alternatives online:

There are basic rules that can help you with that:

  1. A function/method name should contain a verb
  2. A funciton/method should stick it's concerns to this verb and scope
  3. A function/method must do a single thing

then just follow a workaround that lets you work well:

  1. Code tests first (preferably)
  2. Code your features the way you want till they are working and the tests are passing
  3. Refactor your code according to the method/function rules before
  4. Ensure your tests are good with the refactored code or code the tests now if you didn't on step 1

If you avoid tests or refactor, consultants avoid doing feature documentation and all this avoidance is to get things done quickly, you'll be struggling in a future to get things done, team members will burn out and eventually leave the project. As it's not documented not even well coded, each dev leaving means that you lose valuable knowledge, you need to pay more to the current devs to make them stay here, then they keep here for the salary but are burned anyway so they don't want to struggle refactoring anymore and simply add more and more code to get the things done. The end result? well... at this point you probably already figure it out

Thread Thread
 
wuz profile image
Conlin Durbin

lol ok