Düz Eski Yakut Nesneleriyle Şık Ray Bileşenleri Oluşturun
Yayınlanan: 2022-03-11Web siteniz çekiş kazanıyor ve hızla büyüyorsunuz. Ruby/Rails, seçtiğiniz programlama dilidir. Ekibiniz daha büyük ve Rails uygulamalarınız için bir tasarım stili olarak "şişman modeller, sıska kontrolörler"den vazgeçmişsiniz. Ancak yine de Rails'i kullanmaktan vazgeçmek istemezsiniz.
Sorun yok. Bugün, kodunuzu daha temiz, daha yalıtılmış ve daha ayrıştırılmış hale getirmek için OOP'nin en iyi uygulamalarını nasıl kullanacağınızı tartışacağız.
Uygulamanız Yeniden Düzenlemeye Değer mi?
Uygulamanızın yeniden düzenleme için iyi bir aday olup olmadığına nasıl karar vermeniz gerektiğine bakarak başlayalım.
Kodumun yeniden düzenlenmesi gerekip gerekmediğini belirlemek için genellikle kendime sorduğum metriklerin ve soruların bir listesi burada.
- Yavaş birim testleri. PORO birim testleri genellikle iyi izole edilmiş kodla hızlı çalışır, bu nedenle yavaş çalışan testler genellikle kötü bir tasarımın ve aşırı birleştirilmiş sorumlulukların göstergesi olabilir.
- FAT modelleri veya kontrolörleri. 200'den fazla kod satırına (LOC) sahip bir model veya denetleyici genellikle yeniden düzenleme için iyi bir adaydır.
- Aşırı büyük kod tabanı. 30.000'den fazla LOC içeren ERB/HTML/HAML veya 50.000'den fazla LOC içeren Ruby kaynak kodunuz (GEM'ler olmadan) varsa, yeniden düzenleme yapmanız için iyi bir şans vardır.
Kaç satırlık Ruby kaynak koduna sahip olduğunuzu öğrenmek için böyle bir şey kullanmayı deneyin:
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
Bu komut, /app klasöründeki .rb uzantılı (ruby dosyaları) tüm dosyaları arayacak ve satır sayısını yazdıracaktır. Yorum satırları bu toplamlara dahil edileceğinden, bu sayının yalnızca yaklaşık olduğunu lütfen unutmayın.
Daha kesin ve daha bilgilendirici bir başka seçenek de kod satırlarının, sınıfların, yöntemlerin sayısının, yöntemlerin sınıflara oranının ve yöntem başına kod satırlarının oranının hızlı bir özetini veren Rails komisyon görev stats
kullanmaktır:
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- Kod tabanımda yinelenen kalıpları çıkarabilir miyim?
Eylemde Ayrıştırma
Gerçek dünyadan bir örnekle başlayalım.
Koşucular için zamanı takip eden bir uygulama yazmak istediğimizi varsayın. Ana sayfada kullanıcı girdiği saatleri görebilir.
Her zaman girişinde bir tarih, mesafe, süre ve ilgili ek “durum” bilgileri (örn. hava durumu, arazi türü vb.) ve gerektiğinde hesaplanabilen ortalama bir hız bulunur.
Haftalık ortalama hız ve mesafeyi gösteren bir rapor sayfasına ihtiyacımız var.
Giriş için ortalama hız, genel ortalama hızdan daha yüksekse, kullanıcıyı bir SMS ile bilgilendireceğiz (bu örnekte, SMS'i göndermek için Nexmo RESTful API'sini kullanacağız).
Ana sayfa, buna benzer bir giriş oluşturmak için mesafeyi, tarihi ve koşu için harcanan zamanı seçmenize olanak tanır:
Ayrıca, temelde haftada katedilen ortalama hız ve mesafeyi içeren haftalık bir rapor olan bir statistics
sayfamız da var.
- Online örneği buradan inceleyebilirsiniz.
kod
app
dizininin yapısı şuna benzer:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Kimlik doğrulamayı uygulamak için Devise ile kullandığımızdan, özel bir şey olmadığı için User
modelini tartışmayacağım.
Entry
modeline gelince, uygulamamız için iş mantığını içerir.
Her Entry
bir User
aittir.
Her giriş için distance
, time_period
, date_time
ve status
özniteliklerinin varlığını doğrularız.
Her giriş oluşturduğumuzda, kullanıcının ortalama hızını sistemdeki diğer tüm kullanıcıların ortalamasıyla karşılaştırır ve Nexmo kullanarak SMS ile kullanıcıyı bilgilendiririz (Nexmo kütüphanesinin nasıl kullanıldığını tartışmayacağız, ancak ben istedim. harici bir kütüphane kullandığımız bir durumu göstermek için).
- Özet örneği
Entry
modelinin yalnızca iş mantığından fazlasını içerdiğine dikkat edin. Ayrıca bazı doğrulamaları ve geri aramaları da yönetir.
entries_controller.rb
, ana CRUD eylemlerine sahiptir (yine de güncelleme yok). EntriesController#index
mevcut kullanıcı için girdileri alır ve kayıtları oluşturulma tarihine göre sıralar, EntriesController#create
yeni bir girdi oluşturur. EntriesController#destroy
:
- Özet örneği
Statistics_controller.rb haftalık raporu hesaplamaktan sorumluyken, StatisticsController#index
oturum açmış kullanıcı için girdileri alır ve statistics_controller.rb
Enumerable sınıfında bulunan #group_by
yöntemini kullanarak bunları haftaya göre gruplandırır. Daha sonra bazı özel yöntemler kullanarak sonuçları süslemeye çalışır.
- Özet örneği
Kaynak kodu kendi kendini açıklayıcı olduğu için burada görüşleri fazla tartışmıyoruz.
Aşağıda, oturum açmış kullanıcı ( index.html.erb
) için girişlerin listelendiği görünüm bulunmaktadır. Bu, girdiler denetleyicisinde dizin eyleminin (yöntem) sonuçlarını görüntülemek için kullanılacak şablondur:
- Özet örneği
Paylaşılan kodu _entry.html.erb
kısmi şablonuna çekmek için kısmi render @entries
kullandığımızı unutmayın, böylece kodumuzu KURU ve yeniden kullanılabilir tutabiliriz:
- Özet örneği
Aynı şey kısmi _form
için de geçerlidir. Aynı kodu (yeni ve düzenle) eylemlerle kullanmak yerine, yeniden kullanılabilir bir kısmi form oluşturuyoruz:
- Özet örneği
Haftalık rapor sayfası görünümüne gelince, statistics/index.html.erb
bazı istatistikleri gösterir ve bazı girdileri gruplayarak kullanıcının haftalık performansını raporlar:
- Özet örneği
Ve son olarak, girdiler için yardımcı olan entries_helper.rb
, öznitelikleri daha insanca okunabilir hale getirmesi gereken iki yardımcı readable_time_period
ve readable_speed
içerir:
- Özet örneği
Şimdiye kadar süslü bir şey yok.
Çoğunuz bunu yeniden düzenlemenin KISS ilkesine aykırı olduğunu ve sistemi daha karmaşık hale getireceğini tartışacaksınız.
Peki bu uygulamanın gerçekten yeniden düzenlemeye ihtiyacı var mı?
Kesinlikle hayır , ancak bunu yalnızca tanıtım amaçlı olarak değerlendireceğiz.
Sonuçta, önceki bölüme ve bir uygulamanın yeniden düzenlemeye ihtiyaç duyduğunu gösteren özelliklere göz atarsanız, örneğimizdeki uygulamanın yeniden düzenleme için geçerli bir aday olmadığı açıkça görülür.
Yaşam döngüsü
O halde Rails MVC model yapısını açıklayarak başlayalım.
Genellikle tarayıcının https://www.toptal.com/jogging/show/1
gibi bir istekte bulunmasıyla başlar.
Web sunucusu, isteği alır ve hangi controller
kullanılacağını bulmak için routes
kullanır.
Denetleyiciler, kullanıcı isteklerini, veri gönderimlerini, tanımlama bilgilerini, oturumları vb. ayrıştırma işini yapar ve ardından model
verileri almasını ister.
models
, veritabanıyla konuşan, verileri depolayan ve doğrulayan, iş mantığını gerçekleştiren ve diğer türlü ağır işleri yapan Ruby sınıflarıdır. Görünümler, kullanıcının gördüğü şeydir: HTML, CSS, XML, Javascript, JSON.
Bir Rails istek yaşam döngüsünün sırasını göstermek istersek, şöyle görünür:
Ulaşmak istediğim, düz eski yakut nesneleri (PORO'lar) kullanarak daha fazla soyutlama eklemek ve create/update
eylemleri için kalıbı aşağıdaki gibi yapmak:
Ve list/show
eylemleri için aşağıdakine benzer bir şey:
POROs soyutlamaları ekleyerek, Rails'in pek iyi olmadığı SRP sorumlulukları arasında tam bir ayrım sağlayacağız.
yönergeler
Yeni tasarımı elde etmek için aşağıda listelenen yönergeleri kullanacağım, ancak lütfen bunların T'ye uymanız gereken kurallar olmadığını unutmayın. Bunları, yeniden düzenlemeyi kolaylaştıran esnek yönergeler olarak düşünün.
- ActiveRecord modelleri, ilişkilendirmeler ve sabitler içerebilir, ancak başka hiçbir şey içermez. Bu, hiçbir geri arama (hizmet nesnelerini kullanın ve geri aramaları oraya ekleyin) ve doğrulama (model için adlandırma ve doğrulamaları dahil etmek için Form nesnelerini kullanın) anlamına gelir.
- Denetleyicileri ince katmanlar olarak tutun ve her zaman Hizmet nesnelerini çağırın. Bazılarınız, mantığı içermek için hizmet nesnelerini çağırmaya devam etmek istediğimize göre neden denetleyicileri kullandığımızı sorarsınız? Kontrolörler, HTTP yönlendirme, parametre ayrıştırma, kimlik doğrulama, içerik anlaşması, doğru hizmeti veya düzenleyici nesneyi çağırma, istisna yakalama, yanıt biçimlendirme ve doğru HTTP durum kodunu döndürme için iyi bir yerdir.
- Hizmetler, Sorgu nesnelerini çağırmalı ve durumu saklamamalıdır. Sınıf yöntemlerini değil, örnek yöntemlerini kullanın. SRP ile uyumlu çok az genel yöntem olmalıdır.
- Sorgu nesnelerinde sorgular yapılmalıdır. Sorgu nesnesi yöntemleri, bir ActiveRecord ilişkilendirmesi değil, bir nesne, karma veya dizi döndürmelidir.
- Yardımcıları kullanmaktan kaçının ve bunun yerine dekoratörleri kullanın. Niye ya? Rails yardımcılarıyla ilgili yaygın bir tuzak, hepsinin bir ad alanını paylaşan ve birbirinin üzerine basan büyük bir OO olmayan işlev yığınına dönüşebilmeleridir. Ancak daha da kötüsü, Rails yardımcılarıyla herhangi bir tür polimorfizmi kullanmanın harika bir yolu olmamasıdır - farklı bağlamlar veya türler için farklı uygulamalar, overriding veya alt sınıf yardımcıları sağlar. Rails yardımcı sınıflarının, herhangi bir sunum mantığı için model özniteliklerini biçimlendirme gibi belirli kullanım durumları için değil, genellikle yardımcı yöntemler için kullanılması gerektiğini düşünüyorum. Onları hafif ve havadar tutun.
- Endişeleri kullanmaktan kaçının ve bunun yerine Dekoratörleri/Yetkilileri kullanın. Niye ya? Ne de olsa, endişeler Rails'in temel bir parçası gibi görünüyor ve birden fazla model arasında paylaşıldığında kodu KURUTULABİLİR. Bununla birlikte, asıl mesele, endişelerin model nesnesini daha uyumlu hale getirmemesidir. Kod sadece daha iyi organize edilmiştir. Başka bir deyişle, modelin API'sinde gerçek bir değişiklik yoktur.
- Kodunuzu daha temiz tutmak ve ilgili nitelikleri gruplandırmak için modellerden Değer Nesneleri çıkarmaya çalışın .
- Görünüm başına her zaman bir örnek değişkeni iletin.
yeniden düzenleme
Başlamadan önce, bir şeyden daha bahsetmek istiyorum. Yeniden düzenlemeye başladığınızda, genellikle kendinize şu soruyu sorarsınız: "Bu gerçekten iyi bir yeniden düzenleme mi?"
Sorumluluklar arasında daha fazla ayrım veya izolasyon yaptığınızı düşünüyorsanız (bu, daha fazla kod ve yeni dosya eklemek anlamına gelse bile), bu genellikle iyi bir şeydir. Sonuçta, bir uygulamayı ayrıştırmak çok iyi bir uygulamadır ve uygun birim testi yapmamızı kolaylaştırır.
Bunu zaten yaptığınızı ve Rails'i (genellikle Skinny Controller ve FAT modeli) kullanmakta rahat olduğunuzu varsaydığım için, mantığı denetleyicilerden modellere taşımak gibi şeyleri tartışmayacağım.
Bu makaleyi sıkı tutmak adına, burada testten bahsetmeyeceğim, ancak bu test etmemeniz gerektiği anlamına gelmez.
Aksine, ilerlemeden önce her şeyin yolunda olduğundan emin olmak için her zaman bir testle başlamalısınız . Bu , özellikle yeniden düzenleme yaparken bir zorunluluktur .
Ardından, değişiklikleri uygulayabilir ve tüm testlerin kodun ilgili bölümleri için geçtiğinden emin olabiliriz.
Değer Nesnelerini Çıkarma
İlk olarak, değer nesnesi nedir?
Martin Fowler şöyle açıklıyor:
Değer Nesnesi, para veya tarih aralığı nesnesi gibi küçük bir nesnedir. Anahtar özellikleri, referans semantiği yerine değer semantiğini takip etmeleridir.
Bazen bir kavramın kendi soyutlamasını hak ettiği ve eşitliğinin değere değil, kimliğe dayandığı bir durumla karşılaşabilirsiniz. Örnekler arasında Ruby'nin Tarihi, URI ve Yol Adı verilebilir. Bir değer nesnesine (veya etki alanı modeline) çıkarma büyük bir kolaylıktır.
Neden rahatsız?
Value nesnesinin en büyük avantajlarından biri, kodunuzda elde edilmesine yardımcı oldukları ifade gücüdür. Kodunuz çok daha net olma eğiliminde olacaktır veya en azından iyi adlandırma uygulamalarınız varsa olabilir. Değer Nesnesi bir soyutlama olduğundan, daha temiz koda ve daha az hataya yol açar.
Başka bir büyük kazanç değişmezliktir. Nesnelerin değişmezliği çok önemlidir. Bir değer nesnesinde kullanılabilecek belirli veri kümelerini depolarken, genellikle bu verilerin manipüle edilmesini istemiyorum.
Bu ne zaman işe yarar?
Herkese uyan tek bir cevap yoktur. Sizin için en iyi olanı ve herhangi bir durumda mantıklı olanı yapın.
Bununla birlikte, bunun ötesine geçerek, bu kararı vermeme yardımcı olmak için kullandığım bazı yönergeler var.
Bir grup yöntemin ilişkili olduğunu düşünüyorsanız, Value nesneleri ile daha anlamlıdırlar. Bu anlamlılık, bir Value nesnesinin, ortalama geliştiricinizin yalnızca nesnenin adına bakarak çıkarabileceği farklı bir veri kümesini temsil etmesi gerektiği anlamına gelir.
Bu nasıl yapılır?
Değer nesneleri bazı temel kurallara uymalıdır:
- Değer nesnelerinin birden çok özniteliği olmalıdır.
- Nitelikler, nesnenin yaşam döngüsü boyunca değişmez olmalıdır.
- Eşitlik, nesnenin nitelikleri tarafından belirlenir.
Örneğimizde, Entry#status_weather
ve Entry#status_landform
özniteliklerini kendi sınıflarına soyutlamak için bir EntryStatus
değer nesnesi oluşturacağım, bu şuna benzer:
- Özet örneği
Not: Bu, ActiveRecord::Base
öğesinden miras almayan yalnızca Düz Eski Yakut Nesnesidir (PORO). Niteliklerimiz için okuyucu yöntemleri tanımladık ve bunları başlatma sırasında atadık. Ayrıca (<=>) yöntemini kullanarak nesneleri karşılaştırmak için karşılaştırılabilir bir karışım kullandık.
Oluşturduğumuz değer nesnesini kullanmak için Entry
modelini değiştirebiliriz:
- Özet örneği
Yeni değer nesnesini uygun şekilde kullanmak için EntryController#create
yöntemini de değiştirebiliriz:
- Özet örneği
Hizmet Nesnelerini Çıkart
Peki Service nesnesi nedir?
Bir Service nesnesinin işi, belirli bir iş mantığı biti için kodu tutmaktır. Az sayıda nesnenin tüm gerekli mantık için çok, çok sayıda yöntem içerdiği "şişman model" stilinin aksine, Service nesnelerini kullanmak, her biri tek bir amaca hizmet eden birçok sınıfla sonuçlanır.

Niye ya? Faydaları nelerdir?
- Ayrışma. Hizmet nesneleri, nesneler arasında daha fazla yalıtım elde etmenize yardımcı olur.
- Görünürlük. Hizmet nesneleri (iyi adlandırılmışsa) bir uygulamanın ne yaptığını gösterir. Bir uygulamanın hangi yetenekleri sağladığını görmek için hizmetler dizinine göz atabilirim.
- Temizleme modelleri ve kontrolörler. Denetleyiciler, isteği (params, session, cookie) argümanlara çevirir, servise iletir ve servis yanıtına göre yeniden yönlendirir veya işler. Modeller yalnızca çağrışımlar ve kalıcılık ile ilgilenirken. Denetleyicilerden/modellerden hizmet nesnelerine kod çıkarmak, SRP'yi destekleyecek ve kodu daha ayrıştırılmış hale getirecektir. Bu durumda modelin sorumluluğu yalnızca ilişkilendirmeler ve kayıtların kaydedilmesi/silinmesi ile ilgilenirken, hizmet nesnesinin tek bir sorumluluğu (SRP) olacaktır. Bu, daha iyi tasarım ve daha iyi birim testleri sağlar.
- KURU ve Değişimi kucakla. Servis nesnelerini olabildiğince basit ve küçük tutarım. Hizmet nesnelerini diğer hizmet nesneleriyle oluşturur ve yeniden kullanırım.
- Test takımınızı temizleyin ve hızlandırın. Servisler, tek bir giriş noktasına (çağrı yöntemi) sahip küçük Ruby nesneleri oldukları için test edilmesi kolay ve hızlıdır. Karmaşık hizmetler, diğer hizmetlerden oluşur, böylece testlerinizi kolayca bölebilirsiniz. Ayrıca, hizmet nesnelerini kullanmak, tüm Rails ortamını yüklemeye gerek kalmadan ilgili nesnelerle alay etmeyi/saplamayı kolaylaştırır.
- Her yerden aranabilir. Hizmet nesnelerinin, denetleyicilerden ve diğer hizmet nesnelerinden, DelayedJob / Rescue / Sidekiq Jobs, Rake görevleri, konsol, vb. çağrılması muhtemeldir.
Öte yandan, hiçbir şey asla mükemmel değildir. Hizmet nesnelerinin bir dezavantajı, çok basit bir eylem için aşırıya kaçabilmeleridir. Bu gibi durumlarda, kodunuzu basitleştirmek yerine karmaşık hale getirebilirsiniz.
Hizmet nesnelerini ne zaman çıkarmalısınız?
Burada da sert ve hızlı bir kural yoktur.
Normalde, Servis nesneleri orta ila büyük sistemler için daha iyidir; standart CRUD işlemlerinin ötesinde makul miktarda mantığa sahip olanlar.
Bu nedenle, bir kod parçacığının onu ekleyeceğiniz dizine ait olmayabileceğini düşündüğünüzde, muhtemelen yeniden düşünmek ve bunun yerine bir hizmet nesnesine gitmesi gerekip gerekmediğini görmek iyi bir fikirdir.
Hizmet nesnelerinin ne zaman kullanılacağına ilişkin bazı göstergeler şunlardır:
- Eylem karmaşıktır.
- Eylem birden fazla modele ulaşır.
- Eylem, harici bir hizmetle etkileşime girer.
- Eylem, temel alınan modelin temel kaygısı değildir.
- Eylemi gerçekleştirmenin birden fazla yolu vardır.
Hizmet Nesnelerini nasıl tasarlamalısınız?
Bir hizmet nesnesi için sınıf tasarlamak nispeten basittir, çünkü özel taşlara ihtiyacınız yoktur, yeni bir DSL öğrenmek zorunda değilsiniz ve zaten sahip olduğunuz yazılım tasarım becerilerine az çok güvenebilirsiniz.
Hizmet nesnesini tasarlamak için genellikle aşağıdaki yönergeleri ve kuralları kullanırım:
- Nesnenin durumunu saklamayın.
- Sınıf yöntemlerini değil, örnek yöntemlerini kullanın.
- Çok az sayıda genel yöntem olmalıdır (tercihen SRP'yi desteklemek için bir tane.
- Yöntemler, boolean değil zengin sonuç nesneleri döndürmelidir.
- Hizmetler,
app/services
dizininin altına gider. İş mantığı ağırlıklı etki alanları için alt dizinleri kullanmanızı öneririm. Örneğin,app/services/report/generate_weekly.rb
dosyasıReport::GenerateWeekly
öğesini tanımlarkenapp/services/report/publish_monthly.rb
dosyasıReport::PublishMonthly
tanımlayacaktır. - Hizmetler bir fiille başlar (ve Service ile bitmez):
ApproveTransaction
,SendTestNewsletter
,ImportUsersFromCsv
. - Hizmetler, çağrı yöntemine yanıt verir. Başka bir fiil kullanmanın onu biraz gereksiz kıldığını buldum: ApproveTransaction.approve() iyi okunmuyor. Ayrıca call yöntemi, lambda, procs ve method nesneleri için fiili yöntemdir.
StatisticsController#index
öğesine bakarsanız, denetleyiciye bağlı bir grup yöntem ( weeks_to_date_from
, weeks_to_date_to
, avg_distance
vb.) fark edeceksiniz. Bu gerçekten iyi değil. Haftalık raporu statistics_controller
dışında oluşturmak istiyorsanız, sonuçları göz önünde bulundurun.
Bizim durumumuzda, Report::GenerateWeekly
oluşturalım ve rapor mantığını StatisticsController
çıkaralım:
- Özet örneği
Böylece StatisticsController#index
artık daha temiz görünüyor:
- Özet örneği
Hizmet nesne kalıbını uygulayarak, kodu belirli, karmaşık bir eylem etrafında birleştirir ve daha küçük, daha net yöntemlerin oluşturulmasını destekleriz.
Ödev: Struct
yerine WeeklyReport
için Value nesnesini kullanmayı düşünün .
Denetleyicilerden Sorgu Nesnelerini Çıkarın
Sorgu nesnesi nedir?
Bir Query nesnesi, bir veritabanı sorgusunu temsil eden bir PORO'dur. Sorgu mantığını gizlerken aynı zamanda uygulamada farklı yerlerde yeniden kullanılabilir. Ayrıca test etmek için iyi bir yalıtılmış birim sağlar.
Karmaşık SQL/NoSQL sorgularını kendi sınıflarına çıkarmalısınız.
Her Query nesnesi, ölçütlere/iş kurallarına göre bir sonuç kümesi döndürmekten sorumludur.
Bu örnekte, herhangi bir karmaşık sorgumuz yok, bu nedenle Query nesnesini kullanmak verimli olmayacaktır. Ancak, gösterim amacıyla, Report::GenerateWeekly#call
içindeki sorguyu çıkaralım ve create_entries_query.rb öğesini generate_entries_query.rb
:
- Özet örneği
Ve Report::GenerateWeekly#call
içinde şunu değiştirelim:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
ile birlikte:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
Sorgu nesne kalıbı, model mantığınızın bir sınıfın davranışıyla sıkı bir şekilde ilişkili olmasına yardımcı olurken, aynı zamanda denetleyicilerinizi de zayıf tutar. Düz eski Ruby sınıflarından başka bir şey olmadıklarından, sorgu nesnelerinin ActiveRecord::Base
miras alması gerekmez ve sorguları yürütmekten başka bir şeyden sorumlu olmamalıdır.
Bir Hizmet Nesnesine Giriş Oluştur
Şimdi yeni bir servis nesnesine yeni bir girdi oluşturmanın mantığını çıkaralım. Kuralı kullanalım ve CreateEntry
oluşturalım:
- Özet örneği
Ve şimdi EntriesController#create
şu şekilde:
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
Doğrulamaları Form Nesnesine Taşıma
Şimdi, burada işler daha da ilginçleşmeye başlıyor.
Yönergelerimizde, modellerin ilişkilendirmeler ve sabitler içermesini, ancak başka hiçbir şey içermemesini (doğrulama ve geri arama yok) istediğimizi kabul ettiğimizi unutmayın. Öyleyse geri aramaları kaldırarak başlayalım ve bunun yerine bir Form nesnesi kullanalım.
Bir Form nesnesi, Düz Eski Yakut Nesnesidir (PORO). Veritabanıyla konuşması gereken her yerde denetleyici/servis nesnesinden devralır.
Form nesneleri neden kullanılır?
Uygulamanızı yeniden düzenlemeye çalışırken, tek sorumluluk ilkesini (SRP) akılda tutmak her zaman iyi bir fikirdir.
SRP, bir sınıfın nelerden sorumlu olması gerektiği konusunda daha iyi tasarım kararları vermenize yardımcı olur.
Örneğin, veritabanı tablo modeliniz (Rails bağlamında bir ActiveRecord modeli), koddaki tek bir veritabanı kaydını temsil eder, bu nedenle, kullanıcınızın yaptığı herhangi bir şeyle ilgilenmesi için hiçbir neden yoktur.
Form nesnelerinin devreye girdiği yer burasıdır.
Bir Form nesnesi, uygulamanızdaki bir formu temsil etmekten sorumludur. Böylece her girdi alanı, sınıfta bir nitelik olarak ele alınabilir. Bu özniteliklerin bazı doğrulama kurallarını karşıladığını doğrulayabilir ve "temiz" verileri gitmesi gereken yere iletebilir (örneğin, veritabanı modelleriniz veya belki de arama sorgusu oluşturucunuz).
Bir Form nesnesini ne zaman kullanmalısınız?
- Doğrulamaları Rails modellerinden çıkarmak istediğinizde.
- Tek bir form gönderimiyle birden çok model güncellenebildiğinde, bir Form nesnesi oluşturmak isteyebilirsiniz.
Bu, tüm form mantığını (adlandırma kuralları, doğrulamalar vb.) tek bir yere koymanızı sağlar.
Bir Form nesnesini nasıl yaratırsınız?
- Düz bir Ruby sınıfı oluşturun.
-
ActiveModel::Model
dahil edin (Rails 3'te bunun yerine Adlandırma, Dönüştürme ve Doğrulamaları dahil etmeniz gerekir) - Yeni form sınıfınızı normal bir ActiveRecord modeliymiş gibi kullanmaya başlayın; en büyük fark, bu nesnede depolanan verileri kalıcı hale getirememenizdir.
Lütfen reform mücevherini kullanabileceğinizi unutmayın, ancak PORO'lara bağlı kalarak şunun gibi görünen entry_form.rb
oluşturacağız:
- Özet örneği
Ve Form nesnesini EntryForm
kullanmaya başlamak için CreateEntry
değiştireceğiz:
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
Not: Bazılarınız Service nesnesinden Form nesnesine erişmeye gerek olmadığını ve Form nesnesini doğrudan denetleyiciden çağırabileceğimizi söyleyebilirsiniz, bu geçerli bir argümandır. Ancak, net bir akış olmasını tercih ederim ve bu yüzden her zaman Form nesnesini Service nesnesinden çağırırım.
Geri Aramaları Hizmet Nesnesine Taşı
Daha önce anlaştığımız gibi, modellerimizin doğrulamalar ve geri aramalar içermesini istemiyoruz. Form nesnelerini kullanarak doğrulamaları çıkardık. Ancak hala bazı geri aramalar kullanıyoruz ( after_create
Entry
modelinde compare_speed_and_notify_user
).
Neden modellerden geri aramaları kaldırmak istiyoruz?
Rails geliştiricileri genellikle test sırasında geri arama ağrısını fark etmeye başlar. ActiveRecord modellerinizi test etmiyorsanız, uygulamanız büyüdükçe ve arama yapmak veya geri aramayı önlemek için daha fazla mantık gerektiğinden daha sonra acıyı fark etmeye başlayacaksınız.
after_*
geri aramaları öncelikle nesneyi kaydetme veya sürdürme ile ilgili olarak kullanılır.
Nesne kaydedildiğinde, nesnenin amacı (yani sorumluluğu) yerine getirilmiş olur. Dolayısıyla, nesne kaydedildikten sonra hala geri aramaların çağrıldığını görüyorsak, muhtemelen gördüğümüz şey, nesnenin sorumluluk alanının dışına ulaşan geri aramalardır ve işte o zaman sorunlarla karşılaşırız.
Bizim durumumuzda, gerçekten Girişin etki alanı ile ilgili olmayan bir girişi kaydettikten sonra kullanıcıya bir SMS gönderiyoruz.
Sorunu çözmenin basit bir yolu, geri aramayı ilgili hizmet nesnesine taşımaktır. Sonuçta, son kullanıcı için bir SMS göndermek, Entry modelinin kendisiyle değil, CreateEntry
Service Object ile ilgilidir.
Bunu yaparken, testlerimizde artık karşılaştırma_hızı_and_notify_user yöntemini compare_speed_and_notify_user
gerekmiyor. SMS gönderilmesine gerek kalmadan bir giriş oluşturmayı basit hale getirdik ve sınıflarımızın tek bir sorumluluğa (SRP) sahip olduğundan emin olarak iyi Nesne Yönelimli tasarımı izliyoruz.
Şimdi CreateEntry
şuna benziyor:
- Özet örneği
Yardımcılar Yerine Dekoratörleri Kullanın
Draper görünüm modelleri ve dekoratör koleksiyonunu kolayca kullanabilirken, şimdiye kadar yaptığım gibi bu makale için PORO'lara bağlı kalacağım.
İhtiyacım olan, dekore edilmiş nesnede yöntemleri çağıracak bir sınıf.
Bunu uygulamak için method_missing
kullanabilirim, ancak Ruby'nin SimpleDelegator
standart kitaplığını kullanacağım.
Aşağıdaki kod, temel dekoratörümüzü uygulamak için SimpleDelegator
nasıl kullanılacağını gösterir:
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
Peki neden _h
yöntemi?
Bu yöntem, görünüm bağlamı için bir proxy görevi görür. Varsayılan olarak, görünüm bağlamı bir görünüm sınıfının bir örneğidir, varsayılan görünüm sınıfı ActionView::Base
. Görünüm yardımcılarına aşağıdaki gibi erişebilirsiniz:
_h.content_tag :div, 'my-div', class: 'my-class'
Daha kullanışlı hale getirmek için ApplicationHelper
bir decorate
yöntemi ekliyoruz:
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
Artık EntriesHelper
yardımcılarını dekoratörlere taşıyabiliriz:
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
Ve readable_time_period
ve readable_speed
şu şekilde kullanabiliriz:
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
Yeniden Düzenlemeden Sonra Yapı
Sonunda daha fazla dosya elde ettik, ancak bu mutlaka kötü bir şey değil (ve unutmayın ki, başlangıçtan itibaren, bu örneğin yalnızca gösterim amaçlı olduğunu ve yeniden düzenleme için iyi bir kullanım durumu olmadığını kabul ettik):
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Çözüm
Bu blog yazısında Rails'e odaklanmış olsak da, RoR açıklanan hizmet nesnelerinin ve diğer PORO'ların bir bağımlılığı değildir. Bu yaklaşımı herhangi bir web çerçevesi, mobil veya konsol uygulamasıyla kullanabilirsiniz.
Web uygulamalarının mimarisi olarak MVC'yi kullanarak, her şey birbirine bağlı kalır ve çoğu değişikliğin uygulamanın diğer bölümlerini etkilediği için daha yavaş ilerlemenizi sağlar. Ayrıca, sizi bazı iş mantığını nereye koyacağınızı düşünmeye zorlar - modele mi, denetleyiciye mi yoksa görünüme mi girmeli?
Basit PORO'ları kullanarak, iş mantığını ActiveRecord
miras almayan modellere veya hizmetlere taşıdık, bu zaten büyük bir kazançtır, ayrıca SRP'yi ve daha hızlı birim testlerini destekleyen daha temiz bir kodumuz olduğundan bahsetmiyoruz bile.
Temiz mimari, uygulamanızın ne yaptığını kolayca görebilmeniz için kullanım örneklerini yapınızın ortasına/üstüne yerleştirmeyi amaçlar. Ayrıca, çok daha modüler ve yalıtılmış olduğu için değişiklikleri benimsemeyi kolaylaştırır.
Umarım Plain Old Ruby Objects ve daha fazla soyutlama kullanmanın endişeleri nasıl ayrıştırdığını, testi basitleştirdiğini ve temiz, sürdürülebilir kod üretmeye nasıl yardımcı olduğunu göstermişimdir.