How do I know how reusable my methods should be? [closed]
up vote
131
down vote
favorite
I am minding my own business at home and my wife comes to me and says
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
And I am super happy because that was what I had been waiting for my whole life with my Java experience and come up with:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(2018, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
}
);
}
}
But then she says she was just testing me whether I was a ethically-trained software engineer, and tells me it looks like I am not since (taken from here)..
It should be noted that no ethically-trained software engineer would
ever consent to write a DestroyBaghdad procedure. Basic professional
ethics would instead require him to write a DestroyCity procedure, to
which Baghdad could be given as a parameter.
And I am like, fine, ok, you got me.. Pass any year you like, here you go:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings(int year) {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(year, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (year == now.getYear()) {
// rest is same..
But how do I know how much (and what) to parameterize? After all, she might say..
- she wants to pass a custom string formatter, maybe she does not like the format I am already printing in:
2018-10-28T02:00+01:00[Arctic/Longyearbyen]
void dayLightSavings(int year, DateTimeFormatter dtf)
- she is interested in only certain month periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)
- she is interested in certain hour periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)
If you are looking for a concrete question:
If destroyCity(City city)
is better than destroyBaghdad()
, is takeActionOnCity(Action action, City city)
even better? Why / why not?
After all, I can first call it with Action.DESTROY
then Action.REBUILD
, isn't it?
But taking actions on cities is not enough for me, how about takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
? After all, I do not want to call:
takeActionOnCity(Action.DESTORY, City.BAGHDAD);
then
takeActionOnCity(Action.DESTORY, City.ERBIL);
and so on when I can do:
takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);
p.s. I only built my question around the quote I mentioned, I have nothing against any country, religion, race or whatsoever in the world. I am just trying to make a point.
design programming-practices clean-code
closed as too broad by gnat, Deduplicator, Bart van Ingen Schenau, Thomas Owens♦ Dec 2 at 18:56
Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.
|
show 22 more comments
up vote
131
down vote
favorite
I am minding my own business at home and my wife comes to me and says
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
And I am super happy because that was what I had been waiting for my whole life with my Java experience and come up with:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(2018, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
}
);
}
}
But then she says she was just testing me whether I was a ethically-trained software engineer, and tells me it looks like I am not since (taken from here)..
It should be noted that no ethically-trained software engineer would
ever consent to write a DestroyBaghdad procedure. Basic professional
ethics would instead require him to write a DestroyCity procedure, to
which Baghdad could be given as a parameter.
And I am like, fine, ok, you got me.. Pass any year you like, here you go:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings(int year) {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(year, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (year == now.getYear()) {
// rest is same..
But how do I know how much (and what) to parameterize? After all, she might say..
- she wants to pass a custom string formatter, maybe she does not like the format I am already printing in:
2018-10-28T02:00+01:00[Arctic/Longyearbyen]
void dayLightSavings(int year, DateTimeFormatter dtf)
- she is interested in only certain month periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)
- she is interested in certain hour periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)
If you are looking for a concrete question:
If destroyCity(City city)
is better than destroyBaghdad()
, is takeActionOnCity(Action action, City city)
even better? Why / why not?
After all, I can first call it with Action.DESTROY
then Action.REBUILD
, isn't it?
But taking actions on cities is not enough for me, how about takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
? After all, I do not want to call:
takeActionOnCity(Action.DESTORY, City.BAGHDAD);
then
takeActionOnCity(Action.DESTORY, City.ERBIL);
and so on when I can do:
takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);
p.s. I only built my question around the quote I mentioned, I have nothing against any country, religion, race or whatsoever in the world. I am just trying to make a point.
design programming-practices clean-code
closed as too broad by gnat, Deduplicator, Bart van Ingen Schenau, Thomas Owens♦ Dec 2 at 18:56
Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
70
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
30
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
7
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,destroyCity(target)
is way more unethical thandestroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.
– Tezra
Nov 27 at 18:44
25
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14
|
show 22 more comments
up vote
131
down vote
favorite
up vote
131
down vote
favorite
I am minding my own business at home and my wife comes to me and says
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
And I am super happy because that was what I had been waiting for my whole life with my Java experience and come up with:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(2018, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
}
);
}
}
But then she says she was just testing me whether I was a ethically-trained software engineer, and tells me it looks like I am not since (taken from here)..
It should be noted that no ethically-trained software engineer would
ever consent to write a DestroyBaghdad procedure. Basic professional
ethics would instead require him to write a DestroyCity procedure, to
which Baghdad could be given as a parameter.
And I am like, fine, ok, you got me.. Pass any year you like, here you go:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings(int year) {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(year, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (year == now.getYear()) {
// rest is same..
But how do I know how much (and what) to parameterize? After all, she might say..
- she wants to pass a custom string formatter, maybe she does not like the format I am already printing in:
2018-10-28T02:00+01:00[Arctic/Longyearbyen]
void dayLightSavings(int year, DateTimeFormatter dtf)
- she is interested in only certain month periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)
- she is interested in certain hour periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)
If you are looking for a concrete question:
If destroyCity(City city)
is better than destroyBaghdad()
, is takeActionOnCity(Action action, City city)
even better? Why / why not?
After all, I can first call it with Action.DESTROY
then Action.REBUILD
, isn't it?
But taking actions on cities is not enough for me, how about takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
? After all, I do not want to call:
takeActionOnCity(Action.DESTORY, City.BAGHDAD);
then
takeActionOnCity(Action.DESTORY, City.ERBIL);
and so on when I can do:
takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);
p.s. I only built my question around the quote I mentioned, I have nothing against any country, religion, race or whatsoever in the world. I am just trying to make a point.
design programming-practices clean-code
I am minding my own business at home and my wife comes to me and says
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
And I am super happy because that was what I had been waiting for my whole life with my Java experience and come up with:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(2018, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
}
);
}
}
But then she says she was just testing me whether I was a ethically-trained software engineer, and tells me it looks like I am not since (taken from here)..
It should be noted that no ethically-trained software engineer would
ever consent to write a DestroyBaghdad procedure. Basic professional
ethics would instead require him to write a DestroyCity procedure, to
which Baghdad could be given as a parameter.
And I am like, fine, ok, you got me.. Pass any year you like, here you go:
import java.time.*;
import java.util.Set;
class App {
void dayLightSavings(int year) {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(
zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(
LocalDate.of(year, 1, 1),
LocalTime.of(0, 0, 0)
);
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (year == now.getYear()) {
// rest is same..
But how do I know how much (and what) to parameterize? After all, she might say..
- she wants to pass a custom string formatter, maybe she does not like the format I am already printing in:
2018-10-28T02:00+01:00[Arctic/Longyearbyen]
void dayLightSavings(int year, DateTimeFormatter dtf)
- she is interested in only certain month periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd)
- she is interested in certain hour periods
void dayLightSavings(int year, DateTimeFormatter dtf, int monthStart, int monthEnd, int hourStart, int hourend)
If you are looking for a concrete question:
If destroyCity(City city)
is better than destroyBaghdad()
, is takeActionOnCity(Action action, City city)
even better? Why / why not?
After all, I can first call it with Action.DESTROY
then Action.REBUILD
, isn't it?
But taking actions on cities is not enough for me, how about takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
? After all, I do not want to call:
takeActionOnCity(Action.DESTORY, City.BAGHDAD);
then
takeActionOnCity(Action.DESTORY, City.ERBIL);
and so on when I can do:
takeActionOnGeographicArea(Action.DESTORY, Country.IRAQ);
p.s. I only built my question around the quote I mentioned, I have nothing against any country, religion, race or whatsoever in the world. I am just trying to make a point.
design programming-practices clean-code
design programming-practices clean-code
edited Nov 29 at 18:31
asked Nov 27 at 3:18
Koray Tugay
8633814
8633814
closed as too broad by gnat, Deduplicator, Bart van Ingen Schenau, Thomas Owens♦ Dec 2 at 18:56
Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.
closed as too broad by gnat, Deduplicator, Bart van Ingen Schenau, Thomas Owens♦ Dec 2 at 18:56
Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
70
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
30
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
7
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,destroyCity(target)
is way more unethical thandestroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.
– Tezra
Nov 27 at 18:44
25
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14
|
show 22 more comments
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
70
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
30
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
7
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,destroyCity(target)
is way more unethical thandestroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.
– Tezra
Nov 27 at 18:44
25
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14
5
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
70
70
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
30
30
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
7
7
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,
destroyCity(target)
is way more unethical than destroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.– Tezra
Nov 27 at 18:44
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,
destroyCity(target)
is way more unethical than destroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.– Tezra
Nov 27 at 18:44
25
25
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14
|
show 22 more comments
17 Answers
17
active
oldest
votes
up vote
111
down vote
accepted
It's turtles all the way down.
Or abstractions in this case.
Good practice coding is something that can be infinitely applied, and at some point you're abstracting for the sake of abstracting, which means you've taken it too far. Finding that line is not something that's easy to put into a rule of thumb, as it very much depends on your environment.
For example, we've had customers who were known to ask for simple applications first but then ask for expansions. We've also had customers that ask what they want and generally never come back to us for an expansion.
Your approach will vary per customer. For the first customer, it will pay to pre-emptively abstract the code because you're reasonably certain that you'll need to revisit this code in the future. For the second customer, you may not want to invest that extra effort if you're expecting them to not want to expand the application at any point (note: this doesn't mean that you don't follow any good practice, but simply that you avoiding doing any more than is currently necessary.
How do I know which features to implement?
The reason I mention the above is because you've already fallen in this trap:
But how do I know how much (and what) to parameterize? After all, she might say.
"She might say" is not a current business requirement. It's a guess at a future business requirement. As a general rule, do not base yourself on guesses, only develop what's currently required.
However, context applies here. I don't know your wife. Maybe you accurately gauged that she will in fact want this. But you should still confirm with the customer that this is indeed what they want, because otherwise you're going to spend time developing a feature that you're never going to end up using.
How do I know which architecture to implement?
This is trickier. The customer doesn't care about the internal code, so you can't ask them if they need it. Their opinion on the matter is mostly irrelevant.
However, you can still confirm the necessity of doing so by asking the right questions to the customer. Instead of asking about the architecture, ask them about their expectations of future development or expansions to the codebase. You can also ask if the current goal has a deadline, because you may not be able to implement your fancy architecture in the timeframe necessary.
How do I know when to abstract my code further?
I don't know where I read it (if anyone knows, let me know and I'll give credit), but a good rule of thumb is that developers should count like a caveman: one, two many.
XKCD #764
In other words, when a certain algorithm/pattern is being used for a third time, it should be abstracted so that it is reusable (= usable many times).
Just to be clear, I'm not implying that you shouldn't write reusable code when there's only two instances of the algorithm being used. Of course you can abstract that as well, but the rule should be that for three instances you must abstract.
Again, this factors in your expectations. If you already know that you need three or more instances, of course you can immediately abstract. But if you only guess that you might want to implement it more times, the correctness of implementing the abstraction fully relies on the correctness of your guess.
If you guessed correctly, you saved yourself some time. If you guessed wrongly, you wasted some of your time and effort and possibly compromised your architecture to implement something you end up not needing.
If
destroyCity(City city)
is better thandestroyBaghdad()
, istakeActionOnCity(Action action, City city)
even better? Why / why not?
That very much depends on multiple things:
- Are there multiple actions that can be taken on any city?
- Can these actions be used interchangeably? Because if the "destroy" and "rebuild" actions have completely different executions, then there's no point in merging the two in a single
takeActionOnCity
method.
Also be aware that if you recursively abstract this, you're going to end up with a method that's so abstract that it's nothing more than a container to run another method in, which means you've made your method irrelevant and meaningless.
If your entire takeActionOnCity(Action action, City city)
method body ends up being nothing more than action.TakeOn(city);
, you should wonder if the takeActionOnCity
method truly has a purpose or isn't just an extra layer that adds nothing of value.
But taking actions on cities is not enough for me, how about
takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
?
The same question pops up here:
- Do you have a use case for geographical regions?
- Is the execution of an action on a city and a region the same?
- Can any action be taken on any region/city?
If you can definitively answer "yes" to all three, then an abstraction is warranted.
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
|
show 10 more comments
up vote
43
down vote
Practice
This is Software Engineering SE, but crafting software is a lot more art than engineering. There's no universal algorithm to follow or measurement to take to figure out how much reusability is enough. Like with anything, the more practice you get designing programs the better you will get at it. You'll get a better feel for what is "enough" because you'll see what goes wrong and how it goes wrong when you parameterize too much or too little.
That's not very helpful now though, so how about some guidelines?
Look back at your question. There's a lot of "she might say" and "I could". A lot of statements theorizing about some future need. Humans are shite at predicting the future. And you (most likely) are a human. The overwhelming problem of software design is trying to account for a future you don't know.
Guideline 1: You Ain't Gonna Need It
Seriously. Just stop. More often than not, that imagined future problem doesn't show up - and it certainly won't show up just like you imagined it.
Guideline 2: Cost/Benefit
Cool, that little program took you a few hours to write maybe? So what if your wife does come back and ask for those things? Worst case, you spend a few more hours tossing together another program to do it. For this case, it's not too much time to make this program more flexible. And it's not going to add much to the runtime speed or memory usage. But non-trivial programs have different answers. Different scenarios have different answers. At some point, the costs are clearly not worth the benefit even with imperfect future telling skills.
Guideline 3: Focus on constants
Look back at the question. In your original code, there's a lot of constant ints. 2018
, 1
. Constant ints, constant strings... They're the most likely things to need to be not-constant. Better yet, they take only a little time to parameterize (or at least define as actual constants). But another thing to be wary of is constant behavior. The System.out.println
for example. That sort of assumption about use tends to be something that changes in the future and tends to be very costly to fix. Not only that, but IO like this makes the function impure (along with the timezone fetching somewhat). Parameterizing that behavior can make the function more pure leading to increased flexibility and testability. Big benefits with minimal cost (especially if you make an overload that uses System.out
by default).
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
|
show 8 more comments
up vote
26
down vote
Firstly: No security minded software developer would write a DestroyCity method without passing an Authorisation Token for any reason.
I too can write anything as an imperative which has evident wisdom without it being applicable in another context. Why is it necessary to authorise a string concatenation?
Secondly: All code when executed must be fully specified.
It does not matter whether the decision was hard coded in place, or deferred to another layer. At some point there is a piece of code in some language that knows both what is to be destroyed and how to instruct it.
That could be in the same object file destroyCity(xyz)
, and it could be in a configuration file: destroy {"city": "XYZ"}"
, or it might be a series of clicks and keypresses in a UI.
Thirdly:
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
is a very different set of requirements to:
she wants to pass a custom string formatter, ... interested in only certain month periods, ... [and] interested in certain hour periods...
Now the second set of requirements obviously makes for a more flexible tool. It has a broader target audience, and a broader realm of application. The danger here is that the most flexible application in the world is in fact a compiler for machine code. It is literally a program so generic it can build anything to make the computer whatever you need it to be (within the constraints of its hardware).
Generally speaking people who need software do not want something generic; they want something specific. By giving more options you are in fact making their lives more complicated. If they wanted that complexity, they would instead be using a compiler, not asking you.
Your wife was asking for functionality, and under-specified her requirements to you. In this case it was seemingly on purpose, and in general it's because they don't know any better. Otherwise they would have just used the compiler themselves. So the first problem is you didn't ask for more details about exactly what she wanted to do. Did she want to run this for several different years? Did she want it in a CSV file? You didn't find out what decisions she wanted to make herself, and what she was asking you to figure out and decide for her. Once you've figured out what decisions need to be deferred you can figure out how to communicate those decisions through parameters (and other configurable means).
That being said, most clients miss-communicate, presume, or are ignorant of certain details (aka. decisions) that they really would like to make themselves, or that they really didn't want to make (but it sounds awesome). This is why work methods like PDSA (plan-develop-study-act) are important. You've planned the work in line with the requirements, and then you developed a set of decisions (code). Now it's time to study it, either by yourself or with your client and learn new things, and these inform your thinking going forward. Finally act on your new insights - update the requirements, refine the process, get new tools, etc... Then start planning again. This would have revealed any hidden requirements over time, and proves progress to many clients.
Finally. Your time is important; it is very real and very finite. Every decision you make entails many other hidden decisions, and this is what developing software is about. Delaying a decision as an argument may make the current function simpler, but it does make somewhere else more complex. Is that decision relevant in that other location? Is it more relevant here? Whose decision is it really to make? You are deciding this; this is coding. If you repeat sets of decision frequently, there is a very real benefit in codifying them inside some abstraction. XKCD has a useful perspective here. And this is relevant at the level of a system be it a function, module, program, etc.
The advice at the start implies that decisions your function has no right to make should be passed in as an argument. The problem is that a DestroyBaghdad
function might actually be the function that has that right.
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
add a comment |
up vote
4
down vote
There's a lot of long winded answers here, but honestly I think it's super simple
Any hard coded information you have in your function that isn't part
of the function name should be a parameter.
so in your function
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
});
}
}
You have:
The zoneIds
2018, 1, 1
System.out
So I would move all these to parameters in one form or another. You could argue that the zoneIds are implicit in the function name, maybe you would want to make that even more so by changing it to "DaylightSavingsAroundTheWorld" or something
You don't have a format string, so adding one is a feature request and you should refer your wife to your family Jira instance. It can be put on the backlog and prioritised at the appropriate project management committee meeting.
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
|
show 7 more comments
up vote
4
down vote
In short, don't engineer your software for reusability because no end user cares if your functions can be reused. Instead, engineer for design comprehensibility -- is my code easy for someone else or my future forgetful self to understand? -- and design flexibility -- when I inevitably have to fix bugs, add features, or otherwise modify functionality, how much will my code resist the changes? The only thing your customer cares about is how quickly you can respond when she reports a bug or asks for a change. Asking these questions about your design incidentally tends to result in code that is reusable, but this approach keeps you focused on avoiding the real problems you will face over the life of that code so you can better serve the end user rather than pursuing lofty, impractical "engineering" ideals to please the neck-beards.
For something as simple as the example you provided, your initial implementation is fine because of how small it is, but this straightforward design will become hard to understand and brittle if you try to jam too much functional flexibility (as opposed to design flexibility) into one procedure. Below is my explanation of my preferred approach to designing complex systems for comprehensibility and flexibility which I hope will demonstrate what I mean by them. I would not employ this strategy for something that could be written in fewer than 20 lines in a single procedure because something so small already meets my criteria for comprehensibility and flexibility as it is.
Objects, not Procedures
Rather than using classes like old-school modules with a bunch of routines you call to execute the things your software should do, consider modeling the domain as objects which interact and cooperate to accomplish the task at hand. Methods in an Object-Oriented paradigm were originally created to be signals between objects so that Object1
could tell Object2
to do its thing, whatever that is, and possibly receive a return signal. This is because the Object-Oriented paradigm is inherently about modeling your domain objects and their interactions rather than a fancy way to organize the same old functions and procedures of the Imperative paradigm. In the case of the void destroyBaghdad
example, instead of trying to write a context-less generic method to handle the destruction of Baghdad or any other thing (which could quickly grow complex, hard to understand, and brittle), every thing that can be destroyed should be responsible for understanding how to destroy itself. For example, you have an interface that describes the behavior of things that can be destroyed:
interface Destroyable {
void destroy();
}
Then you have a city which implements this interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nothing that calls for the destruction of an instance of City
will ever care how that happens, so there is no reason for that code to exist anywhere outside of City::destroy
, and indeed, intimate knowledge of the inner workings of City
outside of itself would be tight coupling which reduces felxibility since you have to consider those outside elements should you ever need to modify the behavior of City
. This is the true purpose behind encapsulation. Think of it like every object has its own API which should enable you to do anything you need to with it so you can let it worry about fulfilling your requests.
Delegation, not "Control"
Now, whether your implementing class is City
or Baghdad
depends on how generic the process of destroying the city turns out to be. In all probability, a City
will be composed of smaller pieces that will need to be destroyed individually to accomplish the total destruction of the city, so in that case, each of those pieces would also implement Destroyable
, and they would each be instructed by the City
to destroy themselves in the same way someone from outside requested the City
to destroy itself.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
If you want to get really crazy and implement the idea of a Bomb
that is dropped on a location and destroys everything within a certain radius, it might look something like this:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
represents a set of objects that is calculated for the Bomb
from the inputs because the Bomb
does not care how that calculation is made so long as it can work with the objects. This is reusable incidentally, but the main goal is to isolate the calculation from the processes of dropping the Bomb
and destroying the objects so you can comprehend each piece and how they fit together and change the behavior of an individual piece without having to reshape the entire algorithm.
Interactions, not Algorithms
Instead of trying to guess at the right number of parameters for a complex algorithm, it makes more sense to model the process as a set of interacting objects, each with extremely narrow roles, since it will give you the ability to model the complexity of your process through the interactions between these well-defined, easy to comprehend, and nearly unchanging objects. When done correctly, this makes even some of the most complex modifications as trivial as implementing an interface or two and reworking which objects are instantiated in your main()
method.
I'd give you something to your original example, but I honestly can't figure out what it means to "print... Day Light Savings." What I can say about that category of problem is that any time you are performing a calculation, the result of which could be formatted a number of ways, my preferred way to break that down is like this:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Since your example uses classes from the Java library which don't support this design, you could just use the API of ZonedDateTime
directly. The idea here is that each calculation is encapsulated within its own object. It makes no assumptions about how many times it should run or how it should format the result. It is exclusively concerned with performing the simplest form of the calculation. This makes it both easy to understand and flexible to change. Likewise, the Result
is exclusively concerned with encapsulating the result of the calculation, and the FormattedResult
is exclusively concerned with interacting with the Result
to format it according to the rules we define. In this way, we can find the perfect number of arguments for each of our methods since they each have a well-defined task. It's also much simpler to modify moving forward so long as the interfaces don't change (which they aren't as likely to do if you've properly minimized the responsibilities of your objects). Our main()
method might look like this:
class App {
public static void main(String args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
As a matter of fact, Object-Oriented Programming was invented specifically as a solution to the complexity/flexibility problem of the Imperative paradigm because there is just no good answer (that everyone can agree on or arrive at independently, anyhow) to how to optimally specify Imperative functions and procedures within the idiom.
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
|
show 4 more comments
up vote
3
down vote
Experience, Domain Knowledge, and Code Reviews.
And, regardless of how much or how little experience, domain knowledge, or team you have, you cannot avoid the need to refactor as-needed.
With Experience, you'll start to recognize patterns in the domain-nonspecific methods (and classes) you write. And, if you're at all interested in DRY code, you'll feel bad feelings when you're about a write a method that you instinctively know you'll write variations of in the future. So, you'll intuitively write a parametrized least common denominator instead.
(This experience may transfer over instinctively into some your domain objects and methods too.)
With Domain Knowledge, you'll have a sense for which business concepts are closely related, which concepts have variables, which are fairly static, etc..
With Code Reviews, under- and over-parametrization will more likely be caught before it becomes production code, because your peers will (hopefully) have unique experiences and perspectives, both on the domain and coding in general.
That said, new developers won't generally have these Spidey Senses or an experienced group of peers to lean on right away. And, even experienced developers benefit from a basic discipline to guide them through new requirements — or through brain-foggy days. So, here's what I'd suggest as a start:
- Start with the naive implementation, with minimal parametrization.
(Include any parameters you already know you'll need, obviously ...)
- Remove magic numbers and strings, moving them to configs and/or parameters
- Factor "large" methods down into smaller, well-named methods
- Refactor highly redundant methods (if convenient) into a common denominator, parametrizing the differences.
These steps don't necessarily occur in the stated order. If you sit down to write a method you already know to be highly redundant with an existing method, jump straight into refactoring if it's convenient. (If it's not going to take significantly more time to refactor than it would be to just write, test, and maintain two methods.)
But, apart from just having lots of experience and so forth, I advise pretty minimalistic code DRY-ing. It's not hard to refactor obvious violations. And, if you're too zealous, you can end up with "over-DRY" code that's even harder to read, understand, and maintain than the "WET" equivalent.
2
So there is no right answer toIf destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong,destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?
– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from thedestroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ...destroyBaghdad()
might be a perfectly reasonable method name/signature ...
– svidgen
Nov 27 at 20:31
1
So you do not agree with the cited quote in my question, right?It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer tois takeActionOnCity(Action action, City city) better?
null
?
– Koray Tugay
Nov 27 at 20:52
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
|
show 1 more comment
up vote
2
down vote
The same answer as with quality, usability, technical debt etc:
As reusable as you, the user,1 need them to be
It's basically a judgement call -- whether the cost of designing and maintaining the abstraction will be repayed by the cost (=time and effort) it will save you down the line.
- Note the "down the line" phrase: there's a payoff mechanic here, so it will depend on how much you will be working with this code further. E.g.:
- Is this a one-off project, or is it going to be progressively improved over a long time?
- Are you confident in your design, or will you likely have to scrap or otherwise drastically change it for the next project/milestone (e.g. try another framework)?
- The projected benefit also depends on your ability to predict the future (changes to the app). Sometimes, you can reasonably see the venue(s) your app is going to take. More times, you think you can but you actually cannot. The rules of thumb here is the YAGNI principle and the rule of three -- both emphasize working off of what you know, now.
1This is a code construct, so you're the "user" in this case -- the user of the source code
add a comment |
up vote
1
down vote
There's a clear process you can follow:
- Write a failing test for a single feature which is in itself a "thing" (i.e., not some arbitrary split of a feature where neither half really makes sense).
- Write the absolute minimum code to make it pass green, not a line more.
- Rinse and repeat.
- (Refactor relentlessly if necessary, which should be easy due to the great test coverage.)
This turns up with - at least in the opinion of some people - pretty much optimal code, since it is as small as possible, each finished feature takes as little time as possible (which might or might not be true if you look at the finished product after refactoring), and it has very good test coverage. It also noticeably avoids over-engineered too-generic methods or classes.
This also gives you very clear instructions when to make things generic and when to specialize.
I find your city example weird; I would very likely never ever hardcode a city name. It is so obvious that additional cities will be included later, whatever it is you're doing. But another example would be colors. In some circumstances, hardcoding "red" or "green" would be a possibility. For example, traffic lights are such an ubiquitous color that you can just get away with it (and you can always refactor). The difference is that "red" and "green" have universal, "hardcoded" meaning in our world, it is incredibly unlikely that it will ever change, and there is not really an alternative either.
Your first daylight savings method is simply broken. While it conforms to the specifications, the hardcoded 2018 is particularly bad because a) it is not mentioned in the technical "contract" (in the method name, in this case), and b) it will be out of date soon, so breakage is included from the get-go. For things that are time/date related, it would very seldomly make sense to hardcode a specific value since, well, time moves on. But apart from that, everything else is up for discussion. If you give it a simple year and then always calculate the complete year, go ahead. Most of the things you listed (formatting, choice of a smaller range, etc.) screams that your method is doing too much, and it should instead probably return a list/array of values so the caller can do the formatting/filtering themselves.
But at the end of the day, most of this is opinion, taste, experience and personal bias, so don't fret too much about it.
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
add a comment |
up vote
1
down vote
I've come to the opinion that there are two sorts of reusable code:
- Code which is reusable because it's such a fundamental, basic thing.
- Code which is reusable because it has parameters, overrides and hooks for everywhere.
The first sort of reusability is often a good idea. It applies to things like lists, hashmaps, key/value stores, string matchers (e.g. regex, glob, ...), tuples, unification, search trees (depth-first, breadth-first, iterative-deepening, ...), parser combinators, caches/memoisers, data format readers/writers (s-expressions, XML, JSON, protobuf, ...), task queues, etc.
These things are so general, in a very abstract way, that they're re-used all over the place in day to day programming. If you find yourself writing special-purpose code that would be simpler if it were made more abstract/general (e.g. if we have "a list of customer orders", we could throw away the "customer order" stuff to get "a list") then it might be a good idea to pull that out. Even if it doesn't get re-used, it lets us decouple unrelated functionality.
The second sort is where we have some concrete code, which solves a real issue, but does so by making a whole bunch of decisions. We can make it more general/reusable by "soft-coding" those decisions, e.g. turning them into parameters, complicating the implementation and baking in even more concrete details (i.e. knowledge of which hooks we might want for overrides). Your example seems to be of this sort. The problem with this sort of reusability is that we may end up trying to guess at the use-cases of other people, or our future selves. Eventually we might end up having so many parameters that our code isn't usable, let alone reusable! In other words, when calling it takes more effort than just writing our own version. This is where YAGNI (You Ain't Gonna Need It) is important. Many times, such attempts at "reusable" code end up not being reused, since it may be incompatable with those use-cases more fundamentally than parameters can account for, or those potential users would rather roll their own (heck, look at all the standards and libraries out there whose authors prefixed with the word "Simple", to distinguish them from the predecessors!).
This second form of "reusability" should basically be done on an as-needed basis. Sure, you can stick some "obvious" parameters in there, but don't start trying to predict the future. YAGNI.
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what ifdestroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.
– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form ofstatic
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.
– Marco13
Dec 1 at 21:24
|
show 1 more comment
up vote
0
down vote
A good rule of thumb is: your method should be as reusable as… reusable.
If you expect that you will call your method only in one place, it should have only parameters that are known to the call site and that are not available to this method.
If you have more callers, you can introduce new parameters as long as other callers may pass those parameters; otherwise you need new method.
As the number of callers may grow in time, you need to be prepared for refactoring or overloading. In many cases it means that you should feel safe to select expression and run “extract parameter” action of your IDE.
add a comment |
up vote
0
down vote
Ultra short answer: The less coupling or dependency to other code your generic module has the more reusable it can be.
Your example only depends on
import java.time.*;
import java.util.Set;
so in theory it can be highly reusable.
In practise i donot think that you will ever have a second usecase that needs this code so following yagni principle i would not make it reusable if there are not more than 3 different projets that need this code.
Other aspects of reusability are ease of use and doocumentation which corelate with Test Driven Development: It is helpfull if you have a simple unit-test that demonstrates/documents an easy use of your generic module as a coding example for users of your lib.
add a comment |
up vote
0
down vote
This is a good opportunity to state a rule I coined recently:
Being a good programmer means being able to predict the future.
Of course, this is strictly impossible! After all, you never know for sure what generalisations you'll find useful later on, which related tasks you'll want to perform, what new features your users will want, &c. But experience sometimes gives you a rough idea of what might come in handy.
The other factors you have to balance that against are how much extra time and effort would be involved, and how much more complex would it make your code. Sometimes you're lucky, and solving the more general problem is actually simpler! (At least conceptually, if not in the amount of code.) But more often, there's a complexity cost as well as one of time and effort.
So if you think a generalisation is very likely to be needed, it's often worth doing (unless it adds a lot of work or complexity); but if it seems much less likely, then it's probably not (unless it's very easy and/or simplifies the code).
(For a recent example: last week I was given a spec. for actions a system should take exactly 2 days after something expired. So of course I made the 2-day period a parameter. This week the business folks were delighted, as they were about to ask for that enhancement! I was lucky: it was an easy change, and I guessed it was quite likely to be wanted. Often it's harder to judge. But it's still worth trying to predict, and experience is often a good guide.)
add a comment |
up vote
0
down vote
Firstly, the best answer to 'How do I know how reusable my methods should be?" is "experience." Do this a few thousand times, and you typically get the right answer. But as a teaser, I can give you the last line of this answer: Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Many of these answers have specific pieces of advice. I wanted to give something more generic... because the irony is too much fun to pass up!
As some of the answers have noted, generality is expensive. However, it really isn't. Not always. Understanding the expense is essential to playing the reusability game.
I focus on putting things on a scale from "irreversable" to "reversable." It's a smooth scale. The only truly irreversable thing is "time spent on the project." You'll never get those resources back. Slightly less reversable might be "golden handcuffs" situations such as the Windows API. Deprecated features remain in that API for decades because Microsoft's business model calls for it. If you have customers whose relationship would be permanently damaged by undoing some API feature, then that should be treated as irreversable. Looking at the other end of the scale, you have things like prototype code. If you don't like where it goes, you can simply throw it away. Slightly less reversable might be internal use APIs. They can be refactored without bothering a customer, but they may cost more time (the most irreversable resource of all!)
So put these on a scale. Now you can apply a heuristic: the more reversable something is, the more you can use it for future looking activities. If something is irreversable, only use it for concrete customer-driven tasks. This is why you see principles like those from extreme programming which suggest only doing what the customer asks for and nothing more. These principles are good at making sure you don't do something you regret.
Things like the DRY principle suggest a way to move that balance. If you find yourself repeating yourself, that's an opportunity to create what's basically an internal API. No customer sees it, so you can always change it. Once you have this internal API, now you can start playing with forward looking things. How many different timezone based tasks do you think your wife is going to give you? Do you have any other customers who might want timezone based tasks? Your flexibility here is bought by the concrete demands of your current customers, and it supports the potential future demands of future customers.
This layered thinking approach, which comes naturally from DRY naturally provides the generalization you want without waste. But is there a limit? Of course there is. But to see it, you have to see the forest for the trees.
If you have many layers of flexiblity, they often lead to a lack of direct control of the layers that face your customers. I've had software where I've had the brutal task of explaining to a customer why they can't have what they want due to flexibility built in 10 layers down which they were never supposed to see. We wrote ourselves into a corner. We tied ourselves in a knot with all the flexibility we thought we needed.
So when you're doing this generalization/DRY trick, always keep a pulse on your customer. What do you think your wife is going to ask for next? Are you putting yourself in a position to fulfil those needs? If you have the knack, the customer will effectively tell you their future needs. If you don't have the knack, well, most of us just rely on guesswork! (especially with spouses!) Some customers will want great flexibility, and be willing to accept the extra cost of you developing with all these layers because they directly benefit from those layers' flexibility. Other customers have rather fixed unwavering requirements, and they prefer development be more direct. Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? AlsoYour customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?
– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
add a comment |
up vote
0
down vote
no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
This is something known in advanced software engineering circles as a "joke". Jokes do not have to be what we call "true", although to be funny they generally must hint at something true.
In this particular case, the "joke" is not "true". The work involved in writing a general procedure to destroy any city is, we can safely assume, orders of magnitude beyond that required to destroy one specific city. Otherwise, anybody who has destroyed one or a handful of cities (the biblical Joshua, shall we say, or President Truman) could trivially generalise what they did and be able to destroy absolutely any city at will. This in point of fact is not the case. The methods that those two people famously used to destroy a small number of specific cities would not necessarily work on just any city at any time. Another city whose walls had different resonant frequency or whose high-altitude air defences were rather better, would need either minor or fundamental changes of approach (a differently-pitched trumpet or a rocket).
This also leads to code maintenance against changes over time: there are rather a lot of cities now that would fall to neither of those approaches, thanks to modern construction methods and ubiquitous radar.
Developing and testing a completely general means that will destroy any city, before you agree to destro just one city, is a desperately inefficient approach. No ethically-trained software engineer would try to generalise a problem to an extent requiring orders of magnitude more work than their employer/client actually needs to pay for, without demonstrated requirement.
So what is true? Sometimes adding generality is trivial. Should we then always add generality when it is trivial to do so? I would still argue "no, not always", because of the long-term maintenance issue. Supposing that at time of writing, all cities are basically the same, so I go ahead with DestroyCity. Once I've written this, along with integration tests that (due to the finitely enumerable space of inputs) iterate over every known city and make sure the function works on each (not sure how that works. Probably calls City.clone() and destroys the clone? Anyway).
In practice the function is used solely to destroy Baghdad, suppose somebody builds a new city that is resistant to my techniques (it's deep underground or something). Now I have an integration test failure for a use-case that doesn't even really exist, and before I can continue my campaign of terror against the innocent civilians of Iraq, I have to figure out how to destroy Subterrania. Never mind whether this is ethical or not, it is dumb and a waste of my freaking time.
So, do you really want a function that can output daylight savings for any year, just to output data for 2018? Maybe, but it's certainly going to require a small amount of extra effort just putting together test cases. It may require a large amount of effort to get a better timezone database than the one you actually have. So for example in 1908, the town of Port Arthur, Ontario had a period of DST commencing on the 1st of July. Is that in your OS's timezone database? Thought not, so your generalised function is wrong. There's nothing especially ethical about writing code that makes promises it can't keep.
Alright, so, with appropriate caveats it's easy to write a function that does timezones for a range of years, say 1970-present day. But it's just as easy to take the function that you actually wrote, and generalise it to parameterise the year. So it's not really any more ethical/sensible to generalise now, that it is to do what you did and then generalise if and when you need it.
However, if you knew why your wife wanted to check this DST list, then you would have an informed opinion of whether she's likely to ask the same question again in 2019 and, if so, whether you can take yourself out of that loop by giving her a function that she can call, without needing to recompile it. Once you've done that analysis, the answer to the question "should this generalise to recent years" might be "yes". But you create yet another problem for yourself, which is that timezone data in the future is only provisional, and therefore if she runs it for 2019 today, she may or may not realise that it's feeding her a best-guess. So you still have to write a bunch of documentation which would not be needed for the less general function ("the data comes from timezone database blah blah here's the link to see their policies on pushing updates blah blah"). If you refuse the special case, then all the time you're doing that, she can't get on with the task for which she needed the 2018 data, because of some nonsense about 2019 she doesn't even care about yet.
Do not do difficult things without thinking them through properly, just because a joke told you to. Is it useful? Is it cheap enough for that degree of usefulness?
add a comment |
up vote
0
down vote
There already are many excellent and elaborate answers. Some of them go deeply into specific details, lay out certain viewpoints on software development methodologies in general, and some of them certainly have controversial elements or "opinions" sprinkled in.
The answer by Warbo already pointed out different types of reusability. Namely, whether something is reusable because it is a fundamental building block, or whether something is reusable because it is "generic" in some way. Referring to the latter, there is something that I'd consider as some sort of measure for reusability:
Whether one method can emulate another.
Regarding the example from the question: Imagine that the method
void dayLightSavings()
was the implementation of a functionality that was requested by a customer. So it will be something that other programmers are supposed to use, and thus, be a public method, as in
public
void dayLightSavings()
This could be implemented as you showed in your answer. Now, someone wants to parameterize it with the year. So you can add a method
public
void dayLightSavings(int year)
and change the original implementation to just be
public void dayLightSavings() {
dayLightSavings(2018);
}
The next "feature requests" and generalizations follow the same pattern. So if and only if there is demand for the most generic form, you can implement it, knowing that this most generic form allows for trivial implementations of the more specific ones:
public void dayLightSavings() {
dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}
If you had anticipated future extensions and feature requests, and had some time at your disposal and wanted to spend a boring weekend with (potentially useless) generalizations, you could have started with the most generic one right from the beginning. But only as a private method. As long as you only exposed the simple method that was requested by the customer as the public method, you're safe.
tl;dr:
The question is actually not so much "how reusable a method should be". The question is how much of this reusability is exposed, and what the API looks like. Creating a reliable API that can stand the test of time (even when further requirements come up later) is an art and a craft, and the topic is far too complex to cover it here. Have a look at this presentation by Joshua Bloch or the API design book wiki for a start.
dayLightSavings()
callingdayLightSavings(2018)
does not seem like a good idea to me.
– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd calldayLightSavings(computeCurrentYear());
. ...
– Marco13
Dec 1 at 22:44
add a comment |
up vote
0
down vote
This is an easy line to draw because re-usability as defined by architecture astronauts is bollocks.
Almost all code created by application developers is extremely domain specific. This isn't 1980. Almost everything worth the bother is already in a framework.
Abstractions and conventions require documentation and learning effort. Kindly stop creating new ones just for the sake of it. (I'm looking at you, JavaScript people!)
Let's indulge the implausible fantasy that you've found something that genuinely ought to be in your framework of choice. You can't just bang up the code the way you usually do. Oh no, you need test coverage not just for intended use but also for departures from intended use, for all known edge cases, for all imaginable failure modes, test cases for diagnostics, test data, technical documentation, user documentation, release management, support scripts, regression tests, change management...
Is your employer happy to pay for all that? I'm going to say no.
Abstraction is the price we pay for flexibility. It makes our code more complex and harder to understand. Unless the flexibility serves a real and present need, just don't, because YAGNI.
Let's have a look at a real world example I just had to deal with: HTMLRenderer. It wasn't scaling properly when I tried to render to the device context of a printer. It took me all day to discover that by default it was using GDI (which doesn't scale) rather than GDI+ (which does, but doesn't antialias) because I had to wade through six levels of indirection in two assemblies before I found code that did anything.
In this case I'll forgive the author. The abstraction is actually necessary because this is framework code that targets five very different rendering targets: WinForms, WPF, dotnet Core, Mono and PdfSharp.
But that only underscores my point: you almost certainly are not doing something extremely complex (HTML rendering with stylesheets) targeting multiple platforms with a stated goal of high performance on all platforms.
Your code is almost certainly yet another database grid with business rules that only apply to your employer and tax rules that only apply in your state, in an application that isn't for sale.
All that indirection solves a problem you don't have and makes your code much harder to read, which massively increases the cost of maintenance and is a huge disservice to your employer. Luckily the people who ought to complain about this are unable to comprehend what you are doing to them.
A counter-argument is that this kind of abstraction supports test driven development, but I think TDD is bollocks too, because it presupposes that the business has a clear, complete and correct understanding of its requirements. TDD is great for NASA and control software for medical equipment and self-driving cars, but way too expensive for everyone else.
Incidentally, it's not possible to predict all the daylight savings in the world. Israel in particular has about 40 transitions every year that hop all over the place because we can't have people praying at the wrong time and god doesn't do daylight savings.
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
add a comment |
up vote
-3
down vote
If you are using at least java 8, you would write the WorldTimeZones class to provide what in essence appears to be a collection of time zones.
Then add a filter(Predicate filter) method to the WorldTimeZones class. This provides the ability for the caller to filter on anything they want by passing a lambda expression as the parameter.
In essence, the single filter method supports filtering on anything contained in the value passed to the predicate.
Alternately, add a stream() method to your WorldTimeZones class that produces a stream of time zones when called. Then the caller can filter, map and reduce as desired without you writing any specializations at all.
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
add a comment |
protected by gnat Nov 28 at 5:18
Thank you for your interest in this question.
Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).
Would you like to answer one of these unanswered questions instead?
17 Answers
17
active
oldest
votes
17 Answers
17
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
111
down vote
accepted
It's turtles all the way down.
Or abstractions in this case.
Good practice coding is something that can be infinitely applied, and at some point you're abstracting for the sake of abstracting, which means you've taken it too far. Finding that line is not something that's easy to put into a rule of thumb, as it very much depends on your environment.
For example, we've had customers who were known to ask for simple applications first but then ask for expansions. We've also had customers that ask what they want and generally never come back to us for an expansion.
Your approach will vary per customer. For the first customer, it will pay to pre-emptively abstract the code because you're reasonably certain that you'll need to revisit this code in the future. For the second customer, you may not want to invest that extra effort if you're expecting them to not want to expand the application at any point (note: this doesn't mean that you don't follow any good practice, but simply that you avoiding doing any more than is currently necessary.
How do I know which features to implement?
The reason I mention the above is because you've already fallen in this trap:
But how do I know how much (and what) to parameterize? After all, she might say.
"She might say" is not a current business requirement. It's a guess at a future business requirement. As a general rule, do not base yourself on guesses, only develop what's currently required.
However, context applies here. I don't know your wife. Maybe you accurately gauged that she will in fact want this. But you should still confirm with the customer that this is indeed what they want, because otherwise you're going to spend time developing a feature that you're never going to end up using.
How do I know which architecture to implement?
This is trickier. The customer doesn't care about the internal code, so you can't ask them if they need it. Their opinion on the matter is mostly irrelevant.
However, you can still confirm the necessity of doing so by asking the right questions to the customer. Instead of asking about the architecture, ask them about their expectations of future development or expansions to the codebase. You can also ask if the current goal has a deadline, because you may not be able to implement your fancy architecture in the timeframe necessary.
How do I know when to abstract my code further?
I don't know where I read it (if anyone knows, let me know and I'll give credit), but a good rule of thumb is that developers should count like a caveman: one, two many.
XKCD #764
In other words, when a certain algorithm/pattern is being used for a third time, it should be abstracted so that it is reusable (= usable many times).
Just to be clear, I'm not implying that you shouldn't write reusable code when there's only two instances of the algorithm being used. Of course you can abstract that as well, but the rule should be that for three instances you must abstract.
Again, this factors in your expectations. If you already know that you need three or more instances, of course you can immediately abstract. But if you only guess that you might want to implement it more times, the correctness of implementing the abstraction fully relies on the correctness of your guess.
If you guessed correctly, you saved yourself some time. If you guessed wrongly, you wasted some of your time and effort and possibly compromised your architecture to implement something you end up not needing.
If
destroyCity(City city)
is better thandestroyBaghdad()
, istakeActionOnCity(Action action, City city)
even better? Why / why not?
That very much depends on multiple things:
- Are there multiple actions that can be taken on any city?
- Can these actions be used interchangeably? Because if the "destroy" and "rebuild" actions have completely different executions, then there's no point in merging the two in a single
takeActionOnCity
method.
Also be aware that if you recursively abstract this, you're going to end up with a method that's so abstract that it's nothing more than a container to run another method in, which means you've made your method irrelevant and meaningless.
If your entire takeActionOnCity(Action action, City city)
method body ends up being nothing more than action.TakeOn(city);
, you should wonder if the takeActionOnCity
method truly has a purpose or isn't just an extra layer that adds nothing of value.
But taking actions on cities is not enough for me, how about
takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
?
The same question pops up here:
- Do you have a use case for geographical regions?
- Is the execution of an action on a city and a region the same?
- Can any action be taken on any region/city?
If you can definitively answer "yes" to all three, then an abstraction is warranted.
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
|
show 10 more comments
up vote
111
down vote
accepted
It's turtles all the way down.
Or abstractions in this case.
Good practice coding is something that can be infinitely applied, and at some point you're abstracting for the sake of abstracting, which means you've taken it too far. Finding that line is not something that's easy to put into a rule of thumb, as it very much depends on your environment.
For example, we've had customers who were known to ask for simple applications first but then ask for expansions. We've also had customers that ask what they want and generally never come back to us for an expansion.
Your approach will vary per customer. For the first customer, it will pay to pre-emptively abstract the code because you're reasonably certain that you'll need to revisit this code in the future. For the second customer, you may not want to invest that extra effort if you're expecting them to not want to expand the application at any point (note: this doesn't mean that you don't follow any good practice, but simply that you avoiding doing any more than is currently necessary.
How do I know which features to implement?
The reason I mention the above is because you've already fallen in this trap:
But how do I know how much (and what) to parameterize? After all, she might say.
"She might say" is not a current business requirement. It's a guess at a future business requirement. As a general rule, do not base yourself on guesses, only develop what's currently required.
However, context applies here. I don't know your wife. Maybe you accurately gauged that she will in fact want this. But you should still confirm with the customer that this is indeed what they want, because otherwise you're going to spend time developing a feature that you're never going to end up using.
How do I know which architecture to implement?
This is trickier. The customer doesn't care about the internal code, so you can't ask them if they need it. Their opinion on the matter is mostly irrelevant.
However, you can still confirm the necessity of doing so by asking the right questions to the customer. Instead of asking about the architecture, ask them about their expectations of future development or expansions to the codebase. You can also ask if the current goal has a deadline, because you may not be able to implement your fancy architecture in the timeframe necessary.
How do I know when to abstract my code further?
I don't know where I read it (if anyone knows, let me know and I'll give credit), but a good rule of thumb is that developers should count like a caveman: one, two many.
XKCD #764
In other words, when a certain algorithm/pattern is being used for a third time, it should be abstracted so that it is reusable (= usable many times).
Just to be clear, I'm not implying that you shouldn't write reusable code when there's only two instances of the algorithm being used. Of course you can abstract that as well, but the rule should be that for three instances you must abstract.
Again, this factors in your expectations. If you already know that you need three or more instances, of course you can immediately abstract. But if you only guess that you might want to implement it more times, the correctness of implementing the abstraction fully relies on the correctness of your guess.
If you guessed correctly, you saved yourself some time. If you guessed wrongly, you wasted some of your time and effort and possibly compromised your architecture to implement something you end up not needing.
If
destroyCity(City city)
is better thandestroyBaghdad()
, istakeActionOnCity(Action action, City city)
even better? Why / why not?
That very much depends on multiple things:
- Are there multiple actions that can be taken on any city?
- Can these actions be used interchangeably? Because if the "destroy" and "rebuild" actions have completely different executions, then there's no point in merging the two in a single
takeActionOnCity
method.
Also be aware that if you recursively abstract this, you're going to end up with a method that's so abstract that it's nothing more than a container to run another method in, which means you've made your method irrelevant and meaningless.
If your entire takeActionOnCity(Action action, City city)
method body ends up being nothing more than action.TakeOn(city);
, you should wonder if the takeActionOnCity
method truly has a purpose or isn't just an extra layer that adds nothing of value.
But taking actions on cities is not enough for me, how about
takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
?
The same question pops up here:
- Do you have a use case for geographical regions?
- Is the execution of an action on a city and a region the same?
- Can any action be taken on any region/city?
If you can definitively answer "yes" to all three, then an abstraction is warranted.
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
|
show 10 more comments
up vote
111
down vote
accepted
up vote
111
down vote
accepted
It's turtles all the way down.
Or abstractions in this case.
Good practice coding is something that can be infinitely applied, and at some point you're abstracting for the sake of abstracting, which means you've taken it too far. Finding that line is not something that's easy to put into a rule of thumb, as it very much depends on your environment.
For example, we've had customers who were known to ask for simple applications first but then ask for expansions. We've also had customers that ask what they want and generally never come back to us for an expansion.
Your approach will vary per customer. For the first customer, it will pay to pre-emptively abstract the code because you're reasonably certain that you'll need to revisit this code in the future. For the second customer, you may not want to invest that extra effort if you're expecting them to not want to expand the application at any point (note: this doesn't mean that you don't follow any good practice, but simply that you avoiding doing any more than is currently necessary.
How do I know which features to implement?
The reason I mention the above is because you've already fallen in this trap:
But how do I know how much (and what) to parameterize? After all, she might say.
"She might say" is not a current business requirement. It's a guess at a future business requirement. As a general rule, do not base yourself on guesses, only develop what's currently required.
However, context applies here. I don't know your wife. Maybe you accurately gauged that she will in fact want this. But you should still confirm with the customer that this is indeed what they want, because otherwise you're going to spend time developing a feature that you're never going to end up using.
How do I know which architecture to implement?
This is trickier. The customer doesn't care about the internal code, so you can't ask them if they need it. Their opinion on the matter is mostly irrelevant.
However, you can still confirm the necessity of doing so by asking the right questions to the customer. Instead of asking about the architecture, ask them about their expectations of future development or expansions to the codebase. You can also ask if the current goal has a deadline, because you may not be able to implement your fancy architecture in the timeframe necessary.
How do I know when to abstract my code further?
I don't know where I read it (if anyone knows, let me know and I'll give credit), but a good rule of thumb is that developers should count like a caveman: one, two many.
XKCD #764
In other words, when a certain algorithm/pattern is being used for a third time, it should be abstracted so that it is reusable (= usable many times).
Just to be clear, I'm not implying that you shouldn't write reusable code when there's only two instances of the algorithm being used. Of course you can abstract that as well, but the rule should be that for three instances you must abstract.
Again, this factors in your expectations. If you already know that you need three or more instances, of course you can immediately abstract. But if you only guess that you might want to implement it more times, the correctness of implementing the abstraction fully relies on the correctness of your guess.
If you guessed correctly, you saved yourself some time. If you guessed wrongly, you wasted some of your time and effort and possibly compromised your architecture to implement something you end up not needing.
If
destroyCity(City city)
is better thandestroyBaghdad()
, istakeActionOnCity(Action action, City city)
even better? Why / why not?
That very much depends on multiple things:
- Are there multiple actions that can be taken on any city?
- Can these actions be used interchangeably? Because if the "destroy" and "rebuild" actions have completely different executions, then there's no point in merging the two in a single
takeActionOnCity
method.
Also be aware that if you recursively abstract this, you're going to end up with a method that's so abstract that it's nothing more than a container to run another method in, which means you've made your method irrelevant and meaningless.
If your entire takeActionOnCity(Action action, City city)
method body ends up being nothing more than action.TakeOn(city);
, you should wonder if the takeActionOnCity
method truly has a purpose or isn't just an extra layer that adds nothing of value.
But taking actions on cities is not enough for me, how about
takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
?
The same question pops up here:
- Do you have a use case for geographical regions?
- Is the execution of an action on a city and a region the same?
- Can any action be taken on any region/city?
If you can definitively answer "yes" to all three, then an abstraction is warranted.
It's turtles all the way down.
Or abstractions in this case.
Good practice coding is something that can be infinitely applied, and at some point you're abstracting for the sake of abstracting, which means you've taken it too far. Finding that line is not something that's easy to put into a rule of thumb, as it very much depends on your environment.
For example, we've had customers who were known to ask for simple applications first but then ask for expansions. We've also had customers that ask what they want and generally never come back to us for an expansion.
Your approach will vary per customer. For the first customer, it will pay to pre-emptively abstract the code because you're reasonably certain that you'll need to revisit this code in the future. For the second customer, you may not want to invest that extra effort if you're expecting them to not want to expand the application at any point (note: this doesn't mean that you don't follow any good practice, but simply that you avoiding doing any more than is currently necessary.
How do I know which features to implement?
The reason I mention the above is because you've already fallen in this trap:
But how do I know how much (and what) to parameterize? After all, she might say.
"She might say" is not a current business requirement. It's a guess at a future business requirement. As a general rule, do not base yourself on guesses, only develop what's currently required.
However, context applies here. I don't know your wife. Maybe you accurately gauged that she will in fact want this. But you should still confirm with the customer that this is indeed what they want, because otherwise you're going to spend time developing a feature that you're never going to end up using.
How do I know which architecture to implement?
This is trickier. The customer doesn't care about the internal code, so you can't ask them if they need it. Their opinion on the matter is mostly irrelevant.
However, you can still confirm the necessity of doing so by asking the right questions to the customer. Instead of asking about the architecture, ask them about their expectations of future development or expansions to the codebase. You can also ask if the current goal has a deadline, because you may not be able to implement your fancy architecture in the timeframe necessary.
How do I know when to abstract my code further?
I don't know where I read it (if anyone knows, let me know and I'll give credit), but a good rule of thumb is that developers should count like a caveman: one, two many.
XKCD #764
In other words, when a certain algorithm/pattern is being used for a third time, it should be abstracted so that it is reusable (= usable many times).
Just to be clear, I'm not implying that you shouldn't write reusable code when there's only two instances of the algorithm being used. Of course you can abstract that as well, but the rule should be that for three instances you must abstract.
Again, this factors in your expectations. If you already know that you need three or more instances, of course you can immediately abstract. But if you only guess that you might want to implement it more times, the correctness of implementing the abstraction fully relies on the correctness of your guess.
If you guessed correctly, you saved yourself some time. If you guessed wrongly, you wasted some of your time and effort and possibly compromised your architecture to implement something you end up not needing.
If
destroyCity(City city)
is better thandestroyBaghdad()
, istakeActionOnCity(Action action, City city)
even better? Why / why not?
That very much depends on multiple things:
- Are there multiple actions that can be taken on any city?
- Can these actions be used interchangeably? Because if the "destroy" and "rebuild" actions have completely different executions, then there's no point in merging the two in a single
takeActionOnCity
method.
Also be aware that if you recursively abstract this, you're going to end up with a method that's so abstract that it's nothing more than a container to run another method in, which means you've made your method irrelevant and meaningless.
If your entire takeActionOnCity(Action action, City city)
method body ends up being nothing more than action.TakeOn(city);
, you should wonder if the takeActionOnCity
method truly has a purpose or isn't just an extra layer that adds nothing of value.
But taking actions on cities is not enough for me, how about
takeActionOnGeographicArea(Action action, GeographicalArea GeographicalArea)
?
The same question pops up here:
- Do you have a use case for geographical regions?
- Is the execution of an action on a city and a region the same?
- Can any action be taken on any region/city?
If you can definitively answer "yes" to all three, then an abstraction is warranted.
edited Nov 27 at 7:56
answered Nov 27 at 7:51
Flater
6,65521221
6,65521221
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
|
show 10 more comments
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
16
16
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
I cannot stress the "one, two, many" rule enough. There are infinite possibilities to abstract /parametrize something, but the useful subset is small, often zero. Knowing exactly which variant has value can most often only be determined in retrospect. So stick to the immediate requirements* and add complexity as needed by new requirements or hindsight. * Sometimes you know the problem space well, then it might be ok to add something because you know you need it tomorrow. But use this power wisely, it can also lead to ruin.
– Christian Sauer
Nov 28 at 8:39
2
2
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
Nov 28 at 10:06
9
9
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
The "one, two, many rule" is really there to prevent you from building the wrong abstraction by applying DRY blindly. The thing is, two pieces of code can start out looking almost exactly the same, so it's tempting to abstract the differences away; but early on, you don't know which parts of the code are stable, and which are not; besides, it could turn out that they actually need to evolve independently (different change patterns, different sets of responsibilities). In either case, a wrong abstraction works against you and gets in the way.
– Filip Milovanović
Nov 28 at 12:10
4
4
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
Waiting for more than two examples of "the same logic" allows you to be a better judge of what should be abstracted, and how (and really, it's about managing dependencies/coupling between code with different change patterns).
– Filip Milovanović
Nov 28 at 12:14
1
1
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
@kukis: The realistic line should be drawn at 2 (as per Baldrickk's comment): zero-one-many (as is the case for database relations). However, this opens the door to needless pattern seeking behavior. Two things may vaguely look alike but that does not mean they are actually the same. However, when a third instance enters the fray which resembles both the first and second instance, you can make a more accurate judgment that their similarities are indeed a reusable pattern. So the common sense line is drawn at 3, which factors in human error when spotting "patterns" between only two instances.
– Flater
Nov 29 at 13:31
|
show 10 more comments
up vote
43
down vote
Practice
This is Software Engineering SE, but crafting software is a lot more art than engineering. There's no universal algorithm to follow or measurement to take to figure out how much reusability is enough. Like with anything, the more practice you get designing programs the better you will get at it. You'll get a better feel for what is "enough" because you'll see what goes wrong and how it goes wrong when you parameterize too much or too little.
That's not very helpful now though, so how about some guidelines?
Look back at your question. There's a lot of "she might say" and "I could". A lot of statements theorizing about some future need. Humans are shite at predicting the future. And you (most likely) are a human. The overwhelming problem of software design is trying to account for a future you don't know.
Guideline 1: You Ain't Gonna Need It
Seriously. Just stop. More often than not, that imagined future problem doesn't show up - and it certainly won't show up just like you imagined it.
Guideline 2: Cost/Benefit
Cool, that little program took you a few hours to write maybe? So what if your wife does come back and ask for those things? Worst case, you spend a few more hours tossing together another program to do it. For this case, it's not too much time to make this program more flexible. And it's not going to add much to the runtime speed or memory usage. But non-trivial programs have different answers. Different scenarios have different answers. At some point, the costs are clearly not worth the benefit even with imperfect future telling skills.
Guideline 3: Focus on constants
Look back at the question. In your original code, there's a lot of constant ints. 2018
, 1
. Constant ints, constant strings... They're the most likely things to need to be not-constant. Better yet, they take only a little time to parameterize (or at least define as actual constants). But another thing to be wary of is constant behavior. The System.out.println
for example. That sort of assumption about use tends to be something that changes in the future and tends to be very costly to fix. Not only that, but IO like this makes the function impure (along with the timezone fetching somewhat). Parameterizing that behavior can make the function more pure leading to increased flexibility and testability. Big benefits with minimal cost (especially if you make an overload that uses System.out
by default).
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
|
show 8 more comments
up vote
43
down vote
Practice
This is Software Engineering SE, but crafting software is a lot more art than engineering. There's no universal algorithm to follow or measurement to take to figure out how much reusability is enough. Like with anything, the more practice you get designing programs the better you will get at it. You'll get a better feel for what is "enough" because you'll see what goes wrong and how it goes wrong when you parameterize too much or too little.
That's not very helpful now though, so how about some guidelines?
Look back at your question. There's a lot of "she might say" and "I could". A lot of statements theorizing about some future need. Humans are shite at predicting the future. And you (most likely) are a human. The overwhelming problem of software design is trying to account for a future you don't know.
Guideline 1: You Ain't Gonna Need It
Seriously. Just stop. More often than not, that imagined future problem doesn't show up - and it certainly won't show up just like you imagined it.
Guideline 2: Cost/Benefit
Cool, that little program took you a few hours to write maybe? So what if your wife does come back and ask for those things? Worst case, you spend a few more hours tossing together another program to do it. For this case, it's not too much time to make this program more flexible. And it's not going to add much to the runtime speed or memory usage. But non-trivial programs have different answers. Different scenarios have different answers. At some point, the costs are clearly not worth the benefit even with imperfect future telling skills.
Guideline 3: Focus on constants
Look back at the question. In your original code, there's a lot of constant ints. 2018
, 1
. Constant ints, constant strings... They're the most likely things to need to be not-constant. Better yet, they take only a little time to parameterize (or at least define as actual constants). But another thing to be wary of is constant behavior. The System.out.println
for example. That sort of assumption about use tends to be something that changes in the future and tends to be very costly to fix. Not only that, but IO like this makes the function impure (along with the timezone fetching somewhat). Parameterizing that behavior can make the function more pure leading to increased flexibility and testability. Big benefits with minimal cost (especially if you make an overload that uses System.out
by default).
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
|
show 8 more comments
up vote
43
down vote
up vote
43
down vote
Practice
This is Software Engineering SE, but crafting software is a lot more art than engineering. There's no universal algorithm to follow or measurement to take to figure out how much reusability is enough. Like with anything, the more practice you get designing programs the better you will get at it. You'll get a better feel for what is "enough" because you'll see what goes wrong and how it goes wrong when you parameterize too much or too little.
That's not very helpful now though, so how about some guidelines?
Look back at your question. There's a lot of "she might say" and "I could". A lot of statements theorizing about some future need. Humans are shite at predicting the future. And you (most likely) are a human. The overwhelming problem of software design is trying to account for a future you don't know.
Guideline 1: You Ain't Gonna Need It
Seriously. Just stop. More often than not, that imagined future problem doesn't show up - and it certainly won't show up just like you imagined it.
Guideline 2: Cost/Benefit
Cool, that little program took you a few hours to write maybe? So what if your wife does come back and ask for those things? Worst case, you spend a few more hours tossing together another program to do it. For this case, it's not too much time to make this program more flexible. And it's not going to add much to the runtime speed or memory usage. But non-trivial programs have different answers. Different scenarios have different answers. At some point, the costs are clearly not worth the benefit even with imperfect future telling skills.
Guideline 3: Focus on constants
Look back at the question. In your original code, there's a lot of constant ints. 2018
, 1
. Constant ints, constant strings... They're the most likely things to need to be not-constant. Better yet, they take only a little time to parameterize (or at least define as actual constants). But another thing to be wary of is constant behavior. The System.out.println
for example. That sort of assumption about use tends to be something that changes in the future and tends to be very costly to fix. Not only that, but IO like this makes the function impure (along with the timezone fetching somewhat). Parameterizing that behavior can make the function more pure leading to increased flexibility and testability. Big benefits with minimal cost (especially if you make an overload that uses System.out
by default).
Practice
This is Software Engineering SE, but crafting software is a lot more art than engineering. There's no universal algorithm to follow or measurement to take to figure out how much reusability is enough. Like with anything, the more practice you get designing programs the better you will get at it. You'll get a better feel for what is "enough" because you'll see what goes wrong and how it goes wrong when you parameterize too much or too little.
That's not very helpful now though, so how about some guidelines?
Look back at your question. There's a lot of "she might say" and "I could". A lot of statements theorizing about some future need. Humans are shite at predicting the future. And you (most likely) are a human. The overwhelming problem of software design is trying to account for a future you don't know.
Guideline 1: You Ain't Gonna Need It
Seriously. Just stop. More often than not, that imagined future problem doesn't show up - and it certainly won't show up just like you imagined it.
Guideline 2: Cost/Benefit
Cool, that little program took you a few hours to write maybe? So what if your wife does come back and ask for those things? Worst case, you spend a few more hours tossing together another program to do it. For this case, it's not too much time to make this program more flexible. And it's not going to add much to the runtime speed or memory usage. But non-trivial programs have different answers. Different scenarios have different answers. At some point, the costs are clearly not worth the benefit even with imperfect future telling skills.
Guideline 3: Focus on constants
Look back at the question. In your original code, there's a lot of constant ints. 2018
, 1
. Constant ints, constant strings... They're the most likely things to need to be not-constant. Better yet, they take only a little time to parameterize (or at least define as actual constants). But another thing to be wary of is constant behavior. The System.out.println
for example. That sort of assumption about use tends to be something that changes in the future and tends to be very costly to fix. Not only that, but IO like this makes the function impure (along with the timezone fetching somewhat). Parameterizing that behavior can make the function more pure leading to increased flexibility and testability. Big benefits with minimal cost (especially if you make an overload that uses System.out
by default).
answered Nov 27 at 4:13
Telastyn
91.8k24207317
91.8k24207317
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
|
show 8 more comments
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
1
1
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
It’s just a guideline, the 1s are fine but you look at them and go “will this ever change?” Nah. And the println could be parameterized with a higher order function - though Java is not great at those.
– Telastyn
Nov 27 at 4:50
5
5
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
@KorayTugay: if the program was really for your wife coming at home, YAGNI would tell you that your initial version is perfect, and you should not invest any more time to introduce constants or parameters. YAGNI needs context - is your program a throw-away solution, or a migration program run only for a few months, or is it part of a huge ERP system, intended to be used and maintained over several decades?
– Doc Brown
Nov 27 at 4:55
8
8
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
@KorayTugay: Separating I/O from computation is a fundamental program structuring technique. Separate generation of data from filtering of data from transformation of data from consumption of data from presentation of data. You should study some functional programs, then you will see this more clearly. In functional programming, it is quite common to generate an infinite amount of data, filter out only the data you are interested in, transform the data into the format you need, construct a string from it, and print this string in 5 different functions, one for each step.
– Jörg W Mittag
Nov 27 at 7:21
3
3
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
As a sidenote, strongly following YAGNI leads to needing to continuously refactor: "Used without continuous refactoring, it could lead to disorganized code and massive rework, known as technical debt." So while YAGNI is a good thing in general, it comes with a great responsibility of revisiting and reevaluating code, which is not something every developer/company is willing to do.
– Flater
Nov 27 at 7:52
4
4
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
@Telastyn: I suggest expanding the question to “will this never change and is the intention of the code trivially readable without naming the constant?” Even for values that never change, it may be relevant to name them just to keep things readable.
– Flater
Nov 27 at 8:12
|
show 8 more comments
up vote
26
down vote
Firstly: No security minded software developer would write a DestroyCity method without passing an Authorisation Token for any reason.
I too can write anything as an imperative which has evident wisdom without it being applicable in another context. Why is it necessary to authorise a string concatenation?
Secondly: All code when executed must be fully specified.
It does not matter whether the decision was hard coded in place, or deferred to another layer. At some point there is a piece of code in some language that knows both what is to be destroyed and how to instruct it.
That could be in the same object file destroyCity(xyz)
, and it could be in a configuration file: destroy {"city": "XYZ"}"
, or it might be a series of clicks and keypresses in a UI.
Thirdly:
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
is a very different set of requirements to:
she wants to pass a custom string formatter, ... interested in only certain month periods, ... [and] interested in certain hour periods...
Now the second set of requirements obviously makes for a more flexible tool. It has a broader target audience, and a broader realm of application. The danger here is that the most flexible application in the world is in fact a compiler for machine code. It is literally a program so generic it can build anything to make the computer whatever you need it to be (within the constraints of its hardware).
Generally speaking people who need software do not want something generic; they want something specific. By giving more options you are in fact making their lives more complicated. If they wanted that complexity, they would instead be using a compiler, not asking you.
Your wife was asking for functionality, and under-specified her requirements to you. In this case it was seemingly on purpose, and in general it's because they don't know any better. Otherwise they would have just used the compiler themselves. So the first problem is you didn't ask for more details about exactly what she wanted to do. Did she want to run this for several different years? Did she want it in a CSV file? You didn't find out what decisions she wanted to make herself, and what she was asking you to figure out and decide for her. Once you've figured out what decisions need to be deferred you can figure out how to communicate those decisions through parameters (and other configurable means).
That being said, most clients miss-communicate, presume, or are ignorant of certain details (aka. decisions) that they really would like to make themselves, or that they really didn't want to make (but it sounds awesome). This is why work methods like PDSA (plan-develop-study-act) are important. You've planned the work in line with the requirements, and then you developed a set of decisions (code). Now it's time to study it, either by yourself or with your client and learn new things, and these inform your thinking going forward. Finally act on your new insights - update the requirements, refine the process, get new tools, etc... Then start planning again. This would have revealed any hidden requirements over time, and proves progress to many clients.
Finally. Your time is important; it is very real and very finite. Every decision you make entails many other hidden decisions, and this is what developing software is about. Delaying a decision as an argument may make the current function simpler, but it does make somewhere else more complex. Is that decision relevant in that other location? Is it more relevant here? Whose decision is it really to make? You are deciding this; this is coding. If you repeat sets of decision frequently, there is a very real benefit in codifying them inside some abstraction. XKCD has a useful perspective here. And this is relevant at the level of a system be it a function, module, program, etc.
The advice at the start implies that decisions your function has no right to make should be passed in as an argument. The problem is that a DestroyBaghdad
function might actually be the function that has that right.
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
add a comment |
up vote
26
down vote
Firstly: No security minded software developer would write a DestroyCity method without passing an Authorisation Token for any reason.
I too can write anything as an imperative which has evident wisdom without it being applicable in another context. Why is it necessary to authorise a string concatenation?
Secondly: All code when executed must be fully specified.
It does not matter whether the decision was hard coded in place, or deferred to another layer. At some point there is a piece of code in some language that knows both what is to be destroyed and how to instruct it.
That could be in the same object file destroyCity(xyz)
, and it could be in a configuration file: destroy {"city": "XYZ"}"
, or it might be a series of clicks and keypresses in a UI.
Thirdly:
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
is a very different set of requirements to:
she wants to pass a custom string formatter, ... interested in only certain month periods, ... [and] interested in certain hour periods...
Now the second set of requirements obviously makes for a more flexible tool. It has a broader target audience, and a broader realm of application. The danger here is that the most flexible application in the world is in fact a compiler for machine code. It is literally a program so generic it can build anything to make the computer whatever you need it to be (within the constraints of its hardware).
Generally speaking people who need software do not want something generic; they want something specific. By giving more options you are in fact making their lives more complicated. If they wanted that complexity, they would instead be using a compiler, not asking you.
Your wife was asking for functionality, and under-specified her requirements to you. In this case it was seemingly on purpose, and in general it's because they don't know any better. Otherwise they would have just used the compiler themselves. So the first problem is you didn't ask for more details about exactly what she wanted to do. Did she want to run this for several different years? Did she want it in a CSV file? You didn't find out what decisions she wanted to make herself, and what she was asking you to figure out and decide for her. Once you've figured out what decisions need to be deferred you can figure out how to communicate those decisions through parameters (and other configurable means).
That being said, most clients miss-communicate, presume, or are ignorant of certain details (aka. decisions) that they really would like to make themselves, or that they really didn't want to make (but it sounds awesome). This is why work methods like PDSA (plan-develop-study-act) are important. You've planned the work in line with the requirements, and then you developed a set of decisions (code). Now it's time to study it, either by yourself or with your client and learn new things, and these inform your thinking going forward. Finally act on your new insights - update the requirements, refine the process, get new tools, etc... Then start planning again. This would have revealed any hidden requirements over time, and proves progress to many clients.
Finally. Your time is important; it is very real and very finite. Every decision you make entails many other hidden decisions, and this is what developing software is about. Delaying a decision as an argument may make the current function simpler, but it does make somewhere else more complex. Is that decision relevant in that other location? Is it more relevant here? Whose decision is it really to make? You are deciding this; this is coding. If you repeat sets of decision frequently, there is a very real benefit in codifying them inside some abstraction. XKCD has a useful perspective here. And this is relevant at the level of a system be it a function, module, program, etc.
The advice at the start implies that decisions your function has no right to make should be passed in as an argument. The problem is that a DestroyBaghdad
function might actually be the function that has that right.
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
add a comment |
up vote
26
down vote
up vote
26
down vote
Firstly: No security minded software developer would write a DestroyCity method without passing an Authorisation Token for any reason.
I too can write anything as an imperative which has evident wisdom without it being applicable in another context. Why is it necessary to authorise a string concatenation?
Secondly: All code when executed must be fully specified.
It does not matter whether the decision was hard coded in place, or deferred to another layer. At some point there is a piece of code in some language that knows both what is to be destroyed and how to instruct it.
That could be in the same object file destroyCity(xyz)
, and it could be in a configuration file: destroy {"city": "XYZ"}"
, or it might be a series of clicks and keypresses in a UI.
Thirdly:
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
is a very different set of requirements to:
she wants to pass a custom string formatter, ... interested in only certain month periods, ... [and] interested in certain hour periods...
Now the second set of requirements obviously makes for a more flexible tool. It has a broader target audience, and a broader realm of application. The danger here is that the most flexible application in the world is in fact a compiler for machine code. It is literally a program so generic it can build anything to make the computer whatever you need it to be (within the constraints of its hardware).
Generally speaking people who need software do not want something generic; they want something specific. By giving more options you are in fact making their lives more complicated. If they wanted that complexity, they would instead be using a compiler, not asking you.
Your wife was asking for functionality, and under-specified her requirements to you. In this case it was seemingly on purpose, and in general it's because they don't know any better. Otherwise they would have just used the compiler themselves. So the first problem is you didn't ask for more details about exactly what she wanted to do. Did she want to run this for several different years? Did she want it in a CSV file? You didn't find out what decisions she wanted to make herself, and what she was asking you to figure out and decide for her. Once you've figured out what decisions need to be deferred you can figure out how to communicate those decisions through parameters (and other configurable means).
That being said, most clients miss-communicate, presume, or are ignorant of certain details (aka. decisions) that they really would like to make themselves, or that they really didn't want to make (but it sounds awesome). This is why work methods like PDSA (plan-develop-study-act) are important. You've planned the work in line with the requirements, and then you developed a set of decisions (code). Now it's time to study it, either by yourself or with your client and learn new things, and these inform your thinking going forward. Finally act on your new insights - update the requirements, refine the process, get new tools, etc... Then start planning again. This would have revealed any hidden requirements over time, and proves progress to many clients.
Finally. Your time is important; it is very real and very finite. Every decision you make entails many other hidden decisions, and this is what developing software is about. Delaying a decision as an argument may make the current function simpler, but it does make somewhere else more complex. Is that decision relevant in that other location? Is it more relevant here? Whose decision is it really to make? You are deciding this; this is coding. If you repeat sets of decision frequently, there is a very real benefit in codifying them inside some abstraction. XKCD has a useful perspective here. And this is relevant at the level of a system be it a function, module, program, etc.
The advice at the start implies that decisions your function has no right to make should be passed in as an argument. The problem is that a DestroyBaghdad
function might actually be the function that has that right.
Firstly: No security minded software developer would write a DestroyCity method without passing an Authorisation Token for any reason.
I too can write anything as an imperative which has evident wisdom without it being applicable in another context. Why is it necessary to authorise a string concatenation?
Secondly: All code when executed must be fully specified.
It does not matter whether the decision was hard coded in place, or deferred to another layer. At some point there is a piece of code in some language that knows both what is to be destroyed and how to instruct it.
That could be in the same object file destroyCity(xyz)
, and it could be in a configuration file: destroy {"city": "XYZ"}"
, or it might be a series of clicks and keypresses in a UI.
Thirdly:
Honey.. Can you print all the Day Light Savings around the world for 2018 in the console? I need to check something.
is a very different set of requirements to:
she wants to pass a custom string formatter, ... interested in only certain month periods, ... [and] interested in certain hour periods...
Now the second set of requirements obviously makes for a more flexible tool. It has a broader target audience, and a broader realm of application. The danger here is that the most flexible application in the world is in fact a compiler for machine code. It is literally a program so generic it can build anything to make the computer whatever you need it to be (within the constraints of its hardware).
Generally speaking people who need software do not want something generic; they want something specific. By giving more options you are in fact making their lives more complicated. If they wanted that complexity, they would instead be using a compiler, not asking you.
Your wife was asking for functionality, and under-specified her requirements to you. In this case it was seemingly on purpose, and in general it's because they don't know any better. Otherwise they would have just used the compiler themselves. So the first problem is you didn't ask for more details about exactly what she wanted to do. Did she want to run this for several different years? Did she want it in a CSV file? You didn't find out what decisions she wanted to make herself, and what she was asking you to figure out and decide for her. Once you've figured out what decisions need to be deferred you can figure out how to communicate those decisions through parameters (and other configurable means).
That being said, most clients miss-communicate, presume, or are ignorant of certain details (aka. decisions) that they really would like to make themselves, or that they really didn't want to make (but it sounds awesome). This is why work methods like PDSA (plan-develop-study-act) are important. You've planned the work in line with the requirements, and then you developed a set of decisions (code). Now it's time to study it, either by yourself or with your client and learn new things, and these inform your thinking going forward. Finally act on your new insights - update the requirements, refine the process, get new tools, etc... Then start planning again. This would have revealed any hidden requirements over time, and proves progress to many clients.
Finally. Your time is important; it is very real and very finite. Every decision you make entails many other hidden decisions, and this is what developing software is about. Delaying a decision as an argument may make the current function simpler, but it does make somewhere else more complex. Is that decision relevant in that other location? Is it more relevant here? Whose decision is it really to make? You are deciding this; this is coding. If you repeat sets of decision frequently, there is a very real benefit in codifying them inside some abstraction. XKCD has a useful perspective here. And this is relevant at the level of a system be it a function, module, program, etc.
The advice at the start implies that decisions your function has no right to make should be passed in as an argument. The problem is that a DestroyBaghdad
function might actually be the function that has that right.
edited Nov 28 at 8:35
Peter Mortensen
1,11621114
1,11621114
answered Nov 27 at 4:57
Kain0_0
1,372110
1,372110
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
add a comment |
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
+1 love the part about the compiler!
– Lee
Nov 28 at 9:10
add a comment |
up vote
4
down vote
There's a lot of long winded answers here, but honestly I think it's super simple
Any hard coded information you have in your function that isn't part
of the function name should be a parameter.
so in your function
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
});
}
}
You have:
The zoneIds
2018, 1, 1
System.out
So I would move all these to parameters in one form or another. You could argue that the zoneIds are implicit in the function name, maybe you would want to make that even more so by changing it to "DaylightSavingsAroundTheWorld" or something
You don't have a format string, so adding one is a feature request and you should refer your wife to your family Jira instance. It can be put on the backlog and prioritised at the appropriate project management committee meeting.
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
|
show 7 more comments
up vote
4
down vote
There's a lot of long winded answers here, but honestly I think it's super simple
Any hard coded information you have in your function that isn't part
of the function name should be a parameter.
so in your function
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
});
}
}
You have:
The zoneIds
2018, 1, 1
System.out
So I would move all these to parameters in one form or another. You could argue that the zoneIds are implicit in the function name, maybe you would want to make that even more so by changing it to "DaylightSavingsAroundTheWorld" or something
You don't have a format string, so adding one is a feature request and you should refer your wife to your family Jira instance. It can be put on the backlog and prioritised at the appropriate project management committee meeting.
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
|
show 7 more comments
up vote
4
down vote
up vote
4
down vote
There's a lot of long winded answers here, but honestly I think it's super simple
Any hard coded information you have in your function that isn't part
of the function name should be a parameter.
so in your function
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
});
}
}
You have:
The zoneIds
2018, 1, 1
System.out
So I would move all these to parameters in one form or another. You could argue that the zoneIds are implicit in the function name, maybe you would want to make that even more so by changing it to "DaylightSavingsAroundTheWorld" or something
You don't have a format string, so adding one is a feature request and you should refer your wife to your family Jira instance. It can be put on the backlog and prioritised at the appropriate project management committee meeting.
There's a lot of long winded answers here, but honestly I think it's super simple
Any hard coded information you have in your function that isn't part
of the function name should be a parameter.
so in your function
class App {
void dayLightSavings() {
final Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
availableZoneIds.forEach(zoneId -> {
LocalDateTime dateTime = LocalDateTime.of(LocalDate.of(2018, 1, 1), LocalTime.of(0, 0, 0));
ZonedDateTime now = ZonedDateTime.of(dateTime, ZoneId.of(zoneId));
while (2018 == now.getYear()) {
int hour = now.getHour();
now = now.plusHours(1);
if (now.getHour() == hour) {
System.out.println(now);
}
}
});
}
}
You have:
The zoneIds
2018, 1, 1
System.out
So I would move all these to parameters in one form or another. You could argue that the zoneIds are implicit in the function name, maybe you would want to make that even more so by changing it to "DaylightSavingsAroundTheWorld" or something
You don't have a format string, so adding one is a feature request and you should refer your wife to your family Jira instance. It can be put on the backlog and prioritised at the appropriate project management committee meeting.
answered Nov 27 at 10:39
Ewan
37.8k32983
37.8k32983
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
|
show 7 more comments
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
1
1
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
You (addressing myself to OP) certainly should not add a format string, since you shouldn't be printing anything. The one thing about this code that absolutely prevents its reuse is that it prints. It should return the zones, or a map of the zones to when they go off DST. (Although why it only identifies when the go off DST, and not on DST, I don't understand. It doesn't seem to match the problem statement.)
– David Conrad
Nov 28 at 18:00
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
the requirement is to print to console. you can mitgate the tight couplung by passing in the output stream as a parameter as i suggest
– Ewan
Nov 28 at 18:21
1
1
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
Even so, if you want the code to be reusable, you shouldn't print to the console. Write a method that returns the results, and then write a caller that gets them and prints. That also makes it testable. If you do want to have it produce output, I wouldn't pass in an output stream, I would pass in a Consumer.
– David Conrad
Nov 28 at 18:52
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
an ourput stream is a consumer
– Ewan
Nov 28 at 20:07
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
No, an OutputStream is not a Consumer.
– David Conrad
Nov 28 at 20:52
|
show 7 more comments
up vote
4
down vote
In short, don't engineer your software for reusability because no end user cares if your functions can be reused. Instead, engineer for design comprehensibility -- is my code easy for someone else or my future forgetful self to understand? -- and design flexibility -- when I inevitably have to fix bugs, add features, or otherwise modify functionality, how much will my code resist the changes? The only thing your customer cares about is how quickly you can respond when she reports a bug or asks for a change. Asking these questions about your design incidentally tends to result in code that is reusable, but this approach keeps you focused on avoiding the real problems you will face over the life of that code so you can better serve the end user rather than pursuing lofty, impractical "engineering" ideals to please the neck-beards.
For something as simple as the example you provided, your initial implementation is fine because of how small it is, but this straightforward design will become hard to understand and brittle if you try to jam too much functional flexibility (as opposed to design flexibility) into one procedure. Below is my explanation of my preferred approach to designing complex systems for comprehensibility and flexibility which I hope will demonstrate what I mean by them. I would not employ this strategy for something that could be written in fewer than 20 lines in a single procedure because something so small already meets my criteria for comprehensibility and flexibility as it is.
Objects, not Procedures
Rather than using classes like old-school modules with a bunch of routines you call to execute the things your software should do, consider modeling the domain as objects which interact and cooperate to accomplish the task at hand. Methods in an Object-Oriented paradigm were originally created to be signals between objects so that Object1
could tell Object2
to do its thing, whatever that is, and possibly receive a return signal. This is because the Object-Oriented paradigm is inherently about modeling your domain objects and their interactions rather than a fancy way to organize the same old functions and procedures of the Imperative paradigm. In the case of the void destroyBaghdad
example, instead of trying to write a context-less generic method to handle the destruction of Baghdad or any other thing (which could quickly grow complex, hard to understand, and brittle), every thing that can be destroyed should be responsible for understanding how to destroy itself. For example, you have an interface that describes the behavior of things that can be destroyed:
interface Destroyable {
void destroy();
}
Then you have a city which implements this interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nothing that calls for the destruction of an instance of City
will ever care how that happens, so there is no reason for that code to exist anywhere outside of City::destroy
, and indeed, intimate knowledge of the inner workings of City
outside of itself would be tight coupling which reduces felxibility since you have to consider those outside elements should you ever need to modify the behavior of City
. This is the true purpose behind encapsulation. Think of it like every object has its own API which should enable you to do anything you need to with it so you can let it worry about fulfilling your requests.
Delegation, not "Control"
Now, whether your implementing class is City
or Baghdad
depends on how generic the process of destroying the city turns out to be. In all probability, a City
will be composed of smaller pieces that will need to be destroyed individually to accomplish the total destruction of the city, so in that case, each of those pieces would also implement Destroyable
, and they would each be instructed by the City
to destroy themselves in the same way someone from outside requested the City
to destroy itself.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
If you want to get really crazy and implement the idea of a Bomb
that is dropped on a location and destroys everything within a certain radius, it might look something like this:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
represents a set of objects that is calculated for the Bomb
from the inputs because the Bomb
does not care how that calculation is made so long as it can work with the objects. This is reusable incidentally, but the main goal is to isolate the calculation from the processes of dropping the Bomb
and destroying the objects so you can comprehend each piece and how they fit together and change the behavior of an individual piece without having to reshape the entire algorithm.
Interactions, not Algorithms
Instead of trying to guess at the right number of parameters for a complex algorithm, it makes more sense to model the process as a set of interacting objects, each with extremely narrow roles, since it will give you the ability to model the complexity of your process through the interactions between these well-defined, easy to comprehend, and nearly unchanging objects. When done correctly, this makes even some of the most complex modifications as trivial as implementing an interface or two and reworking which objects are instantiated in your main()
method.
I'd give you something to your original example, but I honestly can't figure out what it means to "print... Day Light Savings." What I can say about that category of problem is that any time you are performing a calculation, the result of which could be formatted a number of ways, my preferred way to break that down is like this:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Since your example uses classes from the Java library which don't support this design, you could just use the API of ZonedDateTime
directly. The idea here is that each calculation is encapsulated within its own object. It makes no assumptions about how many times it should run or how it should format the result. It is exclusively concerned with performing the simplest form of the calculation. This makes it both easy to understand and flexible to change. Likewise, the Result
is exclusively concerned with encapsulating the result of the calculation, and the FormattedResult
is exclusively concerned with interacting with the Result
to format it according to the rules we define. In this way, we can find the perfect number of arguments for each of our methods since they each have a well-defined task. It's also much simpler to modify moving forward so long as the interfaces don't change (which they aren't as likely to do if you've properly minimized the responsibilities of your objects). Our main()
method might look like this:
class App {
public static void main(String args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
As a matter of fact, Object-Oriented Programming was invented specifically as a solution to the complexity/flexibility problem of the Imperative paradigm because there is just no good answer (that everyone can agree on or arrive at independently, anyhow) to how to optimally specify Imperative functions and procedures within the idiom.
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
|
show 4 more comments
up vote
4
down vote
In short, don't engineer your software for reusability because no end user cares if your functions can be reused. Instead, engineer for design comprehensibility -- is my code easy for someone else or my future forgetful self to understand? -- and design flexibility -- when I inevitably have to fix bugs, add features, or otherwise modify functionality, how much will my code resist the changes? The only thing your customer cares about is how quickly you can respond when she reports a bug or asks for a change. Asking these questions about your design incidentally tends to result in code that is reusable, but this approach keeps you focused on avoiding the real problems you will face over the life of that code so you can better serve the end user rather than pursuing lofty, impractical "engineering" ideals to please the neck-beards.
For something as simple as the example you provided, your initial implementation is fine because of how small it is, but this straightforward design will become hard to understand and brittle if you try to jam too much functional flexibility (as opposed to design flexibility) into one procedure. Below is my explanation of my preferred approach to designing complex systems for comprehensibility and flexibility which I hope will demonstrate what I mean by them. I would not employ this strategy for something that could be written in fewer than 20 lines in a single procedure because something so small already meets my criteria for comprehensibility and flexibility as it is.
Objects, not Procedures
Rather than using classes like old-school modules with a bunch of routines you call to execute the things your software should do, consider modeling the domain as objects which interact and cooperate to accomplish the task at hand. Methods in an Object-Oriented paradigm were originally created to be signals between objects so that Object1
could tell Object2
to do its thing, whatever that is, and possibly receive a return signal. This is because the Object-Oriented paradigm is inherently about modeling your domain objects and their interactions rather than a fancy way to organize the same old functions and procedures of the Imperative paradigm. In the case of the void destroyBaghdad
example, instead of trying to write a context-less generic method to handle the destruction of Baghdad or any other thing (which could quickly grow complex, hard to understand, and brittle), every thing that can be destroyed should be responsible for understanding how to destroy itself. For example, you have an interface that describes the behavior of things that can be destroyed:
interface Destroyable {
void destroy();
}
Then you have a city which implements this interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nothing that calls for the destruction of an instance of City
will ever care how that happens, so there is no reason for that code to exist anywhere outside of City::destroy
, and indeed, intimate knowledge of the inner workings of City
outside of itself would be tight coupling which reduces felxibility since you have to consider those outside elements should you ever need to modify the behavior of City
. This is the true purpose behind encapsulation. Think of it like every object has its own API which should enable you to do anything you need to with it so you can let it worry about fulfilling your requests.
Delegation, not "Control"
Now, whether your implementing class is City
or Baghdad
depends on how generic the process of destroying the city turns out to be. In all probability, a City
will be composed of smaller pieces that will need to be destroyed individually to accomplish the total destruction of the city, so in that case, each of those pieces would also implement Destroyable
, and they would each be instructed by the City
to destroy themselves in the same way someone from outside requested the City
to destroy itself.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
If you want to get really crazy and implement the idea of a Bomb
that is dropped on a location and destroys everything within a certain radius, it might look something like this:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
represents a set of objects that is calculated for the Bomb
from the inputs because the Bomb
does not care how that calculation is made so long as it can work with the objects. This is reusable incidentally, but the main goal is to isolate the calculation from the processes of dropping the Bomb
and destroying the objects so you can comprehend each piece and how they fit together and change the behavior of an individual piece without having to reshape the entire algorithm.
Interactions, not Algorithms
Instead of trying to guess at the right number of parameters for a complex algorithm, it makes more sense to model the process as a set of interacting objects, each with extremely narrow roles, since it will give you the ability to model the complexity of your process through the interactions between these well-defined, easy to comprehend, and nearly unchanging objects. When done correctly, this makes even some of the most complex modifications as trivial as implementing an interface or two and reworking which objects are instantiated in your main()
method.
I'd give you something to your original example, but I honestly can't figure out what it means to "print... Day Light Savings." What I can say about that category of problem is that any time you are performing a calculation, the result of which could be formatted a number of ways, my preferred way to break that down is like this:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Since your example uses classes from the Java library which don't support this design, you could just use the API of ZonedDateTime
directly. The idea here is that each calculation is encapsulated within its own object. It makes no assumptions about how many times it should run or how it should format the result. It is exclusively concerned with performing the simplest form of the calculation. This makes it both easy to understand and flexible to change. Likewise, the Result
is exclusively concerned with encapsulating the result of the calculation, and the FormattedResult
is exclusively concerned with interacting with the Result
to format it according to the rules we define. In this way, we can find the perfect number of arguments for each of our methods since they each have a well-defined task. It's also much simpler to modify moving forward so long as the interfaces don't change (which they aren't as likely to do if you've properly minimized the responsibilities of your objects). Our main()
method might look like this:
class App {
public static void main(String args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
As a matter of fact, Object-Oriented Programming was invented specifically as a solution to the complexity/flexibility problem of the Imperative paradigm because there is just no good answer (that everyone can agree on or arrive at independently, anyhow) to how to optimally specify Imperative functions and procedures within the idiom.
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
|
show 4 more comments
up vote
4
down vote
up vote
4
down vote
In short, don't engineer your software for reusability because no end user cares if your functions can be reused. Instead, engineer for design comprehensibility -- is my code easy for someone else or my future forgetful self to understand? -- and design flexibility -- when I inevitably have to fix bugs, add features, or otherwise modify functionality, how much will my code resist the changes? The only thing your customer cares about is how quickly you can respond when she reports a bug or asks for a change. Asking these questions about your design incidentally tends to result in code that is reusable, but this approach keeps you focused on avoiding the real problems you will face over the life of that code so you can better serve the end user rather than pursuing lofty, impractical "engineering" ideals to please the neck-beards.
For something as simple as the example you provided, your initial implementation is fine because of how small it is, but this straightforward design will become hard to understand and brittle if you try to jam too much functional flexibility (as opposed to design flexibility) into one procedure. Below is my explanation of my preferred approach to designing complex systems for comprehensibility and flexibility which I hope will demonstrate what I mean by them. I would not employ this strategy for something that could be written in fewer than 20 lines in a single procedure because something so small already meets my criteria for comprehensibility and flexibility as it is.
Objects, not Procedures
Rather than using classes like old-school modules with a bunch of routines you call to execute the things your software should do, consider modeling the domain as objects which interact and cooperate to accomplish the task at hand. Methods in an Object-Oriented paradigm were originally created to be signals between objects so that Object1
could tell Object2
to do its thing, whatever that is, and possibly receive a return signal. This is because the Object-Oriented paradigm is inherently about modeling your domain objects and their interactions rather than a fancy way to organize the same old functions and procedures of the Imperative paradigm. In the case of the void destroyBaghdad
example, instead of trying to write a context-less generic method to handle the destruction of Baghdad or any other thing (which could quickly grow complex, hard to understand, and brittle), every thing that can be destroyed should be responsible for understanding how to destroy itself. For example, you have an interface that describes the behavior of things that can be destroyed:
interface Destroyable {
void destroy();
}
Then you have a city which implements this interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nothing that calls for the destruction of an instance of City
will ever care how that happens, so there is no reason for that code to exist anywhere outside of City::destroy
, and indeed, intimate knowledge of the inner workings of City
outside of itself would be tight coupling which reduces felxibility since you have to consider those outside elements should you ever need to modify the behavior of City
. This is the true purpose behind encapsulation. Think of it like every object has its own API which should enable you to do anything you need to with it so you can let it worry about fulfilling your requests.
Delegation, not "Control"
Now, whether your implementing class is City
or Baghdad
depends on how generic the process of destroying the city turns out to be. In all probability, a City
will be composed of smaller pieces that will need to be destroyed individually to accomplish the total destruction of the city, so in that case, each of those pieces would also implement Destroyable
, and they would each be instructed by the City
to destroy themselves in the same way someone from outside requested the City
to destroy itself.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
If you want to get really crazy and implement the idea of a Bomb
that is dropped on a location and destroys everything within a certain radius, it might look something like this:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
represents a set of objects that is calculated for the Bomb
from the inputs because the Bomb
does not care how that calculation is made so long as it can work with the objects. This is reusable incidentally, but the main goal is to isolate the calculation from the processes of dropping the Bomb
and destroying the objects so you can comprehend each piece and how they fit together and change the behavior of an individual piece without having to reshape the entire algorithm.
Interactions, not Algorithms
Instead of trying to guess at the right number of parameters for a complex algorithm, it makes more sense to model the process as a set of interacting objects, each with extremely narrow roles, since it will give you the ability to model the complexity of your process through the interactions between these well-defined, easy to comprehend, and nearly unchanging objects. When done correctly, this makes even some of the most complex modifications as trivial as implementing an interface or two and reworking which objects are instantiated in your main()
method.
I'd give you something to your original example, but I honestly can't figure out what it means to "print... Day Light Savings." What I can say about that category of problem is that any time you are performing a calculation, the result of which could be formatted a number of ways, my preferred way to break that down is like this:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Since your example uses classes from the Java library which don't support this design, you could just use the API of ZonedDateTime
directly. The idea here is that each calculation is encapsulated within its own object. It makes no assumptions about how many times it should run or how it should format the result. It is exclusively concerned with performing the simplest form of the calculation. This makes it both easy to understand and flexible to change. Likewise, the Result
is exclusively concerned with encapsulating the result of the calculation, and the FormattedResult
is exclusively concerned with interacting with the Result
to format it according to the rules we define. In this way, we can find the perfect number of arguments for each of our methods since they each have a well-defined task. It's also much simpler to modify moving forward so long as the interfaces don't change (which they aren't as likely to do if you've properly minimized the responsibilities of your objects). Our main()
method might look like this:
class App {
public static void main(String args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
As a matter of fact, Object-Oriented Programming was invented specifically as a solution to the complexity/flexibility problem of the Imperative paradigm because there is just no good answer (that everyone can agree on or arrive at independently, anyhow) to how to optimally specify Imperative functions and procedures within the idiom.
In short, don't engineer your software for reusability because no end user cares if your functions can be reused. Instead, engineer for design comprehensibility -- is my code easy for someone else or my future forgetful self to understand? -- and design flexibility -- when I inevitably have to fix bugs, add features, or otherwise modify functionality, how much will my code resist the changes? The only thing your customer cares about is how quickly you can respond when she reports a bug or asks for a change. Asking these questions about your design incidentally tends to result in code that is reusable, but this approach keeps you focused on avoiding the real problems you will face over the life of that code so you can better serve the end user rather than pursuing lofty, impractical "engineering" ideals to please the neck-beards.
For something as simple as the example you provided, your initial implementation is fine because of how small it is, but this straightforward design will become hard to understand and brittle if you try to jam too much functional flexibility (as opposed to design flexibility) into one procedure. Below is my explanation of my preferred approach to designing complex systems for comprehensibility and flexibility which I hope will demonstrate what I mean by them. I would not employ this strategy for something that could be written in fewer than 20 lines in a single procedure because something so small already meets my criteria for comprehensibility and flexibility as it is.
Objects, not Procedures
Rather than using classes like old-school modules with a bunch of routines you call to execute the things your software should do, consider modeling the domain as objects which interact and cooperate to accomplish the task at hand. Methods in an Object-Oriented paradigm were originally created to be signals between objects so that Object1
could tell Object2
to do its thing, whatever that is, and possibly receive a return signal. This is because the Object-Oriented paradigm is inherently about modeling your domain objects and their interactions rather than a fancy way to organize the same old functions and procedures of the Imperative paradigm. In the case of the void destroyBaghdad
example, instead of trying to write a context-less generic method to handle the destruction of Baghdad or any other thing (which could quickly grow complex, hard to understand, and brittle), every thing that can be destroyed should be responsible for understanding how to destroy itself. For example, you have an interface that describes the behavior of things that can be destroyed:
interface Destroyable {
void destroy();
}
Then you have a city which implements this interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nothing that calls for the destruction of an instance of City
will ever care how that happens, so there is no reason for that code to exist anywhere outside of City::destroy
, and indeed, intimate knowledge of the inner workings of City
outside of itself would be tight coupling which reduces felxibility since you have to consider those outside elements should you ever need to modify the behavior of City
. This is the true purpose behind encapsulation. Think of it like every object has its own API which should enable you to do anything you need to with it so you can let it worry about fulfilling your requests.
Delegation, not "Control"
Now, whether your implementing class is City
or Baghdad
depends on how generic the process of destroying the city turns out to be. In all probability, a City
will be composed of smaller pieces that will need to be destroyed individually to accomplish the total destruction of the city, so in that case, each of those pieces would also implement Destroyable
, and they would each be instructed by the City
to destroy themselves in the same way someone from outside requested the City
to destroy itself.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
If you want to get really crazy and implement the idea of a Bomb
that is dropped on a location and destroys everything within a certain radius, it might look something like this:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
represents a set of objects that is calculated for the Bomb
from the inputs because the Bomb
does not care how that calculation is made so long as it can work with the objects. This is reusable incidentally, but the main goal is to isolate the calculation from the processes of dropping the Bomb
and destroying the objects so you can comprehend each piece and how they fit together and change the behavior of an individual piece without having to reshape the entire algorithm.
Interactions, not Algorithms
Instead of trying to guess at the right number of parameters for a complex algorithm, it makes more sense to model the process as a set of interacting objects, each with extremely narrow roles, since it will give you the ability to model the complexity of your process through the interactions between these well-defined, easy to comprehend, and nearly unchanging objects. When done correctly, this makes even some of the most complex modifications as trivial as implementing an interface or two and reworking which objects are instantiated in your main()
method.
I'd give you something to your original example, but I honestly can't figure out what it means to "print... Day Light Savings." What I can say about that category of problem is that any time you are performing a calculation, the result of which could be formatted a number of ways, my preferred way to break that down is like this:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Since your example uses classes from the Java library which don't support this design, you could just use the API of ZonedDateTime
directly. The idea here is that each calculation is encapsulated within its own object. It makes no assumptions about how many times it should run or how it should format the result. It is exclusively concerned with performing the simplest form of the calculation. This makes it both easy to understand and flexible to change. Likewise, the Result
is exclusively concerned with encapsulating the result of the calculation, and the FormattedResult
is exclusively concerned with interacting with the Result
to format it according to the rules we define. In this way, we can find the perfect number of arguments for each of our methods since they each have a well-defined task. It's also much simpler to modify moving forward so long as the interfaces don't change (which they aren't as likely to do if you've properly minimized the responsibilities of your objects). Our main()
method might look like this:
class App {
public static void main(String args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
As a matter of fact, Object-Oriented Programming was invented specifically as a solution to the complexity/flexibility problem of the Imperative paradigm because there is just no good answer (that everyone can agree on or arrive at independently, anyhow) to how to optimally specify Imperative functions and procedures within the idiom.
edited Nov 30 at 18:46
answered Nov 28 at 4:42
Stuporman
1493
1493
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
|
show 4 more comments
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
This is a very detailed and thought out answer, but unfortunately I think it misses the mark on what the OP was really asking for. He wasn't asking for a lesson on good OOP practices to solve his specious example, he was asking about the criteria for where we decide investment of time in a solution vs generalization.
– maple_shaft♦
Nov 28 at 13:46
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Maybe I missed the mark, but I think you have, too. The OP doesn't ask about the investment of time vs. generalization. He asks "How do I know how reusable my methods should be?" He goes on to ask in the body of his question, "If destroyCity(City city) is better than destroyBaghdad(), is takeActionOnCity(Action action, City city) even better? Why / why not?" I made the case for an alternative approach to engineering solutions that I believe solves the problem of figuring out how generic to make methods and provided examples to support my claim. I'm sorry you didn't like it.
– Stuporman
Nov 28 at 16:49
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft Frankly, only the OP can make the determination if my answer was relevant to his question since the rest of us could fight wars defending our interpretations of his intentions, all of which could be equally wrong.
– Stuporman
Nov 28 at 16:53
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
@maple_shaft I added an intro to try to clarify how it relates to the question and provided a clear delineation between the answer and the example implementation. Is that better?
– Stuporman
Nov 28 at 18:06
1
1
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
Honestly, if you apply all of these principles, the answer will come naturally, be smooth, and massively readable. Plus, changeable without much fuss. I don't know who you are, but I wish there was more of you! I keep ripping out Reactive code for decent OO, and it's ALWAYS half the size, more readable, more controllable, and still has threading/splitting/mapping. I think React is for people who don't understand the "basic" concepts you just listed.
– Stephen J
Nov 29 at 23:08
|
show 4 more comments
up vote
3
down vote
Experience, Domain Knowledge, and Code Reviews.
And, regardless of how much or how little experience, domain knowledge, or team you have, you cannot avoid the need to refactor as-needed.
With Experience, you'll start to recognize patterns in the domain-nonspecific methods (and classes) you write. And, if you're at all interested in DRY code, you'll feel bad feelings when you're about a write a method that you instinctively know you'll write variations of in the future. So, you'll intuitively write a parametrized least common denominator instead.
(This experience may transfer over instinctively into some your domain objects and methods too.)
With Domain Knowledge, you'll have a sense for which business concepts are closely related, which concepts have variables, which are fairly static, etc..
With Code Reviews, under- and over-parametrization will more likely be caught before it becomes production code, because your peers will (hopefully) have unique experiences and perspectives, both on the domain and coding in general.
That said, new developers won't generally have these Spidey Senses or an experienced group of peers to lean on right away. And, even experienced developers benefit from a basic discipline to guide them through new requirements — or through brain-foggy days. So, here's what I'd suggest as a start:
- Start with the naive implementation, with minimal parametrization.
(Include any parameters you already know you'll need, obviously ...)
- Remove magic numbers and strings, moving them to configs and/or parameters
- Factor "large" methods down into smaller, well-named methods
- Refactor highly redundant methods (if convenient) into a common denominator, parametrizing the differences.
These steps don't necessarily occur in the stated order. If you sit down to write a method you already know to be highly redundant with an existing method, jump straight into refactoring if it's convenient. (If it's not going to take significantly more time to refactor than it would be to just write, test, and maintain two methods.)
But, apart from just having lots of experience and so forth, I advise pretty minimalistic code DRY-ing. It's not hard to refactor obvious violations. And, if you're too zealous, you can end up with "over-DRY" code that's even harder to read, understand, and maintain than the "WET" equivalent.
2
So there is no right answer toIf destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong,destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?
– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from thedestroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ...destroyBaghdad()
might be a perfectly reasonable method name/signature ...
– svidgen
Nov 27 at 20:31
1
So you do not agree with the cited quote in my question, right?It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer tois takeActionOnCity(Action action, City city) better?
null
?
– Koray Tugay
Nov 27 at 20:52
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
|
show 1 more comment
up vote
3
down vote
Experience, Domain Knowledge, and Code Reviews.
And, regardless of how much or how little experience, domain knowledge, or team you have, you cannot avoid the need to refactor as-needed.
With Experience, you'll start to recognize patterns in the domain-nonspecific methods (and classes) you write. And, if you're at all interested in DRY code, you'll feel bad feelings when you're about a write a method that you instinctively know you'll write variations of in the future. So, you'll intuitively write a parametrized least common denominator instead.
(This experience may transfer over instinctively into some your domain objects and methods too.)
With Domain Knowledge, you'll have a sense for which business concepts are closely related, which concepts have variables, which are fairly static, etc..
With Code Reviews, under- and over-parametrization will more likely be caught before it becomes production code, because your peers will (hopefully) have unique experiences and perspectives, both on the domain and coding in general.
That said, new developers won't generally have these Spidey Senses or an experienced group of peers to lean on right away. And, even experienced developers benefit from a basic discipline to guide them through new requirements — or through brain-foggy days. So, here's what I'd suggest as a start:
- Start with the naive implementation, with minimal parametrization.
(Include any parameters you already know you'll need, obviously ...)
- Remove magic numbers and strings, moving them to configs and/or parameters
- Factor "large" methods down into smaller, well-named methods
- Refactor highly redundant methods (if convenient) into a common denominator, parametrizing the differences.
These steps don't necessarily occur in the stated order. If you sit down to write a method you already know to be highly redundant with an existing method, jump straight into refactoring if it's convenient. (If it's not going to take significantly more time to refactor than it would be to just write, test, and maintain two methods.)
But, apart from just having lots of experience and so forth, I advise pretty minimalistic code DRY-ing. It's not hard to refactor obvious violations. And, if you're too zealous, you can end up with "over-DRY" code that's even harder to read, understand, and maintain than the "WET" equivalent.
2
So there is no right answer toIf destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong,destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?
– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from thedestroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ...destroyBaghdad()
might be a perfectly reasonable method name/signature ...
– svidgen
Nov 27 at 20:31
1
So you do not agree with the cited quote in my question, right?It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer tois takeActionOnCity(Action action, City city) better?
null
?
– Koray Tugay
Nov 27 at 20:52
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
|
show 1 more comment
up vote
3
down vote
up vote
3
down vote
Experience, Domain Knowledge, and Code Reviews.
And, regardless of how much or how little experience, domain knowledge, or team you have, you cannot avoid the need to refactor as-needed.
With Experience, you'll start to recognize patterns in the domain-nonspecific methods (and classes) you write. And, if you're at all interested in DRY code, you'll feel bad feelings when you're about a write a method that you instinctively know you'll write variations of in the future. So, you'll intuitively write a parametrized least common denominator instead.
(This experience may transfer over instinctively into some your domain objects and methods too.)
With Domain Knowledge, you'll have a sense for which business concepts are closely related, which concepts have variables, which are fairly static, etc..
With Code Reviews, under- and over-parametrization will more likely be caught before it becomes production code, because your peers will (hopefully) have unique experiences and perspectives, both on the domain and coding in general.
That said, new developers won't generally have these Spidey Senses or an experienced group of peers to lean on right away. And, even experienced developers benefit from a basic discipline to guide them through new requirements — or through brain-foggy days. So, here's what I'd suggest as a start:
- Start with the naive implementation, with minimal parametrization.
(Include any parameters you already know you'll need, obviously ...)
- Remove magic numbers and strings, moving them to configs and/or parameters
- Factor "large" methods down into smaller, well-named methods
- Refactor highly redundant methods (if convenient) into a common denominator, parametrizing the differences.
These steps don't necessarily occur in the stated order. If you sit down to write a method you already know to be highly redundant with an existing method, jump straight into refactoring if it's convenient. (If it's not going to take significantly more time to refactor than it would be to just write, test, and maintain two methods.)
But, apart from just having lots of experience and so forth, I advise pretty minimalistic code DRY-ing. It's not hard to refactor obvious violations. And, if you're too zealous, you can end up with "over-DRY" code that's even harder to read, understand, and maintain than the "WET" equivalent.
Experience, Domain Knowledge, and Code Reviews.
And, regardless of how much or how little experience, domain knowledge, or team you have, you cannot avoid the need to refactor as-needed.
With Experience, you'll start to recognize patterns in the domain-nonspecific methods (and classes) you write. And, if you're at all interested in DRY code, you'll feel bad feelings when you're about a write a method that you instinctively know you'll write variations of in the future. So, you'll intuitively write a parametrized least common denominator instead.
(This experience may transfer over instinctively into some your domain objects and methods too.)
With Domain Knowledge, you'll have a sense for which business concepts are closely related, which concepts have variables, which are fairly static, etc..
With Code Reviews, under- and over-parametrization will more likely be caught before it becomes production code, because your peers will (hopefully) have unique experiences and perspectives, both on the domain and coding in general.
That said, new developers won't generally have these Spidey Senses or an experienced group of peers to lean on right away. And, even experienced developers benefit from a basic discipline to guide them through new requirements — or through brain-foggy days. So, here's what I'd suggest as a start:
- Start with the naive implementation, with minimal parametrization.
(Include any parameters you already know you'll need, obviously ...)
- Remove magic numbers and strings, moving them to configs and/or parameters
- Factor "large" methods down into smaller, well-named methods
- Refactor highly redundant methods (if convenient) into a common denominator, parametrizing the differences.
These steps don't necessarily occur in the stated order. If you sit down to write a method you already know to be highly redundant with an existing method, jump straight into refactoring if it's convenient. (If it's not going to take significantly more time to refactor than it would be to just write, test, and maintain two methods.)
But, apart from just having lots of experience and so forth, I advise pretty minimalistic code DRY-ing. It's not hard to refactor obvious violations. And, if you're too zealous, you can end up with "over-DRY" code that's even harder to read, understand, and maintain than the "WET" equivalent.
answered Nov 27 at 17:21
svidgen
11.2k22754
11.2k22754
2
So there is no right answer toIf destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong,destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?
– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from thedestroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ...destroyBaghdad()
might be a perfectly reasonable method name/signature ...
– svidgen
Nov 27 at 20:31
1
So you do not agree with the cited quote in my question, right?It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer tois takeActionOnCity(Action action, City city) better?
null
?
– Koray Tugay
Nov 27 at 20:52
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
|
show 1 more comment
2
So there is no right answer toIf destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong,destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?
– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from thedestroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ...destroyBaghdad()
might be a perfectly reasonable method name/signature ...
– svidgen
Nov 27 at 20:31
1
So you do not agree with the cited quote in my question, right?It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer tois takeActionOnCity(Action action, City city) better?
null
?
– Koray Tugay
Nov 27 at 20:52
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
2
2
So there is no right answer to
If destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong, destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?– Koray Tugay
Nov 27 at 17:45
So there is no right answer to
If destoryCity(City city) is better than destoryBaghdad(), is takeActionOnCity(Action action, City city) even better?
? It is a yes / no question, but it does not have an answer, right? Or the initial assumption is wrong, destroyCity(City)
might not necessarly be better and it actually depends? So it does not mean that I am not a not ethically-trained software engineer because I directly implemented without any parameters the first place? I mean what is the answer to the concrete question I am asking?– Koray Tugay
Nov 27 at 17:45
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
Your question sort of asks a few questions. The answer to the title question is, "experience, domain knowledge, code reviews ... and ... don't be afraid to refactor." The answer to any concrete "are these the right parameters for method ABC" question is ... "I don't know. Why are you asking? Is something wrong with the number of parameters it currently has??? If so, articulate it. Fix it." ... I might refer you to "the POAP" for further guidance: You need to understand why you're doing what you're doing!
– svidgen
Nov 27 at 20:28
I mean ... let's even take a step back from the
destroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ... destroyBaghdad()
might be a perfectly reasonable method name/signature ...– svidgen
Nov 27 at 20:31
I mean ... let's even take a step back from the
destroyBaghdad()
method. What's the context? Is it a video game where the end of the game results in Baghdad being destroyed??? If so ... destroyBaghdad()
might be a perfectly reasonable method name/signature ...– svidgen
Nov 27 at 20:31
1
1
So you do not agree with the cited quote in my question, right?
It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer to is takeActionOnCity(Action action, City city) better?
null
?– Koray Tugay
Nov 27 at 20:52
So you do not agree with the cited quote in my question, right?
It should be noted that no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure.
If you were in the room with Nathaniel Borenstein, you would argue him that it actually depends and his statement is not correct? I mean it is beautiful that many people are answering, spending their times and energy, but I do not see a single concrete answer anywhere. Spidey-senses, code reviews.. But what is the answer to is takeActionOnCity(Action action, City city) better?
null
?– Koray Tugay
Nov 27 at 20:52
1
1
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
@svidgen Of course, another way to abstract this without any extra effort is to invert the dependency - have the function return a list of cities, instead of doing any action on them (like the "println" in the original code). This can be abstracted further if necessary, but just this one change on its own takes care of about half of the added requirements in the original question - and instead of one impure function that can do all sorts of bad stuff, you just have a function that returns a list, and the caller does the bad stuff.
– Luaan
Nov 29 at 12:46
|
show 1 more comment
up vote
2
down vote
The same answer as with quality, usability, technical debt etc:
As reusable as you, the user,1 need them to be
It's basically a judgement call -- whether the cost of designing and maintaining the abstraction will be repayed by the cost (=time and effort) it will save you down the line.
- Note the "down the line" phrase: there's a payoff mechanic here, so it will depend on how much you will be working with this code further. E.g.:
- Is this a one-off project, or is it going to be progressively improved over a long time?
- Are you confident in your design, or will you likely have to scrap or otherwise drastically change it for the next project/milestone (e.g. try another framework)?
- The projected benefit also depends on your ability to predict the future (changes to the app). Sometimes, you can reasonably see the venue(s) your app is going to take. More times, you think you can but you actually cannot. The rules of thumb here is the YAGNI principle and the rule of three -- both emphasize working off of what you know, now.
1This is a code construct, so you're the "user" in this case -- the user of the source code
add a comment |
up vote
2
down vote
The same answer as with quality, usability, technical debt etc:
As reusable as you, the user,1 need them to be
It's basically a judgement call -- whether the cost of designing and maintaining the abstraction will be repayed by the cost (=time and effort) it will save you down the line.
- Note the "down the line" phrase: there's a payoff mechanic here, so it will depend on how much you will be working with this code further. E.g.:
- Is this a one-off project, or is it going to be progressively improved over a long time?
- Are you confident in your design, or will you likely have to scrap or otherwise drastically change it for the next project/milestone (e.g. try another framework)?
- The projected benefit also depends on your ability to predict the future (changes to the app). Sometimes, you can reasonably see the venue(s) your app is going to take. More times, you think you can but you actually cannot. The rules of thumb here is the YAGNI principle and the rule of three -- both emphasize working off of what you know, now.
1This is a code construct, so you're the "user" in this case -- the user of the source code
add a comment |
up vote
2
down vote
up vote
2
down vote
The same answer as with quality, usability, technical debt etc:
As reusable as you, the user,1 need them to be
It's basically a judgement call -- whether the cost of designing and maintaining the abstraction will be repayed by the cost (=time and effort) it will save you down the line.
- Note the "down the line" phrase: there's a payoff mechanic here, so it will depend on how much you will be working with this code further. E.g.:
- Is this a one-off project, or is it going to be progressively improved over a long time?
- Are you confident in your design, or will you likely have to scrap or otherwise drastically change it for the next project/milestone (e.g. try another framework)?
- The projected benefit also depends on your ability to predict the future (changes to the app). Sometimes, you can reasonably see the venue(s) your app is going to take. More times, you think you can but you actually cannot. The rules of thumb here is the YAGNI principle and the rule of three -- both emphasize working off of what you know, now.
1This is a code construct, so you're the "user" in this case -- the user of the source code
The same answer as with quality, usability, technical debt etc:
As reusable as you, the user,1 need them to be
It's basically a judgement call -- whether the cost of designing and maintaining the abstraction will be repayed by the cost (=time and effort) it will save you down the line.
- Note the "down the line" phrase: there's a payoff mechanic here, so it will depend on how much you will be working with this code further. E.g.:
- Is this a one-off project, or is it going to be progressively improved over a long time?
- Are you confident in your design, or will you likely have to scrap or otherwise drastically change it for the next project/milestone (e.g. try another framework)?
- The projected benefit also depends on your ability to predict the future (changes to the app). Sometimes, you can reasonably see the venue(s) your app is going to take. More times, you think you can but you actually cannot. The rules of thumb here is the YAGNI principle and the rule of three -- both emphasize working off of what you know, now.
1This is a code construct, so you're the "user" in this case -- the user of the source code
answered Dec 2 at 12:19
ivan_pozdeev
398113
398113
add a comment |
add a comment |
up vote
1
down vote
There's a clear process you can follow:
- Write a failing test for a single feature which is in itself a "thing" (i.e., not some arbitrary split of a feature where neither half really makes sense).
- Write the absolute minimum code to make it pass green, not a line more.
- Rinse and repeat.
- (Refactor relentlessly if necessary, which should be easy due to the great test coverage.)
This turns up with - at least in the opinion of some people - pretty much optimal code, since it is as small as possible, each finished feature takes as little time as possible (which might or might not be true if you look at the finished product after refactoring), and it has very good test coverage. It also noticeably avoids over-engineered too-generic methods or classes.
This also gives you very clear instructions when to make things generic and when to specialize.
I find your city example weird; I would very likely never ever hardcode a city name. It is so obvious that additional cities will be included later, whatever it is you're doing. But another example would be colors. In some circumstances, hardcoding "red" or "green" would be a possibility. For example, traffic lights are such an ubiquitous color that you can just get away with it (and you can always refactor). The difference is that "red" and "green" have universal, "hardcoded" meaning in our world, it is incredibly unlikely that it will ever change, and there is not really an alternative either.
Your first daylight savings method is simply broken. While it conforms to the specifications, the hardcoded 2018 is particularly bad because a) it is not mentioned in the technical "contract" (in the method name, in this case), and b) it will be out of date soon, so breakage is included from the get-go. For things that are time/date related, it would very seldomly make sense to hardcode a specific value since, well, time moves on. But apart from that, everything else is up for discussion. If you give it a simple year and then always calculate the complete year, go ahead. Most of the things you listed (formatting, choice of a smaller range, etc.) screams that your method is doing too much, and it should instead probably return a list/array of values so the caller can do the formatting/filtering themselves.
But at the end of the day, most of this is opinion, taste, experience and personal bias, so don't fret too much about it.
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
add a comment |
up vote
1
down vote
There's a clear process you can follow:
- Write a failing test for a single feature which is in itself a "thing" (i.e., not some arbitrary split of a feature where neither half really makes sense).
- Write the absolute minimum code to make it pass green, not a line more.
- Rinse and repeat.
- (Refactor relentlessly if necessary, which should be easy due to the great test coverage.)
This turns up with - at least in the opinion of some people - pretty much optimal code, since it is as small as possible, each finished feature takes as little time as possible (which might or might not be true if you look at the finished product after refactoring), and it has very good test coverage. It also noticeably avoids over-engineered too-generic methods or classes.
This also gives you very clear instructions when to make things generic and when to specialize.
I find your city example weird; I would very likely never ever hardcode a city name. It is so obvious that additional cities will be included later, whatever it is you're doing. But another example would be colors. In some circumstances, hardcoding "red" or "green" would be a possibility. For example, traffic lights are such an ubiquitous color that you can just get away with it (and you can always refactor). The difference is that "red" and "green" have universal, "hardcoded" meaning in our world, it is incredibly unlikely that it will ever change, and there is not really an alternative either.
Your first daylight savings method is simply broken. While it conforms to the specifications, the hardcoded 2018 is particularly bad because a) it is not mentioned in the technical "contract" (in the method name, in this case), and b) it will be out of date soon, so breakage is included from the get-go. For things that are time/date related, it would very seldomly make sense to hardcode a specific value since, well, time moves on. But apart from that, everything else is up for discussion. If you give it a simple year and then always calculate the complete year, go ahead. Most of the things you listed (formatting, choice of a smaller range, etc.) screams that your method is doing too much, and it should instead probably return a list/array of values so the caller can do the formatting/filtering themselves.
But at the end of the day, most of this is opinion, taste, experience and personal bias, so don't fret too much about it.
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
add a comment |
up vote
1
down vote
up vote
1
down vote
There's a clear process you can follow:
- Write a failing test for a single feature which is in itself a "thing" (i.e., not some arbitrary split of a feature where neither half really makes sense).
- Write the absolute minimum code to make it pass green, not a line more.
- Rinse and repeat.
- (Refactor relentlessly if necessary, which should be easy due to the great test coverage.)
This turns up with - at least in the opinion of some people - pretty much optimal code, since it is as small as possible, each finished feature takes as little time as possible (which might or might not be true if you look at the finished product after refactoring), and it has very good test coverage. It also noticeably avoids over-engineered too-generic methods or classes.
This also gives you very clear instructions when to make things generic and when to specialize.
I find your city example weird; I would very likely never ever hardcode a city name. It is so obvious that additional cities will be included later, whatever it is you're doing. But another example would be colors. In some circumstances, hardcoding "red" or "green" would be a possibility. For example, traffic lights are such an ubiquitous color that you can just get away with it (and you can always refactor). The difference is that "red" and "green" have universal, "hardcoded" meaning in our world, it is incredibly unlikely that it will ever change, and there is not really an alternative either.
Your first daylight savings method is simply broken. While it conforms to the specifications, the hardcoded 2018 is particularly bad because a) it is not mentioned in the technical "contract" (in the method name, in this case), and b) it will be out of date soon, so breakage is included from the get-go. For things that are time/date related, it would very seldomly make sense to hardcode a specific value since, well, time moves on. But apart from that, everything else is up for discussion. If you give it a simple year and then always calculate the complete year, go ahead. Most of the things you listed (formatting, choice of a smaller range, etc.) screams that your method is doing too much, and it should instead probably return a list/array of values so the caller can do the formatting/filtering themselves.
But at the end of the day, most of this is opinion, taste, experience and personal bias, so don't fret too much about it.
There's a clear process you can follow:
- Write a failing test for a single feature which is in itself a "thing" (i.e., not some arbitrary split of a feature where neither half really makes sense).
- Write the absolute minimum code to make it pass green, not a line more.
- Rinse and repeat.
- (Refactor relentlessly if necessary, which should be easy due to the great test coverage.)
This turns up with - at least in the opinion of some people - pretty much optimal code, since it is as small as possible, each finished feature takes as little time as possible (which might or might not be true if you look at the finished product after refactoring), and it has very good test coverage. It also noticeably avoids over-engineered too-generic methods or classes.
This also gives you very clear instructions when to make things generic and when to specialize.
I find your city example weird; I would very likely never ever hardcode a city name. It is so obvious that additional cities will be included later, whatever it is you're doing. But another example would be colors. In some circumstances, hardcoding "red" or "green" would be a possibility. For example, traffic lights are such an ubiquitous color that you can just get away with it (and you can always refactor). The difference is that "red" and "green" have universal, "hardcoded" meaning in our world, it is incredibly unlikely that it will ever change, and there is not really an alternative either.
Your first daylight savings method is simply broken. While it conforms to the specifications, the hardcoded 2018 is particularly bad because a) it is not mentioned in the technical "contract" (in the method name, in this case), and b) it will be out of date soon, so breakage is included from the get-go. For things that are time/date related, it would very seldomly make sense to hardcode a specific value since, well, time moves on. But apart from that, everything else is up for discussion. If you give it a simple year and then always calculate the complete year, go ahead. Most of the things you listed (formatting, choice of a smaller range, etc.) screams that your method is doing too much, and it should instead probably return a list/array of values so the caller can do the formatting/filtering themselves.
But at the end of the day, most of this is opinion, taste, experience and personal bias, so don't fret too much about it.
edited Nov 29 at 11:35
answered Nov 28 at 0:08
AnoE
3,901717
3,901717
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
add a comment |
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
Regarding your second to last paragraph - look at the "requirements" initially given, ie those that the first method was based on. It specifies 2018, so the code is technically correct (and would probably match your feature-driven approach).
– dwizum
Nov 28 at 21:29
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
@dwizum, it is correct regarding the requirements, but there's the method name is misleading. In 2019, any programmer just looking at the method name would assume that it's doing whatever (maybe return the values for the current year), not 2018... I'll add a sentence to the answer to make more clear what I meant.
– AnoE
Nov 29 at 11:34
add a comment |
up vote
1
down vote
I've come to the opinion that there are two sorts of reusable code:
- Code which is reusable because it's such a fundamental, basic thing.
- Code which is reusable because it has parameters, overrides and hooks for everywhere.
The first sort of reusability is often a good idea. It applies to things like lists, hashmaps, key/value stores, string matchers (e.g. regex, glob, ...), tuples, unification, search trees (depth-first, breadth-first, iterative-deepening, ...), parser combinators, caches/memoisers, data format readers/writers (s-expressions, XML, JSON, protobuf, ...), task queues, etc.
These things are so general, in a very abstract way, that they're re-used all over the place in day to day programming. If you find yourself writing special-purpose code that would be simpler if it were made more abstract/general (e.g. if we have "a list of customer orders", we could throw away the "customer order" stuff to get "a list") then it might be a good idea to pull that out. Even if it doesn't get re-used, it lets us decouple unrelated functionality.
The second sort is where we have some concrete code, which solves a real issue, but does so by making a whole bunch of decisions. We can make it more general/reusable by "soft-coding" those decisions, e.g. turning them into parameters, complicating the implementation and baking in even more concrete details (i.e. knowledge of which hooks we might want for overrides). Your example seems to be of this sort. The problem with this sort of reusability is that we may end up trying to guess at the use-cases of other people, or our future selves. Eventually we might end up having so many parameters that our code isn't usable, let alone reusable! In other words, when calling it takes more effort than just writing our own version. This is where YAGNI (You Ain't Gonna Need It) is important. Many times, such attempts at "reusable" code end up not being reused, since it may be incompatable with those use-cases more fundamentally than parameters can account for, or those potential users would rather roll their own (heck, look at all the standards and libraries out there whose authors prefixed with the word "Simple", to distinguish them from the predecessors!).
This second form of "reusability" should basically be done on an as-needed basis. Sure, you can stick some "obvious" parameters in there, but don't start trying to predict the future. YAGNI.
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what ifdestroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.
– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form ofstatic
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.
– Marco13
Dec 1 at 21:24
|
show 1 more comment
up vote
1
down vote
I've come to the opinion that there are two sorts of reusable code:
- Code which is reusable because it's such a fundamental, basic thing.
- Code which is reusable because it has parameters, overrides and hooks for everywhere.
The first sort of reusability is often a good idea. It applies to things like lists, hashmaps, key/value stores, string matchers (e.g. regex, glob, ...), tuples, unification, search trees (depth-first, breadth-first, iterative-deepening, ...), parser combinators, caches/memoisers, data format readers/writers (s-expressions, XML, JSON, protobuf, ...), task queues, etc.
These things are so general, in a very abstract way, that they're re-used all over the place in day to day programming. If you find yourself writing special-purpose code that would be simpler if it were made more abstract/general (e.g. if we have "a list of customer orders", we could throw away the "customer order" stuff to get "a list") then it might be a good idea to pull that out. Even if it doesn't get re-used, it lets us decouple unrelated functionality.
The second sort is where we have some concrete code, which solves a real issue, but does so by making a whole bunch of decisions. We can make it more general/reusable by "soft-coding" those decisions, e.g. turning them into parameters, complicating the implementation and baking in even more concrete details (i.e. knowledge of which hooks we might want for overrides). Your example seems to be of this sort. The problem with this sort of reusability is that we may end up trying to guess at the use-cases of other people, or our future selves. Eventually we might end up having so many parameters that our code isn't usable, let alone reusable! In other words, when calling it takes more effort than just writing our own version. This is where YAGNI (You Ain't Gonna Need It) is important. Many times, such attempts at "reusable" code end up not being reused, since it may be incompatable with those use-cases more fundamentally than parameters can account for, or those potential users would rather roll their own (heck, look at all the standards and libraries out there whose authors prefixed with the word "Simple", to distinguish them from the predecessors!).
This second form of "reusability" should basically be done on an as-needed basis. Sure, you can stick some "obvious" parameters in there, but don't start trying to predict the future. YAGNI.
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what ifdestroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.
– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form ofstatic
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.
– Marco13
Dec 1 at 21:24
|
show 1 more comment
up vote
1
down vote
up vote
1
down vote
I've come to the opinion that there are two sorts of reusable code:
- Code which is reusable because it's such a fundamental, basic thing.
- Code which is reusable because it has parameters, overrides and hooks for everywhere.
The first sort of reusability is often a good idea. It applies to things like lists, hashmaps, key/value stores, string matchers (e.g. regex, glob, ...), tuples, unification, search trees (depth-first, breadth-first, iterative-deepening, ...), parser combinators, caches/memoisers, data format readers/writers (s-expressions, XML, JSON, protobuf, ...), task queues, etc.
These things are so general, in a very abstract way, that they're re-used all over the place in day to day programming. If you find yourself writing special-purpose code that would be simpler if it were made more abstract/general (e.g. if we have "a list of customer orders", we could throw away the "customer order" stuff to get "a list") then it might be a good idea to pull that out. Even if it doesn't get re-used, it lets us decouple unrelated functionality.
The second sort is where we have some concrete code, which solves a real issue, but does so by making a whole bunch of decisions. We can make it more general/reusable by "soft-coding" those decisions, e.g. turning them into parameters, complicating the implementation and baking in even more concrete details (i.e. knowledge of which hooks we might want for overrides). Your example seems to be of this sort. The problem with this sort of reusability is that we may end up trying to guess at the use-cases of other people, or our future selves. Eventually we might end up having so many parameters that our code isn't usable, let alone reusable! In other words, when calling it takes more effort than just writing our own version. This is where YAGNI (You Ain't Gonna Need It) is important. Many times, such attempts at "reusable" code end up not being reused, since it may be incompatable with those use-cases more fundamentally than parameters can account for, or those potential users would rather roll their own (heck, look at all the standards and libraries out there whose authors prefixed with the word "Simple", to distinguish them from the predecessors!).
This second form of "reusability" should basically be done on an as-needed basis. Sure, you can stick some "obvious" parameters in there, but don't start trying to predict the future. YAGNI.
I've come to the opinion that there are two sorts of reusable code:
- Code which is reusable because it's such a fundamental, basic thing.
- Code which is reusable because it has parameters, overrides and hooks for everywhere.
The first sort of reusability is often a good idea. It applies to things like lists, hashmaps, key/value stores, string matchers (e.g. regex, glob, ...), tuples, unification, search trees (depth-first, breadth-first, iterative-deepening, ...), parser combinators, caches/memoisers, data format readers/writers (s-expressions, XML, JSON, protobuf, ...), task queues, etc.
These things are so general, in a very abstract way, that they're re-used all over the place in day to day programming. If you find yourself writing special-purpose code that would be simpler if it were made more abstract/general (e.g. if we have "a list of customer orders", we could throw away the "customer order" stuff to get "a list") then it might be a good idea to pull that out. Even if it doesn't get re-used, it lets us decouple unrelated functionality.
The second sort is where we have some concrete code, which solves a real issue, but does so by making a whole bunch of decisions. We can make it more general/reusable by "soft-coding" those decisions, e.g. turning them into parameters, complicating the implementation and baking in even more concrete details (i.e. knowledge of which hooks we might want for overrides). Your example seems to be of this sort. The problem with this sort of reusability is that we may end up trying to guess at the use-cases of other people, or our future selves. Eventually we might end up having so many parameters that our code isn't usable, let alone reusable! In other words, when calling it takes more effort than just writing our own version. This is where YAGNI (You Ain't Gonna Need It) is important. Many times, such attempts at "reusable" code end up not being reused, since it may be incompatable with those use-cases more fundamentally than parameters can account for, or those potential users would rather roll their own (heck, look at all the standards and libraries out there whose authors prefixed with the word "Simple", to distinguish them from the predecessors!).
This second form of "reusability" should basically be done on an as-needed basis. Sure, you can stick some "obvious" parameters in there, but don't start trying to predict the future. YAGNI.
answered Nov 29 at 23:20
Warbo
98759
98759
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what ifdestroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.
– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form ofstatic
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.
– Marco13
Dec 1 at 21:24
|
show 1 more comment
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what ifdestroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.
– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form ofstatic
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.
– Marco13
Dec 1 at 21:24
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Can we say that you agree that my first take was fine, where even the year was hardcoded? Or if you were initially implementing that requirement, would you make the year a parameter in your first take?
– Koray Tugay
Nov 29 at 23:38
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
Your first take was fine, since the requirement was a one-off script to 'check something'. It fails her 'ethical' test, but she fails the 'no dogma' test. "She might say..." is inventing requirements You Ain't Gonna Need.
– Warbo
Nov 30 at 7:21
We can't say which 'destroy city' is "better" without more info:
destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what if destroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.– Warbo
Nov 30 at 7:34
We can't say which 'destroy city' is "better" without more info:
destroyBaghdad
is a one-off script (or at least, it's idempotent). Maybe destroying any city would be an improvement, but what if destroyBaghdad
works by flooding the Tigris? That may be reusable for Mosul and Basra, but not for Mecca or Atlanta.– Warbo
Nov 30 at 7:34
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I see so you disagree with the Nathaniel Borenstein, the owner of the quote. I am tryting to understand slowly I think by reading all these responses and the discussions.
– Koray Tugay
Nov 30 at 19:11
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form of
static
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.– Marco13
Dec 1 at 21:24
I like this differentiation. It's not always clear, and there are always "border cases". But in general, I'm also a fan of "building blocks" (often in form of
static
methods) that are purely functional and low-level, and in contrast to that, deciding about the "configuring parameters and hooks" is usually that where you have to build some structures that ask to be justified.– Marco13
Dec 1 at 21:24
|
show 1 more comment
up vote
0
down vote
A good rule of thumb is: your method should be as reusable as… reusable.
If you expect that you will call your method only in one place, it should have only parameters that are known to the call site and that are not available to this method.
If you have more callers, you can introduce new parameters as long as other callers may pass those parameters; otherwise you need new method.
As the number of callers may grow in time, you need to be prepared for refactoring or overloading. In many cases it means that you should feel safe to select expression and run “extract parameter” action of your IDE.
add a comment |
up vote
0
down vote
A good rule of thumb is: your method should be as reusable as… reusable.
If you expect that you will call your method only in one place, it should have only parameters that are known to the call site and that are not available to this method.
If you have more callers, you can introduce new parameters as long as other callers may pass those parameters; otherwise you need new method.
As the number of callers may grow in time, you need to be prepared for refactoring or overloading. In many cases it means that you should feel safe to select expression and run “extract parameter” action of your IDE.
add a comment |
up vote
0
down vote
up vote
0
down vote
A good rule of thumb is: your method should be as reusable as… reusable.
If you expect that you will call your method only in one place, it should have only parameters that are known to the call site and that are not available to this method.
If you have more callers, you can introduce new parameters as long as other callers may pass those parameters; otherwise you need new method.
As the number of callers may grow in time, you need to be prepared for refactoring or overloading. In many cases it means that you should feel safe to select expression and run “extract parameter” action of your IDE.
A good rule of thumb is: your method should be as reusable as… reusable.
If you expect that you will call your method only in one place, it should have only parameters that are known to the call site and that are not available to this method.
If you have more callers, you can introduce new parameters as long as other callers may pass those parameters; otherwise you need new method.
As the number of callers may grow in time, you need to be prepared for refactoring or overloading. In many cases it means that you should feel safe to select expression and run “extract parameter” action of your IDE.
answered Nov 27 at 21:33
Karol
142
142
add a comment |
add a comment |
up vote
0
down vote
Ultra short answer: The less coupling or dependency to other code your generic module has the more reusable it can be.
Your example only depends on
import java.time.*;
import java.util.Set;
so in theory it can be highly reusable.
In practise i donot think that you will ever have a second usecase that needs this code so following yagni principle i would not make it reusable if there are not more than 3 different projets that need this code.
Other aspects of reusability are ease of use and doocumentation which corelate with Test Driven Development: It is helpfull if you have a simple unit-test that demonstrates/documents an easy use of your generic module as a coding example for users of your lib.
add a comment |
up vote
0
down vote
Ultra short answer: The less coupling or dependency to other code your generic module has the more reusable it can be.
Your example only depends on
import java.time.*;
import java.util.Set;
so in theory it can be highly reusable.
In practise i donot think that you will ever have a second usecase that needs this code so following yagni principle i would not make it reusable if there are not more than 3 different projets that need this code.
Other aspects of reusability are ease of use and doocumentation which corelate with Test Driven Development: It is helpfull if you have a simple unit-test that demonstrates/documents an easy use of your generic module as a coding example for users of your lib.
add a comment |
up vote
0
down vote
up vote
0
down vote
Ultra short answer: The less coupling or dependency to other code your generic module has the more reusable it can be.
Your example only depends on
import java.time.*;
import java.util.Set;
so in theory it can be highly reusable.
In practise i donot think that you will ever have a second usecase that needs this code so following yagni principle i would not make it reusable if there are not more than 3 different projets that need this code.
Other aspects of reusability are ease of use and doocumentation which corelate with Test Driven Development: It is helpfull if you have a simple unit-test that demonstrates/documents an easy use of your generic module as a coding example for users of your lib.
Ultra short answer: The less coupling or dependency to other code your generic module has the more reusable it can be.
Your example only depends on
import java.time.*;
import java.util.Set;
so in theory it can be highly reusable.
In practise i donot think that you will ever have a second usecase that needs this code so following yagni principle i would not make it reusable if there are not more than 3 different projets that need this code.
Other aspects of reusability are ease of use and doocumentation which corelate with Test Driven Development: It is helpfull if you have a simple unit-test that demonstrates/documents an easy use of your generic module as a coding example for users of your lib.
edited Nov 28 at 10:32
answered Nov 28 at 10:27
k3b
6,65011227
6,65011227
add a comment |
add a comment |
up vote
0
down vote
This is a good opportunity to state a rule I coined recently:
Being a good programmer means being able to predict the future.
Of course, this is strictly impossible! After all, you never know for sure what generalisations you'll find useful later on, which related tasks you'll want to perform, what new features your users will want, &c. But experience sometimes gives you a rough idea of what might come in handy.
The other factors you have to balance that against are how much extra time and effort would be involved, and how much more complex would it make your code. Sometimes you're lucky, and solving the more general problem is actually simpler! (At least conceptually, if not in the amount of code.) But more often, there's a complexity cost as well as one of time and effort.
So if you think a generalisation is very likely to be needed, it's often worth doing (unless it adds a lot of work or complexity); but if it seems much less likely, then it's probably not (unless it's very easy and/or simplifies the code).
(For a recent example: last week I was given a spec. for actions a system should take exactly 2 days after something expired. So of course I made the 2-day period a parameter. This week the business folks were delighted, as they were about to ask for that enhancement! I was lucky: it was an easy change, and I guessed it was quite likely to be wanted. Often it's harder to judge. But it's still worth trying to predict, and experience is often a good guide.)
add a comment |
up vote
0
down vote
This is a good opportunity to state a rule I coined recently:
Being a good programmer means being able to predict the future.
Of course, this is strictly impossible! After all, you never know for sure what generalisations you'll find useful later on, which related tasks you'll want to perform, what new features your users will want, &c. But experience sometimes gives you a rough idea of what might come in handy.
The other factors you have to balance that against are how much extra time and effort would be involved, and how much more complex would it make your code. Sometimes you're lucky, and solving the more general problem is actually simpler! (At least conceptually, if not in the amount of code.) But more often, there's a complexity cost as well as one of time and effort.
So if you think a generalisation is very likely to be needed, it's often worth doing (unless it adds a lot of work or complexity); but if it seems much less likely, then it's probably not (unless it's very easy and/or simplifies the code).
(For a recent example: last week I was given a spec. for actions a system should take exactly 2 days after something expired. So of course I made the 2-day period a parameter. This week the business folks were delighted, as they were about to ask for that enhancement! I was lucky: it was an easy change, and I guessed it was quite likely to be wanted. Often it's harder to judge. But it's still worth trying to predict, and experience is often a good guide.)
add a comment |
up vote
0
down vote
up vote
0
down vote
This is a good opportunity to state a rule I coined recently:
Being a good programmer means being able to predict the future.
Of course, this is strictly impossible! After all, you never know for sure what generalisations you'll find useful later on, which related tasks you'll want to perform, what new features your users will want, &c. But experience sometimes gives you a rough idea of what might come in handy.
The other factors you have to balance that against are how much extra time and effort would be involved, and how much more complex would it make your code. Sometimes you're lucky, and solving the more general problem is actually simpler! (At least conceptually, if not in the amount of code.) But more often, there's a complexity cost as well as one of time and effort.
So if you think a generalisation is very likely to be needed, it's often worth doing (unless it adds a lot of work or complexity); but if it seems much less likely, then it's probably not (unless it's very easy and/or simplifies the code).
(For a recent example: last week I was given a spec. for actions a system should take exactly 2 days after something expired. So of course I made the 2-day period a parameter. This week the business folks were delighted, as they were about to ask for that enhancement! I was lucky: it was an easy change, and I guessed it was quite likely to be wanted. Often it's harder to judge. But it's still worth trying to predict, and experience is often a good guide.)
This is a good opportunity to state a rule I coined recently:
Being a good programmer means being able to predict the future.
Of course, this is strictly impossible! After all, you never know for sure what generalisations you'll find useful later on, which related tasks you'll want to perform, what new features your users will want, &c. But experience sometimes gives you a rough idea of what might come in handy.
The other factors you have to balance that against are how much extra time and effort would be involved, and how much more complex would it make your code. Sometimes you're lucky, and solving the more general problem is actually simpler! (At least conceptually, if not in the amount of code.) But more often, there's a complexity cost as well as one of time and effort.
So if you think a generalisation is very likely to be needed, it's often worth doing (unless it adds a lot of work or complexity); but if it seems much less likely, then it's probably not (unless it's very easy and/or simplifies the code).
(For a recent example: last week I was given a spec. for actions a system should take exactly 2 days after something expired. So of course I made the 2-day period a parameter. This week the business folks were delighted, as they were about to ask for that enhancement! I was lucky: it was an easy change, and I guessed it was quite likely to be wanted. Often it's harder to judge. But it's still worth trying to predict, and experience is often a good guide.)
answered Nov 30 at 18:35
gidds
1552
1552
add a comment |
add a comment |
up vote
0
down vote
Firstly, the best answer to 'How do I know how reusable my methods should be?" is "experience." Do this a few thousand times, and you typically get the right answer. But as a teaser, I can give you the last line of this answer: Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Many of these answers have specific pieces of advice. I wanted to give something more generic... because the irony is too much fun to pass up!
As some of the answers have noted, generality is expensive. However, it really isn't. Not always. Understanding the expense is essential to playing the reusability game.
I focus on putting things on a scale from "irreversable" to "reversable." It's a smooth scale. The only truly irreversable thing is "time spent on the project." You'll never get those resources back. Slightly less reversable might be "golden handcuffs" situations such as the Windows API. Deprecated features remain in that API for decades because Microsoft's business model calls for it. If you have customers whose relationship would be permanently damaged by undoing some API feature, then that should be treated as irreversable. Looking at the other end of the scale, you have things like prototype code. If you don't like where it goes, you can simply throw it away. Slightly less reversable might be internal use APIs. They can be refactored without bothering a customer, but they may cost more time (the most irreversable resource of all!)
So put these on a scale. Now you can apply a heuristic: the more reversable something is, the more you can use it for future looking activities. If something is irreversable, only use it for concrete customer-driven tasks. This is why you see principles like those from extreme programming which suggest only doing what the customer asks for and nothing more. These principles are good at making sure you don't do something you regret.
Things like the DRY principle suggest a way to move that balance. If you find yourself repeating yourself, that's an opportunity to create what's basically an internal API. No customer sees it, so you can always change it. Once you have this internal API, now you can start playing with forward looking things. How many different timezone based tasks do you think your wife is going to give you? Do you have any other customers who might want timezone based tasks? Your flexibility here is bought by the concrete demands of your current customers, and it supports the potential future demands of future customers.
This layered thinking approach, which comes naturally from DRY naturally provides the generalization you want without waste. But is there a limit? Of course there is. But to see it, you have to see the forest for the trees.
If you have many layers of flexiblity, they often lead to a lack of direct control of the layers that face your customers. I've had software where I've had the brutal task of explaining to a customer why they can't have what they want due to flexibility built in 10 layers down which they were never supposed to see. We wrote ourselves into a corner. We tied ourselves in a knot with all the flexibility we thought we needed.
So when you're doing this generalization/DRY trick, always keep a pulse on your customer. What do you think your wife is going to ask for next? Are you putting yourself in a position to fulfil those needs? If you have the knack, the customer will effectively tell you their future needs. If you don't have the knack, well, most of us just rely on guesswork! (especially with spouses!) Some customers will want great flexibility, and be willing to accept the extra cost of you developing with all these layers because they directly benefit from those layers' flexibility. Other customers have rather fixed unwavering requirements, and they prefer development be more direct. Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? AlsoYour customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?
– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
add a comment |
up vote
0
down vote
Firstly, the best answer to 'How do I know how reusable my methods should be?" is "experience." Do this a few thousand times, and you typically get the right answer. But as a teaser, I can give you the last line of this answer: Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Many of these answers have specific pieces of advice. I wanted to give something more generic... because the irony is too much fun to pass up!
As some of the answers have noted, generality is expensive. However, it really isn't. Not always. Understanding the expense is essential to playing the reusability game.
I focus on putting things on a scale from "irreversable" to "reversable." It's a smooth scale. The only truly irreversable thing is "time spent on the project." You'll never get those resources back. Slightly less reversable might be "golden handcuffs" situations such as the Windows API. Deprecated features remain in that API for decades because Microsoft's business model calls for it. If you have customers whose relationship would be permanently damaged by undoing some API feature, then that should be treated as irreversable. Looking at the other end of the scale, you have things like prototype code. If you don't like where it goes, you can simply throw it away. Slightly less reversable might be internal use APIs. They can be refactored without bothering a customer, but they may cost more time (the most irreversable resource of all!)
So put these on a scale. Now you can apply a heuristic: the more reversable something is, the more you can use it for future looking activities. If something is irreversable, only use it for concrete customer-driven tasks. This is why you see principles like those from extreme programming which suggest only doing what the customer asks for and nothing more. These principles are good at making sure you don't do something you regret.
Things like the DRY principle suggest a way to move that balance. If you find yourself repeating yourself, that's an opportunity to create what's basically an internal API. No customer sees it, so you can always change it. Once you have this internal API, now you can start playing with forward looking things. How many different timezone based tasks do you think your wife is going to give you? Do you have any other customers who might want timezone based tasks? Your flexibility here is bought by the concrete demands of your current customers, and it supports the potential future demands of future customers.
This layered thinking approach, which comes naturally from DRY naturally provides the generalization you want without waste. But is there a limit? Of course there is. But to see it, you have to see the forest for the trees.
If you have many layers of flexiblity, they often lead to a lack of direct control of the layers that face your customers. I've had software where I've had the brutal task of explaining to a customer why they can't have what they want due to flexibility built in 10 layers down which they were never supposed to see. We wrote ourselves into a corner. We tied ourselves in a knot with all the flexibility we thought we needed.
So when you're doing this generalization/DRY trick, always keep a pulse on your customer. What do you think your wife is going to ask for next? Are you putting yourself in a position to fulfil those needs? If you have the knack, the customer will effectively tell you their future needs. If you don't have the knack, well, most of us just rely on guesswork! (especially with spouses!) Some customers will want great flexibility, and be willing to accept the extra cost of you developing with all these layers because they directly benefit from those layers' flexibility. Other customers have rather fixed unwavering requirements, and they prefer development be more direct. Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? AlsoYour customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?
– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
add a comment |
up vote
0
down vote
up vote
0
down vote
Firstly, the best answer to 'How do I know how reusable my methods should be?" is "experience." Do this a few thousand times, and you typically get the right answer. But as a teaser, I can give you the last line of this answer: Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Many of these answers have specific pieces of advice. I wanted to give something more generic... because the irony is too much fun to pass up!
As some of the answers have noted, generality is expensive. However, it really isn't. Not always. Understanding the expense is essential to playing the reusability game.
I focus on putting things on a scale from "irreversable" to "reversable." It's a smooth scale. The only truly irreversable thing is "time spent on the project." You'll never get those resources back. Slightly less reversable might be "golden handcuffs" situations such as the Windows API. Deprecated features remain in that API for decades because Microsoft's business model calls for it. If you have customers whose relationship would be permanently damaged by undoing some API feature, then that should be treated as irreversable. Looking at the other end of the scale, you have things like prototype code. If you don't like where it goes, you can simply throw it away. Slightly less reversable might be internal use APIs. They can be refactored without bothering a customer, but they may cost more time (the most irreversable resource of all!)
So put these on a scale. Now you can apply a heuristic: the more reversable something is, the more you can use it for future looking activities. If something is irreversable, only use it for concrete customer-driven tasks. This is why you see principles like those from extreme programming which suggest only doing what the customer asks for and nothing more. These principles are good at making sure you don't do something you regret.
Things like the DRY principle suggest a way to move that balance. If you find yourself repeating yourself, that's an opportunity to create what's basically an internal API. No customer sees it, so you can always change it. Once you have this internal API, now you can start playing with forward looking things. How many different timezone based tasks do you think your wife is going to give you? Do you have any other customers who might want timezone based tasks? Your flexibility here is bought by the concrete demands of your current customers, and it supports the potential future demands of future customers.
This layered thinking approach, which comes naturally from DRY naturally provides the generalization you want without waste. But is there a limit? Of course there is. But to see it, you have to see the forest for the trees.
If you have many layers of flexiblity, they often lead to a lack of direct control of the layers that face your customers. I've had software where I've had the brutal task of explaining to a customer why they can't have what they want due to flexibility built in 10 layers down which they were never supposed to see. We wrote ourselves into a corner. We tied ourselves in a knot with all the flexibility we thought we needed.
So when you're doing this generalization/DRY trick, always keep a pulse on your customer. What do you think your wife is going to ask for next? Are you putting yourself in a position to fulfil those needs? If you have the knack, the customer will effectively tell you their future needs. If you don't have the knack, well, most of us just rely on guesswork! (especially with spouses!) Some customers will want great flexibility, and be willing to accept the extra cost of you developing with all these layers because they directly benefit from those layers' flexibility. Other customers have rather fixed unwavering requirements, and they prefer development be more direct. Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Firstly, the best answer to 'How do I know how reusable my methods should be?" is "experience." Do this a few thousand times, and you typically get the right answer. But as a teaser, I can give you the last line of this answer: Your customer will tell you how much flexibility and how many layers of generalization you should seek.
Many of these answers have specific pieces of advice. I wanted to give something more generic... because the irony is too much fun to pass up!
As some of the answers have noted, generality is expensive. However, it really isn't. Not always. Understanding the expense is essential to playing the reusability game.
I focus on putting things on a scale from "irreversable" to "reversable." It's a smooth scale. The only truly irreversable thing is "time spent on the project." You'll never get those resources back. Slightly less reversable might be "golden handcuffs" situations such as the Windows API. Deprecated features remain in that API for decades because Microsoft's business model calls for it. If you have customers whose relationship would be permanently damaged by undoing some API feature, then that should be treated as irreversable. Looking at the other end of the scale, you have things like prototype code. If you don't like where it goes, you can simply throw it away. Slightly less reversable might be internal use APIs. They can be refactored without bothering a customer, but they may cost more time (the most irreversable resource of all!)
So put these on a scale. Now you can apply a heuristic: the more reversable something is, the more you can use it for future looking activities. If something is irreversable, only use it for concrete customer-driven tasks. This is why you see principles like those from extreme programming which suggest only doing what the customer asks for and nothing more. These principles are good at making sure you don't do something you regret.
Things like the DRY principle suggest a way to move that balance. If you find yourself repeating yourself, that's an opportunity to create what's basically an internal API. No customer sees it, so you can always change it. Once you have this internal API, now you can start playing with forward looking things. How many different timezone based tasks do you think your wife is going to give you? Do you have any other customers who might want timezone based tasks? Your flexibility here is bought by the concrete demands of your current customers, and it supports the potential future demands of future customers.
This layered thinking approach, which comes naturally from DRY naturally provides the generalization you want without waste. But is there a limit? Of course there is. But to see it, you have to see the forest for the trees.
If you have many layers of flexiblity, they often lead to a lack of direct control of the layers that face your customers. I've had software where I've had the brutal task of explaining to a customer why they can't have what they want due to flexibility built in 10 layers down which they were never supposed to see. We wrote ourselves into a corner. We tied ourselves in a knot with all the flexibility we thought we needed.
So when you're doing this generalization/DRY trick, always keep a pulse on your customer. What do you think your wife is going to ask for next? Are you putting yourself in a position to fulfil those needs? If you have the knack, the customer will effectively tell you their future needs. If you don't have the knack, well, most of us just rely on guesswork! (especially with spouses!) Some customers will want great flexibility, and be willing to accept the extra cost of you developing with all these layers because they directly benefit from those layers' flexibility. Other customers have rather fixed unwavering requirements, and they prefer development be more direct. Your customer will tell you how much flexibility and how many layers of generalization you should seek.
answered Nov 30 at 19:12
Cort Ammon
9,57331730
9,57331730
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? AlsoYour customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?
– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
add a comment |
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? AlsoYour customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?
– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? Also
Your customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?– Koray Tugay
Nov 30 at 19:29
Well there should be other people who done this 10000 times, so why should I do it 10000 times and gain experience when I can learn from others? Because the answer will be different for each individual so answers from experienced are not applicable to me? Also
Your customer will tell you how much flexibility and how many layers of generalization you should seek.
what world is this?– Koray Tugay
Nov 30 at 19:29
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
@KorayTugay It's a business world. If your customers aren't telling you what to do, then you aren't listening hard enough. Of course, they wont always tell you in words, but they tell you in other ways. Experience helps you listen to their more subtle messages. If you don't have the skill yet, find someone in your company who does have the skill of listening to those subtle customer hints, and ply them for direction. Someone will have that skill, even if they're the CEO or in marketing.
– Cort Ammon
Nov 30 at 19:45
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
In your specific case, if you had failed to take the trash out because you were too busy coding up a generalized version of this timezone problem instead of hacking together the specific solution, how would your customer feel?
– Cort Ammon
Nov 30 at 19:46
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
So you agree that my first approach was the right one, hardcoding 2018 in the first take instead of parameterizing the year? (btw, That is not really listening to my customer I think, the garbage example. That is knowing your customer.. Even if get support from The Oracle, there are no subtle messages to listen to when she says I need a list of daylight changes for 2018.) Thanks for your time and answer btw.
– Koray Tugay
Nov 30 at 19:50
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
@KorayTugay Without knowing any additional details, I'd say hardcoding was the right approach. You had no way of knowing if you were going to need future DLS code, nor any idea what sort of request she might give next. And if your customer is trying to test you, they get what they get =D
– Cort Ammon
Nov 30 at 21:40
add a comment |
up vote
0
down vote
no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
This is something known in advanced software engineering circles as a "joke". Jokes do not have to be what we call "true", although to be funny they generally must hint at something true.
In this particular case, the "joke" is not "true". The work involved in writing a general procedure to destroy any city is, we can safely assume, orders of magnitude beyond that required to destroy one specific city. Otherwise, anybody who has destroyed one or a handful of cities (the biblical Joshua, shall we say, or President Truman) could trivially generalise what they did and be able to destroy absolutely any city at will. This in point of fact is not the case. The methods that those two people famously used to destroy a small number of specific cities would not necessarily work on just any city at any time. Another city whose walls had different resonant frequency or whose high-altitude air defences were rather better, would need either minor or fundamental changes of approach (a differently-pitched trumpet or a rocket).
This also leads to code maintenance against changes over time: there are rather a lot of cities now that would fall to neither of those approaches, thanks to modern construction methods and ubiquitous radar.
Developing and testing a completely general means that will destroy any city, before you agree to destro just one city, is a desperately inefficient approach. No ethically-trained software engineer would try to generalise a problem to an extent requiring orders of magnitude more work than their employer/client actually needs to pay for, without demonstrated requirement.
So what is true? Sometimes adding generality is trivial. Should we then always add generality when it is trivial to do so? I would still argue "no, not always", because of the long-term maintenance issue. Supposing that at time of writing, all cities are basically the same, so I go ahead with DestroyCity. Once I've written this, along with integration tests that (due to the finitely enumerable space of inputs) iterate over every known city and make sure the function works on each (not sure how that works. Probably calls City.clone() and destroys the clone? Anyway).
In practice the function is used solely to destroy Baghdad, suppose somebody builds a new city that is resistant to my techniques (it's deep underground or something). Now I have an integration test failure for a use-case that doesn't even really exist, and before I can continue my campaign of terror against the innocent civilians of Iraq, I have to figure out how to destroy Subterrania. Never mind whether this is ethical or not, it is dumb and a waste of my freaking time.
So, do you really want a function that can output daylight savings for any year, just to output data for 2018? Maybe, but it's certainly going to require a small amount of extra effort just putting together test cases. It may require a large amount of effort to get a better timezone database than the one you actually have. So for example in 1908, the town of Port Arthur, Ontario had a period of DST commencing on the 1st of July. Is that in your OS's timezone database? Thought not, so your generalised function is wrong. There's nothing especially ethical about writing code that makes promises it can't keep.
Alright, so, with appropriate caveats it's easy to write a function that does timezones for a range of years, say 1970-present day. But it's just as easy to take the function that you actually wrote, and generalise it to parameterise the year. So it's not really any more ethical/sensible to generalise now, that it is to do what you did and then generalise if and when you need it.
However, if you knew why your wife wanted to check this DST list, then you would have an informed opinion of whether she's likely to ask the same question again in 2019 and, if so, whether you can take yourself out of that loop by giving her a function that she can call, without needing to recompile it. Once you've done that analysis, the answer to the question "should this generalise to recent years" might be "yes". But you create yet another problem for yourself, which is that timezone data in the future is only provisional, and therefore if she runs it for 2019 today, she may or may not realise that it's feeding her a best-guess. So you still have to write a bunch of documentation which would not be needed for the less general function ("the data comes from timezone database blah blah here's the link to see their policies on pushing updates blah blah"). If you refuse the special case, then all the time you're doing that, she can't get on with the task for which she needed the 2018 data, because of some nonsense about 2019 she doesn't even care about yet.
Do not do difficult things without thinking them through properly, just because a joke told you to. Is it useful? Is it cheap enough for that degree of usefulness?
add a comment |
up vote
0
down vote
no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
This is something known in advanced software engineering circles as a "joke". Jokes do not have to be what we call "true", although to be funny they generally must hint at something true.
In this particular case, the "joke" is not "true". The work involved in writing a general procedure to destroy any city is, we can safely assume, orders of magnitude beyond that required to destroy one specific city. Otherwise, anybody who has destroyed one or a handful of cities (the biblical Joshua, shall we say, or President Truman) could trivially generalise what they did and be able to destroy absolutely any city at will. This in point of fact is not the case. The methods that those two people famously used to destroy a small number of specific cities would not necessarily work on just any city at any time. Another city whose walls had different resonant frequency or whose high-altitude air defences were rather better, would need either minor or fundamental changes of approach (a differently-pitched trumpet or a rocket).
This also leads to code maintenance against changes over time: there are rather a lot of cities now that would fall to neither of those approaches, thanks to modern construction methods and ubiquitous radar.
Developing and testing a completely general means that will destroy any city, before you agree to destro just one city, is a desperately inefficient approach. No ethically-trained software engineer would try to generalise a problem to an extent requiring orders of magnitude more work than their employer/client actually needs to pay for, without demonstrated requirement.
So what is true? Sometimes adding generality is trivial. Should we then always add generality when it is trivial to do so? I would still argue "no, not always", because of the long-term maintenance issue. Supposing that at time of writing, all cities are basically the same, so I go ahead with DestroyCity. Once I've written this, along with integration tests that (due to the finitely enumerable space of inputs) iterate over every known city and make sure the function works on each (not sure how that works. Probably calls City.clone() and destroys the clone? Anyway).
In practice the function is used solely to destroy Baghdad, suppose somebody builds a new city that is resistant to my techniques (it's deep underground or something). Now I have an integration test failure for a use-case that doesn't even really exist, and before I can continue my campaign of terror against the innocent civilians of Iraq, I have to figure out how to destroy Subterrania. Never mind whether this is ethical or not, it is dumb and a waste of my freaking time.
So, do you really want a function that can output daylight savings for any year, just to output data for 2018? Maybe, but it's certainly going to require a small amount of extra effort just putting together test cases. It may require a large amount of effort to get a better timezone database than the one you actually have. So for example in 1908, the town of Port Arthur, Ontario had a period of DST commencing on the 1st of July. Is that in your OS's timezone database? Thought not, so your generalised function is wrong. There's nothing especially ethical about writing code that makes promises it can't keep.
Alright, so, with appropriate caveats it's easy to write a function that does timezones for a range of years, say 1970-present day. But it's just as easy to take the function that you actually wrote, and generalise it to parameterise the year. So it's not really any more ethical/sensible to generalise now, that it is to do what you did and then generalise if and when you need it.
However, if you knew why your wife wanted to check this DST list, then you would have an informed opinion of whether she's likely to ask the same question again in 2019 and, if so, whether you can take yourself out of that loop by giving her a function that she can call, without needing to recompile it. Once you've done that analysis, the answer to the question "should this generalise to recent years" might be "yes". But you create yet another problem for yourself, which is that timezone data in the future is only provisional, and therefore if she runs it for 2019 today, she may or may not realise that it's feeding her a best-guess. So you still have to write a bunch of documentation which would not be needed for the less general function ("the data comes from timezone database blah blah here's the link to see their policies on pushing updates blah blah"). If you refuse the special case, then all the time you're doing that, she can't get on with the task for which she needed the 2018 data, because of some nonsense about 2019 she doesn't even care about yet.
Do not do difficult things without thinking them through properly, just because a joke told you to. Is it useful? Is it cheap enough for that degree of usefulness?
add a comment |
up vote
0
down vote
up vote
0
down vote
no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
This is something known in advanced software engineering circles as a "joke". Jokes do not have to be what we call "true", although to be funny they generally must hint at something true.
In this particular case, the "joke" is not "true". The work involved in writing a general procedure to destroy any city is, we can safely assume, orders of magnitude beyond that required to destroy one specific city. Otherwise, anybody who has destroyed one or a handful of cities (the biblical Joshua, shall we say, or President Truman) could trivially generalise what they did and be able to destroy absolutely any city at will. This in point of fact is not the case. The methods that those two people famously used to destroy a small number of specific cities would not necessarily work on just any city at any time. Another city whose walls had different resonant frequency or whose high-altitude air defences were rather better, would need either minor or fundamental changes of approach (a differently-pitched trumpet or a rocket).
This also leads to code maintenance against changes over time: there are rather a lot of cities now that would fall to neither of those approaches, thanks to modern construction methods and ubiquitous radar.
Developing and testing a completely general means that will destroy any city, before you agree to destro just one city, is a desperately inefficient approach. No ethically-trained software engineer would try to generalise a problem to an extent requiring orders of magnitude more work than their employer/client actually needs to pay for, without demonstrated requirement.
So what is true? Sometimes adding generality is trivial. Should we then always add generality when it is trivial to do so? I would still argue "no, not always", because of the long-term maintenance issue. Supposing that at time of writing, all cities are basically the same, so I go ahead with DestroyCity. Once I've written this, along with integration tests that (due to the finitely enumerable space of inputs) iterate over every known city and make sure the function works on each (not sure how that works. Probably calls City.clone() and destroys the clone? Anyway).
In practice the function is used solely to destroy Baghdad, suppose somebody builds a new city that is resistant to my techniques (it's deep underground or something). Now I have an integration test failure for a use-case that doesn't even really exist, and before I can continue my campaign of terror against the innocent civilians of Iraq, I have to figure out how to destroy Subterrania. Never mind whether this is ethical or not, it is dumb and a waste of my freaking time.
So, do you really want a function that can output daylight savings for any year, just to output data for 2018? Maybe, but it's certainly going to require a small amount of extra effort just putting together test cases. It may require a large amount of effort to get a better timezone database than the one you actually have. So for example in 1908, the town of Port Arthur, Ontario had a period of DST commencing on the 1st of July. Is that in your OS's timezone database? Thought not, so your generalised function is wrong. There's nothing especially ethical about writing code that makes promises it can't keep.
Alright, so, with appropriate caveats it's easy to write a function that does timezones for a range of years, say 1970-present day. But it's just as easy to take the function that you actually wrote, and generalise it to parameterise the year. So it's not really any more ethical/sensible to generalise now, that it is to do what you did and then generalise if and when you need it.
However, if you knew why your wife wanted to check this DST list, then you would have an informed opinion of whether she's likely to ask the same question again in 2019 and, if so, whether you can take yourself out of that loop by giving her a function that she can call, without needing to recompile it. Once you've done that analysis, the answer to the question "should this generalise to recent years" might be "yes". But you create yet another problem for yourself, which is that timezone data in the future is only provisional, and therefore if she runs it for 2019 today, she may or may not realise that it's feeding her a best-guess. So you still have to write a bunch of documentation which would not be needed for the less general function ("the data comes from timezone database blah blah here's the link to see their policies on pushing updates blah blah"). If you refuse the special case, then all the time you're doing that, she can't get on with the task for which she needed the 2018 data, because of some nonsense about 2019 she doesn't even care about yet.
Do not do difficult things without thinking them through properly, just because a joke told you to. Is it useful? Is it cheap enough for that degree of usefulness?
no ethically-trained software engineer would ever consent to write a DestroyBaghdad procedure. Basic professional ethics would instead require him to write a DestroyCity procedure, to which Baghdad could be given as a parameter.
This is something known in advanced software engineering circles as a "joke". Jokes do not have to be what we call "true", although to be funny they generally must hint at something true.
In this particular case, the "joke" is not "true". The work involved in writing a general procedure to destroy any city is, we can safely assume, orders of magnitude beyond that required to destroy one specific city. Otherwise, anybody who has destroyed one or a handful of cities (the biblical Joshua, shall we say, or President Truman) could trivially generalise what they did and be able to destroy absolutely any city at will. This in point of fact is not the case. The methods that those two people famously used to destroy a small number of specific cities would not necessarily work on just any city at any time. Another city whose walls had different resonant frequency or whose high-altitude air defences were rather better, would need either minor or fundamental changes of approach (a differently-pitched trumpet or a rocket).
This also leads to code maintenance against changes over time: there are rather a lot of cities now that would fall to neither of those approaches, thanks to modern construction methods and ubiquitous radar.
Developing and testing a completely general means that will destroy any city, before you agree to destro just one city, is a desperately inefficient approach. No ethically-trained software engineer would try to generalise a problem to an extent requiring orders of magnitude more work than their employer/client actually needs to pay for, without demonstrated requirement.
So what is true? Sometimes adding generality is trivial. Should we then always add generality when it is trivial to do so? I would still argue "no, not always", because of the long-term maintenance issue. Supposing that at time of writing, all cities are basically the same, so I go ahead with DestroyCity. Once I've written this, along with integration tests that (due to the finitely enumerable space of inputs) iterate over every known city and make sure the function works on each (not sure how that works. Probably calls City.clone() and destroys the clone? Anyway).
In practice the function is used solely to destroy Baghdad, suppose somebody builds a new city that is resistant to my techniques (it's deep underground or something). Now I have an integration test failure for a use-case that doesn't even really exist, and before I can continue my campaign of terror against the innocent civilians of Iraq, I have to figure out how to destroy Subterrania. Never mind whether this is ethical or not, it is dumb and a waste of my freaking time.
So, do you really want a function that can output daylight savings for any year, just to output data for 2018? Maybe, but it's certainly going to require a small amount of extra effort just putting together test cases. It may require a large amount of effort to get a better timezone database than the one you actually have. So for example in 1908, the town of Port Arthur, Ontario had a period of DST commencing on the 1st of July. Is that in your OS's timezone database? Thought not, so your generalised function is wrong. There's nothing especially ethical about writing code that makes promises it can't keep.
Alright, so, with appropriate caveats it's easy to write a function that does timezones for a range of years, say 1970-present day. But it's just as easy to take the function that you actually wrote, and generalise it to parameterise the year. So it's not really any more ethical/sensible to generalise now, that it is to do what you did and then generalise if and when you need it.
However, if you knew why your wife wanted to check this DST list, then you would have an informed opinion of whether she's likely to ask the same question again in 2019 and, if so, whether you can take yourself out of that loop by giving her a function that she can call, without needing to recompile it. Once you've done that analysis, the answer to the question "should this generalise to recent years" might be "yes". But you create yet another problem for yourself, which is that timezone data in the future is only provisional, and therefore if she runs it for 2019 today, she may or may not realise that it's feeding her a best-guess. So you still have to write a bunch of documentation which would not be needed for the less general function ("the data comes from timezone database blah blah here's the link to see their policies on pushing updates blah blah"). If you refuse the special case, then all the time you're doing that, she can't get on with the task for which she needed the 2018 data, because of some nonsense about 2019 she doesn't even care about yet.
Do not do difficult things without thinking them through properly, just because a joke told you to. Is it useful? Is it cheap enough for that degree of usefulness?
edited Dec 1 at 12:33
answered Dec 1 at 12:00
Steve Jessop
4,7631523
4,7631523
add a comment |
add a comment |
up vote
0
down vote
There already are many excellent and elaborate answers. Some of them go deeply into specific details, lay out certain viewpoints on software development methodologies in general, and some of them certainly have controversial elements or "opinions" sprinkled in.
The answer by Warbo already pointed out different types of reusability. Namely, whether something is reusable because it is a fundamental building block, or whether something is reusable because it is "generic" in some way. Referring to the latter, there is something that I'd consider as some sort of measure for reusability:
Whether one method can emulate another.
Regarding the example from the question: Imagine that the method
void dayLightSavings()
was the implementation of a functionality that was requested by a customer. So it will be something that other programmers are supposed to use, and thus, be a public method, as in
public
void dayLightSavings()
This could be implemented as you showed in your answer. Now, someone wants to parameterize it with the year. So you can add a method
public
void dayLightSavings(int year)
and change the original implementation to just be
public void dayLightSavings() {
dayLightSavings(2018);
}
The next "feature requests" and generalizations follow the same pattern. So if and only if there is demand for the most generic form, you can implement it, knowing that this most generic form allows for trivial implementations of the more specific ones:
public void dayLightSavings() {
dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}
If you had anticipated future extensions and feature requests, and had some time at your disposal and wanted to spend a boring weekend with (potentially useless) generalizations, you could have started with the most generic one right from the beginning. But only as a private method. As long as you only exposed the simple method that was requested by the customer as the public method, you're safe.
tl;dr:
The question is actually not so much "how reusable a method should be". The question is how much of this reusability is exposed, and what the API looks like. Creating a reliable API that can stand the test of time (even when further requirements come up later) is an art and a craft, and the topic is far too complex to cover it here. Have a look at this presentation by Joshua Bloch or the API design book wiki for a start.
dayLightSavings()
callingdayLightSavings(2018)
does not seem like a good idea to me.
– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd calldayLightSavings(computeCurrentYear());
. ...
– Marco13
Dec 1 at 22:44
add a comment |
up vote
0
down vote
There already are many excellent and elaborate answers. Some of them go deeply into specific details, lay out certain viewpoints on software development methodologies in general, and some of them certainly have controversial elements or "opinions" sprinkled in.
The answer by Warbo already pointed out different types of reusability. Namely, whether something is reusable because it is a fundamental building block, or whether something is reusable because it is "generic" in some way. Referring to the latter, there is something that I'd consider as some sort of measure for reusability:
Whether one method can emulate another.
Regarding the example from the question: Imagine that the method
void dayLightSavings()
was the implementation of a functionality that was requested by a customer. So it will be something that other programmers are supposed to use, and thus, be a public method, as in
public
void dayLightSavings()
This could be implemented as you showed in your answer. Now, someone wants to parameterize it with the year. So you can add a method
public
void dayLightSavings(int year)
and change the original implementation to just be
public void dayLightSavings() {
dayLightSavings(2018);
}
The next "feature requests" and generalizations follow the same pattern. So if and only if there is demand for the most generic form, you can implement it, knowing that this most generic form allows for trivial implementations of the more specific ones:
public void dayLightSavings() {
dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}
If you had anticipated future extensions and feature requests, and had some time at your disposal and wanted to spend a boring weekend with (potentially useless) generalizations, you could have started with the most generic one right from the beginning. But only as a private method. As long as you only exposed the simple method that was requested by the customer as the public method, you're safe.
tl;dr:
The question is actually not so much "how reusable a method should be". The question is how much of this reusability is exposed, and what the API looks like. Creating a reliable API that can stand the test of time (even when further requirements come up later) is an art and a craft, and the topic is far too complex to cover it here. Have a look at this presentation by Joshua Bloch or the API design book wiki for a start.
dayLightSavings()
callingdayLightSavings(2018)
does not seem like a good idea to me.
– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd calldayLightSavings(computeCurrentYear());
. ...
– Marco13
Dec 1 at 22:44
add a comment |
up vote
0
down vote
up vote
0
down vote
There already are many excellent and elaborate answers. Some of them go deeply into specific details, lay out certain viewpoints on software development methodologies in general, and some of them certainly have controversial elements or "opinions" sprinkled in.
The answer by Warbo already pointed out different types of reusability. Namely, whether something is reusable because it is a fundamental building block, or whether something is reusable because it is "generic" in some way. Referring to the latter, there is something that I'd consider as some sort of measure for reusability:
Whether one method can emulate another.
Regarding the example from the question: Imagine that the method
void dayLightSavings()
was the implementation of a functionality that was requested by a customer. So it will be something that other programmers are supposed to use, and thus, be a public method, as in
public
void dayLightSavings()
This could be implemented as you showed in your answer. Now, someone wants to parameterize it with the year. So you can add a method
public
void dayLightSavings(int year)
and change the original implementation to just be
public void dayLightSavings() {
dayLightSavings(2018);
}
The next "feature requests" and generalizations follow the same pattern. So if and only if there is demand for the most generic form, you can implement it, knowing that this most generic form allows for trivial implementations of the more specific ones:
public void dayLightSavings() {
dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}
If you had anticipated future extensions and feature requests, and had some time at your disposal and wanted to spend a boring weekend with (potentially useless) generalizations, you could have started with the most generic one right from the beginning. But only as a private method. As long as you only exposed the simple method that was requested by the customer as the public method, you're safe.
tl;dr:
The question is actually not so much "how reusable a method should be". The question is how much of this reusability is exposed, and what the API looks like. Creating a reliable API that can stand the test of time (even when further requirements come up later) is an art and a craft, and the topic is far too complex to cover it here. Have a look at this presentation by Joshua Bloch or the API design book wiki for a start.
There already are many excellent and elaborate answers. Some of them go deeply into specific details, lay out certain viewpoints on software development methodologies in general, and some of them certainly have controversial elements or "opinions" sprinkled in.
The answer by Warbo already pointed out different types of reusability. Namely, whether something is reusable because it is a fundamental building block, or whether something is reusable because it is "generic" in some way. Referring to the latter, there is something that I'd consider as some sort of measure for reusability:
Whether one method can emulate another.
Regarding the example from the question: Imagine that the method
void dayLightSavings()
was the implementation of a functionality that was requested by a customer. So it will be something that other programmers are supposed to use, and thus, be a public method, as in
public
void dayLightSavings()
This could be implemented as you showed in your answer. Now, someone wants to parameterize it with the year. So you can add a method
public
void dayLightSavings(int year)
and change the original implementation to just be
public void dayLightSavings() {
dayLightSavings(2018);
}
The next "feature requests" and generalizations follow the same pattern. So if and only if there is demand for the most generic form, you can implement it, knowing that this most generic form allows for trivial implementations of the more specific ones:
public void dayLightSavings() {
dayLightSavings(2018, 0, 12, 0, 12, new DateTimeFormatter(...));
}
If you had anticipated future extensions and feature requests, and had some time at your disposal and wanted to spend a boring weekend with (potentially useless) generalizations, you could have started with the most generic one right from the beginning. But only as a private method. As long as you only exposed the simple method that was requested by the customer as the public method, you're safe.
tl;dr:
The question is actually not so much "how reusable a method should be". The question is how much of this reusability is exposed, and what the API looks like. Creating a reliable API that can stand the test of time (even when further requirements come up later) is an art and a craft, and the topic is far too complex to cover it here. Have a look at this presentation by Joshua Bloch or the API design book wiki for a start.
answered Dec 1 at 22:10
Marco13
1266
1266
dayLightSavings()
callingdayLightSavings(2018)
does not seem like a good idea to me.
– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd calldayLightSavings(computeCurrentYear());
. ...
– Marco13
Dec 1 at 22:44
add a comment |
dayLightSavings()
callingdayLightSavings(2018)
does not seem like a good idea to me.
– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd calldayLightSavings(computeCurrentYear());
. ...
– Marco13
Dec 1 at 22:44
dayLightSavings()
calling dayLightSavings(2018)
does not seem like a good idea to me.– Koray Tugay
Dec 1 at 22:31
dayLightSavings()
calling dayLightSavings(2018)
does not seem like a good idea to me.– Koray Tugay
Dec 1 at 22:31
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd call
dayLightSavings(computeCurrentYear());
. ...– Marco13
Dec 1 at 22:44
@KorayTugay When the initial request is that it should print "the daylight savings for 2018", then it is fine. In fact, this is exactly the method that you implemented originally. If it should print the "daylight savings for the current year, then of course you'd call
dayLightSavings(computeCurrentYear());
. ...– Marco13
Dec 1 at 22:44
add a comment |
up vote
0
down vote
This is an easy line to draw because re-usability as defined by architecture astronauts is bollocks.
Almost all code created by application developers is extremely domain specific. This isn't 1980. Almost everything worth the bother is already in a framework.
Abstractions and conventions require documentation and learning effort. Kindly stop creating new ones just for the sake of it. (I'm looking at you, JavaScript people!)
Let's indulge the implausible fantasy that you've found something that genuinely ought to be in your framework of choice. You can't just bang up the code the way you usually do. Oh no, you need test coverage not just for intended use but also for departures from intended use, for all known edge cases, for all imaginable failure modes, test cases for diagnostics, test data, technical documentation, user documentation, release management, support scripts, regression tests, change management...
Is your employer happy to pay for all that? I'm going to say no.
Abstraction is the price we pay for flexibility. It makes our code more complex and harder to understand. Unless the flexibility serves a real and present need, just don't, because YAGNI.
Let's have a look at a real world example I just had to deal with: HTMLRenderer. It wasn't scaling properly when I tried to render to the device context of a printer. It took me all day to discover that by default it was using GDI (which doesn't scale) rather than GDI+ (which does, but doesn't antialias) because I had to wade through six levels of indirection in two assemblies before I found code that did anything.
In this case I'll forgive the author. The abstraction is actually necessary because this is framework code that targets five very different rendering targets: WinForms, WPF, dotnet Core, Mono and PdfSharp.
But that only underscores my point: you almost certainly are not doing something extremely complex (HTML rendering with stylesheets) targeting multiple platforms with a stated goal of high performance on all platforms.
Your code is almost certainly yet another database grid with business rules that only apply to your employer and tax rules that only apply in your state, in an application that isn't for sale.
All that indirection solves a problem you don't have and makes your code much harder to read, which massively increases the cost of maintenance and is a huge disservice to your employer. Luckily the people who ought to complain about this are unable to comprehend what you are doing to them.
A counter-argument is that this kind of abstraction supports test driven development, but I think TDD is bollocks too, because it presupposes that the business has a clear, complete and correct understanding of its requirements. TDD is great for NASA and control software for medical equipment and self-driving cars, but way too expensive for everyone else.
Incidentally, it's not possible to predict all the daylight savings in the world. Israel in particular has about 40 transitions every year that hop all over the place because we can't have people praying at the wrong time and god doesn't do daylight savings.
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
add a comment |
up vote
0
down vote
This is an easy line to draw because re-usability as defined by architecture astronauts is bollocks.
Almost all code created by application developers is extremely domain specific. This isn't 1980. Almost everything worth the bother is already in a framework.
Abstractions and conventions require documentation and learning effort. Kindly stop creating new ones just for the sake of it. (I'm looking at you, JavaScript people!)
Let's indulge the implausible fantasy that you've found something that genuinely ought to be in your framework of choice. You can't just bang up the code the way you usually do. Oh no, you need test coverage not just for intended use but also for departures from intended use, for all known edge cases, for all imaginable failure modes, test cases for diagnostics, test data, technical documentation, user documentation, release management, support scripts, regression tests, change management...
Is your employer happy to pay for all that? I'm going to say no.
Abstraction is the price we pay for flexibility. It makes our code more complex and harder to understand. Unless the flexibility serves a real and present need, just don't, because YAGNI.
Let's have a look at a real world example I just had to deal with: HTMLRenderer. It wasn't scaling properly when I tried to render to the device context of a printer. It took me all day to discover that by default it was using GDI (which doesn't scale) rather than GDI+ (which does, but doesn't antialias) because I had to wade through six levels of indirection in two assemblies before I found code that did anything.
In this case I'll forgive the author. The abstraction is actually necessary because this is framework code that targets five very different rendering targets: WinForms, WPF, dotnet Core, Mono and PdfSharp.
But that only underscores my point: you almost certainly are not doing something extremely complex (HTML rendering with stylesheets) targeting multiple platforms with a stated goal of high performance on all platforms.
Your code is almost certainly yet another database grid with business rules that only apply to your employer and tax rules that only apply in your state, in an application that isn't for sale.
All that indirection solves a problem you don't have and makes your code much harder to read, which massively increases the cost of maintenance and is a huge disservice to your employer. Luckily the people who ought to complain about this are unable to comprehend what you are doing to them.
A counter-argument is that this kind of abstraction supports test driven development, but I think TDD is bollocks too, because it presupposes that the business has a clear, complete and correct understanding of its requirements. TDD is great for NASA and control software for medical equipment and self-driving cars, but way too expensive for everyone else.
Incidentally, it's not possible to predict all the daylight savings in the world. Israel in particular has about 40 transitions every year that hop all over the place because we can't have people praying at the wrong time and god doesn't do daylight savings.
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
add a comment |
up vote
0
down vote
up vote
0
down vote
This is an easy line to draw because re-usability as defined by architecture astronauts is bollocks.
Almost all code created by application developers is extremely domain specific. This isn't 1980. Almost everything worth the bother is already in a framework.
Abstractions and conventions require documentation and learning effort. Kindly stop creating new ones just for the sake of it. (I'm looking at you, JavaScript people!)
Let's indulge the implausible fantasy that you've found something that genuinely ought to be in your framework of choice. You can't just bang up the code the way you usually do. Oh no, you need test coverage not just for intended use but also for departures from intended use, for all known edge cases, for all imaginable failure modes, test cases for diagnostics, test data, technical documentation, user documentation, release management, support scripts, regression tests, change management...
Is your employer happy to pay for all that? I'm going to say no.
Abstraction is the price we pay for flexibility. It makes our code more complex and harder to understand. Unless the flexibility serves a real and present need, just don't, because YAGNI.
Let's have a look at a real world example I just had to deal with: HTMLRenderer. It wasn't scaling properly when I tried to render to the device context of a printer. It took me all day to discover that by default it was using GDI (which doesn't scale) rather than GDI+ (which does, but doesn't antialias) because I had to wade through six levels of indirection in two assemblies before I found code that did anything.
In this case I'll forgive the author. The abstraction is actually necessary because this is framework code that targets five very different rendering targets: WinForms, WPF, dotnet Core, Mono and PdfSharp.
But that only underscores my point: you almost certainly are not doing something extremely complex (HTML rendering with stylesheets) targeting multiple platforms with a stated goal of high performance on all platforms.
Your code is almost certainly yet another database grid with business rules that only apply to your employer and tax rules that only apply in your state, in an application that isn't for sale.
All that indirection solves a problem you don't have and makes your code much harder to read, which massively increases the cost of maintenance and is a huge disservice to your employer. Luckily the people who ought to complain about this are unable to comprehend what you are doing to them.
A counter-argument is that this kind of abstraction supports test driven development, but I think TDD is bollocks too, because it presupposes that the business has a clear, complete and correct understanding of its requirements. TDD is great for NASA and control software for medical equipment and self-driving cars, but way too expensive for everyone else.
Incidentally, it's not possible to predict all the daylight savings in the world. Israel in particular has about 40 transitions every year that hop all over the place because we can't have people praying at the wrong time and god doesn't do daylight savings.
This is an easy line to draw because re-usability as defined by architecture astronauts is bollocks.
Almost all code created by application developers is extremely domain specific. This isn't 1980. Almost everything worth the bother is already in a framework.
Abstractions and conventions require documentation and learning effort. Kindly stop creating new ones just for the sake of it. (I'm looking at you, JavaScript people!)
Let's indulge the implausible fantasy that you've found something that genuinely ought to be in your framework of choice. You can't just bang up the code the way you usually do. Oh no, you need test coverage not just for intended use but also for departures from intended use, for all known edge cases, for all imaginable failure modes, test cases for diagnostics, test data, technical documentation, user documentation, release management, support scripts, regression tests, change management...
Is your employer happy to pay for all that? I'm going to say no.
Abstraction is the price we pay for flexibility. It makes our code more complex and harder to understand. Unless the flexibility serves a real and present need, just don't, because YAGNI.
Let's have a look at a real world example I just had to deal with: HTMLRenderer. It wasn't scaling properly when I tried to render to the device context of a printer. It took me all day to discover that by default it was using GDI (which doesn't scale) rather than GDI+ (which does, but doesn't antialias) because I had to wade through six levels of indirection in two assemblies before I found code that did anything.
In this case I'll forgive the author. The abstraction is actually necessary because this is framework code that targets five very different rendering targets: WinForms, WPF, dotnet Core, Mono and PdfSharp.
But that only underscores my point: you almost certainly are not doing something extremely complex (HTML rendering with stylesheets) targeting multiple platforms with a stated goal of high performance on all platforms.
Your code is almost certainly yet another database grid with business rules that only apply to your employer and tax rules that only apply in your state, in an application that isn't for sale.
All that indirection solves a problem you don't have and makes your code much harder to read, which massively increases the cost of maintenance and is a huge disservice to your employer. Luckily the people who ought to complain about this are unable to comprehend what you are doing to them.
A counter-argument is that this kind of abstraction supports test driven development, but I think TDD is bollocks too, because it presupposes that the business has a clear, complete and correct understanding of its requirements. TDD is great for NASA and control software for medical equipment and self-driving cars, but way too expensive for everyone else.
Incidentally, it's not possible to predict all the daylight savings in the world. Israel in particular has about 40 transitions every year that hop all over the place because we can't have people praying at the wrong time and god doesn't do daylight savings.
edited Dec 3 at 0:00
answered Dec 1 at 11:31
Peter Wone
26914
26914
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
add a comment |
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
Although I agree with what you said about abstractions and learning effort, and particularly when it is aimed at the abomination that is called "JavaScript", I strongly have to disagree with the first sentence. Re-usability can happen at many levels, it can go waaaay too far down, and throw-away code can be written by throw-away programmers. But there are people who are idealistic enough to at least aim at reusable code. A pity for you if you don't see the benefits that this can have.
– Marco13
Dec 1 at 21:41
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
@Marco13 - Since your objection is reasonable I'll expand the point.
– Peter Wone
Dec 2 at 23:22
add a comment |
up vote
-3
down vote
If you are using at least java 8, you would write the WorldTimeZones class to provide what in essence appears to be a collection of time zones.
Then add a filter(Predicate filter) method to the WorldTimeZones class. This provides the ability for the caller to filter on anything they want by passing a lambda expression as the parameter.
In essence, the single filter method supports filtering on anything contained in the value passed to the predicate.
Alternately, add a stream() method to your WorldTimeZones class that produces a stream of time zones when called. Then the caller can filter, map and reduce as desired without you writing any specializations at all.
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
add a comment |
up vote
-3
down vote
If you are using at least java 8, you would write the WorldTimeZones class to provide what in essence appears to be a collection of time zones.
Then add a filter(Predicate filter) method to the WorldTimeZones class. This provides the ability for the caller to filter on anything they want by passing a lambda expression as the parameter.
In essence, the single filter method supports filtering on anything contained in the value passed to the predicate.
Alternately, add a stream() method to your WorldTimeZones class that produces a stream of time zones when called. Then the caller can filter, map and reduce as desired without you writing any specializations at all.
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
add a comment |
up vote
-3
down vote
up vote
-3
down vote
If you are using at least java 8, you would write the WorldTimeZones class to provide what in essence appears to be a collection of time zones.
Then add a filter(Predicate filter) method to the WorldTimeZones class. This provides the ability for the caller to filter on anything they want by passing a lambda expression as the parameter.
In essence, the single filter method supports filtering on anything contained in the value passed to the predicate.
Alternately, add a stream() method to your WorldTimeZones class that produces a stream of time zones when called. Then the caller can filter, map and reduce as desired without you writing any specializations at all.
If you are using at least java 8, you would write the WorldTimeZones class to provide what in essence appears to be a collection of time zones.
Then add a filter(Predicate filter) method to the WorldTimeZones class. This provides the ability for the caller to filter on anything they want by passing a lambda expression as the parameter.
In essence, the single filter method supports filtering on anything contained in the value passed to the predicate.
Alternately, add a stream() method to your WorldTimeZones class that produces a stream of time zones when called. Then the caller can filter, map and reduce as desired without you writing any specializations at all.
answered Nov 28 at 11:47
Rodney P. Barbati
1172
1172
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
add a comment |
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
3
3
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
These are good generalization ideas however this answer completely misses the mark on what is being asked in the question. The question is not about how to best generalize the solution but where we draw the line at generalizations and how we weigh these considerations ethically.
– maple_shaft♦
Nov 28 at 13:34
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
So I am saying that you should weigh them by the number of use cases they support modified by the complexity of creation. A method that supports one use case is not as valuable as a method that supports 20 use cases. On the other hand, if all you know of is one use case, and it takes 5 minutes to code for it - go for it. Often coding methods that support specific uses informs you of the way to generalize.
– Rodney P. Barbati
Nov 29 at 21:26
add a comment |
protected by gnat Nov 28 at 5:18
Thank you for your interest in this question.
Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).
Would you like to answer one of these unanswered questions instead?
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
Nov 27 at 6:29
70
The point you're making here is one I have tried to express many times: generality is expensive, and so must be justified by specific, clear benefits. But it goes deeper than that; programming languages are created by their designers to make some kinds of generality easier than others, and that influences our choices as developers. It is easy to parameterize a method by a value, and when that's the easiest tool you have in your toolbox, the temptation is to use it regardless of whether it makes sense for the user.
– Eric Lippert
Nov 27 at 15:02
30
Re-use is not something you want for its own sake. We prioritize re-use because we have a belief that code artifacts are expensive to build and therefore should be usable in as many scenarios as possible, to amortize that cost across those scenarios. This belief is frequently not justified by observations, and the advice to design for reusability is therefore frequently misapplied. Design your code to lower the total cost of the application.
– Eric Lippert
Nov 27 at 18:36
7
Your wife is the unethical one for wasting your time by lying to you. She asked for an answer, and gave a suggested medium; By that contract, how you obtain that output is only between you and yourself. Also,
destroyCity(target)
is way more unethical thandestroyBagdad()
! What kind of monster writes a program to wipe out a city, let alone any city in the world? What if the system was compromised?! Also, what does time/resource management (effort invested) have to do with ethics? As long as the verbal/written contract was completed as agreed upon.– Tezra
Nov 27 at 18:44
25
I think you might be reading too much into this joke. It's a joke about how computer programmers come to make bad ethical decisions, because they prioritize technical considerations over the effects of their work on humans. It's not intended to be good advice about program design.
– Eric Lippert
Nov 27 at 19:14