DDD Uygulama Hizmetleri ile REST API'si arasında kavramsal uyumsuzluk


20

Karmaşık bir iş alanı ve bir REST API (kesinlikle REST değil, kaynak odaklı) desteklemek için bir gereksinimi olan bir uygulama tasarlamaya çalışıyorum. Etki alanı modelini kaynak odaklı bir şekilde ortaya çıkarmanın bir yolunu bulmakta zorlanıyorum.

DDD'de, bir etki alanı modelinin müşterilerinin Varlıklar ve Etki Alanı Hizmetleri tarafından uygulanan herhangi bir işlevsellike erişmek için yordamsal 'Uygulama Hizmetleri' katmanından geçmesi gerekir. Örneğin, bir Kullanıcı varlığını güncellemek için iki yöntem içeren bir uygulama hizmeti vardır:

userService.ChangeName(name);
userService.ChangeEmail(email);

Bu Uygulama Hizmetinin API'si, durumu değil komutları (fiiller, prosedürler) gösterir.

Ancak aynı uygulama için RESTful API sağlamamız gerekirse, şöyle görünen bir Kullanıcı kaynak modeli vardır:

{
name:"name",
email:"email@mail.com"
}

Kaynak yönelimli API komutları değil durumu gösterir . Bu, aşağıdaki endişeleri ortaya çıkarır:

  • bir REST API'sine karşı her güncelleme işlemi, kaynak modelde hangi özelliklerin güncellenmekte olduğuna bağlı olarak bir veya daha fazla Uygulama Hizmeti prosedür çağrısıyla eşleşebilir

  • her güncelleme işlemi REST API istemcisine atom gibi görünür, ancak böyle uygulanmaz. Her Uygulama Hizmeti çağrısı ayrı bir işlem olarak tasarlanmıştır. Bir kaynak modeldeki bir alanı güncellemek, diğer alanlar için doğrulama kurallarını değiştirebilir. Bu nedenle, tüm olası Uygulama Hizmeti çağrılarının yapılmaya başlamadan önce geçerli olduğundan emin olmak için tüm kaynak modeli alanlarını birlikte doğrulamamız gerekir. Bir komut kümesini bir kerede doğrulamak, bir kerede bir komut yapmaktan daha az önemsizdir. Tek tek komutların var olduğunu bile bilmeyen bir istemcide bunu nasıl yapabiliriz?

  • Uygulama Hizmeti yöntemlerini farklı sırayla çağırmak farklı bir etkiye sahip olabilirken, REST API bunu bir fark yokmuş gibi gösterir (tek bir kaynakta)

Daha benzer konularla karşılaşabilirim, ama temelde hepsine aynı şey neden oluyor. Bir Uygulama Hizmetine her çağrıdan sonra, sistemin durumu değişir. Geçerli değişikliğin kuralları, bir işletmenin bir sonraki değişikliği yapabileceği eylemler kümesi. Kaynak odaklı bir API, hepsini atomik bir işlem gibi göstermeye çalışır. Ancak bu boşluğu geçmenin karmaşıklığı bir yere gitmeli ve çok büyük görünüyor.

Ek olarak, kullanıcı arayüzü daha fazla komut odaklıysa, genellikle durum budur, o zaman istemci tarafında komutlar ve kaynaklar arasında ve ardından API tarafında geri gitmemiz gerekir.

Sorular:

  1. Tüm bu karmaşıklık sadece (kalın) REST-AppService eşleme katmanıyla mı ele alınmalı?
  2. Yoksa DDD / REST anlayışımda bir şey mi eksik?
  3. REST, etki alanı modellerinin işlevselliğini belirli (oldukça düşük) bir karmaşıklık derecesi üzerinde göstermek için pratik olmayabilir mi?


REST istemcisini sistemin bir kullanıcısı olarak düşünün. Sistemin gerçekleştirdiği eylemleri NASIL gerçekleştirdiği konusunda hiçbir şey umursamazlar. Artık REST istemcisinin alan adındaki tüm farklı işlemleri bir kullanıcıdan beklediğinizden daha fazla bilmesini beklemezsiniz. Dediğiniz gibi, bu mantık bir yere gitmeli, ancak herhangi bir sistemde bir yere gitmek zorunda kalacaktı, eğer REST'i kullanmasaydınız, onu istemciye taşıyordunuz. Bunu yapmamak tam olarak REST'in noktasıdır, istemci sadece durumu güncellemek istediğini bilmeli ve bu konuda nasıl bir fikriniz olmadığına dair hiçbir fikre sahip olmamalıdır.
Cormac Mulhall

2
@astr Basit yanıt, kaynakların sizin modeliniz olmamasıdır, bu nedenle kaynak işleme kodunun tasarımı, modelinizin tasarımını etkilememelidir. Kaynaklar, modelin dahili olduğu gibi sistemin dışa dönük bir yönüdür. Kaynakları, kullanıcı arayüzünü düşündüğünüz şekilde düşünün. Kullanıcı, kullanıcı arayüzündeki tek bir düğmeyi tıklayabilir ve modelde yüz farklı şey olabilir. Bir kaynağa benzer. İstemci bir kaynağı günceller (tek bir PUT deyimi) ve modelde milyonlarca farklı şey olabilir. Modelinizi kaynaklarınızla yakından eşleştirmek bir anti-modeldir.
Cormac Mulhall

1
Bu, alan adınızdaki eylemleri REST durumu değişikliklerinin yan etkileri olarak ele alma, alan adınızı ve webinizi
Cormac Mulhall

1
Ben de "robot / devlet makinesi olarak kullanıcı" şey hakkında emin değilim. Bence kullanıcı arayüzlerimizi bundan çok daha doğal hale getirmek için çaba göstermeliyiz ...
guillaume31

Yanıtlar:


10

Ben aynı sorunu yaşadım ve REST kaynakları farklı modelleme ile "çözüldü", örneğin:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Temelde daha büyük, karmaşık kaynağı birkaç küçük kaynağa ayırdım. Bunların her biri, orijinal kaynağın birlikte işlenmesi beklenen bir şekilde birbirine bağlı nitelikler grubu içerir.

Bu kaynaklardaki her işlem atomiktir, ancak birkaç hizmet yöntemi kullanılarak uygulanabilse de - en azından Spring / Java EE'de, başlangıçta kendi işlemlerine sahip olması amaçlanan çeşitli yöntemlerden (GEREKLİ işlem kullanılarak) daha büyük işlem oluşturmak sorun değildir yayılma). Bu özel kaynak için genellikle ekstra doğrulama yapmanız gerekir, ancak nitelikler uyumlu olduğu için hala oldukça yönetilebilir.

Bu, HATEOAS yaklaşımı için de iyidir, çünkü daha ayrıntılı kaynaklarınız onlarla neler yapabileceğiniz hakkında daha fazla bilgi taşır (bu mantığın hem istemci hem de sunucuda olması yerine kaynaklarda kolayca temsil edilemez).

Tabii ki mükemmel değil - eğer UI'ler bu kaynaklar göz önünde bulundurularak modellenmemişse (özellikle veri odaklı UI'ler), bazı problemler yaratabilir - örneğin UI, verilen kaynakların (ve alt kaynaklarının) tüm özelliklerinin büyük bir biçimini sunar ve hepsini düzenleyin ve bir kerede kaydedin - müşteri birkaç kaynak işlemini (kendileri atomik olmakla birlikte tüm dizi atomik değildir) çağırsa bile, bu atomisite yanılsaması yaratır.

Ayrıca, bu kaynak ayrımı bazen kolay veya açık değildir. Bunu esas olarak karmaşıklığını yönetmek için karmaşık davranışlara / yaşam döngülerine sahip kaynaklar üzerinde yapıyorum.


Ben de öyle düşünüyordum - daha ayrıntılı kaynak gösterimleri yaratın çünkü yazma işlemleri için daha uygunlar. Bu kadar ayrıntılı hale geldiklerinde kaynak sorgulamayı nasıl ele alırsınız? Ayrıca salt okunur, normalleştirilmemiş gösterimler mi oluşturuyorsunuz?
astreltsov

1
Hayır, salt okunur normalleştirilmiş temsillerim yok. Kullandığım jsonapi.org standart ve verilen kaynağın cevaben ilgili kaynaklar dahil etmek için bir mekanizmaya sahiptir. Temelde "Kullanıcı ID 1 ile verin ve alt kaynak e-posta ve aktivasyonunu da dahil et" diyorum. Bu, alt kaynaklar için ekstra REST çağrılarından kurtulmaya yardımcı olur ve iyi bir JSON API istemci kitaplığı kullanırsanız, alt kaynaklar ile uğraşan istemcinin karmaşıklığını etkilemez.
qbd

Sunucudaki tek bir GET isteği, daha sonra tek bir kaynak nesnesine birleştirilen bir veya daha fazla gerçek sorguya (kaç tane alt kaynağın dahil edildiğine bağlı olarak) dönüşür?
astreltsov

Birden fazla yuvalama seviyesi gerekiyorsa ne olur?
astreltsov

Evet, ilişkisel dbs'de bu muhtemelen birden fazla sorguya dönüşecektir. Keyfi yuvalama JSON API tarafından desteklenir, burada açıklanmıştır: jsonapi.org/format/#fetching-includes
qbd

0

Buradaki temel sorun, REST çağrısı yapıldığında iş mantığı şeffaf bir şekilde nasıl çağrılıyor? Bu doğrudan REST tarafından ele alınmayan bir sorundur.

Bunu, JPA gibi bir kalıcılık sağlayıcısı üzerinde kendi veri yönetimi katmanımı oluşturarak çözdüm. Özel ek açıklamalara sahip bir meta model kullanarak varlık durumu değiştiğinde uygun iş mantığını çağırabiliriz. Bu, işletme devletinin iş mantığını nasıl değiştirdiğine bakılmaksızın çağrılmasını sağlar. Mimarinizi KURU ve aynı zamanda iş mantığınızı tek bir yerde tutar.

Yukarıdaki örneği kullanarak, REST kullanılarak ad alanı değiştirildiğinde validateName adlı bir iş mantığı yöntemini çağırabiliriz:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Emrinizde böyle bir araçla, tek yapmanız gereken iş mantığı yöntemlerinize uygun bir açıklama eklemek.


0

Etki alanı modelini kaynak odaklı bir şekilde ortaya çıkarmanın bir yolunu bulmakta zorlanıyorum.

Etki alanı modelini kaynak odaklı bir şekilde göstermemelisiniz. Uygulamayı kaynak odaklı bir şekilde sergilemelisiniz.

kullanıcı arayüzü daha komut odaklıysa, genellikle durum böyle ise, istemci tarafında komutlar ve kaynaklar arasında ve ardından API tarafında geri eşleşmemiz gerekir.

Hiç de değil - komutları etki alanı modeliyle arabirim oluşturan uygulama kaynaklarına gönderin.

bir REST API'sine karşı her güncelleme işlemi, kaynak modelde hangi özelliklerin güncellenmekte olduğuna bağlı olarak bir veya daha fazla Uygulama Hizmeti prosedür çağrısıyla eşleşebilir

Evet, bunu hecelemenin biraz daha farklı bir yolu olsa da, işleri daha kolay hale getirebilir; bir REST api'sine karşı her güncelleme işlemi, komutları bir veya daha fazla toplamaya gönderen bir işlemle eşleşir.

her güncelleme işlemi REST API istemcisine atom gibi görünür, ancak böyle uygulanmaz. Her Uygulama Hizmeti çağrısı ayrı bir işlem olarak tasarlanmıştır. Bir kaynak modeldeki bir alanı güncellemek, diğer alanlar için doğrulama kurallarını değiştirebilir. Bu nedenle, tüm olası Uygulama Hizmeti çağrılarının yapılmaya başlamadan önce geçerli olduğundan emin olmak için tüm kaynak modeli alanlarını birlikte doğrulamamız gerekir. Bir komut kümesini bir kerede doğrulamak, bir kerede bir komut yapmaktan daha az önemsizdir. Tek tek komutların var olduğunu bile bilmeyen bir istemcide bunu nasıl yapabiliriz?

Burada yanlış kuyruğun peşindesiniz.

Hayal edin: REST'i tamamen resimden çıkarın. Bunun yerine, bu uygulama için bir masaüstü arayüzü yazdığınızı düşünün. Dahası, gerçekten iyi tasarım gereksinimleriniz olduğunu ve göreve dayalı bir kullanıcı arayüzü uyguladığınızı düşünelim. Böylece kullanıcı, çalıştığı görev için mükemmel bir şekilde ayarlanmış minimalist bir arayüz alır; kullanıcı bazı girişleri belirtir ve sonra "VERB!" buton.

Şimdi ne olacak? Kullanıcı açısından bakıldığında, bu tek bir atomik görevdir. DomainModel perspektifinden bakıldığında, her komutun ayrı bir işlemde çalıştırıldığı, toplamalar tarafından çalıştırılan bir dizi komuttur. Bunlar tamamen uyumsuz! Boşluğu doldurmak için ortada bir şeye ihtiyacımız var!

Bir şey "uygulama" dır.

Mutlu yolda, uygulama bazı DTO alır ve anladığı bir ileti almak için bu nesneyi ayrıştırır ve iletideki verileri bir veya daha fazla toplama için iyi biçimlendirilmiş komutlar oluşturmak için kullanır. Uygulama, kümelere gönderdiği komutların her birinin iyi biçimlendirildiğinden (işyerinde yolsuzlukla mücadele katmanı) emin olur ve kümeleri yükler ve işlem başarıyla tamamlanırsa kümeleri kaydeder. Toplama, geçerli durumu göz önüne alındığında komutun geçerli olup olmadığına kendisi karar verecektir.

Olası sonuçlar - tüm komutlar başarılı bir şekilde çalışıyor - yolsuzlukla mücadele katmanı mesajı reddediyor - bazı komutlar başarıyla çalışıyor, ancak daha sonra kümelerden biri şikayet ediyor ve hafifletme olasılığınız var.

Şimdi, bu uygulamayı oluşturduğunuzu düşünün; RESTful şekilde nasıl etkileşime giriyorsunuz?

  1. İstemci, hiper ortam denetimleri de dahil olmak üzere geçerli durumunun (yani görev temelli UI) bir hiper ortam tanımıyla başlar.
  2. İstemci kaynağa bir görevin (yani: DTO) bir temsilini gönderir.
  3. Kaynak, gelen HTTP isteğini ayrıştırır, temsili tutar ve uygulamaya aktarır.
  4. Uygulama görevi çalıştırır; Kaynak açısından bakıldığında, bu, aşağıdaki sonuçlardan birine sahip bir kara kutudur
    • uygulama tüm toplamaları başarıyla güncelledi: kaynak istemciye başarıyı rapor ederek yeni bir uygulama durumuna yönlendiriyor
    • yolsuzlukla mücadele katmanı iletiyi reddeder: kaynak, istemciye bir 4xx hatası bildirir (muhtemelen Kötü İstek) ve muhtemelen karşılaşılan sorunun açıklamasını iletir.
    • uygulama bazı toplamaları günceller: kaynak istemciye komutun kabul edildiğini bildirir ve istemciyi komutun ilerlemesinin bir gösterimini sağlayacak bir kaynağa yönlendirir.

Uygulama, bir iletinin işlenmesini istemciye yanıt verene kadar erteleyeceği zamanki olağan dışa vurma kabul edilir - genellikle eşzamansız bir komutu kabul ederken kullanılır. Ancak atomik olması gereken bir operasyonun hafifletilmesi gereken bu durum için de iyi çalışır.

Bu deyimde, kaynak görevin kendisini temsil eder - görev kaynağına uygun temsili göndererek görevin yeni bir örneğini başlatırsınız ve bu kaynak uygulama ile arabirim oluşturur ve sizi bir sonraki uygulama durumuna yönlendirir.

In , birden komutları koordine edilmektedir hemen hemen her zaman, siz (destan aka iş süreci aka) bir sürecin açısından düşünmek istiyorum.

Okuma modelinde benzer bir kavramsal uyumsuzluk var. Yine, göreve dayalı arayüzü düşünün; görev birden fazla toplama değiştirmeyi gerektiriyorsa, görevi hazırlamak için kullanıcı arayüzü muhtemelen birkaç toplamadan veri içerir. Kaynak şemanızda toplamlar 1: 1 ise, bunun düzenlenmesi zor olacaktır; bunun yerine, yukarıda tartışıldığı gibi "görevi başlat" ilişkisini görev bitiş noktasıyla eşleştiren bir hiper ortam denetimi ile birlikte birkaç toplamadan verilerin bir gösterimini döndüren bir kaynak sağlayın.

Ayrıca bakınız: Jim Webber tarafından Pratikte REST .


Alan adımızla kullanım durumlarımıza göre etkileşimde bulunmak için API'yi tasarlıyorsak .. Neden şeyleri Sagas'a hiç ihtiyaç duyulmayacak şekilde tasarlamıyorsunuz? Belki bir şey eksik ama cevabınızı okuyarak REST'in DDD ile iyi bir eşleşme olmadığını ve uzaktan prosedürleri (RPC) kullanmak daha iyi olduğuna inanıyorum. DDD davranış merkezli, REST ise http-fiil merkezli. Neden REST'i resimden kaldırmamalı ve API'deki davranışı (komutlar) ortaya çıkarmıyorsunuz? Sonuçta, muhtemelen kullanım senaryolarını tatmin edecek şekilde tasarlandılar ve problar işlemsel. Kullanıcı arayüzüne sahipsek REST'in avantajı nedir?
iberodev
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.