层次结构和设计模式问题

5
我正在为系统建模一个文档类。该文档可以是两种类型之一:输入输出
如果类型是输入,则该文档有一个发送者。发送者可以是两种类型之一:个人或公司。
如果类型是输出,则该文档有一个接收者。接收者可以是三种类型之一:个人、公司或部门。
我不确定使用具有枚举的属性来表示文档类型还是使用基类文档层次结构和每种文档类型的两个类更好。
对于发送者和接收者,我不确定是否使用基类层次结构是一个好选择,因为这三种类型没有任何共同点(个人、公司、部门),如何避免无效的发送者。
如果您可以告诉我如何建模文档类,或者告诉我应该使用哪些设计模式,那将很好。
谢谢。
“输入”和“输出”之间只有几个不同之处,相同字段除了发送者和接收者。此外,行为相同但有些微小变化。
发送者和接收者没有特定行为,它们所要做的仅仅是包含正确的对象。例如,发送者可以包含个人或公司,但不能包含部门,因为部门不是有效的发送者。另外,如果发送者包含一个人,则不能再包含公司,因为只有一个发送者被接受。
主要问题是如何在获取文档并阅读数据时读取发送者或接收者的信息。例如,如果我需要读取发送者,并使用包含发送者类型的枚举,则必须编写类似这样的代码:如果发送者==个人,则读取个人并将其分配给个人,否则读取公司并将其分配给公司。如果使用继承,我如何避免使用Cast或如何知道发送者是人还是公司而不需要太多代码或Cast?再次感谢。

1
你计划使用这些文档进行哪些操作?它们如何依赖于输入/输出等因素? - Andrei
@Fdo. - 我支持 @Andrei 的问题。具体来说,当您有一个文档时,您通常在编译时知道它是输入文档还是输出文档吗?还是您将在运行时大多数情况下处理文档类型(输入/输出)? - Stephen Swensen
安德烈,根据类型、发送方或接收方的某些验证,我必须保存文档的信息。此外,我还需要获取该文档。 - Fdo.
史蒂芬,我知道在运行时。 - Fdo.
@Andrei,我需要在某些验证之后根据类型、发件人或收件人保存文档信息。此外,我还需要获取该文档。谢谢。 - Fdo.
显示剩余2条评论
5个回答

1
个人而言,我并不认为将文档类建模成层次结构有特别的优势,因为内部和外部文档之间的差异非常小。特别是考虑到如果文档中的类型有一个发送者,即使它是隐式的,它也有一个接收者(您)。对于输出文档也是如此。因此,根据您提供的信息,我会使用枚举来区分文档(输入和输出)和类型(个人、公司、部门)。
尽可能遵循kiss principle。无论如何,您还必须考虑您将要对文档执行的操作类型以及它们需要存储的其他数据。如果您发现随着应用程序的增长,输入和输出文档之间的差异可能会增加,那么引入层次结构可能是一个好主意。(即使在这种情况下,我也会保持类型分离,使用一些类似枚举的结构来存储它们)
编辑:
关于您的评论:也许存在。我不知道您计划使用哪种语言。但是,例如,在Java中,每个实体(人、公司等)都可以是实现接口(例如称为Entity的接口)的类。然后使用泛型,您可以实例化您的类,并强制泛型类型为实现实体接口的实现。类似这样:
public interface Entity{...}
public class Document<T implements Entity>{}

感谢您的回答。根据您的建议,我将使用枚举来表示文档类型,因为内部和外部之间没有太大的区别。对于发件人和收件人,在保存文档时没问题,但是每次需要读取发件人或收件人时,我都需要编写一些代码。例如,如果发件人==个人,则读取个人,否则读取公司。我不知道是否有任何方法可以避免这种代码。 - Fdo.

1

如果您使用的语言允许对象实现接口,那么这将是处理复杂类型关系的好方法。

ISender可以由Person和Company实现。IReceiver可以由Person、Company和Department实现。

这样,即使这三种类型没有任何共同点,文档也可以持有对接收者的引用。

对于这两种类型的文档,它们之间的功能共享程度很大程度上取决于它们之间是否存在关系。如果没有任何共享功能,则根本没有必要建立关系。如果它们之间共享了很多功能,则包含一个抽象基类来实现它可能是值得的。如果它们完全(或几乎)相同,则一个带有输入/输出标志的单一类可能是一个好主意。


感谢您的回答。根据您的建议,我将使用枚举来表示文档类型,因为内部和外部之间没有太大的区别。 - Fdo.

0
基本上就是这样,如果有像if语句这样的东西。
if(document.IsIncoming()){
  do something
}elseif (document.SentToDepartment()) {
  do something else
}

如果在多个地方重复使用,最好采用某种多态解决方案(考虑抽象类或接口)。发送者和接收者类型也是如此。

但是,您不需要在顶部进行子类化。您可以通过多态将不同的行为附加到单个文档类上。假设打印行为对于传入和传出文档是不同的。然后,您创建IPrinter接口,并在两个类中实现它,如下所示。

public class Document
{
  DocumentType type;
  IPrinter printer;
}
interface IPrinter{
  print();
}

class IncomingPrinter :IPrinter{}

class OutgoingPrinter :IPrinter{}

无论何时决定一个文档将要进入(可能是在创建时),都要分配IncomingPrinter。如果需要分配多种类型的行为,通常会使用工厂模式。这样可以将if(doc.IsIncoming())语句局限于一个地方。不在代码的不同位置重复决策的好处很多。

为什么你需要从文档中引用打印机?为什么不是Print(Document)呢? - Boris Bucha
@Boris:在这个例子中的假设是打印功能对于传入和传出文档是不同的。我们想避免重复使用 if(doc.IsIncoming()) 这种类型的决策。IPrinter 接口正好做到了这一点。 - derdo
啊,好的,我明白了你的意思。我实际上错过了你回答中的“工厂”部分。顺便说一下,在像C# 4这样的语言中,这也可以通过Print(document)方法来实现访问者模式。这种方式不会导致类爆炸,这在强类型语言中是典型的访问者模式问题。(http://blogs.msdn.com/b/shawnhar/archive/2011/04/05/visitor-and-multiple-dispatch-via-c-dynamic.aspx) - Boris Bucha

0

对于建模在vs. out文档中,最佳设计取决于您通常如何处理文档。

如果您通常在编译时知道文档是输入/输出类型,并且除了发送方与接收方之外还有许多共享属性和行为,则层次结构将很好(因为您将拥有接受InDocument或OutDocument的方法,这些方法可以在编译时解析,甚至根据您的语言在运行时解析)。

另一方面,如果您的文档都混杂在一起,并且大多数情况下在运行时处理它们,则另一种解决方案(我很快会介绍)可能更清洁。我怀疑这几乎肯定适用于发送方和接收方类型,这几乎肯定不需要层次结构(尽管根据您的语言,您可能需要使用接口标记来模拟我即将介绍的功能解决方案)。在这些情况下,您想要的是一种“带数据的枚举”。在像F#这样的对象函数语言中,您可以组合记录类型和辨别联合以很好地进行建模:

type PersonInfo = {Name:string ; Age:int}

type CompanyInfo = {Name:string ; Location:string} 

type sender =
    | Person of PersonInfo
    | Company of CompanyInfo

type DepartmentInfo = {Name:string ; Floor: int}

type receiver =
    | Person of PersonInfo
    | Company of CompanyInfo
    | Department of DepartmentInfo

type documentType =
    | In of sender
    | Out of receiver

type Document = {Type:documentType ; Title:string ; Body:string ; Date:DateTime } with 
    //example of how to process a Document using pattern matching
    override this.ToString() =
        match this.Type with
        | In(s) -> 
            match s with
            | sender.Person(info) -> sprintf "In / Person, extra info: Name = %s ; Age= %i" info.Name info.Age
            | sender.Company(_) -> "In / Company"
        | Out(r) ->
            match r with
            | receiver.Person(_) -> "In / Person"
            | receiver.Company(_) -> "In / Company"
            | receiver.Department(_) -> "In / Department"


//create a list of In and Out documents all mixed up
let documents = 
   [{Type = In(sender.Person({Name="John"; Age=3})); Title="My In Doc"; Body="Hello World"; Date=DateTime.Now}
    {Type = Out(receiver.Department({Name="John"; Floor=20})); Title="My Out Doc"; Body="Testing"; Date=DateTime.MinValue}]

//partition documents into a list of In and Out Types

let inDocuments, outDocuments = 
    documents |> List.partition (function | {Type=In(_)} -> true | {Type=Out(_)} -> false)

0

我非常反对在这里使用层次结构。发送者和接收者显然是具有某些行为并携带一些数据的概念。我会专注于这些职责,并创建发送者和接收者实体。稍后,如果需要,可以创建这些实体的子类,例如公司发送者。基本上,如果整个系统不涉及发送者和接收者(我有点假设它不涉及),就不要污染公司实体与发送者逻辑。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接