Bir sınıf, hangi yöntem alt kümesini uyguladığı kullanıcılarıyla nasıl iletişim kurmalıdır?


12

senaryo

Bir web uygulaması, kullanıcı arka uç arabirimini IUserBackendyöntemlerle tanımlar

  • GetUser (UID)
  • CREATEUSER (UID)
  • deleteUser (UID)
  • setPassword (uid, şifre)
  • ...

Farklı kullanıcı arka uçları (örneğin LDAP, SQL, ...) bu arabirimi uygular, ancak her arka uç her şeyi yapamaz. Örneğin, somut bir LDAP sunucusu bu web uygulamasının kullanıcıları silmesine izin vermez. Yani uygulayan LdapUserBackendsınıf IUserBackenduygulanmaz deleteUser(uid).

Somut sınıfın, web uygulamasının arka ucun kullanıcılarıyla yapmasına izin verilen web uygulamasıyla iletişim kurması gerekir.

Bilinen çözüm

Bitbit VE bit eylemleri bit bit VE VE sonuçları istenen bir eylem sonucu olan bir tamsayı döndüren IUserInterfacebir implementedActionsyöntem var bir çözüm gördüm :

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

Nerede

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

vb.

Böylece web uygulaması, ihtiyaç duyduğu şeylerle bir bit maskesi oluşturur ve implementedActions()destekleyip desteklemediğini bir boole ile yanıtlar.

görüş

Bu bit işlemleri bana C çağından kalma kalıntılara benziyor, temiz kod açısından anlaşılması kolay değil.

Soru

Sınıfın uyguladığı arabirim yöntemlerinin alt kümesini iletmesi için modern (daha iyi?) Bir desen nedir? Yoksa yukarıdan "bit operasyon yöntemi" hala en iyi uygulama mı?

( Önemli olması durumunda: PHP, OO dilleri için genel bir çözüm arıyorum )


5
Genel çözüm arayüzü ayırmaktır. IUserBackendİçermemelidir deleteUserhiç yöntemini. Bu bir parçası olmalı IUserDeleteBackend(ya da ona ne demek istiyorsanız). Kullanıcıları silmesi gereken IUserDeleteBackendkodun argümanları, işlevselliğin kullanması gerekmeyen kodlar IUserBackendve uygulanmayan yöntemlerle herhangi bir sıkıntı yaşamayacaklardır.
Bakuriu

3
Önemli bir tasarım değerlendirmesi, bir eylemin kullanılabilirliğinin çalışma zamanı koşullarına bağlı olup olmadığıdır. Öyle mi tüm silinmesini desteklemeyen LDAP sunucusu? Yoksa bu sunucu yapılandırmasının bir özelliği midir ve sistem yeniden başlatıldığında değişebilir mi? LDAP bağlacınız bu durumu otomatik olarak bulmalı mı yoksa farklı özelliklere sahip farklı bir LDAP bağlacı takmak için yapılandırmanın değiştirilmesini mi gerektiriyor? Bunlar, hangi çözümlerin uygulanabilir olduğu üzerinde güçlü bir etkiye sahiptir.
Sebastian Redl

@SebastianRedl Evet, bu dikkate almadığım bir şey. Aslında çalışma zamanı için bir çözüme ihtiyacım var. Çok iyi cevapları geçersiz kılmak istemediğim için , çalışma
zamanına

Yanıtlar:


24

Genel olarak, burada alabileceğiniz iki yaklaşım vardır: polimorfizm yoluyla test etme ve atma veya kompozisyon.

Test et ve at

Bu zaten tarif ettiğiniz yaklaşımdır. Bazı yollarla, sınıf kullanıcısına diğer bazı yöntemlerin uygulanıp uygulanmadığını belirtirsiniz. Bu, tek bir yöntem ve bitsel numaralandırma (açıkladığınız gibi) ile veya bir dizi supportsDelete()vb yöntemle yapılabilir.

Daha sonra, supportsDelete()döndürürse false, çağrı atılmaya veya yöntemin hiçbir şey yapmamasına deleteUser()neden olabilir NotImplementedExeption.

Bu basit olduğu için bazıları arasında popüler bir çözümdür. Bununla birlikte, birçok kişi - ben dahil - Liskov'un ikame ilkesinin (SOLID'deki L) bir ihlali olduğunu ve bu nedenle hoş bir çözüm olmadığını savunuyor.

Polimorfizm ile Kompozisyon

Buradaki yaklaşım IUserBackendbir enstrümanı çok kör olarak görmektir. Sınıflar her zaman bu arabirimdeki tüm yöntemleri uygulayamazsa, arabirimi daha odaklanmış parçalara ayırın. Yani sahip olabilirsiniz: IGeneralUser IDeletableUser IRenamableUser ... Başka bir deyişle, tüm arka uçlarınızın uygulayabileceği tüm yöntemler devreye girer IGeneralUserve yalnızca bazılarının gerçekleştirebileceği eylemlerin her biri için ayrı bir arayüz oluşturursunuz.

Bu şekilde, LdapUserBackenduygulanmaz IDeletableUserve bunu (C # sözdizimi kullanarak) gibi bir test kullanarak test edersiniz:

if (backend is IDeletableUser deletableUser)
{
    deletableUser.deleteUser(id);
}

(Bir örneğin bir arabirim uygulayıp uygulamadığını ve daha sonra bu arabirime nasıl yayınladığınızı belirlemek için PHP'deki mekanizmadan emin değilim, ancak bu dilde bir eşdeğer olduğundan eminim)

Bu yöntemin avantajı, kodunuzun SOLID ilkelerine uymasını sağlamak için polimorfizmi iyi kullanması ve benim görüşüme göre çok daha zarif olmasıdır.

Dezavantajı, çok kolay bir şekilde hantal olabilmesidir. Örneğin, düzinelerce arayüz uygulamak zorunda kalırsanız, çünkü her beton arka ucun biraz farklı yetenekleri vardır, o zaman bu iyi bir çözüm değildir. Bu nedenle, bu yaklaşımın bu durumda sizin için pratik olup olmadığı konusundaki kararınızı kullanmanızı ve varsa kullanmanızı öneririm.


4
SOLID tasarım konuları için +1. Kodun daha da ileriye taşınmasını sağlayacak farklı yaklaşımlarla cevaplar göstermek her zaman güzel!
Caleb

2
PHP'de olurduif (backend instanceof IDelatableUser) {...}
Rad80

LSP ihlalinden zaten bahsettiniz. Kabul ediyorum, ancak hafifçe eklemek istedim: Giriş değeri eylemin gerçekleştirilmesini imkansız hale getirirse , örneğin bir Divide(float,float)yöntemde bölen olarak 0'ı geçerek Test ve Atma geçerlidir . Girdi değeri değişkendir ve istisna olası yürütmelerin küçük bir alt kümesini kapsar. Ancak, uygulama türünüze bağlı olarak atarsanız, o zaman yürütülememesi belirli bir gerçektir. İstisna , sadece bir alt kümesini değil, tüm olası girdileri kapsar . Bu, her katın her zaman ıslak olduğu bir dünyada her ıslak zemine "ıslak zemin" işareti koymak gibidir.
Flater

Bir tür üzerine atmama ilkesi için bir istisna vardır (cinayet amaçlı değildir). C # için bu NotImplementedException. Bu istisna içindir geçici kesintileri, yani kod henüz geliştirilmiş fakat olacaktır geliştirilecek. Yani kesin Verilen sınıfa edeceğini karar aynı değil asla gelişimi tamamlandıktan sonra bile, belli bir yöntemle bir şey yapmak.
flater

Cevap için teşekkür ederim. Aslında bir çalışma zamanı çözümüne ihtiyacım vardı ama sorumu vurgulayamadım. Cevabınızı geçersiz kılmak istemediğim için yeni bir soru oluşturmaya karar verdim .
problem

5

Mevcut durum

Mevcut kurulum, Arayüz Ayırma İlkesini (SOLID'deki I) ihlal ediyor.

Referans

Wikipedia'ya göre arayüz ayırma ilkesi (İSS), hiçbir müşterinin kullanmadığı yöntemlere bağımlı olmaya zorlanmaması gerektiğini belirtir . Arayüz ayrım ilkesi, 1990'ların ortalarında Robert Martin tarafından formüle edildi.

Başka bir deyişle, eğer arayüzünüz buysa:

public interface IUserBackend
{
    User getUser(int uid);
    User createUser(int uid);
    void deleteUser(int uid);
    void setPassword(int uid, string password);
}

Daha sonra bu arabirimi uygulayan her sınıf, listelenen her arabirim yöntemini kullanmalıdır . İstisna yok.

Genelleştirilmiş bir yöntem olup olmadığını düşünün:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     backendService.deleteUser(user.Uid);
}

Eğer gerçekten sadece bazı uygulayıcı sınıfların bir kullanıcıyı silebilmesi için bunu yapacak olsaydınız, bu yöntem zaman zaman yüzünüze patlar (ya da hiçbir şey yapmaz). Bu iyi bir tasarım değil.


Önerilen çözümünüz

IUserInterface, bit eylem VE bit eylem VE bit istenen VEed eylemleri sonucu olan bir tamsayı döndüren bir APPLICActions yöntemi olduğu bir çözüm gördüm.

Temel olarak yapmak istediğiniz şey:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     if(backendService.canDeleteUser())
         backendService.deleteUser(user.Uid);
}

Belirli bir sınıfın bir kullanıcıyı silip edemediğini tam olarak nasıl belirlediğimizi görmezden geliyorum . İster boolean, ister biraz bayrak olsun, ... önemli değil. Her şey bir ikili cevaba kadar kaynar: bir kullanıcıyı silebilir mi, evet mi hayır mı?

Bu sorunu çözecektir, değil mi? Teknik olarak öyle. Ama şimdi, Liskov İkame İlkesini ( SOLID'deki L) ihlal ediyorsunuz .

Oldukça karmaşık Wikipedia açıklamasından sonra StackOverflow'da iyi bir örnek buldum . "Kötü" örneğe dikkat edin:

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();

    duck.Swim();
}

Sanırım burada benzerliği görüyorsunuz. Bu, soyutlanmış bir nesneyi ( IDuck, IUserBackend) işlemesi gereken bir yöntemdir , ancak güvenliği ihlal edilmiş bir sınıf tasarımı nedeniyle, ilk olarak belirli uygulamaları işlemek zorunda kalır ( bunun, kullanıcıları silemeyen ElectricDuckbir IUserBackendsınıf olmadığından emin olun ).

Bu soyutlanmış bir yaklaşım geliştirme amacını bozar.

Not: Buradaki örneği düzeltmek sizin durumunuzdan daha kolaydır. Örneğin, sahip olmak yeterli ElectricDucküzerindeki dönüş kendisi içerideSwim() yöntemle. Her iki ördek de hala yüzebilir, bu yüzden fonksiyonel sonuç aynıdır.

Benzer bir şey yapmak isteyebilirsiniz. Yapma . Sadece olamaz taklit bir kullanıcıyı silmek ama gerçekte boş bir yöntem vücut var. Bu teknik açıdan işe yarıyor olsa da, uygulayıcı sınıfınızın bir şey yapması istendiğinde gerçekten bir şey yapıp yapmayacağını bilmek imkansız hale gelir. Bu, sürdürülemez kod için bir üreme alanıdır.


Önerilen çözümüm

Fakat bir uygulayıcı sınıfın bu yöntemlerden sadece bazılarını ele almasının mümkün (ve doğru) olduğunu söylediniz.

Örnek olarak, bu yöntemlerin olası her birleşimi için, onu uygulayacak bir sınıf olduğunu varsayalım. Tüm üslerimizi kapsar.

Buradaki çözüm arayüzü ayırmaktır .

public interface IGetUserService
{
    User getUser(int uid);
}

public interface ICreateUserService
{
    User createUser(int uid);
}

public interface IDeleteUserService
{
    void deleteUser(int uid);
}

public interface ISetPasswordService
{
    void setPassword(int uid, string password);
}

Bunun cevabımın başında geldiğini görebildiğinizi unutmayın. Arayüz Ayrışma İlkesi adı zaten bu prensip yapmak için tasarlanmıştır ortaya koymaktadır arayüzleri ayırmak yeterli derecede.

Bu, arayüzleri istediğiniz gibi karıştırmanıza ve eşleştirmenize olanak tanır:

public class UserRetrievalService 
              : IGetUserService, ICreateUserService
{
    //getUser and createUser methods implemented here
}

public class UserDeleteService
              : IDeleteUserService
{
    //deleteUser method implemented here
}

public class DoesEverythingService 
              : IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
    //All methods implemented here
}

Her sınıf, arayüzlerinin sözleşmesini bozmadan hangisini yapmak istediklerine karar verebilir.

Bu aynı zamanda belirli bir sınıfın bir kullanıcıyı silip edemediğini kontrol etmemiz gerekmediği anlamına gelir. IDeleteUserServiceArabirimi uygulayan her sınıf bir kullanıcıyı silebilir = Liskov İkame İlkesi ihlal edilmez .

public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
     backendService.deleteUser(user.Uid); //guaranteed to work
}

Herhangi biri uygulamayan bir nesneyi iletmeye çalışırsa IDeleteUserService, program derlemeyi reddeder. Bu yüzden tip güvenliğine sahip olmayı seviyoruz.

HaveUserDeleted(new DoesEverythingService());    // No problem.
HaveUserDeleted(new UserDeleteService());        // No problem.
HaveUserDeleted(new UserRetrievalService());     // COMPILE ERROR

dipnot

Örneği aşırı derecede ele aldım, arayüzü mümkün olan en küçük parçalara ayırarak. Bununla birlikte, durumunuz farklıysa, daha büyük parçalarla kurtulabilirsiniz.

Örneğin, bir kullanıcı oluşturabilen her hizmet her zaman bir kullanıcıyı silme yeteneğine sahipse (veya tersi), bu yöntemleri tek bir arabirimin parçası olarak saklayabilirsiniz:

public interface IManageUserService
{
    User createUser(int uid);
    void deleteUser(int uid);
}

Daha küçük parçalara ayırmak yerine bunu yapmanın teknik bir yararı yoktur; ancak daha az kaynatma gerektirdiğinden gelişimi biraz daha kolaylaştıracaktır.


Arabirimleri, arabirimin tüm amacı olan, destekledikleri davranışa göre bölmek için +1.
Greg Burghardt

Cevap için teşekkür ederim. Aslında bir çalışma zamanı çözümüne ihtiyacım vardı ama sorumu vurgulayamadım. Cevabınızı geçersiz kılmak istemediğim için yeni bir soru oluşturmaya karar verdim .
problem

@problemofficer: Bu vakalar için çalışma zamanı değerlendirmesi nadiren en iyi seçenektir, ancak bunu yapmak için gerçekten de durumlar vardır. Böyle bir durumda, çağrılabilecek ancak hiçbir şey yapamayabilecek bir yöntem yaratırsınız (bunu TryDeleteUseryansıtmak için çağırın ); ya da olası ama sorunlu bir durumsa, yöntem bir İstisna atar. Bir CanDoThing()ve DoThing()yöntem yaklaşımı kullanmak işe yarar, ancak harici arayanlarınızın iki çağrı kullanmasını (ve bunu yapamadıkları için cezalandırılmasını) gerektirir, bu da daha az sezgisel ve zarif değildir.
Flater

0

Daha yüksek seviye türleri kullanmak istiyorsanız, seçtiğiniz dilde ayarlanan türle gidebilirsiniz. İnşallah set kavşakları ve alt küme saptamaları için sözdizimi şekeri sağlar.

Temelde Java'nın EnumSet ile yaptığı şeydir (sözdizimi şekeri eksi, ama hey, Java)


0

.NET dünyasında, yöntemleri ve sınıfları özel niteliklerle dekore edebilirsiniz. Bu, davanızla ilgili olmayabilir.

Kulağa geldiğinizde, tasarımın daha yüksek bir seviyesiyle daha fazla ilgisi olabilir.

Bu, kullanıcıları düzenle sayfası veya bileşeni gibi bir kullanıcı arayüzü özelliğiyse, farklı özellikler nasıl maskelenir? Bu durumda 'test etme ve atma' bu amaç için oldukça verimsiz bir yaklaşım olacaktır. Her sayfayı yüklemeden önce, widget veya öğenin gizlenip gizlenmeyeceğini veya farklı bir şekilde sunulup sunulmayacağını belirlemek için her işleve bir sahte çağrı yaptığınızı varsayar. Alternatif olarak, kullanıcıyı hangi kodlama yolunu seçerseniz seçin, manuel olarak 'test etme ve atma' ile mevcut olanları keşfetmeye zorlayan bir web sayfanız vardır, çünkü kullanıcı bir açılır uyarı görünene kadar bir şeyin kullanılamadığını keşfetmez.

Bu nedenle, bir kullanıcı arayüzü için, seçilen uygulamaların hangi özelliklerin yönetilebileceğini yönlendirmesinden ziyade, özellik yönetimini nasıl yaptığınıza bakmak ve mevcut uygulamaların seçimini buna bağlamak isteyebilirsiniz. Özellik bağımlılıkları oluşturmak için çerçevelere bakmak ve yetenekleri etki alanı modelinizde varlıklar olarak açıkça tanımlamak isteyebilirsiniz. Bu yetkilendirmeye bile bağlanabilir. Esasen, yetkilendirme düzeyine dayalı olarak bir yeteneğin mevcut olup olmadığına karar vermek, bir yeteneğin gerçekten uygulanıp uygulanmadığına karar vermek için genişletilebilir ve daha sonra yüksek düzey UI 'özellikleri', yetenek kümeleriyle açık eşleşmelere sahip olabilir.

Bu bir Web API'sı ise, genel tasarım seçeneği, yetenekler zamanla genişledikçe 'Kullanıcıyı Yönet' API'sının veya 'Kullanıcı' REST kaynağının birden çok genel sürümünü desteklemek zorunda kalmadan karmaşık olabilir.

Özetlemek gerekirse, .NET dünyasında hangi sınıfların neyi uyguladığını belirlemenin çeşitli Yansıma / Öznitelik yollarından yararlanabilirsiniz, ancak her durumda gerçek sorunların bu bilgilerle yaptığınız şeyde olacağı anlaşılmaktadır.

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.