How do I know how reusable my methods should be?
up vote
88
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
|
show 14 more comments
up vote
88
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
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
2 days ago
40
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
2 days ago
18
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
2 days ago
5
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
2 days ago
12
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
2 days ago
|
show 14 more comments
up vote
88
down vote
favorite
up vote
88
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 9 hours ago
asked Nov 27 at 3:18
Koray Tugay
6431714
6431714
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
2 days ago
40
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
2 days ago
18
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
2 days ago
5
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
2 days ago
12
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
2 days ago
|
show 14 more comments
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
2 days ago
40
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
2 days ago
18
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
2 days ago
5
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
2 days ago
12
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
2 days ago
5
5
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
2 days ago
Possible duplicate of Rule of thumb for cost vs. savings for code re-use
– gnat
2 days ago
40
40
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
2 days ago
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
2 days ago
18
18
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
2 days ago
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
2 days ago
5
5
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
2 days ago
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
2 days ago
12
12
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
2 days ago
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
2 days ago
|
show 14 more comments
11 Answers
11
active
oldest
votes
up vote
80
down vote
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.
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
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
yesterday
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
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ć
yesterday
1
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ć
yesterday
|
show 9 more comments
up vote
32
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
2 days ago
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
2 days ago
5
@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
2 days ago
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
2 days ago
3
@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
2 days ago
|
show 7 more comments
up vote
20
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
yesterday
add a 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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.
– svidgen
2 days ago
|
show 1 more comment
up vote
2
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
add a comment |
up vote
1
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()
)
);
});
}
}
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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
|
show 2 more comments
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
yesterday
@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
16 hours ago
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
4 hours ago
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
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
-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.
1
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♦
yesterday
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
6 hours ago
add a comment |
protected by gnat yesterday
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?
11 Answers
11
active
oldest
votes
11 Answers
11
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
80
down vote
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.
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
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
yesterday
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
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ć
yesterday
1
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ć
yesterday
|
show 9 more comments
up vote
80
down vote
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.
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
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
yesterday
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
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ć
yesterday
1
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ć
yesterday
|
show 9 more comments
up vote
80
down vote
up vote
80
down vote
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 2 days ago
answered 2 days ago
Flater
6,15511120
6,15511120
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
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
yesterday
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
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ć
yesterday
1
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ć
yesterday
|
show 9 more comments
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
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
yesterday
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
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ć
yesterday
1
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ć
yesterday
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
Even if the execution for city and region is different, abstracting that away might be worthwhile ("Everything is a file" is a well-known example). And even if some actions do not make sense for any/some cities/regions, it might be worth allowing the caller to try, one just has to decide what to do for errors (skip, error-code, exception, ...). Still, it's easier if there is no need to paper over differences, be it there are none or some lower level did it already.
– Deduplicator
2 days ago
13
13
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
yesterday
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
yesterday
1
1
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
>"I don't know where I read it [..]". You may have been reading Coding Horror: The Rule of Three.
– Rune
yesterday
5
5
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ć
yesterday
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ć
yesterday
1
1
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ć
yesterday
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ć
yesterday
|
show 9 more comments
up vote
32
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
2 days ago
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
2 days ago
5
@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
2 days ago
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
2 days ago
3
@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
2 days ago
|
show 7 more comments
up vote
32
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
2 days ago
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
2 days ago
5
@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
2 days ago
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
2 days ago
3
@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
2 days ago
|
show 7 more comments
up vote
32
down vote
up vote
32
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 2 days ago
Telastyn
91.5k24206315
91.5k24206315
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
2 days ago
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
2 days ago
5
@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
2 days ago
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
2 days ago
3
@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
2 days ago
|
show 7 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
2 days ago
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
2 days ago
5
@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
2 days ago
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
2 days ago
3
@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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
@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
2 days ago
5
5
@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
2 days ago
@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
2 days ago
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
2 days ago
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
2 days ago
3
3
@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
2 days ago
@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
2 days ago
|
show 7 more comments
up vote
20
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
yesterday
add a comment |
up vote
20
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
yesterday
add a comment |
up vote
20
down vote
up vote
20
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 yesterday
Peter Mortensen
1,11621114
1,11621114
answered 2 days ago
Kain0_0
8158
8158
+1 love the part about the compiler!
– Lee
yesterday
add a comment |
+1 love the part about the compiler!
– Lee
yesterday
+1 love the part about the compiler!
– Lee
yesterday
+1 love the part about the compiler!
– Lee
yesterday
add a 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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.
– svidgen
2 days ago
|
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.
– svidgen
2 days ago
|
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 2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.
– svidgen
2 days ago
|
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.
– svidgen
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
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
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...
takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.– svidgen
2 days ago
Heh. Sure, I guess I might argue the point with Nathaniel Borenstein if I were in a room with him and he was spouting useless dogma...
takeActionOnCity(Action a ...)
looks like an abstraction you'd end up with if you needed the command pattern -- if you needed strong "implicit" auditing or the ability to perform rollbacks and retries. Otherwise, it looks to me like it just makes harder-to-read code than is necessary.– svidgen
2 days ago
|
show 1 more comment
up vote
2
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
add a comment |
up vote
2
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
add a comment |
up vote
2
down vote
up vote
2
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 2 days ago
Ewan
36.8k32981
36.8k32981
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
add a comment |
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
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
yesterday
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
yesterday
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
yesterday
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
yesterday
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
yesterday
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
yesterday
an ourput stream is a consumer
– Ewan
yesterday
an ourput stream is a consumer
– Ewan
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
No, an OutputStream is not a Consumer.
– David Conrad
yesterday
add a comment |
up vote
1
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()
)
);
});
}
}
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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
|
show 2 more comments
up vote
1
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()
)
);
});
}
}
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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
|
show 2 more comments
up vote
1
down vote
up vote
1
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()
)
);
});
}
}
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()
)
);
});
}
}
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 yesterday
answered yesterday
Stuporman
1193
1193
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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
|
show 2 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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
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♦
yesterday
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♦
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
@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
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
It is better, I gave you an upvote.
– maple_shaft♦
yesterday
|
show 2 more comments
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
yesterday
@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
16 hours ago
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
yesterday
@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
16 hours ago
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 16 hours ago
answered 2 days ago
AnoE
3,873717
3,873717
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
yesterday
@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
16 hours ago
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
yesterday
@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
16 hours ago
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
yesterday
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
yesterday
@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
16 hours ago
@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
16 hours ago
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
4 hours ago
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
4 hours ago
add a 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 4 hours ago
Warbo
97959
97959
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
4 hours ago
add a 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
4 hours ago
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
4 hours ago
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
4 hours ago
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
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 2 days ago
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 yesterday
answered yesterday
k3b
6,65011127
6,65011127
add a comment |
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.
1
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♦
yesterday
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
6 hours ago
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.
1
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♦
yesterday
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
6 hours ago
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 yesterday
Rodney P. Barbati
1172
1172
1
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♦
yesterday
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
6 hours ago
add a comment |
1
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♦
yesterday
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
6 hours ago
1
1
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♦
yesterday
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♦
yesterday
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
6 hours ago
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
6 hours ago
add a comment |
protected by gnat yesterday
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
2 days ago
40
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
2 days ago
18
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
2 days ago
5
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
2 days ago
12
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
2 days ago