За прошлый год я принципиально изменил своё отношение
к тому как писать код, что я считаю хорошей практикой и что плохой.
Вот несколько подходов которые я использую:
1. final and Immutable (inspired by
errlang):
Идея простая, все значения которые вы передаёте между методами
должны быть Immutable. Все переменные должны быть final.
Но без фанатизма. Например это я считаю нормальным:
int index = 0;
for(final String name:myNames){
index++;
sout(index+" "+name);
}
Хотя лучше делать вот так:
final int positiveSum; {
int pisitiveSumAccumulator = 0;
for(final Integer rawValue:listRawValues()){
if (rawValue>0){
pisitiveSumAccumulator +=rawValue;
}
}
positiveSum = pisitiveSumAccumulator;
}
Т.е. если вы используете какую то логику для вычисления значения переменной,
которая требует модификации значения этой переменной и вы не вынесли эту логику
в отдельный метод - такую логику нужно завернуть во вложенный scope
что бы скрыть из видимости дальнейшего кода метода все промежуточные значения.
Раз они в дальнейшем вам не нужны - пусть они будут невидимыми.
Причём выносить отдельный метод часто хуже чем написать именно так,
поскольку читаемость кода сильно снизиться - а переиспользоать метод будет просто негде.
В частности: формальные параметры метода должны быть final.
public void setName(final String name){
this.name = name;
}
Бонус конкретно в этом случае в том что если упустить "this." - код просто не скомпилируется.
2. Robert C. Martin
считает что максимальное количество архументов функции - 1 (один).
Я отношусь к этому спокойнее. Если параметры метода просто имеют разный тип то то что их несколько не большая проблема. Пять параметров разного типа - почему нет.
Плохо когда:
void foo(int,int,int,int,int,int);
Сложные аргументы нужно передавать через Immutable бины, такие бины нужно строить через билдер, например:
class Param {
private String firstName;
private String lastName;
public String getFirstName(){ return firstName;}
public String getLastName(){ return lastName;}
public static class Builder {
private Param delegate = new Param();
public Builder setFirstName(final String firstName){ this.firstName=Preconditions.checkNotNull(firstName); return this;}
public Builder setLastNameOptinal(final String lastName){ this.lastName=Preconditions.checkNotNull(lastName); return this;}
public Param build(){
final Param result = Preconditions.checkNotNull(this.delegate);
this.delegate = null; //close builder.
Preconditions.checkNotNull(result.firstName); //note: second name is optional.
return result;
}
}
Если параметров всё же мало, лучше передать их явно, тогда IDE подсветить если кто то из них не
используется. А это уже знак.
3. С результатом метода всё ещё проще. Результат должен всегда возвращается
с возвращаемым значением. Если результат состоит более чем из одного значения
и в стандартном домене приложения под него нет говтового типа - да нужно заводить
отдельный бин для хренения результата метода. Под каждый метод - отдельный такой бин.
И тут начинается самое интересное.
4. Classes Layout. Тут есть два основным принципа.
- не нужно стрематься добавлять новые классы, это нормально.
- классы нужно декларировать как можно ближе к их использованию.
Приведу пример сразу интерфейса и реализации (пример является так же
продолжением предыдущего пункта):
public interface FooFace {
public static class FooParam{
...
public static class Builder {
...
}
}
public static class FooResult {...}
public static class FooException extends Exception {...}
FooResult performFoo(FooParam param) throws FooException;
}
public class FooFaceImpl implements FooFace {
public FooResult performFoo(final FooParam param) throws FooException {
...
return new FooResult(a,b,c);
}
}
Здесь всё получается очень логично. Вмест того что бы мыкаться, выдумывать
имена пакетов в которые рассовать нужные нам классы параметров и результатов
мы просто определям их (классы) в интерфейсе, рядом с методом в котором
они используются.
В частности это хорошо тем что если мы решим выкинуть какую то часть логики,
и удалить соответствущий интерфейс - вся вспомогательная инфраструктура
уйдёт в никуда вместе с ним - просто потому что вспомогательные классы лежат
в том же файле. Это очень правильно.
Да, если вдруг окажется что какой то из типов параметров или результата
используется более чем в одном интерфейсе - можно будет подумать,
что же я делаю не так,
и рефакторить код соответствующим образом (может быть вынести, может быть нет).
5. Method Body. В язык, простите, Pascal была классная идея - можно было определять
одни методы внутри других методов. Но этим никто не пользовался. В "С" эта фишка
и в буквальном смысле вложенных методов в java нет.
Когда пишешь логику, почему то часто получается что один метод занимает
допустим 500 строк. С точки зрения поддержки кода это очень плохо, по этому
некоторые разбивают такой метод на пачку приватных методов.
Допустим у вас есть интерфейс из 10 сравнительно тяжёлых и не очень методов,
каждый из которых в реализации разбит от 3-х до 5-ми методов.
Вы открываете класс реализации и О БОЖЕ МОЙ. Совершенно не понятно
какой метод относится к какой части логики, и если не дай бог,
нужно удалить какой то из публичных методов, на то что бы вычистить
код и удалить приватные методы которые больше не используются
может уйти значительное время (какие то использовались напрямую,
какие то через цепочку вызовов, какие то используются в нескольких
публичных методах) - ребус.
На самом деле решение очень простое, его активно применяют при
разработке на swing, а вот в backend-е я его ни у кого ещё не видел.
public class FooFaceImpl implements FooFace {
public FooResult performFoo(final FooParam param) throws FooException {
return new Callable {
private List listSomeData(final FooParam listParam){....}
private List filterSomeData(final List dataList){...}
private FooResult doSomeStuff(final List filteredDataList) throws FooException{...}
public call() throws FooException {
final List rawList = listSomeData(param);
final List filteredList = filterSomeData(rawList);
final FooResult result = doSomeStuff(filteredList);
return result;
}
}.call();
...
return new FooResult(a,b,c);
}
}
Таким образом логику внутри метода можно без проблем разбивать на
более мелкие этапы, заворачивая отдельные элементы логики в отдельные
методы анонимного inline класса.
(И да, оверхед по производительности в этом случае крайне мал.)
Такая схема очень хорошо масштабируется. Если вам нужно написать
два публичных метода которые разделяют какую то общую логику,
эту логику можно вынести в inner класс, а уже внутри публичных методов
унаследоваться от неё и в inline классах реализовать специфику каждого
метода. Пример:
public class FooFaceImpl {
public FooResultOne fooOne(){
return new FooCommonLogic(){ public FooResultOne call(){....}}.call();
}
public FooResultTwo fooTwo(){
return new FooCommonLogic(){ public FooResultTwo call(){....}}.call();
}
private abstract class FooCommonLogic implements Callable {
protected void barOne(){}
protected void barTwo(){}
....
}
}
Такое решение хорошо тем, что логика реализации оказывается
тесно связана с соответствущим публичным методом. Здесь гораздо
проще понять что у чему относится, и в частности выпилить устаревшую
логику если это потребуется.