OO设计建议 - toString方法

8

我得到了 Address 类:

class Address 
{
    private String streetAddress;
    private int number;
    private String postalCode;
    private City city;
    private State state;
    private Country country;
}

我想要将其可读版本显示在网格列中,最好的简洁实现方式是什么?

  1. Address 类中实现 toString 方法(我个人不喜欢这种方法,因为“toString”与地址没有直接关系
  2. 创建 ReadableAddressFormatter
    • ReadableAddressFormatter(Address addressToFormat)
    • public String getFormatted()
  3. 上述类,但将 getFormmated 设为静态方法,接收 Address 实例并返回字符串
  4. 其他?请提出建议。

我正在寻找一个良好的设计,注重清晰的代码解耦可维护性


3
我会选择Option 2,因为它将模型与展示内容分离。 - Deleted
3
我不同意你的说法,即"'toString'与地址本身没有直接关系"。打印地址是地址的核心目的。 - Chris Pfohl
3
ToString用于返回一个对象的字符串表示形式,它并不是演示工具,而是基础设施方面的关注点。 - Deleted
5
@Mr.Disappointment:它被用于一些简单的场景(如日志记录、简单结构和基本类型等),但是对于正确呈现方面,你应该有某种分离的转换/转化实现,就像WPF和MVC一样。上述的“Address”是一个未封装的复杂类型,可能有几种表现模式,因此如果你使用ToString,你将违反 a)SRP 和 b)Liskov 替换原则。另外,国际化方面怎么办?如果必须包含几个国际地址格式,则 SRP 将被彻底摧毁。 - Deleted
1
@ChrisSmith 可以说这是一种定制的互操作实现尝试,但同样,在.NET中有一个具体的基础可以建立在上面,而这并未引起注意,且演示了违反这些机制,因此,“正确”的答案在两种语言之间的具体细节上确实会有所不同。但是,为了避免小题大作,我可以到此为止,只是提出异议。 - Grant Thomas
显示剩余7条评论
7个回答

7
所有这些方法都被使用过,没有一种“上下文无关”的最佳实践。在软件工程中,最好的答案通常是“取决于情况”。话虽如此,让我们分析一下每个方法:
1. KISS方法是最简单的方法。我用它来处理基本的“打印到控制台,确保事情正常运行”之类的事情。如果您有一个特定的地址格式,那么这是低成本/易获得胜利的解决方案。您始终可以覆盖此方法或以不同方式打印对象。
2. 这是最具可扩展性的解决方案,因为它将很好地允许本地化和自定义格式。是否适用取决于您预计地址以不同格式显示的频率。您真的需要死星来消灭苍蝇,还是能够更改为全大写或在不同语言之间切换对您的应用程序至关重要?
3. 我不建议使用此方法,因为它通常会将“视图级别”逻辑渗入域中,而这通常最好由其他层(在类MVC方法中)处理。有人可能会认为toString()也会做同样的事情,但toString()也可以被认为是对象在外部世界中出现的“名称”或“本质”,因此我认为它不仅仅是呈现。
希望这可以帮到你,并且赞扬你从一开始就考虑了代码的清晰、解耦和可维护性。
以下是第二个原则的实例——使用策略模式,遵循单一职责原则开闭原则和通过依赖注入实现控制反转。请参考下面的方法(由@SteveJ慷慨提供):
public class Address {
        private String streetAddress;
        private int number;
        private String postalCode;
        private String city;
        private String state;
        private String country;

        public String toLongFormat(){
            return null; // stitch together your long format
        }

        public String toShortFormat(){
            return null; // stitch together your short format
        }

        public String toMailingLabelFormat(){
            return null; // stitch together your mailing label format
        }

        @Override
        public String toString(){
            return toShortFormat(); // your default format
        }
    }

}

使用这个(在“大部分正确”的Groovy中):

public interface AddressFormatter {
   String format(Address toFormat)
}

public class LongAddressFormatter implements AddressFormatter {
    @Override
    public String format(Address toFormat){
         return String.format("%sBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH%n%s", toFormat.streetAddress, toFormat.postalCode)
    }
}


public class ShortAddressFormatter implements AddressFormatter {
    @Override
    public String format(Address toFormat){
         return String.format("%d", toFormat.number)
    }
}

public  class Address {
        private String streetAddress;
        private int number;
        private String postalCode;
        private String city;
        private String state;
        private String country;
        public  AddressFormatter formatter = new ShortAddressFormatter(); // just to avoid NPE

        public void setFormatter(AddressFormatter fr) { this.formatter = fr; }



        @Override
        public String toString(){
            return formatter.format(this); // your default format
        }
    }

def addrr = new Address(streetAddress:"1234 fun drive", postalCode:"11223", number:1)
addr.setFormatter(new LongAddressFormatter());
println "The address is ${addrr}"
addr.setFormatter(new ShortAddressFormatter());
println "The address is ${addrr}"

正如@SteveJ所观察到的:

"因此,您拥有不同的格式化“策略”,可以在它们之间切换...我有这样一个想法,您只需设置一次格式,然后就会被困住...如果您想要添加另一种格式化样式,则无需打开并重写地址类,而是编写一个新的单独样式,并在需要使用时注入它。"


2
你可能会遇到 setFormatter() 的竞态条件。我建议按照 DateDateFormat 的风格,将 Address 传递给 AddressFormatter,类似于 AddressFormatter.format(Address) - Mike Christianson

6

.NET解决方案:

覆盖Object.ToString()似乎是最合理的解决方案。这使得在以下情况下使用它更加清晰:Console.WriteLine("Home Address: {0}", homeAddress);

如果您希望提供其他格式化,则Address类应实现IFormattable

此外,您应该创建一个AddressFormatter类,该类从IFormatProviderICustomFormatter实现。

MSDN链接提供了非常好的示例(BinaryFormatter和AcctNumberFormat),但如果这些不足以满足您的需求,还可以查看这个很好的示例:PhoneFormatter


此外,如果您决定全力以赴并实现IFormattable和自定义的IFormatProvider/ICustomFormatter,那么我建议您的ToString()只是使用默认提供程序调用ToString(String format, IFormatProvider formatProvider)。这样,您就可以考虑本地化和地址类型(短,长等)等因素。


Steve J: 我不理解,既然这是一个C#问题,那有什么关系呢? - myermian
1
实际上,他将其标记为C#和Java,我想我应该将我的答案标记为.NET特定的,但我确信Java有一些类似的实现接口格式化字符串的方法。 - myermian

2
通常我将展示层与数据层分开。向GUI呈现似乎与展示层有关,而不是数据层。
我建议您在展示层的某个地方放置一个函数,用于将地址转换为字符串。
数据的呈现与数据无关!
静态方法很好。转换器类会更好,您可以为应用程序保留一个单一实例,但如果您要将应用程序从GUI移动到具有另一种格式的WEB,则可以替换它或编写另一个,或者如果您想要在一个窗口中显示所有内容,在另一个窗口中仅显示部分信息或以另一种方式格式化的信息。
有几种模型可以遵循,例如Microsoft WPF使用完全不同的方法,即MVVM,Model View View Model,它将允许您非常好地将数据层与业务逻辑与展示层分开。
我通常仅出于调试目的(呈现可用于调试的字符串)或某些简单序列化为字符串的目的(通常还放置一个FromString(或Java中的fromString方法))覆盖C#中的ToString或Java中的toString。一个例子是自定义类型,如Point,Vector,Matrix等。
谈论C#世界...
public class AddressToStringConverter
{
    public virtual string ToString(Address address)
    {
        return address.Street + ", " + address.City
    }
}

然后在您的表单中(例如)。
AddressToStringConverter myConverter = new AddressToStringConverter();

public Address CurrentSelectedAddress { get { ... } }

public button1_click(object sender, EventArgs e)
{
    button1.Text = myConverter.Convert(address);
}

如果您愿意,您可以实现其他有用的接口,例如ITypeConverter。

只是提醒一下,增强类的调试器信息有比使用 ToString() 方法更好的替代方案,可以使用 DebuggerDisplayAttribute,因为这正是它的设计目的:https://msdn.microsoft.com/zh-cn/library/ms228992(v=vs.110).aspx - myermian

2

使用toString不需要在函数本身之外添加任何额外的工具; 看起来是最简单的解决方案。它存在是有原因的,对吧?


5
不是因为这个原因。如果这个国家国际化了怎么办?如果地址的某些部分根据语言环境不同而显示顺序也不同怎么办?如果有时需要将其显示为文本,有时需要将其显示为HTML怎么办? - JB Nizet
也许一个超类会更合适,它可以覆盖toString()方法。 - williamg
这取决于上下文。如果应用程序的范围不涉及这些领域,那么没有太多理由为它们提供支持 - 除非您已经知道这是未来的可能性。 - Toomai
@JBNizet:对于大多数类而言,ToString()方法查看当前的国际化设置以确定要发出什么内容。这是一种完全有效的方法。 - NotMe

1

toString() 是最灵活和方便的方法,当您将 Address 类的对象与字符串组合时(例如 System.out.println("My address is " + objectOfAddressClass)),它会被隐式调用。

我能想到不覆盖 toString() 的唯一原因是如果您需要更改格式。然后,您需要不同的方法(例如 toMailingString() 和 toShortFormString() 等)或参数化方法(例如 toMailingString(boolean useShortForm) 或其他方法),但无论如何,toString() 都不能胜任。

当然,您可以(而且应该)两者兼备。将 toString() 作为默认值,可能调用其中一个特定格式的方法,然后使用其他辅助方法进行替代格式。

public class TestClass {

    class City{};

    class State{};

    class Country{};

    class Address {
        private String streetAddress;
        private int number;
        private String postalCode;
        private City city;
        private State state;
        private Country country;

        public String toLongFormat(){
            return null; // stitch together your long format
        }

        public String toShortFormat(){
            return null; // stitch together your short format
        }

        public String toMailingLabelFormat(){
            return null; // stitch together your mailing label format
        }

        @Override
        public String toString(){
            return toShortFormat(); // your default format
        }
    }

}

1
我认为当你需要依赖于格式时,永远不应该使用toString()。这使得它在除了调试和日志记录之外的用途非常可疑。唯一的例外是对于具有单个接受的显示格式的非常简单的类型。 - Robin
不确定您所说的“太多逻辑”是什么意思。我有一个类,其中备用格式很有用,并为每个格式编写了一个方法。其中一种格式作为我的默认格式用于实现toString()的有意义版本。有各种方法可以解决这个问题,比如将单个格式化方法参数化,但这样你就要用一个方法和一个包含3个成员的枚举来交换三个方法,似乎更费力而不是更省力。但我会研究这个单一职责原则。我想看看它会变得更加灵活(当然,“灵活”是“笨重”的相反词)。 - Steve J
2
你刚刚展示了框架。显然,“拼接”实现将具有内部逻辑/处理,导致行数急剧增加并导致混乱不堪的代码。Salvatore的方法在这里非常有效:“数据的呈现与数据本身无关!”我们可以争论所有与地址相关的功能都应该在Address中,“因为那是它的工作”,但这违背了Clean Code的精神。Robert Martin在他的书中很好地阐明了这些概念。我鼓励你去读一读。 - Visionary Software Solutions
1
@VisionarySoftwareSolutions那本书启发了我的问题。 - Yuri Ghensev
1
如果你想添加另一种格式样式,你不必打开并重写地址类,而是编写一个新的独立样式,并在需要使用它时注入它。这真是太有道理了。 - Steve J
显示剩余8条评论

0

我认为使用返回字符串的toString()方法是您最好的方法。如果您有一个地址实例,比如说address,那么address.toString()的作用是显而易见的。toString()方法与Address没有直接关联并不会改变任何东西。


0

你在帖子中标记了Java,所以我将为Java(更具体地说是Swing)回答。这个任务通常是特定的TableCellRenderer的任务。如果其他可视化组件必须使用相同的格式,则确实应该将格式提取到一个可实例化的类中(解决方案2)。这将允许子类根据需要自定义格式。


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