2010-02-16 14 views
11

Tôi có nghi ngờ này trong một thời gian dài ... hy vọng bất cứ ai cũng có thể khai sáng cho tôi.đa hình và các ứng dụng n-tier

Giả sử tôi có 3 lớp trong mô hình của mình.

abstract class Document {} 
class Letter extends Document {} 
class Email extends Document {} 

và lớp dịch vụ có phương thức trả về tài liệu (thư hoặc email).

class MyService { 
    public Document getDoc(){...} 
} 

Vì vậy, trong điều khiển của tôi, tôi muốn để hiển thị các tài liệu trả về bởi MyService, và tôi muốn nó sẽ được hiển thị bằng một cái nhìn cho email và khác cho Thư. Làm cách nào một trình điều khiển có thể biết chế độ xem tài liệu nào được gọi? letterView hoặc emailView ?.

Thường thì tôi thực hiện câu lệnh if trên bộ điều khiển để kiểm tra loại tài liệu mà tầng dịch vụ nhận được ... tuy nhiên tôi không nghĩ đó là cách tiếp cận tốt nhất từ ​​quan điểm OOP, nếu tôi thực hiện một vài boolean phương pháp Document.isLetter(), Document.isEmail() các giải pháp, về bản chất, giống nhau.

Một điều khác là ủy quyền lựa chọn chế độ xem cho Tài liệu bằng cách nào đó. một cái gì đó như:

class MyController { 
    public View handleSomething() { 
     Document document = myService.getDocument(); 
     return document.getView(); 
    } 
} 

Nhưng, omg, tại sao đối tượng mô hình của tôi phải biết gì về chế độ xem?

Bất kỳ toughts được đánh giá cao :)

Trả lời

11

Đây là một câu hỏi hay. Có nhiều cách tiếp cận hợp lý ở đây; bạn phải cân bằng sự cân bằng và đưa ra lựa chọn phù hợp với hoàn cảnh của bạn.

(1) Một số sẽ cho rằng giao diện Tài liệu nên cung cấp phương thức cho các trường hợp tự hiển thị. Điều này hấp dẫn từ quan điểm của OO, nhưng tùy thuộc vào công nghệ khung nhìn của bạn, có thể không thực tế hoặc hết sức xấu xí để tải các lớp Tài liệu cụ thể của bạn - đó có thể là các lớp mô hình miền đơn giản - với kiến ​​thức về JSP, Swing Components hoặc bất kỳ thứ gì.

(2) Một số sẽ đề xuất đặt một phương thức String getViewName() trên Document trả về, ví dụ: đường dẫn đến tệp JSP có thể hiển thị đúng loại tài liệu đó. Điều này tránh được sự xấu xí của # 1 ở một mức độ (phụ thuộc thư viện/"nâng hạng nặng"), nhưng khái niệm đặt ra cùng một vấn đề: mô hình miền của bạn biết rằng nó được kết xuất bởi JSP và nó biết cấu trúc của webapp của bạn.

(3) Mặc dù các điểm này, tốt hơn nếu lớp điều khiển của bạn không biết loại tài liệu nào tồn tại trong vũ trụ và loại mỗi thể hiện của Document thuộc về. Xem xét thiết lập một số loại ánh xạ xem trong một số loại tệp dựa trên văn bản: hoặc .properties hoặc .xml. Bạn có sử dụng Spring không? Spring DI có thể giúp bạn nhanh chóng chỉ định một Bản đồ các lớp tài liệu cụ thể và các thành phần JSP/view hiển thị chúng, sau đó chuyển nó tới một bộ dựng/constructor của lớp Controller của bạn. Cách tiếp cận này cho phép cả hai: (1) Mã điều khiển của bạn vẫn độc lập với các loại Document và (2) mô hình miền của bạn vẫn đơn giản và không thuyết phục về công nghệ xem. Nó đi kèm với chi phí của cấu hình gia tăng: hoặc .properties hoặc .xml.

Tôi muốn đi # 3 hoặc - nếu ngân sách của tôi (trong thời gian) để giải quyết vấn đề này là nhỏ - tôi muốn (4) chỉ cần viết mã một số kiến ​​thức cơ bản về các loại Document trong Trình kiểm soát của tôi (như bạn nói bạn đang làm bây giờ) với một cái nhìn hướng tới việc chuyển sang # 3 trong tương lai vào lần sau, tôi buộc phải cập nhật Bộ điều khiển của mình do các đặc tính OO ít hơn tối ưu. Thực tế là số 1-3 mất nhiều thời gian hơn và phức tạp hơn # 4, ngay cả khi chúng "chính xác hơn". Gắn bó với # 4 cũng là một cái gật đầu với số YAGNI Principal: không chắc chắn bạn sẽ bao giờ trải nghiệm những tác động tiêu cực của # 4, liệu có phải là có ý nghĩa để trả chi phí tránh chúng lên phía trước không?

+0

Tôi sẽ bỏ phiếu này hai lần nếu có thể. Câu trả lời rất hay. –

1

Có lẽ bạn có thể có một cái gì đó giống như getView() trong Document, trọng nó trong từng thực hiện?

+0

hi! xin lỗi cho -1, nhưng điều này không được thông báo. "Mô hình" đại diện cho dữ liệu nghiệp vụ [đôi khi chúng ta cũng bị lười biếng và thêm logic nghiệp vụ: S]. tuy nhiên, Model không biết phải trình bày như thế nào. xem xét ứng dụng "người dùng cuối" so với ứng dụng "quản trị". cả hai ứng dụng đều có thể sử dụng cùng một tầng nghiệp vụ và Mô hình, nhưng mỗi ứng dụng có thể muốn một Chế độ xem khác nhau có thể có nhiều dữ liệu hơn. nhúng Xem lựa chọn trong Mô hình sẽ hạn chế cùng một chế độ xem cho cả hai ứng dụng. trừ khi bạn thực hiện ghi đè, kết xuất phương thức khắc phục. nói chung, Mô hình phải là thuyết bất khả tri. –

2

Bộ điều khiển của bạn không được để biết. Nó sẽ yêu cầu các Document để hiển thị chính nó, và Document có thể làm điều này, hoặc cung cấp thông tin đầy đủ để cho phép xem xử lý này đa hình.

Hãy tưởng tượng nếu ở giai đoạn sau bạn thêm loại Document mới (giả sử, Spreadsheet). Bạn thực sự chỉ muốn thêm đối tượng Spreadsheet (kế thừa từ Document) và có mọi thứ hoạt động. Do đó, Spreadsheet cần cung cấp khả năng tự hiển thị.

Có lẽ nó có thể hoạt động độc lập. ví dụ.

new Spreadsheet().display(); 

Có lẽ nó có thể làm điều đó trong kết hợp với Xem ví dụ một đôi công văn cơ chế

new Spreadsheet().display(view); 

Trong cả hai trường hợp, các bảng tính/Thư/Email bài sẽ thực hiện phương pháp view() này và chịu trách nhiệm cho màn hình. Các đối tượng của bạn nên nói bằng một số ngôn ngữ xem-thuyết bất khả tri. ví dụ. tài liệu của bạn nói "hiển thị chữ in đậm này". Sau đó, chế độ xem của bạn có thể diễn giải nó theo loại của nó. Nếu đối tượng của bạn biết về chế độ xem? Có lẽ nó cần phải biết các khả năng mà khung nhìn đó có, nhưng nó sẽ có thể nói chuyện trong thời trang bất khả tri này mà không biết chi tiết xem.

+0

@ Brian Agnew - Tôi thích câu trả lời này nhưng tôi nghĩ nó khiến mọi người bối rối về cách thực hiện điều này. Mặc dù bạn đặt trạng thái 'new spreadsheet(). Display();' Tôi đảm bảo mọi người đặt câu hỏi hiển thị trông như thế nào ... mọi người sẽ kết thúc với kiểu kiểm tra mã (eachObject). Nếu bạn đi sâu hơn một chút về màn hình, tôi sẽ +1 bạn. :) – JonH

0

Mở rộng dịch vụ của bạn để trả lại loại tài liệu:

class MyService { 

    public static final int TYPE_EMAIL = 1; 
    public static final int TYPE_LETTER = 2; 

    public Document getDoc(){...} 
    public int getType(){...} 
} 

Trong một cách tiếp cận hướng đối tượng hơn, sử dụng một ViewFactory trở lại một cái nhìn khác nhau cho e-mail và chữ. Sử dụng quan điểm xử lý với một ViewFactory và bạn có thể hỏi mỗi người trong số các trình xử lý nếu nó có thể xử lý tài liệu:

class ViewFactory { 
    private List<ViewHandler> viewHandlers; 

    public viewFactory() { 
     viewHandlers = new List<ViewHandler>(); 
    } 

    public void registerViewHandler(ViewHandler vh){ 
     viewHandlers.add(vh); 
    } 

    public View getView(Document doc){ 
     for(ViewHandler vh : viewHandlers){ 
      View v = vh.getView(doc); 
      if(v != null){ 
      return v; 
      } 
     } 
     return null; 
    } 
} 

Với nhà máy này, lớp nhà máy của bạn không cần phải thay đổi khi bạn thêm các loại quan điểm mới. Các loại chế độ xem có thể kiểm tra xem liệu chúng có thể xử lý loại tài liệu được cung cấp hay không. Nếu họ không thể họ có thể trả về null. Nếu không, họ có thể trả lại chế độ xem bạn cần. Nếu không có khung nhìn nào có thể xử lý tài liệu của bạn, thì null được trả về.

Các ViewHandlers có thể được thực sự rất đơn giản:

public interface ViewHandler { 
    public getView(Document doc) 
} 

public class EmailViewHandler implements ViewHandler { 
    public View getView(Document doc){ 
     if(doc instanceof Email){ 
     // return a view for the e-mail type 
     } 
     return null; // this handler cannot handle this type 
    } 
} 
+0

Câu hỏi là về thực hành lập trình hướng đối tượng tốt. Câu trả lời của bạn sử dụng phương pháp thủ tục, không phải là cách tiếp cận hướng đối tượng. Vấn đề là cách tiếp cận thủ tục không quy mô tốt vì "mã khách hàng" được kết hợp mạnh mẽ với việc thực hiện hiện tại của "mã thư viện". – richj

+0

Tôi nhận ra điều đó. Tôi hy vọng rằng câu trả lời hiện tại của tôi phù hợp với câu hỏi tốt hơn. – Scharrels

+0

Chắc chắn - nhưng nếu bạn cần kiểm tra kiểu, tôi sẽ có khuynh hướng đưa chúng vào lớp nhà máy. Bạn có thể làm tốt hơn điều này bằng cách đăng ký trình xử lý đối với lớp xem. – richj

2

Tôi không chắc chắn, nhưng bạn có thể thử thêm một lớp nhà máy dựa trên chức năng trọng, và giả định trở lại một cái nhìn tùy thuộc vào loại tài liệu. Ví dụ:

class ViewFactory { 
    public View getView(Letter doc) { 
     return new LetterView(); 
    } 
    public View getView(Email doc) { 
     return new EmailView(); 
    } 
} 
+0

Mẫu "Nhà máy" là cách tốt nhất tôi nghĩ. Nó phải nằm trong gói "phổ biến", nơi giao diện của bạn cũng vậy. +1 –

+0

Tôi không nghĩ điều đó sẽ hiệu quả. Để gọi phương thức ViewFactory.getView(), bạn cần tham chiếu về kiểu thích hợp (Thư hoặc Email) nhưng dịch vụ trả về một Tài liệu, để lại cho chúng tôi vấn đề ban đầu. Thanx! – Mauricio

+0

Tôi nghĩ rằng dịch vụ trả về một đối tượng thuộc loại cụ thể, được đưa xuống tài liệu. Liệu tôi có sai? – woo

1

Tôi đã thấy "mẫu" này nhiều lần trong công việc của mình và đã thấy nhiều cách tiếp cận để giải quyết nó. Vào vấn đề, tôi sẽ đề nghị

  1. Tạo một dịch vụ mới IViewSelector

  2. Thực hiện IViewSelector, hoặc bằng cách hardcoding ánh xạ hoặc bằng cách cấu hình, và ném NotSupportedException bất cứ khi nào một yêu cầu không hợp lệ được thực hiện.

này thực hiện việc lập bản đồ bạn cần trong khi tạo điều kiện Tách quan tâm [SoC]

// a service that provides explicit view-model mapping 
// 
// NOTE: SORRY did not notice originally stated in java, 
// pattern still applies, just remove generic parameters, 
// and add signature parameters of Type 
public interface IViewSelector 
{ 

    // simple mapping function, specify source model and 
    // desired view interface, it will return an implementation 
    // for your requirements 
    IView Resolve<IView>(object model); 

    // offers fine level of granularity, now you can support 
    // views based on source model and calling controller, 
    // essentially contextual views 
    IView Resolve<IView, TController>(object model); 

} 

Như một ví dụ về việc sử dụng, hãy xem xét

public abstract Document { } 
public class Letter : Document { } 
public class Email : Document { } 

// defines contract between Controller and View. should 
// contain methods common to both email and letter views 
public interface IDocumentView { } 
public class EmailView : IDocumentView { } 
public class LetterView : IDocumentView { } 

// controller for a particular flow in your business 
public class Controller 
{ 
    // selector service injected 
    public Controller (IViewSelector selector) { } 

    // method to display a model 
    public void DisplayModel (Document document) 
    { 
     // get a view based on model and view contract 
     IDocumentView view = selector.Resolve<IDocumentView> (model); 
     // er ... display? or operate on? 
    } 
} 

// simple implementation of IViewSelector. could also delegate 
// to an object factory [preferably a configurable IoC container!] 
// but here we hard code our mapping. 
public class Selector : IViewSelector 
{ 
    public IView Resolve<IView>(object model) 
    { 
     return Resolve<IView> (model, null); 
    } 

    public IView Resolve<IView, TController>(object model) 
    { 
     return Resolve<IView> (model, typeof (TController)); 
    } 

    public IView Resolve<IView> (object model, Type controllerType) 
    { 
     IVew view = default (IView); 
     Type modelType = model.GetType(); 
     if (modelType == typeof (EmailDocument)) 
     { 
      // in this trivial sample, we ignore controllerType, 
      // however, in practice, we would probe map, or do 
      // something that is business-appropriate 
      view = (IView)(new EmailView(model)); 
     } 
     else if (modelType == typeof (LetterDocument)) 
     { 
      // who knows how to instantiate view? well, we are 
      // *supposed* to. though named "selector" we are also 
      // a factory [could also be factored out]. notice here 
      // LetterView does not require model on instantiation 
      view = (IView)(new LetterView()); 
     } 
     else 
     { 
      throw new NotSupportedOperation (
       string.Format (
       "Does not currently support views for model [{0}].", 
       modelType)); 
     } 
     return view; 
    } 
} 
+0

+1 cho lý thuyết nhưng việc thực hiện có thể được cải thiện đáng kể.Sử dụng Generics, bạn sẽ có thể loại bỏ các tham chiếu đến các đối tượng và kiểm tra/đúc kiểu rõ ràng. – CurtainDog

1

Các mô hình của khách sau đây có thể làm việc ở đây :

abstract class Document { 
    public abstract void accept(View view); 
} 

class Letter extends Document { 
    public void accept(View view) { view.display(this); } 
} 

class Email extends Document { 
    public void accept(View view) { view.display(this); } 
} 

abstract class View { 
    public abstract void display(Email document); 
    public abstract void display(Letter document); 
} 

Khách truy cập là một trong những mô hình gây tranh cãi nhiều hơn, mặc dù có một số biến thể cố gắng khắc phục các hạn chế của mẫu ban đầu. Sẽ dễ thực hiện hơn nếu phương thức accept (...) có thể được thực hiện trong Document, nhưng mô hình dựa trên kiểu tĩnh của tham số "this", vì vậy tôi không nghĩ rằng điều này là có thể Java - bạn phải lặp lại chính mình trong trường hợp này bởi vì kiểu tĩnh của "this" phụ thuộc vào lớp đang nắm giữ việc triển khai thực hiện.

Nếu số lượng loại tài liệu tương đối nhỏ và khó phát triển và số loại chế độ xem có nhiều khả năng phát triển hơn, thì điều này sẽ hiệu quả. Nếu không, tôi sẽ tìm cách tiếp cận sử dụng một lớp thứ ba để điều phối hiển thị và cố gắng giữ cho View và Document độc lập. Cách tiếp cận thứ hai này có thể trông như thế này:

abstract class Document {} 
class Letter extends Document {} 
class Email extends Document {} 

abstract class View {} 
class LetterView extends View {} 
class EmailView extends View {} 

class ViewManager { 
    public void display(Document document) { 
     View view = getAssociatedView(document); 
     view.display(); 
    } 

    protected View getAssociatedView(Document document) { ... } 
} 

Mục đích của ViewManager là để kết hợp tài liệu (hoặc các loại tài liệu nếu chỉ có một tài liệu của một loại nhất định có thể được mở) với quan điểm các trường hợp (hoặc xem loại nếu chỉ một chế độ xem của một loại đã cho có thể được mở). Nếu một tài liệu có thể có nhiều chế độ xem được liên kết thì việc triển khai ViewManager sẽ giống như thế này:

class ViewManager { 
    public void display(Document document) { 
     List<View> views = getAssociatedViews(document); 

     for (View view : views) { 
      view.display(); 
     } 
    } 

    protected List<View> getAssociatedViews(Document document) { ... } 
} 

Logic kết hợp tài liệu xem phụ thuộc vào ứng dụng của bạn. Nó có thể đơn giản hoặc phức tạp như nó cần. Logic liên kết được đóng gói trong ViewManager vì vậy nó sẽ tương đối dễ thay đổi. Tôi thích những điểm mà Drew Wills thực hiện trong câu trả lời của ông về tiêm phụ thuộc và cấu hình.

1

Trước tiên, phản hồi của Drew Wills là tuyệt đối tuyệt vời - Tôi mới ở đây và tôi không có danh tiếng để bỏ phiếu cho nó được nêu ra, hoặc người nào khác tôi sẽ.

Thật không may, và điều này có thể là thiếu kinh nghiệm của riêng tôi, tôi không thấy bất kỳ cách nào mà bạn sẽ tránh làm ảnh hưởng đến một số mối quan tâm. Một cái gì đó sẽ phải biết loại xem để tạo ra cho một tài liệu nhất định - không có cách nào xung quanh đó.

Như Drew chỉ ra ở điểm số 3, bạn có thể đi với một số loại cấu hình bên ngoài có thể hướng dẫn hệ thống của bạn sử dụng loại Chế độ xem để sử dụng cho loại tài liệu nào.Điểm số 4 của Drew cũng là một cách tốt để đi, bởi vì mặc dù nó phá vỡ nguyên tắc Open/Closed (tôi tin đó là cái tôi đang nghĩ đến), nếu bạn chỉ có một số loại tài liệu phụ, nó có lẽ không thực sự đáng làm phiền.

Đối với một sự thay đổi trên mà điểm thứ hai, nếu bạn muốn tránh sử dụng kiểm tra loại, bạn có thể thực hiện một lớp nhà máy/phương pháp dựa trên một bản đồ của tài liệu loại phụ để xem ví dụ:

public final class DocumentViewFactory { 
    private final Map<Class<?>, View> viewMap = new HashMap<Class<?>, View>(); 

    private void addView(final Class<?> docClass, final View docView) { 
     this.viewMap.put(docClass, docView); 
    } 

    private void initializeViews() { 
     this.addView(Email.class, new EmailView()); 
     this.addView(Letter.class, new LetterView()); 
    } 

    public View getView(Document doc) { 
     if (this.viewMap.containsKey(doc.getClass()) { 
      return this.viewMap.get(doc.getClass()); 
     } 

     return null; 
    } 
} 

Trong số tất nhiên, bạn vẫn cần chỉnh sửa phương thức initializeViews bất cứ khi nào bạn cần thêm chế độ xem mới vào bản đồ - vì vậy nó vẫn vi phạm OCP - nhưng ít nhất các thay đổi của bạn sẽ được tập trung vào lớp Nhà máy thay vì bên trong điều khiển.

(Tôi chắc rằng có rất nhiều có thể được tinh chỉnh trong ví dụ đó - xác nhận, đối với một -. Nhưng nó phải là tốt, đủ để có được một ý tưởng tốt về những gì tôi nhận được tại)

Hi vọng điêu nay co ich.

1

Cứ làm đi!

public class DocumentController { 
    public View handleSomething(request, response) { 
     request.setAttribute("document", repository.getById(Integer.valueOf(request.getParameter("id")))); 

     return new View("document"); 
    } 
} 

...

// document.jsp 

<c:import url="render-${document.class.simpleName}.jsp"/> 

Không có gì khác!

+0

@Mauricio Như được hiển thị ở trên, Nó có thể xuất ra hoặc gửi email.jsp hoặc render-Letter.jsp –