Заголовок темы может звучать, для Вас, странно, но довольно часто требуется, чтобы приложение имело сразу несколько вариантов интерфейса. Например, GUI, CLI и/или web. В этой статье пойдёт речь о построении такого приложения с использованием паттерна Observer.
По традиции, я буду вести рассказ на примере. Пример, как всегда, высосан из пальца и мало похож на реальное приложение.
Я уже сказал, что использовать мы будем паттерн Observer, но кроме него, нам понадобится ещё и паттерн MVC (Model-View-Controller). MVC состоит из трёх частей: модель, контроллер и представление. Модель — это те данные, с которыми мы работаем. Контроллер определяет способ работы с данными. Представление — это, не что иное, как интерфейс приложения, с которым взаимодействует пользователь. Если данные достаточно просты, то, часто, модель и контроллер объединяют в одном объекте. В своём примере, я именно так и поступил.
На следующей диаграмме показана упрощённая схема приложения.

Из диаграммы видно, что мы имеем два подприложения: консольное и оконное. MyObject, в данном примере, является как моделью, так и контроллером. Объект класса MyObject содержит список наблюдателей и оповещает их о смене состояние через интерфейс MyObjectObserver.
Предполагается, что мы имеем один объект класса MyObject разделённый между двумя приложениями. Когда одно из приложений меняет состояние объекта, второе приложение реагирует на это и меняет своё отображение.
Думаю, что с теорией всё ясно, давайте посмотрим на код.

package test.core;

import java.util.HashSet;

public class MyObject
{

State state = State.State1;
HashSet<MyObjectObserver> observers = new HashSet<MyObjectObserver>();

public static enum State
{
State1,
State2,
State3
}

public synchronized void addObserver(MyObjectObserver observer)
{
if(observer != null)
observers.add(observer);
}

public synchronized void removeObserver(MyObjectObserver observer)
{
observers.remove(observer);
}

public synchronized void setState(State newState)
{
if(state != newState)
{
State oldState = state;
state = newState;
for(MyObjectObserver observer : observers)
{
observer.onStateChanged(oldState, newState);
}
}
}

public synchronized State getState()
{
return state;
}
}

Так как класс MyObject выполняет роль контроллера в многопоточном приложении, то некоторые методы в нём объявлены  как synchronized.
Далее, интерфейс наблюдателя

package test.core;

public interface MyObjectObserver
{

public void onStateChanged(MyObject.State oldState, MyObject.State newState);
}

Каждое подприложение реализует интерфейс Application

package test;

import test.core.MyObject;

public interface Application
{

public void run(MyObject object);
}

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

package test.cli;

import test.Application;
import test.core.MyObject;
import test.core.MyObjectObserver;
import java.util.Scanner;

public class CliApplication
implements Application, MyObjectObserver
{

private boolean needPrompt = true;

@Override
public void run(MyObject object)
{
Scanner stdin = new Scanner(System.in);
object.addObserver(this);
while(true)
{
showPrompt();
String input = stdin.nextLine().trim();
if(input.equals("q")) break;
int number;
try
{
number = Integer.parseInt(input);
}
catch(NumberFormatException e)
{
System.out.println(String.format("\"%s\" is not number", input));
needPrompt = true;
continue;
}
MyObject.State state;
switch(number)
{
case 1:
state = MyObject.State.State1;
break;
case 2:
state = MyObject.State.State2;
break;
case 3:
state = MyObject.State.State3;
break;
default:
System.out.println(
String.format("\"%d\" is wrong number", number));
needPrompt = true;
continue;
}
object.setState(state);
}
object.removeObserver(this);
}

void showPrompt()
{
if(needPrompt)
{
System.out.print("Enter state number or 'q' for exit:\n" +
" 1: State1\n" +
" 2: State2\n" +
" 3: State3\n: ");
}
needPrompt = false;
}

@Override
public void onStateChanged(MyObject.State oldState, MyObject.State newState)
{
System.out.println(String.format(
"\nObject state was changed from \"%s\" to \"%s\"",
oldState.toString(), newState.toString()));
needPrompt = true;
showPrompt();
}
}

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

package test.gui;

import test.Application;
import test.core.MyObject;


public class GuiApplication
implements Application
{

@Override
public void run(MyObject object)
{
MainWindow window = new MainWindow(object);
window.setDefaultCloseOperation(MainWindow.EXIT_ON_CLOSE);
window.setVisible(true);
}
}

Объект класса MyObject транзитом проходит через класс GuiApplication в класс MainWindow.

package test.gui;

import test.core.MyObject;
import test.core.MyObjectObserver;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class MainWindow
extends JFrame
implements MyObjectObserver
{

private JComboBox comboBox;
private MyObject object;

class ComboBoxListener
implements ActionListener
{

@Override
public void actionPerformed(ActionEvent e)
{
MyObject.State state = (MyObject.State)comboBox.getSelectedItem();
if(state != null)
object.setState(state);
}
}

class MainWindowListener
extends WindowAdapter
{

@Override
public void windowOpened(WindowEvent e)
{
object.addObserver(MainWindow.this);
}

@Override
public void windowClosed(WindowEvent e)
{
object.removeObserver(MainWindow.this);
}
}

public MainWindow(MyObject object)
{
this.object = object;
comboBox = new JComboBox(MyObject.State.values());
comboBox.setSelectedItem(object.getState());
comboBox.addActionListener(new ComboBoxListener());
add(comboBox);
addWindowListener(new MainWindowListener());
setMinimumSize(new Dimension(200, 50));
pack();
}

@Override
public void onStateChanged(MyObject.State oldState, MyObject.State newState)
{
comboBox.setSelectedItem(newState);
}
}

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

package test;

import test.cli.CliApplication;
import test.core.MyObject;
import test.gui.GuiApplication;

class ApplicationRunner
implements Runnable
{

private Application application;
private MyObject object;

public ApplicationRunner(Application application, MyObject object)
{
this.application = application;
this.object = object;
}

@Override
public void run()
{
application.run(object);
}
}

public class Main
{

static MyObject object = new MyObject();

static Application[] getApplications()
{
return new Application[]
{
new CliApplication(),
new GuiApplication()
};
}

public static void main(String[] args)
{
for(Application app : getApplications())
{
ApplicationRunner runner = new ApplicationRunner(app, object);
Thread thread = new Thread(runner);
thread.start();
}
}
}

То, что в итоге получилось, можно увидеть в ролике ниже.