Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

четверг, 31 января 2013 г.

Мечты! Сбывайтесь!!

На сегодняшний день из материальных вещей хочу штуки из этого списка:

1) Я недавно на ночь завис на одном портале по роботостроению и нашел просто чудесный конструктор. Только как его купить покаа не нашел.. Милашка такой.


2) А еще наткнулся на такой дивайс. Так как люблю всякие мозговые штуки и мечтаю просто, что когда-то не надо будет клацать по клавишам с очепятками и держаться одной рукой за мышку....


3) Еще домой хочу проектор и плакат, чтобы фильмы смотреть + звуковую какую-то систему, чтобы громко, объемно. Ну или просто громко :) Чтобы как-то так...


4) А еще хочу электрогитару и комбик, но это будет осенью скорее всего от Друга. А если не будет, то будет даже раньше! Хочу попробовать себя в этом. А может получится. Как минимум в компании я должен иметь возможность взять гитару и сиграть пару красивых песен.

Хотя внешний вид, это наверное самое последнее, на что стоит обращать внимание - но я обычно доверяю своей интуиции и совету опытных друзей. Тем более, что мне не с чем сравнивать, и первая гитара будет абсолютно идеальной. Так что пусть будет эта. Может когда-то в детстве при просмотре какого-то яркого фильма я ее видел и поставил мысленную заметку в ТУДУ, не знаю. Но как увидел ее зацепился взглядом...

5) По музыке с синтезатором уже вроде как освоился. Еще хочу фортепиано электронное, хорошее. Но пока дорого, зараза. Физику клавиш надобно передать.На синтезаторе не то немного.


6) Хочу ребенку какой-то плашнетник, чтобы она играться в игрушки могла и оставила Ксюхин телефон в покое :) Она любит очень паззззлы собирать, а на планшете это интереснее чем в реале. Как минимум, не терются запчасти..


7) Хочу на свой велик электромотор с акумулятором, чтобы хоть иногда не крутить педали или просто далеко ехать... Или вот такой ebike


8) А вообще хочу мотоцикл. Пусть будет этот
Коль уж такая пьянка, открыл свои старые заказы. Часть из них уже реализована, но что-то осталось девственно-нетронутым.

9) Домик хочу на дереве вот такой
А чтобы внутри там было как-то так, или чтобы так было где-то еще, а домик не трогать!
Камин обязательно.Пледик теплый и глинтвейн. А на улице пусть дождь, сильный сильный, гроза прям.

10) В коммандировку хочу сюда, на пару неделек - пол года. Чтобы через день-два какие-то походы, с фотками, с очучениями! А на следующий день в кроватке с ноутом писать отчеты и делать свою работу-хобби.

11) Хочу электронный микроскоп, чтобы к компу подключался и зум был зумистый

12) Если не в близ, так в даль. Телескоп. А еще лучше мини обсерваторию на крыше моего домика.

13) А еще вдруг подумалось, что хочу спасти жизнь, хотя бы одну...


Спасибо!

среда, 30 января 2013 г.

Как сделать робота?

С этим вопросом недавно сунулся я в сеть. И что там увидел?

Первый робот Hexy - я так и не нашел, как заказать его себе домой.

Если кто найдет - пожалуйста, отпишите в комментах.

Построен Hexy на базе контроллера Arduino.

Кстати вот http://robocraft.ru/ отличнейший портал про разработки на базе этого контроллера (описание контроллера можно почитать тут). Я на нем застрял на многие часы. Портал этот по совместительству еще и магазин.

В сети есть множество других магазинов, таких как этот http://ru.farnell.com - купить там можно все: контроллеры, ходовую, платы расширения с помощью которых твой робот сможет общаться со всем, чем угодно, хитрый пластик для его скелета.

Кстати про чудопластик. Плавится он при 60 градусах. Лепишь с него то, что надо. Потом он застывает и становится прочный что аж сверлить можно. Примеры использования пластика, а тут про его свойства.

А вот и пару разработок (зацепивших меня)

Описание того, как это делается лежит тут
Какой милашка
А эта змея - вообще разрыв сознания

Немного неуклюжий но зато музыкальный робот

Еще один красавчег

Только вот очень на фейк похож...
Потом я ушел в сторону упрвления компьютером/роботами с помощью мысли

Блин, как классно было бы избавиться от клавиатуры и мышки навсегда... Подробнее тут.

Кстати, эта штука уже и продается. На этом меня настигло утро и я пошел спать...

вторник, 22 января 2013 г.

Сегодня суперкод - завтра говнокод

Сегодня знакомились с новой группой Java тренинга в КПИ. На этот раз был приглашен в качестве гостя, чем бесконечно рад. Но не об этом сейчас. А о том, что случилось на этой встрече. Цель собрания была познакомиться, сделать коллективно code review, после чего ребята самостоятельно могли бы общаться и решать поставленные задачи. В ходе code review мы зацепили тему Don't Repeat Yourself, а так же магических констант. Не сдержался - показал свой код 10 летней давности. Код все того же фрактального проводника, который писал во времена студенческие. Писал я его на Delphi7. В общем, я его просто тут оставлю и все станет понятно...
type
  TPal = array [0..511] of TColor;
  TSavePalRec = array [1..50] of record
    Color:TColor;
    X:Integer;
  end;
//---------------------------------------------------------------------------------------------
var
    PalProp:record
        mT, mW, mH, mW05:integer;
    end;
    ClkX, ClkY:integer;
    bMovePan, bDouble:boolean;
    SavePalRec:TSavePalRec;
    pan:array [1..100] of record
        x:integer;
        col:TColor;
    end;
    CountPan:integer;
    bDown:boolean;
    bIncDec:boolean;
    RedPos, GreenPos, BluePos:integer;
    RedCount, GreenCount, BlueCount:integer;
    ColArrR, ColArrG, ColArrB:array [0..1024] of byte;
    bMoveR, bMoveG, bMoveB:boolean;
    bmp:TBitMap;
//--------------------------------------------------------------------------------------------- 
 const NumbColor=19;
      ColorArray:array [0..NumbColor - 1] of TColor=($FFFFFF, $00FFFF, $FF00FF, $FFFF00, $0000FF, $FF0000, $00FF00, $C0FFFF, $FFC0FF, $FFFFC0, $C0C0FF, $FFC0C0, $C0FFC0, $C000FF, $00C0FF, $00FFC0, $C0FF00, $FFC000, $FF00C0);
//---------------------------------------------------------------------------------------------
function TForm2.CreateAndSortPanel(X:integer; bRepaint:boolean; cl:TColor):integer;
var i, j, tle, le:integer;
    col, tcol:TColor;
begin
    for i:=1 to CountPan-1 do // определяем после которого маркера будет создаваемый
        if (pan[i].x < X) and (X < pan[i+1].x) then break;

    if i = CountPan then Exit; // если после последнего то выходим

    Result:=i;

    if (pan[i+1].x - pan[i].x) < PalProp.mW+2 then Exit;  // если маркер некуда втиснуть между двумя ближайшими то выходим
    if ((pan[i].x + PalProp.mW+1) > X) or ((pan[i+1].x - PalProp.mW-1) < X) then Exit; // очень близко ставить маркер возле соседнего нельзя

    le:=pan[i+1].x;
    col:=pan[i+1].Col;
    pan[i+1].x:=X;
    pan[i+1].Col:=cl;

    for j:=i+2 to CountPan do begin
        tle:=pan[j].x;
        tcol:=pan[j].Col;
        pan[j].x:=le;
        pan[j].Col:=col;
        le:=tle;
        col:=tcol;
    end;

    CreatePanel(pb.Width);

    CurrentPan:=i+1;

    pan[CountPan].Col:=col;
    if bRepaint then begin
        RepaintPan(CurrentPan);
        RepaintPalitra(i, i+2);
    end;
end;
//---------------------------------------------------------------------------------------------
procedure TForm2.RepaintPan(Num: Integer);
var x1, x2, y1, y2:integer;
begin
    if Num = 1
        then x1:=0
        else x1:=pan[Num-1].x + PalProp.mW05+1;
    if Num = CountPan
        then x2:=pb.Width
        else x2:=pan[Num+1].x - PalProp.mW05-1;
    y1:=PalProp.mT + 1;
    y2:=PalProp.mT + PalProp.mH + 1;

    bmp.Canvas.Pen.Color:=clBtnFace;
    bmp.Canvas.Brush.Color:=clBtnFace;
    bmp.Canvas.Rectangle(x1, y1, x2, y2);

//    if Num = CurrentPan
//        then bmp.Canvas.Pen.Color:=clRed
//        else bmp.Canvas.Pen.Color:=clBlack;
    bmp.Canvas.Pen.Color:=clBlack;
    bmp.Canvas.Brush.Color:=pan[Num].Col;
    bmp.Canvas.Rectangle(pan[Num].x - PalProp.mW05, y1, pan[Num].x + PalProp.mW05, y2);

    x2:=x2 - x1;
    y2:=PalProp.mH+2;
    BitBlt(pb.Canvas.Handle, x1, y1, x2, y2, bmp.Canvas.Handle, x1, y1, SRCCOPY);
end;
//---------------------------------------------------------------------------------------------
procedure TForm2.RepaintAllPan;
var i:integer;
begin
    bmp.Canvas.Pen.Color:=clBtnFace;
    bmp.Canvas.Brush.Color:=clBtnFace;
    bmp.Canvas.Rectangle(0,
                         PalProp.mT + 1,
                         pb.Width,
                         PalProp.mT + PalProp.mW + 1);
    for i:=1 to CountPan do begin
//        if i = CurrentPan
//            then bmp.Canvas.Pen.Color:=clRed
//            else bmp.Canvas.Pen.Color:=clBlack;
        bmp.Canvas.Pen.Color:=clBlack;
        bmp.Canvas.Brush.Color:=pan[i].Col;
        bmp.Canvas.Rectangle(pan[i].x - PalProp.mW05,
                             PalProp.mT + 1,
                             pan[i].x + PalProp.mW05,
                             PalProp.mT + PalProp.mW + 1);
    end;
    BitBlt(pb.Canvas.Handle,
           0,PalProp.mT + 1,
           pb.Width, PalProp.mH,
           bmp.Canvas.Handle,
           0, PalProp.mT + 1, SRCCOPY);
end;
//----------------------------------------------------------------------------------------------------------------------------------------------------------
procedure TForm2.RepaintPalitra(Num1, Num2:integer); // перерисовка
var i, j, a, b:integer;
begin
    if Num1 < 1 then Num1:=1;  // проверка выхода за пределы (они есть в Panels_onDblClick)
    if Num2 > CountPan then Num2:=CountPan;

    b:=pan[Num1].x;
    for i:=Num1 to Num2-1 do begin
        a:=pan[i+1].x - pan[i].x;
        for j:=0 to a do begin
            bmp.Canvas.Pen.Color:=ColorChange(pan[i].Col, pan[i+1].Col, a, j);
            bmp.Canvas.MoveTo(b+j, 0);
            bmp.Canvas.LineTo(b+j, PalProp.mT);
        end;
        b:=b+a;
    end;
    BitBlt(pb.Canvas.Handle,
           pan[Num1].x, 0,
           pan[Num2].x - pan[Num1].x, PalProp.mT,
           bmp.Canvas.Handle,
           pan[Num1].x, 0, SRCCOPY);
    if not Form1.bFirstLoad then SaveChangeToMainForm;
end;
//---------------------------------------------------------------------------------------------
procedure TForm2.SaveChangeToMainForm;
var i:integer;
begin
    for i:=0 to 511 do
        Form1.pal[i]:=bmp.Canvas.Pixels[i, 1]; // заганяем новую палитру
    Form1.DrawFromArray(Form1.FractArr, Rect(0, 0, Form1.pb.Width - 1, Form1.pb.Height - 1), Form1.Bmp);
    Form1.pbPaint(self);
end;
//---------------------------------------------------------------------------------------------
procedure TForm2.RandomPalitra;
var i, j, k:integer;
    r:real;
begin
    if cb1.Checked
        then begin
            Randomize;
            CountPan:=2;
            pan[1].x:=0;
            pan[1].col:=RGB(Random(256), Random(256), Random(256));
            pan[2].x:=pb.Width;
            pan[2].col:=pan[1].col;

            k:=Random(10) + 3;
            r:=pb.Width / (k + 1);
            CreateAndSortPanel(9, false);
            for i:=1 to k do begin
                j:=Round(i*r);
                if Random(2) = 1 then CreateAndSortPanel(j - PalProp.mW - 1, false);
                CreateAndSortPanel(j, false, ColorArray[Random(NumbColor - 1)]);
                if Random(2) = 1 then CreateAndSortPanel(j + PalProp.mW + 1, false);
            end;
            CreateAndSortPanel(pb.Width - PalProp.mW - 1, false);
            RepaintAllPan;
            RepaintPalitra(1, CountPan);
    end
    else begin
        if Form1.smnPerelyv.Checked then Form1.smnPerelyvClick(Self);
        RedCount:=Random(9)+1;
        GreenCount:=Random(9)+1;
        BlueCount:=Random(9)+1;
        RedPos:=Random(511)+1;
        GreenPos:=Random(511)+1;
        BluePos:=Random(511)+1;
        bMoveR:=Random(2)=1;
        bMoveG:=Random(2)=1;
        bMoveB:=Random(2)=1;
        ChangeParam;
    end;
end;
//---------------------------------------------------------------------------------------------
function ColorChange(col1, col2: TColor; R, i: real): TColor;
var cr, cg, cb:real;
    cr1, cg1, cb1:byte;
    cr2, cg2, cb2:byte;
    dcr, dcg, dcb:byte;
begin
    cr1:=GetRvalue(col1);   cr2:=GetRvalue(col2);
    cg1:=GetGvalue(col1);   cg2:=GetGvalue(col2);
    cb1:=GetBvalue(col1);   cb2:=GetBvalue(col2);
    dcr:=abs(cr1 - cr2);
    dcg:=abs(cg1 - cg2);
    dcb:=abs(cb1 - cb2);
    if cr1 <> cr2
        then begin
            if cr1 < cr2 then cr:=cr1 + dcr*i/R;
            if cr1 > cr2 then cr:=cr1 - dcr*i/R;
        end
        else cr:=cr1;
    if cg1 <> cg2
        then begin
            if cg1 < cg2 then cg:=cg1 + dcg*i/R;
            if cg1 > cg2 then cg:=cg1 - dcg*i/R;
        end
        else cg:=cg1;
    if cb1 <> cb2
        then begin
            if cb1 < cb2 then cb:=cb1 + dcb*i/R;
            if cb1 > cb2 then cb:=cb1 - dcb*i/R;
        end
        else cb:=cb1;
    Result:=RGB(Round(cr), Round(cg), Round(cb));
end;
Выложил я только 1/10 часть кода, которая отвечает за формирование рендомной палитры, по которой потом отрисуется фарктал в красивых, пестрых красках. Вчера он мне понадобился. Я хотел сделать то же но на Java. Я хотел приделать эту же палитру к своему недавнему минипроектику "Рисуем фракталы на Java".

Что я хотел донести ребятам - так это те эмоции, которые я получал когда вчера портировал этот код на Java, а потом отлаживал его. На все про все 2 часа времени. Хотя это могло занять 10 минут, если бы код был поддерживаем. Я глядя в код, не мог сразу понять что он делает. Не мог вспомнить что я хранил в переменных с хитромудрным названием. Я знал лишь только, что он работает и работает так как надо. Так же я знал, что написать такой же с нуля у меня займет существенно больше времени, чем портирование существующего. И я взялся за рефакторинг.

Первым делом я написал некоторое подобие тестов и сделал механическое превращение (портирование) кода Pascal в Java. Работа не сложная, но требует внимания, ведь чем больше я ошибок тут сделаю, тем больше потом времени потрачу на отладку. 30 минут и код компилировался в Idea. Первая зеленая полоса! Я даже на радостях закоммитился, хотя понимал, что ничего оно не работает.

Потом был небольшой тестовый класс, который генерит палитру, рисует ее в bmp и сохраняет в BMP файл. А я уже чекаю результат. Конечно же он рисовал не то, что требовалось. Иногда случается, что код работает сразу, но я рад что это было не так. Пришлось его отлаживать.

Еще пол часа внимательного рефакторинга, и постепенного разтуманивания прдназначения методов и переменных. Потом полировка и тюнинг под новые нужны. Тоже пол часа. Итого я получил вот это
public class RandomPalette implements Palette {

    class Marker {
        int x;
        int color;

        public Marker(int x, int color) {
            this.x = x;
            this.color = color;
        }
    }

    private static final int MW = 8; // ширина маркера
    private static final int[] colorArray = new int[]{
            0xFFFFFF, 0x00FFFF, 0xFF00FF, 0xFFFF00, 0x0000FF, 0xFF0000, 0x00FF00,
            0xC0FFFF, 0xFFC0FF, 0xFFFFC0, 0xC0C0FF, 0xFFC0C0, 0xC0FFC0, 0xC000FF,
            0x00C0FF, 0x00FFC0, 0xC0FF00, 0xFFC000, 0xFF00C0};

    private List<Marker> markers = new LinkedList<Marker>();
    private int[] palette;

    public RandomPalette(int size) {
        palette = new int[size];

        int color = getRandomColor();
        markers.add(new Marker(0, color));
        markers.add(new Marker(size, color));

        int count = random(size / (MW * 3)) + 3;
        double r = size / (count + 1);

        addBlackMarker(MW + 1);
        for (int i = 1; i <= count; i++) {
            int j = (int) (i * r);
            if (yesOrNo()) {
                addBlackMarker(j - MW - 1);
            }
            addMarker(j, getRandomColor());
            if (yesOrNo()) {
                addBlackMarker(j + MW + 1);
            }
        }
        addBlackMarker(size - MW - 1);

        calculatePalette();
    }

    private int getRandomColor() {
        return colorArray[random(colorArray.length)];
    }

    private boolean yesOrNo() {
        return random(2) == 1;
    }

    private void addBlackMarker(int x) {
        addMarker(x, 0);
    }

    private void calculatePalette() {
        int x = markers.get(0).x;
        for (int i = 0; i < markers.size() - 1; i++) {
            int length = markers.get(i + 1).x - markers.get(i).x;
            for (int dx = 0; dx < length; dx++) {
                palette[x + dx] = colorChange(markers.get(i).color, markers.get(i + 1).color, length, dx);
            }
            x = x + length;
        }
        palette[0] = 0;
    }

    private int colorChange(int from, int to, double len, double x) {
        double red = change(getR(from), getR(to), len, x);
        double green = change(getG(from), getG(to), len, x);
        double blue = change(getB(from), getB(to), len, x);

        return rgb((int) red, (int) green, (int) blue);
    }

    private double change(double from, double to, double len, double x) {
        if (from == to) {
            return from;
        }

        double delta = Math.abs(from - to) * x / len;

        if (from < to) {
            return from + delta;
        } else {
            return from - delta;
        }
    }

    private int getR(int col) {
        return (col & 0x0000FF);
    }

    private int getG(int col) {
        return (col & 0x00FF00) >>> 8;
    }

    private int getB(int col) {
        return (col & 0xFF0000) >>> 16;
    }

    private int rgb(int r, int g, int b) {
        return (r) | (g << 8) | (b << 16);
    }

    private void addMarker(int x, int color) {
        // определяем после которого маркера будет создаваемый
        int index;
        for (index = 0; index < markers.size(); index++) {
            if ((markers.get(index).x < x) && (x < markers.get(index + 1).x)) {
                break;
            }
        }

        // если после последнего то выходим
        if (index == markers.size()) {
            return;
        }

        // если маркер некуда втиснуть между двумя ближайшими то выходим
        if (markers.get(index + 1).x - markers.get(index).x < MW + 2) {
            return;
        }

        // очень близко ставить маркер возле соседнего нельзя
        if ((markers.get(index).x + MW + 1 > x) || (markers.get(index + 1).x - MW - 1 < x)) {
            return;
        }

        markers.add(index + 1, new Marker(x, color));
    }

    private int random(int n) {
        return new Random().nextInt(n);
    }

    @Override
    public int getColor(int r) {
        return palette[r];
    }

    @Override
    public int getSize() {
        return palette.length;
    }
}
И я более чем уверен, что глядя на этот код спустя некоторое время я буду считать его говнокодом. Если этого не случится – значит. Не даром, если заметил, я этот код назвал «это». «Это» только оно сейчас работает… Вообще считаю, стоит относиться к своему коду не как к произведению искусства, а как к продукту жизнедеятельности, тому что могло быть чуточку лучше, раз уж появилось на этот свет.

Что есть code review? Это всего лишь озвучивания той дельты, которая существует у двух специалистов в их опыте. Если мне больше нечего сказать напарнику по поводу его кода, это вовсе не значит что его код идеален. Я уже завтра могу прочитать новую статью и пережив ее понять, что код не такой уж и совершенный, как казалось вчера. Кроме того ревью может сделать более сеньорный специалист и рассказать о том, на какие грабли он наступал, на какие еще не наступали я с моим напарником. И то, что этому сеньорному специалисту в какой-то момент больше нечего будет сказать нам - значит лишь одно - мы написали такой код, который хотел бы написать он сам. А завтра все поменяется.

По этой причине код стоит пересматривать регулярно. Если в какой-то момент про код забыли - он устаревает со скоростью света. Но если в код вносились изменения сегодня - велика вероятность, что в него будут вноситься изменения завтра. Такой код после внесения серии изменений стоит пересматривать его коллективно.

Ну вот как бы и все, что хотел запечатлеть в памяти. Надеюсь кому-то пригодится...

понедельник, 21 января 2013 г.

Мистический Scott Dunbar и не только

И еще одно не в тему, но очень красиво!

Как прочистить узкую трубку изнутри

Вода у нас привозная. С крана в Киеве пить воду нельзя. То, что привозят, надеюсь не с крана :) - говорят скважена за городом. Но не об этом сейчас. Сейчас о помпе, которая качает воду из бултля 20 литрового. У помпы внутрь бутля спускается трубка. И вот некоторое время назад я стал замечать, что трубка эта зеленеет, как травка. Хлорофилл! - подумал я и записал в туду почистить. Только вот чем?



Отобрал у ребенка кисточку. Ватный диск. Нитка. Ножницы. Через некоторое время был готов поршенек, которым все и почистил. Нефиг растениям расти в моей помпе!

Далее пару наглядных скриншотов
 Ниткой стоит крепко намотать,чтобы не отвалилось. Усилия будут еще те.
 Обрезать по размеру - в трубку не засунешь.
 Остаток все же попробовать просунуть
 Туда-сюда-туда-сюда
Ии вот результат
 Может пригодится кому...
Валерка, спасибо, что показал мне это видео
Передай дальше...

Рисуем фракталы на Java

Для одной задумки понадобилась библиотека, для работы с bmp изображениями. Найти ее было не сложно. Сейчас чего-только не написали уже для Java. Спасибо Nayuki Minase за то, что он потрудился и написал библиотеку для работы с bmp.

И вот я конечно же ищу сэмплы, чтобы разобраться как сие диво работает. И что вижу? Одна из демок рисует Множество Мандельброта. Ну вот я и завис, уж простите. В прошлом, когда еще кодил на Delphi я тоже завис - на по-дольше. В результате родилась такой вот фрактальный браузер. Сегодня я решил помянуть это и написать свою версию рисовалки фракталов на основе библиотеки работы с bmp рисунками.

Вот один из рисунков в большом разрешении (клик по рисунку увеличит его, но осторожно - там 20 мег)...
А теперь о реализации (исходник можно скачать тут, осторожно внутри исходники библиотеки по работе с BMP, она распостраняется по MIT лицензии).

Итак интерфейсик Фрактала
public interface Fractal {
    Position getZoom();

    int getFunction(double x, double y, int iterations);
}
Вот Мандельброт
public class Mandelbrot implements Fractal {
    private Position mandelbrot = new Position(-1.9, 0.5, -1.2, 1.2);

    @Override
    public Position getZoom() {
        return mandelbrot.zoom(40).move(-1.25, 2).zoom(5);
    }

    @Override
    public int getFunction(double a, double b, int iterations) {
        double r = 0;
        double x = 0;
        double y = 0;
        int color = iterations;
        while (color > 0 && r < 4) {
            double x2 = x * x;
            double y2 = y * y;
            double xy = x * y;
            x = x2 - y2 + a;
            y = 2 * xy + b;
            r = x2 + y2;
            color--;
        }
        return color;
    }
}
Вот Джулия
public class Julia implements Fractal {

    @Override
    public Position getZoom() {
        return new Position(-1.59, 1.527, -1.558, 1.558);
    }

    @Override
    public int getFunction(double x0, double y0, int iterations) {
        double r = 0;
        double a = -0.55;
        double b = -0.55;
        double x = x0;
        double y = y0;
        int color = iterations;
        while (color > 0 && r < 4) {
            double x2 = x * x;
            double y2 = y * y;
            double xy = x * y;
            x = x2 - y2 + a;
            y = 2 * xy + b;
            r = x2 + y2;
            color--;
        }
        return color;
    }
}
Класс Position я сделал для удобной работы с координатами (зум, мув). Простите за французский в названии полей/переменных.
import p79068.bmpio.Rgb888Image;

public class Position {
    private double bxMin;
    private double bxMax;
    private double byMin;
    private double byMax;

    Position(double bxMin, double bxMax, double byMin, double byMax) {
        this.bxMin = bxMin;
        this.bxMax = bxMax;
        this.byMin = byMin;
        this.byMax = byMax;
    }

    public Position zoom(double zoom) {
        double lx = (bxMax - bxMin)*(zoom-1)/(2*zoom);
        double ly = (byMax - byMin)*(zoom-1)/(2*zoom);
        double xMin = bxMin + lx;
        double xMax = bxMax - lx;
        double yMin = byMin + ly;
        double yMax = byMax - ly;

        return new Position(xMin, xMax, yMin, yMax);
    }

    public Position move(double dx, double dy) {
        double xMin = bxMin + (bxMax - bxMin)*dx;
        double xMax = bxMax + (bxMax - bxMin)*dx;
        double yMin = byMin + (byMax - byMin)*dy;
        double yMax = byMax + (byMax - byMin)*dy;

        return new Position(xMin, xMax, yMin, yMax);
    }

    public Position resize(double width, double height) {
        if (width/height > 1) {
            double dx = (bxMax - bxMin)*(width/height - 1)/2;
            double xMin = bxMin - dx;
            double xMax = bxMax + dx;
            double yMin = byMin;
            double yMax = byMax;
            return new Position(xMin, xMax, yMin, yMax);
        } if (width/height < 1) {
            double dy = (byMax - byMin)*(height/width - 1)/2;
            double xMin = bxMin;
            double xMax = bxMax;
            double yMin = byMin - dy;
            double yMax = byMax + dy;
            return new Position(xMin, xMax, yMin, yMax);
        } else {
            return this;
        }
    }

    public double getXMin() {
        return bxMin;
    }

    public double getXMax() {
        return bxMax;
    }

    public double getYMin() {
        return byMin;
    }

    public double getYMax() {
        return byMax;
    }

    public String toString() {
        return String.format("x=[%s...%s]\ny=[%s...%s]",
                bxMin, bxMax, byMin, byMax);
    }
}
Фрактальное изображение, основано на интерфейсе из библиотеки. Суть в том, что метод getRgb888Pixel() будет дергаться либой по каждому пикселю изобраения шириной и высотой getWidth() на getHeight()..
import p79068.bmpio.Rgb888Image;

public class FractalImage implements Rgb888Image {

    private int width;
    private int height;
    private Fractal fractal;
    private Palette palette;
    private Position zoom;

    public FractalImage(int width, int height, Fractal fractal, Palette palette) {
        this.width = width;
        this.height = height;
        this.fractal = fractal;        
        this.palette = palette;
        zoom = fractal.getZoom().resize(width, height);
    }

    @Override
    public int getWidth() {
        return width;
    }

    @Override
    public int getHeight() {
        return height;
    }

    @Override
    public int getRgb888Pixel(int x, int y) {
        double x1 = zoom.getXMin() + (x + 0.5) / width * (zoom.getXMax() - zoom.getXMin());
        double y1 = zoom.getYMax() - (y + 0.5) / height * (zoom.getYMax() - zoom.getYMin());
        int r = fractal.getFunction(x1, y1, palette.getSize() - 1);
        return palette.getColor(r);
    }

    public String getFractalName() {
        return fractal.getClass().getSimpleName();
    }
}
Он зависим не только от фрактала, но и от палитры
public interface Palette {
    int getColor(int index);

    int getSize();
}
Реализация которой в чернобелом варианте будет такой. Всего в палитре этой 256 цветов - 8 бит на канал (red=green=blue).
public class BlackAndWhite256Palette implements Palette {
    @Override
    public int getColor(int r) {
        return (r) | (r << 8) | (r << 16);
    }

    @Override
    public int getSize() {
        return 256;
    }
}
А вот палитра цветная рендомная, она рисует более интересные фракталы.

Код причесал как мог - он был портирован из Delphi. Я его писал лет 15 назад - можешь представить, что там было...
public class RandomPalette implements Palette {
    class Marker {
        int x;
        int color;

        public Marker(int x, int color) {
            this.x = x;
            this.color = color;
        }
    }

    private static final int MW = 8; // ширина маркера
    private static final int[] colorArray = new int[]{
            0xFFFFFF, 0x00FFFF, 0xFF00FF, 0xFFFF00, 0x0000FF, 0xFF0000, 0x00FF00,
            0xC0FFFF, 0xFFC0FF, 0xFFFFC0, 0xC0C0FF, 0xFFC0C0, 0xC0FFC0, 0xC000FF,
            0x00C0FF, 0x00FFC0, 0xC0FF00, 0xFFC000, 0xFF00C0};

    private List<Marker> markers = new LinkedList<Marker>();
    private int[] palette;

    public RandomPalette(int size) {
        palette = new int[size];

        int color = getRandomColor();
        markers.add(new Marker(0, color));
        markers.add(new Marker(size, color));

        int count = random(size / (MW * 3)) + 3;
        double r = size / (count + 1);

        addBlackMarker(MW + 1);
        for (int i = 1; i <= count; i++) {
            int j = (int) (i * r);
            if (yesOrNo()) {
                addBlackMarker(j - MW - 1);
            }
            addMarker(j, getRandomColor());
            if (yesOrNo()) {
                addBlackMarker(j + MW + 1);
            }
        }
        addBlackMarker(size - MW - 1);

        calculatePalette();
    }

    private int getRandomColor() {
        return colorArray[random(colorArray.length)];
    }

    private boolean yesOrNo() {
        return random(2) == 1;
    }

    private void addBlackMarker(int x) {
        addMarker(x, 0);
    }

    private void calculatePalette() {
        int x = markers.get(0).x;
        for (int i = 0; i < markers.size() - 1; i++) {
            int length = markers.get(i + 1).x - markers.get(i).x;
            for (int dx = 0; dx < length; dx++) {
                palette[x + dx] = colorChange(markers.get(i).color, markers.get(i + 1).color, length, dx);
            }
            x = x + length;
        }
        palette[0] = 0;
    }

    private int colorChange(int from, int to, double len, double x) {
        double red = change(getR(from), getR(to), len, x);
        double green = change(getG(from), getG(to), len, x);
        double blue = change(getB(from), getB(to), len, x);

        return rgb((int) red, (int) green, (int) blue);
    }

    private double change(double from, double to, double len, double x) {
        if (from == to) {
            return from;
        }

        double delta = Math.abs(from - to) * x / len;

        if (from < to) {
            return from + delta;
        } else {
            return from - delta;
        }
    }

    private int getR(int col) {
        return (col & 0x0000FF);
    }

    private int getG(int col) {
        return (col & 0x00FF00) >>> 8;
    }

    private int getB(int col) {
        return (col & 0xFF0000) >>> 16;
    }

    private int rgb(int r, int g, int b) {
        return (r) | (g << 8) | (b << 16);
    }

    private void addMarker(int x, int color) {
        // определяем после которого маркера будет создаваемый
        int index;
        for (index = 0; index < markers.size(); index++) {
            if ((markers.get(index).x < x) && (x < markers.get(index + 1).x)) {
                break;
            }
        }

        // если после последнего то выходим
        if (index == markers.size()) {
            return;
        }

        // если маркер некуда втиснуть между двумя ближайшими то выходим
        if (markers.get(index + 1).x - markers.get(index).x < MW + 2) {
            return;
        }

        // очень близко ставить маркер возле соседнего нельзя
        if ((markers.get(index).x + MW + 1 > x) || (markers.get(index + 1).x - MW - 1 < x)) {
            return;
        }

        markers.add(index + 1, new Marker(x, color));
    }

    private int random(int n) {
        return new Random().nextInt(n);
    }

    @Override
    public int getColor(int r) {
        return palette[r];
    }

    @Override
    public int getSize() {
        return palette.length;
    }
}
Небольшой консольный прогресбарчик-декоратор для того, чтобы я знал как долго будет рисоваться фраклат размером 20000x20000 пикселей. Я его прооптимизировал немного, чтобы он не занимался делением каждый пиксель.
public class Progress implements Rgb888Image {
    private long square;
    private long count;
    private long iteration;
    private long next;
    private FractalImage image;

    public Progress(FractalImage image) {
        this.image = image;
        this.square = getWidth()*getHeight();
        this.next = square/100;
    }

    @Override
    public int getWidth() {
        return image.getWidth();
    }

    @Override
    public int getHeight() {
        return image.getHeight();
    }

    @Override
    public int getRgb888Pixel(int x, int y) {
        calculateProgress();

        return image.getRgb888Pixel(x, y);
    }

    private void calculateProgress() {
        iteration++;
        if (iteration == next) {
            count = count + iteration;
            iteration = 0;
            int progress = (int) ((double)count*100 / square);
            System.out.println(progress + "%");
        }
    }

    public String getFractalName() {
        return image.getFractalName();
    }
}
Ну и Main метод
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import p79068.bmpio.BmpImage;
import p79068.bmpio.BmpWriter;

public final class FractalDemo {
 
    public static void main(String[] args) throws IOException {
        draw(new Mandelbrot());
        draw(new Julia());
    }

    private static void draw(Fractal fractal) throws IOException {
        BmpImage bmp = new BmpImage();
        Palette palette = new BlackAndWhite256Palette();
        Progress image = new Progress(new FractalImage(1920, 1080, fractal, palette));
        bmp.image = image;
        File file = new File(image.getFractalName() + ".bmp");
        FileOutputStream out = new FileOutputStream(file);
        BmpWriter.write(out, bmp);
        out.close();
    }
}
Уверен кто-то это полюбит так же как и я...

воскресенье, 20 января 2013 г.

Approval testing: Как переименовать часть имени файла с помощью BAT

Возникла задача. Использую Approval testing в тестировании. Очень удобная тула. Кому интересно - смотрим тут.

В процессе работы с этим фреймворком, когда тесты проходят - создаются файлы с именем <testclass>.<testname>.approve.txt - эти файлы содержат строку, которую мы в конце теста скормили фреймворку, чтобы он ее заапрувил. На самом деле аппрувлю ее я, и когда я аппрувлю строку - она попадает в *.approve.txt файл. Это такой себе аналог assertEqials в jUnit, только зачение expected сохраняется в отдельный файл после того, как я его заапрувлю.

Но в момент когда тест слетает, рядом создается другой файл *.received.txt и в него записывается информация о том, что реально пришло от тестируемого модуля. Мне же после отработки тестов запускается ряд (для каждого слетевшего теста) окон Tortoise diff и могу сравнить их и выбрать правильный. Такой способ принятия решений более удобный чем орудовать со строками в assertEquals.

Пример использования такой - я вдруг решил что хочу чтобы не "?" выводились, а "." причем от этого зависит пяток тестов. Что делать? Ручками править все assertEquals? Неее...

Переписывать мне ассерт пришлось бы в тесте очень долго, а тут я кликаю одну кнопку и файл approve обновляется.

Беда, когда я что-то меняют и слетает пару десятков тестов. Тогда не очень удобно 20 раз кликать на кнопку "сохранить", особенно если я знаю, что новое изменение - ок. Итого у меня есть пары файлов *.approve.txt и *.received.txt и я хочу удалить все старые аппрувы и заменить их новыми received.
Мне в этом поможет простой bat file. Вернее не простой, а такой
@echo off
setlocal enableDelayedExpansion
for %%F in (*.received.txt) do (
  set "name=%%F"
  move /Y "%%F" "!name:~0,-13!.approved.txt"
)
Расшифрую немного его. Мы проходимся по всем файлам *.received.txt, зносим ихние имена по очереди в переменную name. После чего выполняем команду move /Y, что значит мы хотим переместить с заменой файл с исходным именем %%F на файл сформированный в "!name:~0,-13!.approved.txt".

Последняя команда очень интеерсна - ее формат s:~a,b, где s - строка, a и b - позиции символов в строке. В результате мы получим подстроку начинающуюся с символа под номером а (нумерация начинается с 0) и длинной b. Но это, если a и b имеют положительные значения. Если же указать только s:~-a, то команда выкусит только a последних символов. Если же s:~a,-b, то выкусит подстроку начиная с a-того и до конца за вычетом b последних символов.

Пару примеров для понимания
D:\>set var=abcdefghijklmnopqrstuvwxyz0123456789.someext

D:\>echo %var:~0,5%
abcde

D:\>echo %var:~1,5%
bcdef

D:\>echo %var:~1,8%
bcdefghi

D:\>echo %var:~3,8%
defghijk

D:\>echo %var:~-4%
eext

D:\>echo %var:~-9%
9.someext

D:\>echo %var:~0,-1%
abcdefghijklmnopqrstuvwxyz0123456789.someex

D:\>echo %var:~0,-7%
abcdefghijklmnopqrstuvwxyz0123456789.

D:\>echo %var:~7,-7%
hijklmnopqrstuvwxyz0123456789.
Вроде как все...

#10 Пишем Web проект на Java в Eclipse для Tomcat. Билдим Ant. Проверяем Hudson. Тестим jUnit + EasyMock + jWebUnit. Коммитим в Svn.

Привет! Линк на прошлую серию тут. А сегодня история, которую мы реализуем, звучит так: "Появляется возможность работать с несколькими экзаменами - на странице выбора экзаменов теперь отображается список." Я ее разбил на три составляющие, потому что по моей предварительной оценке она занимает слишком много времени. Все что сложнее логики логинки более чем в 4 раза - все стоит разбивать на подзадачи.

Подзадачи я вижу такие: 
1. Теперь о юзере может храниться более сложная информация а не просто пароль/сдал/не сдал. Пускай это будет тот же properties файл, но немного другого формата.
2. Изменение части контроллера - надо научить его понимать какой экзамен пользователь выбрал и от этого подгружать необходимую xml с экзаменом. Тут же решение задачи, что название экзамена != названию xml файла с экзаменом.
3. Получение списка всех экзаменов по существующим xml файлам. Тут же подгрузка только тех экзаменов, которые студент еще не прошел.

Начнем с первой.

Изменим внутреннюю структуру файла так, чтобы была возможность содержать дополнительную информацию о пользователе, но так, чтобы ничего кроме UserService не изменилось.

Сейчас в файле с описанием пользователей информация хранится в таком формате <имя пользователя>=<пароль>|"FAILED"|"PASSED"


Я хочу поменять формат таким образом, что одному пользователю ставится в соответствие пару свойств: пароль, какие экзамены сдал, какие экзамены не сдал.


Но для начала запустим все тесты:


Все зелено? - продолжаем...

Для начала я хочу удалить старые ненужные классы. Даже если они мне очень нравятся, но сейчас в коде не используются - их стоит удалить. Я всегда могу достать их из истории SVN, но вероятнее всего я этого никогда не сделаю.


Так же я хочу превратить функциональный тест UserServiceFileTest в контрактный, завязанный только на интерфейс UserService. Сейчас в этом классе содержится логика тестирования интерфейса UserService и логика подготовки тестовых данных.


Разделим их - логику тестирования интерфейса - в абстрактный класс, а логику подготовки тестовых данных - в наследника этого абстрактного класса.

Для разделения, выделим все тесты которые в своем теле используют поле users и попробуем применить к ним рефакторинг PullUp.



У нас не получилось это сделать потому, что у исходного класса нет родителя, куда бы IDE мог бы переместить выделенные методы. Создадим этого родителя.





А после снова выделим все тесты, использующие поле users, из наследника новоиспеченного UserServiceTest и применим к ним рефакторинг PullUp.


Выделение нам особо не помогло, а потому выделим все методы/константы/поля и сними выделение с тех, которые должны остаться в наследнике - тех, которые отвечают за подготовку и формат файла с пользователями. И нажмем Finish.


Потом еще раз Next.


Подождем, это займет некоторое время


Ну вот, как всегда что-то не получилось, но мы не расстраиваемся и жмем Next


Проглядываем что произошло в результате рефакторинга (может чего пошло не по плану?) и ждем Finish


В результате содержимое исходного класса разделится по двум.

Конкретика относительно структуры файла останется в наследнике


А все, что зависит от интерфейса UserService - уйдет в абстрактного родителя. + Родитель компилировать не будет, потому что там нет никакого упоминания о users поле.


Исправим это, добавив в абстрактного родителя новый абстрактный метод getUsers конфигурируемый списком пользователей и их паролями. Ну и родителя которого мы называем абстрактным - сделаем наконец-то абстрактным.


Это нам слабо помогло избавиться от ошибок компиляции, но минутку терпения. Добавим теперь метод getDefaultUsers и заменим во всем классе users на вызов этого метода, но ВНИМАНИЕ!, только в тех тестах, которые users используют один раз. В тех тестах, которые используют более одного раза users надо добавить одноименную локальную переменную, значение которой получить вызвав тот же метод getDefaultUsers.


Замечу, что default данные мы украли из наследника


Как результат большинство ошибок компиляции устранено - не компилируются пока только всего два теста, использующие метод, которые остался в наследнике.


Добавим его тут как абстрактный.


С абстрактным родителем можно закончить - в результате у нас получился тест, который зависит только от UserService - его как мы уже знаем называют контрактным.


Осталась разобраться с наследником-конкретикой. В нем как минимум две ошибки компиляции.


IDE нам поможет. Вначале нам надо модифицировать метод assertThatUserPassed так, чтобы он стал реализацией одноименного абстрактного метода из родителя.

Первый шаг - убрать ключевое слово static.


Следующий шаг - сделать методы похожими по типу прокидываемых исключительных ситуаций.


Но то, как это сделала IDE мне не совсем нравится - я еще больше абстрагируюсь, указав что метод класса наследника может прокидывать любые исключения. Абстрактному классу - более общие зависимости!


Теперь попросил в наследнике дореализовать недостающие методы.


Итак почти все готово, осталось только доделать getUsers метод


Раньше эту роль выполняли initUserService и makeUsersFile методы вместе. Перенесем их реализацию в getUsers


Теперь надо снова разобраться с прокидываемыми исключениями.



После, как и в прошлый раз - обобщим в абстрактном родителе все до прокидывания Exception и попросим абсолютно все тесты так же прокидывать Exception.


В ходе добавления я заметил, что метод assertExceptionWithMessage кажется не на своем месте, т.е. ему тут не место. Перенесем его в какой-то TestUtils. Для этого создадим пустой класс в пакете test/utils.


Мы могли бы вручную его перенести, но это потребует много ручной работы, а мне лень ее делать. Потому что за ручным переносом:


последовало бы ручное изменение всех вызовов с учетом нового местоположения. Фуу.. А если ошибку какую-то внесу? Я и так долго тесты не запускал...

Итак попросим IDE cделать перенос за нас. Поставим курсор ввода на название метода в исходном классе и выполним рефакторинг Move (горячая клавиша Alt-Shift-V или меню Refactoring->Move...).

И получим вполне заслуженное сообщение - IDE может многое но не все. Оказывается IDE не нашла класса, который мог бы стать контейнером для нашего метода.


Что мы можем сделать, кроме как сдаться и сделать все руками? Идея пришла сразу - надо сделать его статическим и публичным.


А после повторить Рефакторинг Move. Теперь IDE нам предлагает варианты - укажем класс, куда хотим перенести метод и посмотрим от чего IDE нас избавило





IDE даже в разделе import порядок навела. Супер! Вот, что мне было делать лень. Жмем OK и продолжаем


Следующая трудность не такая простая.


Дело в том, что мы прокидывали везде Exception, а для тестирования исключительных ситуаций используем анонимную реализацию стандартного Java интерфейса Runnable - в его сигнатуре не указано throws Exception и поменять мы его не можем.


Но мы можем написать свой! Сделаем это в том же TestUtils классе.


Теперь для каждого теста достаточно будет сделать так


Но вручную я сделаю это всего раз (ленив, помните?) - все остальное я попрошу сделать IDE используя замену по файлу.


Риcунок кишит пометками, потому я его поясню. Дело в том, что в разработке пользоваться мышкой - это все равно что спринтеру привязать к ногам 10кг свинцовые гири и попросить пробежать дистанцию на время. По этой причине везже где только можно я пользуюсь горячими главишами. В принципе мышка нужна только для того, чтобы добраться в соответствующее меню и подглядеть какая там стоит клавиша, а после юзать только ее. Replace All по всему файлу я использую довольно часто, а потому давно делаю это без участия мышки.

Всего в этом процессе 6 этапов:
1) Выделяем строчку, которую только что исправили ручками. Но не мышкой выделяем а, поставив курсор в начало строки нажатием Home с последующтми нажатием Shift-End.
2) Копируем строчку в буфер обмена нажатием Ctrl-C (или Ctril-Ins кто как привык)
3) Перемещаемся к следующему методу, содержащему заменяемую строчку нажатием Ctrl-Shift-Down, а потом к самой строчке (Down, Down, Down)
4) Выделяем и это строчку нажатием Home, Shift-End.
5) Вызываем диалог Поиска/Замены нажатием Ctrl-F
6) Выделенный текст автоматически скопируется в поле Find (собственно за этим его и выделяли на шаге 4) а мы нажмем Tab, чтобы проскочить это поле
7) Вставим ранее скопированную строчку-заменитель нажатием Ctrl-V (или Shift-Ins кому как удобнее)
8) Нажмем на клавишу Replace All? но НЕ МЫШКОЙ! :) а подглянув, какая буква в надписи на кнопке подсвечена и нажав на нее с Alt кнопкой (в данном случае Alt-A)

Итого комбинация Home, Shift-End, Ctril-Ins, Ctrl-Shift-Down, Down, Down, Down, Home, Shift-End, Ctrl-F, Tab, Shift-Ins, Alt-A

Длиннющая! Но запоминать ее всю не надо, дело в том, что регулярно пользуясь горячими клавишами вскоре это войдет в привычку и ты не будешь задумываться о том, что нажать, а всего лишь будешь думать о том, что делать, а руки, в свою очередь, сами нажмут то, что надо. Если сделать горячие клавиши ежедневной привычкой, то вскоре окружающие будут удивляться как молниеносно ты работаешь - и это проверенный факт. У меня процесс замены с горячими клавишами занял 4 секунды. Если бы со мной кто-то сейчас сидел в паре, то вряд ли бы он понял, что я сделал - хотя бы потому, что диалоговое окно поиска/замены мелькнуло на секунду и сразу пропало. Кстати с мышкой тот же процесс занял 24 секунды.

Итак, мы отвлеклись. Если теперь взглянуть на проект в общем, то станет заметно, что один класс (причем в совсем другом пакете) не компилится. Это значит лишь одно - раньше я внес лишнюю зависимость между двумя классами, которой быть не должно. Изменения в одном пакете, должны приводить к поломкам только этого пакета, если только не меняется публичный интерфейс.

Вот она, вредная, зависимость


Можно исправить код и оставить зависимость, но я предпочитаю устранить ее. Вопрос в том, как?

Для начала закрою все public методы, сделав их видимыми только в пакете - именно это стало источником лишних связей.


Очень неудивительно, что это привело к поломке еще одного класса. Вредных зависимостей уже две, и, что примечательно, проблемы исходят из пакета с функциональными тестами.

Вот вторая бяка!


Ну да, для работы функциональных тестов, надо чтобы кто-то перед каждым тестом заполнял файл с пользователями исходными данными. Кто как не тест, который разбирается в дебрях UserServiceFileImpl.

Ошибка тут только одна - наличие функциональных тестов в проекте. Этим тестам всегда надо знать все и вся.

Кстати, наше превращение простого теста в контрактный не сделало его модульным. UserServiceFileTest по прежнему тестирует UserServiceFileImpl вместе с Properties классом и файловой системой. Функциональный тест, косящий под модульный - интересно звучит.


Запущу ка я его, чтобы проверить как прошло выделение контрактного теста.

Некоторые классы в проекте не компилируются, об чем сообщит мне IDE но я попрошу ее проигнорить это.



Зеленый? Это хорошо, но это намекает на то, что надо сделать commit. А потому я вредные зависимости на время оставлю в покое, то есть верну public на место и статический доступ заменю еще более некрасивым динамическим.


Фуу... Теперь видно невооруженным взглядом видно всю некрасивость этой зависимости.

Пока я разоблачением занимался, увидел, провтык в контрактном тесте. Дело в том, что нет связи между теми пользователями, которые передаются из абстрактного контрактного теста в наследник-конкретику и теми, которые будут сохранены в файл. Это станет заметно если убить константу data.


После этого, чтобы устранить ошибку компиляции нам придется связать данные, которые приходят из контрактного теста и метод подготовки тестового файла.


Но это опять поломает нашего вездесущего клиента


Выхода пока нет - передадим ему исходные данные (их я скопипастил из контрактного теста).


Теперь, пока весь проект компилится - я могу запустить все тесты.


Все зеленое? Сохраняемся. А то, что стоит сделать дальше - записано в TODO: устранить вредную зависимость. Вернемся к ней, когда будет настроение.



Итак, если память мне не изменяет (а она скорее всего изменяет - гляну я лучше в свой список задач), то мы хотели всего лишь изменить структуру файла с пользователями, чтобы можно было сохранять туда информацию о более чем одном экзамене.

Если разделить эту задачу на две:
1) изменение формата, но экзамен остается один
2) добавление возможности сохранять информацию о нескольких экзаменах для каждого юзера.
то мы сможем реализовать первую, без добавления новых тестов, т.к. задача по сути - рефакторинг, а вот вторая - это уже расширение функциональности - а это, в свою очередь, требует написания теста-требования перед изменением в коде (помнишь, договаривались? Утром требование, вечером код!)

Вперед! Вот исходная версия UserServiceFileImpl. Только что в ней я заметил одну маленькую недоработку, которую тут же пофиксил.



Тесты не повалились, а потому я закомичу эту правку.



Итак что этот класс делает? При вызове любого его метода класс грузит данные из файла в локальную переменную Properties, после работает с ней и в конце сохраняет обратно в файл. Свойством класса является только путь к файлу. Как-то это не по ООП. Класс в ООП работающий с данными должен инкапсулировать сами данные, а не ссылку на то место где они находятся.

Что если из UserServiceFile сделать singleton а UserFactory будет управлять его созданием. Загрузка из файла будет во время инстанциирования в конструкторе UserServiceFile, а сохранение данных в файл всякий раз, когда это понадобится. Неее...

А что, если мы разделим класс UserServiceFile на два класса - первый будет работать с данными о пользователях инкапсулированных вместе с методами загрузки в файл во втором классе. А еще я узнал, что Properties это реализация Map. И что, если теперь полем класса будет этот самый Map, и лишь на момент загрузки/сохранения будет известно что это Porperties?

А может попробовать наконец-то создать класс User, инкапсулирующий в себе все свойства пользователя, UserService будет хранить в себе Map этих User данных, а третий класс, допустим UsersSaver будет сохранять Map в файл любыми доступными средствами. UserService знает про User и UsersSaver. Не сложно ли? А попробуем!

Я создам новый тест для этих случаев, а так же новый набор рабочих классов. Но начнем с теста.

Для создания класса есть хорошая горячая клавиша: Alt-Shift-N,C ;)



Наполним его смыслом. Тут будем писать код так, как будь-то бы он уже существует. Компилятор будет ругаться, и мы попросим IDE создать все необходимое за нас.


Много ошибок! Создадим для начала интерфейс User



Продолжение следует…