Dagens kata

This commit is contained in:
2015-05-11 16:32:11 +02:00
parent a776f7d403
commit f3d4612069
22 changed files with 765 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>RefactorToVisitorPatternKata</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,5 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.7

View File

@@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@@ -0,0 +1,138 @@
Refactoring to Visitor pattern
==============================
Import the project, run the tests (`FullSystemTest`). Refactor the classes `Partition` and `Session` to visitor pattern.
If you want to increase the challenge, follow these rules:
* you may NOT modify the contract of ColumnStorage nor the TableStorage classes - they're used in production and must be backwards compatible
* you may NOT modify the Storage interface
The Problem Domain
------------------
This is simplified production code, and the domain centers around storing of data objects. In this code base, there are
two different types of data storage: `ColumnStorage` and `TableStorage`. In order to load data from either of these
you start out with a `Session`. You tell the `Session` what storages to use (Column or Table) and then you can ask the
`Session` to: `load` data; `refresh` the data (by rereading all storage's files to get the latest available); `getSize`
to see how many bytes of RAM this session is consuming; `getLatestCommitId` to get the latest seen commit id.
The objects stored are actually version controlled (well they are in the _real_ code base, you can't really see it in
this simplified version) and each "save" is given an ever increasing "commit id".
Now, there's one more concept to this data model, and that is `Partition`. A `Session` actually doesn't hold references
to `Storage` objects, but to `Partition` objects. These `Partition` objects in turn can hold either other `Partitions`,
and/or `Storage` objects.
While all the operations are available from the `Session` class, they're also available on the `Partition` level, but
not all `Storage` types support the `refresh` operation for instance.
A typical tree would look like this:
Session
|
+---Partition
| |
| +---ColumnStorage (file:/tmp/dump1.json)
| |
| +---TableStorage (file:/tmp/dump2.sql)
|
+---Partition
|
+---Partition
| |
| +--- ColumnStorage (file:/tmp/dump3.json)
|
+---ColumnStorage (file:/tmp/dump4.json)
|
+---TableStorage (file:/tmp/dump5.sql)
(In the real code base, the Partitions are clever filters that applies a filter and only shows the data objects that
pass the filter. In this simplified example, they're only kept to make the Visitor pattern more obvious.)
The Problem
-----------
There are currently several "operations" which, when invoked, traverse the tree structure and does some work on each node in the graph.
Code to handle traversing this structure is found in _all_ of these "operations".
You should refactor this into a pattern that follows a Visitor pattern.
The operations are:
* printInfo
* load
* refresh
* getLatestCommitId
* getSize
Refactoring
===========
The refactoring cycle:
* run the tests and make sure everything is passing
* comment out the code you want to refactor
* put a dumb implementation in place
* run the tests and see some of them fail
* if no tests are failing, undo back to the beginning, add tests and start over until you have a sufficient test suite
* write the new code
* run the tests and make sure everything is passing
Visitor Pattern
===============
Don't read this unless you get stuck, or have no clue on how to use the Visitor pattern. Perhaps try reading http://en.wikipedia.org/wiki/Visitor_pattern
Each concrete Visitor knows what to do at each level in the structure, such as representing the visited object/node as JSON. The visitor, however, doesn't know how to traverse the structure. If it knew this, all visitors would have to know this which may lead to duplication of code, and certainly breaks Single Responsibility Principle. The Visitor thus has one method per object type that it visits.
Each visited object (node), knows how to "welcome" the Visotor object (traditionally the method is called `accept`), and pass the Visitor object around. This 'traversing the structure'-logic is put into the structure itself, and is typically marked with an interface, Visitable.
class Visitable():
def accept(self, visitor):
pass
class Visitor():
def processParent(self, parent):
pass
def processChild(self, child):
pass
class Parent(Visitable):
def accept(self, visitor):
visitor.processParent(self)
for c in self.children:
c.accept(visitor)
class SomeVisitor():
def processParent(self, parent_object):
doSomething ...
def processChild(self, child_object):
doSomethingElse ...
Additional Tasks
----------------
* Consider adding a way to save the structure, as XML, to a File.
* Consider adding a way to save the structure, as JSON, to a File.
* Consider doing away with the accept method, see
http://perfectjpattern.sourceforge.net/dp-visitor.html and
http://perfectjpattern.sourceforge.net/xref/org/perfectjpattern/core/behavioral/visitor/AbstractVisitor.html
Notes
-----
The Storage interface is not present in the Python version of this exercise. Python uses duck typing and as such doesn't need a common base class in order to invoke methods on objects that share a common name.
Yes, the Logging.out is a replacement for System.out - the Python code does this much butter.
Yes, the `_mock_data` loading is so much simple with duck typing and object literal declaration support in the language, ie Python wins again. :-)

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>coding.dojo</groupId>
<artifactId>ref-to-visitor</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,10 @@
package data;
/**
* Implementation omitted, we use test doubles for testing
*/
public abstract class ColumnStorage implements Storage {
abstract public void dump();
}

View File

@@ -0,0 +1,20 @@
package data;
import tree.Visitor;
public class LoadVisitor implements Visitor {
@Override
public void handle(Session session) {
}
@Override
public void handle(Partition partition) {
}
@Override
public void handle(Storage storage) {
storage.load();
}
}

View File

@@ -0,0 +1,9 @@
package data;
import java.io.PrintStream;
public class Logging {
public static PrintStream out;
}

View File

@@ -0,0 +1,107 @@
package data;
import java.util.ArrayList;
import java.util.List;
import tree.Visitable;
import tree.Visitor;
public class Partition implements Visitable {
private String name;
private List<Storage> storages;
private List<Partition> partitions;
public Partition(String name, List<Partition> partitions,
List<Storage> storages) {
this.partitions = partitions;
this.storages = storages;
this.name = name;
}
public Partition(String name) {
this(name, new ArrayList<Partition>(), new ArrayList<Storage>());
}
public void addPartition(Partition partition) {
partitions.add(partition);
}
public void addStorage(Storage storage) {
storages.add(storage);
}
public void printInfo() {
Logging.out.println("Partition: " + name);
for (Partition p : partitions) {
p.printInfo();
}
for (Storage s : storages) {
if (s instanceof TableStorage) {
((TableStorage) s).printout();
}
if (s instanceof ColumnStorage) {
((ColumnStorage) s).dump();
}
}
}
public void load() { // Visitor
// Visitor visitor = new LoadVisitor();
// accept(visitor);
// return visitor;
}
public void refresh() {
for (Partition p : partitions) {
p.refresh();
}
for (Storage s : storages) {
if (s instanceof TableStorage) {
((TableStorage) s).refresh();
}
}
}
public int getSize() {
int total_size = 0;
for (Partition p : partitions) {
total_size += p.getSize();
}
for (Storage s : storages) {
total_size += s.getSize();
}
return total_size;
}
public int getLatestCommitId() {
int latestCommitId = 0;
for (Partition p : partitions) {
int partitionCommitId = p.getLatestCommitId();
if (partitionCommitId > latestCommitId) {
latestCommitId = partitionCommitId;
}
}
for (Storage s : storages) {
if (s instanceof TableStorage) {
int storageCommitId = ((TableStorage) s).getLatestCommitId();
if (storageCommitId > latestCommitId) {
latestCommitId = storageCommitId;
}
}
}
return latestCommitId;
}
@Override
public void accept(Visitor visitor) {
visitor.handle(this);
for (Partition p : partitions) {
p.accept(visitor);
}
for (Storage s : storages) {
s.accept(visitor);
}
}
}

View File

@@ -0,0 +1,27 @@
package data;
import tree.Visitor;
public class RefreshVisitor implements Visitor {
@Override
public void handle(Session session) {
// TODO Auto-generated method stub
throw new RuntimeException();
}
@Override
public void handle(Partition partition) {
// TODO Auto-generated method stub
throw new RuntimeException();
}
@Override
public void handle(Storage storage) {
// TODO Auto-generated method stub
throw new RuntimeException();
}
}

View File

@@ -0,0 +1,65 @@
package data;
import java.util.ArrayList;
import java.util.List;
import tree.Visitable;
import tree.Visitor;
public class Session implements Visitable {
private List<Partition> partitions = new ArrayList<>();
public void addPartition(Partition partition) {
partitions.add(partition);
}
public void printInfo() {
Logging.out.println("Session:");
for (Partition p : partitions) {
p.printInfo();
}
}
public Session load() {
Visitor visitor = new LoadVisitor();
accept(visitor);
return this;
}
public Session refresh() {
// for (Partition p : partitions) {
// p.refresh();
// }
Visitor visitor = new RefreshVisitor();
return this;
}
public int getLatestCommitId() {
int latestCommitId = 0;
for (Partition p : partitions) {
int partitionCommitId = p.getLatestCommitId();
if (partitionCommitId > latestCommitId) {
latestCommitId = partitionCommitId;
}
}
return latestCommitId;
}
public int getSize() {
int totalSize = 0;
for (Partition p : partitions) {
totalSize += p.getSize();
}
return totalSize;
}
@Override
public void accept(Visitor visitor) {
visitor.handle(this);
for (Partition p : partitions) {
p.accept(visitor);
}
}
}

View File

@@ -0,0 +1,11 @@
package data;
import tree.Visitable;
public interface Storage extends Visitable {
int getSize();
void load();
}

View File

@@ -0,0 +1,18 @@
package data;
/**
* Implementation omitted, we use test doubles for testing
*/
public abstract class TableStorage implements Storage {
abstract public int getSize();
abstract public int getLatestCommitId();
abstract public void printout();
abstract public void load();
abstract public void refresh();
}

View File

@@ -0,0 +1,6 @@
package tree;
public interface Visitable {
void accept (Visitor visitor);
}

View File

@@ -0,0 +1,13 @@
package tree;
import data.Partition;
import data.Session;
import data.Storage;
public interface Visitor {
void handle (Session session);
void handle (Partition partition);
void handle (Storage storage);
}

View File

@@ -0,0 +1,48 @@
package data;
import java.util.HashMap;
import java.util.Map;
import tree.Visitor;
public class ColumnStorageFake extends ColumnStorage {
private String name;
private int size = 0;
private static final Map<String, Integer> _mock_data;
static {
_mock_data = new HashMap<>();
_mock_data.put("JSON storage 1", 6645);
_mock_data.put("JSON storage 2", 321);
_mock_data.put("JSON storage 3", 566);
}
public ColumnStorageFake(String name) {
this.name = name;
}
public void dump() {
Logging.out.println("ColumnStorage: " + name);
}
@Override
public int getSize() {
return size;
}
@Override
public void load() {
if (_mock_data.containsKey(name)) {
size = _mock_data.get(name);
} else {
size = -1;
}
}
@Override
public void accept(Visitor visitor) {
visitor.handle(this);
}
}

View File

@@ -0,0 +1,27 @@
package data;
import java.io.*;
public class FakeOut extends PrintStream {
private StringBuffer sb = new StringBuffer();
public FakeOut(OutputStream out) {
super(out);
}
public FakeOut() throws FileNotFoundException, IOException {
super((OutputStream) new FileOutputStream(File.createTempFile("fake", "out")));
}
@Override
public void println(String x) {
sb.append(x);
sb.append("\n");
}
public String getOut() {
return sb.toString();
}
}

View File

@@ -0,0 +1,17 @@
package data;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class FakeOutTest {
@Test
public void simpleFlow() throws Exception {
FakeOut f = new FakeOut();
f.println("Hello");
f.println("World");
assertEquals("Hello\nWorld\n", f.getOut());
}
}

View File

@@ -0,0 +1,88 @@
package data;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class FullSystemTest {
private Session session;
private static final int size_after_load = 46544 + 344 + 465 + 6645 + 321 + 566;
private static final int size_after_refreshes = 51345 + 545 + 513 + 6645 + 321 + 566;
@Before
public void setup() throws Exception {
Logging.out = new FakeOut();
this.session = new Session();
Partition p1 = new Partition("partition 1");
p1.addStorage(new TableStorageFake("SQL storage 1"));
p1.addStorage(new ColumnStorageFake("JSON storage 1"));
session.addPartition(p1);
Partition p2 = new Partition("partition 2");
p2.addStorage(new TableStorageFake("SQL storage 2"));
session.addPartition(p2);
Partition p3 = new Partition("partition 3");
p3.addStorage(new TableStorageFake("SQL storage 3"));
p3.addStorage(new ColumnStorageFake("JSON storage 2"));
p3.addStorage(new ColumnStorageFake("JSON storage 3"));
p2.addPartition(p3);
}
@Test
public void initialCommitIdIsZero() throws Exception {
assertEquals(0, session.getLatestCommitId());
}
@Test
public void commitIdIs125AfterLoad() throws Exception {
session.load();
assertEquals(125, session.getLatestCommitId());
}
@Test
public void commitIdAfterTwoRefreshesIs137() throws Exception {
session.load();
session.refresh();
session.refresh();
assertEquals(137, session.getLatestCommitId());
}
@Test
public void initialSizeIsZero() throws Exception {
assertEquals(0, session.getSize());
}
@Test
public void sizeAfterLoadIs_MagicNumber() throws Exception {
session.load();
assertEquals(size_after_load, session.getSize());
}
@Test
public void sizeAfterTwoRefreshesIs_MagicNumber() throws Exception {
session.load().refresh().refresh();
assertEquals(size_after_refreshes, session.getSize());
}
@Test
public void printSession() throws Exception {
session.printInfo();
String actual = ((FakeOut) Logging.out).getOut();
String expected = "Session:\n" +
"Partition: partition 1\n" +
"TableStorage: SQL storage 1\n" +
"ColumnStorage: JSON storage 1\n" +
"Partition: partition 2\n" +
"Partition: partition 3\n" +
"TableStorage: SQL storage 3\n" +
"ColumnStorage: JSON storage 2\n" +
"ColumnStorage: JSON storage 3\n" +
"TableStorage: SQL storage 2\n";
assertEquals(expected, actual);
}
}

View File

@@ -0,0 +1,83 @@
package data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import tree.Visitable;
import tree.Visitor;
public class TableStorageFake extends TableStorage implements Visitable {
private static final Map<String, Map<String, List<Integer>>> _mock_data;
static {
_mock_data = new HashMap<>();
_mock_data.put("SQL storage 1",
m("cids", "0, 123, 126, 134, 156, 158",
"sizes", "0, 46544, 50444, 51345, 52333, 55991"));
_mock_data.put("SQL storage 2",
m("cids", "0, 125, 133, 134, 143, 155",
"sizes", "0, 344, 544, 545, 633, 791"));
_mock_data.put("SQL storage 3",
m("cids", "0, 124, 127, 137, 177, 199",
"sizes", "0, 465, 504, 513, 523, 559"));
}
private String name;
private int mockDataIndex = 0;
public TableStorageFake(String name) {
this.name = name;
}
@Override
public void printout() {
Logging.out.println("TableStorage: " + name);
}
@Override
public int getSize() {
// return DaveStorageMock._mock_data[name]["sizes"][index]
return _mock_data.get(name).get("sizes").get(mockDataIndex);
}
@Override
public int getLatestCommitId() {
// return DaveStorageMock._mock_data[name]["cids"][index]
return _mock_data.get(name).get("cids").get(mockDataIndex);
}
@Override
public void load() {
this.mockDataIndex = 1;
}
@Override
public void refresh() {
this.mockDataIndex++;
if (this.mockDataIndex > _mock_data.get(name).get("cids").size()) {
throw new IllegalStateException("out of mock data!");
}
}
// this is soo ugly
private static Map<String, List<Integer>> m(Object... args) {
Map<String, List<Integer>> map = new HashMap<>();
for (int index = 0; index < args.length; index = index + 2) {
String key = (String) args[index];
List<Integer> values = new ArrayList<Integer>();
for (String n : ((String) args[index + 1]).split(",")) {
values.add(new Integer(n.trim()));
}
map.put(key, values);
}
return map;
}
@Override
public void accept (Visitor visitor) {
visitor.handle(this);
}
}