2014-09-17 10 views
9

Chciałbym mieć ograniczony stały katalog wystąpień pewnego skomplikowanego interfejsu. Standardowy wzór multiton ma kilka fajnych funkcji, takich jak leniwe tworzenie instancji. Jednak opiera się na kluczu takim jak String, który wydaje się być podatny na błędy i delikatny.Używanie enum do implementowania multitonów w Javie

Chciałbym wzór, który używa wyliczenia. Mają wiele wspaniałych funkcji i są solidne. Próbowałem znaleźć standardowy wzór projektu, ale narysowałem pustą. Więc wymyśliłem własne, ale nie jestem z tego powodu bardzo szczęśliwy.

Wzór używam jest następująco (interfejs jest bardzo uproszczony, żeby uczynić go czytelny):

interface Complex { 
    void method(); 
} 

enum ComplexItem implements Complex { 
    ITEM1 { 
     protected Complex makeInstance() { return new Complex() { ... } 
    }, 
    ITEM2 { 
     protected Complex makeInstance() { return new Complex() { ... } 
    }; 

    private Complex instance = null; 

    private Complex getInstance() { 
     if (instance == null) { 
      instance = makeInstance(); 
     } 
     return instance; 
    } 

    protected void makeInstance() { 
    } 

    void method { 
     getInstance().method(); 
    } 
} 

Wzorzec ten ma kilka bardzo ciekawych funkcji do niego:

  • z enum implementuje interfejs, który sprawia, że ​​jego użycie jest całkiem naturalne: ComplexItem.ITEM1.method();
  • Lazy inicjacja: jeśli konstrukcja jest kosztowna (mój przypadek użycia obejmuje czytanie plików), pojawia się tylko wtedy, gdy jest wymagana.

Powiedziawszy, że wydaje się to strasznie skomplikowane i "hackowate" dla tak prostego wymogu i nadpisuje metody enumowe w sposób, który nie jestem pewien, co projektanci języka zamierzali.

Ma również inną poważną wadę. W moim przypadku użycia chciałbym, aby interfejs rozszerzał Porównywalny. Niestety to zderza się z implementacją Enumable i sprawia, że ​​kod jest nieskompilowany.

Jedną z rozważanych przeze mnie opcji było posiadanie standardowego wyliczenia, a następnie oddzielnej klasy, która odwzorowuje wyliczenie na implementację interfejsu (przy użyciu standardowego wzorca wielotonowy). To działa, ale enum nie realizuje już interfejsu, który wydaje mi się nie być naturalnym odzwierciedleniem intencji. Oddziela również implementację interfejsu od elementów wyliczeniowych, które wydają się słabą enkapsulacją.

Inną alternatywą jest to, że konstruktor wyliczeniowy implementuje interfejs (to znaczy we wzorze powyżej usuwa potrzebę metody "makeInstance"). Podczas działania tego usuwa zaletę polegającą na tym, że w razie potrzeby uruchamia się tylko konstruktorów). To również nie rozwiązuje problemu z rozszerzeniem Porównywalne.

Moje pytanie brzmi: czy ktoś może wymyślić bardziej elegancki sposób na zrobienie tego?

W odpowiedzi na komentarze spróbuję określić konkretny problem, który próbuję rozwiązać jako pierwszy, a następnie poprzez przykład.

  • Istnieje stała zestaw obiektów, które implementują określonego interfejsu
  • Obiekty są bezpaństwowcem: są one używane do hermetyzacji zachowanie ruchowe tylko
  • Tylko zostaną wykorzystane podzbiór obiektów za każdym razem kod jest wykonany (w zależności od danych wejściowych użytkownika)
  • Tworzenie tych obiektów jest drogie: trzeba to zrobić tylko raz i tylko w razie potrzeby
  • Przedmiotem dzielić zachowanie partii

Można to zaimplementować z oddzielnymi klasami singleton dla każdego obiektu za pomocą oddzielnych klas lub klas nadrzędnych dla zachowania współużytkowanego. Wydaje się to niepotrzebnie skomplikowane.

Teraz przykład. System oblicza kilka różnych podatków w zbiorze regionów, z których każdy ma swój własny algorytm do kalkulacji podatków. Oczekuje się, że zbiór regionów nigdy się nie zmieni, ale regionalne algorytmy ulegną regularnej zmianie. Konkretne stawki regionalne muszą być ładowane w czasie wykonywania za pomocą usługi zdalnej, która jest wolna i droga. Za każdym razem, gdy system jest wywoływany, zostanie mu przydzielony inny zestaw regionów do obliczenia, więc powinien on tylko ładować stawki żądanych regionów.

Więc:

interface TaxCalculation { 
    float calculateSalesTax(SaleData data); 
    float calculateLandTax(LandData data); 
    .... 
} 

enum TaxRegion implements TaxCalculation { 
    NORTH, NORTH_EAST, SOUTH, EAST, WEST, CENTRAL .... ; 

    private loadRegionalDataFromRemoteServer() { .... } 
} 

Literatura background: Mixing-in an Enum

+2

myślę, że zgadzają się, że rozwiązaniem jest pokazać hackish i zbyt skomplikowane. Myślę, że powinieneś użyć najprostszej enum możliwej, a następnie po prostu użyć statycznej fabryki. Fabryka powinna buforować wartości i tworzyć nowe w razie potrzeby. – markspace

+0

Leniwe ładowanie, czytanie z plików, ograniczony zestaw instancji ... Brzmi jak banda singletonów dla mnie. –

+2

Oczekuje się, że emulatory będą niezmienne i można je przekształcać do postaci szeregowej, co może spowodować problemy. – wolfcastle

Odpowiedz

1

To działa dla mnie - jest bezpieczne dla wątków i ogólne. Interfejs enum musi implementować interfejs Creator, ale jest to łatwe - o czym świadczy użycie próbki na końcu.

To rozwiązanie łamie wiązanie, które zostało nałożone, gdzie jest to enum, który jest przechowywanym obiektem. Tutaj używam tylko enum jako fabryki do tworzenia obiektu - w ten sposób mogę przechowywać dowolny typ obiektu, a nawet każdy z nich może stworzyć inny typ obiektu (co było moim celem).

Używa wspólnego mechanizmu dla bezpieczeństwa wątków i leniwego tworzenia instancji za pomocą ConcurrentMap z FutureTask.

Istnieje niewielki koszt związany z utrzymywaniem wartości FutureTask przez cały czas trwania programu, ale można to poprawić przy odrobinie poprawek.

/** 
* A Multiton where the keys are an enum and each key can create its own value. 
* 
* The create method of the key enum is guaranteed to only be called once. 
* 
* Probably worth making your Multiton static to avoid duplication. 
* 
* @param <K> - The enum that is the key in the map and also does the creation. 
*/ 
public class Multiton<K extends Enum<K> & Multiton.Creator> { 
    // The map to the future. 
    private final ConcurrentMap<K, Future<Object>> multitons = new ConcurrentHashMap<K, Future<Object>>(); 

    // The enums must create 
    public interface Creator { 
    public abstract Object create(); 

    } 

    // The getter. 
    public <V> V get(final K key, Class<V> type) { 
    // Has it run yet? 
    Future<Object> f = multitons.get(key); 
    if (f == null) { 
     // No! Make the task that runs it. 
     FutureTask<Object> ft = new FutureTask<Object>(
       new Callable() { 

       public Object call() throws Exception { 
        // Only do the create when called to do so. 
        return key.create(); 
       } 

       }); 
     // Only put if not there. 
     f = multitons.putIfAbsent(key, ft); 
     if (f == null) { 
     // We replaced null so we successfully put. We were first! 
     f = ft; 
     // Initiate the task. 
     ft.run(); 
     } 
    } 
    try { 
     /** 
     * If code gets here and hangs due to f.status = 0 (FutureTask.NEW) 
     * then you are trying to get from your Multiton in your creator. 
     * 
     * Cannot check for that without unnecessarily complex code. 
     * 
     * Perhaps could use get with timeout. 
     */ 
     // Cast here to force the right type. 
     return type.cast(f.get()); 
    } catch (Exception ex) { 
     // Hide exceptions without discarding them. 
     throw new RuntimeException(ex); 
    } 
    } 

    enum E implements Creator { 
    A { 

       public String create() { 
       return "Face"; 
       } 

      }, 
    B { 

       public Integer create() { 
       return 0xFace; 
       } 

      }, 
    C { 

       public Void create() { 
       return null; 
       } 

      }; 
    } 

    public static void main(String args[]) { 
    try { 
     Multiton<E> m = new Multiton<E>(); 
     String face1 = m.get(E.A, String.class); 
     Integer face2 = m.get(E.B, Integer.class); 
     System.out.println("Face1: " + face1 + " Face2: " + Integer.toHexString(face2)); 
    } catch (Throwable t) { 
     t.printStackTrace(System.err); 
    } 
    } 

} 

W Javie 8 jest jeszcze łatwiejsze:

public class Multiton<K extends Enum<K> & Multiton.Creator> { 

    private final ConcurrentMap<K, Object> multitons = new ConcurrentHashMap<>(); 

    // The enums must create 
    public interface Creator { 

     public abstract Object create(); 

    } 

    // The getter. 
    public <V> V get(final K key, Class<V> type) { 
     return type.cast(multitons.computeIfAbsent(key, k -> k.create())); 
    } 
} 
1

Jedna myśl o tym wzorem: leniwy instancji nie jest bezpieczeństwo wątków. To może, ale nie musi być w porządku, zależy od tego, jak chcesz go użyć, ale warto to wiedzieć. (Biorąc pod uwagę, że sama inicjalizacja jest bezpieczna dla wątków.)

Poza tym nie widzę prostszego rozwiązania, które gwarantuje pełną kontrolę instancji, jest intuicyjne i wykorzystuje leniwą instancję.

Nie sądzę, że jest to nadużycie metod enum, nie różni się zbytnio od tego, co zaleca Josh Bloch, Skuteczna Jawa, aby zakodować różne strategie na wyliczenia.

2

Wydaje się być w porządku. Chciałbym dokonać inicjalizacji threadsafe takiego:

enum ComplexItem implements Complex { 
    ITEM1 { 
     protected Complex makeInstance() { 
      return new Complex() { public void method() { }}; 
     } 
    }, 
    ITEM2 { 
     protected Complex makeInstance() { 
      return new Complex() { public void method() { }} 
    }; 

    private volatile Complex instance; 

    private Complex getInstance() { 
     if (instance == null) { 
      createInstance(); 
     } 
     return instance; 
    } 

    protected abstract Complex makeInstance(); 

    protected synchronized void createInstance() { 
     if (instance == null) { 
      instance = makeInstance(); 
     } 
    } 

    public void method() { 
     getInstance().method(); 
    } 
} 

Modyfikator synchronized pojawia się tylko na metodzie createInstance(), ale okłady wezwanie do makeInstance() - przenośniki threadsafety pominięciem wąskie gardło na połączeniach do getInstance() i bez programatora posiadającego pamiętać, aby dodaj synchronized do każdej implementacji makeInstance().

+0

Dzięki. Bardzo podoba mi się pomysł uczynienia z makeInstance abstrakcyjnego: sprawia, że ​​intencja jest bardziej przejrzysta. – sprinter

+0

Czy nie powinno być '== null' w' createInstance() '? – biziclop

+0

@biziclop ummmm yes. Naprawiony. Dzięki. – Bohemian