代码重构_重构代码

代码重构

介绍与推理 (Introduction and Reasoning)

If the term “Refactoring” is new to you, it is a way of saying that a software engineer thinks that the code in question is unstable, buggy, bloated, and generally bad enough to need a rewrite or “refactor.” It is basically software speak for “somebody incompetent wrote this POS, and I want to fix it.” This statement may even be true. But if you are a team lead or manager, be aware that well over half the time “refactoring” is just some engineer wanting to put their fingerprints all over the code, and the software in question doesn’t really have anything wrong with it.

如果“重构”一词对您来说是陌生的,那么这是一种表达方式,表示软件工程师认为所讨论的代码不稳定,有bug,肿并且通常很糟糕,需要重写或“重构”。 从根本上说,这是软件的代名词,“某位不称职的人写了这个POS,我想修复它。” 这个说法甚至可能是正确的。 但是,如果您是团队负责人或经理,请注意,超过一半的时间“重构”只是一些工程师想要在整个代码中使用其指纹,并且所涉及的软件确实没有任何问题。

There is another term related to refactoring that gets thrown around “Technical Debt,” which at its core means all of the compromises and quick fixes that build up over time in code to get it shipped. These compromises make it unstable, buggy, bloated, and generally not good. Most of the time, code with high technical debt is code that is ripe for refactoring. However, sometimes high technical debt code is working fine and should not be touched. At other times, code with low technical debt has other problems like visibly poor user performance or high resource use that make it a candidate for refactoring. There is no one reason to refactor your code, just experience to guide you on code that needs more than just a new coat of patches to fix.

还有另一个与重构相关的术语,即“技术债务”,它的核心是指随着时间的推移在代码中逐渐积累的所有折衷和快速修复方法,以使其上市。 这些妥协使它变得不稳定,有故障,肿并且通常不好。 在大多数情况下,具有高技术债务的代码是可以重构的成熟代码。 但是,有时高技术债务代码可以正常工作,不应被触及。 在其他时候,技术债务少的代码还有其他问题,例如明显的用户性能低下或资源占用过多,使其成为重构的候选对象。 重构代码没有任何理由,只是经验可以指导您完成所需的代码,而不仅仅是修补新补丁。

The first step is always to decide what makes sense and when it makes sense. This must be a team effort; having one engineer go off on their own and refactor a core component is a disaster. I speak from experience, as I have been both on a team that had this happen and been the brash engineer who did this foolish thing to the rest of the team. Unless the project is a one-woman show, do not refactor without consulting the rest of the team, and making sure what you are about to do will not impact them.

第一步始终是确定什么才有意义,什么时候才有意义。 这必须是团队合作; 让一名工程师独自一人去重构核心组件是一场灾难。 我是从经验上讲的,因为我既曾在发生过这种情况的团队中工作过,又是一位勇敢的工程师,对团队中的其他成员都做了这种愚蠢的事情。 除非该项目是单人表演,否则请在未咨询团队其他成员的情况下进行重构,并确保您将要做的事情不会对他们造成影响。

There are three styles of refactoring to think about when you are looking at code to refactor.

当您查看要重构的代码时,可以考虑三种重构风格。

There is what I call the hidden-refactor, where you keep the external interface to the software the same. So if it is an API, DLL, NuGet package, or library, all of the existing external connections are the same, maybe you add new interfaces or perhaps not. All changes are internal and not visible to the calling software. This style is the safest and most difficult refactor to pull off successfully because you don’t just have to keep documented interfaces the same, you have to keep side-effects and non-documented interfaces the same. This type is what Microsoft tried to do with Win32 for over a decade and failed hard doing, because of these non-documented interfaces that developers found used then complained when they changed with a new version of Win32.

我称之为“隐藏重构”,您可以在其中使软件的外部接口保持不变。 因此,如果它是API,DLL,NuGet包或库,则所有现有的外部连接都相同,也许您添加了新接口,也可能没有。 所有更改都是内部的,对调用软件不可见。 成功实现此样式是最安全,最困难的重构,因为您不仅需要保持文档化的接口相同,还必须保持副作用和未记录的接口相同。 这种类型是Microsoft尝试使用Win32十多年的尝试,但是失败了,因为开发人员发现使用了这些非文档化的接口,然后在使用新版本的Win32进行更改时抱怨了这种抱怨。

Then there is what I think of as the throw everything away method, where you start over from a blank screen and think about what the original software was supposed to do then write entirely new code to do it. Doing this is the default method for far too many engineers, both software and other types. Rip out the old, put up the new, Road, Buildings, Networks, or Software Systems it seems to be endemic of the engineering mindset to want to create something entirely new when given a chance. Attempting this is both expensive and fraught with risk for commercial projects. Again Microsoft, Silverlight, UWP, WCF are all examples of “throw out the existing method and get everyone to use the new.” Now to be fair, UWP hasn’t failed, and it looks like it might not, but it hasn’t taken over the world either, WCF has just disappeared, and Silverlight was rebranded and stuffed inside of .Net Core. But the lesson still holds, trying to force the users of your software to completely change the way they do things is a losing proposition even when you are one of the largest software companies in the world. Throwing the existing code away and starting over is seldom a good idea unless you are moving to a new platform, language, or environment that does not support your current codebase at all.

然后就是我认为的“扔掉一切”方法,从空白屏幕开始,考虑原始软件应该做什么,然后编写全新的代码来完成。 对于太多的软件和其他类型的工程师来说,这样做是默认的方法。 淘汰旧的,安装新的,道路,建筑物,网络或软件系统,这似乎是工程思想的特有现象,只要有机会就想创建全新的东西。 这样做既昂贵又充满商业项目的风险。 同样,Microsoft,Silverlight,UWP,WCF都是“丢弃现有方法并让所有人使用新方法”的示例。 现在公平地说,UWP并没有失败,而且看起来也可能没有失败,但它也没有接管世界,WCF刚刚消失,Silverlight被重新命名并塞入.Net Core中。 但是这一教训仍然存在,即使您是世界上最大的软件公司之一,试图强迫您的软件用户完全改变他们的做事方式也是一个失败的主张。 除非将现有的代码移至根本不支持当前代码库的新平台,语言或环境,否则很少丢掉现有代码并重新开始是个好主意。

The third is the trickiest but, in my opinion, the most useful one. Change what is needed if there are external interfaces that make sense, keep them. If there are blocks of clean, usable code, then use them. If there are interfaces that are ugly, horrible contrived messes, change them, but in a friendly way — I’ll give some ideas for this below. If some classes or methods are buggy, bloated messes, break them up into parts that make sense, and fix them. But, this involves being smart and aware that you might be breaking someone else’s code. So don’t just rip something out because you don’t like the format or rename it because “that name is stupid,” you have a responsibility to the people who are calling this code not to break their software with your changes. Unless there is a reason, and even then give warning where possible.

第三是最棘手的,但我认为是最有用的。 如果有有意义的外部接口,请更改所需内容,并保留它们。 如果有干净,可用的代码块,请使用它们。 如果存在丑陋,人为设计的混乱界面,请以友好的方式进行更改-我将在下面给出一些想法。 如果某些类或方法有错误,bug肿的混乱,请将它们分解成有意义的部分并加以修复。 但是,这涉及到聪明,并意识到您可能正在破坏别人的代码。 因此,不要因为您不喜欢该格式而将其删除,或者因为“那个名字很愚蠢”而将其重命名,您对那些要求使用此代码的人员有责任不要因所做的更改而破坏其软件。 除非有原因,否则在可能的地方发出警告。

代码重构_重构代码_第1张图片
ThisIsEngineering from Pexels的 Pexels ThisIsEngineering摄

如何和为什么 (How and Why)

With the warning out of the way I’ll jump into an example from my latest project, the same application I have been using for all of these articles the web-based star chart viewer that uses C# and .Net Core.

有了警告,我将跳入最新项目的示例,在所有这些文章中,我一直使用同一应用程序,即使用C#和.Net Core的基于Web的星图查看器。

Last week I had over 40,000 stars loaded into a SQL database, web pages that could view lists of star charts, view star systems, add planets to a star system, and view the planets in a star system. Everything was looking promising except for two things that were bugging me. First, the startup time of the first page was about two minutes; second loading the star system page was around a minute. Both of these times were far in excess of acceptable for the simple viewer version of the application, let alone the three-dimensional graphical view I have in mind for my end goal.

上周,我已在SQL数据库中加载了40,000多颗恒星,这些网页可以查看星图列表,查看恒星系统,将行星添加到恒星系统中以及在恒星系统中查看行星。 除了让我烦恼的两件事之外,其他一切看起来都不错。 首先,首页的启动时间约为两分钟; 第二次加载star系统页面大约需要一分钟。 这两个时间都远远超出了应用程序的简单查看器版本所能接受的范围,更不用说我为实现最终目标而想到的三维图形视图了。

With these problems in mind, I started looking at the performance problems. To no surprise, at all, I discovered that the basic model I had built was a horrible bottleneck hitting the database continuously. Even with the database on an SSD and caching turned up, the times stayed stubbornly high. So I walked through the code with a debugger and found that yes, it was the database calls, and no nothing that I did with minor changes in my code or on the database configuration end could fix the problem.

考虑到这些问题,我开始研究性能问题。 毫不奇怪,我发现我建立的基本模型一直是困扰数据库的可怕瓶颈。 即使将数据库放在SSD上并打开了缓存,时间仍然固然很高。 因此,我使用调试器浏览了代码,发现是的,这是数据库调用,并且对代码或数据库配置端的微小更改所做的任何操作都不能解决该问题。

Now, I will confess here that if I were still a manager or team lead at a major corporation, I would be recommending throwing hardware or cloud-resources at the problem. A fast server with enterprise-level SSD and memory would mask the issues that I am seeing and be less expensive to a company than the engineering time to refactor the code, test, and deploy the changes. Which is one reason that Technical Debut accumulates, hardware has gotten fast and cheap enough that throwing more hardware at the problem is almost always a less expensive solution for years before software fails. Even in this case, I could probably “throw hardware” at the problem by moving SQL up to my Azure account and running it on faster hardware. But, these solutions offend me (see engineers love fixing problems), and since I am the only person affected, I am refactoring.

现在,我将在这里承认,如果我仍然是一家大型公司的经理或团队负责人,那么我建议您将硬件或云资源投入到这个问题上。 具有企业级SSD和内存的快速服务器将掩盖我所看到的问题,并且对公司而言,与重构代码,测试和部署更改的工程时间相比,它的成本更低。 这是技术首次亮相的原因之一,硬件变得既便宜又便宜,以至于在出现软件故障之前的多年中,将更多的硬件投入该问题几乎总是一种较便宜的解决方案。 即使在这种情况下,我也可以通过将SQL移到我的Azure帐户并在更快的硬件上运行它来“抛出硬件”来解决问题。 但是,这些解决方案使我感到恼火(请参见工程师喜欢解决问题),并且由于我是唯一受影响的人,因此我正在进行重构。

I started by looking closely at the model I was using, which was a basic hierarchy of related classes:

我首先仔细查看所使用的模型,该模型是相关类的基本层次结构:

StarChart has a list of StarSystems
StarSystems has a list of Planets
Planet has an Atmosphere

There were other properties, but these are important from the database standpoint. What was in the Data Context was:

还有其他属性,但是从数据库的角度来看,这些属性很重要。 数据上下文中的内容是:

public DbSet StarCharts { get; set; }
public DbSet StarSystems { get; set; }
public DbSet Planets { get; set; }
public DbSet Atmospheres { get; set; }

So straightforward, the problem was at its core loading StarCharts ended up pulling in the whole database, and this problem would only get worse as more data was added. Remember that this was just the StarSystems so far since all I had were the raw star map data from astronomy databases. The source of the data was another issue I was facing. The StarSystem data had many fields that were less than useful for my purposes but took up database space and slowed the loading. For example, the astronomy data has four different name fields that use four different naming conventions — useful if you are an astronomer, less useful as the primary information about a star for my current needs. Also, the StarSystem had data that was very useful to my needs buried in a field that was in a less than useful format. The Spectral Class of a star gives a rough estimate of its mass and radius, both useful values for me, even if they are far too rough for detailed scientific use as more than a range or guideline.

如此简单,问题出在StarCharts的核心负载上,最终将其拖入整个数据库,并且随着添加更多数据,此问题只会变得更加严重。 请记住,到目前为止,这只是StarSystems,因为我所拥有的只是来自天文学数据库的原始星图数据。 数据来源是我面临的另一个问题。 StarSystem数据具有许多字段,这些字段对我的用途没有多大用处,但占用了数据库空间并减慢了加载速度。 例如,天文学数据有四个不同的名称字段,它们使用四种不同的命名约定-如果您是天文学家,则很有用;作为满足我当前需求的有关恒星的主要信息,它的用处不大。 此外,StarSystem的数据对我的需求非常有用,而这些数据被埋没在一个不太有用的格式中。 恒星的光谱等级粗略估计了它的质量和半径,这对我来说都是有用的值,即使它们太粗糙而无法用于详细的科学用途,也不仅仅是范围或准则。

So it was evident to me that I had several needs with my refactor. First speed, whatever I did had to be faster. Second, make a class hierarchy that makes more sense and has room for expansion. Third use Interfaces — I skipped this in my first pass, fourth simplify if possible, and fifth add more tests. This list is more “needs” than make sense since some of these are more of “want” or “yea I should do that” stage. So let’s prioritize from most important to least:

因此,对我来说很明显,我的重构有几个需求。 第一速度,无论我做什么都必须更快。 其次,建立一个更有意义并具有扩展空间的类层次结构。 第三次使用的界面-我在第一遍中跳过了这一点,如果可能的话,第四次简化,第五次添加更多测试。 该列表比“需求”更有意义,因为其中一些更像是“想要”或“是,我应该那样做”的阶段。 因此,让我们按从最重要到最不重要的顺序排列优先级:

  1. Speed up the code,

    加快代码速度,
  2. Simplify the code; this includes interfaces and class hierarchy, so combine all three as number 2.

    简化代码; 这包括接口和类层次结构,因此将这三个都合并为数字2。
  3. Write more tests. I should have discovered the speed issues during testing; I need some speed tests to make sure that number 1 is happening.

    编写更多测试。 我应该在测试过程中发现速度问题; 我需要进行一些速度测试,以确保1号正在发生。
  4. Make it possible to expand the code — this is a want, a “wouldn’t it be cool if” kind of feature. I will probably make it possible via interfaces to add classes that would fit into the class structure. But it will not be a “free” or “codeless” process. Just possible to have happened.

    使扩展代码成为可能-这是一种需求,一种“如果不是很酷”的功能。 我可能会通过接口添加适合该类结构的类,从而使其成为可能。 但这不是一个“免费”或“无代码”的过程。 可能发生了。

My target type of refactoring was to change what is needed; unfortunately, “what is needed” was quite extensive. I couldn’t start with my highest priority item because I had already tried all of the easy fixes for speeding up the code. So, I had to start with simplifying.

我重构的目标类型是更改所需的内容。 不幸的是,“需要什么”已经很广泛了。 我无法从优先级最高的项目开始,因为我已经尝试了所有简单的修复方法来加快代码的速度。 因此,我必须从简化开始。

The first step was to go to the model classes, look at what was shared, and write interfaces that encapsulated that functionality. The first interface I spotted right away was obvious, every model object I was working with had five properties, and five methods in common. Over half of the objects had an additional method, so I decided that the objects that didn’t implement it, should implement it. My first interface was:

第一步是进入模型类,查看共享的内容,并编写封装该功能的接口。 我立刻发现的第一个接口是显而易见的,我正在使用的每个模型对象都有五个属性和五个共同的方法。 超过一半的对象具有其他方法,因此我决定未实现该对象的对象应该实现它。 我的第一个界面是:

public interface IStarMapObject
{
[Key]
[Column(Order = 1)]
Guid Id { get; set; }
string Name { get; set; }
DateTime Created { get; set; }
DateTime LastUpdated { get; set; }
[Timestamp]
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
byte[] TimeStamp { get; set; }bool Equals(object obj);
int GetHashCode();
string ToString();
string ToString(string format);
string ToString(string format, IFormatProvider formatProvider);
string ToShortString();}

IStarMapObject is the base interface for all of the model classes in my application. Then I wrote a base class StarMapObject. I had planned on making it abstract. But as I worked, I realized that it made more sense to make some of the string manipulation objects virtual and implement base versions in the base. I ended up with a class that looked like:

IStarMapObject是应用程序中所有模型类的基本接口。 然后,我写了一个基类StarMapObject。 我曾计划将其抽象化。 但是当我工作时,我意识到将一些字符串操作对象虚拟化并在基础中实现基础版本更有意义。 我结束了一堂课,看起来像:

using TevandelSupportLibrary.Classes;
using Interfaces;
public class StarMapObject : IStarMapObject
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public DateTime LastUpdated { get; set; }
[Timestamp]
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public byte[] TimeStamp { get; set; }public StarMapObject(Guid id, string name, DateTime c, DateTime u)
{
Id = id;
Name = name;
Created = c;
LastUpdated = u;
}public StarMapObject(Guid id, string name) : this(id, name, DateTime.Now, DateTime.Now) { }public StarMapObject() : this(Guid.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue) { }public new bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() == typeof(StarMapObject) || obj is IStarMapObject)
{
StarMapObject smo = obj as StarMapObject;
bool result = Id.Equals(smo.Id) &&
Name.Equals(smo.Name);
return result;
}
return base.Equals(obj);
}
virtual public new int GetHashCode()
{
int hash = Id.GetHashCode();
hash = HashCodeHelper
.CombineHashCodes(hash, Name.GetHashCode());
return hash;
}
public new string ToString()
{
return ToString("g", CultureInfo.CurrentCulture);
}
public string ToString(string format)
{
return ToString(format, CultureInfo.CurrentCulture);
}
virtual public string ToString(string format,
IFormatProvider formatProvider)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append($"{Id}{separator} {Name}{separator} {Created}{separator} {LastUpdated}");
return sb.ToString();}
virtual public string ToShortString()
{
return $"{Name}";
}}

Then I wrote a set of tests for the base class to make sure it worked the way I expected. These tests look like:

然后,我为基类编写了一组测试,以确保它能按我期望的方式工作。 这些测试如下所示:

class StarMapObjectTests
{[SetUp]
public void Setup()
{ }private StarMapObject CreateObject(Guid id, string name,
DateTime c, DateTime u)
{
return new StarMapObject(id, name, c, u);
}private StarMapObject CreateObject(StarMapObject s)
{
return CreateObject(s.Id, s.Name, s.Created, s.LastUpdated);
}[Test]
public void EmptyObject()
{
StarMapObject result = CreateObject(Guid.Empty,string.Empty,
DateTime.MinValue, DateTime.MinValue);
Assert.AreEqual(result.Id, Guid.Empty);
Assert.AreEqual(result.Name, string.Empty);
Assert.AreEqual(result.Created, DateTime.MinValue);
Assert.AreEqual(result.LastUpdated, DateTime.MinValue);
}[Test]
public void BasicCreate()
{
Guid expectedId = TestConfiguration.testAtmosphereOneId;
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;
StarMapObject result = CreateObject(expectedId,
expectedName, expectedCreate, expectedChange);
Assert.AreEqual(result.Id, expectedId);
Assert.AreEqual(result.Name, expectedName);
Assert.AreEqual(result.Created, expectedCreate);
Assert.AreEqual(result.LastUpdated, expectedChange);}
[Test]
public void TestEquals()
{
Guid expectedId = TestConfiguration.testAtmosphereOneId;
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;StarMapObject smo1 = CreateObject(expectedId, expectedName,
expectedCreate, expectedChange);
StarMapObject smo2 = CreateObject(smo1);
StarMapObject smo3 = CreateObject(
TestConfiguration.testPlanetOneId,
"NotTheSameName", expectedCreate, expectedChange);bool expected = smo1.Equals(smo2);
Assert.IsTrue(expected);
expected = smo1.Equals(smo3);
Assert.IsFalse(expected);int hashSmo1 = smo1.GetHashCode();
int hashSmo2 = smo2.GetHashCode();
int hashSmo3 = smo3.GetHashCode();Assert.AreEqual(hashSmo1, hashSmo2);
Assert.AreNotEqual(hashSmo1, hashSmo3);}[Test]
public void TestToString()
{
Guid expectedId = TestConfiguration.testAtmosphereOneId;
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;
string expected = CreateExpectedString("g",
CultureInfo.CurrentCulture, expectedId,
expectedName, expectedCreate,
expectedChange);
StarMapObject smo1 = CreateObject(expectedId, expectedName,
expectedCreate,
expectedChange);string actual = smo1.ToString();
Assert.AreEqual(expected, actual);string format = "g3";
expected = CreateExpectedString(format,
CultureInfo.CurrentCulture, expectedId,
expectedName, expectedCreate,
expectedChange);
actual = smo1.ToString(format);
Assert.AreEqual(expected, actual);format = "g4";
expected = CreateExpectedString(format,
CultureInfo.InvariantCulture,
expectedId, expectedName,
expectedCreate,expectedChange);
actual = smo1.ToString(format,
CultureInfo.InvariantCulture);
Assert.AreEqual(expected, actual);expected = $"{expectedName}";
actual = smo1.ToShortString();
Assert.AreEqual(expected, actual);}private string CreateExpectedString(string format,
IFormatProvider formatProvider, Guid id,
string name, DateTime c, DateTime u)
{
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
return $"{id}{separator} {name}{separator} {c}{separator} {u}";
}}

This class contains a simple set of tests, but it is covering a simple class, so the tests don’t need to be complicated.

该类包含一组简单的测试,但是它涵盖了一个简单的类,因此测试不需要复杂。

There are a few essential things that I did here to remember in refactoring your code.

在重构您的代码时,我在这里做了一些基本的事情要记住。

1. I figured out which properties and methods were common to an essential group of my higher-level objects and turned them into an interface. This grouped them nicely and made sure that every object defined these properties the same way.

1.我弄清楚了哪些属性和方法对于我的高级对象的基本组是共有的,并将它们变成了接口。 这样可以很好地对它们进行分组,并确保每个对象都以相同的方式定义这些属性。

2. I defined a base class around that interface. There were three directions I could have gone with this base class. First, I could have made it abstract, forcing the child classes to implement one or more of the methods. Second, I could have made the methods virtual but had them throw Not Implemented exceptions, this has a similar impact to abstract but happens at runtime and is more annoying to the user of the base class — I hate libraries that use this method. Third, is the direction I took, building out virtual methods that work for the base class but are designed to be overridden by the subclasses.

2.我围绕该接口定义了一个基类。 这个基础课本来可以向三个方向发展。 首先,我本可以使其抽象化,从而迫使子类实现一个或多个方法。 其次,我本可以将这些方法设为虚拟的,但让它们抛出“未实现”的异常,这与抽象有类似的影响,但会在运行时发生,并且使基类的用户更烦恼-我讨厌使用此方法的库。 第三,是我所遵循的方向,建立了适用于基类但被子类覆盖的虚拟方法。

3. Write tests of the simple parts of the refactor that you can run early and often. Using earlier tests confirms that the parts you depend on are not impacting child classes.

3.编写对重构的简单部分的测试,这些测试可以尽早且经常运行。 使用较早的测试可以确认您所依赖的部分不会影响子类。

4. Build up to more complex parts of your code. Since all of the more complex classes share the basic functionality, when I test them, I know that the base Equals and GetHashCode work correctly. So any bug hunting is at the next level up. You will see this with the next set of classes.

4.构建代码中更复杂的部分。 由于所有更复杂的类都共享基本功能,因此在测试它们时,我知道基本Equals和GetHashCode可以正常工作。 因此,任何错误查找都将在下一个层次上进行。 您将在下一组类中看到这一点。

After I had the base class for my entire object tree, I looked at the whole set of objects again, looking for commonality. What I found was that two object types shared identical properties and a third that was implied but missing. The two objects were StarSystem and Planet, while the missing object was a star object. Also, I would eventually want to add some additional sub-planetary and super-solar objects those objects would also need a similar set of properties. After looking at the properties and roughing out what I wanted for the Star object, I got the following for an interface:

在获得整个对象树的基类之后,我再次查看了整个对象集,以寻找通用性。 我发现两种对象类型具有相同的属性,而第三种则隐含但缺少。 这两个对象是StarSystem和Planet,而缺少的对象是星形对象。 另外,我最终将要添加一些其他的子行星和超太阳能对象,这些对象也需要一组相似的属性。 在查看了属性并粗化了对Star对象的需求之后,我得到了界面的以下内容:

public interface IAstronomicalObject : IStarMapObject
{
Guid OrbitsId { get; set; }
Star Orbits { get; }
Mass Mass { get; }
Length Radius { get;}
Length OrbitalDistance { get; }
Speed OrbitalVelocity { get; }
Acceleration Gravity { get; }
}

Several things to note with this interface before we move on, first, the interface does not require setters on any of the properties except OrbitsId. The reason the interface does not require setters is that there are cases where all of these values are calculatable. For example, a complete Star System object could and should calculate its mass by adding up the mass of all of the objects that it contains. While Gravity and OrbitalVelocity are calculatable if the remaining values in the object are known. I dithered about making a base class for this interface but then decided that it made complete sense for two of the three objects I wanted to use it for, and I could decide on Star System when I got to that class. So I ended up with this base class:

在继续操作之前,此接口需要注意几件事,首先,除了OrbitsId之外,该接口不需要任何属性上的设置方法。 接口不需要设置器的原因是,在某些情况下所有这些值都是可计算的。 例如,一个完整的星系对象可以并且应该通过将其包含的所有对象的质量相加来计算其质量。 如果已知物体中的剩余值,则可以计算重力和轨道速度。 我为该接口创建一个基类而烦恼,但后来决定对于要使用它的三个对象中的两个完全有意义,并且可以在进入该类时决定使用Star System。 所以我最终得到了这个基类:

using System;
using System.Globalization;
using System.Text;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc;namespace StarMap.Models.BaseClasses
{
using UnitsNet;
using TevandelSupportLibrary.Classes;
using Support;
using Interfaces;
using CustomBinders;public class AstronomicalObject : StarMapObject,
IAstronomicalObject,
IStarMapObject
{
[ForeignKey("Orbits")]
public Guid OrbitsId { get; set; }
public Star Orbits { get; set; }[BindProperty(BinderType = typeof(QuantityValueEntityBinder),
Name = "OrbitalDistance")]
public Length OrbitalDistance { get; set; }[BindProperty(BinderType = typeof(QuantityValueEntityBinder),
Name = "Mass")]
public Mass Mass { get; set; }[BindProperty(BinderType = typeof(QuantityValueEntityBinder),
Name = "Radius")]
public Length Radius { get; set; }[BindProperty(BinderType = typeof(QuantityValueEntityBinder),
Name = "OrbitalVelocity")]
public Speed OrbitalVelocity {
get
{
return GeneralCalculations
.CalculateOrbitalVeclocity(Mass,OrbitalDistance);
}
}[BindProperty(BinderType = typeof(QuantityValueEntityBinder),
Name = "Gravity")]
public Acceleration Gravity
{
get
{
return GeneralCalculations.Gravity(Mass, Radius);
}
}public AstronomicalObject(Guid id, string name, DateTime c,
DateTime u, Star o, Length od, Mass m, Length r) :
base(id, name, c, u)
{
Orbits = o;
OrbitalDistance = od;
Mass = m;
Radius = r;
}public AstronomicalObject(AstronomicalObject a) :
this(a.Id, a.Name, a.Created, a.LastUpdated,
a.Orbits, a.OrbitalDistance, a.Mass,
a.Radius) { }public AstronomicalObject(Guid id, string name, Star o,
Length od, Mass m, Length r) :
this(id, name, DateTime.Now, DateTime.Now, o, od, m, r) { }public AstronomicalObject() :
this(Guid.Empty, string.Empty, DateTime.MinValue,
DateTime.MinValue, Star.Empty, Length.Zero,
Mass.Zero, Length.Zero) { }public new bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() == typeof(AstronomicalObject) ||
obj is IAstronomicalObject)
{
AstronomicalObject ao = obj as AstronomicalObject;
bool result = base.Equals(ao) &&
Equals(Orbits, ao.Orbits) &&
Equals(Mass, ao.Mass) &&
Equals(Radius, ao.Radius) &&
Equals(OrbitalDistance,
ao.OrbitalDistance);
return result;
}
return base.Equals(obj);
}
virtual public new int GetHashCode()
{
int hash = base.GetHashCode();
if (Orbits != null)
hash = HashCodeHelper.CombineHashCodes(hash,
Orbits.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
Mass.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
Radius.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
OrbitalDistance.GetHashCode());
return hash;
}public override string ToString(string format, IFormatProvider formatProvider)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append($"{base.ToString(format, formatProvider)}{separator}" +
$" {Orbits.Name}{separator} " +
$"{Mass.ToString(format, formatProvider)}{separator} " +
$"{Radius.ToString(format, formatProvider)}{separator} " +
$"{OrbitalDistance.ToString(format, formatProvider)}{separator} " +
$"{OrbitalVelocity.ToString(format, formatProvider)}{separator} " +
$"{Gravity.ToString(format, formatProvider)}");
return sb.ToString();}
public override string ToShortString()
{
return $"{base.ToShortString()} Gravity:{Gravity} " +
$"Orbits:{Orbits.Name} @ {OrbitalDistance}";}public static AstronomicalObject Empty { get; } = new AstronomicalObject();}
}

From the above, you can see a few things about what I decided to do with my refactor. First, this is a child of the StarMapObject, so it gets all of the properties and needs to override any methods that I want to have different functionality in this object.

从上面,您可以看到一些有关我决定对重构进行处理的事情。 首先,这是StarMapObject的子级,因此它获取了所有属性,并且需要覆盖我想在此对象中具有不同功能的所有方法。

Second, I decided that most of the interface objects with missing setters would get them in the base class. Because both Planet and Star objects make sense to be able to set the Mass, Radius, Star they are orbiting, and distance they are orbiting. I thought about making the orbital object another Astronomical object but decided to make it a Star, arbitrarily deciding that Black Holes count as stars for purposes of this project. For Gravity and OrbitalVelocity, I put the calculations into static methods, a helper class that the whole project can use.

其次,我认为大多数缺少设置器的接口对象都将它们放入基类中。 因为“行星”和“星体”对象都能够设置它们在轨道上运行的质量,半径,恒星以及它们在轨道上运行的距离,这是有意义的。 我曾考虑过将轨道物体作为另一个天文学物体,但决定将其设为恒星,并任意决定将“黑洞”视为该项目的恒星。 对于Gravity和OrbitalVelocity,我将计算结果放入静态方法中,这是整个项目可以使用的辅助类。

代码重构_重构代码_第2张图片
Yasin Gündogdu from Pexels的 Pexels YasinGündogdu摄

Third, looking at the virtual methods from StarMapObject, there are four different ways I treated the methods from the base class that I could have replaced. I did nothing with two of the methods. The two basic to string methods, ToString() and ToString(format), are the same in every case, so it is unlikely I will override them in any of my classes. For Equals, I replace it with the “new” modifier but do not make any other modifications. The reason is that as a method inherited from the “Object” class marking it as “override” or “virtual” causes warning messages and errors in child classes. You will note that I did the same thing in the StarMapObject, the short explanation is that child calls to base.Equals(), Object.Equals(), or Equals(object, object) get confused if there is a “virtual” or “override” version of “Equals” in the inheritance tree of one of the compared objects. So, don’t do that with the Equals() method, “new” gets the job done and doesn’t cause hard to track down errors in child classes later in the development process. On the other hand, GetHashCode() is both virtual and new, this is a method that has the opposite problem, I have occasionally seen issues where if the parent class does not declare its GetHashCode() as “virtual” as well as “new” a call to base.GetHashCode() will call the parents parent class. So this is important as well when using nested parent classes. Last are the two overridden ToString classes. They both get a simple “override” telling the compiler that yes, we are overriding the base classes virtual method, but may want to call that method.

第三,查看StarMapObject中的虚拟方法,可以用四种不同的方法来处理可以替换的基类中的方法。 我对这两种方法一无所获。 两种基本的字符串方法ToString()和ToString(format)在每种情况下都是相同的,因此不太可能在我的任何类中覆盖它们。 对于Equals,我将其替换为“ new”修饰符,但不进行其他任何修饰。 原因是,作为从“ Object”类继承的方法,将其标记为“ override”或“ virtual”会导致警告消息和子类错误。 您会注意到我在StarMapObject中做了同样的事情,简短的解释是子调用base.Equals(),Object.Equals()或Equals(object,object)会被混淆,如果存在“虚拟”或被比较对象之一的继承树中“等于”的“替代”版本。 因此,不要使用Equals()方法来做到这一点,“ new”可以完成工作,并且在以后的开发过程中不会导致难以追踪子类中的错误。 另一方面,GetHashCode()既是虚拟的又是新的,这是一个存在相反问题的方法,我偶尔会遇到以下问题:如果父类未将其GetHashCode()声明为“虚拟”和“新的”调用base.GetHashCode()将调用父类的父类。 因此,在使用嵌套父类时,这一点也很重要。 最后是两个覆盖的ToString类。 它们都得到一个简单的“覆盖”,告诉编译器是的,我们正在覆盖基类的虚拟方法,但是可能要调用该方法。

Finally, I create a static Empty object that I can use later to represent AstronomicalObject placeholders. I find this sort of stand-in handy for testing and occasional cases where I need a placeholder object. I could just use the empty constructor as I do in the Empty call, but this defines my intent. If I call Empty, then I never intend to fill the object. If I create a new AstronomicalObject(), then I probably mean to fill in the data eventually.

最后,我创建一个静态的Empty对象,以后可以用来表示AstronomicalObject占位符。 我发现这种备用方法可用于测试和偶尔需要占位符对象的情况。 我可以像在Empty调用中那样使用空构造函数,但这定义了我的意图。 如果我叫Empty,那么我从不打算填充对象。 如果我创建一个新的AstronomicalObject(),那么我可能打算最终填写数据。

Then I wrote a set of tests, building on the results from StarMapObject’s Tests. This test set looks like:

然后,我根据StarMapObject的“测试”结果编写了一组测试。 该测试集如下所示:

class AstronomicalObjectTests
{
[SetUp]
public void Setup()
{ }[Test]
public void EmptyObjectTest()
{
AstronomicalObject result = new AstronomicalObject();
Assert.AreEqual(result.Id, Guid.Empty);
Assert.AreEqual(result.Name, string.Empty);
Assert.AreEqual(result.Created, DateTime.MinValue);
Assert.AreEqual(result.LastUpdated, DateTime.MinValue);
Assert.AreEqual(result.OrbitalDistance, Length.Zero);
Assert.AreEqual(result.Mass, Mass.Zero);
Assert.AreEqual(result.Radius, Length.Zero);AstronomicalObject check = AstronomicalObject.Empty;
bool final = check.Equals(result);
Assert.IsTrue(final);
}[Test]
public void BasicCreate()
{
Guid expectedId = Guid.NewGuid();
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;
Star expectedOrbits = Star.Sol;
Length expectedOD = new Length(1, LengthUnit.AU);
Length expectedRad = new Length(1, LengthUnit.EarthRadius);
Mass ExpectedMass = new Mass(1, MassUnit.EarthMass);
AstronomicalObject result = new AstronomicalObject(
expectedId,
expectedName, expectedCreate, expectedChange,
expectedOrbits, expectedOD, ExpectedMass, expectedRad);

Assert.AreEqual(result.Id,expectedId);
Assert.AreEqual(result.Name, expectedName);
Assert.AreEqual(result.Created, expectedCreate);
Assert.AreEqual(result.LastUpdated, expectedChange);
Assert.AreEqual(result.OrbitalDistance, expectedOD);
Assert.AreEqual(result.Mass, ExpectedMass);
Assert.AreEqual(result.Radius, expectedRad);// jumping ahead see if the star compare works?
Assert.AreEqual(result.Orbits, expectedOrbits);}[Test]
public void TestEqualsAndHash()
{
Guid expectedId = Guid.NewGuid();
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;
Star expectedOrbits = Star.Sol;
Length expectedOD = new Length(1, LengthUnit.AU);
Length expectedRad = new Length(1, LengthUnit.EarthRadius);
Mass ExpectedMass = new Mass(1, MassUnit.EarthMass);AstronomicalObject ao1 = new AstronomicalObject(expectedId,
expectedName, expectedCreate, expectedChange,
expectedOrbits, expectedOD, ExpectedMass, expectedRad);
AstronomicalObject ao2 = new AstronomicalObject(ao1);
AstronomicalObject ao3 = new AstronomicalObject(
Guid.NewGuid(), "Not the same name",
expectedCreate, expectedChange,
Star.GalacticCenter,
Star.GalacticCenterOrbitalDistance(1),
new Mass(1, MassUnit.JupiterMass),
new Length(1, LengthUnit.JupiterRadius));
bool expected = ao1.Equals(ao2);
Assert.IsTrue(expected);
expected = ao1.Equals(ao3);
Assert.IsFalse(expected);int hashAo1 = ao1.GetHashCode();
int hashAo2 = ao2.GetHashCode();
int hashAo3 = ao3.GetHashCode();Assert.AreEqual(hashAo1, hashAo2);
Assert.AreNotEqual(hashAo1, hashAo3);}[Test]
public void ToStringTests()
{
Guid expectedId = Guid.NewGuid();
string expectedName = "ExpectedName";
DateTime expectedCreate = DateTime.Now;
DateTime expectedChange = DateTime.Now;
Star expectedOrbits = Star.Sol;
Length expectedOD = new Length(1, LengthUnit.AU);
Length expectedRad = new Length(1, LengthUnit.EarthRadius);
Mass ExpectedMass = new Mass(1, MassUnit.EarthMass);string expected = CreateExpectedString("g",
CultureInfo.CurrentCulture,
expectedId, expectedName,
expectedCreate, expectedChange, expectedOrbits,
expectedOD, ExpectedMass, expectedRad);
AstronomicalObject ao1 = new AstronomicalObject(
expectedId, expectedName,
expectedCreate, expectedChange,
expectedOrbits, expectedOD,
ExpectedMass, expectedRad);string result = ao1.ToString();
Assert.AreEqual(expected, result);expected = CreateExpectedString("g3",
CultureInfo.CurrentCulture,
expectedId, expectedName,
expectedCreate, expectedChange,
expectedOrbits, expectedOD,
ExpectedMass, expectedRad);
result = ao1.ToString("g3");
Assert.AreEqual(expected, result);expected = CreateExpectedString("g4",
CultureInfo.InvariantCulture,
expectedId, expectedName,
expectedCreate, expectedChange,
expectedOrbits, expectedOD,
ExpectedMass, expectedRad);
result = ao1.ToString("g4", CultureInfo.InvariantCulture);
Assert.AreEqual(expected, result);expected = $"{expectedName} " +
$"Gravity:{GeneralCalculations.Gravity(ExpectedMass, expectedRad)} " +
$"Orbits:{expectedOrbits.Name} @ {expectedOD}";
result = ao1.ToShortString();
Assert.AreEqual(expected, result);}private string CreateExpectedString(string format,
IFormatProvider formatProvider,
Guid id, string name, DateTime c, DateTime u,
Star o, Length od, Mass m, Length r)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append($"{id}{separator} {name}{separator}" +
$" {c}{separator} {u}");
sb.Append($"{separator} {o.Name}{separator} " +
$"{m.ToString(format, formatProvider)}{separator} " +
$"{r.ToString(format, formatProvider)}{separator} " +
$"{od.ToString(format, formatProvider)}{separator} " +
$"{GeneralCalculations
.CalculateOrbitalVeclocity(m, od)
.ToString(format, formatProvider)}{separator} " +
$"{GeneralCalculations.Gravity(m,r)
.ToString(format, formatProvider)}");
return sb.ToString();
}
}

A few things to note about these tests: I wrote the tests in the order they are in the file, so the simple checks at the top were written and run first. Second, I ran the tests after I wrote each test so I could confirm that the test and the code it was testing were correct. Third, I started simple and worked my way to complex, so the very first test I ran was the empty object test, checking to make sure that basic creation and comparison with empty objects worked correctly. Now to be fair, this was actually a rather in-depth test since to run the AstronomicalObject() constructor, the class calls the full constructor as well. I then run a full constructor test, including a check on the Star equals method. Then I do a basic analysis of the Equals and GetHashCode methods, confirming that they work as expected, and finally, test the various permutations of the ToString() method. Again this is not a complex or complete test set. In fact, looking at this, I can see several areas that I am going to go back and add tests to cover weak points in the set. However, I am looking at what I have now, not what I will have in a day or two.

关于这些测试的一些注意事项:我按文件中的顺序编写了这些测试,因此最顶部的简单检查已编写并首先运行。 其次,在编写每个测试之后运行测试,以便可以确认测试及其所测试的代码正确无误。 第三,我从简单开始,然后逐步走向复杂,因此我运行的第一个测试是空对象测试,检查以确保基本创建和与空对象的比较都能正确进行。 公平地说,这实际上是一个相当深入的测试,因为要运行AstronomicalObject()构造函数,该类也会调用完整的构造函数。 然后,我运行完整的构造函数测试,包括对Star equals方法的检查。 然后,我对Equals和GetHashCode方法进行了基础分析,确认它们可以按预期工作,最后,测试ToString()方法的各种排列。 同样,这不是一个复杂或完整的测试集。 实际上,看着这个,我可以看到我要回去的几个领域,并添加测试以覆盖集合中的薄弱环节。 但是,我正在查看的是我现在拥有的东西,而不是一两天后要拥有的东西。

Now I want to jump to the implementations that use these base classes. I will start with Atmosphere since it is a StarMapObject but not an AstronomicalObject, giving a view into the simpler of the implementations.

现在,我想跳到使用这些基类的实现。 我将从Atmosphere开始,因为它是StarMapObject而不是AstronomicalObject,从而使实现更简单。

I already had an IAtmosphere interface that looked like:

我已经有一个IAtmosphere界面,如下所示:

using Support;public interface IAtmosphere
{
Guid Id { get; }
PlanetaryAtmosphereDensity AtmospherePrimary { get; set; }
PlanetaryAtmosphereComposition AtmosphereComposition {get; set;}
String Name { get; set; }
Temperature PlanetaryTemperature { get; set; }string ToString();
string ToString(string format);
string ToString(string format, IFormatProvider formatProvider);
}

It was already a simple class, but it had some problems. First, it was attached directly to the Planet Class by having PlanetaryTemperature as a property, and second, it was missing some parts that I had discovered later I needed. These differences were because IAtmosphere was one of the first interfaces I had written for this project, and I had never updated the interface with features that other later classes and interfaces had. So, the new interface looks like:

这已经是一个简单的类,但是有一些问题。 首先,通过将PlanetaryTemperature作为属性将其直接附加到Planet类,其次,它缺少了一些后来我需要的部分。 之所以存在这些差异,是因为IAtmosphere是我为此项目编写的第一个界面之一,而我从未使用其他后来的类和界面所具有的功能来更新该界面。 因此,新界面如下所示:

using Support;
public interface IAtmosphere : IStarMapObject
{
PlanetaryAtmosphereDensity AtmospherePrimary { get; set; }
PlanetaryAtmosphereComposition AtmosphereComposition { get; set; }
}

Simpler but more complete since it inherits all of the properties and functionality of IStarMapObject. So the Atmosphere object looks like:

更简单但更完整,因为它继承了IStarMapObject的所有属性和功能。 所以Atmosphere对象看起来像:

namespace StarMap.Models
{
using TevandelSupportLibrary.Classes;
using Support;
using Interfaces;
using BaseClasses;public class Atmosphere : StarMapObject, IAtmosphere
{
#region Properties[DisplayName("Atmospheric Density")]
public PlanetaryAtmosphereDensity AtmospherePrimary
{ get; set; }[DisplayName("Atmospheric Composition")]
public PlanetaryAtmosphereComposition AtmosphereComposition
{ get; set; }
#endregion#region Constructors
///
///
///

///
///
///
///
///
///
public Atmosphere(Guid id, string name, DateTime cr,
DateTime u,
PlanetaryAtmosphereDensity p,
PlanetaryAtmosphereComposition c)
{
Id = id;
Name = name;
AtmospherePrimary = p;
AtmosphereComposition = c;
Created = cr;
LastUpdated = u;}///
/// Build a new Atmosphere Object
///

/// database key
///
/// general atmospheric thickness
/// general atmospheric composition
public Atmosphere(Guid id, string name,
PlanetaryAtmosphereDensity p,
PlanetaryAtmosphereComposition c) :
this(id, name, DateTime.Now, DateTime.Now, p, c) { }///
/// Build new Atmosphere Object, auto generate database key.
///

///
/// general atmospheric thickness
/// general atmospheric composition
public Atmosphere(string name,
PlanetaryAtmosphereDensity p,
PlanetaryAtmosphereComposition c) :
this(Guid.NewGuid(), name, p, c)
{ }///
/// Build an empty Atmosphere Object, empty id,
/// empty name no atmosphere.
///

public Atmosphere() : this(Guid.Empty, string.Empty,
DateTime.MinValue,
DateTime.MinValue,
PlanetaryAtmosphereDensity.None,
PlanetaryAtmosphereComposition.None) { }///
/// Clone of an Atmosphere
///

///
public Atmosphere(Atmosphere a) : this(a.Id, a.Name,
a.Created, a.LastUpdated,
a.AtmospherePrimary,
a.AtmosphereComposition) { }///
/// Clone of an IAtomophere into a concrete class
///

///
public Atmosphere(IAtmosphere a) : this((Atmosphere)a) { }
#endregion#region Public Instance overrides
///
/// override the base object Equals method to
/// handle the details of
/// and Atmosphere object.
///

/// Object to check for equality
///
public new bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() == typeof(Atmosphere) ||
obj is IAtmosphere)
{
IAtmosphere a = obj as IAtmosphere;
bool result = base.Equals(a) &&
AtmospherePrimary
.Equals(a.AtmospherePrimary) &&
AtmosphereComposition
.Equals(a.AtmosphereComposition);
return result;
}return base.Equals(obj);
}///
/// Build a better hash code than the basic object hash.
///

///
public override int GetHashCode()
{
int hash = base.GetHashCode();
hash = HashCodeHelper.CombineHashCodes(hash,
AtmospherePrimary.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
AtmosphereComposition.GetHashCode());
return hash;
}///
/// Returns a String representing this Atmosphere instance,
/// using the specified format for individual elements
/// and the given IFormatProvider.
///

/// format of elements.
/// The format provider
/// to use when formatting elements.
/// The string representation.
public override string ToString(string format,
IFormatProvider formatProvider)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append('<');
sb.Append($"{base.ToString(format, formatProvider)}" +
$"{separator} {AtmospherePrimary}{separator} " +
$"{AtmosphereComposition}");
sb.Append('>');
return sb.ToString();
}public override string ToShortString()
{
return $"{base.ToShortString()} " +
$"Density:{AtmospherePrimary} " +
$"Composition:{AtmosphereComposition}";
}
#endregion#region public static methods
///
/// Copy right into left and return left, except for the
/// Last Modified date time which is updated.
///

///
///
///
public static IAtmosphere Copy(IAtmosphere left,
IAtmosphere right)
{
left.Id = right.Id;
left.Name = right.Name;
left.Created = right.Created;
left.LastUpdated = DateTime.Now;
left.TimeStamp = right.TimeStamp;
left.AtmosphereComposition = right.AtmosphereComposition;
left.AtmospherePrimary = right.AtmospherePrimary;
return left;
}public static Atmosphere Empty { get; } = new Atmosphere();
public static Atmosphere Standard { get; } =
new Atmosphere( "Standard",
PlanetaryAtmosphereDensity.Normal,
PlanetaryAtmosphereComposition.Normal);
#endregion}

There are a few things to note about this class before I move on. First, where it makes any sense, I use the base class’ method as part of the child class method evaluation. So in Equals, when I am checking two IAtmosphere objects first, I check if their base classes are equal, then check the remaining parts of the Atmosphere object. I do the same with all of the other classes, using existing tested code as much as possible within new code to reduce the scope for bugs to the interface with old code and the new code.

在我继续之前,有一些关于该课程的注意事项。 首先,在有意义的地方,我将基类的方法用作子类方法评估的一部分。 因此,在Equals中,当我首先检查两个IAtmosphere对象时,我先检查它们的基类是否相等,然后再检查Atmosphere对象的其余部分。 我对所有其他类都做同样的事情,在新代码中尽可能使用现有的经过测试的代码,以减少旧代码和新代码与接口的错误范围。

Second, there are two static properties and one static method. These are helpers to make handling Atmosphere objects easier, giving standard earth-like atmosphere, the same kind of Empty I have for other classes, and a static copy right into left for when all you have is an interface. This last is an experiment to see if I need it as I get farther into my refactor so that I may remove or replace the property in a later version of the code.

其次,有两种静态属性和一种静态方法。 这些是使处理大气对象更容易的助手,提供标准的类似地球的气氛,为其他类提供了相同的Empty,以及当您所有的都是接口时,从右到左的静态副本。 这是一个实验,目的是在进一步重构时查看是否需要它,以便可以在更高版本的代码中删除或替换该属性。

When I made the above changes, I also heavily modified the tests for the Atmosphere class, the original tests were nearly the first I had written for this project. At the time, I was playing around with some different ideas about how I wanted to test the objects, since then my thoughts have gelled some and I have a more standard set of tests. The test class for Atmosphere looks like:

当做出上述更改时,我还对Atmosphere类的测试进行了重大修改,原始测试几乎是我为此项目编写的第一个测试。 当时,我在围绕如何测试对象的问题提出不同的想法,从那时起我的想法就化为乌有,并且我有了一套更标准的测试。 Atmosphere的测试类如下:

using NUnit.Framework.Internal;
using TestSupport;[TestFixture]
public class AtmosphereTests
{
private string _expectedName = "Test Atmosphere";[SetUp]
public void SetUp()
{}[Test]
public void TestConstructors()
{
PlanetaryAtmosphereComposition expectedComp =
PlanetaryAtmosphereComposition.None;
PlanetaryAtmosphereDensity expectedDensity =
PlanetaryAtmosphereDensity.None;
Guid expectedId = Guid.Empty;
DateTime expectedCreate = DateTime.MinValue;
DateTime expectedUpdate = DateTime.MinValue;
string expectedName = string.Empty;
Atmosphere result = Atmosphere.Empty;
Assert.AreEqual(expectedComp, result.AtmosphereComposition);
Assert.AreEqual(expectedDensity, result.AtmospherePrimary);
Assert.AreEqual(expectedCreate, result.Created);
Assert.AreEqual(expectedUpdate, result.LastUpdated);
Assert.AreEqual(expectedName, result.Name);
Assert.AreEqual(expectedId, result.Id);expectedComp = PlanetaryAtmosphereComposition.Acidic;
expectedDensity = PlanetaryAtmosphereDensity.Normal;
expectedCreate = DateTime.Now;
expectedUpdate = DateTime.Now;
expectedName = _expectedName;
expectedId = TestConfiguration.testAtmosphereOneId;result = new Atmosphere(expectedId, expectedName, expectedCreate,
expectedUpdate, expectedDensity, expectedComp);
Assert.AreEqual(expectedComp, result.AtmosphereComposition);
Assert.AreEqual(expectedDensity, result.AtmospherePrimary);
Assert.AreEqual(expectedCreate, result.Created);
Assert.AreEqual(expectedUpdate, result.LastUpdated);
Assert.AreEqual(expectedName, result.Name);
Assert.AreEqual(expectedId, result.Id);result = new Atmosphere(expectedId, expectedName,
expectedDensity, expectedComp);
Assert.AreEqual(expectedComp, result.AtmosphereComposition);
Assert.AreEqual(expectedDensity, result.AtmospherePrimary);
Assert.AreNotEqual(expectedCreate, result.Created);
Assert.AreNotEqual(expectedUpdate, result.LastUpdated);
Assert.AreEqual(expectedName, result.Name);
Assert.AreEqual(expectedId, result.Id);result = new Atmosphere(expectedName, expectedDensity, expectedComp);
Assert.AreEqual(expectedComp, result.AtmosphereComposition);
Assert.AreEqual(expectedDensity, result.AtmospherePrimary);
Assert.AreNotEqual(expectedCreate, result.Created);
Assert.AreNotEqual(expectedUpdate, result.LastUpdated);
Assert.AreEqual(expectedName, result.Name);
Assert.AreNotEqual(expectedId, result.Id);}[Test]
public void EqualsAndHashTests()
{
PlanetaryAtmosphereComposition expectedComp =
PlanetaryAtmosphereComposition.Acidic;
PlanetaryAtmosphereDensity expectedDensity =
PlanetaryAtmosphereDensity.Normal;
Guid expectedId = TestConfiguration.testAtmosphereOneId;
DateTime expectedCreate = DateTime.Now;
DateTime expectedUpdate = DateTime.Now;
string expectedName = _expectedName;
Atmosphere a1 = new Atmosphere(expectedId, expectedName,
expectedCreate, expectedUpdate, expectedDensity, expectedComp);
Atmosphere a2 = new Atmosphere(a1);
Atmosphere a3 = Atmosphere.Empty;bool result = a1.Equals(a2);
Assert.IsTrue(result);
result = a1.Equals(a3);
Assert.IsFalse(result);
result = a1.Equals(null);
Assert.IsFalse(result);int hashA1 = a1.GetHashCode();
int hashA2 = a2.GetHashCode();
int hashA3 = a3.GetHashCode();
Assert.AreEqual(hashA1, hashA2);
Assert.AreNotEqual(hashA2, hashA3);int expected = TestConfiguration
.testAtmosphereOneId.GetHashCode();
expected = HashCodeHelper.CombineHashCodes(expected,
expectedName.GetHashCode());
expected = HashCodeHelper.CombineHashCodes(expected,
expectedDensity.GetHashCode());
expected = HashCodeHelper.CombineHashCodes(expected,
expectedComp.GetHashCode());Assert.AreEqual(expected, hashA1);}[Test]
public void ToStringTests()
{
PlanetaryAtmosphereComposition expectedComp =
PlanetaryAtmosphereComposition.Acidic;
PlanetaryAtmosphereDensity expectedDensity =
PlanetaryAtmosphereDensity.Normal;
Guid expectedId = TestConfiguration.testAtmosphereOneId;
DateTime expectedCreate = DateTime.Now;
DateTime expectedUpdate = DateTime.Now;
string expectedName = _expectedName;Atmosphere a1 = new Atmosphere(expectedId, expectedName,
expectedCreate, expectedUpdate, expectedDensity,
expectedComp);
string expected = BuildTestString("g", CultureInfo.CurrentCulture,
expectedId, expectedName, expectedCreate, expectedUpdate,
expectedDensity, expectedComp);
string result = a1.ToString();
Assert.AreEqual(expected, result);string format = "s3";
expected = BuildTestString(format, CultureInfo.CurrentCulture,
expectedId, expectedName, expectedCreate, expectedUpdate,
expectedDensity, expectedComp);
result = a1.ToString(format);
Assert.AreEqual(expected, result);format = "g4";
expected = BuildTestString(format, CultureInfo.InvariantCulture,
expectedId, expectedName, expectedCreate, expectedUpdate,
expectedDensity, expectedComp);
result = a1.ToString(format, CultureInfo.InvariantCulture);
Assert.AreEqual(expected, result);expected = $"{expectedName} Density:{expectedDensity} " +
$"Composition:{expectedComp}";
result = a1.ToShortString();
Assert.AreEqual(expected, result);}private string BuildTestString(string format,
IFormatProvider formatProvider,
Guid id, string name, DateTime created, DateTime updated,
PlanetaryAtmosphereDensity p,
PlanetaryAtmosphereComposition c)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append('<');
sb.Append($"{id}{separator} {name}{separator} {created}" +
$"{separator} {updated}");
sb.Append($"{separator} {p}{separator} {c}");
sb.Append('>');
return sb.ToString();
}
}

Again when I look at this test class even just a couple of days later, I can see places where there are additional test cases that I should add. But not right now, I will revisit these test cases later.

再说一次,即使只是几天后,我也要看这个测试类,可以看到我应该添加其他测试用例的地方。 但是现在不行,稍后我将重新讨论这些测试用例。

代码重构_重构代码_第3张图片

There is not much that is interesting in the tests that I haven’t already discussed, so I will move on to the last object I am going to cover in some detail in this article, the Star object. This class is slightly more interesting because it is a new object in the model for both MVC and Entity Framework. Most of the interesting properties and methods for Star were part of the old StarSystem class. The remaining are properties and methods are needed for the ultimate purpose of this application. First, the IStar interface:

在我尚未讨论的测试中,没有太多有趣的事情,因此,我将继续研究本文中将详细介绍的最后一个对象,即Star对象。 该类稍微有些有趣,因为它是MVC和Entity Framework模型中的新对象。 Star的大多数有趣的属性和方法都是旧的StarSystem类的一部分。 剩下的是本应用程序最终目的所需的属性和方法。 首先,是IStar界面:

public interface IStar: IAstronomicalObject
{
Guid StarSystemId { get; set; }
Temperature Temperature { get; set; }
double AbsoluteMagnitude { get; set; }
string SpectralClass { get; set; }
}

Things to note, it inherits from IAstronomicalObject, which means it also inherits from IStarMapObject, so anything that implements IAstronomicalObject must also implement these parent properties, which is where it gets the other features that were part of the old StarSystem class.

需要注意的是,它继承自IAstronomicalObject,这意味着它也继承自IStarMapObject,因此实现IAstronomicalObject的所有对象也必须实现这些父属性,这是它获取旧StarSystem类中其他功能的地方。

Then I wrote the Star class that looks like:

然后,我编写了如下的Star类:

public class Star : AstronomicalObject, IStar, 
IAstronomicalObject, IStarMapObject
{
#region Properties
[ForeignKey("StarSystem")]
public Guid StarSystemId { get; set; }
public StarSystem StarSystem { get; set; }[BindProperty(BinderType =
typeof(QuantityValueEntityBinder),
Name = "Temperature")]
public Temperature Temperature { get; set; }
public double AbsoluteMagnitude { get; set; }
public string SpectralClass { get; set; }
#endregion#region Constructors
public Star(Guid id, string name,
DateTime c, DateTime u,
Star o, Mass m, Length r, Length od,
Temperature t, double am, string sc)
{
Id = id;
Name = name;
Created = c;
LastUpdated = u;
Orbits = o;
OrbitsId = Guid.Empty;
if (o != null)
{
OrbitsId = o.Id;
}
Mass = m;
Radius = r;
OrbitalDistance = od;
Temperature = t;
AbsoluteMagnitude = am;
SpectralClass = sc;
}public Star(Guid id, string name,
DateTime c, DateTime u,
Star o, string m, string r,
string od, string t,
double am, string sc) :
this(id, name, c, u, o,
Mass.Parse(m), Length.Parse(r),
Length.Parse(od),
Temperature.Parse(t), am, sc)
{ }public Star(Guid id, string name, Mass m,
Length r, Temperature t,
double am, string sc) :
this(id, name, DateTime.Now,
DateTime.Now, Star.Empty, m, r,
Length.Zero, t, am, sc)
{ }public Star(string name, Mass m, Length r,
Temperature t, double am, string sc) :
this(Guid.NewGuid(), name, DateTime.Now,
DateTime.Now, Star.Empty,
m, r, Length.Zero, t, am, sc)
{ }public Star() : this(Guid.Empty, string.Empty,
DateTime.MinValue, DateTime.MinValue, Star.Empty,
Mass.Zero, Length.Zero, Length.Zero,
Temperature.Zero, 0.0, string.Empty)
{ }public Star(Star s) : this(s.Id, s.Name,
s.Created, s.LastUpdated,
s.Orbits, s.Mass, s.Radius,
s.OrbitalDistance, s.Temperature,
s.AbsoluteMagnitude, s.SpectralClass)
{ }#endregion#region Public Instance Methods
public new bool Equals(object obj)
{
if (obj == null)
{
return false;
}
if (obj.GetType() == typeof(Star) || obj is IStar)
{
Star p = obj as Star;

bool result = base.Equals(p) &&
Temperature.Equals(p.Temperature) &&
AbsoluteMagnitude
.Equals(p.AbsoluteMagnitude) &&
SpectralClass.Equals(p.SpectralClass);
return result;
}
return base.Equals(obj);
}public bool Equals(IPlanet p)
{
return Equals(p);
}public override int GetHashCode()
{
int hash = base.GetHashCode();
hash = HashCodeHelper.CombineHashCodes(hash,
Temperature.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
AbsoluteMagnitude.GetHashCode());
hash = HashCodeHelper.CombineHashCodes(hash,
SpectralClass.GetHashCode());
return hash;
}///
/// Returns a String representing this StarSystem instance,
/// using the specified format to format individual elements
/// and the given IFormatProvider.
///

/// format of individual elements.
/// The format provider
/// to use when formatting elements.
/// The string representation.
public override string ToString(string format,
IFormatProvider formatProvider)
{
StringBuilder sb = new StringBuilder();
string separator = NumberFormatInfo
.GetInstance(formatProvider)
.NumberGroupSeparator;
sb.Append('<');
sb.Append($"{base.ToString(format, formatProvider)}" +
$"{separator} " +
$"{Temperature.ToString(format, formatProvider)}"+
$"{separator} " +
$"{AbsoluteMagnitude}{separator} {SpectralClass}");
sb.Append('>');
return sb.ToString();
}public override string ToShortString()
{
return $"{base.ToShortString()} "+
$"Temperature:{Temperature} " +
$"Absolute Magnitude:{AbsoluteMagnitude} " +
$"Class:{SpectralClass}";
}#endregion Public Instance Methods#region public static methods
public static bool operator ==(Star left, Star right)
{
if (Equals(left, null))
{
return Equals(left, right);
}
return left.Equals(right);
}public static bool operator !=(Star left, Star right)
{
if (Equals(left, null))
{
return !Equals(left, right);
}
return !left.Equals(right);
}public new static Star Empty { get; } =
new Star(Guid.Empty, string.Empty, DateTime.MinValue,
DateTime.MinValue, SecondLevelEmpty,
Mass.Zero, Length.Zero,
Length.Zero, Temperature.Zero, 0.0, string.Empty);private static Star SecondLevelEmpty { get; } =
new Star(Guid.NewGuid(), "Second Level Empty Star",
DateTime.MinValue, DateTime.MinValue, null, Mass.Zero,
Length.Zero, Length.Zero, Temperature.Zero, 0.0,
string.Empty);public static Star Sol { get; } =
new Star(Guid.Empty, "Sol", new Mass(1, MassUnit.SolarMass),
new Length(1, LengthUnit.SolarRadius),
new Temperature(5778, TemperatureUnit.Kelvin),
4.3, "G2V");public static Star GalacticCenter { get; } =
new Star(Guid.Empty, "Sagittarius A",
new Mass(4.3, MassUnit.MegasolarMass),
new Length(0.15, LengthUnit.AU),
new Temperature(99999982.2222,
TemperatureUnit.DegreeCelsius),
-21.3d, "SMBH-MW");// This is a rough estimate, taking the basic distance from
// Sagi-A and adding the distance from Sol.
// I need something better but for now this gives a value to
// use for getting Raw data into the system.
public static Length GalacticCenterOrbitalDistance(
double distance )
{
return new Length(8178 + distance, LengthUnit.Parsec);
}///
/// Get rough mass, radius, & temperature from
/// Spectral Class. This is very rough now and has at
/// least one known bug it does not get the correct values
/// when parsing Sol's SC, but it is within a fraction of
/// a percent. So I guess not bad?
/// I am planning on refining to handle remaining parts of SC
/// currently ignored to get better numbers at some point.
///

/// Spectral Class to parse
/// out Mass from parsing SC
/// out Radius from parsing SC
/// out Temperature from parsing SC
public static void ParseSpectralClass(string sc,
out Mass m, out Length r, out Temperature t)
{
m = new Mass(1, MassUnit.SolarMass);
r = new Length(1, LengthUnit.SolarRadius);
t = new Temperature(5778, TemperatureUnit.Kelvin);if(sc.Contains('+') || sc.Contains('/') ||
sc.Contains(':') || sc.Contains('!') || sc.Contains('-'))
{
// other special cases that will need later handling,
// but I happen to know what the super massive black hole
// at the center of the milky-way is, because
// for the purposes of this program I defined it.
if(sc.Equals("SMBH-MW"))
{
m = new Mass(4.3, MassUnit.MegasolarMass);
r = new Length(0.15, LengthUnit.AU);
}

}// grab the Stellar class, this gives the basic
//ranges for each value
char c = sc[0];
string remains = sc.Substring(1);
// assume an average star if not given a
// value in the spectra class.
int scale = 5;
if (remains.Length > 1)
{
if (!int.TryParse(remains.Substring(0, 1), out scale))
{
// If the second character in the Spectral Class isn't a
// number then this is something odd and we
// want to find the first number in the string.
char num = sc.FirstOrDefault(c => c == '0' || c == '1' ||
c == '2' || c == '3' || c == '4' || c == '5' ||
c == '6' || c == '7' || c == '8' || c == '9');// if we get the default value,
// force it back to the average value.
if( num == 0)
{
num = '5';
}
scale = System.Convert.ToInt32(num);
}
// eventually the remains needs to be parsed to
// get a better grasp on the
// luminosity and temperature.
remains = remains.Substring(1);
}
switch(c)
{
case 'O':
case 'o':
// Effective Temperature > 30,000K (hottest found is ~60k)
// > 16 Solar Masses (most massive is 265 solar masses),
// > 6.6 Solar Radii (largest is 1800 solar radii)
List range =
GetLookup(60000, 30000, 265, 16, 1800, 6.6);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
case 'B':
case 'b':
// Effective Temperature 10,000 to 30,000 K
// 2.1 to 16 Solar Mass,
// 1.8 to 6.6 Solar radii
range = GetLookup(30000, 10000, 16, 2.1, 6.6, 1.8);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
case 'A':
case 'a':
// Effective Temperature 7,500 to 10,000 K
//1.4 to 2.1 Solar Mass,
// 1.4 to 1.8 Solar radii
range = GetLookup(10000, 7500, 2.1, 1.4, 1.8, 1.4);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
case 'F':
case 'f':
// Effective Temperature 6,000 to 7,500 K
// 1.04 to 1.4 Solar Mass,
// 1.15 to 1.4 Solar radii
range = GetLookup(7500, 6000, 1.4, 1.04, 1.4, 1.15);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
case 'G':
case 'g':
// Effective Temperature 5,200 to 6,000 K
// 0.8 to 1.04 Solar Mass,
// 0.96 to 1.15 Solar radii
range = GetLookup(6000, 5200, 1.04, 0.8, 1.15, 0.96);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
case 'K':
case 'k':
// Effective Temperature 3,700 to 5,200 K
// 0.45 to 0.8 Solar Mass,
// 0.7 to 0.96 Solar radii
range = GetLookup(5200, 3700, 0.8, 0.45, 0.96, 0.7);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
default:
// Special case but treat it like an M class since over 75%
// of stars are M class.
case 'M':
case 'm':
// Effective Temperature 2,400 to 3,700 K
// 0.08 to 0.45 Solar Mass,
// < 0.7 Solar radii (smallest found is ~0.1 solar radii)
range = GetLookup(3700, 2400, 0.45, 0.08, 0.7, 0.1);
t = range[scale].Temperature;
m = range[scale].Mass;
r = range[scale].Radius;
break;
}
}private static List GetLookup(
double tempMax, double tempMin,
double massMax, double massMin,
double radMax, double radMin)
{
Temperature maxTemp = new Temperature(tempMax,
TemperatureUnit.Kelvin);
Mass maxMass = new Mass(massMax,
MassUnit.SolarMass);
Length maxRad = new Length(radMax,
LengthUnit.SolarRadius);
double tempRange = (tempMax - tempMin);
Temperature tempStep = new Temperature(
tempRange / 10,
TemperatureUnit.Kelvin);
double massRange = massMax - massMin;
Mass massStep = new Mass(massRange / 10.0,
MassUnit.SolarMass);
double radRange = radMax - radMin;
Length radStep = new Length(radRange / 10.0,
LengthUnit.SolarRadius);List range = new List
{
new StarTranslator(
maxMass,
maxRad,
maxTemp),
new StarTranslator(
maxMass - massStep,
maxRad - radStep,
maxTemp - tempStep),
new StarTranslator(
maxMass - (massStep * 2),
maxRad - (radStep *2),
maxTemp - (tempStep *2)),
new StarTranslator(
maxMass - (massStep * 3),
maxRad - (radStep *3),
maxTemp - (tempStep *3)),
new StarTranslator(
maxMass - (massStep * 4),
maxRad - (radStep *4),
maxTemp - (tempStep *4)),
new StarTranslator(
maxMass - (massStep * 5),
maxRad - (radStep *5),
maxTemp - (tempStep *5)),
new StarTranslator(
maxMass - (massStep * 6),
maxRad - (radStep *6),
maxTemp - (tempStep *6)),
new StarTranslator(
maxMass - (massStep * 7),
maxRad - (radStep *7),
maxTemp - (tempStep *7)),
new StarTranslator(
maxMass - (massStep * 8),
maxRad - (radStep *8),
maxTemp - (tempStep *8)),
new StarTranslator(
maxMass - (massStep * 9),
maxRad - (radStep *9),
maxTemp - (tempStep *9)),
};
return range;
}
#endregion}

There are a few things to note in this class. First, Orbits can be null. If so, then the OrbitsId will be the Empty Guid until it is no longer null. Second, some constructors take strings for Mass, Length, and Temperature parameters. These constructors exist because at least sometimes the data will be input via text files where hiding the need to parse inside the class is cleaner than making the caller know how to parse the data before constructing. Third I have additional static helper methods on this class, not just Empty but something called SecondLevelEmpty. Well, that is because of recursion and wanting to avoid both recursion and null references where possible. So while the Empty object needs a Star object for its Orbits, putting either Star () or Empty there causes recursion. So I added an additional level in the SecondLevelEmpty property. If someone looks for what the object that the empty Star is orbiting is itself orbiting and tries to pull values from it without checking for null, they deserve the exception. I also included the sun (Sol) and our Galactic Center (aka Sagittarius A, or the Super Massive Black Hole at the Center of The Milky Way). There are also two helper methods one for getting the distance to Galactic Center given a starting distance and the second for parsing a Stars Spectral Class and giving back rough Mass, Radius, and Temperature for that Star.

在本课程中,有几件事需要注意。 首先,轨道可以为空。 如果是这样,那么OrbitsId将一直是Empty Guid,直到不再为空。 其次,一些构造函数使用质量,长度和温度参数的字符串。 这些构造函数之所以存在,是因为至少有时会通过文本文件输入数据,而在文本文件中隐藏需要在类内部进行解析的内容比使调用者知道在构造之前如何解析数据要干净得多。 第三,我在此类上有其他静态帮助器方法,不仅是Empty,还包括SecondLevelEmpty。 好吧,这是因为递归,并且想要尽可能避免递归和空引用。 因此,尽管Empty对象的轨道需要一个Star对象,但将Star()或Empty放置在那里都会导致递归。 因此,我在SecondLevelEmpty属性中添加了一个附加级别。 如果有人寻找空星正在运行的物体本身正在运行,并试图从中获取值而不检查是否为空,那么他们应该例外。 我还包括了太阳(Sol)和我们的银河系中心(又名射手座A,或银河系中心的超大规模黑洞)。 还有两种辅助方法,一种是在给定起始距离的情况下获得到银河系中心的距离,第二种方法是解析“恒星光谱”类并返回该恒星的粗略质量,半径和温度。

There are additional refactored classes to cover the StarSystem, StarChart, Collections of planets, stars, star systems, and star charts, plus the Context class to handle the database interface for the refactored classes. But, this article is already about three times the length I had planned on it being, so I am going to wrap things up.

还有其他重构类,它们涵盖了StarSystem,StarChart,行星集合,恒星,恒星系统和星图,以及Context类来处理重构类的数据库接口。 但是,这篇文章的长度已经是我计划的长度的三倍,所以我将总结一下。

结论 (Conclusion)

Refactoring is a valuable tool. However, it grows nearly without bounds if the engineers are left with an open-ended mandate to “fix the code.” If you don’t have concrete goals and requirements going into the refactoring project, it can turn into a project that will eat hours and budget like nothing else you have ever seen. On the other hand, well planned and designed refactoring is the best thing that you can do for your codebase. Carefully timed and focused refactoring can keep an old application running long after the replacement of most of its peers with buggier new versions. It can lower technical debt and maintenance costs for ongoing projects and fix bugs that have irritated users but were never a high priority.

重构是一种有价值的工具。 但是,如果工程师有一个开放性的任务来“修复代码”,那么它几乎可以无限地增长。 如果您对重构项目没有具体的目标和要求,那么它可能会变成一个将花费大量时间和预算的项目,这是您从未见过的。 另一方面,精心设计和设计的重构是您可以为代码库做的最好的事情。 精心安排时间和重点的重构可以使旧的应用程序在用错误的新版本替换大多数同级后保持运行很长时间。 它可以降低正在进行的项目的技术债务和维护成本,并修复那些使用户烦恼但从来都不是最重要的错误。

That is all fine for the team leads and management types, but what about the engineers? Well, I have to tell you there are a few perks for us too. First, getting to repair and replace some of the code that is the worst to debug and maintain, during a refactor, that should be the first to go. If there is a module that has needed to be patched every month for the last three years, rewrite it. Just the time savings from engineering and support calls alone would probably convince your managers to let you refactor that mess. I know I have used this argument several times successfully and saved companies hundreds of thousands of dollars with the refactor even after considering the project costs. Also, there is the joy or cringe of replacing your old code. There is nothing quite like finding code you wrote years ago in a block you need to refactor. Whether it’s to wonder that the code is working great and you can leave it alone, or horror at the mess you wrote years ago that now needs to be fixed. It is always an interesting experience. A few times, I returned to a company where I worked in the past, been assigned to fix a bug or refactor a module, and found code with my name on it. Over half the time, I was horrified that I ever wrote the mess that I had to deal with, but sometimes I was happy to discover that the problem wasn’t with what I had written. Either way, it’s an experience.

对于团队负责人和管理类型来说都很好,但是工程师呢? 好吧,我必须告诉您,我们也有一些好处。 首先,在重构期间,要修复和替换一些调试和维护最差的代码,应该首先去修复和替换。 如果在过去三年中每个月需要修补一个模块,请重写它。 仅从工程和支持电话中节省的时间就可以说服您的经理让您重构这些麻烦。 我知道我已经多次成功地使用了这种论点,即使考虑到项目成本,重构也为公司节省了数十万美元。 另外,替换旧代码也很有趣或很胆小。 没有什么比找到您几年前编写的代码中需要重构的代码更像了。 无论是想知道代码是否运行良好,您可以不理会它,还是对您几年前写的烂摊子感到恐惧,现在需要修复。 这总是很有趣的经历。 几次,我回到了我以前工作过的公司,被分配来修复错误或重构模块,并找到上面有我的名字的代码。 在超过一半的时间里,我曾经写过我必须处理的烂摊子而感到震惊,但是有时我很高兴发现问题不在于我写的东西。 无论哪种方式,这都是一种体验。

Good luck, and have fun.

祝好运并玩得开心点。

翻译自: https://medium.com/the-innovation/refactoring-code-59796b32f7

代码重构

你可能感兴趣的:(java,python)