public class Monster {
private static final String code;
private static final String userId;
private static final double weight;
private static final int money;
private static final String name;
private static final double height;
private static final Date birth;
private static final Date death;
private static final String type;
// extra 30 params
}
//construction:
public Monster(String code, String userId, double weight, int money, String name,
double height, Date birth, Date death, String type /* ... */){
// ... fields assignment
}
Passing values to such constructor and guessing is a hell:
Monster mon = new Monster("XY1", "user555", 19.5, 2345, "weird_name",
232, someDate, anotherDate, "animalType", /* ... */);
It is extremely hard to support such code, especially when you want to pass 28th parameter, getting lost in order and types. A good question is why do we need such a huge class, but that's another story (legacy code is legacy code).
A good solution is to use builder pattern, introduced by Joshua Bloch and mentioned in Design Pattern GoF book.
The main idea is to replace huge constructor, with private, which takes builder.
Construction of that odd class now looks pretty:
Monster mon = new Monster("weird_name").code("XY1").userID("user555")
.weight(19.5).type("animal").build();
and our class contain public static class and private constructor:
public class Monster {
// class fields
public static class Builder(...){..}
private Monster (Builder builder){..}
}
Main disadvantage over constructor is that you can forget some parameters, unless in constructors, where all parameters are controlled at compile time. But you can add custom validation in build() method. Another frustrating thing is that builder adds a lot of extra code to existent class. Code generation could really help here.
Currently java IDEs do not support builder generation. I found great plugin for eclipse, which allows to do it in 1 click like generating getters and setters.
As an alternative I wrote python script generateBuilder, which does the same from shell:
# ./generateBuilder.py com/foo/Bar.java > com/foo/_Bar.java
#!/bin/python
import sys, re
IDENT_SIZE=4
def getFields(text):
line PT = r"""
(private)\s*(final)\s* amodifier and final
([a-zA-Z_]+\w*[a-zA-Z_.]+)\s* #java type
([a-zA-Z_]+\w*); #java field name
"""
iter = re.finditer(linePT, text, re.VERBOSE)
return dict([m.group(4), m(group.3) for m in iter)]
def ident(times=1):
return " " * IDENT_SIZE * times
def generateClass(className, fields):
str = ident() + "public static class Builder {\n\n"
for fName in fields.keys():
str += "\n" + ident(2) + "private %s %s;\n" % (fields[fName], fName)
str += "\n" + ident(2) + "public Builder() {\n" + ident(2) + "}\n\n"
for fName in fields.keys():
str += ident(2) + "public Builder %s(%s %s){\n" % (fName, fields
[fName], fName)
str += ident(3) + "this.%s = %s;\n" % (fName, fName)
str += ident(3) + "return this;\n"
str += IDENT(2) + "}\n"
str += ident(2) + "public %s build(){\n" % (className,)
str += ident(3) + "return new %s(this);\n" % (className,)
str += ident(2) + "}\n"
str += ident() + "}\n\n"
return str
def generateConstructor(className, fields):
str = ident() + "private %s(Builder builder){\n" % (className,)
for fName in fields.keys():
str += ident(2) + "this.%s = builder.%s;\n" % (fName, fName)
str += ident() + "}";
return str
if __name__ == "__main__":
if len(sys.argv) <> 2:
print "usage: generateBuilder "
sys.exit(-1)
fName = sys.argv[1]
f = open(fName)
fields = getFields(text)
className = re.search(r'(\w+)\.java', fName).group(1)
print generateClass(className, fields)
print generateConstructor(className, fields)
Добрый день.
ReplyDelete------------------
"Main disadvantage over constructor is that you can forget some parameters"
Тут[http://www.infoq.com/articles/internal-dsls-java] решают эту проблему так:
dialog.table("results").selectCell(6, 8);
заменяют на
dialog.table("results").select(cellAt(row(6), column(8));
или
dialog.table("results").select(cellAt(column(3), row(5));
------------------
"builder adds a lot of extra code to existent class"
Просто билдер должен быть отдельным классом, тогда Вы даже не меняете начальный класс. По ссылке есть пример:
Vacation vacation = vacation().starting("10/09/2007")
.ending("10/17/2007")
.city("Paris")
.hotel("Hilton")
.airline("United")
.flight("UA-6886");
так вот .vacation() - статический метод, НО не обязательно в класе Vacation!
P.S. Попал к Вам случайно, нас обоих читает Илья Гаврисевич:)
P.P.S. О Internal DSL я писал несколько раз, очень популярная сейчас тема:
1) http://kharkovconcurrencygroup.blogspot.com/2010/01/matchers-constraints-predicates.html
2) http://kharkovconcurrencygroup.blogspot.com/2010/04/dsl-apachecamel.html
3) http://kharkovconcurrencygroup.blogspot.com/2010/10/approach-to-internal-domain-specific.html
Здравствуйте, Иван,
ReplyDeleteСпасибо за коментарий и полезные ссылки на статьи.
dialog.table("results").select(cellAt(row(6), column(8));
Мощный вариант, хотя как мне кажется, иногда запутывает код. Подобный подход в jmock используется. Все равно для констуктор из 20 параметров это не оправдывает
-------------
Просто билдер должен быть отдельным классом, тогда Вы даже не меняете начальный класс.
>> Makes sense, особенно для legacy кода
P.S. Илье огромнейший привет.