Problem z podziałem funkcjonalności na moduły

0

Ciężko jakkolwiek nazwać ten temat, bo nie wiem jak opisać mój problem w kilku słowach.

Otóż piszę managera plików na Androida. Mam klasę Explorer, która stanowi interfejs dla różnych typów eksploratora, np FileExplorer, FTPExplorer, ZipExplorer, itd... Dzięki temu mam ładnie oddzielone poszczególne moduły a GUI nie ma pojęcia na czym operuje, wie tylko że to jakiś eksplorator. Program posiada dwa panele, podobnie jak TotalCommander. Każdy panel to oddzielny obiekt implementujący klasę Explorer. Jako schowek (operacje kopiuj/wklej) służy mi singleton, który przechowuje tablicę Object[] tak, aby każdy typ pliku do niego pasował (File, FTPFile, ZipFile, itd...).
I tu zaczyna się problem:
Wszystko jest fajnie dopóki na obu panelach jest taki sam typ Explorer, np. na obu panelach FileExplorer. W jednym panelu klikam kopiuj, do schowka lecą obiekty File, na drugim panelu wklejam, implementacja wie co zrobić z obiektami File.
Problemem jest sytuacja gdy dwa panele posiadają dwa rózne typy Explorera. Powiedzmy, że na jednym jest FTPExplorer a na drugim FileExplorer. Kilkam kopiuj w FTPExplorer, do schowka lecą obiekty FTPFile a na drugim panelu (FileExplorer) daję wklej, no ale FileExplorer nie wie co to jest FTPFile bo on operuje tylko na File więc operacji wklejenia nie da się wykonać.

Potrzebuję czegoś takiego, aby FileExplorer wykonał operację wklejania w inny sposób niż dla File ( w tym przypadku pobrał plik) a jednocześnie chciałbym uniknąć powiązań między modułami, tzn czegoś w stylu "if isFTPFile", "if isFile" itd... FileExplorer ma być do plików lokalnych, FTPExplorer do ftp i do niczego innego.

Mam nadzieje że jasno to opisałem :)

Moje pytanie: czy ktoś spotkał się już z takim problemem i wie jak go rozwiązać?

1

operator instanceof:

if(plik instanceof ZipFile){
}

Lepszy sposób:
Zrób klasę abstrakcyjną, z której dziedziczą te wszystkie typy plików i daj jej potrzebne metody do przenoszenia pliku. Potem zamiast Object użyj po prostu tej klasy abstrakcyjnej. W końcu się da przypisać instancję klasy dziedziczącej do klasy dziedziczonej (nawet abstrakcyjnej).

Być może przydałyby ci się też takie metody "rzutujące" jak toFtpFile, toZipFile itd. wywoływane z konkretnych menedżerów plików żeby uniknąć ciągłego sprawdzania zgodności typu.

0

Właśnie chcę uniknąć sprawdzania typu, bo to wprowadzi powiązania między klasami.

Wspólny interfejs dla plików również nie rozwiązuje problemu bo co z tego, że wszystkie pliki będą traktowane tak samo, jeśli w jednej sytuacji (dwa identyczne panele) wykonuję operację lokalną a w innej (dwa rózne panele) między dwoma różnymi systemami plików, np download z FTP. Wspólny interfejs pliku tu nie pomoże, raczej jakiś wspólny obiekt akceptowalny przez wszystkie klasy implementujące Explorer, który stwierdza obecną sytuację i wykonuje stosowną operację ( zwykłe kopiowanie lub download z serwera idąc już tokiem FTP ). Ale to znowu wprowadzi powiązanie, bo jeśli paste() zostało wywołane w FileExplorer, a okaże się, że w schowku są FTPFile, które trzeba pobrać, to trzeba wykonać operację nie z klasy FileExplorer, ale FTPExplorer. Im więcej będzie modułów, tym więcej możliwości ( a może spakowanie do zipa, lub też kopiowanie do lokalizacji w sieci lokalnej ). Zrobi się to trochę skomplikowane:/

Ogólnie moja koncepcja jest taka, że klasa GUI reaguje na kliknięcie paste i dla aktualnego panelu wywołuje metodę Explorer[currentPanel].paste(), nie obchodzi ją jak to zostanie wykonane. Dzięki temu bedę mógł tworzyć nowe moduły bez zmiany czegokolwiek w klasie GUI bo wszystkie będą traktowane tak samo. Jeśli w konkretnym Explorerze będę sprawdzał instancje typów innych modułów to ten cały podział będzie bez sensu bo jeden moduł będzie powiązany z resztą.

Nie mam pojęcia co zrobić, żeby było to porządnie napisane a nie po lamersku....

1

Musisz jakos tak zapisywac, aby kazdy modul umial to odczytac. Ja bym probowal jakos tak ze zrobilbym klase StoredFile, ktory ma nazwe (niezalezna od typu exploratora, czyli np nie ftp://sciezka, tylko sciezka) oraz tablice bajtow z danymi.
Ma to ten minus ze musisz wczytac caly plik do pamieci, co moze byc kiepskie.
Inna opcja to taka, ze ten StoredFile zamist wczesnie wczytanej tablicy bajtow ma interfejs powiedzmy ReadCallback, ktory ma jedna metode getContents(): byte[] (jeszcze lepiej, jakby ten interfejs byl swiadomy strumieniowania, aby moc wczytywac wilkie pliki, typu void copyTo(OutputStream out), ktora to metoda bylaby wywolywana przy wykonaniu operacji wklej. Teraz, kazdy explorator uzywa tego samego typu StoredFile, ale z inna implementacja tego ReadCallback. W ten sposob opozniasz i strumieniujesz odczyt plikow dopiero gdy jest potrzeba. Po wczytaniu mozesz cachowac dane aby nie byly wczytywane ponownie, a zeby zapobiec out of memory (gdy kopiujesz i wklejasz wiele plikow) mozesz uzyc SoftRefernces do danych, a metoda wczytujaca dane sprawdza czy dane nadal sa w pamieci czy juz nie, i ewentualnie wczytuje ponownie.
W ten sposob wydaje mi sie ze mialbys czyste i skalowanlne rozwiazanie (obsluga wielkich plikow, obsluga wielu plikow).

1

Skoro masz metodę paste, to ta metoda paste w np. ZipExplorer może wywołać file.toZipFile(). Albo inaczej: zrobić nowy ZipFile, tyle że w konstruktorze przekazujesz referencje do pliku, który ma skopiować. Przykład:

class ZipFile extends UniFile{
  public ZipFile(UniFile src, String dest){
    setContent(src.getContent()); //skopiuj treść
    setName(src.getName()); //skopiuj nazwę
    this.dest=dest;
  }
}

i w tym paste() w ZipExplorer:

List<ZipFile> toAdd=new ArrayList<ZipFile>();
for(UniFile file: clipboard){
  toAdd.add( new ZipFile(file, this.getCurrentPath()) );
}

Potem właściwe wklejanie do pliku zip.

Głównie chodzi o to, żeby mieć dostęp do metody dzięki której będziesz mógł pobrać treść, nazwę, ścieżkę uniwersalnie, niezależnie od "protokołu".

0

To, co piszecie ma nawet sens. Przemyślę to jeszcze i postaram się to zrobić. O efektach dam znać. Oczywiście wszelkie inne propozycje nadal mile widziane:)

1

Oprócz tego co już podpowiedzieli koledzy - czyli zrobienia uniwersalnego, abstrakcyjnego obiektu reprezentującego abstrakcyjny odnośnik do pliku - moim zdaniem powinieneś z każdym eksploratorem związać konkretny system plików. Na przykład odczyt listy plików z dysku twardego jest mało kosztowny, ale w przypadku ftp koszt takiego odczytania jest już znaczny i może się nie powieść. Krótko mówiąc powinieneś z abstrakcyjnym eksplorerem związać też abstrakcyjny system plików przechowujący abstrakcyjne pliki. Tylko wtedy cały program będzie wystarczająco elastyczny tak jak to sobie obmyśliłeś.

Jest jeszcze sprawa operacji na plikach, które może bezpośrednio wykonywać Twój program, ale moim zdaniem znacznie lepiej i wydajniej jest delegować to zadanie na system operacyjny (przynajmniej część z nich - kopiowanie, przenoszenie itp.). Na przykład wiele systemów obsługuje kopiowanie plików w swoim API. Wystarczy odpowiedniej funkcji systemowej przekazać tylko plik lub listę plików źródłowych i miejsce docelowe (w formacie akceptowalnym przez niego). System zrobi to szybciej, uniwersalniej i o wiele porządniej niż sam byłbyś to w stanie napisać (a nawet podrzuci notyfikacje o postępie operacji).
Na przykład w Windows system obsłuży kopiowanie mieszane po każdym systemie plików jakie obsługuje, w tym po zasobach podanych jako ścieżki UNC oraz w każdym formacie obsługiwanym przez ładowalne usługi i sterowniki, takie jak CD, DVD, wirtualne napędy itd. Samodzielnie nie jesteś w stanie tego poprawnie obsłużyć (czas, wiedza), a delegowanie takiej operacji systemowi jest proste i skuteczne. Kopiowanie bezpośrednio przez Twój program nie dość, że komplikuje go o obsługę buforowania i rzeczy zależnych wyłącznie od systemu/sprzętu, to na dodatek tak skopiowany plik, albo będzie miał bieżącą datę (co jest np. w Windows błędem), albo zmodyfikowany czas ostatniego dostępu, albo nie skopiowane takie atrybuty jak prawa dostępu (prawa dostępu Javy są bardzo ogólne i biedne). Dodatkowo może okazać się, że plik, który Java podaje jako dostępny do otwarcia, czasem albo trwale może być przestać dostępny z powodów, których JVM nie może się dowiedzieć (np. blokowanie przed skasowaniem tymczasowo zablokowanego pliku), a system sobie z tym poradzi i doprowadzi operację do końca.

0

Na Androidzie wygląda to zupełnie inaczej, nie mam dostępu do API niższego poziomu. Jedyne co mogę zrobić to otworzenie konsoli i wydawanie poleceń ale to działa tylko na zrootowanych telefonach ( aktualnie w ten sposób umożliwiam przeglądanie katalogów systemowych z prawami roota ).

@Olamagato, nie wiem co masz na myśli mówiąc o abstrakcyjnym systemie plików, mógłbyś to wyjaśnić?.

Co do wcześniejszych propozycji jest pewien problem. To co opisaliście nadaje się bardzo dobrze do kopiowania, jednak:

  1. Są jeszcze proste operacje, które nie wymagają przetrzymywania zawartości pliku. O ile miałoby to sens dla przenoszenia (nie kopiowania) pliku z ftp na telefon, to ok trzeba skopiować zawartość i usunąć źródło. Jednak przy przenoszeniu pliku w obrębie tego samego systemu wystarczy zmienić nazwę. A mój uniwersalny interfejs zrobi to kopiując zawartość i usuwając źródło.

  2. Aktualnie korzystam z biblioteki FTP Apache Commons. Gdybym chciał to zrobić tak jak opisaliście musiałbym zaimplementować FTP po swojemu. W Apache Commons jest to tak zrobione, że wszelkie operacje wykonuje obiekt FTPClient a FTPFile tylko reprezentuje plik ( a konkretnie jest to sparsowana linijka z operacji ls ), w przeciwieństwie do lokalnego systemu gdzie klasa File pozwala na operacje na nim. Jeśli chciałbym zastosować wasz pomysł musiałbym w tym swoim UniFile dać referencje do FTPClient, żeby móc operować na tym pliku tak jak na File. Wprowadzi to powiązanie FTPFile -> FTPClient które nie powinno mieć miejsca.

Problem wydaje się bardziej skomplikowany niż myslalem....

1

Klasa np. ZipFile wcale nie musi przechowywać treści mimo obecnej gotowej do użytku metody getContent(). Niech klasa pliku zawiera tylko informacje o położeniu, a metody na tej informacji niech operują. getContent dopiero niech zbuforuje i zwróci zawartość i usunie ją gdy nie będzie potrzebna (np. metodą free()).

Przy przenoszeniu w obrębie danego systemu plików po prostu sprawdź instancję, czy jest zgodna z daną "przeglądarką". Przyda się też w klasie pliku referencja do tej "przeglądarki" i jakieś oznaczenie systemu plików, żeby można było łatwo zidentyfikować, czy to ten sam system/przeglądarka (chyba że masz dwa "okna" w tym samym systemie).

1

Do problemu tych samych systemow plikow, to prosta sprawa - sprawdzasz czy cel i zrodlo jest to samo i wykonujesz optymalny kod dla tego typu.
Nie zrozumiales tych propozycji ktore zostaly przedstawione. Twoj UniFile nie ma miec ani FTPFile ani nic, ma miec string (nazwa) oraz callback ktory jest specyficzny dla systemu. W tym calbacku dla FTP bierzesz nazwe pliku i tworzysz FTPFile czy bog wie co tam chcesz, i wczytujesz. To samo dla lokalnego systemu plikow - Twoj callback bierze nazwe, tworzy nowy File i robisz co ci sie podoba - na zawolanie, nie wczesniej. Nie wiem jak moge to latwiej wyjasnic, ale to dziala jak przemyslisz.

1
glodos napisał(a)

@Olamagato, nie wiem co masz na myśli mówiąc o abstrakcyjnym systemie plików, mógłbyś to wyjaśnić?.

Pewnie. Chodzi o to, że jedynym uniwersalnym założeniem może być to, że każdy system plików przechowuje pliki. Właściwie powinno się zakładać ani istnienia katalogów, ani ich struktury drzewiastej. Takie założenia mogą istnieć tylko w obrębie konkretnego systemu plików. Java próbuje uogólnić wszystkie systemy plików, ale zakłada, że JVM działa w środowisku tylko jednego z nich (skonkretyzowanego). Na przykład nie ma możliwości aby dwie ścieżki takie jak "d:\Dokumenty\tekst.txt" i "/pub/docs/tekst.txt" były jednocześnie poprawnymi i kanonicznymi ścieżkami dostępu do plików w czasie jednego uruchomienia pojedynczej aplikacji. Musisz ten fakt uwzględniać w swoim managerze.

0

Dzięki wielkie za wasze podpowiedzi. Ostatecznie wybrałem następującą opcję:

Mam interfejs CBItem, który pozwala na pobranie InputStream z pliku.
Klasa implementująca ten interfejs rozszerza też któryś plik. Wygląda to mniej więcej tak:

public class MyFile extends File implements CBItem {
    //metoda z CBItem
    public InputStream getData(){
        return new FileInputStream(this);
    }
}

Jeśli jestem w eksploratorze plików lokalnych a CBItem jest instancją File to rzutuję mój obiekt na File i wykonuję operację taka jak do tej pory.
Podobnie jeśli jestem w eksploratorze FTP a CBItem jest instancją FTPFile.
Jeśli typ pliku jest != File, powiedzmy że jestem w eksploratorze lokalnym a mam w schowku FTPFile, to korzystam z metody getData() która daje mi dla niego InputStream. Kiedy mam już input stream, to czytam z niego jak z każdego innego streamu, w wyniku czego pobieram plik nie martwiąc się skąd on pochodzi.

Tego mi było trzeba, w sumie to nie jest jakaś wielka filozofia ale nie mogłem trafić na odpowiedni trop.
Dzięki jeszcze raz za pomoc, mam nadzieję że temat przyda się też innym:)

1 użytkowników online, w tym zalogowanych: 0, gości: 1