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


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

суббота, 23 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #4

На этот раз воспользуемся простой фабрикой для создания фонарика с уже вставленными батарейками. Напомню в прошлый раз мы научились использовать setter для инъекции.

Скажу сразу исходники этого примера можно качнуть тут.

Чтобы не дать возможность никому создавать фонарик мы сделаем его конструктор package-защищенным, так же как и сам класс.

package factory.flashlight;

import factory.battery.Battery;

// Класс защищен потому что мы не хотим, чтобы кто-то из другого пакета
// вызывал его. Только фабрика, которая находится в том же пакете. 
class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    // Констурктор так же защищен. Береженого Бог бережет.  
    SomeFlashlight(Battery battery) {
        this.swithOn = false;
        this.battery = battery;
    }
    
    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();             
        }            
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }
}

Интерфейс фонарика без изменений.

package factory.flashlight;

public interface Flashlight {

    void swithOn();
    
    void swithOff();

    boolean isShines();

}

Появился новый класс - фабрика, которая устанавливает в новый фонарик батпрейку.

package factory.flashlight;

import factory.battery.BatteryFactory;

public class FlashLightFactory {
    
    // простая фабрика знает, какой фонарик и какую батарейку использовать
    // но возвращает интерфейс!
    public Flashlight getFlashlight() {
        return new SomeFlashlight(new BatteryFactory().getBattery());        
    }
    
}

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

Интерфейс батарейки без изменений.

package factory.battery;

public interface Battery {

    boolean getVoltage();

}

А вот с созданием батарейки я учудил так же как и с фонариком. Есть фабрика батареек и только она знает, какие батарейки выпускает.

package factory.battery;

public class BatteryFactory {
    
    // Заметь, везде где только можно возвращается интерфейс!
    public Battery getBattery() {
        return new ChinaBattery();        
    }
    
}

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

package factory.battery;

// класс скрыт в пакете
class ChinaBattery implements Battery {

    private int power = 5;     
    
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        
        return false;
    }

}

Посмотрим тесты:

package factory;
import static org.junit.Assert.*;

import org.junit.Test;

import factory.flashlight.FlashLightFactory;
import factory.flashlight.Flashlight;

public class TestBaterry {
            
    @Test
    public void testDischargeNewBattery() {                                
        Flashlight flashlight = new FlashLightFactory().getFlashlight();        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {
            flashlight.swithOff();                            
            flashlight.swithOn();                    
        }
        
        flashlight.swithOn();
        assertFalse(flashlight.isShines());        
    }
        
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {                        
        Flashlight flashlight = new FlashLightFactory().getFlashlight();    
        assertFalse(flashlight.isShines());
        
        for (int count = 0; count < 1000; count ++) {                    
            flashlight.swithOn();                    
        }
        
        assertTrue(flashlight.isShines());            
    }    

    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {                
        Flashlight flashlight = new FlashLightFactory().getFlashlight();    
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertTrue(flashlight.isShines());    
    }
    
}
Т.к. теперь нет возможности получить фонарик без батарейки и извлечь батарейку, то и тестов поменьше будет. Мы пользуемся фабрикой чтобы получить фонарик, от вызова к вызову фонарик будет с заряженной батарейкой на борту. Вот и все пока. Дальше мы попробуем разорвать зависимость, если нам для этого не подходит ни DI через конструктор ни через setter. Читаем тут....

воскресенье, 17 июля 2011 г.

Java for fun: Шаблоны: Observer или Как работает рекрутинг?

Привет. Есть у меня такая папочка на компьютере, называется Java for fun. Ее я пополняю в моменты вдохновения. Сейчас как раз такой момент. Им решил поделиться с тобой.

Как работает рекрутер в IT? Есть ты, ты - специалист в разработке. Есть компания, которой ты нужен как сотрудник. Есть работник компании - рекрутер - собирающий информацию о разработчиках, после чего часто следует приглашение на собеседование. У тебя есть резюме, которое ты предлагаешь в качестве запроса на добавление тебя в базу компании. Тебе перезванивают, когда находится подходящая для тебя вакансия.

Я не даром отметил "ты предлагаешь" и "тебе перезванивают" - это ключевые моменты шаблона Observer. Единожды подписавшись на рассылку чего либо - ты будешь получать новости до тех пор, пока не решишь прекратить это. Будучи подписанным тебе не надо заботиться о том, где, как и когда появится информация - тебе перезвонят. "Не звоните нам, мы сами вам перезвоним!" Занимайся своими делами, а как только... так сразу...

Давай попробуем развить эту модель пошагово (test driven). Создай пустой проект и тест в нем. пускай он будет таким.

package myobserver;

import static org.junit.Assert.assertSame;

import org.junit.Test;

public class RecrutingTest {

    private Vacancy newVacancy = null;

    @Test
    // проверяем что зарегистрированный кандидат получит уведомление
    public void testRegisterAndNotify() {
        // есть некий ректрутинг департамент 
        Recruiter recruiter = new RecruitingDepartment();

        // есть ты - потенциальный кандидат.
        // реализация интерфейса через анонимный класс в целях удобства тестирования.
        Candidate candidate = new Candidate() {

            @Override
            // когда нас уведомят о новой вакансии, 
            // мы сообщим об этом тест-классу - так мы сможем 
            // проверить уведомлял ли нас кто-то или нет.
            public void haveANew(Vacancy vacancy) {
                newVacancy = vacancy;
            }

        };

        // мы регистрируемся у рекрутера
        recruiter.register(candidate);

        // а теперь рекрутеру упала новая вакансия
        Vacancy someVacancy = new Vacancy() { };  
        recruiter.addNew(someVacancy);

        // вот тут мы и проверяем, как отработал наш рекрутинг
        assertSame("Кандидат не был информирован о новой вакансии",
                someVacancy, newVacancy);
    }

}

Ты наверное заметил, что в коде достаточно много интерфейсов: Vacancy, Candidate, Recruiter? Да, я люблю интерфейсы - программирование с помощью интерфейсов дает невиданную гибкость и слабую связанность классов, что, в часто меняющихся условиях, очень выгодно.

Вот они, эти интерфейсы:

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

package myobserver;

public interface Candidate {
    
    // кандидат умеет получать уведомления о вакансиях
    void haveANew(Vacancy vacancy);
    
}

Рекрутер - это тоже специалист. Он может регистрировать нового соискателя (Candidate). Заметь, только такого, который сможет отреагировать на его уведомление (реализует метод haveANew(Vacancy)). Иначе зачем рекрутеру напрягаться, если никто все равно не обратит внимание.

Так же рекрутер может получать уведомление о новых вакансиях. Вакансии могут быть направлены ему с помощью метода addNew(Vacancy). Сделать это может кто-то, кто знает о такой возможности рекрутера.

package myobserver;

public interface Recruiter {
    
    // через этот метод кандидат может подписаться на рассылку
    void register(Candidate candidate);
         
    // кто-то может через этот метод направить рекрутеру новую вакансию
    void addNew(Vacancy vacancy);
    
}

А это пока пустой интерфейс - вакансия.

package myobserver;

public interface Vacancy {

}

Чтобы все компилировалось нам надо создать реализацию Рекрутера.

package myobserver;

public class RecruitingDepartment implements Recruiter {
   
    @Override
    public void register(Candidate observer) {
    }

    @Override
    public void addNew(Vacancy vacancy) {
    }

}

Если сейчас запустить тест, то он не пройдет (будет красненький). Потому я напишу самую простую реализацию, которая пришла мне в голову.

Писать как можно проще важно для качественного tdd. Часто, после первого написания кода, я стараюсь упростить его максимально. Но так, чтобы тест все еще проходил :).

Вот она, реализация.

package myobserver;

public class RecruitingDepartment implements Recruiter {

    // это база кандидатов у рекрутера
    private Candidate candidate;
    
    @Override
    // в момент, когда кандидат обращается к рекрутеру
    public void register(Candidate candidate) {
        // рекрутер сохраняет кандидата 
        // позже, рекрутер воспользуется этой ссылкой,
        // чтобы направить уведомление о новой вакансии
        this.candidate = candidate;
    }

    @Override
    // вот он момент истины! рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // и он тут же оповещает кандидату о ней 
        candidate.haveANew(vacancy);
    }

}

Запусти тест, он будет зеленый, а это значит - пора код отправлять в svn.

На данном этапе наш рекрутер работает только с одним кандидатом. В какой-то момент рекрутер набирается смелости и решает - я могу больше! Так появляется новый тест.

Замечу, что я немного порефакторил код и превратил anonymous class в inner class. Иначе в двух тестах наблюдалось бы дублирование.

package myobserver;

import static org.junit.Assert.assertSame;

import org.junit.Test;

public class RecrutingTest {

    class SomeCandidate implements Candidate {

        // внутренний клас-кандидат всего лишь сохраняет вакансию. 
        // так позже проверят информаровали ли кандидата
        Vacancy vacancy;
        
        @Override
        // когда кандидата уведомят о новой вакансии, он ее сохранит
        public void haveANew(Vacancy vacancy) {
            this.vacancy = vacancy;
        }

    }
    
    @Test
    // старый тест немного поменялся и использует теперь SomeCandidate 
    public void testRegisterAndNotify() {
        Recruiter recruiter = new RecruitingDepartment();

        // тут раньше был анонимный класс
        SomeCandidate candidate = new SomeCandidate();

        recruiter.register(candidate);

        Vacancy someVacancy = new Vacancy() { };        
        recruiter.addNew(someVacancy);

        // а тут проверялось поле тест-класса, а теперь поле кандидата
        assertSame("Кандидат не был информирован о новой вакансии",
                someVacancy, candidate.vacancy);
    }
    
    @Test
    // новый тест проверяющий оповещение двух зарегистрированных кандидатов
    public void testRegisterAndNotifyAll() {
        // есть некий ректрутинг департамент 
        Recruiter recruiter = new RecruitingDepartment();

        // один потенциальный кандидат.
        SomeCandidate candidate1 = new SomeCandidate();
        
        // второй потенциальный кандидат.
        SomeCandidate candidate2 = new SomeCandidate();

        // оба кандидата регистрируются у рекрутера
        recruiter.register(candidate1);
        recruiter.register(candidate2);

        // а теперь рекрутеру упала новая вакансия
        Vacancy someVacancy = new Vacancy() { };        
        recruiter.addNew(someVacancy);

        // вот тут мы и проверяем, как отработал наш рекрутинг
        assertSame("Первый кандидат не был информирован о новой вакансии",
                someVacancy, candidate1.vacancy);        
        assertSame("Второй кандидат не был информирован о новой вакансии",
                someVacancy, candidate2.vacancy);
    }
    
}

Запускаем и видим, что тест не проходит (красный). Опять же делаем минимум правок, чтобы тест заработал.

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // на этот раз база кандидата по-круче будет 
    private List<Candidate> candidates = new LinkedList<Candidate>();
    
    @Override
    // в момент, когда кандидат обращается к рекрутеру
    public void register(Candidate candidate) {
        // рекрутер сохраняет кандидата в своем списке 
        // позже, рекрутер воспользуется этим списком 
        // чтобы направить всем новое уведомление
        candidates.add(candidate);
    }

    @Override
    // вот он момент истины! рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // он достает список и уведомляет всех всех всех 
        for (Candidate candidate : candidates) {
            candidate.haveANew(vacancy);
        }
    }

}

Ура! Тесты зеленые, а значит делаем commit!

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

package myobserver;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import org.junit.Test;

public class RecrutingTest {

    // ... тут старые тесты, они не менялись ...
    
    @Test
    // проверяем что удаленный кандидат перестает получать уведомление
    public void testUnregisterCandidate() {
        // все тот же ректрутинг департамент 
        Recruiter recruiter = new RecruitingDepartment();

        // кандидат
        SomeCandidate candidate = new SomeCandidate();
        
        // регистрируемся у рекрутера
        recruiter.register(candidate);        
        
        // и тут же передумываем
        recruiter.remove(candidate);
        
        // рекрутеру упала новая вакансия
        Vacancy someVacancy = new Vacancy() { };        
        recruiter.addNew(someVacancy);

        // а кандидат ничего и не узнал
        assertNull("Кандидат получил уведомление, а не должен был",
                candidate.vacancy);        
    }

}

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

package myobserver;

public interface Recruiter {
    
    // через этот метод соискатель может подписаться на рассылку
    void register(Candidate candidate);
         
    // кто-то может через этот метод направить рекрутеру новую вакансию
    void addNew(Vacancy vacancy);

    // новый метод!
    // кандидат может отказаться от услуг рекрутера
    void remove(Candidate candidate);
    
}

Последнее, что надо сделать - это рекрутера расширить одним методом, пускай пока он будет пустым

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... все старое осталось без изменений ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // TODO Auto-generated method stub        
    }

}

Добились компиляции! Ура! Запускаем тесты. И видим что последний тест краснючий.

Добавим первое, что приходит в голову чтобы тест заработал.

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... тут все без изменений ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // согласен, глупость. Но тест ведь заработал после этой строки?
        // какой тест, такая и реализация :) 
        candidates.clear();        
    }

}

Почему я так сделал? А за тем, чтобы заставить добавить еще один тест, который проверит, что при удалении текущего кандидата с остальными ничего не произойдет. Мне кажется, тест вполне логичный. Только вот если бы я написал вместо

candidates.clear();

то, что ожидалось

candidates.remove(candidate);

то мой следующий тест был бы сразу зеленый. А это недопустимо. В tdd тест должен хоть раз проходить через красное состояние - так мы знаем, что он не холостой и проверяет именно то, что мы хотели.

Итак очередной тест

package myobserver;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import org.junit.Test;

public class RecrutingTest {

    // ... все без изменений, менять старые тесты опасно ...

    @Test
    // проверяем, что после удаления перестает получать уведомление только 
    // удаленный кандидат
    public void testUnregisterOnlyOneCandidate() {
        // есть некий ректрутинг департамент 
        Recruiter recruiter = new RecruitingDepartment();

        // раз кандидат
        SomeCandidate candidate1 = new SomeCandidate();
        
        // два кандидат
        SomeCandidate candidate2 = new SomeCandidate();

        // оба регистрируются
        recruiter.register(candidate1);
        recruiter.register(candidate2);
        
        // один передумал
        recruiter.register(candidate1);

        // тем временем рекрутеру упала новая вакансия
        Vacancy someVacancy = new Vacancy() { };        
        recruiter.addNew(someVacancy);

                
        // второго проинформировали
        assertSame("Второй кандидат не был информирован о новой вакансии, хотя подписан",
                someVacancy, candidate2.vacancy);
        
        // а первый остался в неведении
        // assertNull("Первый кандидат после исключения из расписки не должен " +
        //         "был получать уведомление, а получил", 
        //         candidate1.vacancy);
        // но мы это проверять не будем, потому как у нас уже есть тест на этот случай
        // повторяться в коде - плохая примета. 
    }
    
}

Запускаем. Красное! Ура! Теперь правка в реализации. Можно и дальше продолжать фигней страдать, но сейчас у меня умных тестов в голову не приходит, а потому пора остановиться и написать правильную реализацию.

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... тут ничего не менялось ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // находим кандидата в списке и удаляем
        candidates.remove(candidate);
    }

}

Реализация слегка небезопасная и не предусматривает выписку не зарегистрированного ранее кандидата. Но реализация этого теста выходит за пределы шаблона Observer. Если хочешь - реализуй его самостоятельно.

Сейчас у нас уведомление происходит в момент добавления новой вакансии, то есть синхронно. В оригинально версии шаблона Observer, обновление состояния информатора (у нас это добавление вакансии рекрутеру) и информирование всех подписанных слушателей (у нас это кандидаты) происходит асинхронно. Реализуем подобное поведение у нас.

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

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

В каждый тест, сразу после добавления новой вакансии стоит сделать дополнительный вызов нового метода. Итак в каждом тесте после строк

// а теперь рекрутеру упала новая вакансия 
Vacancy someVacancy = new Vacancy() { };        
recruiter.addNew(someVacancy);       

добавляем строки

// тут же оповещаем об этом
recruiter.notice();

После этого в интерфейс рекрутера должен быть добавлен новый метод

package myobserver;

public interface Recruiter {
    
    // ... старые методы без изменений

    // с помощью этого метода рекрутеру подается команда 
    // оповестить всех кандидатов
    void notice();
    
}

Реализация немного усложнится

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // база кандидатов  
    private List<Candidate> candidates = new LinkedList<Candidate>();
    
    // рекрутеру надо временно хранить свои вакансии
    // для этого в него добавлен этот список
    private List<Vacancy> vacancies = new LinkedList<Vacancy>();
    
    @Override
    // кандидат обращается к рекрутеру
    public void register(Candidate candidate) {
       // ... тут ничего не меняется ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
       // ... тут так же ничего не меняется ...

    @Override
    // а когда рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // и он ее добавит в свой список вакансий
        vacancies.add(vacancy);
        // и все!

        // Напомню раньше он тут же оповещал кандидатов 
        // вот так
        // for (Candidate candidate : candidates) {
        //    candidate.haveANew(vacancy);
        // }        
    }

    @Override
    // а тут пришла команда оповестить всех
    public void notice() {
        // он достает список и уведомляет всех всех всех... 
        for (Candidate candidate : candidates) {
            // ...обо всем, что накопилось за это время
            for (Vacancy vacancy : vacancies) {
                candidate.haveANew(vacancy);
            }
        }        

        // по логике потом он чистит список вакансий, 
        // чтобы не дай бог не повторяться
        // vacancies.clear(); 
        // но это уже после добавление теста, проверяющего этот факт
    }

}

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

Вот где-то так и выглядит в чистом виде шаблон Observer. Если глянуть на UML из Википедии



А потом переписать на нашем языке


То можно увидеть они почти идентичны. Разница лишь в том, что у нас кроме методов добавления/удаления/нотификации кандидатов и есть еще метод добавления вакансии. Не беда.

Все что будет происходить дальше с шаблоном Observer имеет мало общего. Просто я хочу еще немного поэкспериментировать с кодом.

Есть один раздражающий для кандидата момент - всякий раз, когда придет время оповещения кандидатов, каждый из них получит полный список вакансий. Может будем очищать временный список после того, как отправим его всем? Вперед!

Вот тест

package myobserver;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;

import java.util.LinkedList;
import java.util.List;

import org.junit.Test;

public class RecrutingTest {

    // ... inner class SomeCandidate не трогаем ...
    
    // а этот внутренний клас-кандидат сохраняет ВСЕ ваканси в список 
    // так позже можно будет проверить как именно информаровали кандидата
    class VacanciesRecorder implements Candidate {

        List<Vacancy> vacancies = new LinkedList<Vacancy>();
        
        @Override
        // когда кандидата уведомляют о вакансии, он ее сохраняет
        public void haveANew(Vacancy vacancy) {
            vacancies.add(vacancy);
        }

    }
    
    // ... другие тесты мы не трогаем ...
    
    @Test
    // проверяем что зарегистрированный кандидат получит уведомление 
    // но только по новым вакансиям 
    public void testNotifyOnlyWithNewVacancies() {
        // все как обычно 
        Recruiter recruiter = new RecruitingDepartment();
        VacanciesRecorder candidate = new VacanciesRecorder();
        recruiter.register(candidate);

        // рекрутеру упала новая вакансия и тут же информируем       
        recruiter.addNew(new Vacancy() {});
        recruiter.notice();

        // очищаем список сохраненный у кандидата
        candidate.vacancies.clear();
        
        // позже упали две вакансим и тут же информируем
        Vacancy someVacancy1 = new Vacancy() { };        
        recruiter.addNew(someVacancy1);        
        Vacancy someVacancy2 = new Vacancy() { };        
        recruiter.addNew(someVacancy2);        
        recruiter.notice();
        
        // вот тут мы и проверяем, что получили новости
        assertSame("Кандидат должен был быть информарован двумя вакансиями",
                2, candidate.vacancies.size());
        assertTrue("Кандидат был информирован не о новых вакансиях",
                candidate.vacancies.contains(someVacancy1));
        assertTrue("Кандидат был информирован не о новых вакансиях",
                candidate.vacancies.contains(someVacancy2));        
    }
}

И он естественно не работает.

После того, как мы добавим одну единственную строку очистки временного списка в метод notice класса RecruitingDepartment, тест зазеленеет.

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... тут ничего не менялось

    @Override
    public void notice() {
        // ... тут тоже ...
      
        // а потом чистит список вакансий, чтобы не дай бог не повторяться
        vacancies.clear();        
    }

}

Опля! Коммитимся!

Но как же! Все нажитое нечесным путем - все вакансии, после информирования пропадут. А как же новые кандидаты, им наверное было бы интересно получить список того, что раньше получали их коллеги.

Новый тест в студию!

package myobserver;
 
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
 
import java.util.LinkedList;
import java.util.List;
 
import org.junit.Test;
 
public class RecrutingTest {

    // ... без изменний ...

    @Test
    // проверяем что новенький кандидат получит уведомление 
    // по всем вакансиям, которые когда либо были добавлены в системе
    // даже если они уже высылались его коллегам
    public void testNotifyAllVacanciesForEachNewCandidate() {
        // все как обычно 
        Recruiter recruiter = new RecruitingDepartment();
        recruiter.register(new VacanciesRecorder());
    
        // рекрутеру упала новая вакансия. тут же информируем       
        Vacancy someVacancy1 = new Vacancy() { };
        recruiter.addNew(someVacancy1);
        recruiter.notice();
    
        // но тут зарегистрировался еще один кандидат. 
        VacanciesRecorder candidate = new VacanciesRecorder();
        recruiter.register(candidate);
            
        // и после упали еще две вакансии.
        Vacancy someVacancy2 = new Vacancy() { };        
        recruiter.addNew(someVacancy2);        
        Vacancy someVacancy3 = new Vacancy() { };        
        recruiter.addNew(someVacancy3);                
    
        // информируем всех
        recruiter.notice();
        
        // проверяем, что наш новенький кандидат получил все "до копеечки"
        assertSame("Кандидат должен был быть информарован тремя вакансиями",
                3, candidate.vacancies.size());
        assertTrue("Кандидат был информирован не о всех вакансиях",
                candidate.vacancies.contains(someVacancy1));
        assertTrue("Кандидат был информирован не о всех вакансиях",
                candidate.vacancies.contains(someVacancy2));
        assertTrue("Кандидат был информирован не о всех вакансиях",
                candidate.vacancies.contains(someVacancy3));
    }
}

Тест естественно красный. Пошаманим над реализацией...

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // рекрутеру надо как-то отличать новых кандидатов от старых
    // тут будут хранитсья новые кандидаты
    private List<Candidate> newCandidates = new LinkedList<Candidate>();
    
    // а старая база кандидатов - для тех, кто хоть раз получал уведомление  
    private List<Candidate> candidates = new LinkedList<Candidate>();
    
    // рекрутеру надо временно хранить свои новые вакансии 
    private List<Vacancy> newVacancies = new LinkedList<Vacancy>();
    
    // но так же ему надо складировать где-то вакансии, которые уже отправлялись
    private List<Vacancy> vacancies = new LinkedList<Vacancy>();
    
    
    @Override
    // в момент, когда кандидат обращается к рекрутеру
    public void register(Candidate candidate) {
        // рекрутер сохраняет кандидата в своем списке новых кандидатов
        newCandidates.add(candidate);
    }

    @Override
    // рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // и он ее добавит в свой список новых вакансий
        newVacancies.add(vacancy);            
    }

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // удяляем кандидата из основного списка, если он там есть
        candidates.remove(candidate);
        
        // то же проделываем со списком новеньких
        newCandidates.remove(candidate);
    }

    @Override
    // а тут пришла команда оповестить всех
    public void notice() {
        // он достает основной список и уведомляет всех старичков... 
        for (Candidate candidate : candidates) {
            // ...обо всем, что накопилось за это время
            for (Vacancy vacancy : newVacancies) {
                candidate.haveANew(vacancy);
            }
        }        
        
        // после он переносит новые вакансии в список историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        // далее он информирует всех новеньких...
        for (Candidate candidate : newCandidates) {
            // ... абсолютно всеми вакансиями
            for (Vacancy vacancy : vacancies) {
                candidate.haveANew(vacancy);
            }
        }
        
        // после чего переносит новеньких кандидатов в основной список
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }

}

Код заметно усложнился, но зато тесты зеленые :) Коммитимся.

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

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

package myobserver;

import java.util.Collection;

public interface Candidate {
    
    // кандидат умеет получать уведомления о вакансиях
    void haveANew(Collection<Vacancy> vacancies);
    
}

Изменился метод нотификации

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... тут без изменений

    @Override
    // а тут пришла команда оповестить всех
    public void notice() {
        // он достает основной список и уведомляет всех старичков... 
        for (Candidate candidate : candidates) {
            // ...обо всем, что накопилось за это время
            candidate.haveANew(newVacancies); // ***
        }        
        
        // после он переносит новые вакансии в список историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        // далее он информирует всех новеньких...
        for (Candidate candidate : newCandidates) {
            // ... абсолютно всеми вакансиями
            candidate.haveANew(vacancies); // ***
        }
        
        // после чего переносит новеньких кандидатов в основной список
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }

}

и тестовые классы реализации кандидатов

package myobserver;
 
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
 
import java.util.LinkedList;
import java.util.List;
 
import org.junit.Test;
 
public class RecrutingTest {

    // ... без изменний ...

    // внутренний клас-кандидат всего лишь сохраняет вакансию. 
    // так позже проверят информаровали ли кандидата
    class SomeCandidate implements Candidate {
    
        // внутренний клас-кандидат всего лишь сохраняет вакансию. 
        // так позже проверят информаровали ли кандидата
        Vacancy vacancy;
          
        @Override
        // когда кандидата уведомят о новой вакансии, он ее сохранят
        public void haveANew(Collection<Vacancy> vacancies) {
            assertEquals("Ожидается одна вакансия", 1, vacancies.size());
            this.vacancy = vacancies.iterator().next();
        }
    
    }
    
    // а этот внутренний клас-кандидат сохраняет ВСЕ ваканси в список 
    // так позже можно будет проверить как именно информаровали кандидата
    class VacanciesRecorder implements Candidate {

        List<Vacancy> vacancies = new LinkedList<Vacancy>();
           
        @Override
        // когда кандидата уведомляют о вакансиях, он их сохраняет
        public void haveANew(Collection<Vacancy> vacancies) {
            this.vacancies.addAll(vacancies);
        }
    
    }

    // ... дальше все без изменений ...
}

Тесты зеленые, значит ничего не поломали - коммит!

Но мне тут подозрение одно закрлось. Рекрутер информирует своих кандидатов своими оригинальными списками. Рекрутер отдает каждому кандидату ссылку на свой список, которым пользуется сам. Это нарушение инкапсуляции! Достаточно хитрый кандидат сможет удалить вакансию из списка и она больше никому не достанется. Так кандидат сможет повысить свои шансы попасть на собеседование.

Напишем тест выскрывающий эту дыру.

package myobserver;

import static org.junit.Assert.*;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import org.junit.Test;

public class RecrutingTest {

    // ... иннер-классы-кандидаты не менялись ...
    
    // а этот внутренний клас-кандидат удаляет ВСЕ ваканси из списка, которй ему вручает  
    // рекрутер в надежде на то, что его шансы попасть на собеседование возрастут  
    class VacanciesEraser implements Candidate {
        
        Collection<Vacancy> vacancies = null;
        
        @Override
        // когда кандидата уведомляют о вакансиях, он их удаляет
        public void haveANew(Collection<Vacancy> vacancies) {
            this.vacancies = vacancies; 
        }
        
        public void clearVacnsiesList() {
            vacancies.clear();
        }
    }

    // ... другие тесты не менялись ...
   
    @Test 
    // Никто из кандидатов н может влиять на списки рекрутера
    public void testDoNotFixRevruterList() {        
        Recruiter recruiter = new RecruitingDepartment();
        
        // первый кандидант - хитрый, он удаляет все, что ему попадается в руки 
        VacanciesEraser candidate1 = new VacanciesEraser();
        recruiter.register(candidate1);       
      
        // появилась вакансия, тут же оповестили
        Vacancy someVacancy = new Vacancy() { };        
        recruiter.addNew(someVacancy);             
        recruiter.notice();
        
        // появился второй кандидант
        VacanciesRecorder candidate2 = new VacanciesRecorder();
        recruiter.register(candidate2);
        
        // но тут злостный кандидат почистил список!
        candidate1.clearVacnsiesList();
        
        // прошла вторая фаза уведомления
        recruiter.notice();        
        
        // проверяем, что наш новый кандидат получил все - справедливость восторжествовала
        assertSame("Кандидат должен был быть информарован вакансией",
                1, candidate2.vacancies.size());
        assertTrue("Кандидат был информирован не о той вакансии",
                candidate2.vacancies.contains(someVacancy));        
    }
    
}

Супер! Тест красный. Решается это копированием списков рекрутером до отправки.

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // ... тут ничего не менялось ...

    @Override
    public void notice() {
        for (Candidate candidate : candidates) {
            // заметь, тут идет вызов makeNewList
            candidate.haveANew(makeNewList(newVacancies));
        }        
        
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        for (Candidate candidate : newCandidates) {
            // заметь, и тут идет вызов makeNewList
            candidate.haveANew(makeNewList(vacancies));
        }
        
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }
    
    // а это новый метод копирования списка 
    private List<Vacancy> makeNewList(List<Vacancy> vacancies) {
        return new LinkedList<Vacancy>(vacancies);
    }

}

Тест зеленый - коммитим!

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

package myobserver;

import java.util.Set;

public interface Resume {
 
    // в резюме есть контактные данные кандидата
    Candidate getCandidate();
 
    // и набор технологий, которыми владеет кандидат
    Set<String> getTechnologies();
 
}

Снова меняется интерфейсная часть, но тут так же добавляется новая функциональность. Потому я вначале незаметно поменяю интерфейс, добавивив резюме. А потом я сделаю возможным фильтрование вакансий по критериям в резюме.

Меняется и вакансия - в ней теперь так же будут указан набор технологий.

package myobserver;

import java.util.Set;

public interface Vacancy {

    // вакансия сожержит набор требуемых технологий
    Set<String> getTechnologies();
    
}

Естественно после этого тесты перестанут компилироваться, а потому добавим внутренний класс EmptyVacancy, который будет подходить абсолютно всем :)

package myobserver;

import static org.junit.Assert.*;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.HashSet;

import org.junit.Test;

public class RecrutingTest {

    // ... другие иннер-классы не менялись ...

    // вакансия, которая подойдет всем кандидатам - пустая
    class EmptyVacancy implements Vacancy {

        @Override
        public Set<String> getTechnologies() {
            return new HashSet<String>();
        }        
    }
   
    // ... тесты без изменений ...

}

А в тестах, везде, где раньше писали

// а теперь рекрутеру упала новая вакансия
Vacancy someVacancy = new Vacancy() { };        
recruiter.addNew(someVacancy);

Запишем так

// а теперь рекрутеру упала новая вакансия
Vacancy someVacancy = new EmptyVacancy();        
recruiter.addNew(someVacancy);

Хух! Компилится и более того все зеленое, а потому коммитим!

Следующая правка будет более болезненной

package myobserver;

public interface Recruiter {
    
    // через этот метод соискатель может подписаться на рассылку
    // раньше регистрировался кандидат лично 
    // void register(Candidate candidate);
    // но теперь как заявка передается резюме
    void register(Resume resume); 
         
    // ... остальное без изменений ...
    
}

Снова тесты не компилятся.

Вот как меняется класс реализуюзий рекрутера. Обрати внимание как все упоминания Candidate заменились на Resume + в методе рассылки вакансий кандидат извлекается из резюме.

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // рекрутеру надо как-то отличать новых кандидатов от старых
    // тут будут хранитсья резюме новых кандидатов
    private List<Resume> newCandidates = new LinkedList<Resume>();
    
    // а старая база резюме кандидатов - для тех, кто хоть раз получал уведомление  
    private List<Resume> candidates = new LinkedList<Resume>();
    
    // рекрутеру надо временно хранить свои новые вакансии 
    private List<Vacancy> newVacancies = new LinkedList<Vacancy>();
    
    // но так же ему надо складировать где-то вакансии, которые уже отправлялись
    private List<Vacancy> vacancies = new LinkedList<Vacancy>();
    
    
    @Override
    // в момент, когда кандидат обращается к рекрутеру он передает свое резюме
    public void register(Resume resume) {
        // рекрутер сохраняет резюме кандидата в своем списке новых кандидатов
        newCandidates.add(resume);
    }

    @Override
    // рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // и он ее добавит в свой список новых вакансий
        newVacancies.add(vacancy);            
    }

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // удяляем резюме кандидата из основного списка, если он там есть
        candidates.remove(candidate);
        
        // то же проделываем со списком новеньких кандидатов
        newCandidates.remove(candidate);
    }

    @Override
    // а тут пришла команда оповестить всех кандидатов
    public void notice() {
        // он достает основной список и уведомляет всех старичков... 
        for (Resume resume : candidates) {
            // ...обо всем, что накопилось за это время
            // заметь, тут идет вызов makeNewList
            resume.getCandidate().haveANew(makeNewList(newVacancies));
        }        
        
        // после он переносит новые вакансии в список историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        // далее он информирует всех новеньких...
        for (Resume resume : newCandidates) {
            // ... абсолютно всеми вакансиями
            // заметь, тут идет вызов makeNewList            
            resume.getCandidate().haveANew(makeNewList(vacancies));
        }
        
        // после чего переносит резюме новеньких кандидатов в основной список
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }

    // а это новый метод копирования списка вакансий
    private Collection<Vacancy> makeNewList(List<Vacancy> vacancies) {
        return new LinkedList<Vacancy>(vacancies);
    }

}

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

Исправляеся легко. Там где было

// регистрируемся у рекрутера
recruiter.register(candidate); 

заменяем на

// регистрируемся у рекрутера
recruiter.register(new CandidateResume(candidate));

И доавляем новый inner класс в класс теста - резюме кандидата, который вообще ничего не знает из технологий. Такие бывают - это многие студенты на первом курсе.

package myobserver;

import static org.junit.Assert.*;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.HashSet;

import org.junit.Test;

public class RecrutingTest {

    // ... другие иннер-классы не менялись ...

    // это резюме кандидата, который ничего не знает
    class FresherResume implements Resume {
    
        // тут будем хранить кандидата 
        private Candidate candidate;
    
        // кандидат вписывается в резюме в момент его создания  
        public FresherResume(Candidate candidate) {
            this.candidate = candidate;
        }
            
        @Override
        // вернем кандидата если спросят
        public Candidate getCandidate() {
            return candidate;
        }
    
        @Override
        // в ответ на список технологий мы скажем что ничего не знаем 
        public Set<String> getTechnologies() {
            return new HashSet<String>();
        }
    }
    
    // ... тесты не менялись ...

}

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

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {
  
    // ... бла бла бла ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // удяляем кандидата из основного списка, если он там есть
        candidates.remove(candidate);
            
        // то же проделываем со списком новеньких
        newCandidates.remove(candidate);
    }

}

А должно быть так

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {
  
    // ... ничего не менялось ...

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // удяляем резюме кандидата из основного списка, если он там есть
        removeCandidate(candidate, candidates);
            
        // то же проделываем со списком новеньких кандидатов
        removeCandidate(candidate, newCandidates);
    }
    
    // так как добавляем мы кандидата по его резюме а удаляем по имени кандидата
    // то вот вам и метод
    private void removeCandidate(Candidate candidate, List<Resume> candidates) {
        Collection<Resume> resumeForRemove = new LinkedList<Resume>(); 
        
        for (Resume resume : candidates) {            
            if (resume.getCandidate().equals(candidate)) {
                resumeForRemove.add(resume);
            }
        }
        
        candidates.removeAll(resumeForRemove);
    }

}

После этого тесты заработают!

Последний метод removeCandidate намекнул мне на то, что пора никапсулировать список кандидатов. Почему? Да потому что метод не работает ни с одним полем класса RecruitingDepartment. Еще бы он работает на уровне конкретного спискА а не на уровне спискОВ. А значит он должен быть перемещен со списком, с которым работает в отдельный класс.

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

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {
  
    // ... ничего не менялось ...

    @Override
    // а тут пришла команда оповестить всех кандидатов
    public void notice() {
        // он достает основной список и уведомляет всех старичков... 
        for (Resume resume : candidates) {
            // ...обо всем, что накопилось за это время
            // заметь, тут идет вызов makeNewList
            resume.getCandidate().haveANew(makeNewList(newVacancies));
        }        
    
        // после он переносит новые вакансии в список историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
    
        // далее он информирует всех новеньких...
        for (Resume resume : newCandidates) {
            // ... абсолютно всеми вакансиями
            // заметь, тут идет вызов makeNewList            
            resume.getCandidate().haveANew(makeNewList(vacancies));
        }
    
        // после чего переносит резюме новеньких кандидатов в основной список
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }

}

Видишь два цикла? Они почти одинаковые, только итерируемся каждый раз по разному. Я давно это место приглянул, потому как в этом методе всякий раз внося новые изменения я вносил из жважды, по ожному в каждый цикл - а это симптом. Хотя нет это уже диагноз.

Заметь, так выглядит читабельнее

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {
  
    // ... ничего не менялось ...

    @Override
    // а тут пришла команда оповестить всех кандидатов
    public void notice() {
        // он достает основной список и уведомляет всех старичков обо всем, 
        // что накопилось за время с прошлого оповещения 
        noticeAll(candidates, newVacancies);        
        
        // после он переносит новые вакансии в список историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        // далее он информирует всех новеньких абсолютно всеми вакансиями
        noticeAll(newCandidates, vacancies);
    
        // после чего переносит резюме новеньких кандидатов в основной список
        candidates.addAll(newCandidates);
        newCandidates.clear();                             
    }

    // метод нотификации всех кандидатов списком вакансий
    private void noticeAll(List<Resume> candidates, List<Vacancy> vacancies) {
        for (Resume resume : candidates) {
            // заметь, тут идет вызов makeNewList
            resume.getCandidate().haveANew(makeNewList(vacancies));
        }
    }

}

Всегда, когда код становится более OOP он становится более читабельным!

Если посмотреть на метод noticeAll, то он так же работает на уровне спискА а не на уровне спискОВ, где полагает быть методам рекрутера.

Закомитимся пока зеленая полоса.

А теперь выделим одно поле List<Resume> и два чуждых тут метода в новый класс и назовем его Candidates.

Осё

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Candidates {

    private List<Resume> list = new LinkedList<Resume>(); 
    
    // так как добавляем мы кандидата по его резюме а удаляем по имени кандидата
    // то вот вам и метод
    void remove(Candidate candidate) {
        Collection<Resume> resumeForRemove = new LinkedList<Resume>(); 
        
        for (Resume resume : list) {            
            if (resume.getCandidate().equals(candidate)) {
                resumeForRemove.add(resume);
            }
        }
        
        list.removeAll(resumeForRemove);
    }
    
    // метод нотификации всех кандидатов списком вакансий
    void noticeAll(List<Vacancy> vacancies) {
        for (Resume resume : list) {
            // заметь, тут идет вызов makeNewList
            resume.getCandidate().haveANew(makeNewList(vacancies));
        }
    }

    // а это новый метод копирования списка вакансий
    private Collection<Vacancy> makeNewList(List<Vacancy> vacancies) {
        return new LinkedList<Vacancy>(vacancies);
    }

    // метод добавления резюме кандидата в список
    void add(Resume resume) {
        list.add(resume);        
    }
    
    // метод переноса всех резюме в другой список кандидатов
    void moveTo(Candidates candidates) {
        candidates.list.addAll(list);
        list.clear();
    }    
}

Вау как много методов у него! А все методы были перемещены из реализации рекрутера, которая теперь выглядит проще

package myobserver;

import java.util.LinkedList;
import java.util.List;

public class RecruitingDepartment implements Recruiter {

    // рекрутеру надо как-то отличать новых кандидатов от старых
    // тут будут хранитсья резюме новых кандидатов
    private Candidates newCandidates = new Candidates();
    
    // а старая база резюме кандидатов - для тех, кто хоть раз получал уведомление  
    private Candidates candidates = new Candidates();
    
    // рекрутеру надо временно хранить свои новые вакансии 
    private List<Vacancy> newVacancies = new LinkedList<Vacancy>();
    
    // но так же ему надо складировать где-то вакансии, которые уже отправлялись
    private List<Vacancy> vacancies = new LinkedList<Vacancy>();
    
    
    @Override
    // в момент, когда кандидат обращается к рекрутеру он передает свое резюме
    public void register(Resume resume) {
        // рекрутер сохраняет резюме кандидата в своем списке новых кандидатов
        newCandidates.add(resume);
    }

    @Override
    // рекрутеру пришла новая вакансия
    public void addNew(Vacancy vacancy) {
        // и он ее добавит в свой список новых вакансий
        newVacancies.add(vacancy);            
    }

    @Override
    // метод отписки от рассылки
    public void remove(Candidate candidate) {
        // удяляем резюме кандидата из основного списка, если он там есть
        candidates.remove(candidate);
        
        // то же проделываем со списком новеньких кандидатов
        newCandidates.remove(candidate);
    }

    @Override
    // а тут пришла команда оповестить всех кандидатов
    public void notice() {
        // он достает список и уведомляет всех стареньких обо всем, 
        // что накопилось за время с прошлого оповещения  
        candidates.noticeAll(newVacancies);        
        
        // после он переносит новые вакансии в список-историю  
        vacancies.addAll(newVacancies);                
        newVacancies.clear();
        
        // потом он уведомляет всех новеньких всеми вакансиями  
        newCandidates.noticeAll(vacancies);

        // после чего переносит резюме новеньких кандидатов в основной список
        newCandidates.moveTo(candidates);                             
    }

}

Вот оно, прелесть ООП - всегда многовложенный сложный код можно сделать простым. Я не уверен нужны ли тут комментарии, но их я оставлю для тебя, читатель. В рабочей версии кода я никогда не пишу комментарии потому, что мой код хорошо за себя сам говорит.

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

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Vacancies {
    
    private List<Vacancy> list = new LinkedList<Vacancy>();

    // метод добавления вакансии в список
    void add(Vacancy vacancy) {
        list.add(vacancy);        
    }

    // а это новый метод копирования списка вакансий    
    Collection<Vacancy> getCopy() {
        return new LinkedList<Vacancy>(list);
    }

    // метод перемещения вакансий в другой список
    void moveTo(Vacancies vacancies) {
        vacancies.list.addAll(list);                
        list.clear();        
    }
    
}

Измениллся так же список кандидатов

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Candidates {

    private List<Resume> list = new LinkedList<Resume>(); 
    
    // так как добавляем мы кандидата по его резюме а удаляем по имени кандидата
    // то вот вам и метод
    void remove(Candidate candidate) {
        Collection<Resume> resumeForRemove = new LinkedList<Resume>(); 
        
        for (Resume resume : list) {            
            if (resume.getCandidate().equals(candidate)) {
                resumeForRemove.add(resume);
            }
        }
        
        list.removeAll(resumeForRemove);
    }
    
    // метод нотификации всех кандидатов списком вакансий
    void noticeAll(Vacancies vacancies) {
        for (Resume resume : list) {
            // заметь, тут список копируется
            resume.getCandidate().haveANew(vacancies.getCopy());
        }
    }

    // метод добавления резюме кандидата в список
    void add(Resume resume) {
        list.add(resume);        
    }
    
    // метод переноса всех резюме в другой список кандидатов
    void moveTo(Candidates candidates) {
        candidates.list.addAll(list);
        list.clear();
    }

}

Обрати внимание на строчку

resume.getCandidate().haveANew(vacancies.getCopy());

когда-то она коряво выглядела так

resume.getCandidate().haveANew(makeNewList(vacancies));

с некрасивым апендиксом
// а это новый метод копирования списка вакансий
private Collection<Vacancy> makeNewList(List<Vacancy> vacancies) {
    return new LinkedList<Vacancy>(vacancies);
}

посмотри как он гармонично выглядит в классе Vacancies

public class Vacancies {
    
    private List<Vacancy> list = new LinkedList<Vacancy>();

    Collection<Vacancy> getCopy() {
        return new LinkedList<Vacancy>(list);
    }

    // ... другие методы
  
}

Нравится? А?!

На еще! Вот как после этого упростится наша реализация рекрутера. Я уберу комментарии, чтобы было видно, насколько красива может быть объектная джава

package myobserver;

public class RecruitingDepartment implements Recruiter {

    private Candidates newCandidates = new Candidates();
    private Candidates candidates = new Candidates();
    private Vacancies newVacancies = new Vacancies();
    private Vacancies vacancies = new Vacancies();   
    
    @Override
    public void register(Resume resume) {
        newCandidates.add(resume);
    }

    @Override
    public void addNew(Vacancy vacancy) {
        newVacancies.add(vacancy);            
    }

    @Override
    public void remove(Candidate candidate) {
        candidates.remove(candidate);
        newCandidates.remove(candidate);
    }

    @Override
    public void notice() {
        candidates.noticeAll(newVacancies);        
        newVacancies.moveTo(vacancies);
        newCandidates.noticeAll(vacancies);
        newCandidates.moveTo(candidates);                             
    }
}

Вот и вся логика нашего рекрутера. Рекрутер, вооружившись своими умными списками

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Vacancies {
    
    private List<Vacancy> list = new LinkedList<Vacancy>();

    void add(Vacancy vacancy) {
        list.add(vacancy);        
    }

    Collection<Vacancy> getCopy() {
        return new LinkedList<Vacancy>(list);
    }

    void moveTo(Vacancies vacancies) {
        vacancies.list.addAll(list);                
        list.clear();        
    }
}

и

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Candidates {

    private List<Resume> list = new LinkedList<Resume>(); 
    
    void remove(Candidate candidate) {
        Collection<Resume> resumeForRemove = new LinkedList<Resume>(); 
        
        for (Resume resume : list) {            
            if (resume.getCandidate().equals(candidate)) {
                resumeForRemove.add(resume);
            }
        }
        
        list.removeAll(resumeForRemove);
    }
    
    void noticeAll(Vacancies vacancies) {
        for (Resume resume : list) {
            resume.getCandidate().haveANew(vacancies.getCopy());
        }
    }

    void add(Resume resume) {
        list.add(resume);        
    }
    
    void moveTo(Candidates candidates) {
        candidates.list.addAll(list);
        list.clear();
    }
}

Стал в два раза проще :)

Тесты зеленые - коммитимся.

Последнее, что осталось сделать - реализовать фильтрование: отправлять вакансии только тем кандидатам, которые потянут эту должность.

Новое требование - новый тест!

package myobserver;

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.junit.Test;

public class RecrutingTest {

    // ... другие inner классы не менялись ...
    
    // вакансия, с заданным списком технологий
    class ConcreteVacancy implements Vacancy {        
        
        private Set<String> technologies;

        // вакансия создается для конкретного списка технологий
        public ConcreteVacancy(String...technologies) {
            this.technologies = new HashSet<String>(Arrays.asList(technologies));
        }
        
        @Override
        // который мы возвращаем при надобности
        public Set<String> getTechnologies() {
            return technologies;
        }

        @Override
        // только если весь список совпадает - тогда нас утраивает
        public boolean statisfy(Set<String> technologies) {
            return technologies.containsAll(technologies);
        }        
    }    
    
    // резюме кандидата, который что-то знает
    class CandidateResume extends FresherResume implements Resume {

        private HashSet<String> technologies;

        // сохраняем имя кандидата и список технологий ему известных
        public CandidateResume(Candidate candidate, String...technologies) {
            super(candidate);
            this.technologies = new HashSet<String>(Arrays.asList(technologies));
        }
        
        @Override
        // в ответ на список технологий мы скажем что знаем 
        public Set<String> getTechnologies() {
            return technologies;
        }    
        
    }   
    
    // ... тесты не менялись ...
    
    @Test
    // проверяем, что вакансии рассылаются только тем кандидатам, которые смогут реализовать вакансию
    public void testFilteringCandidatesByVacationTechnologies() {
        Recruiter recruiter = new RecruitingDepartment();

        // новый javaEE кандидат         
        VacanciesRecorder javaEECandidate = new VacanciesRecorder();
        recruiter.register(new CandidateResume(javaEECandidate, "Java", "SQL", "Hibernate", "XML"));
        
        // новый PHP кандидат
        VacanciesRecorder phpCandidate = new VacanciesRecorder();
        recruiter.register(new CandidateResume(phpCandidate, "PHP"));                
        
        // появилась javaEE вакансия 
        Vacancy javaEEVacancy = new ConcreteVacancy("Java", "SQL", "XML", "Hibernate");        
        recruiter.addNew(javaEEVacancy);
        
        // появилась PHP вакансия 
        Vacancy pnpVacancy = new ConcreteVacancy("PHP");        
        recruiter.addNew(pnpVacancy);
        
        // еще одна фрешер-вакансия, на которую можно всем
        Vacancy fresherVacancy = new EmptyVacancy();        
        recruiter.addNew(fresherVacancy);
        
        // разослать приглашения
        recruiter.notice();

        // джавист поулчил два предложения - джаваЕЕ- и фреш-вакансию
        assertSame(2, javaEECandidate.vacancies.size());
        assertTrue(javaEECandidate.vacancies.contains(javaEEVacancy));        
        assertTrue(javaEECandidate.vacancies.contains(fresherVacancy));
        
        // ПХПшник получил так же две - PHP- и фреш-вакансию 
        assertSame(2, phpCandidate.vacancies.size());
        assertTrue(phpCandidate.vacancies.contains(pnpVacancy));        
        assertTrue(phpCandidate.vacancies.contains(fresherVacancy));
    }
    
}

Тест не работает. Сделаем его рабочим!

Фильтрование добавил в метод информарования всех кандидатов

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class Candidates {

    // ... все остальное без изменений ...
    
    // метод нотификации всех кандидатов списком вакансий
    void noticeAll(Vacancies vacancies) {
        for (Resume resume : list) {
            // заметь, тут список фильтруется а потом копируется
            resume.getCandidate().haveANew(vacancies.filter(resume.getTechnologies()).getCopy());
        }
    }
}

Реализацию фильтра добавил в список вакансий

package myobserver;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class Vacancies {
    
    // ... тут все без изменений ...

    // метод фильтрации списка вакансий по категориям
    Vacancies filter(Set<String> technologies) {
        Vacancies result = new Vacancies();
        
        for (Vacancy vacancy : list) {
            if (vacancy.statisfy(technologies)) {
                result.add(vacancy);                
            }
        }
        
        return result;
    }
    
}

А вакансия умеет теперь отвечать на вопрос, удовлетворяет ли ей набор технологий или нет.

package myobserver;

import java.util.Set;

public interface Vacancy {

    // вакансия сожержит набор требуемых технологий
    // Set<String> getTechnologies();
    // этого уже ненадо

    // вакансия умеет отвечать на вопрос, подходит ли ей список технологий или нет
    boolean statisfy(Set<String> technologies);
    
}

Причем старого метода getTechnologies уже и не надо (слишком много он раскрывал).

И последнее - две тестовых реализации вакансии

package myobserver;

import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.junit.Test;

public class RecrutingTest {

    // ... другие inner классы не менялись ...

    // вакансия, которая подойдет всем кандидатам - пустая
    class EmptyVacancy implements Vacancy {        
        
        @Override
        // ей все подходит!
        public boolean statisfy(Set<String> technologies) {
            return true;
        }
       
    }
    
    // вакансия, с заданным списком технологий
    class ConcreteVacancy implements Vacancy {        
            
        private Set<String> technologies;
    
        // вакансия создается для конкретного списка технологий
        public ConcreteVacancy(String...technologies) {
            this.technologies = new HashSet<String>(Arrays.asList(technologies));
        }
        
        @Override
        // только если весь список совпадает - тогда нас утраивает
        public boolean statisfy(Set<String> technologies) {
            return this.technologies.containsAll(technologies);
        }
        
    }

    // ... тесты не менялись ...

}

Вот и все...

среда, 13 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #3

Привет!! Вчера я писал про инъекцию с помощью конструктора и, как результат, композицию из зависимого объекта и объекта, его использующего. Но фонарик мы привыкли видеть со съемными батарейками, а потому представляю слегка модифицированную версию вчерашнего кода. Итак инъекция посредством get'тера.

Скажу сразу исходники этого примера можно качнуть тут.

Начнем как всегда с интерфейсов. Описание батарейки - не менялся:

package getter.battery;

public interface Battery {

    boolean getVoltage();

}

Описание фонарика - добавили метод установки батарейки (setBattery):

package getter.flashlight;

import getter.battery.Battery;

public interface Flashlight {

    void swithOn();
    
    void swithOff();

    boolean isShines();

    void setBattery(Battery battery);

}

Дальше у нас пойдет китайская батарейка - она так же не менялась:

package getter.battery;

public class ChinaBattery implements Battery {

    private int power = 5; 
    
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        
        return false;
    }

}

Немного поменялся класс фонарика:

- Конструктор, принимающий батарейку, был удален. Напомню он выглядел так:

public SomeFlashlight(Battery battery) {
    this.battery = battery;
    this.swithOn = false;
}

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

public SomeFlashlight() {
    this.swithOn = false;
}

- Так же был добавлен один setter для установки батарейки на место. Метод определен в интерфейсе.

public void setBattery(Battery battery) {
    this.battery = battery;
}

Вот класс целиком и полностью:

package getter.flashlight;

import getter.battery.Battery;

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
    
    public SomeFlashlight() {
        this.swithOn = false;
    }
    
    public void setBattery(Battery battery) {
        this.battery = battery;
    }

    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();             
        }            
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }

}

А вот и тесты. Они так же поменялись - как минимум был добавлен тест на проверку "а что, если во время работы батарейки нагло извлекли из фонарика?"

package getter;
import static org.junit.Assert.*;
import getter.battery.Battery;
import getter.battery.ChinaBattery;
import getter.flashlight.Flashlight;
import getter.flashlight.SomeFlashlight;

import org.junit.Test;

public class TestBaterry {
        
    class DisposableBattery implements Battery{

        private boolean full = true;
        
        @Override
        public boolean getVoltage() {            
            if (full) {
                full = false;
                return true;
            }
            return false;
        }
    }
    
    @Test
    public void testDischargeNewBattery() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOff();        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertFalse(flashlight.isShines());        
    }
    
    @Test
    public void testChangeDischargedBattery() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOff();        
        assertFalse(flashlight.isShines());
        
        flashlight.setBattery(new DisposableBattery());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());        
    }
    
    @Test
    public void testBadBattery() {                        
        Battery battery = new Battery(){
            @Override
            public boolean getVoltage() {            
                return false;
            }
        };
        
        Flashlight flashlight = new SomeFlashlight();        
        flashlight.setBattery(battery);
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertFalse(flashlight.isShines());            
    }
    
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {                        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());            
    }
            
    @Test 
    public void testNoBatteryNoLight() {        
        Flashlight flashlight = new SomeFlashlight();
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertFalse(flashlight.isShines());    
    }
    
    @Test 
    public void testRemoveBatteryFromFlashlightDurringLightOn() {        
        Battery battery = new DisposableBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
                
        flashlight.swithOn();        
        assertTrue(flashlight.isShines());
        
        flashlight.setBattery(null);
        assertFalse(flashlight.isShines());
    }
    
    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {                
        Battery battery = new ChinaBattery();
        
        Flashlight flashlight = new SomeFlashlight();
        flashlight.setBattery(battery);
        
        assertFalse(flashlight.isShines());
        
        flashlight.swithOn();
        
        assertTrue(flashlight.isShines());    
    }
    
}

Многа букоф :)

Глядя на тесты (как документацию), можно заметить что изначально фонарик не рабочий

Flashlight flashlight = new SomeFlashlight();      
assertFalse(flashlight.isShines());

А еще если батарейки высунуть, то он перестанет светиться

assertTrue(flashlight.isShines());        
flashlight.setBattery(null);
assertFalse(flashlight.isShines());

Во всех остальных случаях, сразу после создания фонарика в него вставляются батарейки

Battery battery = ...        
Flashlight flashlight = new SomeFlashlight();
flashlight.setBattery(battery);

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

Приведем наглядный пример (его я стырил из Википедии). Комната является частью квартиры, следовательно здесь подходит композиция, потому что комната без квартиры существовать не может. А, например, мебель не является неотъемлемой частью квартиры, но в то же время, квартира содержит мебель, поэтому следует использовать агрегацию.

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

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

вторник, 12 июля 2011 г.

Java for fun: Что такое Dependency injection, Inversion of Control и почему это возникло. Часть #2

Привет. Уже давненько я писал первую часть поста, видимо пришло время перейти к практической части.

Рассматривать будем 4 случая инъекции:
- инъекция через конструктор
- инъекция через setter
- подготовка объекта с помощью простой фабрики
- подготовка объекта с помощью IoC контейнера написанного собственноручно

Действующие лица:
- какой-то фонарик - class SomeFlashlight
- описание фонарика - integface Flashlight
- описание батарейки - interface Battery
- китайская батарейка - class ChinaBattery

Скажу сразу исходники этого примера можно качнуть тут.

Батарейка - с ней все просто, она дает некоторое напряжение (true) или не дает его (false), если севшая.

package constructor.battery;

public interface Battery {

    boolean getVoltage();

}

По задумке батарейка разряжается от последовательных вызовов ее метода getVoltage, что реализовано в китайской батарейке

package constructor.battery;

public class ChinaBattery implements Battery {

    private int power = 5; 
 
    @Override
    public boolean getVoltage() {
        if (power > 0) {
            power--;
            return true;
        }
        return false;
    }

}

Так как она китайская, то садится за 5 раз.

Идем дальше - интерфейс фонарик

package constructor.flashlight;

public interface Flashlight {

    void swithOn();
 
    void swithOff();

    boolean isShines();

}

Фонарик может включаться (swithOn) и выключаться (swithOff) а светится он или нет, мы узнаем с помощью метода isShines, возвращающего true/false.

Если посмотреть на реализацию, то станет видно, что свет горит (isShines = true) только, если батарейка есть + если она дает заряд. При включении с батарейки снимается заряд, что равносильно ее разрядке, если вспомнить реализацию китайской одноразки.

package constructor.flashlight;

import constructor.battery.Battery;

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    private boolean swithOn;
 
    public SomeFlashlight(Battery battery) {
        this.battery = battery;
        this.swithOn = false;
    }

    @Override
    public boolean isShines() {
        return (battery != null) && swithOn;
    }

    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();    
        }   
    }

    @Override
    public void swithOff() {
        swithOn = false;
    }

}

Вот тест, демонстрирующий работу фонарика

package constructor;

import static org.junit.Assert.*;
import constructor.battery.Battery;
import constructor.battery.ChinaBattery;
import constructor.flashlight.Flashlight;
import constructor.flashlight.SomeFlashlight;

import org.junit.Test;

public class TestBaterry {
  
    class DisposableBattery implements Battery{

        private boolean full = true;
  
        @Override
        public boolean getVoltage() {   
            if (full) {
                full = false;
                return true;
            }
            return false;
        }
    }
 
    @Test
    public void testDischargeNewBattery() {      
        Battery battery = new DisposableBattery();
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());
  
        flashlight.swithOff();  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertFalse(flashlight.isShines());  
    }
 
    @Test
    public void testBadBattery() {      
        Battery battery = new Battery(){
            @Override
            public boolean getVoltage() {   
                 return false;
            }
        };
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertFalse(flashlight.isShines());   
    }
 
    @Test
    public void testNoGetPowerIfDoubleSwithOn() {      
        Battery battery = new DisposableBattery();
  
        Flashlight flashlight = new SomeFlashlight(battery);  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());
  
        flashlight.swithOn();  
        assertTrue(flashlight.isShines());   
    }
  
    @Test 
    public void testNoBatteryNoLight() {  
        Flashlight flashlight = new SomeFlashlight(null);
  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();
  
        assertFalse(flashlight.isShines()); 
    }
 
    @Test
    public void integrationTestGetPowerFormNewChinaBattery() {    
        Battery battery = new ChinaBattery();
        
        Flashlight flashlight = new SomeFlashlight(battery);
  
        assertFalse(flashlight.isShines());
  
        flashlight.swithOn();
  
        assertTrue(flashlight.isShines()); 
    }
 
}

Если посмотреть на каждый тест, то тут что происходит
Battery battery = ...
Flashlight flashlight = new SomeFlashlight(battery);
...

То есть вставляется через конструктор какая-то батарейка. Внутри фонарика все равно какая это будет батарейка, лишь бы соответствовала интерфейсу

public class SomeFlashlight implements Flashlight {

    private Battery battery;
    ...
 
    public SomeFlashlight(Battery battery) {
        this.battery = battery;
        ...
    }
    ...     

Конструктор фонарика мило подставит любую батарейку (которая, implements Battery) в свое поле. Это позволит в дальнейшем пользоваться из фонарика всеми методами, объявленными в интерфейсе Battery

public interface Battery {

    boolean getVoltage();

}

Вот так

public class SomeFlashlight implements Flashlight {
    ...
    @Override
    public void swithOn() {
        if (!swithOn && battery != null) {
            swithOn = battery.getVoltage();    
        }   
    }
...

Любая батарейка (то есть любой класс, главное, чтобы implements Battery) может быть использована фонариком. Фонарик при включении (swithOn()) попросит у батарейки энергии (battery.getVoltage()).

Вообще, так это полиморфизм на интерфейсах - результат выполнения battery.getVoltage() будет зависеть от того, что находится в battery.

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

Это называется композицией, когда части объекта неотъемлемы от него. Вот хлеб с изюмом состоит из муки, воды, соли, изюма - все это разобрать по частям не возможно. Хлеб - это композит. Раз создали используем все вместе.

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

Но об этом в следующей серии "инъекция через setter"...

четверг, 7 июля 2011 г.

Подборка №51

Техника пустого инбокса. "Мозг != TODO list" 15 минут. Очень интересно!

Прикольные комменты к коду

МММ 2011. Айлбибэк! Вот тексту дал, ой мне кажется что у него получится опять. Фейсбук ему в помощь!

Если зарегаться на http://amplifier.com.ua/ то каждый день будут высылать интересные задания.

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

"Черная книга менеджера" Славы Панкратова. Читается быстро. Читатель меняется так же быстро. Советую. Вообще, как оказалось подписаться на рассылку Стратоплана очень полезно для саморазвития.

Настанет, когда-то тот день, когда больше не надо будет офиса. Читаем тут...

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

Человек может сомневаться в чем угодно, но он никогда не сомневается в собственном уме. Мы говорим: «У нас такой характер, мы так привыкли». Однако мы не имеем ни малейшего понятия, как этот характер создавался. Мы смотрим на мир через призму собственных представлений, через узконаправленное, мутное стеклышко. Мы так привыкли смотреть, это наш обзор. Читать дальше...

в китайской пословице:
"Скажи мне, и я забуду.
Покажи мне, и я запомню.
Позволь мне сделать, и это станет моим навсегда".

Начинать обучение имеет смысл с выяснения у слушателей ожиданий, с которыми они пришли на учебу.

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


А вот текст субтитров. (там много, а потому под катом)