Gdzie umieścić dane modelu i zachowanie? [tl; dr; Skorzystaj z usług]

341

Współpracuję z AngularJS przy moim najnowszym projekcie. W dokumentacji i samouczkach wszystkie dane modelu są umieszczane w zakresie kontrolera. Rozumiem, że musi to być dostępne dla kontrolera, a zatem w ramach odpowiednich widoków.

Jednak nie sądzę, że model powinien tam zostać rzeczywiście wdrożony. Może być na przykład złożony i mieć prywatne atrybuty. Ponadto ktoś może chcieć ponownie użyć go w innym kontekście / aplikacji. Włożenie wszystkiego do kontrolera całkowicie psuje wzór MVC.

To samo dotyczy zachowania dowolnego modelu. Gdybym użył architektury DCI i oddzieliłby zachowanie od modelu danych, musiałbym wprowadzić dodatkowe obiekty, aby zachować to zachowanie. Odbyłoby się to poprzez wprowadzenie ról i kontekstów.

DCI == D ATA C ollaboration I nteraction

Oczywiście dane i zachowanie modelu można zaimplementować za pomocą zwykłych obiektów javascript lub dowolnego wzorca „klasy”. Ale jaki byłby sposób AngularJS? Korzystasz z usług?

Sprowadza się to do tego pytania:

W jaki sposób wdrażasz modele oddzielone od kontrolera zgodnie z najlepszymi praktykami AngularJS?

Nils Blum-Oeste
źródło
12
Głosowałbym za tym pytaniem, jeśli można zdefiniować DCI lub przynajmniej podać formularz. Nigdy nie widziałem tego akronimu w żadnej literaturze oprogramowania. Dzięki.
Jim Raden,
13
Właśnie dodałem link do DCI jako odniesienie.
Nils Blum-Oeste,
1
@JimRaden DCI to Dataq, Kontekst, interakcja i jest paradygmatem sformułowanym po raz pierwszy przez ojca MVC (Trygve Reenskauge). Jest już sporo literatury na ten temat. Dobra lektura to Coplien i Bjørnvig „Lean architecture”
Rune FS
3
Dzięki. Na dobre lub złe, większość ludzi nie wie nawet o oryginalnej literaturze. Według Google istnieje 55 milionów artykułów na temat MVC, ale tylko 250 000 mówi o MCI i MVC. A na Microsoft.com? 7. AngularJS.org nawet nie wspomina o akronimie DCI: „Twoje wyszukiwanie - strona: angularjs.org dci - nie pasuje do żadnych dokumentów”.
Jim Raden
Obiektami zasobów są w zasadzie modele w Angular.js .. rozszerzam je.
Salman von Abbas,

Odpowiedzi:

155

Powinieneś skorzystać z usług, jeśli chcesz czegoś użytecznego dla wielu kontrolerów. Oto prosty wymyślony przykład:

myApp.factory('ListService', function() {
  var ListService = {};
  var list = [];
  ListService.getItem = function(index) { return list[index]; }
  ListService.addItem = function(item) { list.push(item); }
  ListService.removeItem = function(item) { list.splice(list.indexOf(item), 1) }
  ListService.size = function() { return list.length; }

  return ListService;
});

function Ctrl1($scope, ListService) {
  //Can add/remove/get items from shared list
}

function Ctrl2($scope, ListService) {
  //Can add/remove/get items from shared list
}
Andrew Joslin
źródło
23
Jaka byłaby korzyść z korzystania z usługi, zamiast tworzenia zwykłego obiektu Javascript jako modelu i przypisywania go do zakresu kontrolera?
Nils Blum-Oeste,
22
Ponieważ potrzebujesz tej samej logiki współdzielonej przez wiele kontrolerów. Ponadto w ten sposób łatwiej jest testować rzeczy niezależnie.
Andrew Joslin,
1
Ostatni przykład jest do bani, ten ma więcej sensu. Zredagowałem to.
Andrew Joslin
9
Tak, z prostym, starym obiektem Javascript nie byłbyś w stanie wstrzyknąć niczego Angular do swojej usługi ListService. Podobnie jak w tym przykładzie, jeśli potrzebujesz $ http.get, aby pobrać dane listy na początku, lub jeśli potrzebujesz wstrzyknąć $ rootScope, abyś mógł $ transmitować zdarzenia.
Andrew Joslin
1
Aby ten przykład bardziej przypominał DCI, czy dane nie powinny znajdować się poza ListService?
PiTheNumber
81

Obecnie próbuję tego wzorca, który, choć nie DCI, zapewnia klasyczne oddzielenie usługi / modelu (z usługami do komunikacji z usługami internetowymi (inaczej model CRUD) oraz modelem definiującym właściwości i metody obiektu).

Zauważ, że używam tego wzorca tylko wtedy, gdy obiekt modelu potrzebuje metod działających na własnych właściwościach, których prawdopodobnie będę używać wszędzie (takich jak ulepszony getter / setters). Ja nie promując w ten sposób dla każdej usługi w sposób systematyczny.

EDYCJA: Kiedyś myślałem, że ten wzór byłby sprzeczny z mantrą „Model kątowy to zwykły stary obiekt javascript”, ale wydaje mi się teraz, że ten wzór jest całkowicie w porządku.

EDYCJA (2): Aby być jeszcze jaśniejszym, używam klasy Model tylko po to, aby uwzględnić proste metody pobierające / ustawiające (np .: do użycia w szablonach widoku). W przypadku logiki dużego biznesu zalecam korzystanie z oddzielnych usług, które „wiedzą” o modelu, ale są oddzielone od nich i obejmują tylko logikę biznesową. Jeśli chcesz, możesz to nazwać warstwą usług „ekspertów biznesowych”

service / ElementServices.js (zauważ, jak element jest wstrzykiwany do deklaracji)

MyApp.service('ElementServices', function($http, $q, Element)
{
    this.getById = function(id)
    {
        return $http.get('/element/' + id).then(
            function(response)
            {
                //this is where the Element model is used
                return new Element(response.data);
            },
            function(response)
            {
                return $q.reject(response.data.error);
            }
        );
    };
    ... other CRUD methods
}

model / Element.js (przy użyciu angularjs Factory, stworzony do tworzenia obiektów)

MyApp.factory('Element', function()
{
    var Element = function(data) {
        //set defaults properties and functions
        angular.extend(this, {
            id:null,
            collection1:[],
            collection2:[],
            status:'NEW',
            //... other properties

            //dummy isNew function that would work on two properties to harden code
            isNew:function(){
                return (this.status=='NEW' || this.id == null);
            }
        });
        angular.extend(this, data);
    };
    return Element;
});
Ben G.
źródło
4
Właśnie wchodzę w Angular, ale byłbym ciekawy, czy / dlaczego weterani uważają, że to herezja. Jest to prawdopodobnie sposób, w jaki początkowo do tego podejdę. Czy ktoś może przekazać jakieś uwagi?
Aaronius
2
@Aaronius tylko dla jasności: tak naprawdę nigdy nie czytałem „nigdy nie powinieneś tego robić” na żadnym dokumencie lub blogach angularjs, ale zawsze czytałem takie rzeczy jak „angularjs nie potrzebuje modelu, używa zwykłego starego javascript” i sam musiałem odkryć ten wzór. Ponieważ jest to mój pierwszy prawdziwy projekt na AngularJS, umieszczam te mocne ostrzeżenia, aby ludzie nie kopiowali / wklejali bez myślenia.
Ben G
Postawiłem na mniej więcej podobny wzór. Szkoda, że ​​Angular nie ma żadnego rzeczywistego wsparcia (lub pozornie chęci wspierania) modelu w sensie „klasycznym”.
drt
3
Nie wygląda mi to na herezję, używasz fabryk do tego, do czego zostały stworzone: do budowania obiektów. Uważam, że wyrażenie „angularjs nie potrzebuje modelu” oznacza „nie musisz dziedziczyć po specjalnej klasie ani używać specjalnych metod (takich jak ko.observable, in knockout), aby pracować z modelami w kanale kątowym, a wystarczy czysty obiekt js ".
Felipe Castro
1
Czy posiadanie odpowiednio nazwanej ElementService dla każdej kolekcji nie doprowadziłoby do powstania prawie identycznych plików?
Collin Allen
29

Dokumentacja Angularjs wyraźnie stwierdza:

W przeciwieństwie do wielu innych frameworków, Angular nie wprowadza żadnych ograniczeń ani wymagań dotyczących modelu. Nie ma klas do dziedziczenia ani specjalnych metod dostępu do dostępu lub zmiany modelu. Model może być prymitywny, skrót obiektu lub pełny typ obiektu. Krótko mówiąc, model jest prostym obiektem JavaScript.

- AngularJS Developer Guide - V1.5 Concepts - Model

Oznacza to, że od Ciebie zależy, jak zadeklarować model. To prosty obiekt JavaScript.

Ja osobiście nie będę korzystać z usług Angular, ponieważ miały one zachowywać się jak obiekty singletonowe, których możesz użyć, na przykład, do utrzymania globalnych stanów w twojej aplikacji.

SC
źródło
Powinieneś podać link do miejsca, w którym jest to określone w dokumentacji. Poszukałem w Google hasła „Angular nie nakłada żadnych ograniczeń ani wymagań na model” i, o ile wiem, nigdzie nie pojawia się w oficjalnych dokumentach.
4
to było w starych dokumentach angularjs (ten żywy podczas odpowiadania): github.com/gitsome/docular/blob/master/lib/angular/ngdocs/guide/…
SC
8

DCI jest paradygmatem i jako taki nie ma sposobu na angularJS, albo język obsługuje DCI, albo nie. JS obsługuje DCI raczej dobrze, jeśli chcesz użyć transformacji źródła i ma pewne wady, jeśli tak nie jest. Znowu DCI nie ma nic wspólnego z wstrzykiwaniem zależności, niż twierdzenie, że klasa C # ma i zdecydowanie nie jest usługą. Zatem najlepszym sposobem na wykonanie DCI z angulusJS jest wykonanie DCI w JS, co jest bardzo zbliżone do tego, jak DCI jest formułowane w pierwszej kolejności. O ile nie wykonasz transformacji źródłowej, nie będziesz w stanie tego zrobić w pełni, ponieważ metody roli będą częścią obiektu nawet poza kontekstem, ale generalnie jest to problem z DCI opartym na wstrzykiwaniu metod. Jeśli spojrzysz na fullOO.infoautorytatywną stronę dla DCI, możesz zapoznać się z implementacjami ruby, w których również używają metody wstrzykiwania, lub możesz zajrzeć tutaj, aby uzyskać więcej informacji na temat DCI. Dotyczy to głównie przykładów RUby, ale DCI jest agnostyczny. Jednym z kluczy do DCI jest to, że to, co robi system, jest oddzielone od tego, czym jest system. Tak więc obiekt danych jest dość głupi, ale po powiązaniu z rolą w kontekście metody roli udostępniają określone zachowanie. Rola jest po prostu identyfikatorem, nic więcej, a podczas uzyskiwania dostępu do obiektu za pomocą tego identyfikatora dostępne są metody ról. Nie ma obiektu roli / klasy. W przypadku wstrzykiwania metod ustalanie zakresu metod ról nie jest dokładnie zgodne z opisem, ale jest bliskie. Przykładem kontekstu w JS może być

function transfer(source,destination){
   source.transfer = function(amount){
        source.withdraw(amount);
        source.log("withdrew " + amount);
        destination.receive(amount);
   };
   destination.receive = function(amount){
      destination.deposit(amount);
      destination.log("deposited " + amount);
   };
   this.transfer = function(amount){
    source.transfer(amount);
   };
}
Rune FS
źródło
1
Dzięki za opracowanie DCI. To świetna lektura. Ale moje pytania naprawdę mają na celu „gdzie umieścić obiekty modelu w angularjs”. DCI jest tu tylko dla odniesienia, że ​​mogę nie tylko mieć model, ale podzielić go w sposób DCI. Zmieni pytanie, aby było bardziej jasne.
Nils Blum-Oeste,
7

Ten artykuł o modelach w AngularJS może pomóc:

http://joelhooks.com/blog/2013/04/24/modeling-data-and-state-in-your-angularjs-application/

marianboda
źródło
7
Zwróć uwagę, że odradzane są odpowiedzi tylko do linków , więc odpowiedzi SO powinny być końcowym punktem poszukiwania rozwiązania (w porównaniu z kolejnym postojem referencji, które z czasem stają się nieaktualne). Rozważ dodanie tutaj autonomicznego streszczenia, zachowując link jako odniesienie.
kleopatra
dodanie takiego linku w komentarzu do pytania byłoby jednak w porządku.
jorrebor
Ten link jest w rzeczywistości bardzo dobrym artykułem, ale to i tak musiałoby zostać przetworzone w odpowiedź, aby była odpowiednia dla SO
Jeremy Zerr
5

Jak stwierdzono w innych plakatach, Angular nie zapewnia gotowej klasy bazowej do modelowania, ale można użytecznie udostępnić kilka funkcji:

  1. Metody interakcji z interfejsem API RESTful i tworzenia nowych obiektów
  2. Ustanawianie relacji między modelami
  3. Sprawdzanie poprawności danych przed utrwaleniem w wewnętrznej bazie danych; przydatne również do wyświetlania błędów w czasie rzeczywistym
  4. Buforowanie i leniwe ładowanie, aby uniknąć marnotrawstwa żądań HTTP
  5. Haki automatu stanów (przed / po zapisaniu, aktualizacji, utworzeniu, nowym itp.)

Jedną biblioteką, która dobrze robi wszystkie te rzeczy, jest ngActiveResource ( https://github.com/FacultyCreative/ngActiveResource ). Pełne ujawnienie - napisałem tę bibliotekę - i z powodzeniem wykorzystałem ją do budowy kilku aplikacji na skalę korporacyjną. Jest dobrze przetestowany i zapewnia interfejs API, który powinien być znany programistom Railsów.

Mój zespół i ja nadal aktywnie rozwijamy tę bibliotekę i chciałbym zobaczyć, jak więcej programistów Angular wnosi do niej swój wkład i testuje ją w bitwie.

Kaseta Bretta
źródło
Hej! To jest naprawdę świetne! Podłączę go teraz do mojej aplikacji. Właśnie rozpoczęły się testy bitewne.
J. Bruni
1
Właśnie patrzyłem na twój post i zastanawiałem się, jakie są różnice między usługą twojego ngActiveResourcei Angulara $resource. Jestem trochę nowy w Angular i szybko przejrzałem oba zestawy dokumentów, ale wydają się one nakładać na siebie. Czy został ngActiveResourceopracowany przed udostępnieniem $resourceusługi?
Eric B.,
5

Starsze pytanie, ale myślę, że temat jest bardziej trafny niż kiedykolwiek, biorąc pod uwagę nowy kierunek Angular 2.0. Powiedziałbym, że najlepszą praktyką jest pisanie kodu przy możliwie najmniejszej zależności od konkretnego frameworka. Używaj tylko części specyficznych dla frameworka, jeżeli wnoszą one bezpośrednią wartość.

Obecnie wydaje się, że usługa Angular jest jedną z niewielu koncepcji, które pozwolą jej przejść do następnej generacji Angulara, więc prawdopodobnie rozsądnie jest postępować zgodnie z ogólną wytyczną dotyczącą przenoszenia całej logiki na usługi. Argumentowałbym jednak, że można tworzyć modele odsprzęgnięte, nawet bez bezpośredniej zależności od usług Angular. Tworzenie samodzielnych obiektów z tylko niezbędnymi zależnościami i obowiązkami jest prawdopodobnie dobrym rozwiązaniem. Ułatwia także życie podczas wykonywania testów automatycznych. Pojedyncza odpowiedzialność to w dzisiejszych czasach burzliwa praca, ale ma wiele sensu!

Oto przykład tupotu, który uważam za dobry do oddzielenia modelu obiektowego od dom.

http://www.syntaxsuccess.com/viewarticle/548ebac8ecdac75c8a09d58e

Kluczowym celem jest ustrukturyzowanie kodu w taki sposób, aby korzystanie z testów jednostkowych było równie łatwe, jak z widoku. Jeśli to osiągniesz, masz dobrą pozycję do pisania realistycznych i przydatnych testów.

TGH
źródło
4

Starałem się rozwiązać dokładnie ten problem w tym poście na blogu .

Zasadniczo najlepszym miejscem do modelowania danych są usługi i fabryki. Jednak w zależności od sposobu pobierania danych i złożoności zachowań, których potrzebujesz, istnieje wiele różnych sposobów realizacji. Obecnie Angular nie ma standardowego sposobu ani najlepszych praktyk.

Post obejmuje trzy podejścia, używając $ http , $ resource i Restangular .

Oto przykładowy kod dla każdego z niestandardową getResult()metodą w modelu Job:

Restangular (łatwy peasy):

angular.module('job.models', [])
  .service('Job', ['Restangular', function(Restangular) {
    var Job = Restangular.service('jobs');

    Restangular.extendModel('jobs', function(model) {
      model.getResult = function() {
        if (this.status == 'complete') {
          if (this.passed === null) return "Finished";
          else if (this.passed === true) return "Pass";
          else if (this.passed === false) return "Fail";
        }
        else return "Running";
      };

      return model;
    });

    return Job;
  }]);

$ zasób (nieco bardziej skomplikowany):

angular.module('job.models', [])
    .factory('Job', ['$resource', function($resource) {
        var Job = $resource('/api/jobs/:jobId', { full: 'true', jobId: '@id' }, {
            query: {
                method: 'GET',
                isArray: false,
                transformResponse: function(data, header) {
                    var wrapped = angular.fromJson(data);
                    angular.forEach(wrapped.items, function(item, idx) {
                        wrapped.items[idx] = new Job(item);
                    });
                    return wrapped;
                }
            }
        });

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    }]);

$ http (hardcore):

angular.module('job.models', [])
    .service('JobManager', ['$http', 'Job', function($http, Job) {
        return {
            getAll: function(limit) {
                var params = {"limit": limit, "full": 'true'};
                return $http.get('/api/jobs', {params: params})
                  .then(function(response) {
                    var data = response.data;
                    var jobs = [];
                    for (var i = 0; i < data.objects.length; i ++) {
                        jobs.push(new Job(data.objects[i]));
                    }
                    return jobs;
                });
            }
        };
    }])
    .factory('Job', function() {
        function Job(data) {
            for (attr in data) {
                if (data.hasOwnProperty(attr))
                    this[attr] = data[attr];
            }
        }

        Job.prototype.getResult = function() {
            if (this.status == 'complete') {
                if (this.passed === null) return "Finished";
                else if (this.passed === true) return "Pass";
                else if (this.passed === false) return "Fail";
            }
            else return "Running";
        };

        return Job;
    });

Sam post na blogu zawiera bardziej szczegółowe informacje na temat uzasadnienia zastosowania każdego z tych podejść, a także przykłady kodu użycia modeli w kontrolerach:

Modele danych AngularJS: $ http VS $ zasób VS Restangular

Istnieje możliwość, że Angular 2.0 zaoferuje bardziej niezawodne rozwiązanie do modelowania danych, dzięki któremu wszyscy znajdą się na tej samej stronie.

Alan Christopher Thomas
źródło