Khi sử dụng System.Timers.Timer trong ứng dụng .NET C# của bạn, bạn có thể gặp sự cố khi trừu tượng hóa nó và có thể bao gồm các mô-đun của mình bằng Bài kiểm tra đơn vị.
Trong bài viết này, chúng ta sẽ thảo luận về Các phương pháp hay nhất về cách chinh phục những thách thức này và cuối cùng, bạn sẽ có thể đạt được mức độ phù hợp 100% cho các mô-đun của mình.
Đây là cách chúng ta sẽ tiếp cận giải pháp của mình:
Hãy đến với một ví dụ rất đơn giản để làm việc trên.
Bắt đầu với giải pháp xấu đơn giản.
Tiếp tục cố gắng nâng cao nó cho đến khi chúng tôi đạt được định dạng cuối cùng.
Tổng kết những bài học kinh nghiệm qua hành trình của chúng tôi.
Trong ví dụ của chúng tôi, chúng tôi sẽ xây dựng một Ứng dụng bảng điều khiển đơn giản chỉ thực hiện một việc đơn giản: sử dụng System.Timers.Timer
để ghi vào bảng điều khiển ngày và giờ mỗi giây .
Cuối cùng, bạn nên kết thúc với điều này:
Như bạn có thể thấy, nó đơn giản về mặt yêu cầu, không có gì lạ mắt.
Một số phương pháp hay nhất sẽ bị bỏ qua/bỏ qua để hướng trọng tâm chính đến các phương pháp hay nhất khác được nhắm mục tiêu trong bài viết này.
Trong bài viết này, chúng tôi sẽ tập trung vào việc trình bày mô-đun bằng cách sử dụng System.Timers.Timer với các bài kiểm tra đơn vị. Tuy nhiên, phần còn lại của giải pháp sẽ không nằm trong các bài kiểm tra đơn vị. Nếu bạn muốn biết thêm về điều này, bạn có thể kiểm tra bài viết
Có một số thư viện của bên thứ ba có thể được sử dụng để đạt được kết quả gần như tương tự. Tuy nhiên, bất cứ khi nào có thể, tôi thà làm theo một thiết kế đơn giản gốc hơn là phụ thuộc vào toàn bộ thư viện lớn của bên thứ ba.
Trong giải pháp này, chúng tôi sẽ trực tiếp sử dụng System.Timers.Timer mà không cung cấp lớp trừu tượng.
Cấu trúc của giải pháp sẽ trông như thế này:
Đó là một giải pháp Sử dụng Timer chỉ với một dự án Console TimerApp .
Tôi đã cố ý đầu tư thời gian và công sức vào việc trừu tượng hóa System.Console
thành IConsole
để chứng minh rằng điều này sẽ không giải quyết được vấn đề của chúng tôi với Bộ hẹn giờ.
namespace TimerApp.Abstractions { public interface IConsole { void WriteLine(object? value); } }
Chúng tôi chỉ cần sử dụng System.Console.WriteLine
trong ví dụ của mình; đó là lý do tại sao đây là phương pháp trừu tượng duy nhất.
namespace TimerApp.Abstractions { public interface IPublisher { void StartPublishing(); void StopPublishing(); } }
Chúng tôi chỉ có hai phương thức trên giao diện IPublisher
: StartPublishing
và StopPublishing
.
Bây giờ, để thực hiện:
using TimerApp.Abstractions; namespace TimerApp.Implementations { public class Console : IConsole { public void WriteLine(object? value) { System.Console.WriteLine(value); } } }
Console
chỉ là một trình bao bọc mỏng cho System.Console
.
using System.Timers; using TimerApp.Abstractions; namespace TimerApp.Implementations { public class Publisher : IPublisher { private readonly Timer m_Timer; private readonly IConsole m_Console; public Publisher(IConsole console) { m_Timer = new Timer(); m_Timer.Enabled = true; m_Timer.Interval = 1000; m_Timer.Elapsed += Handler; m_Console = console; } public void StartPublishing() { m_Timer.Start(); } public void StopPublishing() { m_Timer.Stop(); } private void Handler(object sender, ElapsedEventArgs args) { m_Console.WriteLine(args.SignalTime); } } }
Publisher
là một triển khai đơn giản của IPublisher
. Nó đang sử dụng System.Timers.Timer
và chỉ cần cấu hình nó.
Nó có IConsole
được định nghĩa là một phần phụ thuộc. Đây không phải là một thực hành tốt nhất theo quan điểm của tôi. Nếu bạn muốn hiểu ý tôi, bạn có thể kiểm tra bài viết
Tuy nhiên, chỉ vì mục đích đơn giản, chúng tôi sẽ chỉ thêm nó như một phần phụ thuộc vào hàm tạo.
Chúng tôi cũng đang đặt khoảng thời gian của Bộ hẹn giờ thành 1000 mili giây (1 giây) và đặt trình xử lý để ghi SignalTime
của bộ hẹn giờ vào Bảng điều khiển.
using TimerApp.Abstractions; using TimerApp.Implementations; namespace TimerApp { public class Program { static void Main(string[] args) { IPublisher publisher = new Publisher(new Implementations.Console()); publisher.StartPublishing(); System.Console.ReadLine(); publisher.StopPublishing(); } } }
Ở đây, trong lớp Program
, chúng tôi không làm gì nhiều. Chúng ta chỉ đang tạo một thể hiện của lớp Publisher
và bắt đầu xuất bản.
Chạy này sẽ kết thúc với một cái gì đó như thế này:
Bây giờ, câu hỏi là, nếu bạn định viết một bài kiểm tra đơn vị cho lớp Publisher
, bạn có thể làm gì?
Thật không may, câu trả lời sẽ là: không quá nhiều .
Đầu tiên, bạn không tự tiêm Timer như một phần phụ thuộc. Điều này có nghĩa là bạn đang ẩn phần phụ thuộc bên trong lớp Publisher
. Do đó, chúng tôi không thể chế nhạo hoặc khai thác Bộ hẹn giờ.
Thứ hai, giả sử rằng chúng ta đã sửa đổi mã để Bộ hẹn giờ hiện được đưa vào hàm tạo; Tuy nhiên, câu hỏi sẽ là, làm thế nào để viết một bài kiểm tra đơn vị và thay thế Bộ hẹn giờ bằng một bản giả hoặc sơ khai?
Tôi nghe thấy ai đó hét lên, "hãy gói Bộ hẹn giờ thành một khái niệm trừu tượng và đưa nó vào thay vì Bộ hẹn giờ."
Vâng, đúng vậy, tuy nhiên, nó không đơn giản như vậy. Có một số thủ thuật mà tôi sẽ giải thích trong phần tiếp theo.
Đây là thời gian cho một giải pháp tốt. Hãy xem những gì chúng ta có thể làm về nó.
Cấu trúc của giải pháp sẽ trông như thế này:
Đó là cùng một giải pháp Sử dụng Timer với dự án Console BetterTimerApp mới.
IConsole
, IPublisher
và Console
sẽ giống nhau.
using System; namespace BetterTimerApp.Abstractions { public delegate void TimerIntervalElapsedEventHandler(object sender, DateTime dateTime); public interface ITimer : IDisposable { event TimerIntervalElapsedEventHandler TimerIntervalElapsed; bool Enabled { get; set; } double Interval { get; set; } void Start(); void Stop(); } }
Những gì chúng ta có thể nhận thấy ở đây:
Chúng tôi đã xác định đại biểu mới TimerIntervalElapsedEventHandler
. Đại biểu này đại diện cho sự kiện được đưa ra bởi ITimer
của chúng tôi.
Bạn có thể lập luận rằng chúng tôi không cần đại biểu mới này vì chúng tôi đã có ElapsedEventHandler
gốc đã được sử dụng bởi System.Timers.Timer
.
Đúng vậy đây là sự thật. Tuy nhiên, bạn sẽ nhận thấy rằng sự kiện ElapsedEventHandler
đang cung cấp ElapsedEventArgs
làm đối số sự kiện. ElapsedEventArgs
này có hàm tạo riêng và bạn sẽ không thể tạo phiên bản của riêng mình. Ngoài ra, thuộc tính SignalTime
được xác định trong lớp ElapsedEventArgs
là chỉ đọc. Do đó, bạn sẽ không thể ghi đè nó trong một lớp con.
Có một vé yêu cầu thay đổi được mở để Microsoft cập nhật lớp này, nhưng cho đến thời điểm viết bài này, không có thay đổi nào được áp dụng.
Ngoài ra, lưu ý rằng ITimer
mở rộng IDisposable
.
using System; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Implementations { public class Publisher : IPublisher { private readonly ITimer m_Timer; private readonly IConsole m_Console; public Publisher(ITimer timer, IConsole console) { m_Timer = timer; m_Timer.Enabled = true; m_Timer.Interval = 1000; m_Timer.TimerIntervalElapsed += Handler; m_Console = console; } public void StartPublishing() { m_Timer.Start(); } public void StopPublishing() { m_Timer.Stop(); } private void Handler(object sender, DateTime dateTime) { m_Console.WriteLine(dateTime); } } }
Nó gần giống như Publisher
cũ ngoại trừ những thay đổi nhỏ. Bây giờ, chúng ta có ITimer
được định nghĩa là một phần phụ thuộc được đưa vào thông qua hàm tạo. Phần còn lại của mã sẽ giống nhau.
using System; using System.Collections.Generic; using System.Linq; using System.Timers; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Implementations { public class Timer : ITimer { private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new(); private bool m_IsDisposed; private System.Timers.Timer m_Timer; public Timer() { m_Timer = new System.Timers.Timer(); } public event TimerIntervalElapsedEventHandler TimerIntervalElapsed { add { var internalHandler = (ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); }); if (!m_Handlers.ContainsKey(value)) { m_Handlers.Add(value, new List<ElapsedEventHandler>()); } m_Handlers[value].Add(internalHandler); m_Timer.Elapsed += internalHandler; } remove { m_Timer.Elapsed -= m_Handlers[value].Last(); m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1); if (!m_Handlers[value].Any()) { m_Handlers.Remove(value); } } } public bool Enabled { get => m_Timer.Enabled; set => m_Timer.Enabled = value; } public double Interval { get => m_Timer.Interval; set => m_Timer.Interval = value; } public void Start() { m_Timer.Start(); } public void Stop() { m_Timer.Stop(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Handlers.Any()) { foreach (var internalHandlers in m_Handlers.Values) { if (internalHandlers?.Any() ?? false) { internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler); } } m_Timer.Dispose(); m_Timer = null; m_Handlers.Clear(); m_Handlers = null; } m_IsDisposed = true; } ~Timer() { Dispose(false); } } }
Đây là nơi gần như tất cả các phép thuật xảy ra.
Những gì chúng ta có thể nhận thấy ở đây:
Trong nội bộ, chúng tôi đang sử dụng System.Timers.Timer
.
Chúng tôi đã áp dụng mẫu thiết kế IDisposable . Đó là lý do tại sao bạn có thể thấy private bool m_IsDisposed
, public void Dispose()
, protected virtual void Dispose(bool disposing)
và ~Timer()
.
Trong hàm tạo, chúng tôi đang khởi tạo một thể hiện mới của System.Timers.Timer
. Chúng tôi sẽ gọi đây là Bộ hẹn giờ bên trong trong các bước còn lại.
Đối với public bool Enabled
, public double Interval
, public void Start()
và public void Stop()
, chúng ta chỉ ủy quyền triển khai cho Internal Timer.
Đối với public event TimerIntervalElapsedEventHandler TimerIntervalElapsed
, đây là phần quan trọng nhất; vì vậy hãy phân tích nó từng bước một.
Điều chúng ta cần làm với sự kiện này là xử lý khi ai đó đăng ký/hủy đăng ký từ bên ngoài. Trong trường hợp này, chúng tôi muốn phản chiếu điều này với Bộ hẹn giờ bên trong.
Nói cách khác, nếu ai đó từ bên ngoài có phiên bản ITimer
của chúng tôi, anh ta sẽ có thể làm điều gì đó như thế này t.TimerIntervalElapsed += (sender, dateTime) => { //do something }
.
Tại thời điểm này, điều chúng ta nên làm là thực hiện nội bộ một số thứ như m_Timer.Elapsed += (sender, elapsedEventArgs) => { //do something }
.
Tuy nhiên, chúng ta cần lưu ý rằng hai trình xử lý không giống nhau vì chúng thực sự thuộc các loại khác nhau; TimerIntervalElapsedEventHandler
và ElapsedEventHandler
.
Do đó, những gì chúng ta cần làm là gói phần sắp tới trong TimerIntervalElapsedEventHandler
vào một ElapsedEventHandler
bên trong mới. Đây là điều chúng ta có thể làm.
Tuy nhiên, chúng ta cũng cần lưu ý rằng, tại một thời điểm nào đó, ai đó có thể cần hủy đăng ký trình xử lý khỏi sự kiện TimerIntervalElapsedEventHandler
.
Điều này có nghĩa là, tại thời điểm này, chúng tôi cần biết trình xử lý ElapsedEventHandler
nào tương ứng với trình xử lý TimerIntervalElapsedEventHandler
đó để chúng tôi có thể hủy đăng ký nó khỏi Bộ hẹn giờ nội bộ.
Cách duy nhất để đạt được điều này là theo dõi từng trình xử lý TimerIntervalElapsedEventHandler
và trình xử lý ElapsedEventHandler
mới được tạo trong từ điển. Bằng cách này, bằng cách biết trình xử lý được truyền trong TimerIntervalElapsedEventHandler
, chúng ta có thể biết trình xử lý ElapsedEventHandler
tương ứng.
Tuy nhiên, chúng ta cũng cần lưu ý rằng từ bên ngoài, ai đó có thể đăng ký cùng một trình xử lý TimerIntervalElapsedEventHandler
nhiều lần.
Vâng, điều này không hợp lý, nhưng vẫn có thể thực hiện được. Do đó, để hoàn thiện, đối với mỗi trình xử lý TimerIntervalElapsedEventHandler
, chúng tôi sẽ giữ một danh sách các trình xử lý ElapsedEventHandler
.
Trong hầu hết các trường hợp, danh sách này sẽ chỉ có một mục nhập trừ trường hợp đăng ký trùng lặp.
Và đó là lý do tại sao bạn có thể thấy private Dictionary<TimerIntervalElapsedEventHandler, List<ElapsedEventHandler>> m_Handlers = new();
.
public event TimerIntervalElapsedEventHandler TimerIntervalElapsed { add { var internalHandler = (ElapsedEventHandler)((sender, args) => { value.Invoke(sender, args.SignalTime); }); if (!m_Handlers.ContainsKey(value)) { m_Handlers.Add(value, new List<ElapsedEventHandler>()); } m_Handlers[value].Add(internalHandler); m_Timer.Elapsed += internalHandler; } remove { m_Timer.Elapsed -= m_Handlers[value].Last(); m_Handlers[value].RemoveAt(m_Handlers[value].Count - 1); if (!m_Handlers[value].Any()) { m_Handlers.Remove(value); } } }
Trong add
, chúng ta đang tạo một ElapsedEventHandler
mới, thêm một bản ghi trong từ điển m_Handlers
ánh xạ bản ghi này tới TimerIntervalElapsedEventHandler
và cuối cùng đăng ký Internal Timer.
Trong remove
, chúng tôi sẽ nhận được danh sách tương ứng của trình xử lý ElapsedEventHandler
, chọn trình xử lý cuối cùng, hủy đăng ký nó khỏi Bộ hẹn giờ nội bộ, xóa nó khỏi danh sách và xóa toàn bộ mục nhập nếu danh sách trống.
Cũng đáng nói, việc thực hiện Dispose
.
protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Handlers.Any()) { foreach (var internalHandlers in m_Handlers.Values) { if (internalHandlers?.Any() ?? false) { internalHandlers.ForEach(handler => m_Timer.Elapsed -= handler); } } m_Timer.Dispose(); m_Timer = null; m_Handlers.Clear(); m_Handlers = null; } m_IsDisposed = true; }
Chúng tôi đang hủy đăng ký tất cả các trình xử lý còn lại khỏi Bộ hẹn giờ bên trong, loại bỏ Bộ hẹn giờ bên trong và xóa từ điển m_Handlers
.
using BetterTimerApp.Abstractions; using BetterTimerApp.Implementations; namespace BetterTimerApp { public class Program { static void Main(string[] args) { var timer = new Timer(); IPublisher publisher = new Publisher(timer, new Implementations.Console()); publisher.StartPublishing(); System.Console.ReadLine(); publisher.StopPublishing(); timer.Dispose(); } } }
Ở đây, chúng tôi vẫn chưa làm được gì nhiều. Nó gần giống như giải pháp cũ.
Chạy này sẽ kết thúc với một cái gì đó như thế này:
Bây giờ, chúng tôi có thiết kế cuối cùng của chúng tôi. Tuy nhiên, chúng tôi cần xem liệu thiết kế này có thực sự có thể giúp chúng tôi bao quát mô-đun Publisher
của mình bằng các bài kiểm tra đơn vị hay không.
Cấu trúc của giải pháp sẽ trông như thế này:
Tôi đang sử dụng NUnit và Moq để thử nghiệm. Bạn chắc chắn có thể làm việc với các thư viện ưa thích của mình.
using System; using System.Collections.Generic; using BetterTimerApp.Abstractions; namespace BetterTimerApp.Tests.Stubs { public enum Action { Start = 1, Stop = 2, Triggered = 3, Enabled = 4, Disabled = 5, IntervalSet = 6 } public class ActionLog { public Action Action { get; } public string Message { get; } public ActionLog(Action action, string message) { Action = action; Message = message; } } public class TimerStub : ITimer { private bool m_Enabled; private double m_Interval; public event TimerIntervalElapsedEventHandler TimerIntervalElapsed; public Dictionary<int, ActionLog> Log = new(); public bool Enabled { get => m_Enabled; set { m_Enabled = value; Log.Add(Log.Count + 1, new ActionLog(value ? Action.Enabled : Action.Disabled, value ? "Enabled" : "Disabled")); } } public double Interval { get => m_Interval; set { m_Interval = value; Log.Add(Log.Count + 1, new ActionLog(Action.IntervalSet, m_Interval.ToString("G17"))); } } public void Start() { Log.Add(Log.Count + 1, new ActionLog(Action.Start, "Started")); } public void Stop() { Log.Add(Log.Count + 1, new ActionLog(Action.Stop, "Stopped")); } public void TriggerTimerIntervalElapsed(DateTime dateTime) { OnTimerIntervalElapsed(dateTime); Log.Add(Log.Count + 1, new ActionLog(Action.Triggered, "Triggered")); } protected void OnTimerIntervalElapsed(DateTime dateTime) { TimerIntervalElapsed?.Invoke(this, dateTime); } public void Dispose() { Log.Clear(); Log = null; } } }
Những gì chúng ta có thể nhận thấy ở đây:
Chúng tôi đã xác định Action
enum sẽ được sử dụng trong khi ghi nhật ký các hành động được thực hiện thông qua sơ khai Bộ hẹn giờ của chúng tôi. Điều này sẽ được sử dụng sau này để xác nhận các hành động nội bộ được thực hiện.
Ngoài ra, chúng tôi đã định nghĩa lớp ActionLog
sẽ được sử dụng để ghi nhật ký.
Chúng tôi đã định nghĩa lớp TimerStub
là sơ khai của ITimer
. Chúng tôi sẽ sử dụng sơ khai này sau khi kiểm tra mô-đun Publisher
.
Việc thực hiện rất đơn giản. Điều đáng nói là chúng tôi đã thêm một phương thức public void TriggerTimerIntervalElapsed(DateTime dateTime)
để chúng tôi có thể kích hoạt sơ khai theo cách thủ công trong một bài kiểm tra đơn vị.
Chúng tôi cũng có thể chuyển vào giá trị dự kiến của dateTime
để chúng tôi có một giá trị đã biết để xác nhận.
using System; using BetterTimerApp.Abstractions; using BetterTimerApp.Implementations; using BetterTimerApp.Tests.Stubs; using Moq; using NUnit.Framework; using Action = BetterTimerApp.Tests.Stubs.Action; namespace BetterTimerApp.Tests.Tests { [TestFixture] public class PublisherTests { private TimerStub m_TimerStub; private Mock<IConsole> m_ConsoleMock; private Publisher m_Sut; [SetUp] public void SetUp() { m_TimerStub = new TimerStub(); m_ConsoleMock = new Mock<IConsole>(); m_Sut = new Publisher(m_TimerStub, m_ConsoleMock.Object); } [TearDown] public void TearDown() { m_Sut = null; m_ConsoleMock = null; m_TimerStub = null; } [Test] public void ConstructorTest() { Assert.AreEqual(Action.Enabled, m_TimerStub.Log[1].Action); Assert.AreEqual(Action.Enabled.ToString(), m_TimerStub.Log[1].Message); Assert.AreEqual(Action.IntervalSet, m_TimerStub.Log[2].Action); Assert.AreEqual(1000.ToString("G17"), m_TimerStub.Log[2].Message); } [Test] public void StartPublishingTest() { // Arrange var expectedDateTime = DateTime.Now; m_ConsoleMock .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime) ) ) .Verifiable(); // Act m_Sut.StartPublishing(); m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime); // Assert ConstructorTest(); m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime) ); Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action); Assert.AreEqual("Started", m_TimerStub.Log[3].Message); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message); } [Test] public void StopPublishingTest() { // Act m_Sut.StopPublishing(); // Assert ConstructorTest(); Assert.AreEqual(Action.Stop, m_TimerStub.Log[3].Action); Assert.AreEqual("Stopped", m_TimerStub.Log[3].Message); } [Test] public void FullProcessTest() { // Arrange var expectedDateTime1 = DateTime.Now; var expectedDateTime2 = expectedDateTime1 + TimeSpan.FromSeconds(1); var expectedDateTime3 = expectedDateTime2 + TimeSpan.FromSeconds(1); var sequence = new MockSequence(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime1) ) ) .Verifiable(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime2) ) ) .Verifiable(); m_ConsoleMock .InSequence(sequence) .Setup ( m => m.WriteLine ( It.Is<DateTime>(p => p == expectedDateTime3) ) ) .Verifiable(); // Act m_Sut.StartPublishing(); m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime1); // Assert ConstructorTest(); m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime1) ); Assert.AreEqual(Action.Start, m_TimerStub.Log[3].Action); Assert.AreEqual("Started", m_TimerStub.Log[3].Message); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[4].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[4].Message); // Act m_TimerStub.TriggerTimerIntervalElapsed(expectedDateTime2); // Assert m_ConsoleMock .Verify ( m => m.WriteLine(expectedDateTime2) ); Assert.AreEqual(Action.Triggered, m_TimerStub.Log[5].Action); Assert.AreEqual(Action.Triggered.ToString(), m_TimerStub.Log[5].Message); // Act m_Sut.StopPublishing(); // Assert Assert.AreEqual(Action.Stop, m_TimerStub.Log[6].Action); Assert.AreEqual("Stopped", m_TimerStub.Log[6].Message); } } }
Bây giờ, như bạn có thể thấy, chúng tôi có toàn quyền kiểm soát và chúng tôi có thể dễ dàng bao quát mô-đun Publisher
của mình bằng các bài kiểm tra đơn vị.
Nếu chúng tôi tính toán phạm vi bảo hiểm, chúng tôi sẽ nhận được điều này:
Như bạn có thể thấy, mô-đun Publisher
được bao phủ 100%. Đối với phần còn lại, điều này nằm ngoài phạm vi của bài viết này, nhưng bạn có thể chỉ cần giải quyết nó nếu bạn làm theo cách tiếp cận trong bài viết
Bạn có thể làm được. Nó chỉ là vấn đề chia nhỏ các mô-đun lớn thành các mô-đun nhỏ hơn, xác định các phần trừu tượng của bạn, sáng tạo với các phần phức tạp, và sau đó bạn đã hoàn thành.
Nếu bạn muốn rèn luyện bản thân nhiều hơn, bạn có thể xem các bài viết khác của tôi về một số Phương pháp hay nhất.
Vậy đó, hy vọng bạn thấy thú vị khi đọc bài viết này như tôi thấy khi viết nó.
Cũng được xuất bản ở đây