[welc] 22. i need to change a monster method and i can’t write tests for it
DESCRIPTION
TRANSCRIPT
22. I Need to Change a Monster Method and I Can’t Write Tests
for It
ohyecloudy(http://ohyecloudy.com)
2008.12.20
Bulleted method
• nearly no indentation
___________________________ ____________________ __________________________ ___________________________ ___________________ ____________________________ ______________________ __________________ ____________ _______________ __________________ _______________
Snarled Method
• dominated by a single large, indented section
____________________________ ____________________ __________________________ ___________________________ ___________________ ____________________________ ______________________ ___________________________ _______________________ __________________________ __________________ ______________________________ _______________
몬스터 함수 격파에 앞서
• refactoring tool이 있고 없고 차이 크다.
• extract method 같은 경우 사용 빈도가 크기 때문에 대부분의 리팩토링 툴이 지원하는데,
• 툴이 안젂하게 extract method를 수행하면 테스트 안 해도 된다.
Tackling Monsters with Automated Refactoring Support
• key goals
– dependency가 작은 chunk부터 logic과 분리 시킨다.
– 이후 진행될 refactoring의 test를 쉽게 하기 위해 seam을 삽입한다.
• 리팩토링 툴을 사용하면 extract method는 test 없이 진행 가능하다.
class CommoditySelectionPanel { … public void update() { if (commodities.size() > 0 && commodities.GetSource().equals(“local”)) { listbox.clear(); for (Iterator it = commodities.iterator();
it.hasNext(); ) { Commodity commodity = (Commodity)it.next(); if (commodity.isTwilight()
&& !commodity.match(broker)) { listbox.add(commodity.getView()); } } } } }
class CommoditySelectionPanel { … public void update() { if (commoditiesAreReadyForUpdate()) { clearDisplay(); updateCommodities(); } } private boolean commoditiesAreReadyForUpdate() { return commodities.size() > 0 && commodities.GetSource().equals(“local”); } private void clearDisplay() { listbox.clear(); } private void updateCommodities() { for (Iterator it = commodities.iterator(); it.hasNext(); ) { Commodity commodity = (Commodity)it.next(); if (singleBrokerCommodity(commodity)) { displayCommodity(commodity.getView()); } } } private boolean singleBrokerCommodity(Commodity commodity) { return commodity.isTwilight() && !commodity.match(broker); } private void displayCommodity(CommodityView view)
{ listbox.add(commodity.getView()); } }
동작이 함수단위로 변경됐을 뿐 구조적으로 바뀐 건 하나도 없다. 이후 Dependency를 제거하고 유닛 테스트를 진행한다.
The Manual Refactoring Challenge
• 유닛 테스트를 진행하면서 리팩토링.
• Extract method에서 실수하는 부분.
– 변수 젂달을 잊는 경우
– base class의 method와 동일한 이름을 짓는 경우
– 잘못된 type을 패러매터로 선언 혹은 리턴
Introduce Sensing Variable public class DOMBuilder { … void processNode(XDOMNSnippet root, List childNodes) { if (root != null) { if (childNodes != null) root.addNode(new XDOMNSnippet(childNodes)); root.addChild(XDOMNSnippet.NullSnippet); } List paraList = new ArrayList(); XDOMNSnippet snippet = new XDOMNReSnippet(); snippet.setSource(m_state); for (Iterator it = childNodes.iterator(); it.hasNext(); ) { XDOMNNode node = (XDOMNNode)it.next(); if (node.type() == TF_G || node.type() == TF_H || (node.type() == TF_GLOT && node.isChild())) { paraList.addNode(node); } } } }
public class DOMBuilder { … public boolean nodeAdded = false; void processNode(XDOMNSnippet root, List childNodes) { if (root != null) { if (childNodes != null) root.addNode(new XDOMNSnippet(childNodes)); root.addChild(XDOMNSnippet.NullSnippet); } List paraList = new ArrayList(); XDOMNSnippet snippet = new XDOMNReSnippet(); snippet.setSource(m_state); for (Iterator it = childNodes.iterator(); it.hasNext(); ) { XDOMNNode node = (XDOMNNode)it.next(); if (node.type() == TF_G || node.type() == TF_H || (node.type() == TF_GLOT && node.isChild())) { paraList.addNode(node); nodeAdded = true; } } } }
Sensing Variable을 사용한 Unit Test void testAddNodeOnBasicChild() { DOMBuilder builder = new DomBuilder(); List children = new ArrayList(); children.add(new XDOMNNode(XDOMNNode.TF_G)); builder.processNode(new XDOMNSnippet(), children); assertTrue(builder.nodeAdded); }
void testNoAddNodeOnBasicChild() { DOMBuilder builder = new DomBuilder(); List children = new ArrayList(); children.add(new XDOMNNode(XDOMNNode.TF_A)); builder.processNode(new XDOMNSnippet(), children); assertFalse(builder.nodeAdded); }
public class DOMBuilder { … public boolean nodeAdded = false; void processNode(XDOMNSnippet root, List childNodes) { if (root != null) { if (childNodes != null) root.addNode(new XDOMNSnippet(childNodes)); root.addChild(XDOMNSnippet.NullSnippet); } List paraList = new ArrayList(); XDOMNSnippet snippet = new XDOMNReSnippet(); snippet.setSource(m_state); for (Iterator it = childNodes.iterator(); it.hasNext(); ) { XDOMNNode node = (XDOMNNode)it.next(); if (isBasicChild(node)) { paraList.addNode(node); nodeAdded = true; } } } private boolean isBasicChild(XDOMNNode node) { return node.type() == TF_G || node.type() == TF_H || node.type() ==
TF_GLOT && node.isChild(); } }
extract method를 한 후에 unit test를 통과하면 sensing variables과 test code를 삭제한다.
Break Out a Method Object
• Sensing variable 대신에 사용할 수 있다.
• 메서드의 parameter를 생성자의 parameter로 받고 run() 또는 execute()와 같은 메서드로 동작한다.
• temporary variable을 instance variable로 변경할 수 있다.
– sensing variable 같은 경우 기존에 있는 변수를 사용하기에 적합하나 temporary 혹은 local 변수라서 test에 사용하기 어려운 경우가 있다.
– Method Object는 좋은 해결책이 된다.
Extract What You Know
• 작은 코드 조각을 테스트 없이 extract한다.
– two or three lines
• 테스트와 다음 작업을 위한 좋은 출발점
• Coupling count가 0일때 사용.
– # of values that pass into and out
• Coupling count가 0보다 클때는 Sensing variable을 사용해라.
Coupling count 예 void process(int a, int b, int c) { int maximum; if (a > b) maximum = a; else maximum = b; }
void process(int a, int b, int c) { int maximum = max(a,b); }
in : a, b out : max return int Coupling count : 3
Gleaning Dependencies
• 보호하고 지켜야 하는 logic에 대해서 test를 작성한다.
• 테스트 범위에 들어가지 않는 chuck를 추출한다.
• 적어도 중요한 동작이 제대로 돌아갂다는 확신을 할 수 있다.
void addEntry(Entry entry) { if (view != null && DISPLAY == true) { view.show(entry); } … if (entry.category().equals(“single”) ||
entry.category(“dual”)) { entries.add(entry); view.showUpdate(entry, view.GREEN); } else { … } }
• Display code에 관한 실수가 있으면 빨리 알아챌 수 있다.
• 하지만 add login에서 에러가 있으면 찾는데 시갂이 걸릴 것이다.
• add login에 관한 테스트를 작성하자.
• 테스트가 성공하면 display code를 추출한다.
Sensing variables
• 테스트 하는데 사용해서 리팩토링을 돕는다.
• sensing variables로 쓰기에 딱인데, method의 local variable인 경우가 많다.
– instance variable인 경우에는 method가 동작한 후에도 sense할 수 있다.
• 그래서 local variables into instance variable로 고치는데 많이 혺동된다.
Strategy
• Skeletonize Methods
• Find Sequences
• Extract to the Current Class First
• Extract Small Pieces
• Be Prepared to Redo Extractions
Skeletonize Methods if (marginalRate() > 2 && order.hasLimit()) { order.readjust(rateCalculator.rateForToday()); order.recalculate(); }
if (orderNeedsRecalculation(order)) { recalculateOrder(order, rateCalculator); }
메서드만 남아있는 상태
Find Sequences if (marginalRate() > 2 && order.hasLimit()) { order.readjust(rateCalculator.rateForToday()); order.recalculate(); }
… recalculateOrder(order, rateCalculator); … void recalculateOrder(Order order, RateCalculator rateCalculator) { if (marginalRate() > 2 && order.hasLimit()) { order.readjust(rateCalculator.rateForToday()); order.recalculate(); } }
chunk가 하나의 메서드가 되서 연산(operation)의 한 순서(sequence)
Skeletonize Methods , Find Sequences
• bulleted methods lean me toward finding sequences
• snarled methods lean me toward skeletonizing
• Extract to the Current Class First
– 현재 클래스에서 먼저 extract해라.
• Extract Small Pieces
– 작은 조각을 extract하는 것은 좋은 출발점.
• Be Prepared to Redo Extractions