Như đã nêu trong một vài câu trả lời và ý kiến, DTOs là phù hợp và hữu ích trong một số trường hợp, đặc biệt là trong việc chuyển dữ liệu qua các biên giới (ví dụ serializing để JSON để gửi thông qua một dịch vụ web). Đối với phần còn lại của câu trả lời này, tôi sẽ ít nhiều bỏ qua điều đó và nói về các lớp miền và cách chúng có thể được thiết kế để giảm thiểu (nếu không loại bỏ) getters và setters, và vẫn hữu ích trong một dự án lớn. Tôi cũng sẽ không nói về lý do tại sao loại bỏ getters hoặc setters, hoặc khi nào nên làm như vậy, bởi vì đó là những câu hỏi của riêng họ.
Ví dụ, hãy tưởng tượng rằng dự án của bạn là một trò chơi cờ như Cờ vua hoặc Chiến hạm. Bạn có thể có nhiều cách khác nhau để thể hiện điều này trong một lớp trình bày (ứng dụng bảng điều khiển, dịch vụ web, GUI, v.v.), nhưng bạn cũng có một miền cốt lõi. Một lớp bạn có thể có Coordinate
, đại diện cho một vị trí trên bảng. Cách "ác" để viết nó sẽ là:
public class Coordinate
{
public int X {get; set;}
public int Y {get; set;}
}
.
Xóa Setters: Bất biến
Trong khi các getters và setters công cộng đều có khả năng có vấn đề, setters là "ác" hơn nhiều của hai. Họ cũng thường dễ dàng hơn để loại bỏ. Quá trình này là một đơn giản - đặt giá trị từ bên trong hàm tạo. Thay vào đó, bất kỳ phương thức nào đã làm đột biến đối tượng sẽ trả về một kết quả mới. Vì thế:
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;
}
}
Lưu ý rằng điều này không bảo vệ chống lại các phương thức khác trong lớp đột biến X và Y. Để hoàn toàn bất biến hơn, bạn có thể sử dụng readonly
( final
trong Java). Nhưng dù bằng cách nào - cho dù bạn làm cho tài sản của mình thực sự bất biến hay chỉ ngăn chặn sự đột biến công khai trực tiếp thông qua setters- thì đó là mẹo để loại bỏ setters công khai của bạn. Trong phần lớn các tình huống, điều này hoạt động tốt.
Xóa Getters, Phần 1: Thiết kế cho hành vi
Trên đây là tất cả tốt và tốt cho setters, nhưng về mặt getters, chúng tôi thực sự tự bắn vào chân mình trước khi bắt đầu. Quá trình của chúng tôi là nghĩ về tọa độ là gì - dữ liệu mà nó đại diện - và tạo ra một lớp xung quanh đó. Thay vào đó, chúng ta nên bắt đầu với hành vi nào chúng ta cần từ tọa độ. Nhân tiện, quá trình này được hỗ trợ bởi TDD, nơi chúng tôi chỉ trích xuất các lớp như thế này một khi chúng tôi có nhu cầu, vì vậy chúng tôi bắt đầu với hành vi mong muốn và làm việc từ đó.
Vì vậy, hãy nói rằng nơi đầu tiên bạn thấy mình cần Coordinate
là để phát hiện va chạm: bạn muốn kiểm tra xem hai mảnh có chiếm cùng một không gian trên bảng không. Đây là cách "xấu xa" (các nhà xây dựng bị bỏ qua cho ngắn gọn):
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;
}
Và đây là cách tốt:
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
thực hiện viết tắt cho đơn giản). Bằng cách thiết kế cho hành vi thay vì mô hình hóa dữ liệu, chúng tôi đã quản lý để xóa getters của chúng tôi.
Lưu ý điều này cũng có liên quan đến ví dụ của bạn. Bạn có thể đang sử dụng ORM hoặc hiển thị thông tin khách hàng trên trang web hoặc thứ gì đó, trong trường hợp đó một loại Customer
DTO nào đó có thể có ý nghĩa. Nhưng chỉ vì hệ thống của bạn bao gồm khách hàng và họ được đại diện trong mô hình dữ liệu không tự động có nghĩa là bạn nên có một Customer
lớp trong miền của mình. Có thể khi bạn thiết kế cho hành vi, người ta sẽ xuất hiện, nhưng nếu bạn muốn tránh các getters, đừng tạo ra một cái trước.
Xóa Getters, Phần 2: Hành vi bên ngoài
Vì vậy, ở trên là một khởi đầu tốt, nhưng sớm hay muộn bạn có thể sẽ gặp phải tình huống bạn có hành vi liên quan đến một lớp, theo một cách nào đó phụ thuộc vào trạng thái của lớp, nhưng không thuộc về lớp. Loại hành vi này là những gì thường sống trong lớp dịch vụ của ứng dụng của bạn.
Lấy Coordinate
ví dụ của chúng tôi , cuối cùng bạn sẽ muốn thể hiện trò chơi của mình cho người dùng và điều đó có thể có nghĩa là vẽ lên màn hình. Ví dụ, bạn có thể có một dự án UI dùng Vector2
để thể hiện một điểm trên màn hình. Nhưng nó sẽ không phù hợp khi Coordinate
lớp chịu trách nhiệm chuyển đổi từ tọa độ sang điểm trên màn hình - điều đó sẽ mang tất cả các loại lo ngại về trình bày vào miền cốt lõi của bạn. Thật không may loại tình huống này là cố hữu trong thiết kế OO.
Tùy chọn đầu tiên , được lựa chọn rất phổ biến, chỉ là phơi bày những getters chết tiệt và nói với địa ngục với nó. Điều này có lợi thế của sự đơn giản. Nhưng vì chúng ta đang nói về việc tránh các getters, hãy nói vì lý do chúng ta từ chối cái này và xem những lựa chọn khác có.
Tùy chọn thứ hai là thêm một số loại .ToDTO()
phương thức vào lớp của bạn. Điều này - hoặc tương tự - dù sao cũng có thể cần thiết, ví dụ như khi bạn muốn lưu trò chơi, bạn cần nắm bắt khá nhiều trạng thái của mình. Nhưng sự khác biệt giữa làm điều này cho các dịch vụ của bạn và chỉ cần truy cập trực tiếp vào getter là ít nhiều mang tính thẩm mỹ. Nó vẫn còn nhiều "điều ác" với nó.
Tùy chọn thứ ba - mà tôi đã thấy Zoran Horvat ủng hộ trong một vài video Pluralsight - là sử dụng phiên bản sửa đổi của mẫu khách truy cập. Đây là một cách sử dụng và biến thể khá khác thường của mô hình và tôi nghĩ rằng số dặm của mọi người sẽ thay đổi ồ ạt vào việc liệu nó có thêm phức tạp để không có lợi ích thực sự hay liệu đó có phải là một sự thỏa hiệp tốt đẹp cho tình huống hay không. Ý tưởng về cơ bản là sử dụng mẫu khách truy cập tiêu chuẩn, nhưng có các Visit
phương thức lấy trạng thái họ cần làm tham số, thay vì lớp họ đang truy cập. Ví dụ có thể được tìm thấy ở đây .
Đối với vấn đề của chúng tôi, một giải pháp sử dụng mẫu này sẽ là:
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);
}
}
Như bạn có thể nói, _x
và _y
không thực sự gói gọn nữa. Chúng ta có thể trích xuất chúng bằng cách tạo một IPositionTransformer<Tuple<int,int>>
cái mà chỉ cần trả về chúng trực tiếp. Tùy thuộc vào khẩu vị, bạn có thể cảm thấy điều này làm cho toàn bộ bài tập trở nên vô nghĩa.
Tuy nhiên, với các công cụ thu thập công khai, việc thực hiện sai cách rất dễ dàng, chỉ cần lấy dữ liệu ra trực tiếp và sử dụng nó vi phạm Tell, Đừng hỏi . Trong khi sử dụng mẫu này thực sự đơn giản hơn để thực hiện đúng cách: khi bạn muốn tạo hành vi, bạn sẽ tự động bắt đầu bằng cách tạo một loại được liên kết với nó. Vi phạm TDA sẽ rất rõ ràng và có thể yêu cầu làm việc xung quanh một giải pháp đơn giản hơn, tốt hơn. Trong thực tế, những điểm này làm cho việc thực hiện nó trở nên dễ dàng hơn nhiều, OO, cách hơn là cách "xấu xa" mà các getters khuyến khích.
Cuối cùng , ngay cả khi điều đó không rõ ràng ban đầu, trên thực tế có thể có nhiều cách để phơi bày đủ những gì bạn cần là hành vi để tránh cần phải phơi bày trạng thái. Ví dụ: sử dụng phiên bản trước của chúng tôi Coordinate
có thành viên công khai duy nhất Equals()
(trong thực tế, nó sẽ cần IEquatable
thực hiện đầy đủ ), bạn có thể viết lớp sau trong lớp trình bày của mình:
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];
}
}
Hóa ra, có lẽ đáng ngạc nhiên, tất cả các hành vi chúng ta thực sự cần từ một tọa độ để đạt được mục tiêu của chúng tôi là kiểm tra sự bình đẳng! Tất nhiên, giải pháp này phù hợp với vấn đề này và đưa ra các giả định về việc sử dụng / hiệu suất bộ nhớ chấp nhận được. Đây chỉ là một ví dụ phù hợp với miền vấn đề cụ thể này, chứ không phải là một kế hoạch chi tiết cho một giải pháp chung.
Và một lần nữa, ý kiến sẽ khác nhau về việc trong thực tế điều này là phức tạp không cần thiết. Trong một số trường hợp, không có giải pháp nào như thế này có thể tồn tại, hoặc nó có thể cực kỳ kỳ lạ hoặc phức tạp, trong trường hợp đó bạn có thể trở lại ba điều trên.