Объект в футляре

Это четвёртая статья серии, посвящённая использованию класса Optional при обработке объектов с динамической структурой. В первой статье было рассказано о способах избежания NullPointerException в ситуациях, когда вы не можете или не хотите использовать Optional. Вторая статья посвящена описанию методов класса Optional в том виде, как он появился в Java 8. Третья — методам, добавленным в класс в Java 9.

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

Если сервис возвращает Optional, о причине мы ничего не узнаем. Значит надо использовать что-то похожее на Optional, но содержащее информацию об ошибке в случае неуспеха.
В примере с электрочайником, рассмотренном во второй статье этого цикла мы получали кипячёную воду, если на входе у нас есть и вода и электричество. При отсутствии воды или электричества мы получали Optional.empty() объект.

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

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

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

Но пока это не сделано, я предлагаю использовать класс Result<SUCCESS, FAILURE>, разработка которого была инспирирована чтением одной из глав книги:
Functional Programming in Java. How functional techniques improve your Java programs. Pierre-Yves Saumont. Manning Pubn. ISBN 9781617292736
Я очень советую прочитать вам эту книгу, если вы ее еще не прочитали. Итак, перейдем к рассмотрению класса.

Как его использовать внутри метода (в качестве возвращаемого значения)?


Класс позволяет запаковать в свои объекты (инстанции) сразу результат обработки в случае удачи либо информацию об ошибке в случае неудачи. Поэтому класс параметризуется двумя типами: Result<SUCCESS, FAILURE>.

Трюк состоит в том, что он позволяет запаковать либо одно, либо другое, но не оба вместе.
А дальше все просто: в случае успешной обработки мы записываем результат работы вашего метода как объект типа SUCCESS а в случае неудачи как объект типа FAILURE.

Как его использовать снаружи (в вызове метода)?


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

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

Элегантный подход заключается в использовании элементов функционального программирования из арсенала Java 8.

Но давайте перейдем к примерам.

Модифицируем электрочайник


Мы оттолкнемся от модели электрочайника из примера с электрочайником, рассмотренном во второй статье этого цикла.

Его исходные тексты а также исходные тексты рассмотренного в этой статье примера вы найдете в моём проекте на GitHub.
В примере мы пытались с использованием Java 8 Optional симулировать функционирование стационарного кипятильника, который можно встретить в офисах некоторых компаний.

Его схема представлена на картинке внизу:

Boiler

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

Краткое напоминание тем кто подзабыл или не читал вторую статью цикла
Сырую воду мы моделировали классом:

public class CupOfWater {
	public CupOfBoiledWater boil() {
    return new CupOfBoiledWater();}
}

Как мы видим, мы можем получить из сырой воды кипячёную с помощью метода boil(). Это допущение вызвало справедливые нарекания читателей. Но оно сделано из соображений простоты кода.

Ну а класс моделирующий кипячёную воду совсем прост:

public class CupOfBoiledWater {}


Вход модифицированного прибора можно описать таким вот интерфейсом:

public interface IBoilerInput2 {   
   void setAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}

Не удивляйтесь числам-суффиксам в названии интерфейсов. В серии я рассматривал разные альтернативы реализации модели чайника.

Выход прибора мы определим вот так:

public interface IBoilerOutput3 {   
   Result<CupOfWater, String> getCupOfWater();
   Result<CupOfBoiledWater, String> getCupOfBoiledWater();
}

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

Поведение класса описывается интерфейсом:

interface IBoiler3 extends IBoilerInput2, IBoilerOutput3 {}

По сравнению со старым вариантом можно видеть, что вместо Optional<CupOfBoiledWater> мы используем теперь Result<CupOfBoiledWater, String>.

Чтобы рассмотренные ниже тестовые коды были понятнее, договоримся сначала о текстах ошибок, которые наш класс может выдавать:


...
public static final String WATER_NOT_AVAILABLE = "Water not available.";
public static final String POWER_NOT_AVAILABLE = "Power not available.";
public static final String BOTH_NOT_AVAILABLE = WATER_NOT_AVAILABLE + " " +    POWER_NOT_AVAILABLE;

Пишем тест:

JUnit test Boiler3Test

public class Boiler3Test {
	
	private IBoiler3 boiler;
	
	@Before
	public void setUp() throws Exception {
		boiler = new Boiler3();
	}

	@Test
	public void testBothNotAvailable() {				
		boiler.setAvailability(null, false);
		assertFalse(boiler.getCupOfWater().isSuccess());
        boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));

		assertFalse(boiler.getCupOfBoiledWater().isSuccess());
        boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.BOTH_NOT_AVAILABLE));
	}
	
	
	@Test
	public void testPowerAvailable() {						
		boiler.setAvailability(null, true);
		assertFalse(boiler.getCupOfWater().isSuccess());
        boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));

		assertFalse(boiler.getCupOfBoiledWater().isSuccess());
        boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));
	}
	
	@Test
	public void testWaterAvailable() {		
		boiler.setAvailability(new CupOfWater(), false);
		assertTrue(boiler.getCupOfWater().isSuccess());
		assertFalse(boiler.getCupOfBoiledWater().isSuccess());
        boiler.getCupOfBoiledWater().ifFailure(message->assertEquals(message, Boiler3.POWER_NOT_AVAILABLE));
	}
	
	@Test
	public void testBothAvailable() {		
		boiler.setAvailability(new CupOfWater(), true);
		assertTrue(boiler.getCupOfWater().isSuccess());
		assertTrue(boiler.getCupOfBoiledWater().isSuccess());
	}
}

А вот и реализация класса электрочайника с помощью нашего нового класса Result<SUCCESS, FAILURE>:


public class Boiler3 implements IBoiler3 {

	public static final String WATER_NOT_AVAILABLE = "Water not available.";
	public static final String POWER_NOT_AVAILABLE = "Power not available.";
	public static final String BOTH_NOT_AVAILABLE = WATER_NOT_AVAILABLE + " " + POWER_NOT_AVAILABLE;

	@Nullable
	private CupOfWater water;
	private boolean powerAvailable;
	

	@Override
	public void setAvailability(@Nullable CupOfWater water, boolean powerAvailable) {
		this.water = water;
		this.powerAvailable = powerAvailable;
	}

	@Override
	public Result<CupOfWater, String> getCupOfWater() {
		return water == null
			? Result.failure(WATER_NOT_AVAILABLE)
			: Result.success(water);
	}

	@Override
	public Result<CupOfBoiledWater, String> getCupOfBoiledWater() {
		Result<CupOfWater, String> resultStep1 = getCupOfWater();
		return resultStep1.isSuccess()
				? powerAvailable
					? Result.success(resultStep1.getSuccess().boil())
					: Result.failure(POWER_NOT_AVAILABLE)
				: powerAvailable
					? Result.failure(WATER_NOT_AVAILABLE)
					: Result.failure(BOTH_NOT_AVAILABLE);
	}
}

Обратите внимание на задание значений в методе getCupOfWater() в зависимости от того, что должен вернуть метод.

Простой метод обработки результата продемонстрирован в третьей строчке метода getCupOfBoiledWater(). Вначале мы узнаем, каков результат с помощью resultStep1.isSuccess(). А затем в зависимости от ответа продолжаем обработку.

В тесте был продемонстрирован более функциональный способ обработки с помощью метода ifFailure:

boiler.getCupOfWater().ifFailure(message->assertEquals(message, Boiler3.WATER_NOT_AVAILABLE));

Метод будет вызван только если результат обработки был ошибочным. При этом информация об ошибке (в данном случае это message) будет автоматически предоставлена вашему обработчику.

Как видите, все очень просто.

Ну а под конец – исходный текст самого класса Result:

Класс Result

public abstract class Result<SUCCESS, FAILURE> {
	
	public abstract boolean isSuccess() ;
	
	public abstract SUCCESS getSuccess();
	
	public abstract FAILURE getFailure();
	
	public abstract Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess);
	
	public abstract Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure);
	
	public  static class Success<SUCCESS, FAILURE> extends Result<SUCCESS, FAILURE>{
		
		private final SUCCESS _success;
		
		private Success(SUCCESS success) {
			_success = success;
		}

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

		@Override
		public SUCCESS getSuccess() {
			return _success;
		}

		@Override
		public FAILURE getFailure() {
			throw new IllegalStateException("getFailure called on Success");
		}

		@Override
		public String toString() {
			return "Success [_success=" + _success + "]";
		}

		@Override
		public Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess) {
			consumerSuccess.accept(_success);
			return this;
		}

		@Override
		public Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure) {
			return this;
		}
		
	}
	
	public  static class Failure<SUCCESS, FAILURE> extends Result<SUCCESS, FAILURE>{
		
		private final FAILURE _failure;
		private Failure(FAILURE failure) {
			_failure = failure;
		}
		
		@Override
		public boolean isSuccess() {
			return false;
		}

		@Override
		public SUCCESS getSuccess() {
			throw new IllegalStateException("getSuccess called on Failure");
		}

		@Override
		public FAILURE getFailure() {
			return _failure;
		}

		@Override
		public String toString() {
			return "Failure [_failure=" + _failure + "]";
		}

		@Override
		public Result<SUCCESS, FAILURE> ifSuccess(Consumer consumerSuccess) {
			return this;
		}

		@Override
		public Result<SUCCESS, FAILURE> ifFailure(Consumer consumerFailure) {
			consumerFailure.accept(_failure);
			return this;
		}
		
	}
	
	public static <SUCCESS, FAILURE> Result<SUCCESS, FAILURE> failure(FAILURE failure){ return new Failure<>(failure);}
	
	public static <SUCCESS, FAILURE> Result<SUCCESS, FAILURE> success(SUCCESS success){ return new Success<>(success);}
	
}

Пользуйтесь, код полностью свободен.

Как и в случае прошлых примеров, исходные тексты вы найдете проекте на GitHub.

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

Я планирую также вознаградить ваше терпение подарком. Каким? Об этом вы узнаете в следующей статье.

Иллюстрация: ThePixelman

Комментарии (29)


  1. koldyr
    30.01.2018 13:02

    Прекрасно, вы почти уловили суть. Теперь допишите к классу Result правильные методы map и flatMap. И тогда всё будет совсем красиво и в соответсвии с теорией.


    1. sshikov
      30.01.2018 20:53

      Да я вот читаю уже четвертый пост, и все думаю… ну неужели те, кому это интересно, все еще не читали скажем вот это: https://dzone.com/articles/whats-wrong-java-8-part-iv? Ведь все уже расписано в мае 2014 года!


      1. visirok Автор
        30.01.2018 23:08

        Во второй стате этой серии я упомянул, что существует немало введений в Optional, в том числе и на русском языке, в том числе и на Хабре. Их намного меньше чем книг с кулинарными рецептами или книг по йоге. Но это можно обьяснить более узкой аудиторией и более узким вопросом.
        Не умаляя достоинств других tutorial и статей Optional, включая цитируемую Вами статью, рискну утверждать, что в них не «все уже расписано» а иногда, по моему мнению, кое-что не так преподнесено. Поэтому я и взялся за это дело.
        Ну а кто хорошо разбираетсч в теме, может попытаться написать ещё одну кулинарную книгу, курс по йоге или… введение в Optional (Шутка).


        1. sshikov
          31.01.2018 19:29

          Так еже ли бы вы их читали… Если бы читали — у вас скорее всего не возникало бы вопросов, почему у вашего класса должны быть методы map и flatMap.


          Ну а кто хорошо разбираетсч в теме, может попытаться написать ещё одну кулинарную книгу, курс по йоге или… введение в Optional (Шутка).

          Писать очередное введение в монады — это давно уже моветон. Уже лет пять как каждый новый автор извиняется за графоманию )))


          1. visirok Автор
            31.01.2018 21:11

            Я думаю всё дело в том, что Вам хотелось бы, чтобы читатели видели в Optional монаду. А я пытался добиться, стобы читатели ассоциировали Optional с футляром с дополнительными возможностями. Я думаю, с практической точки зрения моя аналогия практикующим программистам намного полезнее.
            Знать что монады существуют, безусловно полезно. Но в практической деятельности нужны (в Java) классы, которые содержат столько полезных методов, что из-за них монаду уже и не видно.
            Так стоит ли вообще на монаде заострять внимание и вообще упоминать о ней, если её не видно?


            1. sshikov
              31.01.2018 21:55
              -1

              Футляры? Ну вот вам футляры: https://habrahabr.ru/post/183150/. Я же говорю — все уже написано сто раз.


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

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


              1. visirok Автор
                31.01.2018 23:12

                Если предложение начинается с «Я думаю», это и означает «говорить за себя». Аналогия с футляром введена во в орой статье серии. Те кто считают эту аналогию неинтересной, неудачной и т.д., по-видимому не читали третью и четвертую часть. У Вас тоже была такая возможность. В аннотациях к статьям серии я старался не обещать лишнего и относительно точно описать, о чём пойдет речь.
                Если Вы судите о качестве сотни материалов, которые все были лучше моего, вы должны были потратить на их простениеи уйму времени. Для меня это не очень сочетается с моим представлением о практикующих программистах.
                В чём дело? Тоже зуб болит, как и у меня? (Я действительно только от зубного врача вернулся :)
                Может подождать, перетерпеть боль?


            1. koldyr
              31.01.2018 22:30

              Стоит, потому что если вы знаете про что-то, что оно монада, то вы сразу знаете, что там есть fmap, bind, join, return, аппликативное применение. Вы сразу знаете что можете комбинировать стелки Клейсли и при этом получать ожидаемый результат. И это знание даётся вам почти даром, если конечно вы знаете что такое монады.
              А вы пропустили самое интересное — как делать композицию вычислений, возвращающих Result.


              1. visirok Автор
                31.01.2018 23:31

                Эта серия статей — tutorial с конкретными целями, которые не совпали с Вашими ожиданиями. Так бывает. То что интересно для Вас, может быть неинтересно для других и наоборот. Возможно, со временем интересы изменятся.
                Автору остаётся только надеяться, что труд не пропал зря.


              1. visirok Автор
                31.01.2018 23:45

                А не попробовать ли Вам, как это сделал Moriline представить свои интерпертации или расширения рассмотренных в статьях примеров? Например — с композицией? Это был бы конструктивный вклад в Хабр.


              1. visirok Автор
                01.02.2018 00:51

                Относительно монад мне нравится вот эта цитата: «… монады — это шаблон, а не определенный тип. Монады — это форма, они абстрактные интерфейс (не в смысле Java) больше, чем конкретные данные. В результате любое руководство, основанное на примерах, обречено на неполноты и неудачи. [...] Единственный способ понять монады — увидеть их для них: математическую конструкцию.» Daniel Spiewak. В этом переводе
                А моё руководство основано на примерах.
                Так что я против упоминания монад в моем tutorial :)


              1. visirok Автор
                01.02.2018 12:06

                Стоит, потому что если вы знаете про что-то, что оно монада, то вы сразу знаете, что там есть fmap, bind, join, return, аппликативное применение

                Если бы в Java был стандартный интерфейс Monad с перечисленными методами и как минимум 2 стандартных класса, которые его реализовывают, то про это стоило бы говорить. Иначе — только запутывать читателя.
                С моей точки зрения, уже сейчас семантику map и flatMap в Optional не просто соотнести с семантикой этих же методов в Steeam.


                1. koldyr
                  01.02.2018 13:00

                  Он и есть. Если Typename — монада, то у него есть методы of, map,flatMap. И, что характерно, они реализованы у Optional и Stream.


                  1. visirok Автор
                    01.02.2018 15:20

                    Я бы согласился с Вами, если в документации на Optional и Stream в разделе All Superinterfaces стояло в числе других Monad. Но этого нет. Есть методы с одинаковыми названиями. А это далеко не одно и то же.


  1. visirok Автор
    30.01.2018 14:15

    Одобрение как от школьного усителя. Хотя у «ученика» волос на голове больше седых, чем иных. Но всё равно — спасибо.
    Ну а если серьёзно — класс писался для собственного использования. Пока мне ни map ни flatMap не понадобились. Понадобится — напишем. А заодно и filter.
    Если хотите, сделайте сами pull requestt в упомянутый проект.
    Очень интересная тема для дискуссии, навеянная Вашим замечанием: «Какие методы из арсенала функционального программирования были бы уместны в этом классе?“
    В качестве певого набора кандидатов можно рассмотреть методы, реализованные в Optional.


    1. koldyr
      30.01.2018 14:33

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


  1. visirok Автор
    30.01.2018 15:47

    Не воспринимаю упрёк на свой счёт. Как я понимаю, по «вы» Ва понимаете сообщество профессиональных Java программистов.
    Кстати, я лично программировал и программирую далеко не только на Java.
    Я могу понять Вашу досаду, что замечательные языки с замечательными функциональными концепциями в своих недрах никак не отвоюют себе достойную нишу в продакшине. Вы думаете, что это всё из-за лени и костности практикующих программистов?


    1. koldyr
      30.01.2018 18:13

      Я имел в виду Вас лично. Почему этого нет в экосистеме? Думаю потому, что не приспособленный синтаксис и высокий порог входа. Есть SkalaZ, но его синтаксис я вообще не знаю.


      1. sshikov
        30.01.2018 20:03

        Это есть в экосистеме. Возможно не в самом runtime, но есть в десятках (я не преувеличиваю) реализаций от сторонних производителей. Скажем Atlassian fugue, или очень старая Functional Java.


        1. koldyr
          30.01.2018 20:16

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


          1. sshikov
            30.01.2018 20:25

            Ну, я не обещал, что будет как в хаскеле. Не будет — система типов местами слабовата. Но для практического применения тот Either который имеется, вполне пригоден.


  1. Moriline
    30.01.2018 18:27

    1.

    Для этого нам необходимо уметь возвращать не только результат обработки в случае успеха, но и информацию об обнаруженной проблеме в случае неудачи. Для наглядности я сравнивал в предыдущих статьях Optional футляром. Развивая эту аналогию, в данном случае нам необходим футляр с двойным дном.
    Для этого и придумали исключения. Почему Вы снова возвращаетесь к методам возвращения кода ошибки( или текста ошибки) вместо использования исключений? Ведь использование исключений во время потока исполнения программы намного лучше. Что и доказывает их внедрение в поздних версиях языков( PHP, Haskell и т.д.).
    2. Если реализовать Ваш новый тип и КАЖДЫЙ метод его будет использовать, то какова будет реализация обработки таких ошибок на более высоком уровне? Как будет выглядеть код? Например в таком выдуманном случае:
    public void name(IRequest req) {
    		Request request = req.getOriginalParam();
    		Collection<Group> groups = new Groups().findByName(new ConnectionStorage("x", 10L).getConnection(), new Hash("z", 100).getHash(), request.getParam("name"));
    		List<User> result = new FilterByAge(new Users().findByGroup(groups, new Hash("y", 10).getHash(), new ConnectionStorage("w", 20).getConnection()), request.getParam("age"));
    	}


    1. koldyr
      30.01.2018 18:36

      Как раз в Haskell исключения сделаны правильно, через монадический трансформер ErrorT. Возможно есть какие-то ньюансы с IO, но там всегда так.


    1. visirok Автор
      30.01.2018 18:51
      +1

      Я с Вами не согласен. Исключения пришли в С из ассемблера как абстракция аппаратных прерываний «нормального» потока вычислений. С моей точки зрения — это крайность, которую надо использовать как средство обработки брутальных процессорных проблем типа нехватки памяти. Такие исключения случаются редко (если у Вас не барахлит компьютер) и обрабатывать их стоит на верхних этажах.
      Я не призываю КАЖДЫЙ вызов оформлять как Result, а только те, где вероятность неудачи относительно высока и для дальнейшей обработки надо знать причину неудачи.


    1. debose
      31.01.2018 01:19

      Поддерживаю. У меня при чтении этой части в голове крутилась мысль "а чем исключения не угодили?".


  1. Melorian
    31.01.2018 13:50

    Именно по этой причине Мартин Одерски придумал Either[Left, Right] :)


    1. visirok Автор
      31.01.2018 14:23

      Этот паттерн использовался и раньше, как мне кажется, в частности во внутренностях STL. Я назвал класс Result из прагматических соображений.


  1. brake
    02.02.2018 17:54

    Я, в свое время, набросал вот такое (буду благодарен за конструктивную критику, т.к. решено чуть иначе):


    Union
    import java.util.Optional;
    import java.util.function.BiFunction;
    import java.util.function.Consumer;
    import java.util.stream.Stream;
    
    /**
     * Объект-контейнер, который может содержать один из двух типов объектов.
     */
    class Union<T1, T2> {
        private final Optional<T1> first;
        private final Optional<T2> second;
    
        Union(T1 first, T2 second) {
    
            assert (first == null && second != null) || (first != null && second == null);
    
            this.first = Optional.ofNullable(first);
            this.second = Optional.ofNullable(second);
        }
    
        public boolean isFirstPresent() {
            return first.isPresent();
        }
    
        public boolean isSecondPresent() {
            return second.isPresent();
        }
    
        public <U> Stream<U> map(BiFunction<T1, T2, U> function) {
            return Stream.of(function.apply(first.orElse(null), second.orElse(null)));
        }
    
        public void forEach(Consumer<? super T1> whenValue1, Consumer<? super T2> whenValue2) {
            first.ifPresent(whenValue1);
            second.ifPresent(whenValue2);
        }
    } 


    1. visirok Автор
      03.02.2018 00:32

      Это хорошее минималисткое решение. Понятно, что из класса Union можно наследовать другие подобные классы.
      Улучшения также возможны. Класс Union лучше сделать abstract, чтобы его нельзя было вызывать напрямую. А функции isFirstPresent, isSecondPresent лучше сделать protected и заменять их предметными аналогами типа isSuccess и isException.