Epeyce yanıtlar ve yorumlar da belirtildiği üzere, DTOs olan özellikle (web hizmeti üzerinden göndermek için JSON seri hale örn) sınırları ötesinde veri aktarımı olmak üzere bazı durumlarda uygun ve kullanışlı. Bu cevabın geri kalanında, bunu az ya da çok görmezden geleceğim ve etki alanı sınıfları ve alıcıları ve belirleyicileri asgariye indirmek (ortadan kaldırmak değilse) nasıl en aza indirileceklerini ve büyük bir projede nasıl faydalı olabileceklerini konuşacağım. Ayrıca alıcıların veya ayarlayıcıların neden kaldırıldığını ya da ne zaman yapılacağından da bahsetmiyorum , çünkü bunlar kendi sorularıdır.
Örnek olarak, projenizin Satranç ya da Savaş Gemisi gibi bir masa oyunu olduğunu hayal edin. Bunu bir sunum katmanında (konsol uygulaması, web servisi, GUI vb.) Temsil etmenin çeşitli yolları olabilir, ancak aynı zamanda bir çekirdek etki alanınız da var. Sahip olabileceğiniz bir sınıf Coordinate
, tahtadaki pozisyonu temsil etmektir. Bunu yazmanın "kötülük" yolu olacaktır:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
(Java için değil, C # dilinde kod örnekleri yazacağım, kısayım ve daha iyi tanıdığım için. Umarım bu bir problem değildir. Kavramlar aynıdır ve çeviri basit olmalıdır.)
Ayarlayıcıları Çıkarma: İmkansızlık
Halka açık alıcılar ve taraftarlar hem potansiyel olarak sorunluyken, hem taraftarlar ikisinin çok daha "kötüsü". Ayrıca, genellikle ortadan kaldırılması daha kolaydır. Süreç, kurucu içindeki değeri basit bir şekilde belirler. Daha önce nesneyi mutasyona uğratan herhangi bir yöntem bunun yerine yeni bir sonuç vermelidir. Yani:
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
Bunun, X ve Y mutasyonları sınıfındaki diğer yöntemlere karşı koruma sağlamadığına dikkat edin. Daha kesin olarak değişken olmak için, readonly
( final
Java'da) kullanabilirsiniz. Ancak, her iki durumda da - mülkünüzü gerçekten değişmez kılmak ya da sadece ayarlayıcılar aracılığıyla doğrudan halkın mutasyonunu önlemek olsun - herkese açık ayarlayıcılarınızı kaldırma hilesidir. Çoğu durumda, bu sadece işe yarıyor.
Alıcıları Çıkarma, Bölüm 1: Davranış için Tasarım
Yukarıdakiler her şey yolunda ve iyi anlaşmalar için iyidir, ancak alıcılar açısından aslında başlamadan önce kendimizi ayağından vurduk. Bizim sürecimiz bir koordinatın ne olduğunu - temsil ettiği veriyi - düşünmek ve bunun etrafında bir sınıf yaratmaktı. Bunun yerine, bir koordinattan hangi davranışa ihtiyacımız olduğunu başlatmalıydık . Bu süreç, TDD'nin desteğiyle, onlara ancak ihtiyaç duyduğumuzda böyle sınıfları çıkardığımız için, bu nedenle istenen davranışla başlıyoruz ve oradan çalışıyoruz.
Diyelim ki, kendinizi ihtiyaç duyduğunuz ilk yerde Coordinate
çarpışma tespiti içindi: iki parçanın tahtada aynı alanı işgal edip etmediğini kontrol etmek istediniz. İşte "kötülük" yolu (yapıcılar kısalık için göz ardı edildi):
public class Piece
{
public Coordinate Position {get; private set;}
}
public class Coordinate
{
public int X {get; private set;}
public int Y {get; private set;}
}
//...And then, inside some class
public bool DoPiecesCollide(Piece one, Piece two)
{
return one.X == two.X && one.Y == two.Y;
}
Ve işte güzel yol:
public class Piece
{
private Coordinate _position;
public bool CollidesWith(Piece other)
{
return _position.Equals(other._position);
}
}
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public bool Equals(Coordinate other)
{
return _x == other._x && _y == other._y;
}
}
( IEquatable
uygulama basitlik için kısaltılmıştır). Verileri modellemek yerine davranışları tasarlayarak, alıcılarımızı kaldırmayı başardık.
Bunun, örneğinizle de alakalı olduğunu unutmayın. Bir ORM kullanıyor olabilirsiniz veya müşteri bilgilerini bir web sitesinde veya başka bir şeyde görüntüleyebilirsiniz, bu durumda bir tür Customer
DTO muhtemelen mantıklı olacaktır. Ancak, sisteminiz müşterileri içerdiği ve veri modelinde temsil edilmeleri, otomatik olarak Customer
etki alanınızda bir sınıf olması gerektiği anlamına gelmez . Belki davranış için tasarım yaptığınız zaman, biri ortaya çıkar, ancak alıcılardan kaçınmak istiyorsanız, önleyici olarak bir tane yaratmayın.
Alıcıları Çıkarma, Bölüm 2: Dış Davranış
Yani yukarıda iyi bir başlangıç, ama er ya da geç muhtemelen bir şekilde sınıfının durumuna bağlıdır bir sınıfın, ilişkili davranışa sahip bir durumun içine çalışır, ancak hangi ait değil üzerine sınıfa. Bu tür davranışlar, genellikle uygulamanızın hizmet katmanında yaşayan şeydir .
Bizim çıkarak Coordinate
örnek, sonunda kullanıcıya oyununuzu temsil etmek isteyeceksiniz ve bu ekrana çizim anlamına gelebilir. Örneğin, Vector2
ekrandaki bir noktayı temsil etmek için kullanılan bir UI projeniz olabilir . Ancak, Coordinate
sınıfın her türlü sunum kaygısını ana alanınıza getirecek olan bir koordinattan ekrandaki bir noktaya dönüştürülmesinin sorumluluğunu alması uygunsuz olacaktır. Ne yazık ki bu tür bir durum OO tasarımında doğaldır.
Çok yaygın olarak seçilen ilk seçenek sadece lanet alıcıları ortaya çıkarmak ve cehenneme gitmektir. Bu basitlik avantajına sahiptir. Ancak alıcılardan kaçınmaktan bahsettiğimiz için, tartışmanın uğruna, bunu reddediyoruz ve başka hangi seçeneklerin olduğunu görelim.
İkinci seçenek , .ToDTO()
sınıfınıza bir tür yöntem eklemektir . Buna - veya benzer şekilde - yine de, örneğin, oyunu kurtarmak istediğinizde, eyaletinizin neredeyse tamamını yakalamak için ihtiyaç duyduğunuzda gerekli olabilir. Ancak bunu hizmetleriniz için yapmakla ve yalnızca alıcıya doğrudan erişmek arasındaki fark az çok estetiktir. Hala ona göre "kötülük" var.
Zoran Horvat'ın birkaç Çoğul Görüş videosunda savunduğunu gördüğüm üçüncü bir seçenek , ziyaretçi modelinin değiştirilmiş bir versiyonunu kullanmak. Bu, alışılmadık bir kullanım ve örüntü çeşididir ve insanların kilometresinin, gerçek bir kazanç elde etmek için karmaşıklık ekleyip eklemediği veya durum için iyi bir uzlaşma olup olmadığına göre büyük ölçüde değişeceğini düşünüyorum. Buradaki fikir temel olarak standart ziyaretçi kalıbını kullanmaktır, ancak Visit
yöntemlerin ziyaret ettikleri sınıfın yerine ihtiyaç duydukları durumu parametre olarak almasını sağlayın. Örnekler burada bulunabilir .
Sorunumuz için, bu modeli kullanan bir çözüm şöyle olacaktır:
public class Coordinate
{
private readonly int _x;
private readonly int _y;
public T Transform<T>(IPositionTransformer<T> transformer)
{
return transformer.Transform(_x,_y);
}
}
public interface IPositionTransformer<T>
{
T Transform(int x, int y);
}
//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
private readonly float _tileWidth;
private readonly float _tileHeight;
private readonly Vector2 _topLeft;
Vector2 Transform(int x, int y)
{
return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
}
}
Muhtemelen söyleyebileceğiniz gibi, _x
ve _y
artık gerçekten kapsüllenmemiş. IPositionTransformer<Tuple<int,int>>
Onları doğrudan döndüren bir tane yaratarak onları çıkarabiliriz . Tadı bağlı olarak, bunun tüm egzersizi anlamsız hale getirdiğini hissedebilirsiniz.
Ancak, kamu alıcıları ile işleri yanlış bir şekilde yapmak çok kolaydır, sadece verileri doğrudan çekmek ve Tell, Ask Sorma ihlaliyle kullanmak çok kolaydır . Bu kalıbı kullanırken, aslında doğru şekilde yapmak daha kolaydır : davranış oluşturmak istediğinizde, onunla ilişkilendirilmiş bir tür oluşturarak otomatik olarak başlarsınız. TDA ihlalleri çok açık bir şekilde koklayacak ve muhtemelen daha basit ve daha iyi bir çözüm etrafında çalışmayı gerektirecektir. Uygulamada, bu noktalar, doğru yapmayı kolaylaştırıyor, OO, alıcıların cesaretlendirdiği “kötülük” yönteminden daha kolay.
Son olarak , başlangıçta açık olmasa bile, aslında durumu açığa vurmaktan kaçınmak için davranış olarak ihtiyacınız olanı yeterince ortaya çıkarmanın yolları olabilir . Örneğin, Coordinate
tek genel üyesi olan Equals()
(uygulamada tam bir IEquatable
uygulamaya ihtiyaç duyacağı ) önceki sürümümüzü kullanarak , aşağıdaki sınıfı sunum katmanınıza yazabilirsiniz:
public class CoordinateToVectorTransformer
{
private Dictionary<Coordinate,Vector2> _coordinatePositions;
public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
{
for(int x=0; x<boardWidth; x++)
{
for(int y=0; y<boardWidth; y++)
{
_coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
}
}
}
private static Vector2 GetPosition(int x, int y)
{
//Some implementation goes here...
}
public Vector2 Transform(Coordinate coordinate)
{
return _coordinatePositions[coordinate];
}
}
Belki şaşırtıcı bir şekilde, bir koordinattan hedefimize ulaşmak için gerçekten ihtiyaç duyduğumuz tüm davranışların eşitlik kontrolü olduğu ortaya çıktı! Elbette, bu çözüm bu soruna uyarlanmıştır ve kabul edilebilir bellek kullanımı / performansı hakkında varsayımlarda bulunur. Genel bir çözüm için bir taslaktan ziyade, bu özel problem alanına uyan bir örnek.
Ve yine, görüşler pratikte bunun gereksiz bir karmaşıklık olup olmadığına bağlı olarak değişecektir. Bazı durumlarda, bunun gibi bir çözüm bulunmayabilir veya yasaklayıcı bir şekilde tuhaf veya karmaşık olabilir, bu durumda yukarıdaki üçe geri dönebilirsiniz.