Sunday, March 22, 2009

Фабричный метод

В этот раз речь пойдет о паттернах проектирования. Если быть точнее — Статическом фабричном методе (Static Factory Method). Вкратце, он призван для того, чтобы инкапсулировать процесс создания объекта.

Допустим, у нас есть метод, который возвращает определенный набор (список) данных. Этот метод должен иметь возможность вернуть для каждой единицы фактический результат или ошибку. Естественно, что в таком случае исключения бросать нельзя, иначе мы не получим “хорошие” данные. Обычно, в таких целях используется объект-контейнер, который хранит или данные или ошибку или просто пустой.

Response

Response.java:

package factory;

public final class Response<V, E> {
  private final V value;
  private final E error;

  public Response(V value, E error) {
    checkParams(value, error);

    this.value = value;
    this.error = error;
  }

  public Response() {
    value = null;
    error = null;
  }

  public boolean isEmpty() {
    if (value == null && error == null) {
      return true;
    }

    return false;
  }

  public boolean isSuccess() {
    ensureNotEmpty();

    if (value == null) {
      return false;
    }

    return true;
  }

  public V getValue() {
    ensureNotEmpty();
    ensureSuccess();

    return value;
  }

  public E getError() {
    ensureNotEmpty();
    ensureError();

    return error;
  }

  private static <V, E> void checkParams(V value, E error) {
    if (value == null && error == null) {
      throw new IllegalArgumentException("Both error and value cannot be null");
    }
  }

  private void ensureNotEmpty() {
    if (isEmpty()) {
      throw new IllegalStateException("Operation is not allowed for empty response");
    }
  }

  private void ensureError() {
    if (isSuccess()) {
      throw new IllegalStateException("Operation is not allowed for success response");
    }
  }

  private void ensureSuccess() {
    if (!isSuccess()) {
      throw new IllegalStateException("Operation is not allowed for error response");
    }
  }
}

ResponseTest.java:

package factory;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class ResponseTest {
  @Test(expected = IllegalArgumentException.class)
  public void testResponseWithNulls() {
    new Response<Integer, String>(null, null);
  }

  @Test
  public void testResponseSuccess() {
    success(150);
  }

  @Test
  public void testResponseError() {
    error("test error");
  }

  @Test
  public void testResponseEmpty() {
    empty();
  }

  @Test
  public void testIsEmptyWithSuccess() {
    Response<Integer, String> success = success(135);

    assertFalse(success.isEmpty());
  }

  @Test
  public void testIsEmptyWithError() {
    Response<Integer, String> error = error("validation error");

    assertFalse(error.isEmpty());
  }

  @Test
  public void testIsEmptyWithEmpty() {
    Response<Integer, String> empty = empty();

    assertTrue(empty.isEmpty());
  }

  @Test
  public void testIsSuccessWithSuccess() {
    Response<Integer, String> success = success(763);

    assertTrue(success.isSuccess());
  }

  @Test
  public void testIsSuccessWithError() {
    Response<Integer, String> error = error("db error");

    assertFalse(error.isSuccess());
  }

  @Test(expected = IllegalStateException.class)
  public void testIsSuccessWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.isSuccess();
  }

  @Test
  public void testGetValueWithSuccess() {
    Response<Integer, String> success = success(921);

    assertEquals(Integer.valueOf(921), success.getValue());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithError() {
    Response<Integer, String> error = error("out of service");

    error.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithSuccess() {
    Response<Integer, String> success = success(48150);

    success.getError();
  }

  @Test
  public void testGetErrorWithError() {
    Response<Integer, String> error = error("application is broken");

    assertEquals("application is broken", error.getError());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.getError();
  }

  private static Response<Integer, String> success(Integer successValue) {
    return new Response<Integer, String>(successValue, null);
  }

  private static Response<Integer, String> error(String errorMessage) {
    return new Response<Integer, String>(null, errorMessage);
  }

  private static Response<Integer, String> empty() {
    return new Response<Integer, String>();
  }
}

В этом примере Response содержит два конструктора: один (без параметров) для пустых ответов и второй (с двумя параметрами) для ответов с данным или ошибками. Было бы логично иметь три конструктора, но не смотря на использования generic типов, параметры на самом деле имеют тип Object, соответственно конструктор не может быть перегружен.

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

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

Response.java:

package factory1;

public final class Response<V, E> {
  private final V value;
  private final E error;

  public static <V, E> Response<V, E> fromValue(V value) {
    return new Response<V, E>(value, null);
  }

  public static <V, E> Response<V, E> fromError(E error) {
    return new Response<V, E>(null, error);
  }

  public static <V, E> Response<V, E> empty() {
    return new Response<V, E>(null, null);
  }

  private Response(V value, E error) {
    this.value = value;
    this.error = error;
  }

  public boolean isEmpty() {
    if (value == null && error == null) {
      return true;
    }

    return false;
  }

  public boolean isSuccess() {
    ensureNotEmpty();

    if (value == null) {
      return false;
    }

    return true;
  }

  public V getValue() {
    ensureNotEmpty();
    ensureSuccess();

    return value;
  }

  public E getError() {
    ensureNotEmpty();
    ensureError();

    return error;
  }

  private void ensureNotEmpty() {
    if (isEmpty()) {
      throw new IllegalStateException("Operation is not allowed for empty response");
    }
  }

  private void ensureError() {
    if (isSuccess()) {
      throw new IllegalStateException("Operation is not allowed for success response");
    }
  }

  private void ensureSuccess() {
    if (!isSuccess()) {
      throw new IllegalStateException("Operation is not allowed for error response");
    }
  }
}

ResponseTest.java:

package factory1;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class ResponseTest {
  @Test
  public void testResponseSuccess() {
    success(150);
  }

  @Test
  public void testResponseError() {
    error("test error");
  }

  @Test
  public void testResponseEmpty() {
    empty();
  }

  @Test
  public void testIsEmptyWithSuccess() {
    Response<Integer, String> success = success(135);

    assertFalse(success.isEmpty());
  }

  @Test
  public void testIsEmptyWithError() {
    Response<Integer, String> error = error("validation error");

    assertFalse(error.isEmpty());
  }

  @Test
  public void testIsEmptyWithEmpty() {
    Response<Integer, String> empty = empty();

    assertTrue(empty.isEmpty());
  }

  @Test
  public void testIsSuccessWithSuccess() {
    Response<Integer, String> success = success(763);

    assertTrue(success.isSuccess());
  }

  @Test
  public void testIsSuccessWithError() {
    Response<Integer, String> error = error("db error");

    assertFalse(error.isSuccess());
  }

  @Test(expected = IllegalStateException.class)
  public void testIsSuccessWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.isSuccess();
  }

  @Test
  public void testGetValueWithSuccess() {
    Response<Integer, String> success = success(921);

    assertEquals(Integer.valueOf(921), success.getValue());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithError() {
    Response<Integer, String> error = error("out of service");

    error.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithSuccess() {
    Response<Integer, String> success = success(48150);

    success.getError();
  }

  @Test
  public void testGetErrorWithError() {
    Response<Integer, String> error = error("application is broken");

    assertEquals("application is broken", error.getError());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithEmpty() {
    Response<Integer, String> empty = empty();

    empty.getError();
  }

  private static Response<Integer, String> success(Integer successValue) {
    return Response.fromValue(successValue);
  }

  private static Response<Integer, String> error(String errorMessage) {
    return Response.fromError(errorMessage);
  }

  private static Response<Integer, String> empty() {
    return Response.empty();
  }
}

Методы Response.fromValue(), Response.fromError(), Response.empty() прозрачно для пользователя создают объект. Таким образом, они решают путаницу с конструкторами и даже делают код более осмысленным. Собственно, эти методы и являются статическими фабричными методами.

Что же теперь дают нам статические методы? На первый взгляд ничего более, кроме удобного использования. На самом же деле, существуют ещё две полезные возможности фабричных методов:

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

Для нашего примера нас интересует последнее свойство. Допустим, что наш контейнер на самом деле не один класс, а три отдельных класса: Success, Error и Empty. И каждый из них является наследником Response. Таким образом, каждый класс будет решать свою задачу (хранить данные, ошибку или просто быть пустым).

Response

Response.java:

package factory2;

public abstract class Response<V, E> {
  private static final Response<Object, Object> EMPTY = new Empty<Object, Object>();

  public static <V, E> Response<V, E> fromValue(V value) {
    if (value == null) {
      throw new NullPointerException("Parameter 'value' must be not null.");
    }
    return new Success<V, E>(value);
  }

  public static <V, E> Response<V, E> fromError(E error) {
    if (error == null) {
      throw new NullPointerException("Parameter 'error' must be not null.");
    }
    return new Error<V, E>(error);
  }

  @SuppressWarnings("unchecked")
  public static <V, E> Response<V, E> empty() {
    return (Response<V, E>) EMPTY;
  }

  private Response() {
  }

  public abstract boolean isEmpty();

  public abstract boolean isSuccess();

  public abstract V getValue();

  public abstract E getError();

  private final static class Success<V, E> extends Response<V, E> {
    private final V value;

    private Success(V value) {
      this.value = value;
    }

    @Override
    public boolean isEmpty() {
      return false;
    }

    @Override
    public boolean isSuccess() {
      return true;
    }

    @Override
    public V getValue() {
      return value;
    }

    @Override
    public E getError() {
      throw notAllowedOperationException();
    }

    private IllegalStateException notAllowedOperationException() {
      return new IllegalStateException("Operation is not allowed for success response");
    }
  }

  private final static class Error<V, E> extends Response<V, E> {
    private final E error;

    private Error(E error) {
      this.error = error;
    }

    @Override
    public boolean isEmpty() {
      return false;
    }

    @Override
    public boolean isSuccess() {
      return false;
    }

    @Override
    public V getValue() {
      throw notAllowedOperationException();
    }

    @Override
    public E getError() {
      return error;
    }

    private static IllegalStateException notAllowedOperationException() {
      return new IllegalStateException("Operation is not allowed for error response");
    }
  }

  private final static class Empty<V, E> extends Response<V, E> {
    @Override
    public boolean isEmpty() {
      return true;
    }

    @Override
    public boolean isSuccess() {
      throw notAllowedOperationException();
    }

    @Override
    public V getValue() {
      throw notAllowedOperationException();
    }

    @Override
    public E getError() {
      throw notAllowedOperationException();
    }

    private static IllegalStateException notAllowedOperationException() {
      return new IllegalStateException("Operation is not allowed for empty response");
    }
  }
}

ResponseTest.java:

package factory2;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class ResponseTest {
  @Test
  public void testResponseSuccess() {
    success(150);
  }

  @Test(expected = NullPointerException.class)
  public void testResponseSuccessWithNull() {
    success(null);
  }

  @Test
  public void testResponseError() {
    error("test error");
  }

  @Test(expected = NullPointerException.class)
  public void testResponseErrorWithNull() {
    error(null);
  }

  @Test
  public void testResponseEmpty() {
    empty();
  }

  @Test
  public void testIsEmptyWithSuccess() {
    Response<Integer, String> success = success(135);
    assertFalse(success.isEmpty());
  }

  @Test
  public void testIsEmptyWithError() {
    Response<Integer, String> error = error("validation error");
    assertFalse(error.isEmpty());
  }

  @Test
  public void testIsEmptyWithEmpty() {
    Response<Integer, String> empty = empty();
    assertTrue(empty.isEmpty());
  }

  @Test
  public void testIsSuccessWithSuccess() {
    Response<Integer, String> success = success(763);
    assertTrue(success.isSuccess());
  }

  @Test
  public void testIsSuccessWithError() {
    Response<Integer, String> error = error("db error");
    assertFalse(error.isSuccess());
  }

  @Test(expected = IllegalStateException.class)
  public void testIsSuccessWithEmpty() {
    Response<Integer, String> empty = empty();
    empty.isSuccess();
  }

  @Test
  public void testGetValueWithSuccess() {
    Response<Integer, String> success = success(921);
    assertEquals(Integer.valueOf(921), success.getValue());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithError() {
    Response<Integer, String> error = error("out of service");
    error.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetValueWithEmpty() {
    Response<Integer, String> empty = empty();
    empty.getValue();
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithSuccess() {
    Response<Integer, String> success = success(48150);
    success.getError();
  }

  @Test
  public void testGetErrorWithError() {
    Response<Integer, String> error = error("application is broken");
    assertEquals("application is broken", error.getError());
  }

  @Test(expected = IllegalStateException.class)
  public void testGetErrorWithEmpty() {
    Response<Integer, String> empty = empty();
    empty.getError();
  }

  private static Response<Integer, String> success(Integer successValue) {
    return Response.fromValue(successValue);
  }

  private static Response<Integer, String> error(String errorMessage) {
    return Response.fromError(errorMessage);
  }

  private static Response<Integer, String> empty() {
    return Response.empty();
  }
}

Интерфейс остался прежним, соответственно, тесты те же, что и в предыдущем примере. Последний пример имеет не только более опрятную реализацию, к тому же, по производительности и расходу памяти он более оптимален: поведение большинства методов определяется статически в зависимости от конкретной реализации, а ссылки сами по себе (даже пустые) используют память. Более того, в качестве пустого контейнера (Response.Empty) всегда возвращается один и тот же объект, который создается только один раз (такой себе Синглтон). Кстати, в качестве разминки, советую попробовать реализовать правильные методы equals и hashCode для нашего Response (советую начать с тестов). Думаю, что никто не будет отрицать преимуществ использования паттерна Фабричный метод для нашей задачи. Лично у меня для себя есть такое правило: если есть частично или полностью Immutable object, то всегда стоит подумать о том, чтобы сделать фабричный метод вместо конструктора, даже когда он явно делает то же самое, что и конструктор. Да, я знаю, это может немного противоречить принципу YAGNI. Но в таких случаях я себя оправдываю тем, что статические методы выглядят красивее и порой удобней читаются. Далее, в награду, у меня всегда есть возможность что-то поменять внутри класса или сделать некую оптимизацию не затрагивая клиентский код. В нагрузку, советую посмотреть на реализацию метода Integer.valueOf(). Для меня, в свое время, это было очень познавательно.

blog comments powered by Disqus